Merge branch 'dev' of https://github.com/huderlem/porymap into status-bar-log

This commit is contained in:
GriffinR 2025-05-04 18:49:52 -04:00
commit 7511f445bd
22 changed files with 500 additions and 213 deletions

View File

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

View File

@ -39,7 +39,7 @@
<x>0</x>
<y>0</y>
<width>570</width>
<height>692</height>
<height>680</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
@ -94,7 +94,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
@ -164,7 +164,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
@ -248,7 +248,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
@ -1197,23 +1197,62 @@
<x>0</x>
<y>0</y>
<width>570</width>
<height>840</height>
<height>927</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QGroupBox" name="groupBox_EventsTabIcon">
<property name="title">
<string>Tab Icon</string>
</property>
<layout class="QGridLayout" name="gridLayout_12">
<item row="1" column="1">
<widget class="QToolButton" name="button_EventsTabIcon">
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="lineEdit_EventsTabIcon">
<property name="visible">
<bool>false</bool>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The image file path to use for the icon of the Events tab.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="NoScrollComboBox" name="comboBox_EventsTabIcon">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_DefaultIcons">
<property name="title">
<string>Default Icons</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="2" column="0">
<widget class="QLabel" name="label_TriggersIcon">
<property name="text">
<string>Triggers</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_WarpsIcon">
<property name="text">
@ -1221,23 +1260,31 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_WarpsIcon">
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_BGsIcon">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The icon that will be used to represent Warp events&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The icon that will be used to represent BG events&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_HealLocationsIcon">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The icon that will be used to represent Heal Location events&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<item row="4" column="2">
<widget class="QToolButton" name="button_HealLocationsIcon">
<property name="text">
<string>...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
<property name="icon">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_ObjectsIcon">
<property name="text">
<string>Objects</string>
</property>
</widget>
</item>
@ -1248,6 +1295,24 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_TriggersIcon">
<property name="text">
<string>Triggers</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="button_WarpsIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_HealLocationsIcon">
<property name="text">
@ -1265,10 +1330,56 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_ObjectsIcon">
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_WarpsIcon">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The icon that will be used to represent Warp events&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="button_BGsIcon">
<property name="text">
<string>Objects</string>
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_HealLocationsIcon">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The icon that will be used to represent Heal Location events&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="button_TriggersIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="button_ObjectsIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
@ -1282,71 +1393,6 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_BGsIcon">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The icon that will be used to represent BG events&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="button_ObjectsIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="button_WarpsIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="button_TriggersIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="button_BGsIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QToolButton" name="button_HealLocationsIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -1365,7 +1411,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/delete.ico</normaloff>:/icons/delete.ico</iconset>
</property>
</widget>
@ -1418,7 +1464,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/add.ico</normaloff>:/icons/add.ico</iconset>
</property>
</widget>
@ -1628,7 +1674,7 @@
<x>0</x>
<y>0</y>
<width>544</width>
<height>338</height>
<height>341</height>
</rect>
</property>
<layout class="QFormLayout" name="layout_ProjectPaths">
@ -1657,7 +1703,7 @@
<string>Add Global Constants File...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/add.ico</normaloff>:/icons/add.ico</iconset>
</property>
</widget>
@ -1668,7 +1714,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/help.ico</normaloff>:/icons/help.ico</iconset>
</property>
</widget>
@ -1708,7 +1754,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/help.ico</normaloff>:/icons/help.ico</iconset>
</property>
</widget>
@ -1745,7 +1791,7 @@
<x>0</x>
<y>0</y>
<width>544</width>
<height>421</height>
<height>425</height>
</rect>
</property>
<layout class="QFormLayout" name="layout_Identifiers">
@ -1774,7 +1820,7 @@
<string>Add Global Constant...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<iconset>
<normaloff>:/icons/add.ico</normaloff>:/icons/add.ico</iconset>
</property>
</widget>
@ -1840,8 +1886,6 @@
<header location="global">uintspinbox.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/images.qrc"/>
</resources>
<resources/>
<connections/>
</ui>

View File

@ -335,6 +335,7 @@ public:
this->filePaths.clear();
this->eventIconPaths.clear();
this->pokemonIconPaths.clear();
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);
@ -345,6 +346,7 @@ public:
this->unusedTileCovered = 0x0000;
this->unusedTileSplit = 0x0000;
this->maxEventsPerGroup = 255;
this->forcedMajorVersion = 0;
this->globalConstantsFilepaths.clear();
this->globalConstants.clear();
this->identifiers.clear();
@ -355,6 +357,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,11 +419,13 @@ public:
uint16_t unusedTileCovered;
uint16_t unusedTileSplit;
bool mapAllowFlagsEnabled;
QString eventsTabIconPath;
QString collisionSheetPath;
QSize collisionSheetSize;
QMargins playerViewDistance;
QList<uint32_t> warpBehaviors;
int maxEventsPerGroup;
int forcedMajorVersion;
QStringList globalConstantsFilepaths;
QMap<QString,QString> globalConstants;

View File

