Only load event sprites when requested

This commit is contained in:
GriffinR
2025-02-20 16:31:08 -05:00
parent a2a8e27504
commit bf5ead848d
7 changed files with 169 additions and 175 deletions

View File

@@ -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;

View File

@@ -45,7 +45,6 @@ public:
QStringList layoutIdsMaster;
QMap<QString, Layout*> mapLayouts;
QMap<QString, Layout*> mapLayoutsMaster;
QMap<QString, EventGraphics*> eventGraphicsMap;
QMap<QString, int> gfxDefines;
QString defaultSong;
QStringList songNames;
@@ -68,7 +67,6 @@ public:
QMap<QString, uint16_t> unusedMetatileLabels;
QMap<QString, uint32_t> metatileBehaviorMap;
QMap<uint32_t, QString> metatileBehaviorMapInverse;
QMap<QString, QString> facingDirections;
ParseUtil parser;
QFileSystemWatcher fileWatcher;
QSet<QString> modifiedFiles;
@@ -210,7 +208,10 @@ public:
bool readFieldmapMasks();
QMap<QString, QMap<QString, QString>> 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<QString, QString> mapSectionDisplayNames;
QMap<QString, qint64> modifiedFileTimestamps;
QMap<QString, QString> facingDirections;
struct EventGraphics
{
QString filepath;
bool loaded = false;
QImage spritesheet;
int spriteWidth = -1;
int spriteHeight = -1;
bool inanimate = false;
};
QMap<QString, EventGraphics*> eventGraphicsMap;
void updateLayout(Layout *);

View File

@@ -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<QString> expectedObjectFields = {
@@ -257,78 +257,11 @@ QSet<QString> 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);
}

View File

@@ -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);

View File

@@ -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<QString, QString> 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<QString, QString> 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<int, QString>{
static const QHash<int, QString> 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<QString, QStringList> picTables = parser.readCArrayMulti(picTablesFilepath);
const QMap<QString, QString> graphicIncbins = parser.readCIncbinMulti(gfxFilepath);
QMap<QString, QStringList> picTables = parser.readCArrayMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables));
QMap<QString, QString> 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<QString, QString> 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<QString, FrameData> 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();

View File

@@ -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());

View File

@@ -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());
}
}