Merge branch 'dev' of https://github.com/huderlem/porymap into utility

This commit is contained in:
GriffinR 2025-02-26 16:05:04 -05:00
commit 75b8b2c16c
34 changed files with 1139 additions and 1041 deletions

View File

@ -8,8 +8,9 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
## [Unreleased]
### Added
- Redesigned the map list, adding new features including opening/editing layouts with no associated map, duplicating maps or layouts (accessible via right-click), editing the names of map groups, rearranging maps and map groups, and hiding empty folders.
- Redesigned the map list, adding new features including opening/editing layouts with no associated map, editing the names of map groups, rearranging maps and map groups, and hiding empty folders.
- Add a drop-down for changing the layout of the currently opened map.
- Add an option to duplicate maps/layouts.
- Redesigned the Connections tab, adding new features including the option to open or display diving maps and a list UI for easier edit access.
- Add a `Close Project` option
- Add a search button to the `Wild Pokémon` tab that shows the encounter data for a species across all maps.
@ -19,12 +20,13 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Add an option to display a dividing line between tilesets in the Tileset Editor.
- An alert will be displayed when attempting to open a seemingly invalid project.
- Add support for defining project values with `enum` where `#define` was expected.
- Add buttons to hide and show empty folders in each map tree view.
- Add a setting to specify the tile values to use for the unused metatile layer.
- Add a setting to specify the maximum number of events in a group. A warning will be shown if too many events are added.
### Changed
- `Change Dimensions` now has an interactive resizing rectangle.
- Redesigned the new map dialog, including better error checking and a collapsible section for header data.
- New maps/layouts are no longer saved automatically, and can be fully discarded by closing without saving.
- Map groups and ``MAPSEC`` names specified when creating a new map will be added automatically if they don't already exist.
- Edits to map connections now have Undo/Redo and can be viewed in exported timelapses.
- Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions.
@ -42,6 +44,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Porymap will no longer overwrite ``include/constants/map_groups.h`` or ``include/constants/layouts.h``.
- 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.
### Fixed
- Fix `Add Region Map...` not updating the region map settings file.
@ -54,6 +57,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Fix `About porymap` opening a new window each time it's activated.
- Fix the `Edit History` window not raising to the front when reactivated.
- New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded.
- Fix species on the wild pokémon tab retaining icons from previously-opened projects.
- Fix invalid species names clearing from wild pokémon data when revisited.
- Fix editing wild pokémon data not marking the map as unsaved.
- Fix editing an event's `Custom Attributes` not marking the map as unsaved.
@ -86,6 +90,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Fix `Display Metatile Usage Counts` sometimes changing the counts after repeated use.
- The Metatile / Tile usage counts in the Tileset Editor now update to reflect changes.
- Fix regression that stopped the map zoom from centering on the cursor.
- Fix `Open Map Scripts` not working on maps with a `shared_scripts_map` field.
## [5.4.1] - 2024-03-21
### Fixed

View File

@ -2906,6 +2906,7 @@
<addaction name="action_NewMap"/>
<addaction name="action_NewLayout"/>
<addaction name="actionNew_Tileset"/>
<addaction name="actionDuplicate_Current_Map_Layout"/>
<addaction name="separator"/>
<addaction name="actionTileset_Editor"/>
<addaction name="actionRegion_Map_Editor"/>
@ -3278,6 +3279,11 @@
<string>New Layout...</string>
</property>
</action>
<action name="actionDuplicate_Current_Map_Layout">
<property name="text">
<string>Duplicate Current Map/Layout...</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>493</height>
<width>530</width>
<height>432</height>
</rect>
</property>
<property name="windowTitle">
@ -19,244 +19,302 @@
<number>9</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Miscellaneous</string>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="checkBox_MonitorProjectFiles">
<property name="toolTip">
<string>If checked, a prompt to reload your project will appear if relevant project files are edited</string>
</property>
<property name="text">
<string>Monitor project files</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_OpenRecentProject">
<property name="toolTip">
<string>If checked, Porymap will automatically open your most recently opened project on startup</string>
</property>
<property name="text">
<string>Open recent project on launch</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_CheckForUpdates">
<property name="toolTip">
<string>If checked, Porymap will automatically alert you on startup if a new release is available</string>
</property>
<property name="text">
<string>Automatically check for updates</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_DisableEventWarning">
<property name="toolTip">
<string>If checked, no warning will be shown when deleting an event that has an associated #define that may also be deleted.</string>
</property>
<property name="text">
<string>Disable warning when deleting events with IDs</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_EventSelectionMode">
<property name="title">
<string>Event Selection Mode</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_EventSelectionMode">
<item>
<widget class="QRadioButton" name="radioButton_OnSprite">
<property name="toolTip">
<string>If enabled, an event can be selected by clicking directly on the opaque pixels of its sprite. This may be preferable when events are overlapping.</string>
</property>
<property name="text">
<string>Select by clicking on sprite</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButton_WithinRect">
<property name="toolTip">
<string>If enabled, an event can be selected by clicking anywhere within its sprite dimensions. This may be preferable for events with small or mostly transparent sprites.</string>
</property>
<property name="text">
<string>Select by clicking within bounding rectangle</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Themes">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Application Theme</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_TextEditor">
<property name="title">
<string>Preferred Text Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="2" column="0">
<widget class="QScrollArea" name="scrollArea_TextEditor">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_TextEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>492</width>
<height>327</height>
</rect>
<widget class="QWidget" name="tab_General">
<attribute name="title">
<string>General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<widget class="QCheckBox" name="checkBox_MonitorProjectFiles">
<property name="toolTip">
<string>If checked, a prompt to reload your project will appear if relevant project files are edited</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="label_TextEditorGotoLineHelp">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When this command is set a button will appear next to the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Script&lt;/span&gt; combo-box in the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Events&lt;/span&gt; tab which executes this command.&lt;span style=&quot; font-weight:600;&quot;&gt; %F&lt;/span&gt; will be substituted with the file path of the script and &lt;span style=&quot; font-weight:600;&quot;&gt;%L&lt;/span&gt; will be substituted with the line number of the script in that file. &lt;span style=&quot; font-weight:600;&quot;&gt;%F &lt;/span&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;must&lt;/span&gt; be given if &lt;span style=&quot; font-weight:600;&quot;&gt;%L&lt;/span&gt; is given. If &lt;span style=&quot; font-weight:600;&quot;&gt;%F&lt;/span&gt; is &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; given then the script's file path will be added to the end of the command. If the script can't be found then the current map's scripts file is opened.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_TextEditorGotoLine">
<property name="text">
<string>Goto Line Command</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_TextEditorOpenFolder">
<property name="text">
<string>Monitor project files</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_OpenRecentProject">
<property name="toolTip">
<string>If checked, Porymap will automatically open your most recently opened project on startup</string>
</property>
<property name="text">
<string>Open recent project on launch</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_CheckForUpdates">
<property name="toolTip">
<string>If checked, Porymap will automatically alert you on startup if a new release is available</string>
</property>
<property name="text">
<string>Automatically check for updates</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Themes">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Application Theme</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_Events">
<attribute name="title">
<string>Events</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<widget class="QCheckBox" name="checkBox_DisableEventWarning">
<property name="toolTip">
<string>If checked, no warning will be shown when deleting an event that has an associated #define that may also be deleted.</string>
</property>
<property name="text">
<string>Disable warning when deleting events with IDs</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_AutocompleteAllScripts">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, the list of suggestions when typing in an Event's Script field will include all global script labels in the project. Enabling this setting will make Porymap's startup slower.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Autocomplete Script labels using all possible scripts</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_EventSelectionMode">
<property name="title">
<string>Selection Mode</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_EventSelectionMode">
<item>
<widget class="QRadioButton" name="radioButton_OnSprite">
<property name="toolTip">
<string>The shell command for your preferred text editor (possibly an absolute path if the program doesn't exist in your PATH).</string>
<string>If enabled, an event can be selected by clicking directly on the opaque pixels of its sprite. This may be preferable when events are overlapping.</string>
</property>
<property name="placeholderText">
<string>e.g. code %D</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_TextEditorOpenFolderHelp">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is the command that is executed when clicking &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Open Project in Text Editor&lt;/span&gt; in the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Tools&lt;/span&gt; menu. &lt;span style=&quot; font-weight:600;&quot;&gt;%D&lt;/span&gt; will be substituted with the project's root directory. If &lt;span style=&quot; font-weight:600;&quot;&gt;%D&lt;/span&gt; is &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; specified then the project directory will be added to the end of the command.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
<string>Select by clicking on sprite</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_TextEditorOpenFolder">
<property name="text">
<string>Open Directory Command</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_TextEditorGotoLine">
<item>
<widget class="QRadioButton" name="radioButton_WithinRect">
<property name="toolTip">
<string>The shell command for your preferred text editor to open a file to a specific line number (possibly an absolute path if the program doesn't exist in your PATH).</string>
<string>If enabled, an event can be selected by clicking anywhere within its sprite dimensions. This may be preferable for events with small or mostly transparent sprites.</string>
</property>
<property name="placeholderText">
<string>e.g. code --goto %F:%L</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
<property name="text">
<string>Select by clicking within bounding rectangle</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Text Editor</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea_TextEditor">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_TextEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>476</width>
<height>343</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetMinimumSize</enum>
</property>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="label_TextEditorGotoLineHelp">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When this command is set a button will appear next to the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Script&lt;/span&gt; combo-box in the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Events&lt;/span&gt; tab which executes this command.&lt;span style=&quot; font-weight:600;&quot;&gt; %F&lt;/span&gt; will be substituted with the file path of the script and &lt;span style=&quot; font-weight:600;&quot;&gt;%L&lt;/span&gt; will be substituted with the line number of the script in that file. &lt;span style=&quot; font-weight:600;&quot;&gt;%F &lt;/span&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;must&lt;/span&gt; be given if &lt;span style=&quot; font-weight:600;&quot;&gt;%L&lt;/span&gt; is given. If &lt;span style=&quot; font-weight:600;&quot;&gt;%F&lt;/span&gt; is &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; given then the script's file path will be added to the end of the command. If the script can't be found then the current map's scripts file is opened.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_TextEditorGotoLine">
<property name="text">
<string>Goto Line Command</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_TextEditorOpenFolder">
<property name="toolTip">
<string>The shell command for your preferred text editor (possibly an absolute path if the program doesn't exist in your PATH).</string>
</property>
<property name="placeholderText">
<string>e.g. code %D</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="label_TextEditorOpenFolderHelp">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is the command that is executed when clicking &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Open Project in Text Editor&lt;/span&gt; in the &lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Tools&lt;/span&gt; menu. &lt;span style=&quot; font-weight:600;&quot;&gt;%D&lt;/span&gt; will be substituted with the project's root directory. If &lt;span style=&quot; font-weight:600;&quot;&gt;%D&lt;/span&gt; is &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; specified then the project directory will be added to the end of the command.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_TextEditorOpenFolder">
<property name="text">
<string>Open Directory Command</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_TextEditorGotoLine">
<property name="toolTip">
<string>The shell command for your preferred text editor to open a file to a specific line number (possibly an absolute path if the program doesn't exist in your PATH).</string>
</property>
<property name="placeholderText">
<string>e.g. code --goto %F:%L</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
<set>QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>

View File

@ -1341,6 +1341,29 @@
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_MaxEvents" native="true">
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_MaxEvents">
<property name="text">
<string>Maximum Events per Event group</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="NoScrollSpinBox" name="spinBox_MaxEvents">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Maps cannot have more than this number of events in each event group. Object events are additionally limited by 'define_obj_event_count' on the Identifiers tab.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Events">
<property name="title">

View File

@ -79,6 +79,7 @@ public:
this->textEditorGotoLine = "";
this->paletteEditorBitDepth = 24;
this->projectSettingsTab = 0;
this->loadAllEventScripts = false;
this->warpBehaviorWarningDisabled = false;
this->eventDeleteWarningDisabled = false;
this->eventOverlayEnabled = false;
@ -87,6 +88,7 @@ public:
this->lastUpdateCheckVersion = porymapVersion;
this->rateLimitTimes.clear();
this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape;
this->shownInGameReloadMessage = false;
}
void addRecentProject(QString project);
void setRecentProjects(QStringList projects);
@ -135,6 +137,7 @@ public:
QString textEditorGotoLine;
int paletteEditorBitDepth;
int projectSettingsTab;
bool loadAllEventScripts;
bool warpBehaviorWarningDisabled;
bool eventDeleteWarningDisabled;
bool eventOverlayEnabled;
@ -146,6 +149,7 @@ public:
QByteArray wildMonChartGeometry;
QByteArray newMapDialogGeometry;
QByteArray newLayoutDialogGeometry;
bool shownInGameReloadMessage;
protected:
virtual QString getConfigFilepath() override;
@ -320,6 +324,7 @@ public:
this->unusedTileNormal = 0x3014;
this->unusedTileCovered = 0x0000;
this->unusedTileSplit = 0x0000;
this->maxEventsPerGroup = 255;
this->identifiers.clear();
this->readKeys.clear();
}
@ -389,6 +394,7 @@ public:
int collisionSheetWidth;
int collisionSheetHeight;
QList<uint32_t> warpBehaviors;
int maxEventsPerGroup;
protected:
virtual QString getConfigFilepath() override;

View File

@ -41,14 +41,6 @@ public:
virtual void visitSign(SignEvent *) = 0;
};
struct EventGraphics
{
QImage spritesheet;
int spriteWidth;
int spriteHeight;
bool inanimate;
};
///
/// Event base class -- purely virtual
@ -64,11 +56,7 @@ public:
Event& operator=(const Event &other) = delete;
protected:
Event() {
this->spriteWidth = 16;
this->spriteHeight = 16;
this->usingSprite = false;
}
Event() {}
// public enums & static methods
public:
@ -119,8 +107,6 @@ public:
static Event* create(Event::Type type);
static QMap<Event::Group, const QPixmap*> icons;
// standard public methods
public:
@ -143,8 +129,8 @@ public:
int getZ() const { return this->elevation; }
int getElevation() const { return this->elevation; }
int getPixelX() const { return (this->x * 16) - qMax(0, (this->spriteWidth - 16) / 2); }
int getPixelY() const { return (this->y * 16) - qMax(0, this->spriteHeight - 16); }
int getPixelX() const { return (this->x * 16) - qMax(0, (pixmap.width() - 16) / 2); }
int getPixelY() const { return (this->y * 16) - qMax(0, pixmap.height() - 16); }
virtual EventFrame *getEventFrame();
virtual EventFrame *createEventFrame() = 0;
@ -172,25 +158,17 @@ public:
void setPixmapItem(DraggablePixmapItem *item);
DraggablePixmapItem *getPixmapItem() const { return this->pixmapItem; }
void setUsingSprite(bool newUsingSprite) { this->usingSprite = newUsingSprite; }
bool getUsingSprite() const { return this->usingSprite; }
void setSpriteWidth(int newSpriteWidth) { this->spriteWidth = newSpriteWidth; }
int getspriteWidth() const { return this->spriteWidth; }
void setSpriteHeight(int newSpriteHeight) { this->spriteHeight = newSpriteHeight; }
int getspriteHeight() const { return this->spriteHeight; }
void setUsesDefaultPixmap(bool newUsesDefaultPixmap) { this->usesDefaultPixmap = newUsesDefaultPixmap; }
bool getUsesDefaultPixmap() const { return this->usesDefaultPixmap; }
int getEventIndex();
void setIdName(QString newIdName) { this->idName = newIdName; }
QString getIdName() const { return this->idName; }
static QString eventGroupToString(Event::Group group);
static QString eventTypeToString(Event::Type type);
static Event::Type eventTypeFromString(QString type);
static void clearIcons();
static void setIcons();
static QString groupToString(Event::Group group);
static QString typeToString(Event::Type type);
static Event::Type typeFromString(QString type);
// protected attributes
protected:
@ -204,9 +182,7 @@ protected:
int y = 0;
int elevation = 0;
int spriteWidth = 16;
int spriteHeight = 16;
bool usingSprite = false;
bool usesDefaultPixmap = true;
// Some events can have an associated #define name that should be unique to this event.
// e.g. object events can have a 'LOCALID', or Heal Locations have a 'HEAL_LOCATION' id.
@ -273,10 +249,6 @@ public:
void setFlag(QString newFlag) { this->flag = newFlag; }
QString getFlag() const { return this->flag; }
public:
void setFrameFromMovement(QString movement);
void setPixmapFromSpritesheet(EventGraphics * gfx);
protected:
QString gfx;

View File

@ -86,6 +86,7 @@ public:
void removeEvent(Event *);
void addEvent(Event *);
int getIndexOfEvent(Event *) const;
bool hasEvent(Event *) const;
void deleteConnections();
QList<MapConnection*> getConnections() const;

View File

@ -26,6 +26,7 @@ public:
QString id;
QString name;
QString newFolderPath;
int width;
int height;

View File

@ -44,25 +44,24 @@ class ParseUtil
public:
ParseUtil();
void set_root(const QString &dir);
static QString readTextFile(const QString &path);
static QString readTextFile(const QString &path, QString *error = nullptr);
void invalidateTextFile(const QString &path);
static int textFileLineCount(const QString &path);
QList<QStringList> parseAsm(const QString &filename);
QStringList readCArray(const QString &filename, const QString &label);
QMap<QString, QStringList> readCArrayMulti(const QString &filename);
QMap<QString, QString> readNamedIndexCArray(const QString &text, const QString &label);
QMap<QString, QString> readNamedIndexCArray(const QString &text, const QString &label, QString *error = nullptr);
QString readCIncbin(const QString &text, const QString &label);
QMap<QString, QString> readCIncbinMulti(const QString &filepath);
QStringList readCIncbinArray(const QString &filename, const QString &label);
QMap<QString, int> readCDefinesByRegex(const QString &filename, const QStringList &regexList);
QMap<QString, int> readCDefinesByName(const QString &filename, const QStringList &names);
QStringList readCDefineNames(const QString &filename, const QStringList &regexList);
QMap<QString, int> readCDefinesByRegex(const QString &filename, const QStringList &regexList, QString *error = nullptr);
QMap<QString, int> readCDefinesByName(const QString &filename, const QStringList &names, QString *error = nullptr);
QStringList readCDefineNames(const QString &filename, const QStringList &regexList, QString *error = nullptr);
tsl::ordered_map<QString, QHash<QString, QString>> readCStructs(const QString &, const QString & = "", const QHash<int, QString>& = {});
QList<QStringList> getLabelMacros(const QList<QStringList>&, const QString&);
QStringList getLabelValues(const QList<QStringList>&, const QString&);
bool tryParseJsonFile(QJsonDocument *out, const QString &filepath);
bool tryParseOrderedJsonFile(poryjson::Json::object *out, const QString &filepath);
bool ensureFieldsExist(const QJsonObject &obj, const QList<QString> &fields);
bool tryParseJsonFile(QJsonDocument *out, const QString &filepath, QString *error = nullptr);
bool tryParseOrderedJsonFile(poryjson::Json::object *out, const QString &filepath, QString *error = nullptr);
// Returns the 1-indexed line number for the definition of scriptLabel in the scripts file at filePath.
// Returns 0 if a definition for scriptLabel cannot be found.
@ -102,8 +101,8 @@ private:
QMap<QString,QString> expressions; // Map of all define names encountered to their expressions
QStringList filteredNames; // List of define names that matched the search text, in the order that they were encountered
};
ParsedDefines readCDefines(const QString &filename, const QStringList &filterList, bool useRegex);
QMap<QString, int> evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex);
ParsedDefines readCDefines(const QString &filename, const QStringList &filterList, bool useRegex, QString *error);
QMap<QString, int> evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex, QString *error);
bool defineNameMatchesFilter(const QString &name, const QStringList &filterList) const;
bool defineNameMatchesFilter(const QString &name, const QList<QRegularExpression> &filterList) const;

