From 55f44a6257f72abb233481aba105e12e76c36017 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 10 Apr 2025 22:37:32 -0400 Subject: [PATCH 1/9] Add project version check via git --- include/config.h | 2 + include/mainwindow.h | 2 +- include/project.h | 1 + src/config.cpp | 3 ++ src/mainwindow.cpp | 58 +++++++++++++++++------ src/project.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 15 deletions(-) diff --git a/include/config.h b/include/config.h index eb9c9ac3..4bfc2bc5 100644 --- a/include/config.h +++ b/include/config.h @@ -336,6 +336,7 @@ public: this->unusedTileCovered = 0x0000; this->unusedTileSplit = 0x0000; this->maxEventsPerGroup = 255; + this->forcedMajorVersion = 0; this->identifiers.clear(); this->readKeys.clear(); } @@ -406,6 +407,7 @@ public: int collisionSheetHeight; QList warpBehaviors; int maxEventsPerGroup; + int forcedMajorVersion; protected: virtual QString getConfigFilepath() override; diff --git a/include/mainwindow.h b/include/mainwindow.h index cdc641c2..b3df2e9b 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -355,7 +355,7 @@ private: void refreshCollisionSelector(); void setLayoutOnlyMode(bool layoutOnly); - bool checkProjectSanity(); + bool isInvalidProject(Project *project); bool loadProjectData(); bool setProjectUI(); void clearProjectUI(); diff --git a/include/project.h b/include/project.h index 9a031c0d..554fb3c7 100644 --- a/include/project.h +++ b/include/project.h @@ -87,6 +87,7 @@ public: void clearHealLocations(); bool sanityCheck(); + int getSupportedMajorVersion(QString *errorOut = nullptr); bool load(); Map* loadMap(const QString &mapName); diff --git a/src/config.cpp b/src/config.cpp index 42a59149..8a17a45c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -835,6 +835,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->warpBehaviors.append(getConfigUint32(key, s)); } else if (key == "max_events_per_group") { this->maxEventsPerGroup = getConfigInteger(key, value, 1, INT_MAX, 255); + } else if (key == "forced_major_version") { + this->forcedMajorVersion = getConfigInteger(key, value); } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } @@ -931,6 +933,7 @@ QMap ProjectConfig::getKeyValueMap() { warpBehaviorStrs.append("0x" + QString("%1").arg(value, 2, 16, QChar('0')).toUpper()); map.insert("warp_behaviors", warpBehaviorStrs.join(",")); map.insert("max_events_per_group", QString::number(this->maxEventsPerGroup)); + map.insert("forced_major_version", QString::number(this->forcedMajorVersion)); return map; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 650acdaf..6a3bdb56 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -668,7 +668,7 @@ bool MainWindow::openProject(QString dir, bool initial) { this->editor->setProject(project); // Make sure project looks reasonable before attempting to load it - if (!checkProjectSanity()) { + if (isInvalidProject(this->editor->project)) { delete this->editor->project; return false; } @@ -708,22 +708,52 @@ bool MainWindow::loadProjectData() { return success; } -bool MainWindow::checkProjectSanity() { - if (editor->project->sanityCheck()) - return true; +bool MainWindow::isInvalidProject(Project *project) { + if (!project->sanityCheck()) { + logWarn(QString("The directory '%1' failed the project sanity check.").arg(project->root)); - logWarn(QString("The directory '%1' failed the project sanity check.").arg(editor->project->root)); + ErrorMessage msgBox(QStringLiteral("The selected directory appears to be invalid."), this); + msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" + "Make sure you selected the correct project directory " + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(project->root)); + auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); + msgBox.exec(); - ErrorMessage msgBox(QStringLiteral("The selected directory appears to be invalid."), this); - msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" - "Make sure you selected the correct project directory " - "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); - auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); - msgBox.exec(); - if (msgBox.clickedButton() == tryAnyway) { - // The user has chosen to try to load this project anyway. + // The user may choose to try to load this project anyway. // This will almost certainly fail, but they'll get a more specific error message. - return true; + if (msgBox.clickedButton() != tryAnyway){ + return true; + } + } + QString error; + int projectVersion = project->getSupportedMajorVersion(&error); + if (projectVersion <= 0) { + // Failed to identify a supported major version. + // We can't draw any conclusions from this, so we don't consider the project to be invalid. + logWarn(error.isEmpty() ? QStringLiteral("Unable to identify project's Porymap version.") : error); + } else { + logInfo(QString("Successful project version check. Supports at least Porymap v%1.").arg(projectVersion)); + + if (projectVersion < porymapVersion.majorVersion() && projectConfig.forcedMajorVersion < porymapVersion.majorVersion()) { + // We were unable to find the necessary changes for Porymap's current major version. + // Warn the user that this might mean their project is missing breaking changes. + // Note: Do not report 'projectVersion' to the user in this message. We've already logged it for troubleshooting. + // It is very plausible that the user may have reproduced the required changes in an + // unknown commit, rather than merging the required changes directly from the base repo. + // In this case the 'projectVersion' may actually be too old to use for their repo. + ErrorMessage msgBox(QStringLiteral("Your project may be incompatible!"), this); + msgBox.setInformativeText(QString("Make sure '%1' has all the required changes for Porymap version %2." + "") // TODO: Once we have a wiki or manual page describing breaking changes, link that here. + .arg(project->getProjectTitle()) + .arg(porymapVersion.majorVersion())); + auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); + msgBox.exec(); + if (msgBox.clickedButton() != tryAnyway){ + return true; + } + // User opted to try with this version anyway. Don't warn them about this version again. + projectConfig.forcedMajorVersion = porymapVersion.majorVersion(); + } } return false; } diff --git a/src/project.cpp b/src/project.cpp index bbbc052c..1b165df2 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -77,6 +77,114 @@ bool Project::sanityCheck() { return false; } +// Porymap projects have no standardized way for Porymap to determine whether they're compatible as of the latest breaking changes. +// We can use the project's git history (if it has one, and we're able to get it) to make a reasonable guess. +// We know the hashes of the commits in the base repos that contain breaking changes, so if we find one of these then the project +// should support at least up to that Porymap major version. If this fails for any reason it returns a version of -1. +// This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check +// is not important enough to significantly slow down project launch, we'd rather just timeout. +int Project::getSupportedMajorVersion(QString *errorOut) { + const int failureVersion = -1; + QString gitPath = QStandardPaths::findExecutable("git"); + if (gitPath.isEmpty()) { + if (errorOut) *errorOut = QStringLiteral("Failed to identify project history: Unable to locate git."); + return failureVersion; + } + + QProcess process; + process.setWorkingDirectory(this->root); + process.setProgram(gitPath); + process.setReadChannel(QProcess::StandardOutput); + process.setStandardInputFile(QProcess::nullDevice()); // We won't have any writing to do. + + // First we need to know which (if any) known git history this project belongs to. + // We'll get the root commit, then compare it to the known root commits for the base project repos. + static const QStringList args_getRootCommit = { "rev-list", "--max-parents=0", "HEAD" }; + process.setArguments(args_getRootCommit); + process.start(); + if (!process.waitForFinished(500) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) { + if (errorOut) { + *errorOut = QStringLiteral("Failed to identify project history"); + if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) { + errorOut->append(QString(": %1").arg(process.errorString())); + } else { + process.setReadChannel(QProcess::StandardError); + QString error = QString(process.readLine()).remove('\n'); + if (!error.isEmpty()) errorOut->append(QString(": %1").arg(error)); + } + } + return failureVersion; + } + const QString rootCommit = QString(process.readLine()).remove('\n'); + + // The keys in this map are the hashes of the root commits for each of the 3 base repos. + // The values are a list of pairs, where the first element is a major version number, and the + // second element is the hash of the earliest commit that supports that major version. + static const QMap>> historyMap = { + // pokeemerald + {"33b799c967fd63d04afe82eecc4892f3e45781b3", { + {6, "07c897ad48c36b178093bde8ca360823127d812b"}, // TODO: Update to merge commit for pokeemerald's porymap-6 branch + {5, "c76beed98990a57c84d3930190fd194abfedf7e8"}, + {4, "cb5b8da77b9ba6837fcc8c5163bedc5008b12c2c"}, + {3, "204c431993dad29661a9ff47326787cd0cf381e6"}, + {2, "cdae0c1444bed98e652c87dc3e3edcecacfef8be"}, + {1, ""} + }}, + // pokefirered + {"670fef77ac4d9116d5fdc28c0da40622919a062b", { + {6, "7722e7a92ca5fa69925dcef82f6c89c35ec48171"}, // TODO: Update to merge commit for pokefirered's porymap-6 branch + {5, "52591dcee42933d64f60c59276fc13c3bb89c47b"}, + {4, "200c82e01a94dbe535e6ed8768d8afad4444d4d2"}, + }}, + // pokeruby + {"1362b60f3467f0894d55e82f3294980b6373021d", { + {6, "bc5aeaa64ecad03aa4ab9e1000ba94916276c936"}, // TODO: Update to merge commit for pokeruby's porymap-6 branch + {5, "d99cb43736dd1d4ee4820f838cb259d773d8bf25"}, + {4, "f302fcc134bf354c3655e3423be68fd7a99cb396"}, + {3, "b4f4d2c0f03462dcdf3492aad27890294600eb2e"}, + {2, "0e8ccfc4fd3544001f4c25fafd401f7558bdefba"}, + {1, ""} + }}, + }; + if (!historyMap.contains(rootCommit)) { + // Either this repo does not share history with one of the base repos, + // (that's ok, don't report an error) or we got some unexpected result. + return failureVersion; + } + + // We now know which base repo that the user's repo shares history with. + // Next we check to see if it contains the changes required to support particular major versions of Porymap. + // We'll start with the most recent latest version and work backwards. + for (const auto &pair : historyMap.value(rootCommit)) { + int versionNum = pair.first; + QString commitHash = pair.second; + if (commitHash.isEmpty()) { + // An empty commit hash means 'consider any point in the history a supported version' + return versionNum; + } + process.setArguments({ "merge-base", "--is-ancestor", commitHash, "HEAD" }); + process.start(); + if (!process.waitForFinished(500) || process.exitStatus() != QProcess::ExitStatus::NormalExit) { + if (errorOut) { + *errorOut = QStringLiteral("Failed to identify project's supported Porymap version"); + if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) { + errorOut->append(QString(": %1").arg(process.errorString())); + } else { + process.setReadChannel(QProcess::StandardError); + QString error = QString(process.readLine()).remove('\n'); + if (!error.isEmpty()) errorOut->append(QString(": %1").arg(error)); + } + } + return failureVersion; + } + if (process.exitCode() == 0) { + // Identified a supported major version + return versionNum; + } + } + return failureVersion; +} + bool Project::load() { resetFileCache(); this->disabledSettingsNames.clear(); From 168792b9c6d7da724967151e55cf8f5d7a5be8f3 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 13 Apr 2025 23:10:51 -0400 Subject: [PATCH 2/9] Fix comment typo --- src/project.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.cpp b/src/project.cpp index 1b165df2..9ce9f6ed 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -154,7 +154,7 @@ int Project::getSupportedMajorVersion(QString *errorOut) { // We now know which base repo that the user's repo shares history with. // Next we check to see if it contains the changes required to support particular major versions of Porymap. - // We'll start with the most recent latest version and work backwards. + // We'll start with the most recent major version and work backwards. for (const auto &pair : historyMap.value(rootCommit)) { int versionNum = pair.first; QString commitHash = pair.second; From 79ffe668a3596ced52027cfc12bb4ae59eec8ae7 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 24 Apr 2025 16:19:08 -0400 Subject: [PATCH 3/9] Update script dropdowns when a map's scripts file is edited --- include/core/events.h | 42 ++++----------------- include/core/map.h | 18 ++++++--- include/ui/eventframes.h | 2 + src/core/events.cpp | 11 ++++-- src/core/map.cpp | 45 +++++++++++++--------- src/ui/eventframes.cpp | 81 +++++++++++++++++++++------------------- 6 files changed, 99 insertions(+), 100 deletions(-) diff --git a/include/core/events.h b/include/core/events.h index 9da9b531..76a58dcf 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -34,15 +34,6 @@ class HiddenItemEvent; class SecretBaseEvent; class HealLocationEvent; -class EventVisitor { -public: - virtual void nothing() { } - virtual void visitObject(ObjectEvent *) = 0; - virtual void visitTrigger(TriggerEvent *) = 0; - virtual void visitSign(SignEvent *) = 0; -}; - - /// /// Event base class -- purely virtual /// @@ -121,8 +112,6 @@ public: void modify(); - virtual void accept(EventVisitor *) { } - void setX(int newX) { this->x = newX; } void setY(int newY) { this->y = newY; } void setZ(int newZ) { this->elevation = newZ; } @@ -150,6 +139,8 @@ public: virtual QSet getExpectedFields() = 0; + virtual QStringList getScripts() const { return QStringList(); } + QJsonObject getCustomAttributes() const { return this->customAttributes; } void setCustomAttributes(const QJsonObject &newCustomAttributes) { this->customAttributes = newCustomAttributes; } @@ -222,8 +213,6 @@ public: virtual Event *duplicate() const override; - virtual void accept(EventVisitor *visitor) override { visitor->visitObject(this); } - virtual EventFrame *createEventFrame() override; virtual OrderedJson::object buildEventJson(Project *project) override; @@ -233,6 +222,8 @@ public: virtual QSet getExpectedFields() override; + virtual QStringList getScripts() const override { return {getScript()}; } + virtual QPixmap loadPixmap(Project *project) override; void setGfx(QString newGfx) { this->gfx = newGfx; } @@ -392,8 +383,6 @@ public: virtual Event *duplicate() const override; - virtual void accept(EventVisitor *visitor) override { visitor->visitTrigger(this); } - virtual EventFrame *createEventFrame() override; virtual OrderedJson::object buildEventJson(Project *project) override; @@ -403,6 +392,8 @@ public: virtual QSet getExpectedFields() override; + virtual QStringList getScripts() const override { return {getScriptLabel()}; } + void setScriptVar(QString newScriptVar) { this->scriptVar = newScriptVar; } QString getScriptVar() const { return this->scriptVar; } @@ -490,8 +481,6 @@ public: virtual Event *duplicate() const override; - virtual void accept(EventVisitor *visitor) override { visitor->visitSign(this); } - virtual EventFrame *createEventFrame() override; virtual OrderedJson::object buildEventJson(Project *project) override; @@ -501,6 +490,8 @@ public: virtual QSet getExpectedFields() override; + virtual QStringList getScripts() const override { return {getScriptLabel()}; } + void setFacingDirection(QString newFacingDirection) { this->facingDirection = newFacingDirection; } QString getFacingDirection() const { return this->facingDirection; } @@ -633,21 +624,4 @@ inline uint qHash(const Event::Group &key, uint seed = 0) { return qHash(static_cast(key), seed); } - -/// -/// Keeps track of scripts -/// -class ScriptTracker : public EventVisitor { -public: - virtual void visitObject(ObjectEvent *object) override { this->scripts << object->getScript(); }; - virtual void visitTrigger(TriggerEvent *trigger) override { this->scripts << trigger->getScriptLabel(); }; - virtual void visitSign(SignEvent *sign) override { this->scripts << sign->getScriptLabel(); }; - - QStringList getScripts() const { return this->scripts; } - -private: - QStringList scripts; -}; - - #endif // EVENTS_H diff --git a/include/core/map.h b/include/core/map.h index 84791fa3..2ba172ce 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #define DEFAULT_BORDER_WIDTH 2 @@ -56,7 +57,7 @@ public: MapHeader* header() const { return m_header; } void setSharedEventsMap(const QString &sharedEventsMap) { m_sharedEventsMap = sharedEventsMap; } - void setSharedScriptsMap(const QString &sharedScriptsMap) { m_sharedScriptsMap = sharedScriptsMap; } + void setSharedScriptsMap(const QString &sharedScriptsMap); QString sharedEventsMap() const { return m_sharedEventsMap; } QString sharedScriptsMap() const { return m_sharedScriptsMap; } @@ -75,14 +76,15 @@ public: Event* getEvent(Event::Group group, const QString &idName) const; QStringList getEventIdNames(Event::Group group) const; int getNumEvents(Event::Group group = Event::Group::None) const; - QStringList getScriptLabels(Event::Group group = Event::Group::None); - QString getScriptsFilePath() const; - void openScript(QString label); void removeEvent(Event *); void addEvent(Event *); int getIndexOfEvent(Event *) const; bool hasEvent(Event *) const; + QStringList getScriptLabels(Event::Group group = Event::Group::None); + QString getScriptsFilePath() const; + void openScript(const QString &label); + void deleteConnections(); QList getConnections() const { return m_connections; } MapConnection* getConnection(const QString &direction) const; @@ -108,7 +110,7 @@ private: QString m_sharedEventsMap = ""; QString m_sharedScriptsMap = ""; - QStringList m_scriptsFileLabels; + QStringList m_scriptLabels; QJsonObject m_customAttributes; MapHeader *m_header = nullptr; @@ -131,10 +133,14 @@ private: QList m_connections; QSet m_ownedConnections; - QUndoStack *m_editHistory = nullptr; + QPointer m_editHistory; + QPointer m_scriptFileWatcher; + + void invalidateScripts(); signals: void modified(); + void scriptsModified(); void mapDimensionsChanged(const QSize &size); void openScriptRequested(QString label); void connectionAdded(MapConnection*); diff --git a/include/ui/eventframes.h b/include/ui/eventframes.h index 09eae50b..3858e54e 100644 --- a/include/ui/eventframes.h +++ b/include/ui/eventframes.h @@ -56,9 +56,11 @@ protected: bool populated = false; bool initialized = false; bool connected = false; + QPointer project; void populateDropdown(NoScrollComboBox * combo, const QStringList &items); void populateScriptDropdown(NoScrollComboBox * combo, Project * project); + void populateMapNameDropdown(NoScrollComboBox * combo, Project * project); void populateIdNameDropdown(NoScrollComboBox * combo, Project * project, const QString &mapName, Event::Group group); private: diff --git a/src/core/events.cpp b/src/core/events.cpp index 22315211..ce376c44 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -114,7 +114,7 @@ QString Event::typeToString(Event::Type type) { } QPixmap Event::loadPixmap(Project *project) { - this->pixmap = project->getEventPixmap(this->getEventGroup()); + this->pixmap = project ? project->getEventPixmap(this->getEventGroup()) : QPixmap(); this->usesDefaultPixmap = true; return this->pixmap; } @@ -226,7 +226,7 @@ QSet ObjectEvent::getExpectedFields() { } QPixmap ObjectEvent::loadPixmap(Project *project) { - this->pixmap = project->getEventPixmap(this->gfx, this->movement); + this->pixmap = project ? project->getEventPixmap(this->gfx, this->movement) : QPixmap(); if (!this->pixmap.isNull()) { this->usesDefaultPixmap = false; return this->pixmap; @@ -316,7 +316,7 @@ QSet CloneObjectEvent::getExpectedFields() { QPixmap CloneObjectEvent::loadPixmap(Project *project) { // Try to get the targeted object to clone - Map *clonedMap = project->loadMap(this->targetMap); + Map *clonedMap = project ? project->loadMap(this->targetMap) : nullptr; Event *clonedEvent = clonedMap ? clonedMap->getEvent(Event::Group::Object, this->targetID) : nullptr; if (clonedEvent && clonedEvent->getEventType() == Event::Type::Object) { @@ -324,10 +324,13 @@ QPixmap CloneObjectEvent::loadPixmap(Project *project) { ObjectEvent *clonedObject = dynamic_cast(clonedEvent); this->gfx = clonedObject->getGfx(); this->movement = clonedObject->getMovement(); - } else { + } else if (project) { // Invalid object specified, use default graphics data (as would be shown in-game) this->gfx = project->gfxDefines.key(0, "0"); this->movement = project->movementTypes.value(0, "0"); + } else { + this->gfx = "0"; + this->movement = "0"; } return ObjectEvent::loadPixmap(project); } diff --git a/src/core/map.cpp b/src/core/map.cpp index 3cd087e1..c1905f0a 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -14,6 +14,10 @@ Map::Map(QObject *parent) : QObject(parent) { m_editHistory = new QUndoStack(this); + + m_scriptFileWatcher = new QFileSystemWatcher(this); + connect(m_scriptFileWatcher, &QFileSystemWatcher::fileChanged, this, &Map::invalidateScripts); + resetEvents(); m_header = new MapHeader(this); @@ -120,35 +124,40 @@ QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) { return connectionPixmap.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); } -void Map::openScript(QString label) { +void Map::openScript(const QString &label) { emit openScriptRequested(label); } +void Map::setSharedScriptsMap(const QString &sharedScriptsMap) { + if (m_sharedScriptsMap == sharedScriptsMap) + return; + m_sharedScriptsMap = sharedScriptsMap; + invalidateScripts(); +} + +void Map::invalidateScripts() { + m_scriptsLoaded = false; + emit scriptsModified(); +} + QStringList Map::getScriptLabels(Event::Group group) { if (!m_scriptsLoaded) { - m_scriptsFileLabels = ParseUtil::getGlobalScriptLabels(getScriptsFilePath()); + const QString scriptsFilePath = getScriptsFilePath(); + m_scriptLabels = ParseUtil::getGlobalScriptLabels(scriptsFilePath); m_scriptsLoaded = true; + + // Track the scripts file for changes. Path may have changed, so stop tracking old files. + m_scriptFileWatcher->removePaths(m_scriptFileWatcher->files()); + m_scriptFileWatcher->addPath(scriptsFilePath); } - QStringList scriptLabels; + QStringList scriptLabels = m_scriptLabels; - // Get script labels currently in-use by the map's events - if (group == Event::Group::None) { - ScriptTracker scriptTracker; - for (const auto &event : getEvents()) { - event->accept(&scriptTracker); - } - scriptLabels = scriptTracker.getScripts(); - } else { - ScriptTracker scriptTracker; - for (const auto &event : m_events.value(group)) { - event->accept(&scriptTracker); - } - scriptLabels = scriptTracker.getScripts(); + // Add script labels currently in-use by the map's events + for (const auto &event : getEvents(group)) { + scriptLabels.append(event->getScripts()); } - // Add labels from the map's scripts file - scriptLabels.append(m_scriptsFileLabels); scriptLabels.sort(Qt::CaseInsensitive); scriptLabels.removeAll(""); scriptLabels.removeAll("0"); diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index 95a31557..b9cdab22 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -152,8 +152,12 @@ void EventFrame::initialize() { this->label_icon->setPixmap(this->event->getPixmap()); } -void EventFrame::populate(Project *) { +void EventFrame::populate(Project *project) { this->populated = true; + if (this->project && this->project != project) { + this->project->disconnect(this); + } + this->project = project; } void EventFrame::invalidateConnections() { @@ -166,6 +170,10 @@ void EventFrame::invalidateUi() { void EventFrame::invalidateValues() { this->populated = false; + if (this->isVisible()) { + // Repopulate immediately + this->populate(this->project); + } } void EventFrame::setActive(bool active) { @@ -186,14 +194,15 @@ void EventFrame::populateDropdown(NoScrollComboBox * combo, const QStringList &i void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * project) { // The script dropdown and autocomplete are populated with scripts used by the map's events and from its scripts file. - if (!this->event->getMap()) + Map *map = this->event ? this->event->getMap() : nullptr; + if (!map) return; - QStringList scripts = this->event->getMap()->getScriptLabels(this->event->getEventGroup()); + QStringList scripts = map->getScriptLabels(this->event->getEventGroup()); populateDropdown(combo, scripts); // Depending on the settings, the autocomplete may also contain all global scripts. - if (porymapConfig.loadAllEventScripts) { + if (project && porymapConfig.loadAllEventScripts) { project->insertGlobalScriptLabels(scripts); } @@ -209,14 +218,23 @@ void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * proj combo->setCompleter(completer); - // If the project changes the script labels, update the EventFrame. - // TODO: At the moment this only happens when the user changes script settings (i.e. when 'porymapConfig.loadAllEventScripts' changes). - // This should ultimately be connected to a file watcher so that we can also update the dropdown when the scripts file changes. - connect(project, &Project::eventScriptLabelsRead, this, &EventFrame::invalidateValues, Qt::UniqueConnection); + // If the script labels change then we need to update the EventFrame. + if (project) connect(project, &Project::eventScriptLabelsRead, this, &EventFrame::invalidateValues, Qt::UniqueConnection); + connect(map, &Map::scriptsModified, this, &EventFrame::invalidateValues, Qt::UniqueConnection); +} + +void EventFrame::populateMapNameDropdown(NoScrollComboBox * combo, Project * project) { + if (!project) + return; + + populateDropdown(combo, project->mapNames); + + // This frame type displays map names, so when a new map is created we need to repopulate it. + connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void EventFrame::populateIdNameDropdown(NoScrollComboBox * combo, Project * project, const QString &mapName, Event::Group group) { - if (!project->mapNames.contains(mapName)) + if (!project || !project->mapNames.contains(mapName)) return; Map *map = project->loadMap(mapName); @@ -325,7 +343,6 @@ void ObjectFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; // local id this->line_edit_local_id->disconnect(); @@ -336,18 +353,18 @@ void ObjectFrame::connectSignals(MainWindow *window) { // sprite update this->combo_sprite->disconnect(); - connect(this->combo_sprite, &QComboBox::currentTextChanged, [this, project](const QString &text) { + connect(this->combo_sprite, &QComboBox::currentTextChanged, [this](const QString &text) { this->object->setGfx(text); - this->object->getPixmapItem()->render(project); + this->object->getPixmapItem()->render(this->project); this->object->modify(); }); connect(this->object->getPixmapItem(), &EventPixmapItem::rendered, this->label_icon, &QLabel::setPixmap); // movement this->combo_movement->disconnect(); - connect(this->combo_movement, &QComboBox::currentTextChanged, [this, project](const QString &text) { + connect(this->combo_movement, &QComboBox::currentTextChanged, [this](const QString &text) { this->object->setMovement(text); - this->object->getPixmapItem()->render(project); + this->object->getPixmapItem()->render(this->project); this->object->modify(); }); @@ -498,7 +515,6 @@ void CloneObjectFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; // local id this->line_edit_local_id->disconnect(); @@ -512,26 +528,23 @@ void CloneObjectFrame::connectSignals(MainWindow *window) { // target map this->combo_target_map->disconnect(); - connect(this->combo_target_map, &QComboBox::currentTextChanged, [this, project](const QString &mapName) { + connect(this->combo_target_map, &QComboBox::currentTextChanged, [this](const QString &mapName) { this->clone->setTargetMap(mapName); - this->clone->getPixmapItem()->render(project); + this->clone->getPixmapItem()->render(this->project); this->combo_sprite->setCurrentText(this->clone->getGfx()); this->clone->modify(); - populateIdNameDropdown(this->combo_target_id, project, mapName, Event::Group::Object); + populateIdNameDropdown(this->combo_target_id, this->project, mapName, Event::Group::Object); }); connect(window, &MainWindow::mapOpened, this, &CloneObjectFrame::tryInvalidateIdDropdown, Qt::UniqueConnection); // target id this->combo_target_id->disconnect(); - connect(this->combo_target_id, &QComboBox::currentTextChanged, [this, project](const QString &text) { + connect(this->combo_target_id, &QComboBox::currentTextChanged, [this](const QString &text) { this->clone->setTargetID(text); - this->clone->getPixmapItem()->render(project); + this->clone->getPixmapItem()->render(this->project); this->combo_sprite->setCurrentText(this->clone->getGfx()); this->clone->modify(); }); - - // This frame type displays map names, so when a new map is created we need to repopulate it. - connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void CloneObjectFrame::tryInvalidateIdDropdown(Map *map) { @@ -567,7 +580,7 @@ void CloneObjectFrame::populate(Project *project) { const QSignalBlocker blocker(this); EventFrame::populate(project); - populateDropdown(this->combo_target_map, project->mapNames); + populateMapNameDropdown(this->combo_target_map, project); populateIdNameDropdown(this->combo_target_id, project, this->clone->getTargetMap(), Event::Group::Object); } @@ -618,7 +631,6 @@ void WarpFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; // id this->line_edit_id->disconnect(); @@ -629,10 +641,10 @@ void WarpFrame::connectSignals(MainWindow *window) { // dest map this->combo_dest_map->disconnect(); - connect(this->combo_dest_map, &QComboBox::currentTextChanged, [this, project](const QString &mapName) { + connect(this->combo_dest_map, &QComboBox::currentTextChanged, [this](const QString &mapName) { this->warp->setDestinationMap(mapName); this->warp->modify(); - populateIdNameDropdown(this->combo_dest_warp, project, mapName, Event::Group::Warp); + populateIdNameDropdown(this->combo_dest_warp, this->project, mapName, Event::Group::Warp); }); connect(window, &MainWindow::mapOpened, this, &WarpFrame::tryInvalidateIdDropdown, Qt::UniqueConnection); @@ -646,9 +658,6 @@ void WarpFrame::connectSignals(MainWindow *window) { // warning this->warning->disconnect(); connect(this->warning, &QPushButton::clicked, window, &MainWindow::onWarpBehaviorWarningClicked); - - // This frame type displays map names, so when a new map is created we need to repopulate it. - connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void WarpFrame::tryInvalidateIdDropdown(Map *map) { @@ -681,7 +690,7 @@ void WarpFrame::populate(Project *project) { const QSignalBlocker blocker(this); EventFrame::populate(project); - populateDropdown(this->combo_dest_map, project->mapNames); + populateMapNameDropdown(this->combo_dest_map, project); populateIdNameDropdown(this->combo_dest_warp, project, this->warp->getDestinationMap(), Event::Group::Warp); } @@ -1102,7 +1111,6 @@ void HealLocationFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; this->line_edit_id->disconnect(); connect(this->line_edit_id, &QLineEdit::textChanged, [this](const QString &text) { @@ -1111,10 +1119,10 @@ void HealLocationFrame::connectSignals(MainWindow *window) { }); this->combo_respawn_map->disconnect(); - connect(this->combo_respawn_map, &QComboBox::currentTextChanged, [this, project](const QString &mapName) { + connect(this->combo_respawn_map, &QComboBox::currentTextChanged, [this](const QString &mapName) { this->healLocation->setRespawnMapName(mapName); this->healLocation->modify(); - populateIdNameDropdown(this->combo_respawn_npc, project, mapName, Event::Group::Object); + populateIdNameDropdown(this->combo_respawn_npc, this->project, mapName, Event::Group::Object); }); connect(window, &MainWindow::mapOpened, this, &HealLocationFrame::tryInvalidateIdDropdown, Qt::UniqueConnection); @@ -1123,9 +1131,6 @@ void HealLocationFrame::connectSignals(MainWindow *window) { this->healLocation->setRespawnNPC(text); this->healLocation->modify(); }); - - // This frame type displays map names, so when a new map is created we need to repopulate it. - connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void HealLocationFrame::tryInvalidateIdDropdown(Map *map) { @@ -1158,7 +1163,7 @@ void HealLocationFrame::populate(Project *project) { EventFrame::populate(project); if (projectConfig.healLocationRespawnDataEnabled) { - populateDropdown(this->combo_respawn_map, project->mapNames); + populateMapNameDropdown(this->combo_respawn_map, project); populateIdNameDropdown(this->combo_respawn_npc, project, this->healLocation->getRespawnMapName(), Event::Group::Object); } } From 31cf00de8b5b2e29bc4e1e9e36228374d5fdb483 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 30 Apr 2025 13:03:30 -0400 Subject: [PATCH 4/9] Update git search error messages --- include/mainwindow.h | 2 ++ src/mainwindow.cpp | 49 +++++++++++++++++++++++++++----------------- src/project.cpp | 27 +++++++++++++----------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index b2bba6ca..5de2a50a 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -352,6 +352,8 @@ private: void setLayoutOnlyMode(bool layoutOnly); bool isInvalidProject(Project *project); + bool checkProjectSanity(Project *project); + bool checkProjectVersion(Project *project); bool loadProjectData(); bool setProjectUI(); void clearProjectUI(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e15828ae..4e82dce4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -725,39 +725,50 @@ bool MainWindow::loadProjectData() { } bool MainWindow::isInvalidProject(Project *project) { - if (!project->sanityCheck()) { - logWarn(QString("The directory '%1' failed the project sanity check.").arg(project->root)); + return !(checkProjectSanity(project) && checkProjectVersion(project)); +} - ErrorMessage msgBox(QStringLiteral("The selected directory appears to be invalid."), porysplash); - msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" - "Make sure you selected the correct project directory " - "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(project->root)); - auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); - msgBox.exec(); +bool MainWindow::checkProjectSanity(Project *project) { + if (project->sanityCheck()) + return true; - // The user may choose to try to load this project anyway. + logWarn(QString("The directory '%1' failed the project sanity check.").arg(project->root)); + + ErrorMessage msgBox(QStringLiteral("The selected directory appears to be invalid."), porysplash); + msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" + "Make sure you selected the correct project directory " + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(project->root)); + auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); + msgBox.exec(); + if (msgBox.clickedButton() == tryAnyway) { + // The user has chosen to try to load this project anyway. // This will almost certainly fail, but they'll get a more specific error message. - if (msgBox.clickedButton() != tryAnyway){ - return true; - } + return true; } + return false; +} + +bool MainWindow::checkProjectVersion(Project *project) { QString error; int projectVersion = project->getSupportedMajorVersion(&error); - if (projectVersion <= 0) { + if (projectVersion < 0) { // Failed to identify a supported major version. // We can't draw any conclusions from this, so we don't consider the project to be invalid. - logWarn(error.isEmpty() ? QStringLiteral("Unable to identify project's Porymap version.") : error); + QString msg = QStringLiteral("Failed to check project version"); + logWarn(error.isEmpty() ? msg : QString("%1: '%2'").arg(msg).arg(error)); } else { - logInfo(QString("Successful project version check. Supports at least Porymap v%1.").arg(projectVersion)); + QString msg = QStringLiteral("Successfully checked project version. "); + logInfo(msg + ((projectVersion != 0) ? QString("Supports at least Porymap v%1").arg(projectVersion) + : QStringLiteral("Too old for any Porymap version"))); if (projectVersion < porymapVersion.majorVersion() && projectConfig.forcedMajorVersion < porymapVersion.majorVersion()) { // We were unable to find the necessary changes for Porymap's current major version. - // Warn the user that this might mean their project is missing breaking changes. + // Unless they have explicitly suppressed this message, warn the user that this might mean their project is missing breaking changes. // Note: Do not report 'projectVersion' to the user in this message. We've already logged it for troubleshooting. // It is very plausible that the user may have reproduced the required changes in an // unknown commit, rather than merging the required changes directly from the base repo. // In this case the 'projectVersion' may actually be too old to use for their repo. - ErrorMessage msgBox(QStringLiteral("Your project may be incompatible!"), this); + ErrorMessage msgBox(QStringLiteral("Your project may be incompatible!"), porysplash); msgBox.setInformativeText(QString("Make sure '%1' has all the required changes for Porymap version %2." "") // TODO: Once we have a wiki or manual page describing breaking changes, link that here. .arg(project->getProjectTitle()) @@ -765,13 +776,13 @@ bool MainWindow::isInvalidProject(Project *project) { auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); msgBox.exec(); if (msgBox.clickedButton() != tryAnyway){ - return true; + return false; } // User opted to try with this version anyway. Don't warn them about this version again. projectConfig.forcedMajorVersion = porymapVersion.majorVersion(); } } - return false; + return true; } void MainWindow::showProjectOpenFailure() { diff --git a/src/project.cpp b/src/project.cpp index b8183553..b340ed27 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -79,13 +79,15 @@ bool Project::sanityCheck() { // We can use the project's git history (if it has one, and we're able to get it) to make a reasonable guess. // We know the hashes of the commits in the base repos that contain breaking changes, so if we find one of these then the project // should support at least up to that Porymap major version. If this fails for any reason it returns a version of -1. -// This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check -// is not important enough to significantly slow down project launch, we'd rather just timeout. int Project::getSupportedMajorVersion(QString *errorOut) { + // This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check + // is not important enough to significantly slow down project launch, we'd rather just timeout. + const int timeoutLimit = 500; const int failureVersion = -1; - QString gitPath = QStandardPaths::findExecutable("git"); + QString gitName = "git"; + QString gitPath = QStandardPaths::findExecutable(gitName); if (gitPath.isEmpty()) { - if (errorOut) *errorOut = QStringLiteral("Failed to identify project history: Unable to locate git."); + if (errorOut) *errorOut = QString("Unable to locate %1.").arg(gitName); return failureVersion; } @@ -95,14 +97,14 @@ int Project::getSupportedMajorVersion(QString *errorOut) { process.setReadChannel(QProcess::StandardOutput); process.setStandardInputFile(QProcess::nullDevice()); // We won't have any writing to do. - // First we need to know which (if any) known git history this project belongs to. + // First we need to know which (if any) known history this project belongs to. // We'll get the root commit, then compare it to the known root commits for the base project repos. static const QStringList args_getRootCommit = { "rev-list", "--max-parents=0", "HEAD" }; process.setArguments(args_getRootCommit); process.start(); - if (!process.waitForFinished(500) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) { + if (!process.waitForFinished(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) { if (errorOut) { - *errorOut = QStringLiteral("Failed to identify project history"); + *errorOut = QStringLiteral("Failed to identify commit history"); if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) { errorOut->append(QString(": %1").arg(process.errorString())); } else { @@ -145,8 +147,8 @@ int Project::getSupportedMajorVersion(QString *errorOut) { }}, }; if (!historyMap.contains(rootCommit)) { - // Either this repo does not share history with one of the base repos, - // (that's ok, don't report an error) or we got some unexpected result. + // Either this repo does not share history with one of the base repos, or we got some unexpected result. + if (errorOut) *errorOut = QStringLiteral("Unrecognized commit history"); return failureVersion; } @@ -162,9 +164,9 @@ int Project::getSupportedMajorVersion(QString *errorOut) { } process.setArguments({ "merge-base", "--is-ancestor", commitHash, "HEAD" }); process.start(); - if (!process.waitForFinished(500) || process.exitStatus() != QProcess::ExitStatus::NormalExit) { + if (!process.waitForFinished(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit) { if (errorOut) { - *errorOut = QStringLiteral("Failed to identify project's supported Porymap version"); + *errorOut = QStringLiteral("Failed to search commit history"); if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) { errorOut->append(QString(": %1").arg(process.errorString())); } else { @@ -180,7 +182,8 @@ int Project::getSupportedMajorVersion(QString *errorOut) { return versionNum; } } - return failureVersion; + // We recognized the commit history, but it's too old for any version of Porymap to support. + return 0; } bool Project::load() { From 311b6c8638be5614704280abc4caace304ac2792 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 30 Apr 2025 15:23:48 -0400 Subject: [PATCH 5/9] Add more player icons --- include/config.h | 5 +++ resources/icons/player/brendan_em.ico | Bin 0 -> 3638 bytes resources/icons/player/brendan_rs.ico | Bin 0 -> 3638 bytes resources/icons/player/green.ico | Bin 0 -> 3638 bytes resources/icons/player/may_em.ico | Bin 0 -> 3638 bytes resources/icons/player/may_rs.ico | Bin 0 -> 3638 bytes .../icons/{viewsprites.ico => player/red.ico} | Bin resources/images.qrc | 7 ++++- src/config.cpp | 29 ++++++++++++++++++ src/mainwindow.cpp | 17 +++++++++- 10 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 resources/icons/player/brendan_em.ico create mode 100644 resources/icons/player/brendan_rs.ico create mode 100644 resources/icons/player/green.ico create mode 100644 resources/icons/player/may_em.ico create mode 100644 resources/icons/player/may_rs.ico rename resources/icons/{viewsprites.ico => player/red.ico} (100%) diff --git a/include/config.h b/include/config.h index 26d2a353..285a86e1 100644 --- a/include/config.h +++ b/include/config.h @@ -335,6 +335,7 @@ public: this->filePaths.clear(); this->eventIconPaths.clear(); this->pokemonIconPaths.clear(); + this->eventTabIconPath = QString(); this->collisionSheetPath = QString(); this->collisionSheetSize = QSize(2, 16); this->playerViewDistance = QMargins(GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER, GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER); @@ -355,6 +356,9 @@ public: static const QStringList versionStrings; static BaseGameVersion stringToBaseGameVersion(const QString &string); + static QString getPlayerIconPath(BaseGameVersion baseGameVersion, int character); + static QIcon getPlayerIcon(BaseGameVersion baseGameVersion, int character); + void reset(BaseGameVersion baseGameVersion); void setFilePath(ProjectFilePath pathId, const QString &path); void setFilePath(const QString &pathId, const QString &path); @@ -414,6 +418,7 @@ public: uint16_t unusedTileCovered; uint16_t unusedTileSplit; bool mapAllowFlagsEnabled; + QString eventTabIconPath; QString collisionSheetPath; QSize collisionSheetSize; QMargins playerViewDistance; diff --git a/resources/icons/player/brendan_em.ico b/resources/icons/player/brendan_em.ico new file mode 100644 index 0000000000000000000000000000000000000000..2c896c4494b7b0a951758d8ea1aae283294b6743 GIT binary patch literal 3638 zcmeHKF>V_%5S$~$GkFwXJwa9AFSv@`x=fieReF_f@`9w%Bl3VeK#fb6X`NIq4Fhgc z1h~wQM}w!tcWD#cKzrQXSuVvDv?~D(3^p5Z^?2`qR{*x#Y=8E^NBYj_V+iO@aa>-+ z`F4Zrt8-jlUQ=%HW`BjZ?|SU^ci7$Tu)pc?z_uM|(1otGx=f%-G)>SV*b^dg#R}osLozghuN-27?YbttsB&*WHvqB$JWR4`hcWb-!slJMaagL*HfcMTF4XGL4 z5s~9h^H4)icB#A8IUe8~P17_@RBH8Z>C?P1Iz5B)RwH*bRp)qBK)dta{5;Mu%_~EO z;-nqlV4%}Ef8tC0?>`X=lh-+3?_|8=*sA#hzc{r#0;iIn!0TFyzmAn&{AI28Naw$@ R(ajS_ZhX1P!`SmQF7sm7r6H;KoGYqUC+t*~m`M~&ENXPyH$PMD8Es zsw>L9&Dl9CeWPd|x1pc2ZQbd)z5}1R1CH~Rv({;}8j8YvHt4$flF)e6c(y9wjOLUM zW7KhSxAE$2%(6aPrL^Z=ykcFL{$T^uDcLg zuz7E|6HV+$>mp5prNo^fAIafx1yX=vg|jnoZSZjoyaBM;xcsdFzEF2g9Zf)Wvg2}) zE{6g4+by=+OANyf?>~*$eZ9bNeT$2$2i)J>;A%JG!^4R2@$q1s1z0MuRN$YhK+{;& z)s;1D9irG+XST8?CNaQjZB~Y$H3Wo)#KmQBA0u^+6OD-c(!%Y7B?`%&;o3e?>jAB8 zPZG6Muvzu(2|wx4Q$d;b>vgX+8BFd}NR-7yE|&2V4nN)|*me1N$0Ws{iO#X?zOxqM zr&C}Yz0YnK>>XMC_!upH_E>cmY^lKiUIFz@v#RR4x~?i?%gs&KZ$5{|DF2S_nGJd+ ze>U2Jj^paTW_9Z3a#DoU&!-6UxGX~I{>s!PJ)PL#XK-*<$S;yS&hzt+;eNJmEPp*c zhkX7VuKdC@jC>}zCi}aTr-jxM*WHZ6Do-1&99h3AIETA;9(H-;c?8cu(GTOsIVaS4 zzw3LSY_G#T|24_;mt6WE#$VokUx9u8(v?)+PZ3H?dp|qBfqDE-_0QffGyMH@*_Cfs l`@OI4=J4nCua%#G(~|G_WBnVPWBvf%mdw8<`g<~F`U&vfakT&d literal 0 HcmV?d00001 diff --git a/resources/icons/player/may_em.ico b/resources/icons/player/may_em.ico new file mode 100644 index 0000000000000000000000000000000000000000..ffa56bab11cba96ad61cf197a37bf13a688dbff8 GIT binary patch literal 3638 zcmeHKu}&L75Pcr6*SFrY%qO^tNExM~BG;y*Oi2~dQl^QNJ{2E8K|zs^po!AplGcip zAMght9dCBL+?^8NB_+{~ygNJdcIIG=-aCK^1V=}pFYs{&yaKRZTYOspUzs~*j#t2R zx<_}=s*~3^dGj7s^&Mv)KH>8G9A_6dSbwW<`DKHvn=Pt`3v4$VY_>J(x<1%z1jY%B z6L?V*h!Im7=#wp%m`tY=(3_PV$G{adw?|VLLQaUdB1lAe%jkGBv`;*u6+|3sND77M1LcH;x)GGk&fk`FYY^?K}DW>j%h##4)J(HlxQWiH;8^VEdPv%zBjWpNIbR(;AiT#rTG!kG?+WT=s1MSZp z4&&r+%0f|uKtZ92B+;R~JoEbiPEnz0rPb?6d*=FnoXSN>vRb`qJt$VsQy9X%4?#QR zUBohZctbegY5HtO(_Q0St$qmCs+iPN|59Sq9_{)eT&Sw5wbWnJWOiuR_v4BRrP(@A zv1gXa9sUNVs;3@SYf-zt4^Qs^{KlL6`u*ofy~a(Z8@2EK`*{bf+UpTG)x}o`eT}H^ cfw#KuNDqCh-9yj0&u{tF?(~t1|Enc?0&RwMrvLx| literal 0 HcmV?d00001 diff --git a/resources/icons/player/may_rs.ico b/resources/icons/player/may_rs.ico new file mode 100644 index 0000000000000000000000000000000000000000..7450d620ac4609a00441b9eb863875f1f0d5aefa GIT binary patch literal 3638 zcmeHKv2NQi5Pdq55@ij}CwL0lG3b;j^_nqb#!f-kj9m(3@6-?IkRhLurN|Qhf>EG9 z;139(zT<@vgT_)irqF{(-rc)9T9kb!0~G`Z2jCiU5r9JgtCi_D5%@;iJKE?1nv)-w zy;vW=$MMlA*6W`*JO6^S&(}Er)Z_AMgYVZ{Y&RQhwmtg3-s;k;0NL54bfpyt&QQi>3R=p__V z$`HJGUcpp&aW&!OaYw1^w(Huu)@!H=z74v=M(0V+Exz3KA&dkjx@V}oxOK2sk2rWmZ4|hGqcLeJU z%QFi!tCZA`@E@+^SZZ)#aSM+xKU=~doJ(!N literal 0 HcmV?d00001 diff --git a/resources/icons/viewsprites.ico b/resources/icons/player/red.ico similarity index 100% rename from resources/icons/viewsprites.ico rename to resources/icons/player/red.ico diff --git a/resources/images.qrc b/resources/images.qrc index 41789a6e..8d6af6fd 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -46,9 +46,14 @@ icons/sort_number.ico icons/tall_grass.ico icons/minimap.ico - icons/viewsprites.ico icons/application_form_edit.ico icons/connections.ico + icons/player/brendan_em.ico + icons/player/brendan_rs.ico + icons/player/green.ico + icons/player/may_em.ico + icons/player/may_rs.ico + icons/player/red.ico icons/ui/dark/checkbox_checked_disabled.png icons/ui/dark/checkbox_checked_disabled@2x.png icons/ui/dark/checkbox_checked.png diff --git a/src/config.cpp b/src/config.cpp index 0e22840e..6ecce6d4 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -731,6 +731,32 @@ BaseGameVersion ProjectConfig::stringToBaseGameVersion(const QString &string) { return version; } +QString ProjectConfig::getPlayerIconPath(BaseGameVersion baseGameVersion, int character) { + switch (baseGameVersion) { + case BaseGameVersion::pokeemerald: { + static const QStringList paths = { QStringLiteral(":/icons/player/brendan_em.ico"), + QStringLiteral(":/icons/player/may_em.ico"), }; + return paths.value(character); + } + case BaseGameVersion::pokefirered: { + static const QStringList paths = { QStringLiteral(":/icons/player/red.ico"), + QStringLiteral(":/icons/player/green.ico"), }; + return paths.value(character); + } + case BaseGameVersion::pokeruby: { + static const QStringList paths = { QStringLiteral(":/icons/player/brendan_rs.ico"), + QStringLiteral(":/icons/player/may_rs.ico"), }; + return paths.value(character); + } + default: break; + } + return QString(); +} + +QIcon ProjectConfig::getPlayerIcon(BaseGameVersion baseGameVersion, int character) { + return QIcon(getPlayerIconPath(baseGameVersion, character)); +} + ProjectConfig projectConfig; QString ProjectConfig::getConfigFilepath() { @@ -868,6 +894,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->eventIconPaths[Event::Group::Heal] = value; } else if (key.startsWith("pokemon_icon_path/")) { this->pokemonIconPaths.insert(key.mid(QStringLiteral("pokemon_icon_path/").length()), value); + } else if (key == "event_tab_icon_path") { + this->eventTabIconPath = value; } else if (key == "collision_sheet_path") { this->collisionSheetPath = value; } else if (key == "collision_sheet_width") { @@ -985,6 +1013,7 @@ QMap ProjectConfig::getKeyValueMap() { for (auto it = this->identifiers.constBegin(); it != this->identifiers.constEnd(); it++) { map.insert("ident/"+defaultIdentifiers.value(it.key()).first, it.value()); } + map.insert("event_tab_icon_path", this->eventTabIconPath); map.insert("collision_sheet_path", this->collisionSheetPath); map.insert("collision_sheet_width", QString::number(this->collisionSheetSize.width())); map.insert("collision_sheet_height", QString::number(this->collisionSheetSize.height())); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ac41f582..18fbe198 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -249,7 +249,7 @@ void MainWindow::initCustomUI() { static const QMap mainTabIcons = { {MainTab::Map, QIcon(QStringLiteral(":/icons/minimap.ico"))}, - {MainTab::Events, QIcon(QStringLiteral(":/icons/viewsprites.ico"))}, + {MainTab::Events, ProjectConfig::getPlayerIcon(BaseGameVersion::pokefirered, 0)}, // Arbitrary default {MainTab::Header, QIcon(QStringLiteral(":/icons/application_form_edit.ico"))}, {MainTab::Connections, QIcon(QStringLiteral(":/icons/connections.ico"))}, {MainTab::WildPokemon, QIcon(QStringLiteral(":/icons/tall_grass.ico"))}, @@ -1255,6 +1255,21 @@ bool MainWindow::setProjectUI() { ui->mapCustomAttributesFrame->table()->setRestrictedKeys(project->getTopLevelMapFields()); + // Set a version dependent player icon (or user-chosen icon) for the Events tab. + QIcon eventTabIcon; + if (!projectConfig.eventTabIconPath.isEmpty()) { + eventTabIcon = QIcon(projectConfig.eventTabIconPath); + if (eventTabIcon.isNull()) { + logWarn(QString("Failed to load custom Events tab icon '%1'.").arg(projectConfig.eventTabIconPath)); + } + } + if (eventTabIcon.isNull()) { + // We randomly choose between the available characters for ~flavor~. + // For now, this correctly assumes all versions have 2 icons. + eventTabIcon = ProjectConfig::getPlayerIcon(projectConfig.baseGameVersion, QRandomGenerator::global()->bounded(0, 2)); + } + ui->mainTabBar->setTabIcon(MainTab::Events, eventTabIcon); + return true; } From 6d3fd5bc7e7f8c70d8e5d7474d7f8ebfea49b8c8 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 30 Apr 2025 16:58:50 -0400 Subject: [PATCH 6/9] Add settings for events tab icon --- forms/projectsettingseditor.ui | 244 ++++++++++++++++++------------- include/config.h | 4 +- src/config.cpp | 6 +- src/mainwindow.cpp | 6 +- src/ui/projectsettingseditor.cpp | 34 +++++ 5 files changed, 186 insertions(+), 108 deletions(-) diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index 05598b8f..c840d331 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -39,7 +39,7 @@ 0 0 570 - 692 + 680 @@ -94,7 +94,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -164,7 +164,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -248,7 +248,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1197,23 +1197,62 @@ 0 0 570 - 840 + 927 + + + + Tab Icon + + + + + + false + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + false + + + <html><head/><body><p>The image file path to use for the icon of the Events tab.</p></body></html> + + + true + + + + + + + <html><head/><body><p>The icon that will be displayed for the Events tab in the editor. If 'Automatic' is chosen, the icon will be a random player character from the project's base game version. If 'Custom' is chosen an image file path may be specified.</p></body></html> + + + false + + + + + + Default Icons - - - - Triggers - - - @@ -1221,23 +1260,31 @@ - - + + - <html><head/><body><p>The icon that will be used to represent Warp events</p></body></html> + <html><head/><body><p>The icon that will be used to represent BG events</p></body></html> true - - - - <html><head/><body><p>The icon that will be used to represent Heal Location events</p></body></html> + + + + ... - - true + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + Objects @@ -1248,6 +1295,24 @@ + + + + Triggers + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + @@ -1265,10 +1330,56 @@ - - + + + + <html><head/><body><p>The icon that will be used to represent Warp events</p></body></html> + + + true + + + + + - Objects + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + <html><head/><body><p>The icon that will be used to represent Heal Location events</p></body></html> + + + true + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico @@ -1282,71 +1393,6 @@ - - - - <html><head/><body><p>The icon that will be used to represent BG events</p></body></html> - - - true - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - @@ -1365,7 +1411,7 @@ ... - + :/icons/delete.ico:/icons/delete.ico @@ -1418,7 +1464,7 @@ ... - + :/icons/add.ico:/icons/add.ico @@ -1628,7 +1674,7 @@ 0 0 544 - 338 + 341 @@ -1657,7 +1703,7 @@ Add Global Constants File... - + :/icons/add.ico:/icons/add.ico @@ -1668,7 +1714,7 @@ ... - + :/icons/help.ico:/icons/help.ico @@ -1708,7 +1754,7 @@ ... - + :/icons/help.ico:/icons/help.ico @@ -1745,7 +1791,7 @@ 0 0 544 - 421 + 425 @@ -1774,7 +1820,7 @@ Add Global Constant... - + :/icons/add.ico:/icons/add.ico @@ -1840,8 +1886,6 @@
uintspinbox.h
- - - + diff --git a/include/config.h b/include/config.h index 285a86e1..f2353ca4 100644 --- a/include/config.h +++ b/include/config.h @@ -335,7 +335,7 @@ public: this->filePaths.clear(); this->eventIconPaths.clear(); this->pokemonIconPaths.clear(); - this->eventTabIconPath = QString(); + this->eventsTabIconPath = QString(); this->collisionSheetPath = QString(); this->collisionSheetSize = QSize(2, 16); this->playerViewDistance = QMargins(GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER, GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER); @@ -418,7 +418,7 @@ public: uint16_t unusedTileCovered; uint16_t unusedTileSplit; bool mapAllowFlagsEnabled; - QString eventTabIconPath; + QString eventsTabIconPath; QString collisionSheetPath; QSize collisionSheetSize; QMargins playerViewDistance; diff --git a/src/config.cpp b/src/config.cpp index 6ecce6d4..08e8ba8c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -894,8 +894,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->eventIconPaths[Event::Group::Heal] = value; } else if (key.startsWith("pokemon_icon_path/")) { this->pokemonIconPaths.insert(key.mid(QStringLiteral("pokemon_icon_path/").length()), value); - } else if (key == "event_tab_icon_path") { - this->eventTabIconPath = value; + } else if (key == "events_tab_icon_path") { + this->eventsTabIconPath = value; } else if (key == "collision_sheet_path") { this->collisionSheetPath = value; } else if (key == "collision_sheet_width") { @@ -1013,7 +1013,7 @@ QMap ProjectConfig::getKeyValueMap() { for (auto it = this->identifiers.constBegin(); it != this->identifiers.constEnd(); it++) { map.insert("ident/"+defaultIdentifiers.value(it.key()).first, it.value()); } - map.insert("event_tab_icon_path", this->eventTabIconPath); + map.insert("events_tab_icon_path", this->eventsTabIconPath); map.insert("collision_sheet_path", this->collisionSheetPath); map.insert("collision_sheet_width", QString::number(this->collisionSheetSize.width())); map.insert("collision_sheet_height", QString::number(this->collisionSheetSize.height())); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 18fbe198..58ce61fe 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1257,10 +1257,10 @@ bool MainWindow::setProjectUI() { // Set a version dependent player icon (or user-chosen icon) for the Events tab. QIcon eventTabIcon; - if (!projectConfig.eventTabIconPath.isEmpty()) { - eventTabIcon = QIcon(projectConfig.eventTabIconPath); + if (!projectConfig.eventsTabIconPath.isEmpty()) { + eventTabIcon = QIcon(project->getExistingFilepath(projectConfig.eventsTabIconPath)); if (eventTabIcon.isNull()) { - logWarn(QString("Failed to load custom Events tab icon '%1'.").arg(projectConfig.eventTabIconPath)); + logWarn(QString("Failed to load custom Events tab icon '%1'.").arg(projectConfig.eventsTabIconPath)); } } if (eventTabIcon.isNull()) { diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 166874ee..de39c09b 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -67,6 +67,7 @@ void ProjectSettingsEditor::connectSignals() { connect(ui->button_BGsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_BGsIcon); }); connect(ui->button_HealLocationsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_HealLocationsIcon); }); connect(ui->button_PokemonIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_PokemonIcon); }); + connect(ui->button_EventsTabIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_EventsTabIcon); }); // Display a warning if a mask value overlaps with another mask in its group. @@ -113,6 +114,20 @@ void ProjectSettingsEditor::initUi() { ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings); ui->comboBox_AttributesSize->addItems({"1", "2", "4"}); + ui->comboBox_EventsTabIcon->addItem("Automatic", ""); + ui->comboBox_EventsTabIcon->addItem("Brendan (Emerald)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeemerald, 0)); + ui->comboBox_EventsTabIcon->addItem("Brendan (R/S)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeruby, 0)); + ui->comboBox_EventsTabIcon->addItem("May (Emerald)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeemerald, 1)); + ui->comboBox_EventsTabIcon->addItem("May (R/S)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeruby, 1)); + ui->comboBox_EventsTabIcon->addItem("Red", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 0)); + ui->comboBox_EventsTabIcon->addItem("Green", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 1)); + ui->comboBox_EventsTabIcon->addItem("Custom", "Custom"); + connect(ui->comboBox_EventsTabIcon, &NoScrollComboBox::currentIndexChanged, [this](int index) { + bool usingCustom = (index == ui->comboBox_EventsTabIcon->findText("Custom")); + ui->lineEdit_EventsTabIcon->setVisible(usingCustom); + ui->button_EventsTabIcon->setVisible(usingCustom); + }); + // Validate that the border metatiles text is a comma-separated list of metatile values static const QString regex_Hex = "(0[xX])?[A-Fa-f0-9]+"; static const QRegularExpression expression_HexList(QString("^(%1,)*%1$").arg(regex_Hex)); // Comma-separated list of hex values @@ -520,6 +535,15 @@ void ProjectSettingsEditor::refresh() { } this->setWarpBehaviorsList(behaviorNames); + int index = ui->comboBox_EventsTabIcon->findData(projectConfig.eventsTabIconPath); + if (index < 0) { + index = ui->comboBox_EventsTabIcon->findData("Custom"); + ui->lineEdit_EventsTabIcon->setText(projectConfig.eventsTabIconPath); + } else { + ui->lineEdit_EventsTabIcon->setText(""); + } + ui->comboBox_EventsTabIcon->setCurrentIndex(index); + this->refreshing = false; // Allow signals } @@ -608,6 +632,16 @@ void ProjectSettingsEditor::save() { for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++) projectConfig.setPokemonIconPath(i.key(), i.value()); + QString eventsTabIconPath; + QVariant data = ui->comboBox_EventsTabIcon->currentData(); + if (data.isValid() && data.canConvert()) { + eventsTabIconPath = data.toString(); + if (eventsTabIconPath == "Custom") { + eventsTabIconPath = ui->lineEdit_EventsTabIcon->text(); + } + } + projectConfig.eventsTabIconPath = eventsTabIconPath; + projectConfig.save(); userConfig.save(); porymapConfig.save(); From 91d89a742242a953aeba3d2a36312820ce75203a Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 1 May 2025 00:29:08 -0400 Subject: [PATCH 7/9] Fix Qt5 build --- src/ui/projectsettingseditor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index de39c09b..fa2d3ee2 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -122,7 +122,7 @@ void ProjectSettingsEditor::initUi() { ui->comboBox_EventsTabIcon->addItem("Red", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 0)); ui->comboBox_EventsTabIcon->addItem("Green", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 1)); ui->comboBox_EventsTabIcon->addItem("Custom", "Custom"); - connect(ui->comboBox_EventsTabIcon, &NoScrollComboBox::currentIndexChanged, [this](int index) { + connect(ui->comboBox_EventsTabIcon, QOverload::of(&NoScrollComboBox::currentIndexChanged), [this](int index) { bool usingCustom = (index == ui->comboBox_EventsTabIcon->findText("Custom")); ui->lineEdit_EventsTabIcon->setVisible(usingCustom); ui->button_EventsTabIcon->setVisible(usingCustom); From e2371eb1e61282400ad79f9031ee728c4dfd6ef3 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 4 May 2025 16:34:43 -0400 Subject: [PATCH 8/9] Remove redundant text change for tileset combo boxes --- src/mainwindow.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 322d6e83..16b24f1e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1192,11 +1192,6 @@ void MainWindow::displayMapProperties() { ui->frame_HeaderData->setEnabled(true); this->mapHeaderForm->setHeader(editor->map->header()); - const QSignalBlocker b_PrimaryTileset(ui->comboBox_PrimaryTileset); - const QSignalBlocker b_SecondaryTileset(ui->comboBox_SecondaryTileset); - ui->comboBox_PrimaryTileset->setTextItem(editor->map->layout()->tileset_primary_label); - ui->comboBox_SecondaryTileset->setTextItem(editor->map->layout()->tileset_secondary_label); - ui->mapCustomAttributesFrame->table()->setAttributes(editor->map->customAttributes()); } From 3a02df50af7a7c36795639864600beccf357343f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 4 May 2025 16:36:34 -0400 Subject: [PATCH 9/9] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d489b17..abdb4d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Primary/secondary metatile images are now kept on separate rows, rather than blending together if the primary size is not divisible by 8. - The prompt to reload the project when a file has changed will now only appear when Porymap is the active application. - `Script` dropdowns now autocomplete only with scripts from the current map, rather than every script in the project. The old behavior is available via a new setting. +- `Script` dropdowns now update automatically if the current map's scripts file is edited. - The options for `Encounter Type` and `Terrain Type` in the Tileset Editor are not hardcoded anymore, they're now read from the project. - The `symbol_wild_encounters` setting was replaced; this value is now read from the project. - The max encounter rate is now read from the project, rather than assuming the default value from RSE.