diff --git a/include/core/events.h b/include/core/events.h index 265ec541..73e61141 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -41,14 +41,6 @@ public: virtual void visitSign(SignEvent *) = 0; }; -struct EventGraphics -{ - QImage spritesheet; - int spriteWidth; - int spriteHeight; - bool inanimate; -}; - /// /// Event base class -- purely virtual @@ -64,11 +56,7 @@ public: Event& operator=(const Event &other) = delete; protected: - Event() { - this->spriteWidth = 16; - this->spriteHeight = 16; - this->usingSprite = false; - } + Event() {} // public enums & static methods public: @@ -143,8 +131,8 @@ public: int getZ() const { return this->elevation; } int getElevation() const { return this->elevation; } - int getPixelX() const { return (this->x * 16) - qMax(0, (this->spriteWidth - 16) / 2); } - int getPixelY() const { return (this->y * 16) - qMax(0, this->spriteHeight - 16); } + int getPixelX() const { return (this->x * 16) - qMax(0, (pixmap.width() - 16) / 2); } + int getPixelY() const { return (this->y * 16) - qMax(0, pixmap.height() - 16); } virtual EventFrame *getEventFrame(); virtual EventFrame *createEventFrame() = 0; @@ -172,14 +160,8 @@ public: void setPixmapItem(DraggablePixmapItem *item); DraggablePixmapItem *getPixmapItem() const { return this->pixmapItem; } - void setUsingSprite(bool newUsingSprite) { this->usingSprite = newUsingSprite; } - bool getUsingSprite() const { return this->usingSprite; } - - void setSpriteWidth(int newSpriteWidth) { this->spriteWidth = newSpriteWidth; } - int getspriteWidth() const { return this->spriteWidth; } - - void setSpriteHeight(int newSpriteHeight) { this->spriteHeight = newSpriteHeight; } - int getspriteHeight() const { return this->spriteHeight; } + void setUsesDefaultPixmap(bool newUsesDefaultPixmap) { this->usesDefaultPixmap = newUsesDefaultPixmap; } + bool getUsesDefaultPixmap() const { return this->usesDefaultPixmap; } int getEventIndex(); @@ -204,9 +186,7 @@ protected: int y = 0; int elevation = 0; - int spriteWidth = 16; - int spriteHeight = 16; - bool usingSprite = false; + bool usesDefaultPixmap = true; // Some events can have an associated #define name that should be unique to this event. // e.g. object events can have a 'LOCALID', or Heal Locations have a 'HEAL_LOCATION' id. @@ -273,10 +253,6 @@ public: void setFlag(QString newFlag) { this->flag = newFlag; } QString getFlag() const { return this->flag; } -public: - void setFrameFromMovement(QString movement); - void setPixmapFromSpritesheet(EventGraphics * gfx); - protected: QString gfx; diff --git a/include/project.h b/include/project.h index c2707063..38a31f4a 100644 --- a/include/project.h +++ b/include/project.h @@ -45,7 +45,6 @@ public: QStringList layoutIdsMaster; QMap mapLayouts; QMap mapLayoutsMaster; - QMap eventGraphicsMap; QMap gfxDefines; QString defaultSong; QStringList songNames; @@ -68,7 +67,6 @@ public: QMap unusedMetatileLabels; QMap metatileBehaviorMap; QMap metatileBehaviorMapInverse; - QMap facingDirections; ParseUtil parser; QFileSystemWatcher fileWatcher; QSet modifiedFiles; @@ -210,7 +208,10 @@ public: bool readFieldmapMasks(); QMap> readObjEventGfxInfo(); - void setEventPixmap(Event *event, bool forceLoad = false); + QPixmap getEventPixmap(const QString &gfxName, const QString &movementName); + QPixmap getEventPixmap(const QString &gfxName, int frame, bool hFlip); + QPixmap getEventPixmap(Event::Group group); + void loadEventPixmap(Event *event, bool forceLoad = false); QString fixPalettePath(QString path); QString fixGraphicPath(QString path); @@ -254,6 +255,18 @@ public: private: QMap mapSectionDisplayNames; QMap modifiedFileTimestamps; + QMap facingDirections; + + struct EventGraphics + { + QString filepath; + bool loaded = false; + QImage spritesheet; + int spriteWidth = -1; + int spriteHeight = -1; + bool inanimate = false; + }; + QMap eventGraphicsMap; void updateLayout(Layout *); diff --git a/src/core/events.cpp b/src/core/events.cpp index 3d2ef9d2..37e5ec0a 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -107,9 +107,10 @@ Event::Type Event::typeFromString(QString type) { return typeToStringMap.key(type, Event::Type::None); } -void Event::loadPixmap(Project *) { +void Event::loadPixmap(Project *project) { const QPixmap * pixmap = Event::icons.value(this->getEventGroup()); this->pixmap = pixmap ? *pixmap : QPixmap(); + this->usesDefaultPixmap = true; } void Event::clearIcons() { @@ -230,7 +231,6 @@ void ObjectEvent::setDefaultValues(Project *project) { this->setRadiusX(0); this->setRadiusY(0); this->setSightRadiusBerryTreeID("0"); - this->setFrameFromMovement(project->facingDirections.value(this->getMovement())); } const QSet expectedObjectFields = { @@ -257,78 +257,11 @@ QSet ObjectEvent::getExpectedFields() { } void ObjectEvent::loadPixmap(Project *project) { - EventGraphics *eventGfx = project->eventGraphicsMap.value(this->gfx, nullptr); - if (!eventGfx) { - // Invalid gfx constant. - // If this is a number, try to use that instead. - bool ok; - int altGfx = ParseUtil::gameStringToInt(this->gfx, &ok); - if (ok && (altGfx < project->gfxDefines.count())) { - eventGfx = project->eventGraphicsMap.value(project->gfxDefines.key(altGfx, "NULL"), nullptr); - } - } - if (!eventGfx || eventGfx->spritesheet.isNull()) { - // No sprite associated with this gfx constant. - // Use default sprite instead. + this->pixmap = project->getEventPixmap(this->gfx, this->movement); + if (!this->pixmap.isNull()) { + this->usesDefaultPixmap = false; + } else { Event::loadPixmap(project); - this->spriteWidth = 16; - this->spriteHeight = 16; - this->usingSprite = false; - } else { - this->setFrameFromMovement(project->facingDirections.value(this->movement)); - this->setPixmapFromSpritesheet(eventGfx); - } -} - -void ObjectEvent::setPixmapFromSpritesheet(EventGraphics * gfx) -{ - QImage img; - if (gfx->inanimate) { - img = gfx->spritesheet.copy(0, 0, gfx->spriteWidth, gfx->spriteHeight); - } else { - int x = 0; - int y = 0; - - // Get frame's position in spritesheet. - // Assume horizontal layout. If position would exceed sheet width, try vertical layout. - if ((this->frame + 1) * gfx->spriteWidth <= gfx->spritesheet.width()) { - x = this->frame * gfx->spriteWidth; - } else if ((this->frame + 1) * gfx->spriteHeight <= gfx->spritesheet.height()) { - y = this->frame * gfx->spriteHeight; - } - - img = gfx->spritesheet.copy(x, y, gfx->spriteWidth, gfx->spriteHeight); - - // Right-facing sprite is just the left-facing sprite mirrored - if (this->hFlip) { - img = img.transformed(QTransform().scale(-1, 1)); - } - } - // Set first palette color fully transparent. - img.setColor(0, qRgba(0, 0, 0, 0)); - pixmap = QPixmap::fromImage(img); - this->spriteWidth = gfx->spriteWidth; - this->spriteHeight = gfx->spriteHeight; - this->usingSprite = true; -} - -void ObjectEvent::setFrameFromMovement(QString facingDir) { - // defaults - // TODO: read this from a file somewhere? - this->frame = 0; - this->hFlip = false; - if (facingDir == "DIR_NORTH") { - this->frame = 1; - this->hFlip = false; - } else if (facingDir == "DIR_SOUTH") { - this->frame = 0; - this->hFlip = false; - } else if (facingDir == "DIR_WEST") { - this->frame = 2; - this->hFlip = false; - } else if (facingDir == "DIR_EAST") { - this->frame = 2; - this->hFlip = true; } } @@ -430,19 +363,7 @@ void CloneObjectEvent::loadPixmap(Project *project) { this->gfx = project->gfxDefines.key(0, "0"); this->movement = project->movementTypes.value(0, "0"); } - - EventGraphics *eventGfx = project->eventGraphicsMap.value(gfx, nullptr); - if (!eventGfx || eventGfx->spritesheet.isNull()) { - // No sprite associated with this gfx constant. - // Use default sprite instead. - Event::loadPixmap(project); - this->spriteWidth = 16; - this->spriteHeight = 16; - this->usingSprite = false; - } else { - this->setFrameFromMovement(project->facingDirections.value(this->movement)); - this->setPixmapFromSpritesheet(eventGfx); - } + ObjectEvent::loadPixmap(project); } diff --git a/src/editor.cpp b/src/editor.cpp index ecc056f0..ad99dded 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1670,7 +1670,7 @@ void Editor::displayMapEvents() { } DraggablePixmapItem *Editor::addEventPixmapItem(Event *event) { - this->project->setEventPixmap(event); + this->project->loadEventPixmap(event); auto item = new DraggablePixmapItem(event, this); redrawEventPixmapItem(item); this->events_group->addToGroup(item); @@ -1955,13 +1955,13 @@ qreal Editor::getEventOpacity(const Event *event) const { // - On the Events tab, and the event has a custom sprite (1.0) if (this->editMode != EditMode::Events) return porymapConfig.eventOverlayEnabled ? 0.5 : 0.0; - return event->getUsingSprite() ? 1.0 : 0.7; + return event->getUsesDefaultPixmap() ? 0.7 : 1.0; } void Editor::redrawEventPixmapItem(DraggablePixmapItem *item) { if (item && item->event && !item->event->getPixmap().isNull()) { item->setOpacity(getEventOpacity(item->event)); - project->setEventPixmap(item->event, true); + project->loadEventPixmap(item->event, true); item->setPixmap(item->event->getPixmap()); item->setShapeMode(porymapConfig.eventSelectionShapeMode); diff --git a/src/project.cpp b/src/project.cpp index b25b3776..7f9a1ed5 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -2664,92 +2664,176 @@ QStringList Project::getEventScriptsFilePaths() const { return filePaths; } -void Project::setEventPixmap(Event *event, bool forceLoad) { +void Project::loadEventPixmap(Event *event, bool forceLoad) { if (event && (event->getPixmap().isNull() || forceLoad)) event->loadPixmap(this); } void Project::clearEventGraphics() { - qDeleteAll(eventGraphicsMap); - eventGraphicsMap.clear(); + qDeleteAll(this->eventGraphicsMap); + this->eventGraphicsMap.clear(); } bool Project::readEventGraphics() { clearEventGraphics(); - fileWatcher.addPaths(QStringList() << root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_pointers) - << root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info) - << root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables) - << root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx)); - const QString pointersFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_pointers); - const QString pointersName = projectConfig.getIdentifier(ProjectIdentifier::symbol_obj_event_gfx_pointers); - QMap pointerHash = parser.readNamedIndexCArray(pointersFilepath, pointersName); + const QString gfxInfoFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info); + const QString picTablesFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables); + const QString gfxFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx); + fileWatcher.addPaths({pointersFilepath, gfxInfoFilepath, picTablesFilepath, gfxFilepath}); - QStringList gfxNames = gfxDefines.keys(); + // Read the table mapping OBJ_EVENT_GFX constants to the names of pointers to data about their graphics. + const QString pointersName = projectConfig.getIdentifier(ProjectIdentifier::symbol_obj_event_gfx_pointers); + const QMap pointerMap = parser.readNamedIndexCArray(pointersFilepath, pointersName); // The positions of each of the required members for the gfx info struct. // For backwards compatibility if the struct doesn't use initializers. - static const auto gfxInfoMemberMap = QHash{ + static const QHash gfxInfoMemberMap = { {8, "inanimate"}, {11, "oam"}, {12, "subspriteTables"}, {14, "images"}, }; + // Read the structs containing data about each of the event sprites. + auto gfxInfos = parser.readCStructs(gfxInfoFilepath, "", gfxInfoMemberMap); - QString filepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info); - auto gfxInfos = parser.readCStructs(filepath, "", gfxInfoMemberMap); + // We need data in both of these files to translate data from the structs above into the path for a .png file. + const QMap picTables = parser.readCArrayMulti(picTablesFilepath); + const QMap graphicIncbins = parser.readCIncbinMulti(gfxFilepath); - QMap picTables = parser.readCArrayMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables)); - QMap graphicIncbins = parser.readCIncbinMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx)); + for (auto i = this->gfxDefines.constBegin(); i != this->gfxDefines.constEnd(); i++) { + const QString gfxName = i.key(); - for (QString gfxName : gfxNames) { - QString info_label = pointerHash[gfxName].replace("&", ""); + // Strip the address-of operator to get the pointer's name. We'll use this name to get data about the event's sprite. + // If we don't recognize the name, ignore it. The event will use a default sprite. + QString info_label = pointerMap[gfxName].replace("&", ""); if (!gfxInfos.contains(info_label)) continue; + const QHash gfxInfoAttributes = gfxInfos[info_label]; - auto gfxInfoAttributes = gfxInfos[info_label]; + auto gfx = new EventGraphics; - auto eventGraphics = new EventGraphics; - eventGraphics->inanimate = ParseUtil::gameStringToBool(gfxInfoAttributes.value("inanimate")); - QString pic_label = gfxInfoAttributes.value("images"); - QString dimensions_label = gfxInfoAttributes.value("oam"); - QString subsprites_label = gfxInfoAttributes.value("subspriteTables"); - - QString gfx_label = picTables[pic_label].value(0); + // We need the .png filepath for the event's sprite. This is buried behind a few levels of indirection. + // The 'images' field gives us the name of the table containing the sprite's image data. + // The entries in this table are expected to be in the format (PngSymbolName, ...). + // We extract the symbol name of the .png's INCBIN'd data by looking at the first entry in this table. + // Once we have the .png's symbol name we can get the actual filepath from its INCBIN. + QString gfx_label = picTables[gfxInfoAttributes.value("images")].value(0); static const QRegularExpression re_parens("[\\(\\)]"); gfx_label = gfx_label.section(re_parens, 1, 1); - QString path = graphicIncbins[gfx_label]; + gfx->filepath = fixGraphicPath(graphicIncbins[gfx_label]); - if (!path.isNull()) { - path = fixGraphicPath(path); - eventGraphics->spritesheet = QImage(root + "/" + path); - if (!eventGraphics->spritesheet.isNull()) { - // Infer the sprite dimensions from the OAM labels. - static const QRegularExpression re("\\S+_(\\d+)x(\\d+)"); - QRegularExpressionMatch dimensionMatch = re.match(dimensions_label); - QRegularExpressionMatch oamTablesMatch = re.match(subsprites_label); - if (oamTablesMatch.hasMatch()) { - eventGraphics->spriteWidth = oamTablesMatch.captured(1).toInt(nullptr, 0); - eventGraphics->spriteHeight = oamTablesMatch.captured(2).toInt(nullptr, 0); - } else if (dimensionMatch.hasMatch()) { - eventGraphics->spriteWidth = dimensionMatch.captured(1).toInt(nullptr, 0); - eventGraphics->spriteHeight = dimensionMatch.captured(2).toInt(nullptr, 0); - } else { - eventGraphics->spriteWidth = eventGraphics->spritesheet.width(); - eventGraphics->spriteHeight = eventGraphics->spritesheet.height(); - } - } - } else { - eventGraphics->spritesheet = QImage(); - eventGraphics->spriteWidth = 16; - eventGraphics->spriteHeight = 16; + // Note: gfx has a 'spritesheet' field that will contain a QImage for the event's sprite. + // We don't create this QImage yet. Reading the image now is unnecessary overhead for startup. + // We'll read the image file when the event's sprite is first requested to be drawn. + + // The .png file is expected to be a spritesheet that can have multiple frames. + // We only want to show one frame at a time, so we need to know the dimensions of each frame. + // TODO: Describe different ways we read these. Use width/height? + static const QRegularExpression re("\\S+_(\\d+)x(\\d+)"); + QRegularExpressionMatch dimensionMatch = re.match(gfxInfoAttributes.value("oam")); + QRegularExpressionMatch oamTablesMatch = re.match(gfxInfoAttributes.value("subspriteTables")); + if (oamTablesMatch.hasMatch()) { + gfx->spriteWidth = oamTablesMatch.captured(1).toInt(nullptr, 0); + gfx->spriteHeight = oamTablesMatch.captured(2).toInt(nullptr, 0); + } else if (dimensionMatch.hasMatch()) { + gfx->spriteWidth = dimensionMatch.captured(1).toInt(nullptr, 0); + gfx->spriteHeight = dimensionMatch.captured(2).toInt(nullptr, 0); } - eventGraphicsMap.insert(gfxName, eventGraphics); + + // Inanimate events will only ever use the first frame of their spritesheet. + gfx->inanimate = ParseUtil::gameStringToBool(gfxInfoAttributes.value("inanimate")); + + this->eventGraphicsMap.insert(gfxName, gfx); } return true; } +QPixmap Project::getEventPixmap(const QString &gfxName, const QString &movementName) { + struct FrameData { + int index = 0; + bool hFlip = false; + }; + // TODO: Expose as a setting to users + static const QMap directionToFrameData = { + {"DIR_SOUTH", { .index = 0, .hFlip = false }}, + {"DIR_NORTH", { .index = 1, .hFlip = false }}, + {"DIR_WEST", { .index = 2, .hFlip = false }}, + {"DIR_EAST", { .index = 2, .hFlip = true }}, // East-facing sprite is just the West-facing sprite mirrored + }; + const QString direction = this->facingDirections.value(movementName, "DIR_SOUTH"); + auto frameData = directionToFrameData.value(direction); + return getEventPixmap(gfxName, frameData.index, frameData.hFlip); +} + +QPixmap Project::getEventPixmap(const QString &gfxName, int frame, bool hFlip) { + EventGraphics* gfx = this->eventGraphicsMap.value(gfxName, nullptr); + if (!gfx) { + // Invalid gfx constant. If this is a number, try to use that instead. + bool ok; + int gfxNum = ParseUtil::gameStringToInt(gfxName, &ok); + if (ok && gfxNum < this->gfxDefines.count()) { + gfx = this->eventGraphicsMap.value(this->gfxDefines.key(gfxNum, "NULL"), nullptr); + } + } + if (gfx && !gfx->loaded) { + // This is the first request for this event's sprite. We'll attempt to load it now. + if (!gfx->filepath.isEmpty()) { + gfx->spritesheet = QImage(QString("%1/%2").arg(this->root).arg(gfx->filepath)); + if (gfx->spritesheet.isNull()) { + logWarn(QString("Failed to open '%1' for event's sprite. Event will use a default sprite instead.").arg(gfx->filepath)); + } else { + // If we were unable to find the dimensions of a frame within the spritesheet we'll use the full image dimensions. + if (gfx->spriteWidth <= 0) { + gfx->spriteWidth = gfx->spritesheet.width(); + } + if (gfx->spriteHeight <= 0) { + gfx->spriteHeight = gfx->spritesheet.height(); + } + } + } + // Set this whether we were successful or not, we only need to try to load it once. + gfx->loaded = true; + } + if (!gfx || gfx->spritesheet.isNull()) { + // Either we didn't recognize the gfxName, or we were unable to load the sprite's image. + return QPixmap(); + } + + QImage img; + if (gfx->inanimate) { + img = gfx->spritesheet.copy(0, 0, gfx->spriteWidth, gfx->spriteHeight); + } else { + int x = 0; + int y = 0; + + // Get frame's position in spritesheet. + // Assume horizontal layout. If position would exceed sheet width, try vertical layout. + if ((frame + 1) * gfx->spriteWidth <= gfx->spritesheet.width()) { + x = frame * gfx->spriteWidth; + } else if ((frame + 1) * gfx->spriteHeight <= gfx->spritesheet.height()) { + y = frame * gfx->spriteHeight; + } + + img = gfx->spritesheet.copy(x, y, gfx->spriteWidth, gfx->spriteHeight); + if (hFlip) { + img = img.transformed(QTransform().scale(-1, 1)); + } + } + // Set first palette color fully transparent. + img.setColor(0, qRgba(0, 0, 0, 0)); + QPixmap pixmap = QPixmap::fromImage(img); + + // TODO: Cache? + return pixmap; +} + +QPixmap Project::getEventPixmap(Event::Group) { + // TODO + return QPixmap(); +} + bool Project::readSpeciesIconPaths() { this->speciesToIconPath.clear(); diff --git a/src/ui/draggablepixmapitem.cpp b/src/ui/draggablepixmapitem.cpp index 846cfb7e..f2fdeb91 100644 --- a/src/ui/draggablepixmapitem.cpp +++ b/src/ui/draggablepixmapitem.cpp @@ -27,7 +27,7 @@ void DraggablePixmapItem::emitPositionChanged() { } void DraggablePixmapItem::updatePixmap() { - editor->project->setEventPixmap(event, true); + editor->project->loadEventPixmap(event, true); this->updatePosition(); editor->redrawEventPixmapItem(this); emit spriteChanged(event->getPixmap()); diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 0237795a..4b11ea95 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -517,7 +517,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { || (m_settings.showBGs && group == Event::Group::Bg) || (m_settings.showTriggers && group == Event::Group::Coord) || (m_settings.showHealLocations && group == Event::Group::Heal)) { - m_editor->project->setEventPixmap(event); + m_editor->project->loadEventPixmap(event); eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage()); } }