View File

@ -57,9 +57,8 @@ public:
GridSettings gridSettings;
void setProject(Project * project);
void save();
void saveProject();
void saveUiFields();
void saveAll();
void saveCurrent();
void saveEncounterTabData();
void closeProject();
@ -110,15 +109,14 @@ public:
DraggablePixmapItem *addEventPixmapItem(Event *event);
void removeEventPixmapItem(Event *event);
bool eventLimitReached(Map *, Event::Type);
void selectMapEvent(DraggablePixmapItem *item, bool toggle = false);
DraggablePixmapItem *addNewEvent(Event::Type type);
void updateSelectedEvents();
bool canAddEvents(const QList<Event*> &events);
void selectMapEvent(Event *event, bool toggle = false);
Event *addNewEvent(Event::Type type);
void updateEvents();
void duplicateSelectedEvents();
void redrawAllEvents();
void redrawEvents(const QList<Event*> &events);
void redrawEventPixmapItem(DraggablePixmapItem *item);
QList<DraggablePixmapItem *> getEventPixmapItems();
qreal getEventOpacity(const Event *event) const;
void updateCursorRectPos(int x, int y);
@ -153,7 +151,7 @@ public:
CurrentSelectedMetatilesPixmapItem *current_metatile_selection_item = nullptr;
QPointer<MovementPermissionsSelector> movement_permissions_selector_item = nullptr;
QList<DraggablePixmapItem *> *selected_events = nullptr;
QList<Event*> selectedEvents;
QPointer<ConnectionPixmapItem> selected_connection_item = nullptr;
QPointer<MapConnection> connection_to_select = nullptr;
@ -185,7 +183,6 @@ public:
void shouldReselectEvents();
void scaleMapView(int);
static void openInTextEditor(const QString &path, int lineNum = 0);
bool eventLimitReached(Event::Type type);
void setCollisionGraphics();
public slots:
@ -204,6 +201,7 @@ private:
EditMode editMode = EditMode::None;
void save(bool currentOnly);
void clearMap();
void clearMetatileSelector();
void clearMovementPermissionSelector();

View File

@ -182,15 +182,15 @@ public:
// Parse. If parse fails, return Json() and assign an error message to err.
static Json parse(const QString & in,
QString & err,
QString * err = nullptr,
JsonParse strategy = JsonParse::STANDARD);
static Json parse(const char * in,
QString & err,
QString * err = nullptr,
JsonParse strategy = JsonParse::STANDARD) {
if (in) {
return parse(QString(in), err, strategy);
} else {
err = "null input";
if (err) *err = "null input";
return nullptr;
}
}

View File

@ -175,6 +175,7 @@ private slots:
void on_action_Reload_Project_triggered();
void on_action_Close_Project_triggered();
void on_action_Save_Project_triggered();
void save(bool currentOnly = false);
void openWarpMap(QString map_name, int event_id, Event::Group event_group);
@ -218,7 +219,6 @@ private slots:
void on_actionMove_triggered();
void on_actionMap_Shift_triggered();
void addNewEvent(Event::Type type);
void tryAddEventTab(QWidget * tab);
void displayEventTabs();
void updateSelectedEvents();
@ -333,7 +333,7 @@ private:
MapHeaderForm *mapHeaderForm = nullptr;
QMap<Event::Group, DraggablePixmapItem*> lastSelectedEvent;
QMap<Event::Group, Event*> lastSelectedEvent;
bool isProgrammaticEventTabChange;
@ -361,6 +361,7 @@ private:
NewLayoutDialog* createNewLayoutDialog(const Layout *layoutToCopy = nullptr);
void openNewLayoutDialog();
void openDuplicateLayoutDialog(const QString &layoutId);
void openDuplicateMapOrLayoutDialog();
void openNewMapGroupDialog();
void openNewLocationDialog();
void openSubWindow(QWidget * window);

View File

@ -45,7 +45,6 @@ public:
QStringList layoutIdsMaster;
QMap<QString, Layout*> mapLayouts;
QMap<QString, Layout*> mapLayoutsMaster;
QMap<QString, EventGraphics*> eventGraphicsMap;
QMap<QString, int> gfxDefines;
QString defaultSong;
QStringList songNames;
@ -68,7 +67,6 @@ public:
QMap<QString, uint16_t> unusedMetatileLabels;
QMap<QString, uint32_t> metatileBehaviorMap;
QMap<uint32_t, QString> metatileBehaviorMapInverse;
QMap<QString, QString> facingDirections;
ParseUtil parser;
QFileSystemWatcher fileWatcher;
QSet<QString> modifiedFiles;
@ -157,6 +155,7 @@ public:
void initTopLevelMapFields();
bool readMapJson(const QString &mapName, QJsonDocument * out);
bool loadMapEvent(Map *map, const QJsonObject &json, Event::Type defaultType = Event::Type::None);
bool loadMapData(Map*);
bool readMapLayouts();
Layout *loadLayout(QString layoutId);
@ -167,13 +166,13 @@ public:
void loadTilesetMetatileLabels(Tileset*);
void readTilesetPaths(Tileset* tileset);
void saveAll();
void saveGlobalData();
void saveLayout(Layout *);
void saveLayoutBlockdata(Layout *);
void saveLayoutBorder(Layout *);
void writeBlockdata(QString, const Blockdata &);
void saveAllMaps();
void saveMap(Map *);
void saveAllDataStructures();
void saveMap(Map *map, bool skipLayout = false);
void saveConfig();
void saveMapLayouts();
void saveMapGroups();
@ -209,7 +208,10 @@ public:
bool readFieldmapMasks();
QMap<QString, QMap<QString, QString>> readObjEventGfxInfo();
void setEventPixmap(Event *event, bool forceLoad = false);
QPixmap getEventPixmap(const QString &gfxName, const QString &movementName);
QPixmap getEventPixmap(const QString &gfxName, int frame, bool hFlip);
QPixmap getEventPixmap(Event::Group group);
void loadEventPixmap(Event *event, bool forceLoad = false);
QString fixPalettePath(QString path);
QString fixGraphicPath(QString path);
@ -217,6 +219,7 @@ public:
static QString getScriptFileExtension(bool usePoryScript);
QString getScriptDefaultString(bool usePoryScript, QString mapName) const;
QStringList getEventScriptsFilePaths() const;
void insertGlobalScriptLabels(QStringList &scriptLabels) const;
QString getDefaultPrimaryTilesetLabel() const;
QString getDefaultSecondaryTilesetLabel() const;
@ -244,13 +247,25 @@ public:
static int getMapDataSize(int width, int height);
static bool mapDimensionsValid(int width, int height);
bool calculateDefaultMapSize();
static int getMaxObjectEvents();
int getMaxEvents(Event::Group group);
static QString getEmptyMapsecName();
static QString getMapGroupPrefix();
private:
QMap<QString, QString> mapSectionDisplayNames;
QMap<QString, qint64> modifiedFileTimestamps;
QMap<QString, QString> facingDirections;
struct EventGraphics
{
QString filepath;
bool loaded = false;
QImage spritesheet;
int spriteWidth = -1;
int spriteHeight = -1;
bool inanimate = false;
};
QMap<QString, EventGraphics*> eventGraphicsMap;
void updateLayout(Layout *);
@ -260,6 +275,8 @@ private:
void ignoreWatchedFileTemporarily(QString filepath);
void recordFileChange(const QString &filepath);
int maxEventsPerGroup;
int maxObjectEvents;
static int num_tiles_primary;
static int num_tiles_total;
static int num_metatiles_primary;
@ -267,7 +284,6 @@ private:
static int num_pals_total;
static int max_map_data_size;
static int default_map_dimension;
static int max_object_events;
signals:
void fileChanged(const QString &filepath);
@ -280,6 +296,7 @@ signals:
void mapSectionDisplayNameChanged(const QString &idName, const QString &displayName);
void mapSectionIdNamesChanged(const QStringList &idNames);
void mapsExcluded(const QStringList &excludedMapNames);
void eventScriptLabelsRead();
};
#endif // PROJECT_H

View File

@ -31,8 +31,6 @@ public:
void invalidateUi();
void invalidateValues();
void populateScriptDropdown(NoScrollComboBox * combo, Project * project);
virtual void setActive(bool active);
public:
@ -59,6 +57,8 @@ protected:
bool initialized = false;
bool connected = false;
void populateScriptDropdown(NoScrollComboBox * combo, Project * project);
private:
Event *event;
};

View File

@ -23,6 +23,7 @@ public:
signals:
void preferencesSaved();
void themeChanged(const QString &theme);
void scriptSettingsChanged(bool on);
private:
Ui::PreferenceEditor *ui;

View File

@ -391,6 +391,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
}
} else if (key == "project_settings_tab") {
this->projectSettingsTab = getConfigInteger(key, value, 0);
} else if (key == "load_all_event_scripts") {
this->loadAllEventScripts = getConfigBool(key, value);
} else if (key == "warp_behavior_warning_disabled") {
this->warpBehaviorWarningDisabled = getConfigBool(key, value);
} else if (key == "event_delete_warning_disabled") {
@ -423,6 +425,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
} else {
logWarn(QString("Invalid config value for %1: '%2'. Must be 'mask' or 'bounding_rect'.").arg(key).arg(value));
}
} else if (key == "shown_in_game_reload_message") {
this->shownInGameReloadMessage = getConfigBool(key, value);
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
@ -480,6 +484,7 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("text_editor_goto_line", this->textEditorGotoLine);
map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth));
map.insert("project_settings_tab", QString::number(this->projectSettingsTab));
map.insert("load_all_event_scripts", QString::number(this->loadAllEventScripts));
map.insert("warp_behavior_warning_disabled", QString::number(this->warpBehaviorWarningDisabled));
map.insert("event_delete_warning_disabled", QString::number(this->eventDeleteWarningDisabled));
map.insert("event_overlay_enabled", QString::number(this->eventOverlayEnabled));
@ -493,6 +498,7 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("rate_limit_time/" + i.key().toString(), time.toUTC().toString());
}
map.insert("event_selection_shape_mode", (this->eventSelectionShapeMode == QGraphicsPixmapItem::MaskShape) ? "mask" : "bounding_rect");
map.insert("shown_in_game_reload_message", this->shownInGameReloadMessage ? "1" : "0");
return map;
}
@ -804,6 +810,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
const QStringList behaviorList = value.split(",", Qt::SkipEmptyParts);
for (auto s : behaviorList)
this->warpBehaviors.append(getConfigUint32(key, s));
} else if (key == "max_events_per_group") {
this->maxEventsPerGroup = getConfigInteger(key, value, 1, INT_MAX, 255);
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
@ -899,6 +907,7 @@ QMap<QString, QString> ProjectConfig::getKeyValueMap() {
for (const auto &value : this->warpBehaviors)
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));
return map;
}

View File

@ -328,10 +328,7 @@ void EventCreate::redo() {
map->addEvent(event);
editor->addEventPixmapItem(event);
// select this event
editor->selected_events->clear();
editor->selectMapEvent(event->getPixmapItem());
editor->selectMapEvent(event);
}
void EventCreate::undo() {
@ -374,9 +371,9 @@ void EventDelete::redo() {
editor->removeEventPixmapItem(event);
}
editor->selected_events->clear();
editor->selectedEvents.clear();
if (nextSelectedEvent)
editor->selected_events->append(nextSelectedEvent->getPixmapItem());
editor->selectedEvents.append(nextSelectedEvent);
editor->shouldReselectEvents();
}
@ -386,11 +383,7 @@ void EventDelete::undo() {
editor->addEventPixmapItem(event);
}
// select these events
editor->selected_events->clear();
for (Event *event : selectedEvents) {
editor->selected_events->append(event->getPixmapItem());
}
editor->selectedEvents = selectedEvents;
editor->shouldReselectEvents();
QUndoCommand::undo();
@ -427,11 +420,7 @@ void EventDuplicate::redo() {
editor->addEventPixmapItem(event);
}
// select these events
editor->selected_events->clear();
for (Event *event : selectedEvents) {
editor->selected_events->append(event->getPixmapItem());
}
editor->selectedEvents = selectedEvents;
editor->shouldReselectEvents();
}

View File