@ -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<QString> 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<QString> 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<QString> 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<QString> 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<int>(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

View File

@ -13,6 +13,7 @@
#include <QPixmap>
#include <QObject>
#include <QGraphicsPixmapItem>
#include <QFileSystemWatcher>
#include <math.h>
#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<MapConnection*> 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<MapConnection*> m_connections;
QSet<MapConnection*> m_ownedConnections;
QUndoStack *m_editHistory = nullptr;
QPointer<QUndoStack> m_editHistory;
QPointer<QFileSystemWatcher> m_scriptFileWatcher;
void invalidateScripts();
signals:
void modified();
void scriptsModified();
void mapDimensionsChanged(const QSize &size);
void openScriptRequested(QString label);
void connectionAdded(MapConnection*);

View File

@ -351,7 +351,9 @@ private:
void refreshCollisionSelector();
void setLayoutOnlyMode(bool layoutOnly);
bool checkProjectSanity();
bool isInvalidProject(Project *project);
bool checkProjectSanity(Project *project);
bool checkProjectVersion(Project *project);
bool loadProjectData();
bool setProjectUI();
void clearProjectUI();

View File

@ -85,6 +85,7 @@ public:
void clearHealLocations();
bool sanityCheck();
int getSupportedMajorVersion(QString *errorOut = nullptr);
bool load();
Map* loadMap(const QString &mapName);

View File

@ -56,9 +56,11 @@ protected:
bool populated = false;
bool initialized = false;
bool connected = false;
QPointer<Project> 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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -49,9 +49,14 @@
<file>icons/tall_grass.ico</file>
<file>icons/warning.ico</file>
<file>icons/minimap.ico</file>
<file>icons/viewsprites.ico</file>
<file>icons/application_form_edit.ico</file>
<file>icons/connections.ico</file>
<file>icons/player/brendan_em.ico</file>
<file>icons/player/brendan_rs.ico</file>
<file>icons/player/green.ico</file>
<file>icons/player/may_em.ico</file>
<file>icons/player/may_rs.ico</file>
<file>icons/player/red.ico</file>
<file>icons/ui/dark/checkbox_checked_disabled.png</file>
<file>icons/ui/dark/checkbox_checked_disabled@2x.png</file>
<file>icons/ui/dark/checkbox_checked.png</file>

View File

@ -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 == "events_tab_icon_path") {
this->eventsTabIconPath = value;
} else if (key == "collision_sheet_path") {
this->collisionSheetPath = value;
} else if (key == "collision_sheet_width") {
@ -890,6 +918,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));
}
@ -985,6 +1015,7 @@ QMap<QString, QString> 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("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()));
@ -997,6 +1028,7 @@ QMap<QString, QString> 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;
}

View File

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

View File

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

View File

@ -250,7 +250,7 @@ void MainWindow::initCustomUI() {
static const QMap<int, QIcon> 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"))},
@ -683,7 +683,7 @@ bool MainWindow::openProject(QString dir, bool initial) {
// Make sure project looks reasonable before attempting to load it
porysplash->showMessage("Verifying project");
if (!checkProjectSanity()) {
if (isInvalidProject(this->editor->project)) {
delete this->editor->project;
porysplash->stop();
return false;
@ -725,16 +725,20 @@ bool MainWindow::loadProjectData() {
return success;
}
bool MainWindow::checkProjectSanity() {
if (editor->project->sanityCheck())
bool MainWindow::isInvalidProject(Project *project) {
return !(checkProjectSanity(project) && checkProjectVersion(project));
}
bool MainWindow::checkProjectSanity(Project *project) {
if (project->sanityCheck())
return true;
logWarn(QString("The directory '%1' failed the project sanity check.").arg(editor->project->root));
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(editor->project->root));
"(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) {
@ -745,6 +749,43 @@ bool MainWindow::checkProjectSanity() {
return false;
}
bool MainWindow::checkProjectVersion(Project *project) {
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.
QString msg = QStringLiteral("Failed to check project version");
logWarn(error.isEmpty() ? msg : QString("%1: '%2'").arg(msg).arg(error));
} else {
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.
// 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!"), 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())
.arg(porymapVersion.majorVersion()));
auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole);
msgBox.exec();
if (msgBox.clickedButton() != tryAnyway){
return false;
}
// User opted to try with this version anyway. Don't warn them about this version again.
projectConfig.forcedMajorVersion = porymapVersion.majorVersion();
}
}
return true;
}
void MainWindow::showProjectOpenFailure() {
if (!this->isVisible()){
// The main window is not visible during the initial project open; the splash screen is busy providing visual feedback.
@ -1148,11 +1189,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());
}
@ -1266,6 +1302,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.eventsTabIconPath.isEmpty()) {
eventTabIcon = QIcon(project->getExistingFilepath(projectConfig.eventsTabIconPath));
if (eventTabIcon.isNull()) {
logWarn(QString("Failed to load custom Events tab icon '%1'.").arg(projectConfig.eventsTabIconPath));
}
}
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;
}

View File

@ -75,6 +75,117 @@ 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.
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 gitName = "git";
QString gitPath = QStandardPaths::findExecutable(gitName);
if (gitPath.isEmpty()) {
if (errorOut) *errorOut = QString("Unable to locate %1.").arg(gitName);
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 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(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) {
if (errorOut) {
*errorOut = QStringLiteral("Failed to identify commit 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<QString, QList<QPair<int, QString>>> 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, or we got some unexpected result.
if (errorOut) *errorOut = QStringLiteral("Unrecognized commit history");
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 major 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(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
if (errorOut) {
*errorOut = QStringLiteral("Failed to search commit 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;
}
if (process.exitCode() == 0) {
// Identified a supported major version
return versionNum;
}
}
// We recognized the commit history, but it's too old for any version of Porymap to support.
return 0;
}
bool Project::load() {
this->parser.setUpdatesSplashScreen(true);
resetFileCache();

View File

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

View File

@ -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, QOverload<int>::of(&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<QString>()) {
eventsTabIconPath = data.toString();
if (eventsTabIconPath == "Custom") {
eventsTabIconPath = ui->lineEdit_EventsTabIcon->text();
}
}
projectConfig.eventsTabIconPath = eventsTabIconPath;
projectConfig.save();
userConfig.save();
porymapConfig.save();