@ -4,8 +4,6 @@
#include "project.h"
#include "config.h"
QMap<Event::Group, const QPixmap*> Event::icons;
Event* Event::create(Event::Type type) {
switch (type) {
case Event::Type::Object: return new ObjectEvent();
@ -75,112 +73,43 @@ void Event::modify() {
this->map->modify();
}
QString Event::eventGroupToString(Event::Group group) {
switch (group) {
case Event::Group::Object:
return "Object";
case Event::Group::Warp:
return "Warp";
case Event::Group::Coord:
return "Trigger";
case Event::Group::Bg:
return "BG";
case Event::Group::Heal:
return "Heal Location";
default:
return "";
}
const QMap<Event::Group, QString> groupToStringMap = {
{Event::Group::Object, "Object"},
{Event::Group::Warp, "Warp"},
{Event::Group::Coord, "Trigger"},
{Event::Group::Bg, "BG"},
{Event::Group::Heal, "Heal Location"},
};
QString Event::groupToString(Event::Group group) {
return groupToStringMap.value(group);
}
QString Event::eventTypeToString(Event::Type type) {
switch (type) {
case Event::Type::Object:
return "event_object";
case Event::Type::CloneObject:
return "event_clone_object";
case Event::Type::Warp:
return "event_warp";
case Event::Type::Trigger:
return "event_trigger";
case Event::Type::WeatherTrigger:
return "event_weather_trigger";
case Event::Type::Sign:
return "event_sign";
case Event::Type::HiddenItem:
return "event_hidden_item";
case Event::Type::SecretBase:
return "event_secret_base";
case Event::Type::HealLocation:
return "event_heal_location";
default:
return "";
}
const QMap<Event::Type, QString> typeToStringMap = {
{Event::Type::Object, "object"},
{Event::Type::CloneObject, "clone_object"},
{Event::Type::Warp, "warp"},
{Event::Type::Trigger, "trigger"},
{Event::Type::WeatherTrigger, "weather"},
{Event::Type::Sign, "sign"},
{Event::Type::HiddenItem, "hidden_item"},
{Event::Type::SecretBase, "secret_base"},
{Event::Type::HealLocation, "heal_location"},
};
QString Event::typeToString(Event::Type type) {
return typeToStringMap.value(type);
}
Event::Type Event::eventTypeFromString(QString type) {
if (type == "event_object") {
return Event::Type::Object;
} else if (type == "event_clone_object") {
return Event::Type::CloneObject;
} else if (type == "event_warp") {
return Event::Type::Warp;
} else if (type == "event_trigger") {
return Event::Type::Trigger;
} else if (type == "event_weather_trigger") {
return Event::Type::WeatherTrigger;
} else if (type == "event_sign") {
return Event::Type::Sign;
} else if (type == "event_hidden_item") {
return Event::Type::HiddenItem;
} else if (type == "event_secret_base") {
return Event::Type::SecretBase;
} else if (type == "event_heal_location") {
return Event::Type::HealLocation;
} else {
return Event::Type::None;
}
Event::Type Event::typeFromString(QString type) {
return typeToStringMap.key(type, Event::Type::None);
}
void Event::loadPixmap(Project *) {
const QPixmap * pixmap = Event::icons.value(this->getEventGroup());
this->pixmap = pixmap ? *pixmap : QPixmap();
void Event::loadPixmap(Project *project) {
this->pixmap = project->getEventPixmap(this->getEventGroup());
this->usesDefaultPixmap = true;
}
void Event::clearIcons() {
qDeleteAll(icons);
icons.clear();
}
void Event::setIcons() {
clearIcons();
const int w = 16;
const int h = 16;
static const QPixmap defaultIcons = QPixmap(":/images/Entities_16x16.png");
// Custom event icons may be provided by the user.
const int numIcons = qMin(defaultIcons.width() / w, static_cast<int>(Event::Group::None));
for (int i = 0; i < numIcons; i++) {
Event::Group group = static_cast<Event::Group>(i);
QString customIconPath = projectConfig.getEventIconPath(group);
if (customIconPath.isEmpty()) {
// No custom icon specified, use the default icon.
icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h));
continue;
}
// Try to load custom icon
QString validPath = Project::getExistingFilepath(customIconPath);
if (!validPath.isEmpty()) customIconPath = validPath; // Otherwise allow it to fail with the original path
const QPixmap customIcon = QPixmap(customIconPath);
if (customIcon.isNull()) {
// Custom icon failed to load, use the default icon.
icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h));
logWarn(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath));
} else {
icons[group] = new QPixmap(customIcon.scaled(w, h));
}
}
}
Event *ObjectEvent::duplicate() const {
@ -189,6 +118,7 @@ Event *ObjectEvent::duplicate() const {
copy->setX(this->getX());
copy->setY(this->getY());
copy->setElevation(this->getElevation());
copy->setIdName(this->getIdName());
copy->setGfx(this->getGfx());
copy->setMovement(this->getMovement());
copy->setRadiusX(this->getRadiusX());
@ -216,6 +146,9 @@ OrderedJson::object ObjectEvent::buildEventJson(Project *) {
if (projectConfig.eventCloneObjectEnabled) {
objectJson["type"] = "object";
}
QString idName = this->getIdName();
if (!idName.isEmpty())
objectJson["local_id"] = idName;
objectJson["graphics_id"] = this->getGfx();
objectJson["x"] = this->getX();
objectJson["y"] = this->getY();
@ -236,6 +169,7 @@ bool ObjectEvent::loadFromJson(const QJsonObject &json, Project *) {
this->setX(ParseUtil::jsonToInt(json["x"]));
this->setY(ParseUtil::jsonToInt(json["y"]));
this->setElevation(ParseUtil::jsonToInt(json["elevation"]));
this->setIdName(ParseUtil::jsonToQString(json["local_id"]));
this->setGfx(ParseUtil::jsonToQString(json["graphics_id"]));
this->setMovement(ParseUtil::jsonToQString(json["movement_type"]));
this->setRadiusX(ParseUtil::jsonToInt(json["movement_range_x"]));
@ -259,10 +193,10 @@ void ObjectEvent::setDefaultValues(Project *project) {
this->setRadiusX(0);
this->setRadiusY(0);
this->setSightRadiusBerryTreeID("0");
this->setFrameFromMovement(project->facingDirections.value(this->getMovement()));
}
const QSet<QString> expectedObjectFields = {
"local_id",
"graphics_id",
"elevation",
"movement_type",
@ -285,78 +219,11 @@ QSet<QString> ObjectEvent::getExpectedFields() {
}
void ObjectEvent::loadPixmap(Project *project) {
EventGraphics *eventGfx = project->eventGraphicsMap.value(this->gfx, nullptr);
if (!eventGfx) {
// Invalid gfx constant.
// If this is a number, try to use that instead.
bool ok;
int altGfx = ParseUtil::gameStringToInt(this->gfx, &ok);
if (ok && (altGfx < project->gfxDefines.count())) {
eventGfx = project->eventGraphicsMap.value(project->gfxDefines.key(altGfx, "NULL"), nullptr);
}
}
if (!eventGfx || eventGfx->spritesheet.isNull()) {
// No sprite associated with this gfx constant.
// Use default sprite instead.
this->pixmap = project->getEventPixmap(this->gfx, this->movement);
if (!this->pixmap.isNull()) {
this->usesDefaultPixmap = false;
} else {
Event::loadPixmap(project);
this->spriteWidth = 16;
this->spriteHeight = 16;
this->usingSprite = false;
} else {
this->setFrameFromMovement(project->facingDirections.value(this->movement));
this->setPixmapFromSpritesheet(eventGfx);
}
}
void ObjectEvent::setPixmapFromSpritesheet(EventGraphics * gfx)
{
QImage img;
if (gfx->inanimate) {
img = gfx->spritesheet.copy(0, 0, gfx->spriteWidth, gfx->spriteHeight);
} else {
int x = 0;
int y = 0;
// Get frame's position in spritesheet.
// Assume horizontal layout. If position would exceed sheet width, try vertical layout.
if ((this->frame + 1) * gfx->spriteWidth <= gfx->spritesheet.width()) {
x = this->frame * gfx->spriteWidth;
} else if ((this->frame + 1) * gfx->spriteHeight <= gfx->spritesheet.height()) {
y = this->frame * gfx->spriteHeight;
}
img = gfx->spritesheet.copy(x, y, gfx->spriteWidth, gfx->spriteHeight);
// Right-facing sprite is just the left-facing sprite mirrored
if (this->hFlip) {
img = img.transformed(QTransform().scale(-1, 1));
}
}
// Set first palette color fully transparent.
img.setColor(0, qRgba(0, 0, 0, 0));
pixmap = QPixmap::fromImage(img);
this->spriteWidth = gfx->spriteWidth;
this->spriteHeight = gfx->spriteHeight;
this->usingSprite = true;
}
void ObjectEvent::setFrameFromMovement(QString facingDir) {
// defaults
// TODO: read this from a file somewhere?
this->frame = 0;
this->hFlip = false;
if (facingDir == "DIR_NORTH") {
this->frame = 1;
this->hFlip = false;
} else if (facingDir == "DIR_SOUTH") {
this->frame = 0;
this->hFlip = false;
} else if (facingDir == "DIR_WEST") {
this->frame = 2;
this->hFlip = false;
} else if (facingDir == "DIR_EAST") {
this->frame = 2;
this->hFlip = true;
}
}
@ -368,6 +235,7 @@ Event *CloneObjectEvent::duplicate() const {
copy->setX(this->getX());
copy->setY(this->getY());
copy->setElevation(this->getElevation());
copy->setIdName(this->getIdName());
copy->setGfx(this->getGfx());
copy->setTargetID(this->getTargetID());
copy->setTargetMap(this->getTargetMap());
@ -388,6 +256,9 @@ OrderedJson::object CloneObjectEvent::buildEventJson(Project *project) {
OrderedJson::object cloneJson;
cloneJson["type"] = "clone";
QString idName = this->getIdName();
if (!idName.isEmpty())
cloneJson["local_id"] = idName;
cloneJson["graphics_id"] = this->getGfx();
cloneJson["x"] = this->getX();
cloneJson["y"] = this->getY();
@ -402,6 +273,7 @@ OrderedJson::object CloneObjectEvent::buildEventJson(Project *project) {
bool CloneObjectEvent::loadFromJson(const QJsonObject &json, Project *project) {
this->setX(ParseUtil::jsonToInt(json["x"]));
this->setY(ParseUtil::jsonToInt(json["y"]));
this->setIdName(ParseUtil::jsonToQString(json["local_id"]));
this->setGfx(ParseUtil::jsonToQString(json["graphics_id"]));
this->setTargetID(ParseUtil::jsonToInt(json["target_local_id"]));
@ -424,6 +296,7 @@ void CloneObjectEvent::setDefaultValues(Project *project) {
const QSet<QString> expectedCloneObjectFields = {
"type",
"local_id",
"graphics_id",
"target_local_id",
"target_map",
@ -452,19 +325,7 @@ void CloneObjectEvent::loadPixmap(Project *project) {
this->gfx = project->gfxDefines.key(0, "0");
this->movement = project->movementTypes.value(0, "0");
}
EventGraphics *eventGfx = project->eventGraphicsMap.value(gfx, nullptr);
if (!eventGfx || eventGfx->spritesheet.isNull()) {
// No sprite associated with this gfx constant.
// Use default sprite instead.
Event::loadPixmap(project);
this->spriteWidth = 16;
this->spriteHeight = 16;
this->usingSprite = false;
} else {
this->setFrameFromMovement(project->facingDirections.value(this->movement));
this->setPixmapFromSpritesheet(eventGfx);
}
ObjectEvent::loadPixmap(project);
}

View File

@ -147,13 +147,12 @@ QStringList Map::getScriptLabels(Event::Group group) {
scriptLabels = scriptTracker.getScripts();
}
// Add scripts from map's scripts file, and empty names.
// Add labels from the map's scripts file
scriptLabels.append(m_scriptsFileLabels);
scriptLabels.sort(Qt::CaseInsensitive);
scriptLabels.prepend("0x0");
scriptLabels.prepend("NULL");
scriptLabels.removeAll("");
scriptLabels.removeAll("0");
scriptLabels.removeAll("0x0");
scriptLabels.removeDuplicates();
return scriptLabels;
@ -164,7 +163,7 @@ QString Map::getScriptsFilePath() const {
auto path = QDir::cleanPath(QString("%1/%2/%3/scripts")
.arg(projectConfig.projectDir)
.arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders))
.arg(m_name));
.arg(!m_sharedScriptsMap.isEmpty() ? m_sharedScriptsMap : m_name));
auto extension = Project::getScriptFileExtension(usePoryscript);
if (usePoryscript && !QFile::exists(path + extension))
extension = Project::getScriptFileExtension(false);
@ -224,6 +223,10 @@ int Map::getIndexOfEvent(Event *event) const {
return m_events.value(event->getEventGroup()).indexOf(event);
}
bool Map::hasEvent(Event *event) const {
return getIndexOfEvent(event) >= 0;
}
void Map::deleteConnections() {
qDeleteAll(m_ownedConnections);
m_ownedConnections.clear();

View File

@ -434,5 +434,5 @@ QPixmap Layout::getLayoutItemPixmap() {
}
bool Layout::hasUnsavedChanges() const {
return !this->editHistory.isClean();
return !this->editHistory.isClean() || !this->newFolderPath.isEmpty();
}

View File

@ -67,10 +67,10 @@ QString ParseUtil::createErrorMessage(const QString &message, const QString &exp
return QString("%1:%2:%3: %4").arg(this->file).arg(lineNum).arg(colNum).arg(message);
}
QString ParseUtil::readTextFile(const QString &path) {
QString ParseUtil::readTextFile(const QString &path, QString *error) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
logError(QString("Could not open '%1': ").arg(path) + file.errorString());
if (error) *error = file.errorString();
return QString();
}
QTextStream in(&file);
@ -380,7 +380,7 @@ bool ParseUtil::defineNameMatchesFilter(const QString &name, const QList<QRegula
return false;
}
ParseUtil::ParsedDefines ParseUtil::readCDefines(const QString &filename, const QStringList &filterList, bool useRegex) {
ParseUtil::ParsedDefines ParseUtil::readCDefines(const QString &filename, const QStringList &filterList, bool useRegex, QString *error) {
ParsedDefines result;
this->file = filename;
@ -389,12 +389,9 @@ ParseUtil::ParsedDefines ParseUtil::readCDefines(const QString &filename, const
}
QString filepath = this->root + "/" + this->file;
this->text = readTextFile(filepath);
if (this->text.isNull()) {
logError(QString("Failed to read C defines file: '%1'").arg(filepath));
this->text = readTextFile(filepath, error);
if (this->text.isNull())
return result;
}
static const QRegularExpression re_extraChars("(//.*)|(\\/+\\*+[^*]*\\*+\\/+)");
this->text.replace(re_extraChars, "");
@ -466,8 +463,8 @@ ParseUtil::ParsedDefines ParseUtil::readCDefines(const QString &filename, const
}
// Read all the define names and their expressions in the specified file, then evaluate the ones matching the search text (and any they depend on).
QMap<QString, int> ParseUtil::evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex) {
ParsedDefines defines = readCDefines(filename, filterList, useRegex);
QMap<QString, int> ParseUtil::evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex, QString *error) {
ParsedDefines defines = readCDefines(filename, filterList, useRegex, error);
// Evaluate defines
QMap<QString, int> filteredValues;
@ -486,20 +483,20 @@ QMap<QString, int> ParseUtil::evaluateCDefines(const QString &filename, const QS
}
// Find and evaluate a specific set of defines with known names.
QMap<QString, int> ParseUtil::readCDefinesByName(const QString &filename, const QStringList &names) {
return evaluateCDefines(filename, names, false);
QMap<QString, int> ParseUtil::readCDefinesByName(const QString &filename, const QStringList &names, QString *error) {
return evaluateCDefines(filename, names, false, error);
}
// Find and evaluate an unknown list of defines with a known name pattern.
QMap<QString, int> ParseUtil::readCDefinesByRegex(const QString &filename, const QStringList &regexList) {
return evaluateCDefines(filename, regexList, true);
QMap<QString, int> ParseUtil::readCDefinesByRegex(const QString &filename, const QStringList &regexList, QString *error) {
return evaluateCDefines(filename, regexList, true, error);
}
// Find an unknown list of defines with a known name pattern.
// Similar to readCDefinesByRegex, but for cases where we only need to show a list of define names.
// We can skip evaluating any expressions (and by extension skip reporting any errors from this process).
QStringList ParseUtil::readCDefineNames(const QString &filename, const QStringList &regexList) {
return readCDefines(filename, regexList, true).filteredNames;
QStringList ParseUtil::readCDefineNames(const QString &filename, const QStringList &regexList, QString *error) {
return readCDefines(filename, regexList, true, error).filteredNames;
}
QStringList ParseUtil::readCArray(const QString &filename, const QString &label) {
@ -558,8 +555,8 @@ QMap<QString, QStringList> ParseUtil::readCArrayMulti(const QString &filename) {
return map;
}
QMap<QString, QString> ParseUtil::readNamedIndexCArray(const QString &filename, const QString &label) {
this->text = readTextFile(this->root + "/" + filename);
QMap<QString, QString> ParseUtil::readNamedIndexCArray(const QString &filename, const QString &label, QString *error) {
this->text = readTextFile(this->root + "/" + filename, error);
QMap<QString, QString> map;
QRegularExpression re_text(QString(R"(\b%1\b\s*(\[?[^\]]*\])?\s*=\s*\{([^\}]*)\})").arg(label));
@ -659,10 +656,10 @@ QStringList ParseUtil::getLabelValues(const QList<QStringList> &list, const QStr
return values;
}
bool ParseUtil::tryParseJsonFile(QJsonDocument *out, const QString &filepath) {
bool ParseUtil::tryParseJsonFile(QJsonDocument *out, const QString &filepath, QString *error) {
QFile file(filepath);
if (!file.open(QIODevice::ReadOnly)) {
logError(QString("Error: Could not open %1 for reading").arg(filepath));
if (error) *error = file.errorString();
return false;
}
@ -671,7 +668,7 @@ bool ParseUtil::tryParseJsonFile(QJsonDocument *out, const QString &filepath) {
const QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &parseError);
file.close();
if (parseError.error != QJsonParseError::NoError) {
logError(QString("Error: Failed to parse json file %1: %2").arg(filepath).arg(parseError.errorString()));
if (error) *error = parseError.errorString();
return false;
}
@ -679,23 +676,15 @@ bool ParseUtil::tryParseJsonFile(QJsonDocument *out, const QString &filepath) {
return true;
}
bool ParseUtil::tryParseOrderedJsonFile(poryjson::Json::object *out, const QString &filepath) {
bool ParseUtil::tryParseOrderedJsonFile(poryjson::Json::object *out, const QString &filepath, QString *error) {
QString err;
QString jsonTxt = readTextFile(filepath);
*out = OrderedJson::parse(jsonTxt, err).object_items();
if (!err.isEmpty()) {
logError(QString("Error: Failed to parse json file %1: %2").arg(filepath).arg(err));
QString jsonTxt = readTextFile(filepath, error);
if (error && !error->isEmpty()) {
return false;
}
return true;
}
bool ParseUtil::ensureFieldsExist(const QJsonObject &obj, const QList<QString> &fields) {
for (QString field : fields) {
if (!obj.contains(field)) {
logError(QString("JSON object is missing field '%1'.").arg(field));
return false;
}
*out = OrderedJson::parse(jsonTxt, error).object_items();
if (error && !error->isEmpty()) {
return false;
}
return true;
}

View File

@ -13,6 +13,7 @@
#include "customattributesframe.h"
#include "validator.h"
#include "message.h"
#include "eventframes.h"
#include <QCheckBox>
#include <QPainter>
#include <QMouseEvent>
@ -28,7 +29,6 @@ QList<QList<const QImage*>> Editor::collisionIcons;
Editor::Editor(Ui::MainWindow* ui)
{
this->ui = ui;
this->selected_events = new QList<DraggablePixmapItem*>;
this->settings = new Settings();
this->playerViewRect = new MovableRect(&this->settings->playerViewRectEnabled, 30 * 8, 20 * 8, qRgb(255, 255, 255));
this->cursorMapTileRect = new CursorTileRect(&this->settings->cursorTileRectEnabled, qRgb(255, 255, 255));
@ -40,7 +40,7 @@ Editor::Editor(Ui::MainWindow* ui)
/// the index is changed.
connect(&editGroup, &QUndoGroup::indexChanged, [this](int) {
if (selectNewEvents) {
updateSelectedEvents();
updateEvents();
selectNewEvents = false;
}
});
@ -58,7 +58,6 @@ Editor::Editor(Ui::MainWindow* ui)
Editor::~Editor()
{
delete this->selected_events;
delete this->settings;
delete this->playerViewRect;
delete this->cursorMapTileRect;
@ -69,28 +68,30 @@ Editor::~Editor()
closeProject();
}
void Editor::saveProject() {
if (project) {
saveUiFields();
project->saveAllMaps();
project->saveAllDataStructures();
}
void Editor::saveCurrent() {
save(true);
}
void Editor::save() {
if (this->project && this->map) {
saveUiFields();
this->project->saveMap(this->map);
this->project->saveAllDataStructures();
}
else if (this->project && this->layout) {
this->project->saveLayout(this->layout);
this->project->saveAllDataStructures();
}
void Editor::saveAll() {
save(false);
}
void Editor::saveUiFields() {
void Editor::save(bool currentOnly) {
if (!this->project)
return;
saveEncounterTabData();
if (currentOnly) {
if (this->map) {
this->project->saveMap(this->map);
} else if (this->layout) {
this->project->saveLayout(this->layout);
}
this->project->saveGlobalData();
} else {
this->project->saveAll();
}
}
void Editor::setProject(Project * project) {
@ -651,6 +652,9 @@ void Editor::configureEncounterJSON(QWidget *window) {
}
void Editor::saveEncounterTabData() {
if (!this->map || !this->project)
return;
// This function does not save to disk so it is safe to use before user clicks Save.
QStackedWidget *stack = ui->stackedWidget_WildMons;
QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel;
@ -1167,7 +1171,7 @@ bool Editor::setMap(QString map_name) {
editGroup.addStack(map->editHistory());
editGroup.setActiveStack(map->editHistory());
selected_events->clear();
this->selectedEvents.clear();
if (!displayMap()) {
return false;
}
@ -1176,7 +1180,7 @@ bool Editor::setMap(QString map_name) {
connect(map, &Map::openScriptRequested, this, &Editor::openScript);
connect(map, &Map::connectionAdded, this, &Editor::displayConnection);
connect(map, &Map::connectionRemoved, this, &Editor::removeConnectionPixmap);
updateSelectedEvents();
updateEvents();
return true;
}
@ -1330,17 +1334,13 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i
} else {
// Left-clicking while in paint mode will add a new event of the
// type of the first currently selected events.
// Disallow adding heal locations, deleting them is not possible yet
Event::Type eventType = Event::Type::Object;
if (this->selected_events->size() > 0)
eventType = this->selected_events->first()->event->getEventType();
if (!this->selectedEvents.isEmpty())
eventType = this->selectedEvents.first()->getEventType();
DraggablePixmapItem *newEvent = addNewEvent(eventType);
if (newEvent) {
newEvent->move(pos.x(), pos.y());
emit eventsChanged();
selectMapEvent(newEvent);
}
Event* event = addNewEvent(eventType);
if (event && event->getPixmapItem())
event->getPixmapItem()->moveTo(pos);
}
} else if (eventEditAction == EditAction::Select) {
// do nothing here, at least for now
@ -1356,15 +1356,9 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i
if (pos.x() != selection_origin.x() || pos.y() != selection_origin.y()) {
int xDelta = pos.x() - selection_origin.x();
int yDelta = pos.y() - selection_origin.y();
QList<Event *> selectedEvents;
for (DraggablePixmapItem *pixmapItem : getEventPixmapItems()) {
selectedEvents.append(pixmapItem->event);
}
selection_origin = QPoint(pos.x(), pos.y());
map->commit(new EventShift(selectedEvents, xDelta, yDelta, this->eventShiftActionId));
this->map->commit(new EventShift(this->map->getEvents(), xDelta, yDelta, this->eventShiftActionId));
}
}
}
@ -1665,7 +1659,7 @@ void Editor::clearMapEvents() {
delete events_group;
events_group = nullptr;
}
selected_events->clear();
this->selectedEvents.clear();
}
void Editor::displayMapEvents() {
@ -1682,7 +1676,7 @@ void Editor::displayMapEvents() {
}
DraggablePixmapItem *Editor::addEventPixmapItem(Event *event) {
this->project->setEventPixmap(event);
this->project->loadEventPixmap(event);
auto item = new DraggablePixmapItem(event, this);
redrawEventPixmapItem(item);
this->events_group->addToGroup(item);
@ -1694,7 +1688,7 @@ void Editor::removeEventPixmapItem(Event *event) {
if (!item) return;
this->events_group->removeFromGroup(item);
this->selected_events->removeOne(item);
this->selectedEvents.removeOne(event);
event->setPixmapItem(nullptr);
delete item;
@ -1959,14 +1953,6 @@ void Editor::redrawEvents(const QList<Event*> &events) {
}
}
QList<DraggablePixmapItem *> Editor::getEventPixmapItems() {
QList<DraggablePixmapItem *> list;
for (QGraphicsItem *child : events_group->childItems()) {
list.append(static_cast<DraggablePixmapItem *>(child));
}
return list;
}
qreal Editor::getEventOpacity(const Event *event) const {
// There are 4 possible opacities for an event's sprite:
// - Off the Events tab, and the event overlay is off (0.0)
@ -1975,18 +1961,18 @@ qreal Editor::getEventOpacity(const Event *event) const {
// - On the Events tab, and the event has a custom sprite (1.0)
if (this->editMode != EditMode::Events)
return porymapConfig.eventOverlayEnabled ? 0.5 : 0.0;
return event->getUsingSprite() ? 1.0 : 0.7;
return event->getUsesDefaultPixmap() ? 0.7 : 1.0;
}
void Editor::redrawEventPixmapItem(DraggablePixmapItem *item) {
if (item && item->event && !item->event->getPixmap().isNull()) {
item->setOpacity(getEventOpacity(item->event));
project->setEventPixmap(item->event, true);
project->loadEventPixmap(item->event, true);
item->setPixmap(item->event->getPixmap());
item->setShapeMode(porymapConfig.eventSelectionShapeMode);
if (this->editMode == EditMode::Events) {
if (selected_events && selected_events->contains(item)) {
if (this->selectedEvents.contains(item->event)) {
// Draw the selection rectangle
QImage image = item->pixmap().toImage();
QPainter painter(&image);
@ -2031,44 +2017,40 @@ void Editor::updateWarpEventWarning(Event *event) {
void Editor::updateWarpEventWarnings() {
if (porymapConfig.warpBehaviorWarningDisabled)
return;
if (selected_events) {
for (auto selection : *selected_events)
updateWarpEventWarning(selection->event);
}
for (const auto &event : this->selectedEvents)
updateWarpEventWarning(event);
}
void Editor::shouldReselectEvents() {
selectNewEvents = true;
}
void Editor::updateSelectedEvents() {
for (DraggablePixmapItem *item : getEventPixmapItems()) {
redrawEventPixmapItem(item);
}
// TODO: This is frequently used to do more work than necessary.
void Editor::updateEvents() {
redrawAllEvents();
emit eventsChanged();
}
void Editor::selectMapEvent(DraggablePixmapItem *item, bool toggle) {
if (!selected_events || !item)
void Editor::selectMapEvent(Event *event, bool toggle) {
if (!event)
return;
if (!toggle) {
// Selecting just this event
selected_events->clear();
selected_events->append(item);
} else if (!selected_events->contains(item)) {
this->selectedEvents.clear();
this->selectedEvents.append(event);
} else if (!this->selectedEvents.contains(event)) {
// Adding event to group selection
selected_events->append(item);
} else if (selected_events->length() > 1) {
this->selectedEvents.append(event);
} else if (this->selectedEvents.length() > 1) {
// Removing event from group selection
selected_events->removeOne(item);
this->selectedEvents.removeOne(event);
} else {
// Attempting to toggle the only currently-selected event.
// Unselecting an event this way would be unexpected, so we ignore it.
return;
}
updateSelectedEvents();
updateEvents();
}
void Editor::selectedEventIndexChanged(int index, Event::Group eventGroup) {
@ -2076,64 +2058,82 @@ void Editor::selectedEventIndexChanged(int index, Event::Group eventGroup) {
index = index - event_offs;
Event *event = this->map->getEvent(eventGroup, index);
if (event && event->getPixmapItem()) {
this->selectMapEvent(event->getPixmapItem());
if (event) {
selectMapEvent(event);
} else {
updateSelectedEvents();
updateEvents();
}
}
bool Editor::canAddEvents(const QList<Event*> &events) {
if (!this->project || !this->map)
return false;
QMap<Event::Group, int> newEventCounts;
for (const auto &event : events) {
Event::Group group = event->getEventGroup();
int maxEvents = this->project->getMaxEvents(group);
if (this->map->getNumEvents(group) + newEventCounts[group]++ >= maxEvents) {
return false;
}
}
return true;
}
void Editor::duplicateSelectedEvents() {
if (!selected_events || !selected_events->length() || !map || !current_view || this->getEditingLayout())
if (this->selectedEvents.isEmpty() || !project || !map || !current_view || this->getEditingLayout())
return;
QList<Event *> selectedEvents;
for (int i = 0; i < selected_events->length(); i++) {
Event *original = selected_events->at(i)->event;
Event::Type eventType = original->getEventType();
if (eventLimitReached(eventType)) {
logWarn(QString("Skipping duplication, the map limit for events of type '%1' has been reached.").arg(Event::eventTypeToString(eventType)));
continue;
}
Event *duplicate = original->duplicate();
duplicate->setX(duplicate->getX() + 1);
duplicate->setY(duplicate->getY() + 1);
selectedEvents.append(duplicate);
QList<Event *> duplicatedEvents;
for (const auto &event : this->selectedEvents) {
duplicatedEvents.append(event->duplicate());
}
map->commit(new EventDuplicate(this, map, selectedEvents));
if (!canAddEvents(duplicatedEvents)) {
WarningMessage::show(QStringLiteral("Unable to duplicate, the maximum number of events would be exceeded."), ui->graphicsView_Map);
qDeleteAll(duplicatedEvents);
return;
}
this->map->commit(new EventDuplicate(this, this->map, duplicatedEvents));
}
DraggablePixmapItem *Editor::addNewEvent(Event::Type type) {
if (!project || !map || eventLimitReached(type))
Event *Editor::addNewEvent(Event::Type type) {
if (!this->project || !this->map)
return nullptr;
Event::Group group = Event::typeToGroup(type);
int maxEvents = this->project->getMaxEvents(group);
if (this->map->getNumEvents(group) >= maxEvents) {
WarningMessage::show(QString("The maximum number of %1 events (%2) has been reached.").arg(Event::groupToString(group)).arg(maxEvents), ui->graphicsView_Map);
return nullptr;
}
Event *event = Event::create(type);
if (!event)
return nullptr;
event->setMap(this->map);
event->setDefaultValues(this->project);
map->commit(new EventCreate(this, map, event));
return event->getPixmapItem();
}
// Currently only object events have an explicit limit
bool Editor::eventLimitReached(Event::Type event_type) {
if (project && map) {
if (Event::typeToGroup(event_type) == Event::Group::Object)
return map->getNumEvents(Event::Group::Object) >= project->getMaxObjectEvents();
// This will add the event to the map, create the event pixmap item, and select the event.
this->map->commit(new EventCreate(this, this->map, event));
auto pixmapItem = event->getPixmapItem();
if (pixmapItem) {
auto halfSize = ui->graphicsView_Map->size() / 2;
auto centerPos = ui->graphicsView_Map->mapToScene(halfSize.width(), halfSize.height());
pixmapItem->moveTo(Metatile::coordFromPixmapCoord(centerPos));
}
return false;
return event;
}
void Editor::deleteSelectedEvents() {
if (!this->selected_events || this->selected_events->length() == 0 || !this->map || this->editMode != EditMode::Events)
if (this->selectedEvents.isEmpty() || !this->map || this->editMode != EditMode::Events)
return;
QList<Event*> eventsToDelete;
bool skipWarning = porymapConfig.eventDeleteWarningDisabled;
for (DraggablePixmapItem *item : *this->selected_events) {
Event* event = item->event;
for (auto event : this->selectedEvents) {
const QString idName = event->getIdName();
if (skipWarning || idName.isEmpty()) {
eventsToDelete.append(event);
@ -2151,7 +2151,7 @@ void Editor::deleteSelectedEvents() {
msgBox.setCheckBox(new QCheckBox(QStringLiteral("Don't warn me again")));
QAbstractButton* deleteAllButton = nullptr;
if (this->selected_events->length() > 1) {
if (this->selectedEvents.length() > 1) {
deleteAllButton = msgBox.addButton(QStringLiteral("Delete All"), QMessageBox::DestructiveRole);
msgBox.addButton(QStringLiteral("Skip"), QMessageBox::NoRole);
}
@ -2176,7 +2176,7 @@ void Editor::deleteSelectedEvents() {
}
}
// TODO: Are we just calling this to invalidate connections?
event->setPixmapItem(item);
event->setPixmapItem(event->getPixmapItem());
}
if (eventsToDelete.isEmpty())
return;
@ -2293,9 +2293,9 @@ void Editor::eventsView_onMousePress(QMouseEvent *event) {
}
bool multiSelect = event->modifiers() & Qt::ControlModifier;
if (!selectingEvent && !multiSelect && selected_events->length() > 1) {
if (!selectingEvent && !multiSelect && this->selectedEvents.length() > 1) {
// User is clearing group selection by clicking on the background
this->selectMapEvent(selected_events->first());
this->selectMapEvent(this->selectedEvents.first());
}
selectingEvent = false;
}

View File

@ -393,7 +393,7 @@ struct JsonParser final {
*/
const QString &str;
int i;
QString &err;
QString *err;
bool failed;
const JsonParse strategy;
@ -407,8 +407,8 @@ struct JsonParser final {
template <typename T>
T fail(QString &&msg, const T err_ret) {
if (!failed)
err = std::move(msg);
if (!failed && err)
*err = std::move(msg);
failed = true;
return err_ret;
}
@ -775,7 +775,7 @@ struct JsonParser final {
};
}//namespace {
Json Json::parse(const QString &in, QString &err, JsonParse strategy) {
Json Json::parse(const QString &in, QString *err, JsonParse strategy) {
JsonParser parser { in, 0, err, false, strategy };
Json result = parser.parse_json(0);

View File

@ -264,8 +264,6 @@ void MainWindow::initCustomUI() {
}
void MainWindow::initExtraSignals() {
// other signals
connect(ui->newEventToolButton, &NewEventToolButton::newEventAdded, this, &MainWindow::addNewEvent);
connect(ui->tabWidget_EventType, &QTabWidget::currentChanged, this, &MainWindow::eventTabChanged);
// Change pages on wild encounter groups
@ -299,6 +297,7 @@ void MainWindow::initExtraSignals() {
connect(ui->action_NewMap, &QAction::triggered, this, &MainWindow::openNewMapDialog);
connect(ui->action_NewLayout, &QAction::triggered, this, &MainWindow::openNewLayoutDialog);
connect(ui->actionDuplicate_Current_Map_Layout, &QAction::triggered, this, &MainWindow::openDuplicateMapOrLayoutDialog);
}
void MainWindow::on_actionCheck_for_Updates_triggered() {
@ -343,6 +342,7 @@ void MainWindow::initEditor() {
connect(this->editor, &Editor::wildMonTableEdited, [this] { this->markMapEdited(); });
connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged);
connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated);
connect(ui->newEventToolButton, &NewEventToolButton::newEventAdded, this->editor, &Editor::addNewEvent);
connect(ui->toolButton_deleteEvent, &QAbstractButton::clicked, this->editor, &Editor::deleteSelectedEvents);
this->loadUserSettings();
@ -935,7 +935,7 @@ void MainWindow::setLayoutOnlyMode(bool layoutOnly) {
this->ui->mainTabBar->setTabEnabled(MainTab::Events, mapEditingEnabled);
this->ui->mainTabBar->setTabEnabled(MainTab::Header, mapEditingEnabled);
this->ui->mainTabBar->setTabEnabled(MainTab::Connections, mapEditingEnabled);
this->ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, mapEditingEnabled);
this->ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, mapEditingEnabled && editor->project->wildEncountersLoaded);
this->ui->comboBox_LayoutSelector->setEnabled(mapEditingEnabled);
}
@ -1028,18 +1028,13 @@ void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_
// Select the target event.
int index = event_id - Event::getIndexOffset(event_group);
Event* event = editor->map->getEvent(event_group, index);
Event* event = this->editor->map->getEvent(event_group, index);
if (event) {
auto item = event->getPixmapItem();
if (item) {
editor->selected_events->clear();
editor->selected_events->append(item);
editor->updateSelectedEvents();
return;
}
this->editor->selectMapEvent(event);
} else {
// Can still warp to this map, but can't select the specified event
logWarn(QString("%1 %2 doesn't exist on map '%3'").arg(Event::groupToString(event_group)).arg(event_id).arg(map_name));
}
// Can still warp to this map, but can't select the specified event
logWarn(QString("%1 %2 doesn't exist on map '%3'").arg(Event::eventGroupToString(event_group)).arg(event_id).arg(map_name));
}
void MainWindow::displayMapProperties() {
@ -1109,7 +1104,6 @@ bool MainWindow::setProjectUI() {
ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled);
ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled);
Event::setIcons();
editor->setCollisionGraphics();
ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation());
ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision());
@ -1167,8 +1161,6 @@ void MainWindow::clearProjectUI() {
delete this->layoutTreeModel;
delete this->layoutListProxyModel;
resetMapListFilters();
Event::clearIcons();
}
void MainWindow::scrollMapList(MapTree *list, const QString &itemName) {
@ -1302,15 +1294,9 @@ void MainWindow::onNewMapCreated(Map *newMap, const QString &groupName) {
logInfo(QString("Created a new map named %1.").arg(newMap->name()));
if (newMap->needsHealLocation()) {
addNewEvent(Event::Type::HealLocation);
this->editor->addNewEvent(Event::Type::HealLocation);
}
// TODO: Creating a new map shouldn't be automatically saved.
// For one, it takes away the option to discard the new map.
// For two, if the new map uses an existing layout, any unsaved changes to that layout will also be saved.
editor->project->saveMap(newMap);
editor->project->saveAllDataStructures();
// Add new map to the map lists
this->mapGroupModel->insertMapItem(newMap->name(), groupName);
this->mapLocationModel->insertMapItem(newMap->name(), newMap->header()->location());
@ -1419,6 +1405,14 @@ void MainWindow::openDuplicateLayoutDialog(const QString &layoutId) {
}
}
void MainWindow::openDuplicateMapOrLayoutDialog() {
if (this->editor->map) {
openDuplicateMapDialog(this->editor->map->name());
} else if (this->editor->layout) {
openDuplicateLayoutDialog(this->editor->layout->id);
}
}
void MainWindow::on_actionNew_Tileset_triggered() {
auto dialog = new NewTilesetDialog(editor->project, this);
connect(dialog, &NewTilesetDialog::applied, [this](Tileset *tileset) {
@ -1549,16 +1543,30 @@ void MainWindow::updateMapList() {
}
void MainWindow::on_action_Save_Project_triggered() {
editor->saveProject();
updateWindowTitle();
updateMapList();
saveGlobalConfigs();
save(false);
}
void MainWindow::on_action_Save_triggered() {
editor->save();
save(true);
}
void MainWindow::save(bool currentOnly) {
if (currentOnly) {
this->editor->saveCurrent();
} else {
this->editor->saveAll();
}
updateWindowTitle();
updateMapList();
if (!porymapConfig.shownInGameReloadMessage) {
// Show a one-time warning that the user may need to reload their map to see their new changes.
static const QString message = QStringLiteral("Reload your map in-game!\n\nIf your game is currently saved on a map you have edited, "
"the changes may not appear until you leave the map and return.");
InfoMessage::show(message, this);
porymapConfig.shownInGameReloadMessage = true;
}
saveGlobalConfigs();
}
@ -1627,17 +1635,10 @@ void MainWindow::copy() {
OrderedJson::object copyObject;
copyObject["object"] = "events";
QList<DraggablePixmapItem *> events;
if (editor->selected_events && editor->selected_events->length()) {
events = *editor->selected_events;
}
OrderedJson::array eventsArray;
for (auto item : events) {
Event *event = item->event;
for (const auto &event : this->editor->selectedEvents) {
OrderedJson::object eventContainer;
eventContainer["event_type"] = Event::eventTypeToString(event->getEventType());
eventContainer["event_type"] = Event::typeToString(event->getEventType());
OrderedJson::object eventJson = event->buildEventJson(editor->project);
eventContainer["event"] = eventJson;
eventsArray.append(eventContainer);
@ -1748,14 +1749,7 @@ void MainWindow::paste() {
QJsonArray events = pasteObject["events"].toArray();
for (QJsonValue event : events) {
// paste the event to the map
const QString typeString = event["event_type"].toString();
Event::Type type = Event::eventTypeFromString(typeString);
if (this->editor->eventLimitReached(type)) {
logWarn(QString("Cannot paste event, the limit for type '%1' has been reached.").arg(typeString));
continue;
}
Event::Type type = Event::typeFromString(event["event_type"].toString());
Event *pasteEvent = Event::create(type);
if (!pasteEvent)
continue;
@ -1764,12 +1758,16 @@ void MainWindow::paste() {
pasteEvent->setMap(this->editor->map);
newEvents.append(pasteEvent);
}
if (newEvents.empty())
return;
if (!newEvents.empty()) {
editor->map->commit(new EventPaste(this->editor, editor->map, newEvents));
updateEvents();
if (!this->editor->canAddEvents(newEvents)) {
WarningMessage::show(QStringLiteral("Unable to paste, the maximum number of events would be exceeded."), this);
qDeleteAll(newEvents);
return;
}
this->editor->map->commit(new EventPaste(this->editor, this->editor->map, newEvents));
updateEvents();
break;
}
}
@ -1981,33 +1979,10 @@ void MainWindow::resetMapViewScale() {
editor->scaleMapView(0);
}
void MainWindow::addNewEvent(Event::Type type) {
if (editor && editor->project) {
DraggablePixmapItem *item = editor->addNewEvent(type);
if (item) {
auto halfSize = ui->graphicsView_Map->size() / 2;
auto centerPos = ui->graphicsView_Map->mapToScene(halfSize.width(), halfSize.height());
item->moveTo(Metatile::coordFromPixmapCoord(centerPos));
updateEvents();
editor->selectMapEvent(item);
} else {
WarningMessage msgBox(QStringLiteral("Failed to add new event."), this);
if (Event::typeToGroup(type) == Event::Group::Object) {
msgBox.setInformativeText(QString("The limit for object events (%1) has been reached.\n\n"
"This limit can be adjusted with %2 in '%3'.")
.arg(editor->project->getMaxObjectEvents())
.arg(projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count))
.arg(projectConfig.getFilePath(ProjectFilePath::constants_global)));
}
msgBox.exec();
}
}
}
void MainWindow::tryAddEventTab(QWidget * tab) {
auto group = getEventGroupFromTabWidget(tab);
if (editor->map->getNumEvents(group))
ui->tabWidget_EventType->addTab(tab, QString("%1s").arg(Event::eventGroupToString(group)));
ui->tabWidget_EventType->addTab(tab, QString("%1s").arg(Event::groupToString(group)));
}
void MainWindow::displayEventTabs() {
@ -2022,31 +1997,32 @@ void MainWindow::displayEventTabs() {
}
void MainWindow::updateEvents() {
QList<DraggablePixmapItem *> items = editor->getEventPixmapItems();
for (auto i = this->lastSelectedEvent.cbegin(), end = this->lastSelectedEvent.cend(); i != end; i++) {
if (i.value() && !items.contains(i.value()))
this->lastSelectedEvent.insert(i.key(), nullptr);
if (this->editor->map) {
for (auto i = this->lastSelectedEvent.begin(); i != this->lastSelectedEvent.end(); i++) {
if (i.value() && !this->editor->map->hasEvent(i.value()))
this->lastSelectedEvent.insert(i.key(), nullptr);
}
}
displayEventTabs();
updateSelectedEvents();
}
void MainWindow::updateSelectedEvents() {
QList<DraggablePixmapItem *> events;
QList<Event*> events;
if (editor->selected_events && editor->selected_events->length()) {
events = *editor->selected_events;
if (!this->editor->selectedEvents.isEmpty()) {
events = this->editor->selectedEvents;
}
else {
QList<Event *> all_events;
if (editor->map) {
all_events = editor->map->getEvents();
QList<Event *> allEvents;
if (this->editor->map) {
allEvents = this->editor->map->getEvents();
}
if (all_events.length()) {
DraggablePixmapItem *selectedEvent = all_events.first()->getPixmapItem();
if (!allEvents.isEmpty()) {
Event *selectedEvent = allEvents.first();
if (selectedEvent) {
editor->selected_events->append(selectedEvent);
editor->redrawEventPixmapItem(selectedEvent);
this->editor->selectedEvents.append(selectedEvent);
this->editor->redrawEventPixmapItem(selectedEvent->getPixmapItem());
events.append(selectedEvent);
}
}
@ -2059,12 +2035,13 @@ void MainWindow::updateSelectedEvents() {
if (events.length() == 1) {
// single selected event case
Event *current = events[0]->event;
Event *current = events.constFirst();
Event::Group eventGroup = current->getEventGroup();
int event_offs = Event::getIndexOffset(eventGroup);
if (eventGroup != Event::Group::None)
this->lastSelectedEvent.insert(eventGroup, current->getPixmapItem());
if (eventGroup != Event::Group::None) {
this->lastSelectedEvent.insert(eventGroup, current);
}
switch (eventGroup) {
case Event::Group::Object: {
@ -2135,8 +2112,7 @@ void MainWindow::updateSelectedEvents() {
this->isProgrammaticEventTabChange = false;
QList<QFrame *> frames;
for (DraggablePixmapItem *item : events) {
Event *event = item->event;
for (auto event : events) {
EventFrame *eventFrame = event->createEventFrame();
eventFrame->populate(this->editor->project);
eventFrame->initialize();
@ -2191,7 +2167,7 @@ Event::Group MainWindow::getEventGroupFromTabWidget(QWidget *tab) {
void MainWindow::eventTabChanged(int index) {
if (editor->map) {
Event::Group group = getEventGroupFromTabWidget(ui->tabWidget_EventType->widget(index));
DraggablePixmapItem *selectedItem = this->lastSelectedEvent.value(group, nullptr);
Event *selectedEvent = this->lastSelectedEvent.value(group, nullptr);
switch (group) {
case Event::Group::Object:
@ -2214,11 +2190,8 @@ void MainWindow::eventTabChanged(int index) {
}
if (!isProgrammaticEventTabChange) {
if (!selectedItem) {
Event *event = editor->map->getEvent(group, 0);
if (event) selectedItem = event->getPixmapItem();
}
if (selectedItem) editor->selectMapEvent(selectedItem);
if (!selectedEvent) selectedEvent = this->editor->map->getEvent(group, 0);
this->editor->selectMapEvent(selectedEvent);
}
}
@ -2763,12 +2736,10 @@ void MainWindow::on_actionOpen_Config_Folder_triggered() {
void MainWindow::on_actionPreferences_triggered() {
if (!preferenceEditor) {
preferenceEditor = new PreferenceEditor(this);
connect(preferenceEditor, &PreferenceEditor::themeChanged,
this, &MainWindow::setTheme);
connect(preferenceEditor, &PreferenceEditor::themeChanged,
editor, &Editor::maskNonVisibleConnectionTiles);
connect(preferenceEditor, &PreferenceEditor::preferencesSaved,
this, &MainWindow::togglePreferenceSpecificUi);
connect(preferenceEditor, &PreferenceEditor::themeChanged, this, &MainWindow::setTheme);
connect(preferenceEditor, &PreferenceEditor::themeChanged, editor, &Editor::maskNonVisibleConnectionTiles);
connect(preferenceEditor, &PreferenceEditor::preferencesSaved, this, &MainWindow::togglePreferenceSpecificUi);
connect(preferenceEditor, &PreferenceEditor::scriptSettingsChanged, editor->project, &Project::readEventScriptLabels);
}
openSubWindow(preferenceEditor);
@ -2783,8 +2754,9 @@ void MainWindow::togglePreferenceSpecificUi() {
if (this->updatePromoter)
this->updatePromoter->updatePreferences();
// Redraw all events to use updated porymapConfig.eventSelectionShapeMode
this->editor->redrawAllEvents();
// Changes to porymapConfig.loadAllEventScripts or porymapConfig.eventSelectionShapeMode
// require us to repopulate the EventFrames and redraw event pixmaps, respectively.
this->editor->updateEvents();
}
void MainWindow::openProjectSettingsEditor(int tab) {
@ -3006,7 +2978,7 @@ bool MainWindow::closeProject() {
auto reply = msgBox.exec();
if (reply == QMessageBox::Yes) {
editor->saveProject();
save();
} else if (reply == QMessageBox::No) {
logWarn("Closing project with unsaved changes.");
} else if (reply == QMessageBox::Cancel) {

View File

@ -31,7 +31,6 @@ int Project::num_pals_primary = 6;
int Project::num_pals_total = 13;
int Project::max_map_data_size = 10240; // 0x2800
int Project::default_map_dimension = 20;
int Project::max_object_events = 64;
Project::Project(QObject *parent) :
QObject(parent)
@ -46,6 +45,7 @@ Project::~Project()
clearMapLayouts();
clearEventGraphics();
clearHealLocations();
QPixmapCache::clear();
}
void Project::set_root(QString dir) {
@ -197,13 +197,29 @@ void Project::initTopLevelMapFields() {
bool Project::readMapJson(const QString &mapName, QJsonDocument * out) {
const QString mapFilepath = QString("%1%2/map.json").arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)).arg(mapName);
if (!parser.tryParseJsonFile(out, QString("%1/%2").arg(this->root).arg(mapFilepath))) {
logError(QString("Failed to read map data from %1").arg(mapFilepath));
QString error;
if (!parser.tryParseJsonFile(out, QString("%1/%2").arg(this->root).arg(mapFilepath), &error)) {
logError(QString("Failed to read map data from '%1': %2").arg(mapFilepath).arg(error));
return false;
}
return true;
}
bool Project::loadMapEvent(Map *map, const QJsonObject &json, Event::Type defaultType) {
QString typeString = ParseUtil::jsonToQString(json["type"]);
Event::Type type = typeString.isEmpty() ? defaultType : Event::typeFromString(typeString);
Event* event = Event::create(type);
if (!event) {
return false;
}
if (!event->loadFromJson(json, this)) {
delete event;
return false;
}
map->addEvent(event);
return true;
}
bool Project::loadMapData(Map* map) {
if (!map->isPersistedToFile()) {
return true;
@ -242,75 +258,22 @@ bool Project::loadMapData(Map* map) {
// Events
map->resetEvents();
QJsonArray objectEventsArr = mapObj["object_events"].toArray();
for (int i = 0; i < objectEventsArr.size(); i++) {
QJsonObject event = objectEventsArr[i].toObject();
// If clone objects are not enabled then no type field is present
QString type = projectConfig.eventCloneObjectEnabled ? ParseUtil::jsonToQString(event["type"]) : "object";
if (type.isEmpty() || type == "object") {
ObjectEvent *object = new ObjectEvent();
object->loadFromJson(event, this);
map->addEvent(object);
} else if (type == "clone") {
CloneObjectEvent *clone = new CloneObjectEvent();
if (clone->loadFromJson(event, this)) {
map->addEvent(clone);
static const QMap<QString, Event::Type> defaultEventTypes = {
// Map of the expected keys for each event group, and the default type of that group.
// If the default type is Type::None then each event must specify its type, or its an error.
{"object_events", Event::Type::Object},
{"warp_events", Event::Type::Warp},
{"coord_events", Event::Type::None},
{"bg_events", Event::Type::None},
};
for (auto i = defaultEventTypes.constBegin(); i != defaultEventTypes.constEnd(); i++) {
QString eventGroupKey = i.key();
Event::Type defaultType = i.value();
const QJsonArray eventsJsonArr = mapObj[eventGroupKey].toArray();
for (int i = 0; i < eventsJsonArr.size(); i++) {
if (!loadMapEvent(map, eventsJsonArr.at(i).toObject(), defaultType)) {
logError(QString("Failed to load event for %1, in %2 at index %3.").arg(map->name()).arg(eventGroupKey).arg(i));
}
else {
delete clone;
}
} else {
logError(QString("Map %1 object_event %2 has invalid type '%3'. Must be 'object' or 'clone'.").arg(map->name()).arg(i).arg(type));
}
}
QJsonArray warpEventsArr = mapObj["warp_events"].toArray();
for (int i = 0; i < warpEventsArr.size(); i++) {
QJsonObject event = warpEventsArr[i].toObject();
WarpEvent *warp = new WarpEvent();
if (warp->loadFromJson(event, this)) {
map->addEvent(warp);
}
else {
delete warp;
}
}
QJsonArray coordEventsArr = mapObj["coord_events"].toArray();
for (int i = 0; i < coordEventsArr.size(); i++) {
QJsonObject event = coordEventsArr[i].toObject();
QString type = ParseUtil::jsonToQString(event["type"]);
if (type == "trigger") {
TriggerEvent *coord = new TriggerEvent();
coord->loadFromJson(event, this);
map->addEvent(coord);
} else if (type == "weather") {
WeatherTriggerEvent *coord = new WeatherTriggerEvent();
coord->loadFromJson(event, this);
map->addEvent(coord);
} else {
logError(QString("Map %1 coord_event %2 has invalid type '%3'. Must be 'trigger' or 'weather'.").arg(map->name()).arg(i).arg(type));
}
}
QJsonArray bgEventsArr = mapObj["bg_events"].toArray();
for (int i = 0; i < bgEventsArr.size(); i++) {
QJsonObject event = bgEventsArr[i].toObject();
QString type = ParseUtil::jsonToQString(event["type"]);
if (type == "sign") {
SignEvent *bg = new SignEvent();
bg->loadFromJson(event, this);
map->addEvent(bg);
} else if (type == "hidden_item") {
HiddenItemEvent *bg = new HiddenItemEvent();
bg->loadFromJson(event, this);
map->addEvent(bg);
} else if (type == "secret_base") {
SecretBaseEvent *bg = new SecretBaseEvent();
bg->loadFromJson(event, this);
map->addEvent(bg);
} else {
logError(QString("Map %1 bg_event %2 has invalid type '%3'. Must be 'sign', 'hidden_item', or 'secret_base'.").arg(map->name()).arg(i).arg(type));
}
}
@ -352,6 +315,26 @@ Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* t
// Generate a unique MAP constant.
map->setConstantName(toUniqueIdentifier(map->expectedConstantName()));
// Make sure we keep the order of the map names the same as in the map group order.
int mapNamePos;
if (this->groupNames.contains(settings.group)) {
mapNamePos = 0;
for (const auto &name : this->groupNames) {
mapNamePos += this->groupNameToMapNames[name].length();
if (name == settings.group)
break;
}
} else if (isValidNewIdentifier(settings.group)) {
// Adding map to a map group that doesn't exist yet.
// Create the group, and we already know the map will be last in the list.
addNewMapGroup(settings.group);
mapNamePos = this->mapNames.length();
} else {
logError(QString("Cannot create new map with invalid map group name '%1'.").arg(settings.group));
delete map;
return nullptr;
}
Layout *layout = this->mapLayouts.value(settings.layout.id);
if (!layout) {
// Layout doesn't already exist, create it.
@ -367,24 +350,6 @@ Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* t
}
map->setLayout(layout);
// Make sure we keep the order of the map names the same as in the map group order.
int mapNamePos;
if (this->groupNames.contains(settings.group)) {
mapNamePos = 0;
for (const auto &name : this->groupNames) {
mapNamePos += this->groupNameToMapNames[name].length();
if (name == settings.group)
break;
}
} else {
// Adding map to a map group that doesn't exist yet.
// Create the group, and we already know the map will be last in the list.
if (isValidNewIdentifier(settings.group)) {
addNewMapGroup(settings.group);
}
mapNamePos = this->mapNames.length();
}
const QString location = map->header()->location();
if (!this->mapSectionIdNames.contains(location) && isValidNewIdentifier(location)) {
// Unrecognized MAPSEC name, we can automatically add a new MAPSEC for it.
@ -424,20 +389,10 @@ Layout *Project::createNewLayout(const Layout::Settings &settings, const Layout
// Otherwise the new layout's folder name will just be the layout's name.
const QString folderName = !settings.folderName.isEmpty() ? settings.folderName : layout->name;
const QString folderPath = projectConfig.getFilePath(ProjectFilePath::data_layouts_folders) + folderName;
layout->newFolderPath = folderPath;
layout->border_path = folderPath + "/border.bin";
layout->blockdata_path = folderPath + "/map.bin";
// Create a new directory for the layout, if it doesn't already exist.
const QString fullPath = QString("%1/%2").arg(this->root).arg(folderPath);
if (!QDir::root().mkpath(fullPath)) {
logError(QString("Failed to create directory for new layout: '%1'").arg(fullPath));
delete layout;
return nullptr;
}
this->mapLayouts.insert(layout->id, layout);
this->layoutIds.append(layout->id);
if (layout->blockdata.isEmpty()) {
// Fill layout using default fill settings
setNewLayoutBlockdata(layout);
@ -447,7 +402,15 @@ Layout *Project::createNewLayout(const Layout::Settings &settings, const Layout
setNewLayoutBorder(layout);
}
saveLayout(layout); // TODO: Ideally we shouldn't automatically save new layouts
// No need for a full load, we already have all the blockdata.
layout->loaded = loadLayoutTilesets(layout);
if (!layout->loaded) {
delete layout;
return nullptr;
}
this->mapLayouts.insert(layout->id, layout);
this->layoutIds.append(layout->id);
emit layoutCreated(layout);
@ -515,8 +478,9 @@ bool Project::readMapLayouts() {
const QString fullFilepath = QString("%1/%2").arg(this->root).arg(layoutsFilepath);
fileWatcher.addPath(fullFilepath);
QJsonDocument layoutsDoc;
if (!parser.tryParseJsonFile(&layoutsDoc, fullFilepath)) {
logError(QString("Failed to read map layouts from %1").arg(fullFilepath));
QString error;
if (!parser.tryParseJsonFile(&layoutsDoc, fullFilepath, &error)) {
logError(QString("Failed to read map layouts from '%1': %2").arg(fullFilepath).arg(error));
return false;
}
@ -701,6 +665,10 @@ void Project::saveMapGroups() {
for (const auto &groupName : this->groupNames) {
OrderedJson::array groupArr;
for (const auto &mapName : this->groupNameToMapNames.value(groupName)) {
if (this->mapCache.value(mapName) && !this->mapCache.value(mapName)->isPersistedToFile()) {
// This is a new map that hasn't been saved yet, don't add it to the global map groups list yet.
continue;
}
groupArr.push_back(mapName);
}
mapGroupsObj[groupName] = groupArr;
@ -986,25 +954,25 @@ bool Project::loadLayoutTilesets(Layout *layout) {
layout->tileset_primary = getTileset(layout->tileset_primary_label);
if (!layout->tileset_primary) {
QString defaultTileset = this->getDefaultPrimaryTilesetLabel();
logWarn(QString("%1 has invalid primary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_primary_label).arg(defaultTileset));
layout->tileset_primary_label = defaultTileset;
layout->tileset_primary = getTileset(layout->tileset_primary_label);
if (!layout->tileset_primary) {
logError(QString("Failed to set default primary tileset."));
logError(QString("%1 has invalid primary tileset '%2'.").arg(layout->name).arg(layout->tileset_primary_label));
return false;
}
logWarn(QString("%1 has invalid primary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_primary_label).arg(defaultTileset));
}
layout->tileset_secondary = getTileset(layout->tileset_secondary_label);
if (!layout->tileset_secondary) {
QString defaultTileset = this->getDefaultSecondaryTilesetLabel();
logWarn(QString("%1 has invalid secondary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_secondary_label).arg(defaultTileset));
layout->tileset_secondary_label = defaultTileset;
layout->tileset_secondary = getTileset(layout->tileset_secondary_label);
if (!layout->tileset_secondary) {
logError(QString("Failed to set default secondary tileset."));
logError(QString("%1 has invalid secondary tileset '%2'.").arg(layout->name).arg(layout->tileset_secondary_label));
return false;
}
logWarn(QString("%1 has invalid secondary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_secondary_label).arg(defaultTileset));
}
return true;
}
@ -1156,18 +1124,24 @@ void Project::writeBlockdata(QString path, const Blockdata &blockdata) {
}
}
void Project::saveAllMaps() {
for (auto *map : mapCache.values())
saveMap(map);
void Project::saveAll() {
for (auto map : this->mapCache) {
saveMap(map, true); // Avoid double-saving the layouts
}
for (auto layout : this->mapLayouts) {
saveLayout(layout);
}
saveGlobalData();
}
void Project::saveMap(Map *map) {
void Project::saveMap(Map *map, bool skipLayout) {
// Create/Modify a few collateral files for brand new maps.
const QString folderPath = projectConfig.getFilePath(ProjectFilePath::data_map_folders) + map->name();
const QString fullPath = QString("%1/%2").arg(this->root).arg(folderPath);
if (!map->isPersistedToFile()) {
if (!QDir::root().mkpath(fullPath)) {
logError(QString("Failed to create directory for new map: '%1'").arg(fullPath));
return;
}
// Create file data/maps/<map_name>/scripts.inc
@ -1290,12 +1264,25 @@ void Project::saveMap(Map *map) {
jsonDoc.dump(&mapFile);
mapFile.close();
saveLayout(map->layout());
if (!skipLayout) saveLayout(map->layout());
map->setClean();
}
void Project::saveLayout(Layout *layout) {
if (!layout || !layout->loaded)
return;
if (!layout->newFolderPath.isEmpty()) {
// Layout directory doesn't exist yet, create it now.
const QString fullPath = QString("%1/%2").arg(this->root).arg(layout->newFolderPath);
if (!QDir::root().mkpath(fullPath)) {
logError(QString("Failed to create directory for new layout: '%1'").arg(fullPath));
return;
}
layout->newFolderPath = QString();
}
saveLayoutBorder(layout);
saveLayoutBlockdata(layout);
@ -1318,7 +1305,7 @@ void Project::updateLayout(Layout *layout) {
}
}
void Project::saveAllDataStructures() {
void Project::saveGlobalData() {
saveMapLayouts();
saveMapGroups();
saveRegionMapSections();
@ -1645,13 +1632,15 @@ bool Project::readWildMonData() {
this->pokemonMaxLevel = qMax(this->pokemonMinLevel, this->pokemonMaxLevel);
// Read encounter data
QString wildMonJsonFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_wild_encounters));
const QString wildMonJsonBaseFilepath = projectConfig.getFilePath(ProjectFilePath::json_wild_encounters);
QString wildMonJsonFilepath = QString("%1/%2").arg(root).arg(wildMonJsonBaseFilepath);
fileWatcher.addPath(wildMonJsonFilepath);
OrderedJson::object wildMonObj;
if (!parser.tryParseOrderedJsonFile(&wildMonObj, wildMonJsonFilepath)) {
QString error;
if (!parser.tryParseOrderedJsonFile(&wildMonObj, wildMonJsonFilepath, &error)) {
// Failing to read wild encounters data is not a critical error, the encounter editor will just be disabled
logWarn(QString("Failed to read wild encounters from %1").arg(wildMonJsonFilepath));
logWarn(QString("Failed to read wild encounters from '%1': %2").arg(wildMonJsonBaseFilepath).arg(error));
return true;
}
@ -1780,8 +1769,9 @@ bool Project::readMapGroups() {
const QString filepath = root + "/" + projectConfig.getFilePath(ProjectFilePath::json_map_groups);
fileWatcher.addPath(filepath);
QJsonDocument mapGroupsDoc;
if (!parser.tryParseJsonFile(&mapGroupsDoc, filepath)) {
logError(QString("Failed to read map groups from %1").arg(filepath));
QString error;
if (!parser.tryParseJsonFile(&mapGroupsDoc, filepath, &error)) {
logError(QString("Failed to read map groups from '%1': %2").arg(filepath).arg(error));
return false;
}
@ -2287,8 +2277,9 @@ bool Project::readRegionMapSections() {
QJsonDocument doc;
const QString baseFilepath = projectConfig.getFilePath(ProjectFilePath::json_region_map_entries);
const QString filepath = QString("%1/%2").arg(this->root).arg(baseFilepath);
if (!parser.tryParseJsonFile(&doc, filepath)) {
logError(QString("Failed to read region map sections from '%1'").arg(baseFilepath));
QString error;
if (!parser.tryParseJsonFile(&doc, filepath, &error)) {
logError(QString("Failed to read region map sections from '%1': %2").arg(baseFilepath).arg(error));
return false;
}
fileWatcher.addPath(filepath);
@ -2413,8 +2404,9 @@ bool Project::readHealLocations() {
QJsonDocument doc;
const QString baseFilepath = projectConfig.getFilePath(ProjectFilePath::json_heal_locations);
const QString filepath = QString("%1/%2").arg(this->root).arg(baseFilepath);
if (!parser.tryParseJsonFile(&doc, filepath)) {
logError(QString("Failed to read heal locations from '%1'").arg(baseFilepath));
QString error;
if (!parser.tryParseJsonFile(&doc, filepath, &error)) {
logError(QString("Failed to read heal locations from '%1': %2").arg(baseFilepath).arg(error));
return false;
}
fileWatcher.addPath(filepath);
@ -2440,9 +2432,10 @@ bool Project::readItemNames() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_items)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_items);
fileWatcher.addPath(root + "/" + filename);
itemNames = parser.readCDefineNames(filename, regexList);
if (itemNames.isEmpty())
logWarn(QString("Failed to read item constants from %1").arg(filename));
QString error;
this->itemNames = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read item constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2450,9 +2443,10 @@ bool Project::readFlagNames() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_flags)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_flags);
fileWatcher.addPath(root + "/" + filename);
flagNames = parser.readCDefineNames(filename, regexList);
if (flagNames.isEmpty())
logWarn(QString("Failed to read flag constants from %1").arg(filename));
QString error;
this->flagNames = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read flag constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2460,9 +2454,10 @@ bool Project::readVarNames() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_vars)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_vars);
fileWatcher.addPath(root + "/" + filename);
varNames = parser.readCDefineNames(filename, regexList);
if (varNames.isEmpty())
logWarn(QString("Failed to read var constants from %1").arg(filename));
QString error;
this->varNames = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read var constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2470,18 +2465,20 @@ bool Project::readMovementTypes() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_movement_types)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_obj_event_movement);
fileWatcher.addPath(root + "/" + filename);
movementTypes = parser.readCDefineNames(filename, regexList);
if (movementTypes.isEmpty())
logWarn(QString("Failed to read movement type constants from %1").arg(filename));
QString error;
this->movementTypes = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read movement type constants from '%1': %2").arg(filename).arg(error));
return true;
}
bool Project::readInitialFacingDirections() {
QString filename = projectConfig.getFilePath(ProjectFilePath::initial_facing_table);
fileWatcher.addPath(root + "/" + filename);
facingDirections = parser.readNamedIndexCArray(filename, projectConfig.getIdentifier(ProjectIdentifier::symbol_facing_directions));
if (facingDirections.isEmpty())
logWarn(QString("Failed to read initial movement type facing directions from %1").arg(filename));
QString error;
this->facingDirections = parser.readNamedIndexCArray(filename, projectConfig.getIdentifier(ProjectIdentifier::symbol_facing_directions), &error);
if (!error.isEmpty())
logWarn(QString("Failed to read initial movement type facing directions from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2489,9 +2486,10 @@ bool Project::readMapTypes() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_map_types)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_map_types);
fileWatcher.addPath(root + "/" + filename);
mapTypes = parser.readCDefineNames(filename, regexList);
if (mapTypes.isEmpty())
logWarn(QString("Failed to read map type constants from %1").arg(filename));
QString error;
this->mapTypes = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read map type constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2499,9 +2497,10 @@ bool Project::readMapBattleScenes() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_battle_scenes)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_map_types);
fileWatcher.addPath(root + "/" + filename);
mapBattleScenes = parser.readCDefineNames(filename, regexList);
if (mapBattleScenes.isEmpty())
logWarn(QString("Failed to read map battle scene constants from %1").arg(filename));
QString error;
this->mapBattleScenes = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read map battle scene constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2509,9 +2508,10 @@ bool Project::readWeatherNames() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_weather)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_weather);
fileWatcher.addPath(root + "/" + filename);
weatherNames = parser.readCDefineNames(filename, regexList);
if (weatherNames.isEmpty())
logWarn(QString("Failed to read weather constants from %1").arg(filename));
QString error;
this->weatherNames = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read weather constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2522,9 +2522,10 @@ bool Project::readCoordEventWeatherNames() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_coord_event_weather)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_weather);
fileWatcher.addPath(root + "/" + filename);
coordEventWeatherNames = parser.readCDefineNames(filename, regexList);
if (coordEventWeatherNames.isEmpty())
logWarn(QString("Failed to read coord event weather constants from %1").arg(filename));
QString error;
this->coordEventWeatherNames = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read coord event weather constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2535,9 +2536,10 @@ bool Project::readSecretBaseIds() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_secret_bases)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_secret_bases);
fileWatcher.addPath(root + "/" + filename);
secretBaseIds = parser.readCDefineNames(filename, regexList);
if (secretBaseIds.isEmpty())
logWarn(QString("Failed to read secret base id constants from '%1'").arg(filename));
QString error;
this->secretBaseIds = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read secret base id constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2545,9 +2547,10 @@ bool Project::readBgEventFacingDirections() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_sign_facing_directions)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_event_bg);
fileWatcher.addPath(root + "/" + filename);
bgEventFacingDirections = parser.readCDefineNames(filename, regexList);
if (bgEventFacingDirections.isEmpty())
logWarn(QString("Failed to read bg event facing direction constants from %1").arg(filename));
QString error;
this->bgEventFacingDirections = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read bg event facing direction constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2555,9 +2558,10 @@ bool Project::readTrainerTypes() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_trainer_types)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_trainer_types);
fileWatcher.addPath(root + "/" + filename);
trainerTypes = parser.readCDefineNames(filename, regexList);
if (trainerTypes.isEmpty())
logWarn(QString("Failed to read trainer type constants from %1").arg(filename));
QString error;
this->trainerTypes = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read trainer type constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2568,13 +2572,14 @@ bool Project::readMetatileBehaviors() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_behaviors)};
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_metatile_behaviors);
fileWatcher.addPath(root + "/" + filename);
QMap<QString, int> defines = parser.readCDefinesByRegex(filename, regexList);
if (defines.isEmpty()) {
// Not having any metatile behavior names is ok (their values will be displayed instead).
// If the user's metatiles can have nonzero values then warn them, as they likely want names.
if (projectConfig.metatileBehaviorMask)
logWarn(QString("Failed to read metatile behaviors from %1.").arg(filename));
return true;
QString error;
QMap<QString, int> defines = parser.readCDefinesByRegex(filename, regexList, &error);
if (defines.isEmpty() && projectConfig.metatileBehaviorMask) {
// Not having any metatile behavior names is ok (their values will be displayed instead)
// but if the user's metatiles can have nonzero values then warn them, as they likely want names.
QString warning = QString("Failed to read metatile behaviors from '%1'").arg(filename);
if (!error.isEmpty()) warning += QString(": %1").arg(error);
logWarn(warning);
}
for (auto i = defines.cbegin(), end = defines.cend(); i != end; i++) {
@ -2590,9 +2595,10 @@ bool Project::readSongNames() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_music)};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_songs);
fileWatcher.addPath(root + "/" + filename);
this->songNames = parser.readCDefineNames(filename, regexList);
if (this->songNames.isEmpty())
logWarn(QString("Failed to read song names from %1.").arg(filename));
QString error;
this->songNames = parser.readCDefineNames(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read song names from '%1': %2").arg(filename).arg(error));
// Song names don't have a very useful order (esp. if we include SE_* values), so sort them alphabetically.
// The default song should be the first in the list, not the first alphabetically, so save that before sorting.
@ -2605,9 +2611,10 @@ bool Project::readObjEventGfxConstants() {
const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_obj_event_gfx)};
QString filename = projectConfig.getFilePath(ProjectFilePath::constants_obj_events);
fileWatcher.addPath(root + "/" + filename);
this->gfxDefines = parser.readCDefinesByRegex(filename, regexList);
if (this->gfxDefines.isEmpty())
logWarn(QString("Failed to read object event graphics constants from %1.").arg(filename));
QString error;
this->gfxDefines = parser.readCDefinesByRegex(filename, regexList, &error);
if (!error.isEmpty())
logWarn(QString("Failed to read object event graphics constants from '%1': %2").arg(filename).arg(error));
return true;
}
@ -2617,37 +2624,49 @@ bool Project::readMiscellaneousConstants() {
fileWatcher.addPath(root + "/" + filename);
QMap<QString, int> defines = parser.readCDefinesByName(filename, {maxObjectEventsName});
this->maxObjectEvents = 64; // Default value
auto it = defines.find(maxObjectEventsName);
if (it != defines.end()) {
if (it.value() > 0) {
Project::max_object_events = it.value();
this->maxObjectEvents = it.value();
} else {
logWarn(QString("Value for '%1' is %2, must be greater than 0. Using default (%3) instead.")
.arg(maxObjectEventsName)
.arg(it.value())
.arg(Project::max_object_events));
.arg(this->maxObjectEvents));
}
}
else {
logWarn(QString("Value for '%1' not found. Using default (%2) instead.")
.arg(maxObjectEventsName)
.arg(Project::max_object_events));
.arg(this->maxObjectEvents));
}
return true;
}
bool Project::readEventScriptLabels() {
globalScriptLabels.clear();
for (const auto &filePath : getEventScriptsFilePaths())
globalScriptLabels << ParseUtil::getGlobalScriptLabels(filePath);
this->globalScriptLabels.clear();
globalScriptLabels.sort(Qt::CaseInsensitive);
globalScriptLabels.removeDuplicates();
if (porymapConfig.loadAllEventScripts) {
for (const auto &filePath : getEventScriptsFilePaths())
this->globalScriptLabels << ParseUtil::getGlobalScriptLabels(filePath);
this->globalScriptLabels.sort(Qt::CaseInsensitive);
this->globalScriptLabels.removeDuplicates();
}
emit eventScriptLabelsRead();
return true;
}
void Project::insertGlobalScriptLabels(QStringList &scriptLabels) const {
if (this->globalScriptLabels.isEmpty())
return;
scriptLabels.append(this->globalScriptLabels);
scriptLabels.sort();
scriptLabels.removeDuplicates();
}
QString Project::fixPalettePath(QString path) {
static const QRegularExpression re_gbapal("\\.gbapal$");
path = path.replace(re_gbapal, ".pal");
@ -2703,92 +2722,220 @@ QStringList Project::getEventScriptsFilePaths() const {
return filePaths;
}
void Project::setEventPixmap(Event *event, bool forceLoad) {
void Project::loadEventPixmap(Event *event, bool forceLoad) {
if (event && (event->getPixmap().isNull() || forceLoad))
event->loadPixmap(this);
}
void Project::clearEventGraphics() {
qDeleteAll(eventGraphicsMap);
eventGraphicsMap.clear();
qDeleteAll(this->eventGraphicsMap);
this->eventGraphicsMap.clear();
}
bool Project::readEventGraphics() {
clearEventGraphics();
fileWatcher.addPaths(QStringList() << root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_pointers)
<< root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info)
<< root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables)
<< root + "/" + projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx));
const QString pointersFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_pointers);
const QString pointersName = projectConfig.getIdentifier(ProjectIdentifier::symbol_obj_event_gfx_pointers);
QMap<QString, QString> pointerHash = parser.readNamedIndexCArray(pointersFilepath, pointersName);
const QString gfxInfoFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info);
const QString picTablesFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables);
const QString gfxFilepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx);
fileWatcher.addPaths({pointersFilepath, gfxInfoFilepath, picTablesFilepath, gfxFilepath});
QStringList gfxNames = gfxDefines.keys();
// Read the table mapping OBJ_EVENT_GFX constants to the names of pointers to data about their graphics.
const QString pointersName = projectConfig.getIdentifier(ProjectIdentifier::symbol_obj_event_gfx_pointers);
const QMap<QString, QString> pointerMap = parser.readNamedIndexCArray(pointersFilepath, pointersName);
// The positions of each of the required members for the gfx info struct.
// For backwards compatibility if the struct doesn't use initializers.
static const auto gfxInfoMemberMap = QHash<int, QString>{
static const QHash<int, QString> gfxInfoMemberMap = {
{4, "width"},
{5, "height"},
{8, "inanimate"},
{11, "oam"},
{12, "subspriteTables"},
{14, "images"},
};
// Read the structs containing data about each of the event sprites.
auto gfxInfos = parser.readCStructs(gfxInfoFilepath, "", gfxInfoMemberMap);
QString filepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info);
auto gfxInfos = parser.readCStructs(filepath, "", gfxInfoMemberMap);
// We need data in both of these files to translate data from the structs above into the path for a .png file.
const QMap<QString, QStringList> picTables = parser.readCArrayMulti(picTablesFilepath);
const QMap<QString, QString> graphicIncbins = parser.readCIncbinMulti(gfxFilepath);
QMap<QString, QStringList> picTables = parser.readCArrayMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables));
QMap<QString, QString> graphicIncbins = parser.readCIncbinMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx));
for (auto i = this->gfxDefines.constBegin(); i != this->gfxDefines.constEnd(); i++) {
const QString gfxName = i.key();
for (QString gfxName : gfxNames) {
QString info_label = pointerHash[gfxName].replace("&", "");
// Strip the address-of operator to get the pointer's name. We'll use this name to get data about the event's sprite.
// If we don't recognize the name, ignore it. The event will use a default sprite.
QString info_label = pointerMap.value(gfxName);
info_label.replace("&", "");
if (!gfxInfos.contains(info_label))
continue;
const QHash<QString, QString> gfxInfoAttributes = gfxInfos[info_label];
auto gfxInfoAttributes = gfxInfos[info_label];
auto gfx = new EventGraphics;
auto eventGraphics = new EventGraphics;
eventGraphics->inanimate = ParseUtil::gameStringToBool(gfxInfoAttributes.value("inanimate"));
QString pic_label = gfxInfoAttributes.value("images");
QString dimensions_label = gfxInfoAttributes.value("oam");
QString subsprites_label = gfxInfoAttributes.value("subspriteTables");
QString gfx_label = picTables[pic_label].value(0);
// We need the .png filepath for the event's sprite. This is buried behind a few levels of indirection.
// The 'images' field gives us the name of the table containing the sprite's image data.
// The entries in this table are expected to be in the format (PngSymbolName, ...).
// We extract the symbol name of the .png's INCBIN'd data by looking at the first entry in this table.
// Once we have the .png's symbol name we can get the actual filepath from its INCBIN.
QString gfx_label = picTables[gfxInfoAttributes.value("images")].value(0);
static const QRegularExpression re_parens("[\\(\\)]");
gfx_label = gfx_label.section(re_parens, 1, 1);
QString path = graphicIncbins[gfx_label];
gfx->filepath = fixGraphicPath(graphicIncbins[gfx_label]);
if (!path.isNull()) {
path = fixGraphicPath(path);
eventGraphics->spritesheet = QImage(root + "/" + path);
if (!eventGraphics->spritesheet.isNull()) {
// Infer the sprite dimensions from the OAM labels.
static const QRegularExpression re("\\S+_(\\d+)x(\\d+)");
QRegularExpressionMatch dimensionMatch = re.match(dimensions_label);
QRegularExpressionMatch oamTablesMatch = re.match(subsprites_label);
if (oamTablesMatch.hasMatch()) {
eventGraphics->spriteWidth = oamTablesMatch.captured(1).toInt(nullptr, 0);
eventGraphics->spriteHeight = oamTablesMatch.captured(2).toInt(nullptr, 0);
} else if (dimensionMatch.hasMatch()) {
eventGraphics->spriteWidth = dimensionMatch.captured(1).toInt(nullptr, 0);
eventGraphics->spriteHeight = dimensionMatch.captured(2).toInt(nullptr, 0);
} else {
eventGraphics->spriteWidth = eventGraphics->spritesheet.width();
eventGraphics->spriteHeight = eventGraphics->spritesheet.height();
}
}
} else {
eventGraphics->spritesheet = QImage();
eventGraphics->spriteWidth = 16;
eventGraphics->spriteHeight = 16;
// Note: gfx has a 'spritesheet' field that will contain a QImage for the event's sprite.
// We don't create this QImage yet. Reading the image now is unnecessary overhead for startup.
// We'll read the image file when the event's sprite is first requested to be drawn.
// The .png file is expected to be a spritesheet that can have multiple frames.
// We only want to show one frame at a time, so we need to know the dimensions of each frame.
// The true dimensions are buried in the subsprite data, so we try to infer the dimensions from the name of the 'subspriteTables' symbol.
// If we are unable to do this, we can read the dimensions from the width and height fields.
// This is much more straightforward, but the numbers are not necessarily accurate (one vanilla event sprite,
// the Town Map in FRLG, has width/height values that differ from its true dimensions).
static const QRegularExpression re_dimensions("\\S+_(\\d+)x(\\d+)");
const QRegularExpressionMatch dimensionsMatch = re_dimensions.match(gfxInfoAttributes.value("subspriteTables"));
if (dimensionsMatch.hasMatch()) {
gfx->spriteWidth = dimensionsMatch.captured(1).toInt(nullptr, 0);
gfx->spriteHeight = dimensionsMatch.captured(2).toInt(nullptr, 0);
} else if (gfxInfoAttributes.contains("width") && gfxInfoAttributes.contains("height")) {
gfx->spriteWidth = gfxInfoAttributes.value("width").toInt(nullptr, 0);
gfx->spriteHeight = gfxInfoAttributes.value("height").toInt(nullptr, 0);
}
eventGraphicsMap.insert(gfxName, eventGraphics);
// If we fail to get sprite dimensions then they should remain -1, and the sprite will use the full spritesheet as its image.
if (gfx->spriteWidth <= 0 || gfx->spriteHeight <= 0) {
gfx->spriteWidth = -1;
gfx->spriteHeight = -1;
}
// Inanimate events will only ever use the first frame of their spritesheet.
gfx->inanimate = ParseUtil::gameStringToBool(gfxInfoAttributes.value("inanimate"));
this->eventGraphicsMap.insert(gfxName, gfx);
}
return true;
}
QPixmap Project::getEventPixmap(const QString &gfxName, const QString &movementName) {
struct FrameData {
int index = 0;
bool hFlip = false;
};
// TODO: Expose as a setting to users
static const QMap<QString, FrameData> directionToFrameData = {
{"DIR_SOUTH", { .index = 0, .hFlip = false }},
{"DIR_NORTH", { .index = 1, .hFlip = false }},
{"DIR_WEST", { .index = 2, .hFlip = false }},
{"DIR_EAST", { .index = 2, .hFlip = true }}, // East-facing sprite is just the West-facing sprite mirrored
};
const QString direction = this->facingDirections.value(movementName, "DIR_SOUTH");
auto frameData = directionToFrameData.value(direction);
return getEventPixmap(gfxName, frameData.index, frameData.hFlip);
}
QPixmap Project::getEventPixmap(const QString &gfxName, int frame, bool hFlip) {
QPixmap pixmap;
const QString cacheKey = QString("EVENT#%1#%2#%3").arg(gfxName).arg(frame).arg(hFlip ? "1" : "0");
if (QPixmapCache::find(cacheKey, &pixmap)) {
return pixmap;
}
EventGraphics* gfx = this->eventGraphicsMap.value(gfxName, nullptr);
if (!gfx) {
// Invalid gfx constant. If this is a number, try to use that instead.
bool ok;
int gfxNum = ParseUtil::gameStringToInt(gfxName, &ok);
if (ok && gfxNum < this->gfxDefines.count()) {
gfx = this->eventGraphicsMap.value(this->gfxDefines.key(gfxNum, "NULL"), nullptr);
}
}
if (gfx && !gfx->loaded) {
// This is the first request for this event's sprite. We'll attempt to load it now.
if (!gfx->filepath.isEmpty()) {
gfx->spritesheet = QImage(QString("%1/%2").arg(this->root).arg(gfx->filepath));
if (gfx->spritesheet.isNull()) {
logWarn(QString("Failed to open '%1' for event's sprite. Event will use a default sprite instead.").arg(gfx->filepath));
} else {
// If we were unable to find the dimensions of a frame within the spritesheet we'll use the full image dimensions.
if (gfx->spriteWidth <= 0) {
gfx->spriteWidth = gfx->spritesheet.width();
}
if (gfx->spriteHeight <= 0) {
gfx->spriteHeight = gfx->spritesheet.height();
}
}
}
// Set this whether we were successful or not, we only need to try to load it once.
gfx->loaded = true;
}
if (!gfx || gfx->spritesheet.isNull()) {
// Either we didn't recognize the gfxName, or we were unable to load the sprite's image.
return QPixmap();
}
QImage img;
if (gfx->inanimate) {
img = gfx->spritesheet.copy(0, 0, gfx->spriteWidth, gfx->spriteHeight);
} else {
int x = 0;
int y = 0;
// Get frame's position in spritesheet.
// Assume horizontal layout. If position would exceed sheet width, try vertical layout.
if ((frame + 1) * gfx->spriteWidth <= gfx->spritesheet.width()) {
x = frame * gfx->spriteWidth;
} else if ((frame + 1) * gfx->spriteHeight <= gfx->spritesheet.height()) {
y = frame * gfx->spriteHeight;
}
img = gfx->spritesheet.copy(x, y, gfx->spriteWidth, gfx->spriteHeight);
if (hFlip) {
img = img.transformed(QTransform().scale(-1, 1));
}
}
// Set first palette color fully transparent.
img.setColor(0, qRgba(0, 0, 0, 0));
pixmap = QPixmap::fromImage(img);
QPixmapCache::insert(cacheKey, pixmap);
return pixmap;
}
QPixmap Project::getEventPixmap(Event::Group group) {
if (group == Event::Group::None)
return QPixmap();
QPixmap pixmap;
const QString cacheKey = QString("EVENT#%1").arg(Event::groupToString(group));
if (QPixmapCache::find(cacheKey, &pixmap)) {
return pixmap;
}
const int defaultWidth = 16;
const int defaultHeight = 16;
static const QPixmap defaultIcons = QPixmap(":/images/Entities_16x16.png");
QPixmap defaultIcon = QPixmap(defaultIcons.copy(static_cast<int>(group) * defaultWidth, 0, defaultWidth, defaultHeight));
// Custom event icons may be provided by the user.
QString customIconPath = projectConfig.getEventIconPath(group);
if (customIconPath.isEmpty()) {
// No custom icon specified, use the default icon.
pixmap = defaultIcon;
} else {
// Try to load custom icon
QString validPath = Project::getExistingFilepath(customIconPath);
if (!validPath.isEmpty()) customIconPath = validPath; // Otherwise allow it to fail with the original path
pixmap = QPixmap(customIconPath);
if (pixmap.isNull()) {
pixmap = defaultIcon;
logWarn(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath));
}
}
QPixmapCache::insert(cacheKey, pixmap);
return pixmap;
}
bool Project::readSpeciesIconPaths() {
this->speciesToIconPath.clear();
@ -2983,9 +3130,14 @@ bool Project::calculateDefaultMapSize(){
return true;
}
int Project::getMaxObjectEvents()
{
return Project::max_object_events;
// Object events have their own limit specified by ProjectIdentifier::define_obj_event_count.
// The default value for this is 64. All events (object events included) are also limited by
// the data types of the event counters in the project. This would normally be u8, so the limit is 255.
// We let the users tell us this limit in case they change these data types.
int Project::getMaxEvents(Event::Group group) {
if (group == Event::Group::Object)
return qMin(this->maxObjectEvents, projectConfig.maxEventsPerGroup);
return projectConfig.maxEventsPerGroup;
}
QString Project::getEmptyMapDefineName() {

View File

@ -11,7 +11,8 @@ ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection)
this->setEditable(true);
setFlag(ItemIsFocusable, true);
this->basePixmap = pixmap();
refresh();
updateOrigin();
render(false);
// If the connection changes externally we want to update the pixmap to reflect the change.
connect(connection, &MapConnection::offsetChanged, this, &ConnectionPixmapItem::updatePos);

View File

@ -8,11 +8,11 @@ static unsigned currentActionId = 0;
void DraggablePixmapItem::updatePosition() {
int x = event->getPixelX();
int y = event->getPixelY();
int x = this->event->getPixelX();
int y = this->event->getPixelY();
setX(x);
setY(y);
if (editor->selected_events && editor->selected_events->contains(this)) {
if (this->editor->selectedEvents.contains(this->event)) {
setZValue(event->getY() + 1);
} else {
setZValue(event->getY());
@ -27,7 +27,7 @@ void DraggablePixmapItem::emitPositionChanged() {
}
void DraggablePixmapItem::updatePixmap() {
editor->project->setEventPixmap(event, true);
editor->project->loadEventPixmap(event, true);
this->updatePosition();
editor->redrawEventPixmapItem(this);
emit spriteChanged(event->getPixmap());
@ -40,10 +40,10 @@ void DraggablePixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *mouse) {
this->lastPos = Metatile::coordFromPixmapCoord(mouse->scenePos());
bool selectionToggle = mouse->modifiers() & Qt::ControlModifier;
if (selectionToggle || !editor->selected_events->contains(this)) {
if (selectionToggle || !this->editor->selectedEvents.contains(this->event)) {
// User is either toggling this selection on/off as part of a group selection,
// or they're newly selecting just this item.
this->editor->selectMapEvent(this, selectionToggle);
this->editor->selectMapEvent(this->event, selectionToggle);
} else {
// This item is already selected and the user isn't toggling the selection, so there are 4 possibilities:
// 1. This is the only selected event, and the selection is pointless.
@ -84,10 +84,8 @@ void DraggablePixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *mouse) {
emit this->editor->map_item->hoveredMapMetatileChanged(pos);
QList <Event *> selectedEvents;
if (editor->selected_events->contains(this)) {
for (DraggablePixmapItem *item : *editor->selected_events) {
selectedEvents.append(item->event);
}
if (this->editor->selectedEvents.contains(this->event)) {
selectedEvents = this->editor->selectedEvents;
} else {
selectedEvents.append(this->event);
}
@ -103,7 +101,7 @@ void DraggablePixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouse) {
if (this->releaseSelectionQueued) {
this->releaseSelectionQueued = false;
if (Metatile::coordFromPixmapCoord(mouse->scenePos()) == this->lastPos)
this->editor->selectMapEvent(this);
this->editor->selectMapEvent(this->event);
}
}

View File

@ -173,12 +173,19 @@ void EventFrame::setActive(bool active) {
}
void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * project) {
// The script dropdown is populated with scripts used by the map's events and from its scripts file.
if (this->event->getMap())
combo->addItems(this->event->getMap()->getScriptLabels(this->event->getEventGroup()));
// The script dropdown and autocomplete are populated with scripts used by the map's events and from its scripts file.
if (!this->event->getMap())
return;
// The dropdown's autocomplete has all script labels across the full project.
auto completer = new QCompleter(project->globalScriptLabels, combo);
QStringList scripts = this->event->getMap()->getScriptLabels(this->event->getEventGroup());
combo->addItems(scripts);
// Depending on the settings, the autocomplete may also contain all global scripts.
if (porymapConfig.loadAllEventScripts) {
project->insertGlobalScriptLabels(scripts);
}
auto completer = new QCompleter(scripts, combo);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel);
completer->setFilterMode(Qt::MatchContains);
@ -188,6 +195,9 @@ void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * proj
if (popup) popup->setUniformItemSizes(true);
combo->setCompleter(completer);
// If the project changes the script labels, update the EventFrame.
connect(project, &Project::eventScriptLabelsRead, this, &EventFrame::invalidateValues, Qt::UniqueConnection);
}

View File

@ -517,7 +517,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) {
|| (m_settings.showBGs && group == Event::Group::Bg)
|| (m_settings.showTriggers && group == Event::Group::Coord)
|| (m_settings.showHealLocations && group == Event::Group::Heal)) {
m_editor->project->setEventPixmap(event);
m_editor->project->loadEventPixmap(event);
eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage());
}
}

View File

@ -235,17 +235,23 @@ QMimeData *MapGroupModel::mimeData(const QModelIndexList &indexes) const {
QDataStream stream(&encodedData, QIODevice::WriteOnly);
// if dropping a selection containing a group(s) and map(s), clear all selection but first group.
bool droppingGroups = false;
// if dropping a selection containing both group(s) and map(s), only copy groups
for (const QModelIndex &index : indexes) {
if (index.isValid() && data(index, MapListUserRoles::TypeRole).toString() == "map_group") {
QString groupName = data(index, MapListUserRoles::NameRole).toString();
stream << groupName;
mimeData->setData("application/porymap.mapgroupmodel.group", encodedData);
mimeData->setData("application/porymap.mapgroupmodel.source.row", QByteArray::number(index.row()));
return mimeData;
stream << index.row();
droppingGroups = true;
}
}
if (droppingGroups) {
mimeData->setData("application/porymap.mapgroupmodel.group", encodedData);
return mimeData;
}
for (const QModelIndex &index : indexes) {
if (index.isValid()) {
QString mapName = data(index, MapListUserRoles::NameRole).toString();
@ -277,33 +283,48 @@ bool MapGroupModel::dropMimeData(const QMimeData *data, Qt::DropAction action, i
if (parentIndex.row() != -1 || parentIndex.column() != -1) {
return false;
}
if (row < 0) {
row = this->rowCount(parentIndex);
}
QByteArray encodedData = data->data("application/porymap.mapgroupmodel.group");
QDataStream stream(&encodedData, QIODevice::ReadOnly);
QString groupName;
QStringList droppedGroups;
QList<int> droppedGroupIndexes;
int rowCount = 0;
while (!stream.atEnd()) {
QString groupName;
stream >> groupName;
int groupIndex;
stream >> groupIndex;
droppedGroups << groupName;
droppedGroupIndexes << groupIndex;
rowCount++;
}
this->insertRow(row, parentIndex);
for (int r = 0; r < rowCount; r++) {
QString groupName = droppedGroups[r];
// copy children to new node
int sourceRow = droppedGroupIndexes[r];
QModelIndex originIndex = this->index(sourceRow, 0);
QModelIndexList children;
QStringList mapsToMove;
for (int i = 0; i < this->rowCount(originIndex); ++i ) {
children << this->index(i, 0, originIndex);
mapsToMove << this->index(i, 0 , originIndex).data(MapListUserRoles::NameRole).toString();
}
// copy children to new node
int sourceRow = data->data("application/porymap.mapgroupmodel.source.row").toInt();
QModelIndex originIndex = this->index(sourceRow, 0);
QModelIndexList children;
QStringList mapsToMove;
for (int i = 0; i < this->rowCount(originIndex); ++i ) {
children << this->index( i, 0, originIndex);
mapsToMove << this->index( i, 0 , originIndex).data(MapListUserRoles::NameRole).toString();
}
this->insertRow(row + r, parentIndex);
QModelIndex groupIndex = index(row + r, 0, parentIndex);
QStandardItem *groupItem = this->itemFromIndex(groupIndex);
createMapFolderItem(groupName, groupItem);
QModelIndex groupIndex = index(row, 0, parentIndex);
QStandardItem *groupItem = this->itemFromIndex(groupIndex);
createMapFolderItem(groupName, groupItem);
for (QString mapName : mapsToMove) {
QStandardItem *mapItem = createMapItem(mapName);
groupItem->appendRow(mapItem);
for (QString mapName : mapsToMove) {
QStandardItem *mapItem = createMapItem(mapName);
groupItem->appendRow(mapItem);
}
}
}
else if (data->hasFormat("application/porymap.mapgroupmodel.map")) {

View File

@ -27,8 +27,12 @@ void Prefab::loadPrefabs() {
QJsonDocument prefabDoc;
QString validPath = Project::getExistingFilepath(filepath);
if (validPath.isEmpty() || !parser.tryParseJsonFile(&prefabDoc, validPath)) {
logError(QString("Failed to read prefab data from %1").arg(filepath));
if (validPath.isEmpty())
return;
QString error;
if (!parser.tryParseJsonFile(&prefabDoc, validPath, &error)) {
logError(QString("Failed to read prefab data from %1: %2").arg(filepath).arg(error));
return;
}
filepath = validPath;

View File

@ -56,6 +56,7 @@ void PreferenceEditor::updateFields() {
ui->checkBox_OpenRecentProject->setChecked(porymapConfig.reopenOnLaunch);
ui->checkBox_CheckForUpdates->setChecked(porymapConfig.checkForUpdates);
ui->checkBox_DisableEventWarning->setChecked(porymapConfig.eventDeleteWarningDisabled);
ui->checkBox_AutocompleteAllScripts->setChecked(porymapConfig.loadAllEventScripts);
}
void PreferenceEditor::saveFields() {
@ -64,6 +65,11 @@ void PreferenceEditor::saveFields() {
porymapConfig.theme = theme;
emit themeChanged(theme);
}
bool loadAllEventScripts = ui->checkBox_AutocompleteAllScripts->isChecked();
if (loadAllEventScripts != porymapConfig.loadAllEventScripts) {
porymapConfig.loadAllEventScripts = loadAllEventScripts;
emit scriptSettingsChanged(loadAllEventScripts);
}
porymapConfig.eventSelectionShapeMode = ui->radioButton_OnSprite->isChecked() ? QGraphicsPixmapItem::MaskShape : QGraphicsPixmapItem::BoundingRectShape;
porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text();
porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text();

View File

@ -136,6 +136,7 @@ void ProjectSettingsEditor::initUi() {
ui->spinBox_UnusedTileNormal->setMaximum(Tile::maxValue);
ui->spinBox_UnusedTileCovered->setMaximum(Tile::maxValue);
ui->spinBox_UnusedTileSplit->setMaximum(Tile::maxValue);
ui->spinBox_MaxEvents->setMaximum(INT_MAX);
// The values for some of the settings we provide in this window can be determined using constants in the user's projects.
// If the user has these constants we disable these settings in the UI -- they can modify them using their constants.
@ -465,6 +466,7 @@ void ProjectSettingsEditor::refresh() {
ui->spinBox_UnusedTileNormal->setValue(projectConfig.unusedTileNormal);
ui->spinBox_UnusedTileCovered->setValue(projectConfig.unusedTileCovered);
ui->spinBox_UnusedTileSplit->setValue(projectConfig.unusedTileSplit);
ui->spinBox_MaxEvents->setValue(projectConfig.maxEventsPerGroup);
// Set (and sync) border metatile IDs
this->setBorderMetatileIds(false, projectConfig.newMapBorderMetatileIds);
@ -539,6 +541,7 @@ void ProjectSettingsEditor::save() {
projectConfig.unusedTileNormal = ui->spinBox_UnusedTileNormal->value();
projectConfig.unusedTileCovered = ui->spinBox_UnusedTileCovered->value();
projectConfig.unusedTileSplit = ui->spinBox_UnusedTileSplit->value();
projectConfig.maxEventsPerGroup = ui->spinBox_MaxEvents->value();
// Save line edit settings
projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text();

View File

@ -120,26 +120,20 @@ bool RegionMapEditor::saveRegionMapEntries() {
void buildEmeraldDefaults(poryjson::Json &json) {
ParseUtil parser;
QString emeraldDefault = parser.readTextFile(":/text/region_map_default_emerald.json");
QString err;
json = poryjson::Json::parse(emeraldDefault, err);
json = poryjson::Json::parse(emeraldDefault);
}
void buildRubyDefaults(poryjson::Json &json) {
ParseUtil parser;
QString emeraldDefault = parser.readTextFile(":/text/region_map_default_ruby.json");
QString err;
json = poryjson::Json::parse(emeraldDefault, err);
json = poryjson::Json::parse(emeraldDefault);
}
void buildFireredDefaults(poryjson::Json &json) {
ParseUtil parser;
QString fireredDefault = parser.readTextFile(":/text/region_map_default_firered.json");
QString err;
json = poryjson::Json::parse(fireredDefault, err);
json = poryjson::Json::parse(fireredDefault);
}
poryjson::Json RegionMapEditor::buildDefaultJson() {
@ -199,8 +193,7 @@ bool RegionMapEditor::buildConfigDialog() {
poryjson::Json::object newJson;
poryjson::Json::array mapArr;
for (auto item : regionMapList->findItems("*", Qt::MatchWildcard)) {
QString err;
poryjson::Json itemJson = poryjson::Json::parse(item->data(Qt::UserRole).toString(), err);
poryjson::Json itemJson = poryjson::Json::parse(item->data(Qt::UserRole).toString());
mapArr.append(itemJson);
}
newJson["region_maps"] = mapArr;
@ -213,8 +206,7 @@ bool RegionMapEditor::buildConfigDialog() {
connect(regionMapList, &QListWidget::itemDoubleClicked, [this, &rmConfigJsonUpdate, updateMapList, regionMapList](QListWidgetItem *item) {
int itemIndex = regionMapList->row(item);
QString err;
poryjson::Json clickedJson = poryjson::Json::parse(item->data(Qt::UserRole).toString(), err);
poryjson::Json clickedJson = poryjson::Json::parse(item->data(Qt::UserRole).toString());
RegionMapPropertiesDialog dialog(this);
dialog.setProject(this->project);