Merge pull request #787 from GriffinRichards/config-json

Replace old config format
This commit is contained in:
GriffinR 2026-02-24 18:33:13 -05:00 committed by GitHub
commit 8b4ccc12ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 4511 additions and 2613 deletions

View File

@ -2289,7 +2289,7 @@ All constants are accessible via the global ``constants`` object.
.. js:attribute:: constants.base_game_version
The string value of the config setting ``base_game_version``. This will either be ``pokeruby``, ``pokefirered``, or ``pokeemerald``.
The string value of the config setting ``base_game_version``. This will either be ``pokeruby``, ``pokefirered``, ``pokeemerald``, or an empty string.
.. js:attribute:: constants.version.major

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>264</width>
<height>173</height>
<width>387</width>
<height>357</height>
</rect>
</property>
<property name="windowTitle">
@ -36,8 +36,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>240</width>
<height>109</height>
<width>363</width>
<height>293</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">

View File

@ -2,6 +2,14 @@
<ui version="4.0">
<class>NewLayoutForm</class>
<widget class="QWidget" name="NewLayoutForm">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>196</width>
<height>331</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
@ -168,6 +176,9 @@
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
@ -204,6 +215,9 @@
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>559</width>
<height>614</height>
<width>467</width>
<height>428</height>
</rect>
</property>
<property name="windowTitle">
@ -39,8 +39,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>535</width>
<height>550</height>
<width>443</width>
<height>364</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
@ -129,6 +129,9 @@
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
<item row="8" column="1">
@ -171,6 +174,9 @@
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
<item row="8" column="0">

View File

@ -1,521 +0,0 @@
#pragma once
#ifndef CONFIG_H
#define CONFIG_H
#include <QString>
#include <QObject>
#include <QByteArrayList>
#include <QSize>
#include <QKeySequence>
#include <QMultiMap>
#include <QDateTime>
#include <QUrl>
#include <QVersionNumber>
#include <QGraphicsPixmapItem>
#include <QFontDatabase>
#include <QStandardPaths>
#include <QColorSpace>
#include <set>
#include "events.h"
#include "gridsettings.h"
extern const QVersionNumber porymapVersion;
// Distance in pixels from the edge of a GBA screen (240x160) to the center 16x16 pixels.
#define GBA_H_DIST_TO_CENTER ((240-16)/2)
#define GBA_V_DIST_TO_CENTER ((160-16)/2)
#define CONFIG_BACKWARDS_COMPATABILITY
enum ScriptAutocompleteMode {
MapOnly,
MapAndCommon,
All,
};
class KeyValueConfigBase
{
public:
bool save();
bool load(const QString &dir = QString());
void setRoot(const QString &dir);
QString root() const { return m_root; }
QString filepath() const { return m_filepath; }
QString filename() const { return m_filename; }
explicit KeyValueConfigBase(const QString &filename)
: m_root(QString()),
m_filename(filename),
m_filepath(filename)
{ };
virtual ~KeyValueConfigBase() {};
virtual void reset() = 0;
protected:
virtual void parseConfigKeyValue(QString key, QString value) = 0;
virtual QMap<QString, QString> getKeyValueMap() = 0;
virtual void init() = 0;
virtual void setUnreadKeys() = 0;
static bool getConfigBool(const QString &key, const QString &value);
static int getConfigInteger(const QString &key, const QString &value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0);
static uint32_t getConfigUint32(const QString &key, const QString &value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0);
static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = QColor(Qt::black));
static QString toConfigColor(const QColor &color);
QString m_root;
QString m_filename;
QString m_filepath;
};
class PorymapConfig: public KeyValueConfigBase
{
public:
PorymapConfig();
virtual void reset() override;
void addRecentProject(QString project);
void setRecentProjects(QStringList projects);
QString getRecentProject();
QStringList getRecentProjects();
void setMainGeometry(QByteArray, QByteArray, QByteArray, QByteArray, QByteArray);
void setTilesetEditorGeometry(QByteArray, QByteArray, QByteArray);
void setPaletteEditorGeometry(QByteArray, QByteArray);
void setRegionMapEditorGeometry(QByteArray, QByteArray);
void setProjectSettingsEditorGeometry(QByteArray, QByteArray);
void setCustomScriptsEditorGeometry(QByteArray, QByteArray);
QMap<QString, QByteArray> getMainGeometry();
QMap<QString, QByteArray> getTilesetEditorGeometry();
QMap<QString, QByteArray> getPaletteEditorGeometry();
QMap<QString, QByteArray> getRegionMapEditorGeometry();
QMap<QString, QByteArray> getProjectSettingsEditorGeometry();
QMap<QString, QByteArray> getCustomScriptsEditorGeometry();
static QFont defaultMapListFont() { return QFontDatabase::systemFont(QFontDatabase::FixedFont); }
bool reopenOnLaunch;
bool projectManuallyClosed;
int mapListTab;
bool mapListEditGroupsEnabled;
QMap<int, bool> mapListHideEmptyEnabled;
bool mapListLayoutsSorted;
bool mapListLocationsSorted;
bool prettyCursors;
bool mirrorConnectingMaps;
bool showDiveEmergeMaps;
int diveEmergeMapOpacity;
int diveMapOpacity;
int emergeMapOpacity;
int collisionOpacity;
int collisionZoom;
int metatilesZoom;
int tilesetEditorMetatilesZoom;
int tilesetEditorTilesZoom;
Qt::Orientation tilesetEditorLayerOrientation;
bool showPlayerView;
bool showCursorTile;
bool showBorder;
bool showGrid;
bool showTilesetEditorMetatileGrid;
bool showTilesetEditorLayerGrid;
bool showTilesetEditorDivider;
bool showTilesetEditorRawAttributes;
bool showPaletteEditorUnusedColors;
bool monitorFiles;
bool tilesetCheckerboardFill;
bool newMapHeaderSectionExpanded;
QString theme;
QString wildMonChartTheme;
QString textEditorOpenFolder;
QString textEditorGotoLine;
int paletteEditorBitDepth;
int projectSettingsTab;
ScriptAutocompleteMode scriptAutocompleteMode;
bool warpBehaviorWarningDisabled;
bool eventDeleteWarningDisabled;
bool eventOverlayEnabled;
bool checkForUpdates;
bool showProjectLoadingScreen;
QDateTime lastUpdateCheckTime;
QVersionNumber lastUpdateCheckVersion;
QMap<QUrl, QDateTime> rateLimitTimes;
QGraphicsPixmapItem::ShapeMode eventSelectionShapeMode;
QByteArray wildMonChartGeometry;
QByteArray newMapDialogGeometry;
QByteArray newLayoutDialogGeometry;
bool shownInGameReloadMessage;
GridSettings gridSettings;
// Prefer over QSet to prevent shuffling elements when writing the config file.
std::set<LogType> statusBarLogTypes;
QFont applicationFont;
QFont mapListFont;
int imageExportColorSpaceId;
QMap<QString,QString> trustedScriptHashes;
protected:
virtual void parseConfigKeyValue(QString key, QString value) override;
virtual QMap<QString, QString> getKeyValueMap() override;
virtual void init() override {};
virtual void setUnreadKeys() override {};
private:
QString stringFromByteArray(const QByteArray&);
QByteArray bytesFromString(const QString&);
QStringList recentProjects;
QByteArray mainWindowGeometry;
QByteArray mainWindowState;
QByteArray mapSplitterState;
QByteArray mainSplitterState;
QByteArray metatilesSplitterState;
QByteArray tilesetEditorGeometry;
QByteArray tilesetEditorState;
QByteArray tilesetEditorSplitterState;
QByteArray paletteEditorGeometry;
QByteArray paletteEditorState;
QByteArray regionMapEditorGeometry;
QByteArray regionMapEditorState;
QByteArray projectSettingsEditorGeometry;
QByteArray projectSettingsEditorState;
QByteArray customScriptsEditorGeometry;
QByteArray customScriptsEditorState;
};
extern PorymapConfig porymapConfig;
enum BaseGameVersion {
none,
pokeruby,
pokefirered,
pokeemerald,
};
enum ProjectIdentifier {
symbol_facing_directions,
symbol_obj_event_gfx_pointers,
symbol_pokemon_icon_table,
symbol_attribute_table,
symbol_tilesets_prefix,
symbol_dynamic_map_name,
define_obj_event_count,
define_min_level,
define_max_level,
define_max_encounter_rate,
define_tiles_primary,
define_tiles_total,
define_metatiles_primary,
define_pals_primary,
define_pals_total,
define_tiles_per_metatile,
define_map_size,
define_map_offset_width,
define_map_offset_height,
define_mask_metatile,
define_mask_collision,
define_mask_elevation,
define_mask_behavior,
define_mask_layer,
define_attribute_behavior,
define_attribute_layer,
define_attribute_terrain,
define_attribute_encounter,
define_metatile_label_prefix,
define_heal_locations_prefix,
define_layout_prefix,
define_map_prefix,
define_map_dynamic,
define_map_empty,
define_map_section_prefix,
define_map_section_empty,
define_species_prefix,
define_species_empty,
regex_behaviors,
regex_obj_event_gfx,
regex_items,
regex_flags,
regex_vars,
regex_movement_types,
regex_map_types,
regex_battle_scenes,
regex_weather,
regex_coord_event_weather,
regex_secret_bases,
regex_sign_facing_directions,
regex_trainer_types,
regex_music,
regex_encounter_types,
regex_terrain_types,
pals_output_extension,
tiles_output_extension,
};
enum ProjectFilePath {
data_map_folders,
data_scripts_folders,
data_layouts_folders,
data_primary_tilesets_folders,
data_secondary_tilesets_folders,
data_event_scripts,
json_map_groups,
json_layouts,
json_wild_encounters,
json_heal_locations,
json_region_map_entries,
json_region_porymap_cfg,
tilesets_headers,
tilesets_graphics,
tilesets_metatiles,
tilesets_headers_asm,
tilesets_graphics_asm,
tilesets_metatiles_asm,
data_obj_event_gfx_pointers,
data_obj_event_gfx_info,
data_obj_event_pic_tables,
data_obj_event_gfx,
data_pokemon_gfx,
constants_global,
constants_items,
constants_flags,
constants_vars,
constants_weather,
constants_songs,
constants_pokemon,
constants_map_types,
constants_trainer_types,
constants_secret_bases,
constants_obj_event_movement,
constants_obj_events,
constants_event_bg,
constants_metatile_labels,
constants_metatile_behaviors,
constants_species,
constants_fieldmap,
global_fieldmap,
fieldmap,
initial_facing_table,
wild_encounter,
pokemon_icon_table,
pokemon_gfx,
};
class ProjectConfig: public KeyValueConfigBase
{
public:
ProjectConfig();
virtual void reset() override {
this->baseGameVersion = BaseGameVersion::pokeemerald;
// Reset non-version-specific settings
this->usePoryScript = false;
this->tripleLayerMetatilesEnabled = false;
this->defaultMetatileId = 1;
this->defaultElevation = 3;
this->defaultCollision = 0;
this->defaultMapSize = QSize(20,20);
this->defaultPrimaryTileset = "gTileset_General";
this->prefabFilepath = QString();
this->prefabImportPrompted = false;
this->tilesetsHaveCallback = true;
this->tilesetsHaveIsCompressed = true;
this->transparencyColor = QColor(Qt::black);
this->preserveMatchingOnlyData = false;
this->filePaths.clear();
this->eventIconPaths.clear();
this->pokemonIconPaths.clear();
this->eventsTabIconPath = QString();
this->collisionSheetPath = QString();
this->collisionSheetSize = QSize(2, 16);
this->playerViewDistance = QMargins(GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER, GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER);
this->blockMetatileIdMask = 0x03FF;
this->blockCollisionMask = 0x0C00;
this->blockElevationMask = 0xF000;
this->unusedTileNormal = 0x3014;
this->unusedTileCovered = 0x0000;
this->unusedTileSplit = 0x0000;
this->maxEventsPerGroup = 255;
this->forcedMajorVersion = 0;
this->metatileSelectorWidth = 8;
this->globalConstantsFilepaths.clear();
this->globalConstants.clear();
this->identifiers.clear();
this->readKeys.clear();
}
static const QMap<ProjectIdentifier, QPair<QString, QString>> defaultIdentifiers;
static const QMap<ProjectFilePath, QPair<QString, QString>> defaultPaths;
static const QStringList versionStrings;
static BaseGameVersion stringToBaseGameVersion(const QString &string);
static QString getPlayerIconPath(BaseGameVersion baseGameVersion, int character);
static QIcon getPlayerIcon(BaseGameVersion baseGameVersion, int character);
QString projectDir() const { return m_root; } // Alias for root()
void reset(BaseGameVersion baseGameVersion);
void setFilePath(ProjectFilePath pathId, const QString &path);
void setFilePath(const QString &pathId, const QString &path);
QString getCustomFilePath(ProjectFilePath pathId);
QString getCustomFilePath(const QString &pathId);
QString getFilePath(ProjectFilePath pathId);
void setIdentifier(ProjectIdentifier id, QString text);
void setIdentifier(const QString &id, const QString &text);
QString getCustomIdentifier(ProjectIdentifier id);
QString getCustomIdentifier(const QString &id);
QString getIdentifier(ProjectIdentifier id);
QString getBaseGameVersionString(BaseGameVersion version);
QString getBaseGameVersionString();
int getNumLayersInMetatile();
int getNumTilesInMetatile();
void setEventIconPath(Event::Group group, const QString &path);
QString getEventIconPath(Event::Group group);
void setPokemonIconPath(const QString &species, const QString &path);
QString getPokemonIconPath(const QString &species);
QMap<QString, QString> getPokemonIconPaths();
BaseGameVersion baseGameVersion;
bool usePoryScript;
bool useCustomBorderSize;
bool eventWeatherTriggerEnabled;
bool eventSecretBaseEnabled;
bool hiddenItemQuantityEnabled;
bool hiddenItemRequiresItemfinderEnabled;
bool healLocationRespawnDataEnabled;
bool eventCloneObjectEnabled;
bool floorNumberEnabled;
bool createMapTextFileEnabled;
bool tripleLayerMetatilesEnabled;
uint16_t defaultMetatileId;
uint16_t defaultElevation;
uint16_t defaultCollision;
QSize defaultMapSize;
QList<uint16_t> newMapBorderMetatileIds;
QString defaultPrimaryTileset;
QString defaultSecondaryTileset;
QString prefabFilepath;
bool prefabImportPrompted;
bool tilesetsHaveCallback;
bool tilesetsHaveIsCompressed;
QColor transparencyColor;
bool preserveMatchingOnlyData;
int metatileAttributesSize;
uint32_t metatileBehaviorMask;
uint32_t metatileTerrainTypeMask;
uint32_t metatileEncounterTypeMask;
uint32_t metatileLayerTypeMask;
uint16_t blockMetatileIdMask;
uint16_t blockCollisionMask;
uint16_t blockElevationMask;
uint16_t unusedTileNormal;
uint16_t unusedTileCovered;
uint16_t unusedTileSplit;
bool mapAllowFlagsEnabled;
QString eventsTabIconPath;
QString collisionSheetPath;
QSize collisionSheetSize;
QMargins playerViewDistance;
QList<uint32_t> warpBehaviors;
int maxEventsPerGroup;
int forcedMajorVersion;
int metatileSelectorWidth;
QStringList globalConstantsFilepaths;
QMap<QString,QString> globalConstants;
protected:
virtual void parseConfigKeyValue(QString key, QString value) override;
virtual QMap<QString, QString> getKeyValueMap() override;
virtual void init() override;
virtual void setUnreadKeys() override;
private:
QStringList readKeys;
QMap<ProjectIdentifier, QString> identifiers;
QMap<ProjectFilePath, QString> filePaths;
QMap<Event::Group, QString> eventIconPaths;
QMap<QString, QString> pokemonIconPaths;
};
extern ProjectConfig projectConfig;
class UserConfig: public KeyValueConfigBase
{
public:
UserConfig();
virtual void reset() override {
this->recentMapOrLayout = QString();
this->useEncounterJson = true;
this->customScripts.clear();
this->readKeys.clear();
}
QString projectDir() const { return m_root; } // Alias for root()
void parseCustomScripts(QString input);
QString outputCustomScripts();
void setCustomScripts(QStringList scripts, QList<bool> enabled);
QStringList getCustomScriptPaths();
QList<bool> getCustomScriptsEnabled();
QString recentMapOrLayout;
bool useEncounterJson;
protected:
virtual void parseConfigKeyValue(QString key, QString value) override;
virtual QMap<QString, QString> getKeyValueMap() override;
virtual void init() override;
virtual void setUnreadKeys() override;
#ifdef CONFIG_BACKWARDS_COMPATABILITY
friend class ProjectConfig;
#endif
private:
QStringList readKeys;
QMap<QString, bool> customScripts;
};
extern UserConfig userConfig;
class QAction;
class Shortcut;
class ShortcutsConfig : public KeyValueConfigBase
{
public:
ShortcutsConfig();
virtual void reset() override {
setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
user_shortcuts.clear();
}
// Call this before applying user shortcuts so that the user can restore defaults.
void setDefaultShortcuts(const QObjectList &objects);
QList<QKeySequence> defaultShortcuts(const QObject *object) const;
void setUserShortcuts(const QObjectList &objects);
void setUserShortcuts(const QMultiMap<const QObject *, QKeySequence> &objects_keySequences);
QList<QKeySequence> userShortcuts(const QObject *object) const;
protected:
virtual void parseConfigKeyValue(QString key, QString value) override;
virtual QMap<QString, QString> getKeyValueMap() override;
virtual void init() override { };
virtual void setUnreadKeys() override { };
private:
QMultiMap<QString, QKeySequence> user_shortcuts;
QMultiMap<QString, QKeySequence> default_shortcuts;
enum StoreType {
User,
Default
};
QString cfgKey(const QObject *object) const;
QList<QKeySequence> currentShortcuts(const QObject *object) const;
void storeShortcutsFromList(StoreType storeType, const QObjectList &objects);
void storeShortcuts(
StoreType storeType,
const QString &cfgKey,
const QList<QKeySequence> &keySequences);
};
extern ShortcutsConfig shortcutsConfig;
#endif // CONFIG_H

5
include/config/config.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
#include "porymapconfig.h"
#include "projectconfig.h"
#include "userconfig.h"
#include "shortcutsconfig.h"

View File

@ -0,0 +1,48 @@
#pragma once
#ifndef KEYVALUECONFIGBASE_H
#define KEYVALUECONFIGBASE_H
#include <QJsonObject>
#include <QString>
#include "fieldmanager.h"
class KeyValueConfigBase
{
public:
explicit KeyValueConfigBase(const QString& filename)
: m_root(QString()),
m_filename(filename),
m_filepath(filename)
{ };
virtual ~KeyValueConfigBase() {};
virtual bool save();
virtual bool load();
virtual QJsonObject toJson();
virtual void loadFromJson(const QJsonObject& obj);
void setRoot(const QString& dir);
QString root() const { return m_root; }
QString filepath() const { return m_filepath; }
QString filename() const { return m_filename; }
protected:
virtual void initializeFromEmpty() {};
virtual QJsonObject getDefaultJson() const { return QJsonObject(); }
virtual FieldManager* getFieldManager() { return nullptr; }
virtual bool parseJsonKeyValue(const QString& key, const QJsonValue& value);
virtual bool parseLegacyKeyValue(const QString& , const QString& ) {return false;}
QString m_root;
QString m_filename;
QString m_filepath;
private:
bool loadLegacy();
bool m_saveAllFields = false;
};
#endif // KEYVALUECONFIGBASE_H

View File

@ -0,0 +1,189 @@
#pragma once
#ifndef PORYMAPCONFIG_H
#define PORYMAPCONFIG_H
#include "keyvalueconfigbase.h"
#include "block.h"
#include "events.h"
#include "log.h"
#include <QColorSpace>
#include <QFontDatabase>
#include <QGraphicsPixmapItem>
#include <QStandardPaths>
enum ScriptAutocompleteMode {
MapOnly,
MapAndCommon,
All,
};
class PorymapConfig: public KeyValueConfigBase
{
public:
PorymapConfig() : KeyValueConfigBase(QStringLiteral("settings.json")) {
setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
// Initialize defaults not available at compile time.
this->mapListFont = defaultMapListFont();
}
virtual bool save() override;
virtual void loadFromJson(const QJsonObject& obj) override;
void addRecentProject(const QString& project);
void setRecentProjects(const QStringList& projects);
QString getRecentProject() const;
const QStringList& getRecentProjects() const;
void saveGeometry(const QWidget* widget, const QString& keyPrefix = QString(), bool recursive = true);
bool restoreGeometry(QWidget* widget, const QString& keyPrefix = QString(), bool recursive = true) const;
static QFont defaultMapListFont() { return QFontDatabase::systemFont(QFontDatabase::FixedFont); }
bool reopenOnLaunch = true;
bool projectManuallyClosed = false;
int mapListTab = 0;
bool mapListEditGroupsEnabled = false;
OrderedSet<int> mapListTabsHidingEmptyFolders;
bool mapListLayoutsSorted = true;
bool mapListLocationsSorted = true;
bool prettyCursors = true;
bool mirrorConnectingMaps = true;
bool showDiveEmergeMaps = false;
int diveEmergeMapOpacity = 30;
int diveMapOpacity = 15;
int emergeMapOpacity = 15;
int collisionOpacity = 50;
int collisionZoom = 30;
int metatilesZoom = 30;
int tilesetEditorMetatilesZoom = 30;
int tilesetEditorTilesZoom = 30;
Qt::Orientation tilesetEditorLayerOrientation = Qt::Vertical;
bool showPlayerView = false;
bool showCursorTile = true;
bool showBorder = true;
bool showGrid = false;
bool showTilesetEditorMetatileGrid = false;
bool showTilesetEditorLayerGrid = true;
bool showTilesetEditorDivider = true;
bool showTilesetEditorRawAttributes = false;
bool showPaletteEditorUnusedColors = false;
bool monitorFiles = true;
bool tilesetCheckerboardFill = true;
bool newMapHeaderSectionExpanded = false;
bool displayIdsHexadecimal = true;
QString theme = QStringLiteral("default");
QString wildMonChartTheme;
QString textEditorOpenFolder;
QString textEditorGotoLine;
int paletteEditorBitDepth = 24;
int projectSettingsTab = 0;
ScriptAutocompleteMode scriptAutocompleteMode = ScriptAutocompleteMode::MapOnly;
bool warpBehaviorWarningDisabled = false;
bool eventDeleteWarningDisabled = false;
bool eventOverlayEnabled = false;
bool checkForUpdates = true;
bool showProjectLoadingScreen = true;
QDateTime lastUpdateCheckTime;
QVersionNumber lastUpdateCheckVersion;
QMap<QUrl, QDateTime> rateLimitTimes;
QGraphicsPixmapItem::ShapeMode eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape;
bool shownInGameReloadMessage = false;
GridSettings gridSettings;
OrderedSet<LogType> statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN };
QFont applicationFont;
QFont mapListFont;
#ifdef Q_OS_MACOS
// Since the release of the Retina display, Apple products use the Display P3 color space by default.
// If we don't use this for exported images (which by default will either have no color space or the sRGB
// color space) then they may appear to have different colors than the same image displayed in Porymap.
std::optional<QColorSpace::NamedColorSpace> imageExportColorSpace = QColorSpace::DisplayP3;
#else
// As of writing Qt has no way to get a reasonable color space from the user's environment,
// so we export images without one and let them handle it.
std::optional<QColorSpace::NamedColorSpace> imageExportColorSpace = {};
#endif
QMap<QString,QString> trustedScriptHashes;
FieldManager* getFieldManager() override {
if (!m_fm) {
m_fm = std::make_shared<FieldManager>();
m_fm->addField(&this->reopenOnLaunch, "reopen_on_launch");
m_fm->addField(&this->projectManuallyClosed, "project_manually_closed");
m_fm->addField(&this->mapListTab, "map_list_tab", 0, 2);
m_fm->addField(&this->mapListEditGroupsEnabled, "map_list_edit_groups_enabled");
m_fm->addField(&this->mapListTabsHidingEmptyFolders, "map_list_tabs_hiding_empty_folders");
m_fm->addField(&this->mapListLayoutsSorted, "map_list_layouts_sorted");
m_fm->addField(&this->mapListLocationsSorted, "map_list_locations_sorted");
m_fm->addField(&this->prettyCursors, "pretty_cursors");
m_fm->addField(&this->mirrorConnectingMaps, "mirror_connecting_maps");
m_fm->addField(&this->showDiveEmergeMaps, "show_dive_emerge_maps");
m_fm->addField(&this->diveEmergeMapOpacity, "dive_emerge_map_opacity", 10, 90);
m_fm->addField(&this->diveMapOpacity, "dive_map_opacity", 10, 90);
m_fm->addField(&this->emergeMapOpacity, "emerge_map_opacity", 10, 90);
m_fm->addField(&this->collisionOpacity, "collision_opacity", 0, 100);
m_fm->addField(&this->collisionZoom, "collision_zoom", 10, 100);
m_fm->addField(&this->metatilesZoom, "metatiles_zoom", 10, 100);
m_fm->addField(&this->tilesetEditorMetatilesZoom, "tileset_editor_metatiles_zoom", 10, 100);
m_fm->addField(&this->tilesetEditorTilesZoom, "tileset_editor_tiles_zoom", 10, 100);
m_fm->addField(&this->tilesetEditorLayerOrientation, "tileset_editor_layer_orientation");
m_fm->addField(&this->showPlayerView, "show_player_view");
m_fm->addField(&this->showCursorTile, "show_cursor_tile");
m_fm->addField(&this->showBorder, "show_border");
m_fm->addField(&this->showGrid, "show_grid");
m_fm->addField(&this->showTilesetEditorMetatileGrid, "show_tileset_editor_metatile_grid");
m_fm->addField(&this->showTilesetEditorLayerGrid, "show_tileset_editor_layer_grid");
m_fm->addField(&this->showTilesetEditorDivider, "show_tileset_editor_divider");
m_fm->addField(&this->showTilesetEditorRawAttributes, "show_tileset_editor_raw_attributes");
m_fm->addField(&this->showPaletteEditorUnusedColors, "show_palette_editor_unused_colors");
m_fm->addField(&this->monitorFiles, "monitor_files");
m_fm->addField(&this->tilesetCheckerboardFill, "tileset_checkerboard_fill");
m_fm->addField(&this->newMapHeaderSectionExpanded, "new_map_header_section_expanded");
m_fm->addField(&this->displayIdsHexadecimal, "display_ids_hexadecimal");
m_fm->addField(&this->theme, "theme");
m_fm->addField(&this->wildMonChartTheme, "wild_mon_chart_theme");
m_fm->addField(&this->textEditorOpenFolder, "text_editor_open_folder");
m_fm->addField(&this->textEditorGotoLine, "text_editor_goto_line");
m_fm->addField(&this->paletteEditorBitDepth, "palette_editor_bit_depth", {24,15});
m_fm->addField(&this->projectSettingsTab, "project_settings_tab");
m_fm->addField(&this->scriptAutocompleteMode, "script_autocomplete_mode");
m_fm->addField(&this->warpBehaviorWarningDisabled, "warp_behavior_warning_disabled");
m_fm->addField(&this->eventDeleteWarningDisabled, "event_delete_warning_disabled");
m_fm->addField(&this->eventOverlayEnabled, "event_overlay_enabled");
m_fm->addField(&this->checkForUpdates, "check_for_updates");
m_fm->addField(&this->showProjectLoadingScreen, "show_project_loading_screen");
m_fm->addField(&this->lastUpdateCheckTime, "last_update_check_time");
m_fm->addField(&this->lastUpdateCheckVersion, "last_update_check_version");
m_fm->addField(&this->rateLimitTimes, "rate_limit_times");
m_fm->addField(&this->eventSelectionShapeMode, "event_selection_shape_mode");
m_fm->addField(&this->shownInGameReloadMessage, "shown_in_game_reload_message");
m_fm->addField(&this->gridSettings, "map_grid");
m_fm->addField(&this->statusBarLogTypes, "status_bar_log_types");
m_fm->addField(&this->applicationFont, "application_font");
m_fm->addField(&this->mapListFont, "map_list_font");
m_fm->addField(&this->imageExportColorSpace, "image_export_color_space");
m_fm->addField(&this->trustedScriptHashes, "trusted_script_hashes");
m_fm->addField(&this->recentProjects, "recent_projects");
m_fm->addField(&this->savedGeometryMap, "geometry");
m_fm->addField(&this->geometryVersion, "geometry_version");
}
return m_fm.get();
};
protected:
virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override;
virtual QJsonObject getDefaultJson() const override;
private:
std::shared_ptr<FieldManager> m_fm = nullptr;
QStringList recentProjects;
QMap<QString, QByteArray> savedGeometryMap;
int geometryVersion = 0;
};
extern PorymapConfig porymapConfig;
#endif // PORYMAPCONFIG_H

View File

@ -0,0 +1,276 @@
#pragma once
#ifndef PROJECTCONFIG_H
#define PROJECTCONFIG_H
#include "keyvalueconfigbase.h"
#include "block.h"
#include "events.h"
enum ProjectIdentifier {
symbol_facing_directions,
symbol_obj_event_gfx_pointers,
symbol_pokemon_icon_table,
symbol_attribute_table,
symbol_tilesets_prefix,
symbol_dynamic_map_name,
define_obj_event_count,
define_min_level,
define_max_level,
define_max_encounter_rate,
define_tiles_primary,
define_tiles_total,
define_metatiles_primary,
define_pals_primary,
define_pals_total,
define_tiles_per_metatile,
define_map_size,
define_map_offset_width,
define_map_offset_height,
define_mask_metatile,
define_mask_collision,
define_mask_elevation,
define_mask_behavior,
define_mask_layer,
define_attribute_behavior,
define_attribute_layer,
define_attribute_terrain,
define_attribute_encounter,
define_metatile_label_prefix,
define_heal_locations_prefix,
define_layout_prefix,
define_map_prefix,
define_map_dynamic,
define_map_empty,
define_map_section_prefix,
define_map_section_empty,
define_species_prefix,
define_species_empty,
regex_behaviors,
regex_obj_event_gfx,
regex_items,
regex_flags,
regex_vars,
regex_movement_types,
regex_map_types,
regex_battle_scenes,
regex_weather,
regex_coord_event_weather,
regex_secret_bases,
regex_sign_facing_directions,
regex_trainer_types,
regex_music,
regex_encounter_types,
regex_terrain_types,
pals_output_extension,
tiles_output_extension,
};
enum ProjectFilePath {
data_map_folders,
data_scripts_folders,
data_layouts_folders,
data_primary_tilesets_folders,
data_secondary_tilesets_folders,
data_event_scripts,
json_map_groups,
json_layouts,
json_wild_encounters,
json_heal_locations,
json_region_map_entries,
json_region_porymap_cfg,
tilesets_headers,
tilesets_graphics,
tilesets_metatiles,
tilesets_headers_asm,
tilesets_graphics_asm,
tilesets_metatiles_asm,
data_obj_event_gfx_pointers,
data_obj_event_gfx_info,
data_obj_event_pic_tables,
data_obj_event_gfx,
data_pokemon_gfx,
constants_global,
constants_items,
constants_flags,
constants_vars,
constants_weather,
constants_songs,
constants_pokemon,
constants_map_types,
constants_trainer_types,
constants_secret_bases,
constants_obj_event_movement,
constants_obj_events,
constants_event_bg,
constants_metatile_labels,
constants_metatile_behaviors,
constants_species,
constants_fieldmap,
global_fieldmap,
fieldmap,
initial_facing_table,
wild_encounter,
pokemon_icon_table,
pokemon_gfx,
};
// Distance in pixels from the edge of a GBA screen (240x160) to the center 16x16 pixels.
#define GBA_H_DIST_TO_CENTER ((240-16)/2)
#define GBA_V_DIST_TO_CENTER ((160-16)/2)
class ProjectConfig: public KeyValueConfigBase
{
public:
ProjectConfig(const QString& root = QString()) : KeyValueConfigBase(QStringLiteral("porymap.project.json")) {
setRoot(root);
}
ProjectConfig(BaseGame::Version version, const QString& root = QString()) : ProjectConfig(root) {
setVersionSpecificDefaults(version);
}
virtual bool save() override;
virtual void loadFromJson(const QJsonObject& obj) override;
QString projectDir() const { return m_root; } // Alias for root()
void setVersionSpecificDefaults(BaseGame::Version baseGameVersion);
void setFilePath(ProjectFilePath pathId, const QString& path);
void setFilePath(const QString& pathId, const QString& path);
QString getCustomFilePath(ProjectFilePath pathId);
QString getCustomFilePath(const QString& pathId);
QString getFilePath(ProjectFilePath pathId);
void setIdentifier(ProjectIdentifier id, const QString& text);
void setIdentifier(const QString& id, const QString& text);
QString getCustomIdentifier(ProjectIdentifier id);
QString getCustomIdentifier(const QString& id);
QString getIdentifier(ProjectIdentifier id);
static const QMap<ProjectIdentifier, QPair<QString, QString>> defaultIdentifiers;
static const QMap<ProjectFilePath, QPair<QString, QString>> defaultPaths;
BaseGame::Version baseGameVersion = BaseGame::Version::none;
bool usePoryScript = false;
bool useCustomBorderSize = false;
bool eventWeatherTriggerEnabled = false;
bool eventSecretBaseEnabled = false;
bool hiddenItemQuantityEnabled = false;
bool hiddenItemRequiresItemfinderEnabled = false;
bool healLocationRespawnDataEnabled = false;
bool eventCloneObjectEnabled = false;
bool floorNumberEnabled = false;
bool createMapTextFileEnabled = false;
bool tripleLayerMetatilesEnabled = false;
uint16_t defaultMetatileId = 1;
uint16_t defaultElevation = 3;
uint16_t defaultCollision = 0;
QSize defaultMapSize = QSize(20,20);
QList<uint16_t> newMapBorderMetatileIds;
QString defaultPrimaryTileset = QStringLiteral("gTileset_General");
QString defaultSecondaryTileset;
bool tilesetsHaveCallback = true;
bool tilesetsHaveIsCompressed = true;
QColor transparencyColor = QColorConstants::Black;
bool preserveMatchingOnlyData = false;
int metatileAttributesSize = 2;
uint32_t metatileBehaviorMask = 0;
uint32_t metatileTerrainTypeMask = 0;
uint32_t metatileEncounterTypeMask = 0;
uint32_t metatileLayerTypeMask = 0;
uint16_t blockMetatileIdMask = Block::DefaultMetatileIdMask;
uint16_t blockCollisionMask = Block::DefaultCollisionMask;
uint16_t blockElevationMask = Block::DefaultElevationMask;
uint16_t unusedTileNormal = 0x3014;
uint16_t unusedTileCovered = 0x0000;
uint16_t unusedTileSplit = 0x0000;
bool mapAllowFlagsEnabled = true;
QString eventsTabIconPath;
QString collisionSheetPath;
QSize collisionSheetSize = QSize(2, 16);
QMargins playerViewDistance = QMargins(GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER, GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER);
OrderedSet<uint32_t> warpBehaviors;
int maxEventsPerGroup = 255;
int metatileSelectorWidth = 8;
QStringList globalConstantsFilepaths;
QMap<QString,QString> globalConstants;
QList<ScriptSettings> customScripts;
QMap<Event::Group, QString> eventIconPaths;
QMap<QString, QString> pokemonIconPaths;
QVersionNumber minimumVersion;
FieldManager* getFieldManager() override {
if (!m_fm) {
m_fm = std::make_shared<FieldManager>();
m_fm->addField(&this->baseGameVersion, "base_game_version");
m_fm->addField(&this->usePoryScript, "use_poryscript");
m_fm->addField(&this->useCustomBorderSize, "use_custom_border_size");
m_fm->addField(&this->eventWeatherTriggerEnabled, "enable_event_weather_trigger");
m_fm->addField(&this->eventSecretBaseEnabled, "enable_event_secret_base");
m_fm->addField(&this->hiddenItemQuantityEnabled, "enable_hidden_item_quantity");
m_fm->addField(&this->hiddenItemRequiresItemfinderEnabled, "enable_hidden_item_requires_itemfinder");
m_fm->addField(&this->healLocationRespawnDataEnabled, "enable_heal_location_respawn_data");
m_fm->addField(&this->eventCloneObjectEnabled, "enable_event_clone_object");
m_fm->addField(&this->floorNumberEnabled, "enable_floor_number");
m_fm->addField(&this->createMapTextFileEnabled, "create_map_text_file");
m_fm->addField(&this->tripleLayerMetatilesEnabled, "enable_triple_layer_metatiles");
m_fm->addField<uint16_t>(&this->defaultMetatileId, "default_metatile_id", 0, Block::MaxValue);
m_fm->addField<uint16_t>(&this->defaultElevation, "default_elevation", 0, Block::MaxValue);
m_fm->addField<uint16_t>(&this->defaultCollision, "default_collision", 0, Block::MaxValue);
m_fm->addField(&this->defaultMapSize, "default_map_size");
m_fm->addField(&this->newMapBorderMetatileIds, "new_map_border_metatiles");
m_fm->addField(&this->defaultPrimaryTileset, "default_primary_tileset");
m_fm->addField(&this->defaultSecondaryTileset, "default_secondary_tileset");
m_fm->addField(&this->tilesetsHaveCallback, "tilesets_have_callback");
m_fm->addField(&this->tilesetsHaveIsCompressed, "tilesets_have_is_compressed");
m_fm->addField(&this->transparencyColor, "transparency_color");
m_fm->addField(&this->preserveMatchingOnlyData, "preserve_matching_only_data");
m_fm->addField(&this->metatileAttributesSize, "metatile_attributes_size");
m_fm->addField(&this->metatileBehaviorMask, "metatile_behavior_mask");
m_fm->addField(&this->metatileTerrainTypeMask, "metatile_terrain_type_mask");
m_fm->addField(&this->metatileEncounterTypeMask, "metatile_encounter_type_mask");
m_fm->addField(&this->metatileLayerTypeMask, "metatile_layer_type_mask");
m_fm->addField<uint16_t>(&this->blockMetatileIdMask, "block_metatile_id_mask", 0, Block::MaxValue);
m_fm->addField<uint16_t>(&this->blockCollisionMask, "block_collision_mask", 0, Block::MaxValue);
m_fm->addField<uint16_t>(&this->blockElevationMask, "block_elevation_mask", 0, Block::MaxValue);
m_fm->addField<uint16_t>(&this->unusedTileNormal, "unused_tile_normal", 0, Tile::MaxValue);
m_fm->addField<uint16_t>(&this->unusedTileCovered, "unused_tile_covered", 0, Tile::MaxValue);
m_fm->addField<uint16_t>(&this->unusedTileSplit, "unused_tile_split", 0, Tile::MaxValue);
m_fm->addField(&this->mapAllowFlagsEnabled, "enable_map_allow_flags");
m_fm->addField(&this->eventsTabIconPath, "events_tab_icon_path");
m_fm->addField(&this->collisionSheetPath, "collision_sheet_path");
m_fm->addField(&this->collisionSheetSize, "collision_sheet_size", QSize(1,1), QSize(Block::MaxValue, Block::MaxValue));
m_fm->addField(&this->playerViewDistance, "player_view_distance", QMargins(0,0,0,0), QMargins(INT_MAX, INT_MAX, INT_MAX, INT_MAX));
m_fm->addField(&this->warpBehaviors, "warp_behaviors");
m_fm->addField(&this->maxEventsPerGroup, "max_events_per_group", 1, INT_MAX);
m_fm->addField(&this->metatileSelectorWidth, "metatile_selector_width", 1, INT_MAX);
m_fm->addField(&this->globalConstantsFilepaths, "global_constants_filepaths");
m_fm->addField(&this->globalConstants, "global_constants");
m_fm->addField(&this->customScripts, "custom_scripts");
m_fm->addField(&this->eventIconPaths, "event_icon_paths");
m_fm->addField(&this->pokemonIconPaths, "pokemon_icon_paths");
m_fm->addField(&this->minimumVersion, "minimum_version");
m_fm->addField(&this->identifiers, "custom_identifiers");
m_fm->addField(&this->filePaths, "custom_file_paths");
}
return m_fm.get();
}
protected:
virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override;
virtual QJsonObject getDefaultJson() const override;
virtual void initializeFromEmpty() override;
private:
ProjectFilePath reverseDefaultPaths(const QString& str);
ProjectIdentifier reverseDefaultIdentifier(const QString& str);
std::shared_ptr<FieldManager> m_fm = nullptr;
QMap<ProjectIdentifier, QString> identifiers;
QMap<ProjectFilePath, QString> filePaths;
};
extern ProjectConfig projectConfig;
#endif // PROJECTCONFIG_H

View File

@ -0,0 +1,55 @@
#pragma once
#ifndef SHORTCUTSCONFIG_H
#define SHORTCUTSCONFIG_H
#include "keyvalueconfigbase.h"
#include <QStandardPaths>
class QAction;
class Shortcut;
class ShortcutsConfig : public KeyValueConfigBase
{
public:
ShortcutsConfig() : KeyValueConfigBase(QStringLiteral("shortcuts.json")) {
setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
}
virtual QJsonObject toJson() override;
virtual void loadFromJson(const QJsonObject& obj) override;
// Call this before applying user shortcuts so that the user can restore defaults.
void setDefaultShortcuts(const QObjectList& objects);
QList<QKeySequence> defaultShortcuts(const QObject *object) const;
void setUserShortcuts(const QObjectList& objects);
void setUserShortcuts(const QMultiMap<const QObject *, QKeySequence>& objects_keySequences);
QList<QKeySequence> userShortcuts(const QObject *object) const;
protected:
virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override;
virtual QJsonObject getDefaultJson() const override;
private:
QMultiMap<QString, QKeySequence> user_shortcuts;
QMultiMap<QString, QKeySequence> default_shortcuts;
enum StoreType {
User,
Default
};
QString cfgKey(const QObject *object) const;
QList<QKeySequence> currentShortcuts(const QObject *object) const;
void storeShortcutsFromList(StoreType storeType, const QObjectList& objects);
void storeShortcuts(
StoreType storeType,
const QString& cfgKey,
const QList<QKeySequence>& keySequences);
};
extern ShortcutsConfig shortcutsConfig;
#endif // SHORTCUTSCONFIG_H

View File

@ -0,0 +1,46 @@
#pragma once
#ifndef USERCONFIG_H
#define USERCONFIG_H
#include "keyvalueconfigbase.h"
class UserConfig: public KeyValueConfigBase
{
public:
UserConfig(const QString& root = QString()) : KeyValueConfigBase(QStringLiteral("porymap.user.json")) {
setRoot(root);
}
virtual void loadFromJson(const QJsonObject& obj) override;
QString projectDir() const { return m_root; } // Alias for root()
QString recentMapOrLayout;
QString prefabsFilepath;
bool prefabsImportPrompted = false;
bool useEncounterJson = true;
QList<ScriptSettings> customScripts;
protected:
virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override;
virtual QJsonObject getDefaultJson() const override;
FieldManager* getFieldManager() override {
if (!m_fm) {
m_fm = std::make_shared<FieldManager>();
m_fm->addField(&this->recentMapOrLayout, "recent_map_or_layout");
m_fm->addField(&this->prefabsFilepath, "prefabs_filepath");
m_fm->addField(&this->prefabsImportPrompted, "prefabs_import_prompted");
m_fm->addField(&this->useEncounterJson, "use_encounter_json");
m_fm->addField(&this->customScripts, "custom_scripts");
}
return m_fm.get();
}
private:
std::shared_ptr<FieldManager> m_fm = nullptr;
};
extern UserConfig userConfig;
#endif // USERCONFIG_H

22
include/core/basegame.h Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#ifndef BASEGAMEVERSION_H
#define BASEGAMEVERSION_H
#include <QString>
#include <QIcon>
namespace BaseGame {
enum Version {
none,
pokeruby,
pokefirered,
pokeemerald,
};
Version stringToVersion(const QString &string);
QString versionToString(Version version);
QString getPlayerIconPath(Version version, int character);
QIcon getPlayerIcon(Version version, int character);
};
#endif // BASEGAMEVERSION_H

View File

@ -26,7 +26,12 @@ public:
static uint16_t getMaxCollision();
static uint16_t getMaxElevation();
static const uint16_t maxValue;
// Upper limit for metatile ID, collision, and elevation masks. Used externally.
static constexpr uint16_t MaxValue = 0xFFFF;
static constexpr uint16_t DefaultMetatileIdMask = 0x03FF;
static constexpr uint16_t DefaultCollisionMask = 0x0C00;
static constexpr uint16_t DefaultElevationMask = 0xF000;
private:
uint16_t m_metatileId;

401
include/core/converter.h Normal file
View File

@ -0,0 +1,401 @@
#pragma once
#ifndef CONVERTER_H
#define CONVERTER_H
#include <QDateTime>
#include <QJsonValue>
#include <QUrl>
#include <QVariant>
#include <QVersionNumber>
#include "magic_enum.hpp"
#include "orderedset.h"
#include "scriptsettings.h"
#include "gridsettings.h"
#include "basegame.h"
/*
These are templates for type conversion to/from JSON,
though other type conversions can be implemented here too.
This is mostly useful when converting the type is complicated,
or when the type is generalized away.
## Example Usage ##
QSize size;
QJsonValue json = Converter<QSize>::toJson(size);
QSize sameSize = Converter<QSize>::fromJson(json);
## Adding a new conversion ##
To add a new type conversion, add a new 'Converter' template:
template <>
struct Converter<NewType> : DefaultConverter<NewType> {
// And re-implement any of the desired conversion functions.
// Any functions not implemented will be inherited from DefaultConverter.
static QJsonValue toJson(const NewType& value) {
// your conversion to JSON
return QJsonValue();
}
static NewType fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
// your conversion from JSON
return NewType();
}
};
Note: When serializing to/from JSON, anything that can be serialized to/from
a string is trivially serializable for JSON. In this case, rather than
inheriting from 'DefaultConverter' and reimplementing 'toJson' and 'fromJson',
you can inherit from 'DefaultStringConverter' and/or reimplement 'toString'/'fromString'.
Appropriately implementing 'toString'/'fromString' has the added benefit that your type
can automatically be used as a JSON key if it for example appears as the key in a QMap.
If a type doesn't support the '<'/'>' operators, 'clamp' can be reimplemented as well to allow
the type to be used in range validation.
*/
template <typename T>
struct DefaultConverter {
// Defaults to straightforward QJsonValue construction.
// This handles most of the primitive types.
static QJsonValue toJson(const T& value) {
return QJsonValue(value);
}
static T fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const QVariant v = json.toVariant();
if (!v.canConvert<T>()) {
if (errors) errors->append(QString("Can't convert from type '%1'").arg(v.typeName()));
// Failed conversion will return a default-constructed object below
}
return v.value<T>();
}
// Default to identity
static QString toString(const T& value) {return value;}
static T fromString(const QString& string, QStringList* = nullptr) {return string;}
static T clamp(const T& value, const T& min, const T& max, QStringList* errors = nullptr) {
Q_ASSERT(min <= max);
if (value < min) {
if (errors) errors->append("Value too low");
return min;
}
if (value > max) {
if (errors) errors->append("Value too high");
return max;
}
return value;
}
};
template <typename T, typename Enable = void>
struct Converter : DefaultConverter<T> {};
// This template implements JSON conversion by first converting the data to/from a string.
// This allows any type that can describe how to stringify itself to automatically also
// support JSON conversion with no additional work.
template <typename T>
struct DefaultStringConverter : DefaultConverter<T> {
static QJsonValue toJson(const T& value) {
return Converter<QString>::toJson(Converter<T>::toString(value));
}
static T fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto string = Converter<QString>::fromJson(json, errors);
return Converter<T>::fromString(string, errors);
}
// Many types have a 'toString' function, so we default to trying that.
static QString toString(const T& value) {
return value.toString();
}
};
template <>
struct Converter<QUrl> : DefaultStringConverter<QUrl> {};
template <>
struct Converter<QKeySequence> : DefaultStringConverter<QKeySequence> {};
template <>
struct Converter<uint32_t> : DefaultConverter<uint32_t> {
// Constructing a QJsonValue from uint32_t is ambiguous, so we need an explicit cast.
static QJsonValue toJson(uint32_t value) {
return QJsonValue{static_cast<qint64>(value)};
}
};
// Template for generic enum values.
// Converts JSON -> string/int -> enum, handling the unsafe conversion if the int is out of range of the enum.
// Qt has a system for this (Q_ENUM) but they don't use it for all their internal enums, so we use magic_enum instead.
template <typename T>
struct Converter<T, std::enable_if_t<std::is_enum_v<T>>> : DefaultStringConverter<T> {
static QString toString(const T& value) {
const std::string s = std::string(magic_enum::enum_name(value));
return QString::fromStdString(s);
}
static T fromString(const QString& string, QStringList* errors = nullptr) {
auto e = magic_enum::enum_cast<T>(string.toStdString(), magic_enum::case_insensitive);
if (!e.has_value()) {
if (errors) errors->append(QString("'%1' is not a named enum value.").arg(string));
return magic_enum::enum_value<T>(0);
}
return e.value();
}
// When reading from JSON, handle either the named enum or an enum's number value.
static T fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
if (json.isString()) return Converter<T>::fromString(json.toString());
auto value = Converter<int>::fromJson(json, errors);
auto e = magic_enum::enum_cast<T>(value);
if (!e.has_value()) {
if (errors) errors->append(QString("'%1' is out of range of enum.").arg(QString::number(value)));
return magic_enum::enum_value<T>(0);
}
return e.value();
}
};
template <>
struct Converter<QVersionNumber> : DefaultStringConverter<QVersionNumber> {
static QVersionNumber fromString(const QString& string, QStringList* = nullptr) {
return QVersionNumber::fromString(string);
}
};
template <>
struct Converter<QDateTime> : DefaultStringConverter<QDateTime> {
static QString toString(const QDateTime& value) {
return value.toUTC().toString();
}
static QDateTime fromString(const QString& string, QStringList* = nullptr) {
return QDateTime::fromString(string).toLocalTime();
}
};
template <>
struct Converter<QColor> : DefaultStringConverter<QColor> {
static QString toString(const QColor& value) {
return value.name();
}
static QColor fromString(const QString& string, QStringList* errors = nullptr) {
const QColor color(string);
if (!color.isValid()) {
if (errors) errors->append(QString("'%1' is not a valid color.").arg(string));
return QColorConstants::Black;
}
return color;
}
};
template <>
struct Converter<QFont> : DefaultStringConverter<QFont> {
static QFont fromString(const QString& string, QStringList* errors = nullptr) {
QFont font;
if (!font.fromString(string) && errors) {
errors->append(QString("'%1' is not a valid font description.").arg(string));
}
return font;
}
};
template <>
struct Converter<BaseGame::Version> : DefaultStringConverter<BaseGame::Version> {
static QString toString(const BaseGame::Version& value) {
return BaseGame::versionToString(value);
}
static BaseGame::Version fromString(const QString& string, QStringList* = nullptr) {
return BaseGame::stringToVersion(string);
}
};
template <typename T>
struct Converter<std::optional<T>> : DefaultConverter<std::optional<T>> {
static QJsonValue toJson(const std::optional<T>& optional) {
return optional.has_value() ? Converter<T>::toJson(optional.value()) : QJsonValue();
}
static std::optional<T> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
if (json.isNull()) return {};
return Converter<T>::fromJson(json, errors);
}
};
template <typename T>
struct ListConverter : DefaultConverter<QList<T>> {
static QJsonValue toJson(const QList<T>& list) {
QJsonArray arr;
for (auto& elem : list) arr.append(Converter<T>::toJson(elem));
return arr;
}
static QList<T> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto arr = Converter<QJsonArray>::fromJson(json, errors);
QList<T> list;
for (const auto& elem : arr) list.append(Converter<T>::fromJson(elem, errors));
return list;
}
};
template <typename T>
struct Converter<QList<T>> : ListConverter<T> {};
// Only needed for Qt5
template <>
struct Converter<QStringList> : ListConverter<QString> {};
template <typename K, typename V>
struct Converter<QMap<K,V>> : DefaultConverter<QMap<K,V>> {
static QJsonObject toJson(const QMap<K,V>& map) {
QJsonObject obj;
for (auto it = map.begin(); it != map.end(); it++) {
const QString key = Converter<K>::toString(it.key());
obj[key] = Converter<V>::toJson(it.value());
}
return obj;
}
static QMap<K,V> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto obj = Converter<QJsonObject>::fromJson(json, errors);
QMap<K,V> map;
for (auto it = obj.begin(); it != obj.end(); it++) {
const auto key = Converter<K>::fromString(it.key(), errors);
map.insert(key, Converter<V>::fromJson(it.value(), errors));
}
return map;
}
};
template <typename K, typename V>
struct Converter<QMultiMap<K,V>> : DefaultConverter<QMultiMap<K,V>> {
static QJsonObject toJson(const QMultiMap<K,V>& map) {
QJsonObject obj;
for (const auto& uniqueKey : map.uniqueKeys()) {
const QString key = Converter<K>::toString(uniqueKey);
obj[key] = Converter<QList<V>>::toJson(map.values(uniqueKey));
}
return obj;
}
static QMultiMap<K,V> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto obj = Converter<QJsonObject>::fromJson(json, errors);
QMultiMap<K,V> map;
for (auto it = obj.begin(); it != obj.end(); it++) {
const auto key = Converter<K>::fromString(it.key(), errors);
const auto values = Converter<QList<V>>::fromJson(it.value(), errors);
for (const auto& value : values) map.insert(key, value);
}
return map;
}
};
template <typename T>
struct Converter<OrderedSet<T>> : DefaultConverter<OrderedSet<T>> {
static QJsonValue toJson(const OrderedSet<T>& set) {
QJsonArray arr;
for (auto& elem : set) arr.append(Converter<T>::toJson(elem));
return arr;
}
static OrderedSet<T> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto arr = Converter<QJsonArray>::fromJson(json, errors);
OrderedSet<T> set;
for (const auto& elem : arr) set.insert(Converter<T>::fromJson(elem, errors));
return set;
}
};
template <>
struct Converter<QSize> : DefaultConverter<QSize> {
static QJsonValue toJson(const QSize& value) {
QJsonObject obj;
obj["width"] = value.width();
obj["height"] = value.height();
return obj;
}
static QSize fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto obj = Converter<QJsonObject>::fromJson(json, errors);
QSize size;
size.setWidth(obj.value("width").toInt());
size.setHeight(obj.value("height").toInt());
return size;
}
static QSize clamp(const QSize& value, const QSize& min, const QSize& max, const QStringList* = nullptr) {
Q_ASSERT(min.width() <= max.width());
Q_ASSERT(min.height() <= max.height());
QSize size = value;
if (value.width() < min.width() || value.height() < min.height()) size = value.expandedTo(min);
if (value.width() > max.width() || value.height() > max.height()) size = value.boundedTo(max);
return size;
}
};
template <>
struct Converter<QMargins> : DefaultConverter<QMargins> {
static QJsonValue toJson(const QMargins& value) {
QJsonObject obj;
obj["top"] = value.top();
obj["bottom"] = value.bottom();
obj["left"] = value.left();
obj["right"] = value.right();
return obj;
}
static QMargins fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto obj = Converter<QJsonObject>::fromJson(json, errors);
QMargins margins;
margins.setTop(obj.value("top").toInt());
margins.setBottom(obj.value("bottom").toInt());
margins.setLeft(obj.value("left").toInt());
margins.setRight(obj.value("right").toInt());
return margins;
}
static QMargins clamp(const QMargins& value, const QMargins& min, const QMargins& max, const QStringList* = nullptr) {
Q_ASSERT(min.left() <= max.left());
Q_ASSERT(min.right() <= max.right());
Q_ASSERT(min.top() <= max.top());
Q_ASSERT(min.bottom() <= max.bottom());
QMargins margins = value;
margins.setLeft( std::clamp(value.left(), min.left(), max.left()));
margins.setRight( std::clamp(value.right(), min.right(), max.right()));
margins.setTop( std::clamp(value.top(), min.top(), max.top()));
margins.setBottom(std::clamp(value.bottom(), min.bottom(), max.bottom()));
return margins;
}
};
template <>
struct Converter<ScriptSettings> : DefaultConverter<ScriptSettings> {
static QJsonValue toJson(const ScriptSettings& value) {
QJsonObject obj;
obj["path"] = value.path;
obj["enabled"] = value.enabled;
return obj;
}
static ScriptSettings fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto obj = Converter<QJsonObject>::fromJson(json, errors);
ScriptSettings settings;
settings.path = obj.value("path").toString();
settings.enabled = obj.value("enabled").toBool();
return settings;
}
};
template <>
struct Converter<GridSettings> : DefaultConverter<GridSettings> {
static QJsonValue toJson(const GridSettings& value) {
return value.toJson();
}
static GridSettings fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto obj = Converter<QJsonObject>::fromJson(json, errors);
return GridSettings::fromJson(obj);
}
};
template <>
struct Converter<QByteArray> : DefaultConverter<QByteArray> {
static QJsonValue toJson(const QByteArray& value) {
return QString::fromLocal8Bit(value.toBase64());
}
static QByteArray fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
const auto s = Converter<QString>::fromJson(json, errors);
return QByteArray::fromBase64(s.toLocal8Bit());
}
};
#endif // CONVERTER_H

167
include/core/fieldmanager.h Normal file
View File

@ -0,0 +1,167 @@
#include "converter.h"
// A FieldInterface provides a simple interface for converting a field to/from JSON.
// It's constructed with a pointer to some data, and has two functions:
// - 'get' returns the pointed-to data, converted to JSON
// - 'set' assigns the pointed-to data to a given QJsonValue, with appropriate conversion.
// Returns any errors that occur during conversion/assignment.
//
// A FieldInterface is normally constructed using the 'makeFieldInterface' function:
// Example:
// int someField = 0;
// FieldInterface* fi = makeFieldInterface(&someField);
// fi->set(QJsonValue("5")); // someField is now 5
//
// There are additional implementations of 'makeFieldInterface' that let you specify what valid values are.
// Example:
// int someField = 0;
// int min = 1, max = 4;
// FieldInterface* fi = makeFieldInterface(&someField, min, max);
// fi->set(QJsonValue("5")); // someField is now 4 (defaults to closest bound), error messages returned
// or
// QString someField = "";
// QList<QString> options = {"hello","hi there"};
// FieldInterface* fi = makeFieldInterface(&someField, options);
// fi->set(QJsonValue("5")); // someField is now "hello" (defaults to first element), error messages returned
// Base class lets us use the interface without any type information.
class FieldInterface {
public:
FieldInterface(){};
virtual ~FieldInterface() {};
virtual QJsonValue get() const = 0;
virtual QStringList set(const QJsonValue& json) const = 0;
};
template <typename T>
class BasicFieldInterface : public FieldInterface {
public:
BasicFieldInterface(T* field) : m_field(field) {
Q_ASSERT(m_field);
};
virtual ~BasicFieldInterface() {};
virtual QJsonValue get() const override {return Converter<T>::toJson(*m_field);}
virtual QStringList set(const QJsonValue& json) const override {
QStringList errors;
auto value = Converter<T>::fromJson(json, &errors);
if (errors.isEmpty()) *m_field = value; // Don't bother changing the value if conversion failed
return errors;
}
protected:
T* m_field;
};
template <typename T>
static FieldInterface* makeFieldInterface(T* field) {
return new BasicFieldInterface<T>(field);
}
// Create a regular FieldInterface, but override 'set' to use the given min/max.
template <typename T>
static FieldInterface* makeFieldInterface(T* field, const T& min, const T& max) {
class BoundedFieldInterface : public BasicFieldInterface<T> {
public:
BoundedFieldInterface(T* field, const T& min, const T& max)
: BasicFieldInterface<T>(field), m_min(min), m_max(max) {};
virtual ~BoundedFieldInterface() {};
virtual QStringList set(const QJsonValue& json) const override {
QStringList errors;
auto value = Converter<T>::fromJson(json, &errors);
if (errors.isEmpty()) { // Don't bother changing the value if conversion failed
value = Converter<T>::clamp(value, m_min, m_max, &errors);
*this->m_field = value;
}
return errors;
}
private:
const T m_min;
const T m_max;
};
return new BoundedFieldInterface(field, min, max);
}
// Create a regular FieldInterface, but override 'set' to use the given 'acceptableValues'.
template <typename T>
static FieldInterface* makeFieldInterface(T* field, const QList<T>& acceptableValues) {
Q_ASSERT(!acceptableValues.isEmpty());
class BoundedFieldInterface : public BasicFieldInterface<T> {
public:
BoundedFieldInterface(T* field, const QList<T>& acceptableValues)
: BasicFieldInterface<T>(field),
m_acceptableValues(acceptableValues.begin(), acceptableValues.end()),
m_defaultValue(acceptableValues.first()) {};
virtual ~BoundedFieldInterface() {};
virtual QStringList set(const QJsonValue& json) const override {
QStringList errors;
auto value = Converter<T>::fromJson(json, &errors);
if (errors.isEmpty()) {
if (!m_acceptableValues.contains(value)) {
value = m_defaultValue;
errors.append("Invalid value.");
}
*this->m_field = value;
}
return errors;
}
private:
// The order of the list only matters for determining the default value,
// so save that separately and convert the list to a set for better lookup speed.
const QSet<T> m_acceptableValues;
const T m_defaultValue;
};
return new BoundedFieldInterface(field, acceptableValues);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// FieldManager manages a QHash mapping string keys to FieldInterfaces.
// This makes it easy to map many fields to/from JSON without explicitly serializing anything.
class FieldManager {
public:
~FieldManager(){ clear(); }
QStringList setField(const QString& key, const QJsonValue& value) const {
auto it = m_fields.find(key);
return (it != m_fields.end()) ? it.value()->set(value) : QStringList();
}
QJsonValue getField(const QString& key) const {
auto it = m_fields.find(key);
return (it != m_fields.end()) ? it.value()->get() : QJsonValue();
}
QJsonObject getFields() const {
QJsonObject obj;
for (auto it = m_fields.constBegin(); it != m_fields.constEnd(); it++) {
obj[it.key()] = it.value()->get();
}
return obj;
}
void clear() {
qDeleteAll(m_fields);
m_fields.clear();
}
bool hasField(const QString& key) const {return m_fields.contains(key);}
template <typename T>
void addField(T* field, const QString& key) {
Q_ASSERT(!m_fields.contains(key));
m_fields.insert(key, makeFieldInterface<T>(field));
}
template <typename T>
void addField(T* field, const QString& key, const T& min, const T& max) {
Q_ASSERT(!m_fields.contains(key));
m_fields.insert(key, makeFieldInterface<T>(field, min, max));
}
template <typename T>
void addField(T* field, const QString& key, const QList<T>& acceptableValues) {
Q_ASSERT(!m_fields.contains(key));
m_fields.insert(key, makeFieldInterface<T>(field, acceptableValues));
}
private:
QHash<QString, FieldInterface*> m_fields;
};

View File

@ -8,6 +8,7 @@
#include <QPixmap>
#include <QString>
#include <QUndoStack>
#include <QJsonObject>
class Map;
class LayoutPixmapItem;

View File

@ -3,11 +3,13 @@
#define METATILE_H
#include "tile.h"
#include "config.h"
#include "basegame.h"
#include "bitpacker.h"
#include <QImage>
#include <QPoint>
#include <QString>
#include <QMap>
#include <QSet>
class Project;
@ -41,7 +43,7 @@ public:
uint32_t getAttributes() const;
uint32_t getAttribute(Metatile::Attr attr) const { return this->attributes.value(attr, 0); }
void setAttributes(uint32_t data);
void setAttributes(uint32_t data, BaseGameVersion version);
void setAttributes(uint32_t data, BaseGame::Version version);
void setAttribute(Metatile::Attr attr, uint32_t value);
// For convenience
@ -56,17 +58,18 @@ public:
static int getIndexInTileset(int);
static QPoint coordFromPixmapCoord(const QPointF &pixelCoord);
static uint32_t getDefaultAttributesMask(BaseGameVersion version, Metatile::Attr attr);
static uint32_t getDefaultAttributesMask(BaseGame::Version version, Metatile::Attr attr);
static uint32_t getMaxAttributesMask();
static int getDefaultAttributesSize(BaseGameVersion version);
static int getDefaultAttributesSize(BaseGame::Version version);
static void setLayout(Project*);
static QString getMetatileIdString(uint16_t metatileId);
static QString getMetatileIdStrings(const QList<uint16_t> &metatileIds);
static QString getLayerName(int layerNum);
static int numLayers();
static constexpr int tileWidth() { return 2; }
static constexpr int tileHeight() { return 2; }
static constexpr int tilesPerLayer() { return Metatile::tileWidth() * Metatile::tileHeight(); }
static int maxTiles() { return Metatile::numLayers() * Metatile::tilesPerLayer(); }
static constexpr int pixelWidth() { return Metatile::tileWidth() * Tile::pixelWidth(); }
static constexpr int pixelHeight() { return Metatile::tileHeight() * Tile::pixelHeight(); }
static constexpr QSize pixelSize() { return QSize(pixelWidth(), pixelHeight()); }

31
include/core/orderedset.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#ifndef ORDERED_SET_H
#define ORDERED_SET_H
#include <QJsonArray>
#include <set>
template <typename T>
class OrderedSet : public std::set<T>
{
using std::set<T>::set;
public:
// Not introduced to std::set until C++20
#if __cplusplus < 202002L
bool contains(const T& value) const {
return this->find(value) != this->end();
}
#endif
QSet<T> toQSet() const {
return QSet<T>(this->begin(), this->end());
}
static QSet<T> fromQSet(const QSet<T>& set) {
return OrderedSet<T>(set.begin(), set.end());
}
bool isEmpty() const {
return this->empty();
}
};
#endif // ORDERED_SET_H

View File

@ -5,6 +5,7 @@
#include "map.h"
#include "tilemaptileselector.h"
#include "history.h"
#include "config.h"
#include <QStringList>
#include <QString>

View File

@ -0,0 +1,27 @@
#pragma once
#ifndef SCRIPTSETTINGS_H
#define SCRIPTSETTINGS_H
#include <QString>
#include <QList>
// Holds the basic user-provided information about a plug-in script.
struct ScriptSettings {
QString path;
bool enabled = true;
// Scripts can either by specific to the project, or specific to the user.
// This allows projects to send scripts downstream to their users,
// while still allowing them to use their own personal scripts.
bool userOnly = true;
static QStringList filter(const QList<ScriptSettings>& scripts) {
QStringList paths;
for (auto& script : scripts) {
if (script.enabled) paths.append(script.path);
}
return paths;
}
};
#endif // SCRIPTSETTINGS_H

View File

@ -5,9 +5,6 @@
#include <QObject>
#include <QSize>
// TODO: Replace once config refactoring is complete.
extern bool ConfigDisplayIdsHexadecimal;
class Tile
{
public:
@ -30,7 +27,8 @@ public:
QString toString() const;
static QString getTileIdString(uint16_t tileId);
static const uint16_t maxValue;
// Upper limit for raw value (i.e., uint16_t max).
static constexpr uint16_t MaxValue = 0xFFFF;
static constexpr int pixelWidth() { return 8; }
static constexpr int pixelHeight() { return 8; }

View File

@ -18,9 +18,21 @@ namespace Util {
void setErrorStylesheet(QLineEdit *lineEdit, bool isError);
QString toStylesheetString(const QFont &font);
void show(QWidget *w);
QColorSpace toColorSpace(int colorSpaceInt);
QString mkpath(const QString& dirPath);
QString getFileHash(const QString &filepath);
// Given a QMap<T,QString>, erases all entries with empty strings.
// Returns the number of entries erased.
template <typename T>
int removeEmptyStrings(QMap<T,QString> *map) {
if (!map) return 0;
int numRemoved = 0;
for (auto it = map->begin(); it != map->end();) {
if (it.value().isEmpty()) it = map->erase(it);
else {it++; numRemoved++;}
}
return numRemoved;
}
}
#endif // UTILITY_H

9
include/core/version.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#ifndef VERSION_H
#define VERSION_H
#include <QVersionNumber>
extern const QVersionNumber porymapVersion;
#endif // VERSION_H

1549
include/lib/magic_enum.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -440,7 +440,7 @@ private:
void initShortcuts();
void initExtraShortcuts();
void loadUserSettings();
void restoreWindowState();
void resizeWithinScreen();
void setTheme(QString);
void updateTilesetEditor();
Event::Group getEventGroupFromTabWidget(QWidget *tab);

View File

@ -105,7 +105,7 @@ public:
void clearHealLocations();
bool sanityCheck();
int getSupportedMajorVersion(QString *errorOut = nullptr);
QVersionNumber getMinimumVersion(QString *errorOut = nullptr) const;
bool load();
QMap<QString, Tileset*> tilesetCache;

View File

@ -50,7 +50,7 @@ public:
Q_INVOKABLE bool getBorderVisibility();
Q_INVOKABLE void setSmartPathsEnabled(bool visible);
Q_INVOKABLE bool getSmartPathsEnabled();
Q_INVOKABLE QList<QString> getCustomScripts();
static Q_INVOKABLE QList<QString> getCustomScripts();
Q_INVOKABLE QList<int> getMetatileLayerOrder();
Q_INVOKABLE void setMetatileLayerOrder(const QList<int> &order);
Q_INVOKABLE QList<float> getMetatileLayerOpacity();

View File

@ -1,56 +0,0 @@
#ifndef CITYMAPPIXMAPITEM_H
#define CITYMAPPIXMAPITEM_H
#include "tilemaptileselector.h"
#include <QGraphicsPixmapItem>
#include <QByteArray>
#include <QVector>
class CityMapPixmapItem : public QObject, public QGraphicsPixmapItem {
Q_OBJECT
private:
using QGraphicsPixmapItem::paint;
public:
CityMapPixmapItem(QString fname, TilemapTileSelector *tile_selector) {
this->file = fname;
this->tile_selector = tile_selector;
setAcceptHoverEvents(true);
init();
}
TilemapTileSelector *tile_selector;
QString file;
QByteArray data;
void init();
void save();
void create(QString);
virtual void paint(QGraphicsSceneMouseEvent *);
virtual void draw();
int getIndexAt(int, int);
int width();
int height();
QVector<uint8_t> getTiles();
void setTiles(QVector<uint8_t>);
private:
int width_;
int height_;
signals:
void mouseEvent(QGraphicsSceneMouseEvent *, CityMapPixmapItem *);
void hoveredRegionMapTileChanged(int x, int y);
void hoveredRegionMapTileCleared();
protected:
void mousePressEvent(QGraphicsSceneMouseEvent*);
void mouseMoveEvent(QGraphicsSceneMouseEvent*);
void mouseReleaseEvent(QGraphicsSceneMouseEvent*);
};
#endif // CITYMAPPIXMAPITEM_H

View File

@ -33,7 +33,7 @@ private:
bool hasUnsavedChanges = false;
const QString baseDir;
void displayScript(const QString &filepath, bool enabled);
void displayScript(const ScriptSettings &settings);
void displayNewScript(QString filepath);
QString chooseScript(QString dir);
void removeScript(QListWidgetItem * item);
@ -46,7 +46,6 @@ private:
int prompt(const QString &text, QMessageBox::StandardButton defaultButton);
void save();
void closeEvent(QCloseEvent*);
void restoreWindowState();
void initShortcuts();
QObjectList shortcutableObjects() const;
void openManual();

View File

@ -2,6 +2,7 @@
#define CUSTOMSCRIPTSLISTITEM_H
#include <QFrame>
#include "scriptsettings.h"
namespace Ui {
class CustomScriptsListItem;
@ -13,9 +14,26 @@ class CustomScriptsListItem : public QFrame
public:
explicit CustomScriptsListItem(QWidget *parent = nullptr);
explicit CustomScriptsListItem(const ScriptSettings& settings, QWidget *parent = nullptr);
~CustomScriptsListItem();
public:
void setSettings(const ScriptSettings& settings);
ScriptSettings getSettings() const;
void setPath(const QString& text);
QString path() const;
void setScriptEnabled(bool enabled);
bool scriptEnabled() const;
signals:
void clickedChooseScript();
void clickedEditScript();
void clickedDeleteScript();
void toggledEnable(bool checked);
void pathEdited(const QString& text);
private:
Ui::CustomScriptsListItem *ui;
};

View File

@ -1,5 +1,6 @@
#include <QObject>
#include <QEvent>
#include <QSet>
/// Ctrl+Wheel = zoom
@ -16,7 +17,6 @@ public slots:
};
/// Emits a signal when a window gets activated / regains focus
class ActiveWindowFilter : public QObject {
Q_OBJECT
@ -27,3 +27,15 @@ public:
signals:
void activated();
};
class GeometrySaver : public QObject {
Q_OBJECT
public:
GeometrySaver(QObject *parent, bool enableLogging = true)
: QObject(parent), m_loggingEnabled(enableLogging) {}
bool eventFilter(QObject *obj, QEvent *event) override;
private:
bool m_loggingEnabled = true;
QSet<QObject*> m_shown;
};

View File

@ -3,10 +3,12 @@
#include <QDialog>
#include <QAbstractButton>
#include <QJsonObject>
#include "metatile.h"
class GridSettings {
public:
explicit GridSettings() {};
constexpr GridSettings() {};
~GridSettings() {};
enum Style {
@ -17,15 +19,18 @@ public:
Dots,
};
uint width = 16;
uint height = 16;
uint width = Metatile::pixelWidth();
uint height = Metatile::pixelHeight();
int offsetX = 0;
int offsetY = 0;
Style style = Style::Solid;
QColor color = Qt::black;
QColor color = QColorConstants::Black;
QVector<qreal> getHorizontalDashPattern() const { return this->getDashPattern(this->width); }
QVector<qreal> getVerticalDashPattern() const { return this->getDashPattern(this->height); }
QJsonObject toJson() const;
static GridSettings fromJson(const QJsonObject &obj);
static QString getStyleName(Style style);
static GridSettings::Style getStyleFromName(const QString &name);
private:

View File

@ -84,7 +84,12 @@ private:
QImage m_previewImage;
bool m_previewUpdateQueued = false;
QList<int> m_layerOrder;
ProjectConfig m_savedConfig;
struct SavedConfigSettings {
QColor transparencyColor;
uint16_t unusedTileNormal = 0;
uint16_t unusedTileCovered = 0;
uint16_t unusedTileSplit = 0;
} m_savedConfig;
QList<QRadioButton*> m_transparencyButtons;
void populate(const Settings &settings);

View File

@ -54,7 +54,6 @@ private:
void commitEditHistory();
void commitEditHistory(int paletteId);
void updateEditHistoryActions();
void restoreWindowState();
void invalidateCache();
void closeEvent(QCloseEvent*);
void setColorInputTitles(bool show);

View File

@ -25,7 +25,7 @@ public:
void addPrefab(MetatileSelection selection, Layout *layout, QString name);
void updatePrefabUi(QPointer<Layout> layout);
void clearPrefabUi();
bool tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, QString filepath = "");
bool tryImportDefaultPrefabs(QWidget * parent, BaseGame::Version version, QString filepath = "");
private:
QPointer<MetatileSelector> selector;

View File

@ -40,7 +40,6 @@ private:
void initUi();
void connectSignals();
void restoreWindowState();
void save();
void refresh();
void closeEvent(QCloseEvent*);
@ -72,6 +71,7 @@ private:
void addNewGlobalConstant();
void addGlobalConstant(const QString &name, const QString &expression);
QMap<QString,QString> getGlobalConstants();
BaseGame::Version getBaseGameVersion() const;
private slots:
void dialogButtonClicked(QAbstractButton *button);

View File

@ -2,7 +2,6 @@
#define REGIONMAPEDITOR_H
#include "regionmappixmapitem.h"
#include "citymappixmapitem.h"
#include "regionmaplayoutpixmapitem.h"
#include "regionmapentriespixmapitem.h"
#include "regionmap.h"
@ -118,7 +117,6 @@ private:
void setRegionMap(RegionMap *map);
void setLocations(const QStringList &locations);
void restoreWindowState();
void closeEvent(QCloseEvent* event);
void setTileHFlip(bool enabled);

View File

@ -114,7 +114,6 @@ private:
void initMetatileLayersItem();
void initShortcuts();
void initExtraShortcuts();
void restoreWindowState();
void initMetatileHistory();
void setTilesets(QString primaryTilesetLabel, QString secondaryTilesetLabel);
void reset();

View File

@ -19,8 +19,6 @@ public:
explicit WildMonChart(QWidget *parent, const EncounterTableModel *table);
~WildMonChart();
virtual void closeEvent(QCloseEvent *event) override;
public slots:
void setTable(const EncounterTableModel *table);
void clearTable();

View File

@ -46,9 +46,15 @@ DEFINES += PORYMAP_LATEST_COMMIT=\\\"$$LATEST_COMMIT\\\"
VERSION = 6.3.0
DEFINES += PORYMAP_VERSION=\\\"$$VERSION\\\"
SOURCES += src/core/advancemapparser.cpp \
SOURCES += src/config/keyvalueconfigbase.cpp \
src/config/legacy.cpp \
src/config/porymapconfig.cpp \
src/config/projectconfig.cpp \
src/config/shortcutsconfig.cpp \
src/config/userconfig.cpp \
src/core/advancemapparser.cpp \
src/core/basegame.cpp \
src/core/block.cpp \
src/ui/resizelayoutpopup.cpp \
src/core/bitpacker.cpp \
src/core/blockdata.cpp \
src/core/events.cpp \
@ -66,6 +72,7 @@ SOURCES += src/core/advancemapparser.cpp \
src/core/tileset.cpp \
src/core/utility.cpp \
src/core/validator.cpp \
src/core/version.cpp \
src/core/regionmap.cpp \
src/core/wildmoninfo.cpp \
src/core/editcommands.cpp \
@ -113,7 +120,7 @@ SOURCES += src/core/advancemapparser.cpp \
src/ui/layoutpixmapitem.cpp \
src/ui/prefabcreationdialog.cpp \
src/ui/regionmappixmapitem.cpp \
src/ui/citymappixmapitem.cpp \
src/ui/resizelayoutpopup.cpp \
src/ui/mapheaderform.cpp \
src/ui/metatilelayersitem.cpp \
src/ui/metatileselector.cpp \
@ -152,7 +159,6 @@ SOURCES += src/core/advancemapparser.cpp \
src/ui/colorpicker.cpp \
src/ui/loadingscreen.cpp \
src/ui/unlockableicon.cpp \
src/config.cpp \
src/editor.cpp \
src/main.cpp \
src/mainwindow.cpp \
@ -164,10 +170,16 @@ SOURCES += src/core/advancemapparser.cpp \
src/ui/wildmonchart.cpp \
src/ui/wildmonsearch.cpp
HEADERS += include/core/advancemapparser.h \
HEADERS += include/config/keyvalueconfigbase.h \
include/config/porymapconfig.h \
include/config/projectconfig.h \
include/config/shortcutsconfig.h \
include/config/userconfig.h \
include/core/advancemapparser.h \
include/core/block.h \
include/core/bitpacker.h \
include/core/blockdata.h \
include/core/converter.h \
include/core/events.h \
include/core/filedialog.h \
include/core/history.h \
@ -184,6 +196,7 @@ HEADERS += include/core/advancemapparser.h \
include/core/tileset.h \
include/core/utility.h \
include/core/validator.h \
include/core/version.h \
include/core/regionmap.h \
include/core/wildmoninfo.h \
include/core/editcommands.h \
@ -233,7 +246,6 @@ HEADERS += include/core/advancemapparser.h \
include/ui/mapview.h \
include/ui/prefabcreationdialog.h \
include/ui/regionmappixmapitem.h \
include/ui/citymappixmapitem.h \
include/ui/colorinputwidget.h \
include/ui/metatilelayersitem.h \
include/ui/metatileselector.h \
@ -274,7 +286,6 @@ HEADERS += include/core/advancemapparser.h \
include/ui/colorpicker.h \
include/ui/loadingscreen.h \
include/ui/unlockableicon.h \
include/config.h \
include/editor.h \
include/mainwindow.h \
include/project.h \
@ -332,6 +343,7 @@ RESOURCES += \
resources/text.qrc
INCLUDEPATH += include
INCLUDEPATH += include/config
INCLUDEPATH += include/core
INCLUDEPATH += include/ui
INCLUDEPATH += include/lib

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
#include "keyvalueconfigbase.h"
#include "log.h"
#include <QDir>
#include <QJsonDocument>
#include <QJsonParseError>
void KeyValueConfigBase::setRoot(const QString& root) {
m_root = root;
QDir dir(m_root);
if (!m_root.isEmpty() && !dir.exists()) {
dir.mkpath(m_root);
}
// Caching the filepath constructed from m_root + m_filename
m_filepath = dir.absoluteFilePath(m_filename);
}
bool KeyValueConfigBase::load() {
QFile file(filepath());
if (file.exists()) {
if (!file.open(QIODevice::ReadOnly)) {
logError(QString("Failed to read config file '%1': %2").arg(filepath()).arg(file.errorString()));
return false;
}
if (file.size() == 0) {
logWarn(QString("Config file '%1' was empty.").arg(filepath()));
// An empty file isn't a valid JSON file, but other than the warning
// we'll treat it the same as if it were a JSON file with an empty object.
initializeFromEmpty();
return true;
}
QJsonParseError parseError;
const QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
logError(QString("Failed to read config file '%1': %2").arg(filepath()).arg(parseError.errorString()));
return false;
}
if (!jsonDoc.isObject()) {
logError(QString("Failed to read config file '%1': Expected top level JSON object.").arg(filepath()));
return false;
}
loadFromJson(jsonDoc.object());
logInfo(QString("Loaded config file '%1'").arg(filename()));
} else if (!loadLegacy()) {
// No config file is present (either in the new or old format)
initializeFromEmpty();
}
return true;
}
void KeyValueConfigBase::loadFromJson(const QJsonObject& obj) {
for (auto it = obj.begin(); it != obj.end(); it++) {
if (!parseJsonKeyValue(it.key(), it.value())) {
logWarn(QString("Discarding unrecognized config key '%1'").arg(it.key()));
}
}
}
bool KeyValueConfigBase::parseJsonKeyValue(const QString& key, const QJsonValue& value) {
auto fieldManager = getFieldManager();
if (!fieldManager || !fieldManager->hasField(key)) return false;
// Recognized 'key' as a registered field. Let the FieldManager try to assign the value.
const QStringList errors = fieldManager->setField(key, value);
if (errors.length() == 1) logWarn(QString("Failed to read config key '%1': %2").arg(key).arg(errors.at(0)));
else if (errors.length() > 1) logWarn(QString("Failed to read config key '%1':\n%2").arg(key).arg(errors.join("\n")));
return true;
}
QJsonObject KeyValueConfigBase::toJson() {
auto fieldManager = getFieldManager();
return fieldManager ? fieldManager->getFields() : QJsonObject();
}
bool KeyValueConfigBase::save() {
QFile file(filepath());
if (!file.open(QIODevice::WriteOnly)) {
logError(QString("Could not open config file '%1' for writing: ").arg(filepath()) + file.errorString());
return false;
}
QJsonObject savedObject;
if (m_saveAllFields) {
savedObject = toJson();
} else {
// We limit the output to fields that have changed from the default value.
// This has a few notable benefits:
// - It allows changes to the default values to be downstreamed from Porymap.
// - It reduces diff noise for configs as Porymap's settings change over time.
// - It discourages manual editing of the file; all settings should be edited in the GUI.
// If the child class does not reimplement getDefaultJson it returns an empty QJsonObject,
// and so the default behavior is to output all fields.
const QJsonObject curObject = toJson();
const QJsonObject defaultObject = getDefaultJson();
for (auto it = curObject.begin(); it != curObject.end(); it++) {
if (it.value() != defaultObject.value(it.key())) {
savedObject[it.key()] = it.value();
}
}
}
QJsonDocument doc(savedObject);
file.write(doc.toJson());
return true;
}

419
src/config/legacy.cpp Normal file
View File

@ -0,0 +1,419 @@
#include "config.h"
#include "log.h"
#include "tile.h"
#include "block.h"
#include <QString>
#include <QDir>
#include <QColor>
QString getLegacyFilename(const QString &newFilename) {
static const QMap<QString, QString> map = {
{QStringLiteral("settings.json"), QStringLiteral("porymap.cfg")},
{QStringLiteral("shortcuts.json"), QStringLiteral("porymap.shortcuts.cfg")},
{QStringLiteral("porymap.project.json"), QStringLiteral("porymap.project.cfg")},
{QStringLiteral("porymap.user.json"), QStringLiteral("porymap.user.cfg")},
};
return map.value(newFilename);
}
bool KeyValueConfigBase::loadLegacy() {
const QString oldFilename = getLegacyFilename(filename());
if (oldFilename.isEmpty()) return false;
QDir dir(root());
QFile file(dir.absoluteFilePath(oldFilename));
if (!file.exists() || !file.open(QIODevice::ReadOnly)) return false;
QTextStream in(&file);
static const QRegularExpression re("^(?<key>[^=]+)=(?<value>.*)$");
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
int commentIndex = line.indexOf("#");
if (commentIndex >= 0) {
line = line.left(commentIndex).trimmed();
}
if (line.length() == 0) {
continue;
}
QRegularExpressionMatch match = re.match(line);
if (match.hasMatch()) {
parseLegacyKeyValue(match.captured("key").trimmed(), match.captured("value").trimmed());
}
}
logInfo(QString("Loaded legacy config file '%1'").arg(oldFilename));
// Save before deleting the old config file to ensure no data is lost.
if (save()) {
if (!file.remove()) logWarn(QString("Failed to delete legacy config file '%1'.").arg(oldFilename));
else logInfo(QString("Deleted legacy config file '%1'.").arg(oldFilename));
}
return true;
}
bool toBool(const QString &value) {
return (value.toInt(nullptr, 0) != 0);
}
int toInt(const QString &value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0) {
bool ok;
int result = value.toInt(&ok, 0);
if (!ok) result = defaultValue;
return qBound(min, result, max);
}
uint32_t toUInt(const QString &value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0) {
bool ok;
uint32_t result = value.toUInt(&ok, 0);
if (!ok) result = defaultValue;
return qBound(min, result, max);
}
QColor toColor(const QString &value, const QColor &defaultValue = QColor(Qt::black)) {
if (value.isEmpty()) return QColor();
QColor color = QColor("#" + value);
if (!color.isValid()) color = defaultValue;
return color;
}
bool PorymapConfig::parseLegacyKeyValue(const QString &key, const QString &value) {
if (key == "recent_project") {
this->recentProjects = value.split(",", Qt::SkipEmptyParts);
this->recentProjects.removeDuplicates();
} else if (key == "project_manually_closed") {
this->projectManuallyClosed = toBool(value);
} else if (key == "reopen_on_launch") {
this->reopenOnLaunch = toBool(value);
} else if (key == "pretty_cursors") {
this->prettyCursors = toBool(value);
} else if (key == "map_list_tab") {
this->mapListTab = toInt(value, 0, 2, 0);
} else if (key == "map_list_edit_groups_enabled") {
this->mapListEditGroupsEnabled = toBool(value);
} else if (key.startsWith("map_list_hide_empty_enabled/")) {
bool ok;
int tab = key.mid(QStringLiteral("map_list_hide_empty_enabled/").length()).toInt(&ok, 0);
if (ok && toBool(value)) this->mapListTabsHidingEmptyFolders.insert(tab);
} else if (key == "map_list_layouts_sorted") {
this->mapListLayoutsSorted = toBool(value);
} else if (key == "map_list_locations_sorted") {
this->mapListLocationsSorted = toBool(value);
} else if (key == "mirror_connecting_maps") {
this->mirrorConnectingMaps = toBool(value);
} else if (key == "show_dive_emerge_maps") {
this->showDiveEmergeMaps = toBool(value);
} else if (key == "dive_emerge_map_opacity") {
this->diveEmergeMapOpacity = toInt(value, 10, 90, 30);
} else if (key == "dive_map_opacity") {
this->diveMapOpacity = toInt(value, 10, 90, 15);
} else if (key == "emerge_map_opacity") {
this->emergeMapOpacity = toInt(value, 10, 90, 15);
} else if (key == "collision_opacity") {
this->collisionOpacity = toInt(value, 0, 100, 50);
} else if (key == "metatiles_zoom") {
this->metatilesZoom = toInt(value, 10, 100, 30);
} else if (key == "collision_zoom") {
this->collisionZoom = toInt(value, 10, 100, 30);
} else if (key == "tileset_editor_metatiles_zoom") {
this->tilesetEditorMetatilesZoom = toInt(value, 10, 100, 30);
} else if (key == "tileset_editor_tiles_zoom") {
this->tilesetEditorTilesZoom = toInt(value, 10, 100, 30);
} else if (key == "tileset_editor_layer_orientation") {
// Being explicit here to avoid casting out-of-range values.
this->tilesetEditorLayerOrientation = (toInt(value) == static_cast<int>(Qt::Horizontal)) ? Qt::Horizontal : Qt::Vertical;
} else if (key == "show_player_view") {
this->showPlayerView = toBool(value);
} else if (key == "show_cursor_tile") {
this->showCursorTile = toBool(value);
} else if (key == "show_border") {
this->showBorder = toBool(value);
} else if (key == "show_grid") {
this->showGrid = toBool(value);
} else if (key == "show_tileset_editor_metatile_grid") {
this->showTilesetEditorMetatileGrid = toBool(value);
} else if (key == "show_tileset_editor_layer_grid") {
this->showTilesetEditorLayerGrid = toBool(value);
} else if (key == "show_tileset_editor_divider") {
this->showTilesetEditorDivider = toBool(value);
} else if (key == "show_tileset_editor_raw_attributes") {
this->showTilesetEditorRawAttributes = toBool(value);
} else if (key == "show_palette_editor_unused_colors") {
this->showPaletteEditorUnusedColors = toBool(value);
} else if (key == "monitor_files") {
this->monitorFiles = toBool(value);
} else if (key == "tileset_checkerboard_fill") {
this->tilesetCheckerboardFill = toBool(value);
} else if (key == "new_map_header_section_expanded") {
this->newMapHeaderSectionExpanded = toBool(value);
} else if (key == "theme") {
this->theme = value;
} else if (key == "wild_mon_chart_theme") {
this->wildMonChartTheme = value;
} else if (key == "text_editor_open_directory") {
this->textEditorOpenFolder = value;
} else if (key == "text_editor_goto_line") {
this->textEditorGotoLine = value;
} else if (key == "palette_editor_bit_depth") {
int bitDepth = toInt(value, 15, 24, 24);
if (bitDepth == 15 || bitDepth == 24){
this->paletteEditorBitDepth = bitDepth;
}
} else if (key == "project_settings_tab") {
this->projectSettingsTab = toInt(value, 0);
} else if (key == "load_all_event_scripts") { // Old setting replaced by script_autocomplete_mode
this->scriptAutocompleteMode = toBool(value) ? ScriptAutocompleteMode::All : ScriptAutocompleteMode::MapOnly;
} else if (key == "script_autocomplete_mode") {
this->scriptAutocompleteMode = static_cast<ScriptAutocompleteMode>(toInt(value, ScriptAutocompleteMode::MapOnly, ScriptAutocompleteMode::All));
} else if (key == "warp_behavior_warning_disabled") {
this->warpBehaviorWarningDisabled = toBool(value);
} else if (key == "event_delete_warning_disabled") {
this->eventDeleteWarningDisabled = toBool(value);
} else if (key == "event_overlay_enabled") {
this->eventOverlayEnabled = toBool(value);
} else if (key == "check_for_updates") {
this->checkForUpdates = toBool(value);
} else if (key == "show_project_loading_screen") {
this->showProjectLoadingScreen = toBool(value);
} else if (key == "last_update_check_time") {
this->lastUpdateCheckTime = QDateTime::fromString(value).toLocalTime();
} else if (key == "last_update_check_version") {
auto version = QVersionNumber::fromString(value);
if (version.segmentCount() == 3) {
this->lastUpdateCheckVersion = version;
}
} else if (key.startsWith("rate_limit_time/")) {
static const QRegularExpression regex("\\brate_limit_time/(?<url>.+)");
QRegularExpressionMatch match = regex.match(key);
if (match.hasMatch()) {
this->rateLimitTimes.insert(match.captured("url"), QDateTime::fromString(value).toLocalTime());
}
} else if (key == "event_selection_shape_mode") {
if (value == "mask") {
this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape;
} else if (value == "bounding_rect") {
this->eventSelectionShapeMode = QGraphicsPixmapItem::BoundingRectShape;
}
} else if (key == "shown_in_game_reload_message") {
this->shownInGameReloadMessage = toBool(value);
} else if (key == "grid_width") {
this->gridSettings.width = toUInt(value);
} else if (key == "grid_height") {
this->gridSettings.height = toUInt(value);
} else if (key == "grid_x") {
this->gridSettings.offsetX = toInt(value, 0, 999);
} else if (key == "grid_y") {
this->gridSettings.offsetY = toInt(value, 0, 999);
} else if (key == "grid_style") {
this->gridSettings.style = GridSettings::getStyleFromName(value);
} else if (key == "grid_color") {
this->gridSettings.color = toColor(value);
} else if (key == "status_bar_log_types") {
this->statusBarLogTypes.clear();
auto typeStrings = value.split(",", Qt::SkipEmptyParts);
for (const auto &typeString : typeStrings) {
LogType type = static_cast<LogType>(toInt(typeString, 0, 2));
this->statusBarLogTypes.insert(type);
}
} else if (key == "application_font") {
this->applicationFont = QFont();
this->applicationFont.fromString(value);
} else if (key == "map_list_font") {
this->mapListFont = QFont();
this->mapListFont.fromString(value);
} else {
return false;
}
return true;
}
bool ProjectConfig::parseLegacyKeyValue(const QString &key, const QString &value_) {
QString value(value_);
if (key == "base_game_version") {
this->baseGameVersion = BaseGame::stringToVersion(value);
} else if (key == "use_poryscript") {
this->usePoryScript = toBool(value);
} else if (key == "use_custom_border_size") {
this->useCustomBorderSize = toBool(value);
} else if (key == "enable_event_weather_trigger") {
this->eventWeatherTriggerEnabled = toBool(value);
} else if (key == "enable_event_secret_base") {
this->eventSecretBaseEnabled = toBool(value);
} else if (key == "enable_hidden_item_quantity") {
this->hiddenItemQuantityEnabled = toBool(value);
} else if (key == "enable_hidden_item_requires_itemfinder") {
this->hiddenItemRequiresItemfinderEnabled = toBool(value);
} else if (key == "enable_heal_location_respawn_data") {
this->healLocationRespawnDataEnabled = toBool(value);
} else if (key == "enable_event_clone_object" || key == "enable_object_event_in_connection") {
this->eventCloneObjectEnabled = toBool(value);
} else if (key == "enable_floor_number") {
this->floorNumberEnabled = toBool(value);
} else if (key == "create_map_text_file") {
this->createMapTextFileEnabled = toBool(value);
} else if (key == "enable_triple_layer_metatiles") {
this->tripleLayerMetatilesEnabled = toBool(value);
} else if (key == "default_metatile") {
this->defaultMetatileId = toUInt(value, 0, Block::MaxValue);
} else if (key == "default_elevation") {
this->defaultElevation = toUInt(value, 0, Block::MaxValue);
} else if (key == "default_collision") {
this->defaultCollision = toUInt(value, 0, Block::MaxValue);
} else if (key == "default_map_width") {
this->defaultMapSize.setWidth(toInt(value, 1));
} else if (key == "default_map_height") {
this->defaultMapSize.setHeight(toInt(value, 1));
} else if (key == "new_map_border_metatiles") {
this->newMapBorderMetatileIds.clear();
QList<QString> metatileIds = value.split(",");
for (int i = 0; i < metatileIds.size(); i++) {
int metatileId = toUInt(metatileIds.at(i), 0, Block::MaxValue);
this->newMapBorderMetatileIds.append(metatileId);
}
} else if (key == "default_primary_tileset") {
this->defaultPrimaryTileset = value;
} else if (key == "default_secondary_tileset") {
this->defaultSecondaryTileset = value;
} else if (key == "metatile_attributes_size") {
int size = toInt(value, 1, 4, 2);
if (!(size & (size - 1))) this->metatileAttributesSize = size;
} else if (key == "metatile_behavior_mask") {
this->metatileBehaviorMask = toUInt(value);
} else if (key == "metatile_terrain_type_mask") {
this->metatileTerrainTypeMask = toUInt(value);
} else if (key == "metatile_encounter_type_mask") {
this->metatileEncounterTypeMask = toUInt(value);
} else if (key == "metatile_layer_type_mask") {
this->metatileLayerTypeMask = toUInt(value);
} else if (key == "block_metatile_id_mask") {
this->blockMetatileIdMask = toUInt(value, 0, Block::MaxValue);
} else if (key == "block_collision_mask") {
this->blockCollisionMask = toUInt(value, 0, Block::MaxValue);
} else if (key == "block_elevation_mask") {
this->blockElevationMask = toUInt(value, 0, Block::MaxValue);
} else if (key == "unused_tile_normal") {
this->unusedTileNormal = toUInt(value, 0, Tile::MaxValue);
} else if (key == "unused_tile_covered") {
this->unusedTileCovered = toUInt(value, 0, Tile::MaxValue);
} else if (key == "unused_tile_split") {
this->unusedTileSplit = toUInt(value, 0, Tile::MaxValue);
} else if (key == "enable_map_allow_flags") {
this->mapAllowFlagsEnabled = toBool(value);
} else if (key.startsWith("path/")) {
auto k = reverseDefaultPaths(key.mid(QStringLiteral("path/").length()));
if (k != static_cast<ProjectFilePath>(-1)) {
this->setFilePath(k, value);
}
} else if (key.startsWith("ident/")) {
auto identifierId = reverseDefaultIdentifier(key.mid(QStringLiteral("ident/").length()));
if (identifierId != static_cast<ProjectIdentifier>(-1)) {
this->setIdentifier(identifierId, value);
}
} else if (key.startsWith("global_constant/")) {
this->globalConstants.insert(key.mid(QStringLiteral("global_constant/").length()), value);
} else if (key == "global_constants_filepaths") {
this->globalConstantsFilepaths = value.split(",", Qt::SkipEmptyParts);
} else if (key == "tilesets_have_callback") {
this->tilesetsHaveCallback = toBool(value);
} else if (key == "tilesets_have_is_compressed") {
this->tilesetsHaveIsCompressed = toBool(value);
} else if (key == "set_transparent_pixels_black") { // Old setting replaced by transparency_color
this->transparencyColor = toBool(value) ? QColor(Qt::black) : QColor();
} else if (key == "transparency_color") {
this->transparencyColor = toColor(value);
} else if (key == "preserve_matching_only_data") {
this->preserveMatchingOnlyData = toBool(value);
} else if (key == "event_icon_path_object") {
this->eventIconPaths[Event::Group::Object] = value;
} else if (key == "event_icon_path_warp") {
this->eventIconPaths[Event::Group::Warp] = value;
} else if (key == "event_icon_path_coord") {
this->eventIconPaths[Event::Group::Coord] = value;
} else if (key == "event_icon_path_bg") {
this->eventIconPaths[Event::Group::Bg] = value;
} else if (key == "event_icon_path_heal") {
this->eventIconPaths[Event::Group::Heal] = value;
} else if (key.startsWith("pokemon_icon_path/")) {
this->pokemonIconPaths.insert(key.mid(QStringLiteral("pokemon_icon_path/").length()), value);
} else if (key == "events_tab_icon_path") {
this->eventsTabIconPath = value;
} else if (key == "collision_sheet_path") {
this->collisionSheetPath = value;
} else if (key == "collision_sheet_width") {
this->collisionSheetSize.setWidth(toInt(value, 1, Block::MaxValue));
} else if (key == "collision_sheet_height") {
this->collisionSheetSize.setHeight(toInt(value, 1, Block::MaxValue));
} else if (key == "player_view_north") {
this->playerViewDistance.setTop(toInt(value, 0, INT_MAX, GBA_V_DIST_TO_CENTER));
} else if (key == "player_view_south") {
this->playerViewDistance.setBottom(toInt(value, 0, INT_MAX, GBA_V_DIST_TO_CENTER));
} else if (key == "player_view_west") {
this->playerViewDistance.setLeft(toInt(value, 0, INT_MAX, GBA_H_DIST_TO_CENTER));
} else if (key == "player_view_east") {
this->playerViewDistance.setRight(toInt(value, 0, INT_MAX, GBA_H_DIST_TO_CENTER));
} else if (key == "warp_behaviors") {
this->warpBehaviors.clear();
value.remove(" ");
const QStringList behaviorList = value.split(",", Qt::SkipEmptyParts);
for (auto s : behaviorList)
this->warpBehaviors.insert(toUInt(s));
} else if (key == "max_events_per_group") {
this->maxEventsPerGroup = toInt(value, 1, INT_MAX, 255);
} else if (key == "metatile_selector_width") {
this->metatileSelectorWidth = toInt(value, 1, INT_MAX, 8);
} else {
return false;
}
return true;
}
// Read input from the config to get the script paths and whether each is enabled or disbled.
// The format is a comma-separated list of paths. Each path can be followed (before the comma)
// by a :0 or :1 to indicate whether it should be disabled or enabled, respectively. If neither
// follow, it's assumed the script should be enabled.
QList<ScriptSettings> parseCustomScripts(const QString &input) {
QMap<QString,bool> customScripts;
const QList<QString> paths = input.split(",", Qt::SkipEmptyParts);
for (QString path : paths) {
// Read and remove suffix
bool enabled = !path.endsWith(":0");
if (!enabled || path.endsWith(":1"))
path.chop(2);
if (!path.isEmpty()) {
// If a path is repeated only its last instance will be considered.
customScripts.insert(path, enabled);
}
}
QList<ScriptSettings> settingsList;
for (auto it = customScripts.begin(); it != customScripts.end(); it++) {
settingsList.append({
.path = it.key(),
.enabled = it.value(),
.userOnly = true,
});
}
return settingsList;
}
bool UserConfig::parseLegacyKeyValue(const QString &key, const QString &value) {
if (key == "recent_map_or_layout") {
this->recentMapOrLayout = value;
} else if (key == "use_encounter_json") {
this->useEncounterJson = toBool(value);
} else if (key == "custom_scripts") {
this->customScripts = parseCustomScripts(value);
} else {
return false;
}
return true;
}
bool ShortcutsConfig::parseLegacyKeyValue(const QString &key, const QString &value) {
QStringList keySequences = value.split(' ');
for (auto keySequence : keySequences)
user_shortcuts.insert(key, keySequence);
return true;
}

View File

@ -0,0 +1,106 @@
#include "porymapconfig.h"
#include <QMainWindow>
#include <QSplitter>
PorymapConfig porymapConfig;
bool PorymapConfig::save() {
// Clean out old rate limit times, leaving only times still in the future.
for (auto it = this->rateLimitTimes.begin(); it != this->rateLimitTimes.end();) {
const QDateTime time = it.value();
if (!time.isNull() && time > QDateTime::currentDateTime()) {
it = this->rateLimitTimes.erase(it);
} else it++;
}
return KeyValueConfigBase::save();
}
void PorymapConfig::loadFromJson(const QJsonObject& obj) {
KeyValueConfigBase::loadFromJson(obj);
// Reset geometry between major/minor versions.
// We could try to keep separate versions for each geometry,
// but that requires a lot of careful maintenance.
// This ensures that as widgets change they won't
// receive data for old layouts/states, and that as widgets
// get renamed their old keys wont accumulate in the config.
constexpr int CurrentGeometryVersion = 1;
if (this->geometryVersion != CurrentGeometryVersion) {
this->geometryVersion = CurrentGeometryVersion;
this->savedGeometryMap.clear();
}
this->gridSettings.offsetX = std::clamp(this->gridSettings.offsetX, 0, 999);
this->gridSettings.offsetY = std::clamp(this->gridSettings.offsetY, 0, 999);
}
QJsonObject PorymapConfig::getDefaultJson() const {
PorymapConfig defaultConfig;
return defaultConfig.toJson();
}
void PorymapConfig::addRecentProject(const QString& project) {
this->recentProjects.removeOne(project);
this->recentProjects.prepend(project);
}
void PorymapConfig::setRecentProjects(const QStringList& projects) {
this->recentProjects = projects;
}
QString PorymapConfig::getRecentProject() const {
return this->recentProjects.value(0);
}
const QStringList& PorymapConfig::getRecentProjects() const {
return this->recentProjects;
}
void PorymapConfig::saveGeometry(const QWidget* widget, const QString& keyPrefix, bool recursive) {
if (!widget || widget->objectName().isEmpty()) return;
const QString key = keyPrefix + widget->objectName();
this->savedGeometryMap.insert(key, widget->saveGeometry());
// In addition to geometry, some widgets have other states that can be saved/restored.
const QString stateKey = key + QStringLiteral("/State");
auto mainWindow = qobject_cast<const QMainWindow*>(widget);
if (mainWindow) this->savedGeometryMap.insert(stateKey, mainWindow->saveState());
else {
auto splitter = qobject_cast<const QSplitter*>(widget);
if (splitter) this->savedGeometryMap.insert(stateKey, splitter->saveState());
}
if (recursive) {
for (const auto splitter : widget->findChildren<QSplitter*>()) {
saveGeometry(splitter, key + "_", false);
}
}
}
bool PorymapConfig::restoreGeometry(QWidget* widget, const QString& keyPrefix, bool recursive) const {
if (!widget || widget->objectName().isEmpty()) return false;
const QString key = keyPrefix + widget->objectName();
auto it = this->savedGeometryMap.constFind(key);
if (it == this->savedGeometryMap.constEnd()) return false;
widget->restoreGeometry(it.value());
// In addition to geometry, some widgets have other states that can be saved/restored.
it = this->savedGeometryMap.constFind(key + QStringLiteral("/State"));
if (it != this->savedGeometryMap.constEnd()) {
auto mainWindow = qobject_cast<QMainWindow*>(widget);
if (mainWindow) mainWindow->restoreState(it.value());
else {
auto splitter = qobject_cast<QSplitter*>(widget);
if (splitter) splitter->restoreState(it.value());
}
}
if (recursive) {
for (const auto splitter : widget->findChildren<QSplitter*>()) {
restoreGeometry(splitter, key + "_", false);
}
}
return true;
}

View File

@ -0,0 +1,368 @@
#include "projectconfig.h"
#include "utility.h"
#include "validator.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFormLayout>
#include <QLabel>
// TODO: This should eventually be contained by each individual Project instance.
ProjectConfig projectConfig;
const QMap<ProjectIdentifier, QPair<QString, QString>> ProjectConfig::defaultIdentifiers = {
// Symbols
{ProjectIdentifier::symbol_facing_directions, {"symbol_facing_directions", "gInitialMovementTypeFacingDirections"}},
{ProjectIdentifier::symbol_obj_event_gfx_pointers, {"symbol_obj_event_gfx_pointers", "gObjectEventGraphicsInfoPointers"}},
{ProjectIdentifier::symbol_pokemon_icon_table, {"symbol_pokemon_icon_table", "gMonIconTable"}},
{ProjectIdentifier::symbol_attribute_table, {"symbol_attribute_table", "sMetatileAttrMasks"}},
{ProjectIdentifier::symbol_tilesets_prefix, {"symbol_tilesets_prefix", "gTileset_"}},
{ProjectIdentifier::symbol_dynamic_map_name, {"symbol_dynamic_map_name", "Dynamic"}},
// Defines
{ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}},
{ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}},
{ProjectIdentifier::define_max_level, {"define_max_level", "MAX_LEVEL"}},
{ProjectIdentifier::define_max_encounter_rate, {"define_max_encounter_rate", "MAX_ENCOUNTER_RATE"}},
{ProjectIdentifier::define_tiles_primary, {"define_tiles_primary", "NUM_TILES_IN_PRIMARY"}},
{ProjectIdentifier::define_tiles_total, {"define_tiles_total", "NUM_TILES_TOTAL"}},
{ProjectIdentifier::define_metatiles_primary, {"define_metatiles_primary", "NUM_METATILES_IN_PRIMARY"}},
{ProjectIdentifier::define_pals_primary, {"define_pals_primary", "NUM_PALS_IN_PRIMARY"}},
{ProjectIdentifier::define_pals_total, {"define_pals_total", "NUM_PALS_TOTAL"}},
{ProjectIdentifier::define_tiles_per_metatile, {"define_tiles_per_metatile", "NUM_TILES_PER_METATILE"}},
{ProjectIdentifier::define_map_size, {"define_map_size", "MAX_MAP_DATA_SIZE"}},
{ProjectIdentifier::define_map_offset_width, {"define_map_offset_width", "MAP_OFFSET_W"}},
{ProjectIdentifier::define_map_offset_height, {"define_map_offset_height", "MAP_OFFSET_H"}},
{ProjectIdentifier::define_mask_metatile, {"define_mask_metatile", "MAPGRID_METATILE_ID_MASK"}},
{ProjectIdentifier::define_mask_collision, {"define_mask_collision", "MAPGRID_COLLISION_MASK"}},
{ProjectIdentifier::define_mask_elevation, {"define_mask_elevation", "MAPGRID_ELEVATION_MASK"}},
{ProjectIdentifier::define_mask_behavior, {"define_mask_behavior", "METATILE_ATTR_BEHAVIOR_MASK"}},
{ProjectIdentifier::define_mask_layer, {"define_mask_layer", "METATILE_ATTR_LAYER_MASK"}},
{ProjectIdentifier::define_attribute_behavior, {"define_attribute_behavior", "METATILE_ATTRIBUTE_BEHAVIOR"}},
{ProjectIdentifier::define_attribute_layer, {"define_attribute_layer", "METATILE_ATTRIBUTE_LAYER_TYPE"}},
{ProjectIdentifier::define_attribute_terrain, {"define_attribute_terrain", "METATILE_ATTRIBUTE_TERRAIN"}},
{ProjectIdentifier::define_attribute_encounter, {"define_attribute_encounter", "METATILE_ATTRIBUTE_ENCOUNTER_TYPE"}},
{ProjectIdentifier::define_metatile_label_prefix, {"define_metatile_label_prefix", "METATILE_"}},
{ProjectIdentifier::define_heal_locations_prefix, {"define_heal_locations_prefix", "HEAL_LOCATION_"}},
{ProjectIdentifier::define_layout_prefix, {"define_layout_prefix", "LAYOUT_"}},
{ProjectIdentifier::define_map_prefix, {"define_map_prefix", "MAP_"}},
{ProjectIdentifier::define_map_dynamic, {"define_map_dynamic", "MAP_DYNAMIC"}},
{ProjectIdentifier::define_map_empty, {"define_map_empty", "MAP_UNDEFINED"}},
{ProjectIdentifier::define_map_section_prefix, {"define_map_section_prefix", "MAPSEC_"}},
{ProjectIdentifier::define_map_section_empty, {"define_map_section_empty", "NONE"}},
{ProjectIdentifier::define_species_prefix, {"define_species_prefix", "SPECIES_"}},
{ProjectIdentifier::define_species_empty, {"define_species_empty", "NONE"}},
// Regex
{ProjectIdentifier::regex_behaviors, {"regex_behaviors", "\\bMB_"}},
{ProjectIdentifier::regex_obj_event_gfx, {"regex_obj_event_gfx", "\\bOBJ_EVENT_GFX_"}},
{ProjectIdentifier::regex_items, {"regex_items", "\\bITEM_(?!(B_)?USE_)"}}, // Exclude ITEM_USE_ and ITEM_B_USE_ constants
{ProjectIdentifier::regex_flags, {"regex_flags", "\\bFLAG_"}},
{ProjectIdentifier::regex_vars, {"regex_vars", "\\bVAR_"}},
{ProjectIdentifier::regex_movement_types, {"regex_movement_types", "\\bMOVEMENT_TYPE_"}},
{ProjectIdentifier::regex_map_types, {"regex_map_types", "\\bMAP_TYPE_"}},
{ProjectIdentifier::regex_battle_scenes, {"regex_battle_scenes", "\\bMAP_BATTLE_SCENE_"}},
{ProjectIdentifier::regex_weather, {"regex_weather", "\\bWEATHER_"}},
{ProjectIdentifier::regex_coord_event_weather, {"regex_coord_event_weather", "\\bCOORD_EVENT_WEATHER_"}},
{ProjectIdentifier::regex_secret_bases, {"regex_secret_bases", "\\bSECRET_BASE_[\\w]+_[\\d]+"}},
{ProjectIdentifier::regex_sign_facing_directions, {"regex_sign_facing_directions", "\\bBG_EVENT_PLAYER_FACING_"}},
{ProjectIdentifier::regex_trainer_types, {"regex_trainer_types", "\\bTRAINER_TYPE_"}},
{ProjectIdentifier::regex_music, {"regex_music", "\\b(SE|MUS)_"}},
{ProjectIdentifier::regex_encounter_types, {"regex_encounter_types", "\\bTILE_ENCOUNTER_"}},
{ProjectIdentifier::regex_terrain_types, {"regex_terrain_types", "\\bTILE_TERRAIN_"}},
// Other
{ProjectIdentifier::pals_output_extension, {"pals_output_extension", ".gbapal"}},
{ProjectIdentifier::tiles_output_extension, {"tiles_output_extension", ".4bpp.lz"}},
};
const QMap<ProjectFilePath, QPair<QString, QString>> ProjectConfig::defaultPaths = {
{ProjectFilePath::data_map_folders, { "data_map_folders", "data/maps/"}},
{ProjectFilePath::data_scripts_folders, { "data_scripts_folders", "data/scripts/"}},
{ProjectFilePath::data_layouts_folders, { "data_layouts_folders", "data/layouts/"}},
{ProjectFilePath::data_primary_tilesets_folders, { "data_primary_tilesets_folders", "data/tilesets/primary/"}},
{ProjectFilePath::data_secondary_tilesets_folders, { "data_secondary_tilesets_folders", "data/tilesets/secondary/"}},
{ProjectFilePath::data_event_scripts, { "data_event_scripts", "data/event_scripts.s"}},
{ProjectFilePath::json_map_groups, { "json_map_groups", "data/maps/map_groups.json"}},
{ProjectFilePath::json_layouts, { "json_layouts", "data/layouts/layouts.json"}},
{ProjectFilePath::json_wild_encounters, { "json_wild_encounters", "src/data/wild_encounters.json"}},
{ProjectFilePath::json_heal_locations, { "json_heal_locations", "src/data/heal_locations.json"}},
{ProjectFilePath::json_region_map_entries, { "json_region_map_entries", "src/data/region_map/region_map_sections.json"}},
{ProjectFilePath::json_region_porymap_cfg, { "json_region_porymap_cfg", "src/data/region_map/porymap_config.json"}},
{ProjectFilePath::tilesets_headers, { "tilesets_headers", "src/data/tilesets/headers.h"}},
{ProjectFilePath::tilesets_graphics, { "tilesets_graphics", "src/data/tilesets/graphics.h"}},
{ProjectFilePath::tilesets_metatiles, { "tilesets_metatiles", "src/data/tilesets/metatiles.h"}},
{ProjectFilePath::tilesets_headers_asm, { "tilesets_headers_asm", "data/tilesets/headers.inc"}},
{ProjectFilePath::tilesets_graphics_asm, { "tilesets_graphics_asm", "data/tilesets/graphics.inc"}},
{ProjectFilePath::tilesets_metatiles_asm, { "tilesets_metatiles_asm", "data/tilesets/metatiles.inc"}},
{ProjectFilePath::data_obj_event_gfx_pointers, { "data_obj_event_gfx_pointers", "src/data/object_events/object_event_graphics_info_pointers.h"}},
{ProjectFilePath::data_obj_event_gfx_info, { "data_obj_event_gfx_info", "src/data/object_events/object_event_graphics_info.h"}},
{ProjectFilePath::data_obj_event_pic_tables, { "data_obj_event_pic_tables", "src/data/object_events/object_event_pic_tables.h"}},
{ProjectFilePath::data_obj_event_gfx, { "data_obj_event_gfx", "src/data/object_events/object_event_graphics.h"}},
{ProjectFilePath::data_pokemon_gfx, { "data_pokemon_gfx", "src/data/graphics/pokemon.h"}},
{ProjectFilePath::constants_global, { "constants_global", "include/constants/global.h"}},
{ProjectFilePath::constants_items, { "constants_items", "include/constants/items.h"}},
{ProjectFilePath::constants_flags, { "constants_flags", "include/constants/flags.h"}},
{ProjectFilePath::constants_vars, { "constants_vars", "include/constants/vars.h"}},
{ProjectFilePath::constants_weather, { "constants_weather", "include/constants/weather.h"}},
{ProjectFilePath::constants_songs, { "constants_songs", "include/constants/songs.h"}},
{ProjectFilePath::constants_pokemon, { "constants_pokemon", "include/constants/pokemon.h"}},
{ProjectFilePath::constants_map_types, { "constants_map_types", "include/constants/map_types.h"}},
{ProjectFilePath::constants_trainer_types, { "constants_trainer_types", "include/constants/trainer_types.h"}},
{ProjectFilePath::constants_secret_bases, { "constants_secret_bases", "include/constants/secret_bases.h"}},
{ProjectFilePath::constants_obj_event_movement, { "constants_obj_event_movement", "include/constants/event_object_movement.h"}},
{ProjectFilePath::constants_obj_events, { "constants_obj_events", "include/constants/event_objects.h"}},
{ProjectFilePath::constants_event_bg, { "constants_event_bg", "include/constants/event_bg.h"}},
{ProjectFilePath::constants_metatile_labels, { "constants_metatile_labels", "include/constants/metatile_labels.h"}},
{ProjectFilePath::constants_metatile_behaviors, { "constants_metatile_behaviors", "include/constants/metatile_behaviors.h"}},
{ProjectFilePath::constants_species, { "constants_species", "include/constants/species.h"}},
{ProjectFilePath::constants_fieldmap, { "constants_fieldmap", "include/fieldmap.h"}},
{ProjectFilePath::global_fieldmap, { "global_fieldmap", "include/global.fieldmap.h"}},
{ProjectFilePath::fieldmap, { "fieldmap", "src/fieldmap.c"}},
{ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}},
{ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}},
{ProjectFilePath::wild_encounter, { "wild_encounter", "src/wild_encounter.c"}},
{ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}},
};
ProjectIdentifier ProjectConfig::reverseDefaultIdentifier(const QString& str) {
for (auto i = defaultIdentifiers.cbegin(), end = defaultIdentifiers.cend(); i != end; i++) {
if (i.value().first == str) return i.key();
}
return static_cast<ProjectIdentifier>(-1);
}
ProjectFilePath ProjectConfig::reverseDefaultPaths(const QString& str) {
for (auto it = defaultPaths.constKeyValueBegin(); it != defaultPaths.constKeyValueEnd(); ++it) {
if ((*it).second.first == str) return (*it).first;
}
return static_cast<ProjectFilePath>(-1);
}
void ProjectConfig::setVersionSpecificDefaults(BaseGame::Version version) {
this->baseGameVersion = version;
if (this->baseGameVersion == BaseGame::Version::none) return;
this->metatileAttributesSize = Metatile::getDefaultAttributesSize(this->baseGameVersion);
this->metatileBehaviorMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::Behavior);
this->metatileTerrainTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::TerrainType);
this->metatileEncounterTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::EncounterType);
this->metatileLayerTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::LayerType);
if (this->baseGameVersion == BaseGame::Version::pokefirered) {
this->useCustomBorderSize = true;
this->eventWeatherTriggerEnabled = false;
this->eventSecretBaseEnabled = false;
this->hiddenItemQuantityEnabled = true;
this->hiddenItemRequiresItemfinderEnabled = true;
this->healLocationRespawnDataEnabled = true;
this->eventCloneObjectEnabled = true;
this->floorNumberEnabled = true;
this->createMapTextFileEnabled = true;
this->newMapBorderMetatileIds = {0x14, 0x15, 0x1C, 0x1D};
this->defaultSecondaryTileset = QStringLiteral("gTileset_PalletTown");
this->warpBehaviors = {
0x60, // MB_CAVE_DOOR
0x61, // MB_LADDER
0x62, // MB_EAST_ARROW_WARP
0x63, // MB_WEST_ARROW_WARP
0x64, // MB_NORTH_ARROW_WARP
0x65, // MB_SOUTH_ARROW_WARP
0x66, // MB_FALL_WARP
0x67, // MB_REGULAR_WARP
0x68, // MB_LAVARIDGE_1F_WARP
0x69, // MB_WARP_DOOR
0x6A, // MB_UP_ESCALATOR
0x6B, // MB_DOWN_ESCALATOR
0x6C, // MB_UP_RIGHT_STAIR_WARP
0x6D, // MB_UP_LEFT_STAIR_WARP
0x6E, // MB_DOWN_RIGHT_STAIR_WARP
0x6F, // MB_DOWN_LEFT_STAIR_WARP
0x71, // MB_UNION_ROOM_WARP
};
} else { // pokeemerald / pokeruby
this->useCustomBorderSize = false;
this->eventWeatherTriggerEnabled = true;
this->eventSecretBaseEnabled = true;
this->hiddenItemQuantityEnabled = false;
this->hiddenItemRequiresItemfinderEnabled = false;
this->healLocationRespawnDataEnabled = false;
this->eventCloneObjectEnabled = false;
this->floorNumberEnabled = false;
this->createMapTextFileEnabled = false;
this->newMapBorderMetatileIds = {0x1D4, 0x1D5, 0x1DC, 0x1DD};
this->defaultSecondaryTileset = QStringLiteral("gTileset_Petalburg");
this->warpBehaviors = {
0x0E, // MB_MOSSDEEP_GYM_WARP
0x0F, // MB_MT_PYRE_HOLE
0x1B, // MB_STAIRS_OUTSIDE_ABANDONED_SHIP
0x1C, // MB_SHOAL_CAVE_ENTRANCE
0x29, // MB_LAVARIDGE_GYM_B1F_WARP
0x60, // MB_NON_ANIMATED_DOOR
0x61, // MB_LADDER
0x62, // MB_EAST_ARROW_WARP
0x63, // MB_WEST_ARROW_WARP
0x64, // MB_NORTH_ARROW_WARP
0x65, // MB_SOUTH_ARROW_WARP
0x67, // MB_AQUA_HIDEOUT_WARP
0x68, // MB_LAVARIDGE_GYM_1F_WARP
0x69, // MB_ANIMATED_DOOR
0x6A, // MB_UP_ESCALATOR
0x6B, // MB_DOWN_ESCALATOR
0x6C, // MB_WATER_DOOR
0x6D, // MB_WATER_SOUTH_ARROW_WARP
0x6E, // MB_DEEP_SOUTH_WARP
0x70, // MB_UNION_ROOM_WARP
0x8D, // MB_PETALBURG_GYM_DOOR
0x91, // MB_SECRET_BASE_SPOT_RED_CAVE_OPEN
0x93, // MB_SECRET_BASE_SPOT_BROWN_CAVE_OPEN
0x95, // MB_SECRET_BASE_SPOT_YELLOW_CAVE_OPEN
0x97, // MB_SECRET_BASE_SPOT_TREE_LEFT_OPEN
0x99, // MB_SECRET_BASE_SPOT_SHRUB_OPEN
0x9B, // MB_SECRET_BASE_SPOT_BLUE_CAVE_OPEN
0x9D, // MB_SECRET_BASE_SPOT_TREE_RIGHT_OPEN
};
}
this->mapAllowFlagsEnabled = (this->baseGameVersion != BaseGame::Version::pokeruby);
}
bool ProjectConfig::save() {
// Clean out empty paths
Util::removeEmptyStrings(&this->globalConstants);
Util::removeEmptyStrings(&this->eventIconPaths);
Util::removeEmptyStrings(&this->pokemonIconPaths);
return KeyValueConfigBase::save();
}
void ProjectConfig::loadFromJson(const QJsonObject& obj_) {
QJsonObject obj(obj_);
// Parse the base game version before anything else so we can initialize the defaults.
auto versionKey = QStringLiteral("base_game_version");
const QJsonValue version = obj.take(versionKey);
if (!version.isUndefined() && parseJsonKeyValue(versionKey, version)) {
setVersionSpecificDefaults(this->baseGameVersion);
}
KeyValueConfigBase::loadFromJson(obj);
// Enforce this setting for projectConfig's custom scripts
for (auto& settings : this->customScripts) settings.userOnly = false;
}
QJsonObject ProjectConfig::getDefaultJson() const {
ProjectConfig defaultConfig(this->baseGameVersion);
// The defaults are version-specific, make sure we always output non-empty versions.
defaultConfig.baseGameVersion = BaseGame::Version::none;
return defaultConfig.toJson();
}
// TODO: Replace with a new prompt that allows choosing either the defaults for each version, or customizing settings.
void ProjectConfig::initializeFromEmpty() {
const QString dirName = QDir(projectDir()).dirName();
BaseGame::Version version = BaseGame::stringToVersion(dirName);
if (version != BaseGame::Version::none) {
this->baseGameVersion = version;
logInfo(QString("Auto-detected base_game_version as '%1'").arg(BaseGame::versionToString(version)));
} else {
QDialog dialog(nullptr, Qt::WindowTitleHint);
dialog.setWindowTitle("Project Configuration");
dialog.setWindowModality(Qt::NonModal);
QFormLayout form(&dialog);
auto comboBox = new QComboBox();
comboBox->addItem(BaseGame::versionToString(BaseGame::Version::pokeruby), BaseGame::Version::pokeruby);
comboBox->addItem(BaseGame::versionToString(BaseGame::Version::pokefirered), BaseGame::Version::pokefirered);
comboBox->addItem(BaseGame::versionToString(BaseGame::Version::pokeemerald), BaseGame::Version::pokeemerald);
form.addRow(new QLabel("Game Version"), comboBox);
QDialogButtonBox buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, &dialog);
QObject::connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
form.addRow(&buttonBox);
if (dialog.exec() == QDialog::Accepted) {
this->baseGameVersion = static_cast<BaseGame::Version>(comboBox->currentData().toInt());
} else {
logWarn(QString("No base_game_version selected, using default '%1'").arg(BaseGame::versionToString(this->baseGameVersion)));
}
}
setVersionSpecificDefaults(this->baseGameVersion);
}
void ProjectConfig::setFilePath(ProjectFilePath pathId, const QString& path) {
if (!defaultPaths.contains(pathId)) return;
if (path.isEmpty()) {
this->filePaths.remove(pathId);
} else {
this->filePaths[pathId] = path;
}
}
void ProjectConfig::setFilePath(const QString& pathId, const QString& path) {
this->setFilePath(reverseDefaultPaths(pathId), path);
}
QString ProjectConfig::getCustomFilePath(ProjectFilePath pathId) {
return QDir::cleanPath(this->filePaths.value(pathId));
}
QString ProjectConfig::getCustomFilePath(const QString& pathId) {
return this->getCustomFilePath(reverseDefaultPaths(pathId));
}
QString ProjectConfig::getFilePath(ProjectFilePath pathId) {
QString customPath = this->getCustomFilePath(pathId);
if (!customPath.isEmpty()) {
// A custom filepath has been specified. If the file/folder exists, use that.
const QString baseDir = this->projectDir() + "/";
if (customPath.startsWith(baseDir)) {
customPath.remove(0, baseDir.length());
}
if (QFileInfo::exists(QDir::cleanPath(baseDir + customPath))) {
return customPath;
} else {
logError(QString("Custom project filepath '%1' not found. Using default.").arg(customPath));
}
}
return defaultPaths.contains(pathId) ? defaultPaths[pathId].second : QString();
}
void ProjectConfig::setIdentifier(ProjectIdentifier id, const QString& text) {
if (!defaultIdentifiers.contains(id))
return;
if (text.isEmpty()) {
this->identifiers.remove(id);
} else {
const QString idName = defaultIdentifiers.value(id).first;
if (idName.startsWith("define_") || idName.startsWith("symbol_")) {
// Validate the input for the identifier, depending on the type.
IdentifierValidator validator;
if (!validator.isValid(text)) {
logError(QString("The name '%1' for project identifier '%2' is invalid. It must only contain word characters, and cannot start with a digit.").arg(text).arg(idName));
return;
}
}
this->identifiers[id] = text;
}
}
void ProjectConfig::setIdentifier(const QString& id, const QString& text) {
this->setIdentifier(reverseDefaultIdentifier(id), text);
}
QString ProjectConfig::getCustomIdentifier(ProjectIdentifier id) {
return this->identifiers.value(id);
}
QString ProjectConfig::getCustomIdentifier(const QString& id) {
return this->getCustomIdentifier(reverseDefaultIdentifier(id));
}
QString ProjectConfig::getIdentifier(ProjectIdentifier id) {
const QString customText = this->getCustomIdentifier(id);
if (!customText.isEmpty())
return customText;
return defaultIdentifiers.contains(id) ? defaultIdentifiers[id].second : QString();
}

View File

@ -0,0 +1,110 @@
#include "shortcutsconfig.h"
#include "shortcut.h"
#include <QAction>
#include <QRegularExpression>
ShortcutsConfig shortcutsConfig;
void ShortcutsConfig::loadFromJson(const QJsonObject& obj) {
this->user_shortcuts = Converter<QMultiMap<QString, QKeySequence>>::fromJson(obj);
}
QJsonObject ShortcutsConfig::toJson() {
return Converter<QMultiMap<QString, QKeySequence>>::toJson(this->user_shortcuts);
}
QJsonObject ShortcutsConfig::getDefaultJson() const {
return Converter<QMultiMap<QString, QKeySequence>>::toJson(this->default_shortcuts);
}
void ShortcutsConfig::setDefaultShortcuts(const QObjectList& objects) {
storeShortcutsFromList(StoreType::Default, objects);
}
QList<QKeySequence> ShortcutsConfig::defaultShortcuts(const QObject *object) const {
return default_shortcuts.values(cfgKey(object));
}
void ShortcutsConfig::setUserShortcuts(const QObjectList& objects) {
storeShortcutsFromList(StoreType::User, objects);
}
void ShortcutsConfig::setUserShortcuts(const QMultiMap<const QObject *, QKeySequence>& objects_keySequences) {
for (auto *object : objects_keySequences.uniqueKeys())
if (!object->objectName().isEmpty() && !object->objectName().startsWith("_q_"))
storeShortcuts(StoreType::User, cfgKey(object), objects_keySequences.values(object));
}
QList<QKeySequence> ShortcutsConfig::userShortcuts(const QObject *object) const {
return user_shortcuts.values(cfgKey(object));
}
void ShortcutsConfig::storeShortcutsFromList(StoreType storeType, const QObjectList& objects) {
for (const auto *object : objects)
if (!object->objectName().isEmpty() && !object->objectName().startsWith("_q_"))
storeShortcuts(storeType, cfgKey(object), currentShortcuts(object));
}
void ShortcutsConfig::storeShortcuts(
StoreType storeType,
const QString& cfgKey,
const QList<QKeySequence>& keySequences)
{
bool storeUser = (storeType == User) || !user_shortcuts.contains(cfgKey);
if (storeType == Default)
default_shortcuts.remove(cfgKey);
if (storeUser)
user_shortcuts.remove(cfgKey);
if (keySequences.isEmpty()) {
if (storeType == Default)
default_shortcuts.insert(cfgKey, QKeySequence());
if (storeUser)
user_shortcuts.insert(cfgKey, QKeySequence());
} else {
for (auto keySequence : keySequences) {
if (storeType == Default)
default_shortcuts.insert(cfgKey, keySequence);
if (storeUser)
user_shortcuts.insert(cfgKey, keySequence);
}
}
}
/* Creates a config key from the object's name prepended with the parent
* window's object name, and converts camelCase to snake_case. */
QString ShortcutsConfig::cfgKey(const QObject *object) const {
auto cfg_key = QString();
auto *parentWidget = static_cast<QWidget *>(object->parent());
if (parentWidget)
cfg_key = parentWidget->window()->objectName() + '_';
cfg_key += object->objectName();
static const QRegularExpression re("[A-Z]");
int i = cfg_key.indexOf(re, 1);
while (i != -1) {
if (cfg_key.at(i - 1) != '_')
cfg_key.insert(i++, '_');
i = cfg_key.indexOf(re, i + 1);
}
return cfg_key.toLower();
}
QList<QKeySequence> ShortcutsConfig::currentShortcuts(const QObject *object) const {
if (object->inherits("QAction")) {
const auto *action = qobject_cast<const QAction *>(object);
return action->shortcuts();
} else if (object->inherits("Shortcut")) {
const auto *shortcut = qobject_cast<const Shortcut *>(object);
return shortcut->keys();
} else if (object->inherits("QShortcut")) {
const auto *qshortcut = qobject_cast<const QShortcut *>(object);
return { qshortcut->key() };
} else if (object->property("shortcut").isValid()) {
return { object->property("shortcut").value<QKeySequence>() };
} else {
return { };
}
}

16
src/config/userconfig.cpp Normal file
View File

@ -0,0 +1,16 @@
#include "userconfig.h"
// TODO: This should eventually be contained by each individual Project instance.
UserConfig userConfig;
void UserConfig::loadFromJson(const QJsonObject& obj) {
KeyValueConfigBase::loadFromJson(obj);
// Enforce this setting for userConfig's custom scripts
for (auto& settings : this->customScripts) settings.userOnly = true;
}
QJsonObject UserConfig::getDefaultJson() const {
UserConfig defaultConfig;
return defaultConfig.toJson();
}

View File

@ -2,6 +2,7 @@
#include "log.h"
#include "project.h"
#include "maplayout.h"
#include "config.h"
Layout *AdvanceMapParser::parseLayout(const QString &filepath, bool *error, const Project *project)
{
@ -118,18 +119,18 @@ QList<Metatile*> AdvanceMapParser::parseMetatiles(const QString &filepath, bool
int projIdOffset = in.length() - 4;
int metatileSize = 16;
BaseGameVersion version;
BaseGame::Version version;
if (in.at(projIdOffset + 0) == 'R'
&& in.at(projIdOffset + 1) == 'S'
&& in.at(projIdOffset + 2) == 'E'
&& in.at(projIdOffset + 3) == ' ') {
// ruby and emerald are handled equally here.
version = BaseGameVersion::pokeemerald;
version = BaseGame::Version::pokeemerald;
} else if (in.at(projIdOffset + 0) == 'F'
&& in.at(projIdOffset + 1) == 'R'
&& in.at(projIdOffset + 2) == 'L'
&& in.at(projIdOffset + 3) == 'G') {
version = BaseGameVersion::pokefirered;
version = BaseGame::Version::pokefirered;
} else {
*error = true;
logError(QString("Detected unsupported game type from .bvd file. Last 4 bytes of file must be 'RSE ' or 'FRLG'."));

62
src/core/basegame.cpp Normal file
View File

@ -0,0 +1,62 @@
#include "basegame.h"
#include <QList>
#include <QMap>
// If a string exclusively contains one version name we assume its identity,
// otherwise we leave it unknown and we'll need the user to tell us the version.
BaseGame::Version BaseGame::stringToVersion(const QString &input_) {
static const QMap<Version, QStringList> versionDetectNames = {
{Version::pokeruby, {"ruby", "sapphire"}},
{Version::pokefirered, {"firered", "leafgreen"}},
{Version::pokeemerald, {"emerald"}},
};
const QString input(input_.toLower());
Version version = Version::none;
for (auto it = versionDetectNames.begin(); it != versionDetectNames.end(); it++) {
// Compare the given string to all the possible names for this game version
for (const auto &name : it.value()) {
if (input.contains(name)) {
if (version != Version::none) {
// The given string matches multiple versions, so we can't be sure which it is.
return Version::none;
}
version = it.key();
break;
}
}
}
// We finished checking the names for each version; the name either matched 1 version or none.
return version;
}
QString BaseGame::versionToString(BaseGame::Version version) {
static const QMap<Version, QString> map = {
{Version::pokeruby, "pokeruby"},
{Version::pokefirered, "pokefirered"},
{Version::pokeemerald, "pokeemerald"},
};
return map.value(version);
}
QString BaseGame::getPlayerIconPath(BaseGame::Version version, int character) {
if (version == Version::pokeemerald) {
static const QStringList paths = { QStringLiteral(":/icons/player/brendan_em.ico"),
QStringLiteral(":/icons/player/may_em.ico"), };
return paths.value(character);
} else if (version == Version::pokefirered) {
static const QStringList paths = { QStringLiteral(":/icons/player/red.ico"),
QStringLiteral(":/icons/player/green.ico"), };
return paths.value(character);
} else if (version == Version::pokeruby) {
static const QStringList paths = { QStringLiteral(":/icons/player/brendan_rs.ico"),
QStringLiteral(":/icons/player/may_rs.ico"), };
return paths.value(character);
}
return QString();
}
QIcon BaseGame::getPlayerIcon(BaseGame::Version baseGameVersion, int character) {
return QIcon(getPlayerIconPath(baseGameVersion, character));
}

View File

@ -2,12 +2,10 @@
#include "bitpacker.h"
#include "config.h"
// Upper limit for metatile ID, collision, and elevation masks. Used externally.
const uint16_t Block::maxValue = 0xFFFF;
static BitPacker bitsMetatileId = BitPacker(0x3FF);
static BitPacker bitsCollision = BitPacker(0xC00);
static BitPacker bitsElevation = BitPacker(0xF000);
static BitPacker bitsMetatileId = BitPacker(Block::DefaultMetatileIdMask);
static BitPacker bitsCollision = BitPacker(Block::DefaultCollisionMask);
static BitPacker bitsElevation = BitPacker(Block::DefaultElevationMask);
Block::Block() :
m_metatileId(0),

View File

@ -45,7 +45,7 @@ QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) {
static int numMetatileIdChars = 4;
QString Metatile::getMetatileIdString(uint16_t metatileId) {
return /*porymapConfig.displayIdsHexadecimal*/ConfigDisplayIdsHexadecimal
return porymapConfig.displayIdsHexadecimal
? Util::toHexString(metatileId, numMetatileIdChars)
: QString::number(metatileId);
};
@ -62,6 +62,10 @@ QString Metatile::getLayerName(int layerNum) {
return layerTitles.value(layerNum);
}
int Metatile::numLayers() {
return projectConfig.tripleLayerMetatilesEnabled ? 3 : 2;
}
// Read and pack together this metatile's attributes.
uint32_t Metatile::getAttributes() const {
uint32_t data = 0;
@ -81,8 +85,8 @@ void Metatile::setAttributes(uint32_t data) {
}
// Unpack and insert metatile attributes from the given data using a vanilla layout. For AdvanceMap import
void Metatile::setAttributes(uint32_t data, BaseGameVersion version) {
const auto vanillaPackers = (version == BaseGameVersion::pokefirered) ? attributePackersFRLG : attributePackersRSE;
void Metatile::setAttributes(uint32_t data, BaseGame::Version version) {
const auto vanillaPackers = (version == BaseGame::Version::pokefirered) ? attributePackersFRLG : attributePackersRSE;
for (auto i = vanillaPackers.cbegin(), end = vanillaPackers.cend(); i != end; i++){
const auto packer = i.value();
this->setAttribute(i.key(), packer.unpack(data));
@ -95,12 +99,12 @@ void Metatile::setAttribute(Metatile::Attr attr, uint32_t value) {
this->attributes.insert(attr, packer.clamp(value));
}
int Metatile::getDefaultAttributesSize(BaseGameVersion version) {
return (version == BaseGameVersion::pokefirered) ? 4 : 2;
int Metatile::getDefaultAttributesSize(BaseGame::Version version) {
return (version == BaseGame::Version::pokefirered) ? 4 : 2;
}
uint32_t Metatile::getDefaultAttributesMask(BaseGameVersion version, Metatile::Attr attr) {
const auto vanillaPackers = (version == BaseGameVersion::pokefirered) ? attributePackersFRLG : attributePackersRSE;
uint32_t Metatile::getDefaultAttributesMask(BaseGame::Version version, Metatile::Attr attr) {
const auto vanillaPackers = (version == BaseGame::Version::pokefirered) ? attributePackersFRLG : attributePackersRSE;
return vanillaPackers.value(attr).mask();
}

View File

@ -2,11 +2,6 @@
#include "project.h"
#include "bitpacker.h"
bool ConfigDisplayIdsHexadecimal = true;
// Upper limit for raw value (i.e., uint16_t max).
const uint16_t Tile::maxValue = 0xFFFF;
// At the moment these are fixed, and not exposed to the user.
// We're only using them for convenience when converting between raw values.
// The actual job of clamping Tile's members to correct values is handled by the widths in the bit field.
@ -76,7 +71,7 @@ QString Tile::toString() const {
}
QString Tile::getTileIdString(uint16_t tileId) {
return /*porymapConfig.displayIdsHexadecimal*/ConfigDisplayIdsHexadecimal
return porymapConfig.displayIdsHexadecimal
? Util::toHexString(tileId, 3)
: QString::number(tileId);
}

View File

@ -89,7 +89,7 @@ void Tileset::resizeMetatiles(int newNumMetatiles) {
while (m_metatiles.length() > newNumMetatiles) {
delete m_metatiles.takeLast();
}
const int numTiles = projectConfig.getNumTilesInMetatile();
const int numTiles = Metatile::maxTiles();
while (m_metatiles.length() < newNumMetatiles) {
m_metatiles.append(new Metatile(numTiles));
}
@ -322,7 +322,7 @@ bool Tileset::appendToHeaders(const QString &filepath, const QString &friendlyNa
dataString.append(QString("\t.4byte gTilesetTiles_%1\n").arg(friendlyName));
dataString.append(QString("\t.4byte gTilesetPalettes_%1\n").arg(friendlyName));
dataString.append(QString("\t.4byte gMetatiles_%1\n").arg(friendlyName));
if (projectConfig.baseGameVersion == BaseGameVersion::pokefirered) {
if (projectConfig.baseGameVersion == BaseGame::Version::pokefirered) {
dataString.append("\t.4byte NULL @ animation callback\n");
dataString.append(QString("\t.4byte gMetatileAttributes_%1\n").arg(friendlyName));
} else {
@ -439,7 +439,7 @@ QHash<int, QString> Tileset::getHeaderMemberMap(bool usingAsm)
int paddingOffset = usingAsm ? 1 : 0;
// The position of metatileAttributes changes between games
bool isPokefirered = (projectConfig.baseGameVersion == BaseGameVersion::pokefirered);
bool isPokefirered = (projectConfig.baseGameVersion == BaseGame::Version::pokefirered);
int metatileAttrPosition = (isPokefirered ? 6 : 5) + paddingOffset;
auto map = QHash<int, QString>();
@ -461,7 +461,7 @@ bool Tileset::loadMetatiles() {
}
QByteArray data = file.readAll();
int tilesPerMetatile = projectConfig.getNumTilesInMetatile();
int tilesPerMetatile = Metatile::maxTiles();
int bytesPerMetatile = Tile::sizeInBytes() * tilesPerMetatile;
int numMetatiles = data.length() / bytesPerMetatile;
if (numMetatiles > maxMetatiles()) {
@ -493,7 +493,7 @@ bool Tileset::saveMetatiles() {
}
QByteArray data;
int numTiles = projectConfig.getNumTilesInMetatile();
int numTiles = Metatile::maxTiles();
for (const auto &metatile : m_metatiles) {
for (int i = 0; i < numTiles; i++) {
uint16_t tile = metatile->tiles.value(i).rawValue();

View File

@ -129,26 +129,6 @@ void Util::show(QWidget *w) {
}
}
// Safe conversion from an int representing a QColorSpace::NamedColorSpace to a QColorSpace.
// This lets us use 0 to mean "no color space".
QColorSpace Util::toColorSpace(int colorSpaceInt) {
QColorSpace colorSpace;
int min = static_cast<int>(QColorSpace::SRgb);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0))
// Qt 6.8.0 introduced additional color spaces
int max = static_cast<int>(QColorSpace::Bt2100Hlg);
#else
int max = static_cast<int>(QColorSpace::ProPhotoRgb);
#endif
if (colorSpaceInt >= min && colorSpaceInt <= max) {
return QColorSpace(static_cast<QColorSpace::NamedColorSpace>(colorSpaceInt));
} else {
return QColorSpace();
}
}
// Creates a directory named 'dirPath', including any non-existent parent directories. Returns an error message, if any.
// If 'dirPath' already exists it's considered an error unless the directory has no files.
QString Util::mkpath(const QString& dirPath) {

3
src/core/version.cpp Normal file
View File

@ -0,0 +1,3 @@
#include "version.h"
const QVersionNumber porymapVersion = QVersionNumber::fromString(PORYMAP_VERSION);

View File

@ -29,6 +29,7 @@
#include "newmapgroupdialog.h"
#include "newlocationdialog.h"
#include "loadingscreen.h"
#include "version.h"
#include <QClipboard>
#include <QDirIterator>
@ -81,6 +82,7 @@ MainWindow::MainWindow(QWidget *parent) :
void MainWindow::initialize() {
this->initWindow();
this->installEventFilter(new GeometrySaver(this));
if (porymapConfig.reopenOnLaunch && !porymapConfig.projectManuallyClosed && this->openProject(porymapConfig.getRecentProject(), true)) {
on_toolButton_Paint_clicked();
}
@ -92,7 +94,7 @@ void MainWindow::initialize() {
if (porymapConfig.checkForUpdates)
this->checkForUpdates(false);
this->restoreWindowState();
this->resizeWithinScreen();
this->show();
}
@ -109,13 +111,6 @@ MainWindow::~MainWindow()
}
void MainWindow::saveGlobalConfigs() {
porymapConfig.setMainGeometry(
this->saveGeometry(),
this->saveState(),
this->ui->splitter_map->saveState(),
this->ui->splitter_main->saveState(),
this->ui->splitter_Metatiles->saveState()
);
porymapConfig.save();
shortcutsConfig.save();
}
@ -185,6 +180,7 @@ void MainWindow::setWindowDisabled(bool disabled) {
}
void MainWindow::initWindow() {
porymapConfig = PorymapConfig();
porymapConfig.load();
this->initLogStatusBar();
this->initCustomUI();
@ -227,6 +223,7 @@ void MainWindow::initWindow() {
void MainWindow::initShortcuts() {
initExtraShortcuts();
shortcutsConfig = ShortcutsConfig();
shortcutsConfig.load();
shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
applyUserShortcuts();
@ -303,9 +300,8 @@ void MainWindow::applyUserShortcuts() {
void MainWindow::initLogStatusBar() {
removeLogStatusBar(this->statusBar());
auto logTypes = QSet<LogType>(porymapConfig.statusBarLogTypes.begin(), porymapConfig.statusBarLogTypes.end());
if (!logTypes.isEmpty()) {
addLogStatusBar(this->statusBar(), logTypes);
if (!porymapConfig.statusBarLogTypes.isEmpty()) {
addLogStatusBar(this->statusBar(), porymapConfig.statusBarLogTypes.toQSet());
}
}
@ -320,7 +316,7 @@ void MainWindow::initCustomUI() {
static const QMap<int, QIcon> mainTabIcons = {
{MainTab::Map, QIcon(QStringLiteral(":/icons/minimap.ico"))},
{MainTab::Events, ProjectConfig::getPlayerIcon(BaseGameVersion::pokefirered, 0)}, // Arbitrary default
{MainTab::Events, BaseGame::getPlayerIcon(BaseGame::Version::pokefirered, 0)}, // Arbitrary default
{MainTab::Header, QIcon(QStringLiteral(":/icons/application_form_edit.ico"))},
{MainTab::Connections, QIcon(QStringLiteral(":/icons/connections.ico"))},
{MainTab::WildPokemon, QIcon(QStringLiteral(":/icons/tall_grass.ico"))},
@ -591,9 +587,9 @@ void MainWindow::initMapList() {
// Initialize settings from config
ui->mapListToolBar_Groups->setEditsAllowed(porymapConfig.mapListEditGroupsEnabled);
for (auto i = porymapConfig.mapListHideEmptyEnabled.constBegin(); i != porymapConfig.mapListHideEmptyEnabled.constEnd(); i++) {
auto toolbar = getMapListToolBar(i.key());
if (toolbar) toolbar->setEmptyFoldersVisible(!i.value());
for (const auto &tab : porymapConfig.mapListTabsHidingEmptyFolders) {
auto toolbar = getMapListToolBar(tab);
if (toolbar) toolbar->setEmptyFoldersVisible(false);
}
// Update config if map list settings change
@ -601,13 +597,16 @@ void MainWindow::initMapList() {
porymapConfig.mapListEditGroupsEnabled = allowed;
});
connect(ui->mapListToolBar_Groups, &MapListToolBar::emptyFoldersVisibleChanged, [](bool visible) {
porymapConfig.mapListHideEmptyEnabled[MapListTab::Groups] = !visible;
if (visible) porymapConfig.mapListTabsHidingEmptyFolders.erase(MapListTab::Groups);
else porymapConfig.mapListTabsHidingEmptyFolders.insert(MapListTab::Groups);
});
connect(ui->mapListToolBar_Locations, &MapListToolBar::emptyFoldersVisibleChanged, [](bool visible) {
porymapConfig.mapListHideEmptyEnabled[MapListTab::Locations] = !visible;
if (visible) porymapConfig.mapListTabsHidingEmptyFolders.erase(MapListTab::Locations);
else porymapConfig.mapListTabsHidingEmptyFolders.insert(MapListTab::Locations);
});
connect(ui->mapListToolBar_Layouts, &MapListToolBar::emptyFoldersVisibleChanged, [](bool visible) {
porymapConfig.mapListHideEmptyEnabled[MapListTab::Layouts] = !visible;
if (visible) porymapConfig.mapListTabsHidingEmptyFolders.erase(MapListTab::Layouts);
else porymapConfig.mapListTabsHidingEmptyFolders.insert(MapListTab::Layouts);
});
// When map list search filter is cleared we want the current map/layout in the editor to be visible in the list.
@ -729,19 +728,8 @@ void MainWindow::loadUserSettings() {
refreshRecentProjectsMenu();
}
void MainWindow::restoreWindowState() {
QMap<QString, QByteArray> geometry = porymapConfig.getMainGeometry();
const QByteArray mainWindowGeometry = geometry.value("main_window_geometry");
if (!mainWindowGeometry.isEmpty()) {
logInfo("Restoring main window geometry from previous session.");
restoreGeometry(mainWindowGeometry);
restoreState(geometry.value("main_window_state"));
ui->splitter_map->restoreState(geometry.value("map_splitter_state"));
ui->splitter_main->restoreState(geometry.value("main_splitter_state"));
ui->splitter_Metatiles->restoreState(geometry.value("metatiles_splitter_state"));
}
// Resize the window if it exceeds the available screen size.
// Resize the window if it exceeds the available screen size.
void MainWindow::resizeWithinScreen() {
auto screen = windowHandle() ? windowHandle()->screen() : QGuiApplication::primaryScreen();
if (!screen) return;
const QRect screenGeometry = screen->availableGeometry();
@ -800,14 +788,14 @@ bool MainWindow::openProject(QString dir, bool initial) {
logInfo("Aborted project open.");
return false;
}
const QString openMessage = QString("Opening %1").arg(projectString);
logInfo(openMessage);
logInfo(QString("Opening %1").arg(projectString));
if (porymapConfig.showProjectLoadingScreen) porysplash->start();
porysplash->showLoadingMessage("config");
if (!projectConfig.load(dir) || !userConfig.load(dir)) {
projectConfig = ProjectConfig(dir);
userConfig = UserConfig(dir);
if (!projectConfig.load() || !userConfig.load()) {
showProjectOpenFailure();
porysplash->stop();
return false;
@ -845,9 +833,6 @@ bool MainWindow::openProject(QString dir, bool initial) {
porysplash->stop();
return false;
}
// Only create the config files once the project has opened successfully in case the user selected an invalid directory
this->editor->project->saveConfig();
updateWindowTitle();
@ -899,24 +884,25 @@ bool MainWindow::checkProjectSanity(Project *project) {
bool MainWindow::checkProjectVersion(Project *project) {
QString error;
int projectVersion = project->getSupportedMajorVersion(&error);
if (projectVersion < 0) {
// Failed to identify a supported major version.
QVersionNumber minimumVersion = project->getMinimumVersion(&error);
if (!error.isEmpty()) {
// Failed to identify a supported version.
// We can't draw any conclusions from this, so we don't consider the project to be invalid.
QString msg = QStringLiteral("Failed to check project version");
logWarn(error.isEmpty() ? msg : QString("%1: '%2'").arg(msg).arg(error));
logWarn(QString("Failed to check project version: '%1'").arg(error));
} else {
QString msg = QStringLiteral("Successfully checked project version. ");
logInfo(msg + ((projectVersion != 0) ? QString("Supports at least Porymap v%1").arg(projectVersion)
: QStringLiteral("Too old for any Porymap version")));
if (projectVersion < porymapVersion.majorVersion() && projectConfig.forcedMajorVersion < porymapVersion.majorVersion()) {
// We were unable to find the necessary changes for Porymap's current major version.
if (minimumVersion.isNull()) {
logInfo(QStringLiteral("Successfully checked project version. Too old for any Porymap version"));
} else {
logInfo(QString("Successfully checked project version. Supports at least Porymap v%1").arg(minimumVersion.toString()));
}
if (minimumVersion > porymapVersion || minimumVersion.majorVersion() != porymapVersion.majorVersion()) {
// Porymap is incompatible with the project if its version is below the specified minimum version,
// or if Porymap is so new that it exceeds the major version of the specified minimum version.
// Unless they have explicitly suppressed this message, warn the user that this might mean their project is missing breaking changes.
// Note: Do not report 'projectVersion' to the user in this message. We've already logged it for troubleshooting.
// Note: Do not report 'minimumVersion' to the user in this message. We've already logged it for troubleshooting.
// It is very plausible that the user may have reproduced the required changes in an
// unknown commit, rather than merging the required changes directly from the base repo.
// In this case the 'projectVersion' may actually be too old to use for their repo.
// In this case the 'minimumVersion' may actually be too old to use for their repo.
ErrorMessage msgBox(QStringLiteral("Your project may be incompatible!"), porysplash);
msgBox.setTextFormat(Qt::RichText);
msgBox.setInformativeText(QString("Make sure '%1' has all the required changes for Porymap version %2.<br>"
@ -929,7 +915,7 @@ bool MainWindow::checkProjectVersion(Project *project) {
return false;
}
// User opted to try with this version anyway. Don't warn them about this version again.
projectConfig.forcedMajorVersion = porymapVersion.majorVersion();
projectConfig.minimumVersion = QVersionNumber(porymapVersion.majorVersion());
}
}
return true;
@ -939,7 +925,7 @@ void MainWindow::showProjectOpenFailure() {
if (!this->isVisible()){
// The main window is not visible during the initial project open; the splash screen is busy providing visual feedback.
// If project opening fails we can immediately display the empty main window (which we need anyway to parent messages to).
restoreWindowState();
resizeWithinScreen();
show();
}
RecentErrorMessage::show(QStringLiteral("There was an error opening the project."), this);
@ -1491,7 +1477,7 @@ bool MainWindow::setProjectUI() {
this->mapGroupModel = new MapGroupModel(editor->project);
this->groupListProxyModel = new FilterChildrenProxyModel();
this->groupListProxyModel->setSourceModel(this->mapGroupModel);
this->groupListProxyModel->setHideEmpty(porymapConfig.mapListHideEmptyEnabled[MapListTab::Groups]);
this->groupListProxyModel->setHideEmpty(porymapConfig.mapListTabsHidingEmptyFolders.contains(MapListTab::Groups));
ui->mapList->setModel(groupListProxyModel);
this->ui->mapList->setItemDelegateForColumn(0, new GroupNameDelegate(this->editor->project, this));
@ -1500,14 +1486,14 @@ bool MainWindow::setProjectUI() {
this->mapLocationModel = new MapLocationModel(editor->project);
this->locationListProxyModel = new FilterChildrenProxyModel();
this->locationListProxyModel->setSourceModel(this->mapLocationModel);
this->locationListProxyModel->setHideEmpty(porymapConfig.mapListHideEmptyEnabled[MapListTab::Locations]);
this->locationListProxyModel->setHideEmpty(porymapConfig.mapListTabsHidingEmptyFolders.contains(MapListTab::Locations));
ui->locationList->setModel(locationListProxyModel);
setMapListSorted(ui->locationList, porymapConfig.mapListLocationsSorted);
this->layoutTreeModel = new LayoutTreeModel(editor->project);
this->layoutListProxyModel = new FilterChildrenProxyModel();
this->layoutListProxyModel->setSourceModel(this->layoutTreeModel);
this->layoutListProxyModel->setHideEmpty(porymapConfig.mapListHideEmptyEnabled[MapListTab::Layouts]);
this->layoutListProxyModel->setHideEmpty(porymapConfig.mapListTabsHidingEmptyFolders.contains(MapListTab::Layouts));
ui->layoutList->setModel(layoutListProxyModel);
setMapListSorted(ui->layoutList, porymapConfig.mapListLayoutsSorted);
@ -1524,7 +1510,7 @@ bool MainWindow::setProjectUI() {
if (eventTabIcon.isNull()) {
// We randomly choose between the available characters for ~flavor~.
// For now, this correctly assumes all versions have 2 icons.
eventTabIcon = ProjectConfig::getPlayerIcon(projectConfig.baseGameVersion, QRandomGenerator::global()->bounded(0, 2));
eventTabIcon = BaseGame::getPlayerIcon(projectConfig.baseGameVersion, QRandomGenerator::global()->bounded(0, 2));
}
ui->mainTabBar->setTabIcon(MainTab::Events, eventTabIcon);
@ -2259,7 +2245,7 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index)
} else if (index == MapViewTab::Collision) {
refreshCollisionSelector();
} else if (index == MapViewTab::Prefabs) {
if (projectConfig.prefabFilepath.isEmpty() && !projectConfig.prefabImportPrompted) {
if (userConfig.prefabsFilepath.isEmpty() && !userConfig.prefabsImportPrompted) {
// User hasn't set up prefabs and hasn't been prompted before.
// Ask if they'd like to import the default prefabs file.
if (prefab.tryImportDefaultPrefabs(this, projectConfig.baseGameVersion))
@ -3299,7 +3285,8 @@ bool MainWindow::closeProject() {
return false;
}
}
editor->closeProject();
logInfo(QString("Closing project '%1'").arg(this->editor->project->root));
this->editor->closeProject();
clearProjectUI();
refreshRecentProjectsMenu();
setWindowDisabled(true);

View File

@ -46,6 +46,11 @@ Project::~Project()
}
void Project::setRoot(const QString &dir) {
// This is not currently designed to actually change the root folder.
// It will not appropriately update instances of the root stored elsewhere,
// like in projectConfig or userConfig.
Q_ASSERT(this->root.isEmpty());
this->root = dir;
FileDialog::setDirectory(dir);
this->parser.setRoot(dir);
@ -72,20 +77,21 @@ bool Project::sanityCheck() {
return false;
}
// Porymap projects have no standardized way for Porymap to determine whether they're compatible as of the latest breaking changes.
// We can use the project's git history (if it has one, and we're able to get it) to make a reasonable guess.
// We know the hashes of the commits in the base repos that contain breaking changes, so if we find one of these then the project
// should support at least up to that Porymap major version. If this fails for any reason it returns a version of -1.
int Project::getSupportedMajorVersion(QString *errorOut) {
QVersionNumber Project::getMinimumVersion(QString *errorOut) const {
if (!projectConfig.minimumVersion.isNull()) return projectConfig.minimumVersion;
// No explicitly supported version, we can use the project's git history (if it has one, and we're able to get it) to make a reasonable guess.
// We know the hashes of the commits in the base repos that contain breaking changes, so if we find one of these then the project
// should support at least up to that Porymap major version. If this fails for any reason it returns an empty QVersionNumber.
// This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check
// is not important enough to significantly slow down project launch, we'd rather just timeout.
const int timeoutLimit = 500;
const int failureVersion = -1;
QString gitName = "git";
constexpr int TimeoutLimit = 500;
const QString gitName = QStringLiteral("git");
QString gitPath = QStandardPaths::findExecutable(gitName);
if (gitPath.isEmpty()) {
if (errorOut) *errorOut = QString("Unable to locate %1.").arg(gitName);
return failureVersion;
return QVersionNumber();
}
QProcess process;
@ -98,7 +104,7 @@ int Project::getSupportedMajorVersion(QString *errorOut) {
// We'll get the root commit, then compare it to the known root commits for the base project repos.
process.setArguments({ "-c", QString("safe.directory=%1").arg(this->root), "rev-list", "--max-parents=0", "HEAD" });
process.start();
if (!process.waitForFinished(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) {
if (!process.waitForFinished(TimeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) {
if (errorOut) {
*errorOut = QStringLiteral("Failed to identify commit history");
if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) {
@ -109,7 +115,7 @@ int Project::getSupportedMajorVersion(QString *errorOut) {
if (!error.isEmpty()) errorOut->append(QString(": %1").arg(error));
}
}
return failureVersion;
return QVersionNumber();
}
const QString rootCommit = QString(process.readLine()).remove('\n');
@ -145,22 +151,22 @@ int Project::getSupportedMajorVersion(QString *errorOut) {
if (!historyMap.contains(rootCommit)) {
// Either this repo does not share history with one of the base repos, or we got some unexpected result.
if (errorOut) *errorOut = QStringLiteral("Unrecognized commit history");
return failureVersion;
return QVersionNumber();
}
// We now know which base repo that the user's repo shares history with.
// Next we check to see if it contains the changes required to support particular major versions of Porymap.
// We'll start with the most recent major version and work backwards.
for (const auto &pair : historyMap.value(rootCommit)) {
int versionNum = pair.first;
QString commitHash = pair.second;
const QVersionNumber version = QVersionNumber(pair.first);
const QString commitHash = pair.second;
if (commitHash.isEmpty()) {
// An empty commit hash means 'consider any point in the history a supported version'
return versionNum;
return version;
}
process.setArguments({ "-c", QString("safe.directory=%1").arg(this->root), "merge-base", "--is-ancestor", commitHash, "HEAD" });
process.start();
if (!process.waitForFinished(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
if (!process.waitForFinished(TimeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit) {
if (errorOut) {
*errorOut = QStringLiteral("Failed to search commit history");
if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) {
@ -171,15 +177,15 @@ int Project::getSupportedMajorVersion(QString *errorOut) {
if (!error.isEmpty()) errorOut->append(QString(": %1").arg(error));
}
}
return failureVersion;
return QVersionNumber();
}
if (process.exitCode() == 0) {
// Identified a supported major version
return versionNum;
return version;
}
}
// We recognized the commit history, but it's too old for any version of Porymap to support.
return 0;
return QVersionNumber();
}
bool Project::load() {
@ -1553,7 +1559,7 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa
tileset->loadTilesImage(&tilesImage);
// Create default metatiles
const int tilesPerMetatile = projectConfig.getNumTilesInMetatile();
const int tilesPerMetatile = Metatile::maxTiles();
for (int i = 0; i < tileset->maxMetatiles(); ++i) {
auto metatile = new Metatile();
for(int j = 0; j < tilesPerMetatile; ++j){
@ -1645,8 +1651,8 @@ bool Project::readTilesetMetatileLabels() {
for (auto i = defines.constBegin(); i != defines.constEnd(); i++) {
QString label = i.key();
uint32_t metatileId = i.value();
if (metatileId > Block::maxValue) {
metatileId &= Block::maxValue;
if (metatileId > Block::MaxValue) {
metatileId &= Block::MaxValue;
logWarn(QString("Value of metatile label '%1' truncated to %2").arg(label).arg(Metatile::getMetatileIdString(metatileId)));
}
QString tilesetName = findMetatileLabelsTileset(label);
@ -3239,7 +3245,7 @@ QPixmap Project::getEventPixmap(Event::Group group) {
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);
QString customIconPath = projectConfig.eventIconPaths.value(group);
if (customIconPath.isEmpty()) {
// No custom icon specified, use the default icon.
pixmap = defaultIcon;
@ -3331,7 +3337,7 @@ QString Project::getDefaultSpeciesIconPath(const QString &species) {
// We failed to find a default icon path, this species will use a placeholder icon.
// If the user has no custom icon path for this species, tell them they can provide one.
if (path.isEmpty() && projectConfig.getPokemonIconPath(species).isEmpty()) {
if (path.isEmpty() && projectConfig.pokemonIconPaths.value(species).isEmpty()) {
logWarn(QString("Failed to find Pokémon icon for '%1'. The filepath can be specified under 'Options->Project Settings'").arg(species));
}
return path;
@ -3372,7 +3378,7 @@ QPixmap Project::getSpeciesIcon(const QString &species) {
QPixmap pixmap;
if (!QPixmapCache::find(species, &pixmap)) {
// Prefer path from config. If not present, use the path parsed from project files
QString path = Project::getExistingFilepath(projectConfig.getPokemonIconPath(species));
QString path = Project::getExistingFilepath(projectConfig.pokemonIconPaths.value(species));
if (path.isEmpty()) {
path = getDefaultSpeciesIconPath(species);
}

View File

@ -715,7 +715,7 @@ void MainWindow::setMetatileAttributes(int metatileId, int attributes) {
}
int MainWindow::calculateTileBounds(int * tileStart, int * tileEnd) {
int maxNumTiles = projectConfig.getNumTilesInMetatile();
int maxNumTiles = Metatile::maxTiles();
if (*tileEnd >= maxNumTiles || *tileEnd < 0)
*tileEnd = maxNumTiles - 1;
if (*tileStart >= maxNumTiles || *tileStart < 0)

View File

@ -198,7 +198,12 @@ bool ScriptUtility::getSmartPathsEnabled() {
}
QList<QString> ScriptUtility::getCustomScripts() {
return userConfig.getCustomScriptPaths();
QList<QString> paths;
for (const auto& settings : userConfig.customScripts)
paths.append(settings.path);
for (const auto& settings : projectConfig.customScripts)
paths.append(settings.path);
return paths;
}
QList<int> ScriptUtility::getMetatileLayerOrder() {

View File

@ -5,6 +5,7 @@
#include "log.h"
#include "config.h"
#include "mainwindow.h"
#include "version.h"
Scripting *instance = nullptr;
@ -22,11 +23,11 @@ Scripting::Scripting(MainWindow *mainWindow)
: QObject(mainWindow), mainWindow(mainWindow), engine(new QJSEngine(this))
{
this->engine->installExtensions(QJSEngine::ConsoleExtension);
const QStringList paths = userConfig.getCustomScriptPaths();
const QList<bool> enabled = userConfig.getCustomScriptsEnabled();
for (int i = 0; i < paths.length(); i++) {
if (enabled.value(i, true))
loadScript(paths.at(i));
const QStringList paths = ScriptSettings::filter(userConfig.customScripts)
+ ScriptSettings::filter(projectConfig.customScripts);
for (const auto& path : paths) {
loadScript(path);
}
}
@ -103,10 +104,10 @@ void Scripting::populateGlobalObject() {
constants.setProperty("max_secondary_metatiles", Project::getNumMetatilesSecondary());
constants.setProperty("num_primary_palettes", Project::getNumPalettesPrimary());
constants.setProperty("num_secondary_palettes", Project::getNumPalettesSecondary());
constants.setProperty("layers_per_metatile", projectConfig.getNumLayersInMetatile());
constants.setProperty("tiles_per_metatile", projectConfig.getNumTilesInMetatile());
constants.setProperty("layers_per_metatile", Metatile::numLayers());
constants.setProperty("tiles_per_metatile", Metatile::maxTiles());
constants.setProperty("base_game_version", projectConfig.getBaseGameVersionString());
constants.setProperty("base_game_version", BaseGame::versionToString(projectConfig.baseGameVersion));
// Read out behavior values into constants object
QJSValue behaviorsArray = instance->engine->newObject();

View File

@ -1,112 +0,0 @@
#include "citymappixmapitem.h"
#include "imageproviders.h"
#include "config.h"
#include "log.h"
#include <QFile>
#include <QPainter>
#include <QDebug>
void CityMapPixmapItem::init() {
width_ = 10;
height_ = 10;
QFile binFile(file);
if (!binFile.open(QIODevice::ReadOnly)) return;
data = binFile.readAll();
if (projectConfig.baseGameVersion == BaseGameVersion::pokeruby) {
for (int i = 0; i < data.size(); i++)
data[i] = data[i] ^ 0x80;
}
binFile.close();
}
void CityMapPixmapItem::draw() {
QImage image(width_ * 8, height_ * 8, QImage::Format_RGBA8888);
// TODO: construct temporary tile from this based on the id?
// QPainter painter(&image);
// for (int i = 0; i < data.size() / 2; i++) {
// QImage img = this->tile_selector->tileImg(data[i * 2]);// need to skip every other tile
// int x = i % width_;
// int y = i / width_;
// QPoint pos = QPoint(x * 8, y * 8);
// painter.drawImage(pos, img);
// }
// painter.end();
this->setPixmap(QPixmap::fromImage(image));
}
void CityMapPixmapItem::save() {
QFile binFile(file);
if (!binFile.open(QIODevice::WriteOnly)) {
logError(QString("Cannot save city map tilemap to %1.").arg(file));
return;
}
if (projectConfig.baseGameVersion == BaseGameVersion::pokeruby) {
for (int i = 0; i < data.size(); i++)
data[i] = data[i] ^ 0x80;
}
binFile.write(data);
binFile.close();
}
void CityMapPixmapItem::paint(QGraphicsSceneMouseEvent *event) {
QPointF pos = event->pos();
int x = static_cast<int>(pos.x()) / 8;
int y = static_cast<int>(pos.y()) / 8;
int index = getIndexAt(x, y);
data[index] = static_cast<uint8_t>(this->tile_selector->selectedTile);
draw();
}
void CityMapPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
emit mouseEvent(event, this);
}
void CityMapPixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
QPointF pos = event->pos();
int x = static_cast<int>(pos.x()) / 8;
int y = static_cast<int>(pos.y()) / 8;
if (x < width_ && x >= 0
&& y < height_ && y >= 0) {
emit this->hoveredRegionMapTileChanged(x, y);
emit mouseEvent(event, this);
}
}
void CityMapPixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
emit mouseEvent(event, this);
}
QVector<uint8_t> CityMapPixmapItem::getTiles() {
QVector<uint8_t> tiles;
for (auto tile : data) {
tiles.append(tile);
}
return tiles;
}
void CityMapPixmapItem::setTiles(QVector<uint8_t> tiles) {
QByteArray newData;
for (auto tile : tiles) {
newData.append(tile);
}
this->data = newData;
}
int CityMapPixmapItem::getIndexAt(int x, int y) {
return 2 * (x + y * this->width_);
}
int CityMapPixmapItem::width() {
return this->width_;
}
int CityMapPixmapItem::height() {
return this->height_;
}

View File

@ -5,6 +5,7 @@
#include "editor.h"
#include "shortcut.h"
#include "filedialog.h"
#include "eventfilters.h"
#include <QDir>
@ -18,10 +19,10 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) :
// This property seems to be reset if we don't set it programmatically
ui->list->setDragDropMode(QAbstractItemView::NoDragDrop);
const QStringList paths = userConfig.getCustomScriptPaths();
const QList<bool> enabled = userConfig.getCustomScriptsEnabled();
for (int i = 0; i < paths.length(); i++)
this->displayScript(paths.at(i), enabled.value(i, true));
for (const auto& settings : projectConfig.customScripts)
displayScript(settings);
for (const auto& settings : userConfig.customScripts)
displayScript(settings);
connect(ui->button_Help, &QAbstractButton::clicked, this, &CustomScriptsEditor::openManual);
connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript);
@ -29,8 +30,8 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) :
connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::userRefreshScripts);
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CustomScriptsEditor::dialogButtonClicked);
this->initShortcuts();
this->restoreWindowState();
initShortcuts();
installEventFilter(new GeometrySaver(this));
}
CustomScriptsEditor::~CustomScriptsEditor()
@ -60,7 +61,6 @@ void CustomScriptsEditor::initShortcuts() {
shortcut_refresh->setObjectName("shortcut_refresh");
shortcut_refresh->setWhatsThis("Refresh Scripts");
shortcutsConfig.load();
shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
applyUserShortcuts();
}
@ -87,26 +87,16 @@ void CustomScriptsEditor::applyUserShortcuts() {
shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut));
}
void CustomScriptsEditor::restoreWindowState() {
logInfo("Restoring custom scripts editor geometry from previous session.");
const QMap<QString, QByteArray> geometry = porymapConfig.getCustomScriptsEditorGeometry();
this->restoreGeometry(geometry.value("custom_scripts_editor_geometry"));
this->restoreState(geometry.value("custom_scripts_editor_state"));
}
void CustomScriptsEditor::displayScript(const QString &filepath, bool enabled) {
auto item = new QListWidgetItem();
auto widget = new CustomScriptsListItem();
widget->ui->checkBox_Enable->setChecked(enabled);
widget->ui->lineEdit_filepath->setText(filepath);
void CustomScriptsEditor::displayScript(const ScriptSettings& settings) {
auto item = new QListWidgetItem(ui->list);
auto widget = new CustomScriptsListItem(settings, ui->list);
item->setSizeHint(widget->sizeHint());
connect(widget->ui->b_Choose, &QAbstractButton::clicked, [this, item](bool) { this->replaceScript(item); });
connect(widget->ui->b_Edit, &QAbstractButton::clicked, [this, item](bool) { this->openScript(item); });
connect(widget->ui->b_Delete, &QAbstractButton::clicked, [this, item](bool) { this->removeScript(item); });
connect(widget->ui->checkBox_Enable, &QCheckBox::toggled, this, &CustomScriptsEditor::markEdited);
connect(widget->ui->lineEdit_filepath, &QLineEdit::textEdited, this, &CustomScriptsEditor::markEdited);
connect(widget, &CustomScriptsListItem::clickedChooseScript, [this, item] { this->replaceScript(item); });
connect(widget, &CustomScriptsListItem::clickedEditScript, [this, item] { this->openScript(item); });
connect(widget, &CustomScriptsListItem::clickedDeleteScript, [this, item] { this->removeScript(item); });
connect(widget, &CustomScriptsListItem::toggledEnable, this, &CustomScriptsEditor::markEdited);
connect(widget, &CustomScriptsListItem::pathEdited, this, &CustomScriptsEditor::markEdited);
// Per the Qt manual, for performance reasons QListWidget::setItemWidget shouldn't be used with non-static items.
// There's an assumption here that users won't have enough scripts for that to be a problem.
@ -122,7 +112,7 @@ QString CustomScriptsEditor::getScriptFilepath(QListWidgetItem * item, bool abso
auto widget = dynamic_cast<CustomScriptsListItem *>(ui->list->itemWidget(item));
if (!widget) return QString();
QString path = widget->ui->lineEdit_filepath->text();
QString path = widget->path();
if (absolutePath) {
QFileInfo fileInfo(path);
if (fileInfo.isRelative())
@ -134,13 +124,13 @@ QString CustomScriptsEditor::getScriptFilepath(QListWidgetItem * item, bool abso
void CustomScriptsEditor::setScriptFilepath(QListWidgetItem * item, QString filepath) const {
auto widget = dynamic_cast<CustomScriptsListItem *>(ui->list->itemWidget(item));
if (widget) {
widget->ui->lineEdit_filepath->setText(Util::stripPrefix(filepath, this->baseDir));
widget->setPath(Util::stripPrefix(filepath, this->baseDir));
}
}
bool CustomScriptsEditor::getScriptEnabled(QListWidgetItem * item) const {
auto widget = dynamic_cast<CustomScriptsListItem *>(ui->list->itemWidget(item));
return widget && widget->ui->checkBox_Enable->isChecked();
return widget && widget->scriptEnabled();
}
QString CustomScriptsEditor::chooseScript(QString dir) {
@ -187,7 +177,9 @@ void CustomScriptsEditor::displayNewScript(QString filepath) {
}
}
this->displayScript(filepath, true);
ScriptSettings settings;
settings.path = filepath;
this->displayScript(settings);
this->markEdited();
}
@ -253,19 +245,23 @@ void CustomScriptsEditor::save() {
if (!this->hasUnsavedChanges)
return;
QStringList paths;
QList<bool> enabledStates;
QList<ScriptSettings> userScripts;
QList<ScriptSettings> projectScripts;
for (int i = 0; i < ui->list->count(); i++) {
auto item = ui->list->item(i);
const QString path = this->getScriptFilepath(item, false);
if (!path.isEmpty()) {
paths.append(path);
enabledStates.append(this->getScriptEnabled(item));
}
auto widget = dynamic_cast<CustomScriptsListItem *>(ui->list->itemWidget(item));
if (!widget) continue;
const ScriptSettings settings = widget->getSettings();
if (settings.userOnly) userScripts.append(settings);
else projectScripts.append(settings);
}
userConfig.setCustomScripts(paths, enabledStates);
userConfig.customScripts = userScripts;
userConfig.save();
projectConfig.customScripts = projectScripts;
projectConfig.save();
this->hasUnsavedChanges = false;
this->refreshScripts();
}
@ -295,9 +291,4 @@ void CustomScriptsEditor::closeEvent(QCloseEvent* event) {
if (result == QMessageBox::Yes)
this->save();
}
porymapConfig.setCustomScriptsEditorGeometry(
this->saveGeometry(),
this->saveState()
);
}

View File

@ -6,9 +6,50 @@ CustomScriptsListItem::CustomScriptsListItem(QWidget *parent) :
ui(new Ui::CustomScriptsListItem)
{
ui->setupUi(this);
connect(ui->b_Choose, &QAbstractButton::clicked, this, &CustomScriptsListItem::clickedChooseScript);
connect(ui->b_Edit, &QAbstractButton::clicked, this, &CustomScriptsListItem::clickedEditScript);
connect(ui->b_Delete, &QAbstractButton::clicked, this, &CustomScriptsListItem::clickedDeleteScript);
connect(ui->checkBox_Enable, &QCheckBox::toggled, this, &CustomScriptsListItem::toggledEnable);
connect(ui->lineEdit_filepath, &QLineEdit::textEdited, this, &CustomScriptsListItem::pathEdited);
}
CustomScriptsListItem::~CustomScriptsListItem()
CustomScriptsListItem::CustomScriptsListItem(const ScriptSettings& settings, QWidget *parent) :
CustomScriptsListItem(parent)
{
setSettings(settings);
}
CustomScriptsListItem::~CustomScriptsListItem() {
delete ui;
}
void CustomScriptsListItem::setPath(const QString& text) {
ui->lineEdit_filepath->setText(text);
}
QString CustomScriptsListItem::path() const {
return ui->lineEdit_filepath->text();
}
void CustomScriptsListItem::setScriptEnabled(bool enabled) {
ui->checkBox_Enable->setChecked(enabled);
}
bool CustomScriptsListItem::scriptEnabled() const {
return ui->checkBox_Enable->isChecked();
}
// TODO: The two functions below should read/write from/to the UI whether the script belongs to the project or user.
void CustomScriptsListItem::setSettings(const ScriptSettings& settings) {
setPath(settings.path);
setScriptEnabled(settings.enabled);
}
ScriptSettings CustomScriptsListItem::getSettings() const {
return {
.path = path(),
.enabled = scriptEnabled(),
.userOnly = true,
};
}

View File

@ -1,4 +1,6 @@
#include "eventfilters.h"
#include "config.h"
#include "log.h"
#include <QGraphicsSceneWheelEvent>
@ -16,10 +18,41 @@ bool MapSceneEventFilter::eventFilter(QObject*, QEvent *event) {
}
bool ActiveWindowFilter::eventFilter(QObject*, QEvent *event) {
if (event->type() == QEvent::WindowActivate) {
emit activated();
}
return false;
}
bool GeometrySaver::eventFilter(QObject *object, QEvent *event) {
if (event->spontaneous()) return false;
auto w = qobject_cast<QWidget*>(object);
if (!w) return false;
if (event->type() == QEvent::Polish) {
// Note: Restoring geometry in QEvent::Show would be too late,
// and the widget would briefly appear with the old geometry.
porymapConfig.restoreGeometry(w);
} else if (event->type() == QEvent::Show) {
if (m_loggingEnabled && !w->windowTitle().isEmpty()) {
logInfo(QString("Opening window: %1").arg(w->windowTitle()));
}
m_shown.insert(object);
} else if (event->type() == QEvent::Close || event->type() == QEvent::DeferredDelete) {
// There are situations where a window might be 'closed' without
// ever actually having been opened (for example, the Shortcuts Editor
// will quietly construct windows to get their shortcuts, and those windows
// can later be closed without having been displayed).
// We don't want to save the geometry of these windows, or log that they closed,
// so we checked to make sure the widget was displayed before proceeding.
if (!m_shown.remove(object)) return false;
porymapConfig.saveGeometry(w);
if (m_loggingEnabled && !w->windowTitle().isEmpty()) {
logInfo(QString("Closing window: %1").arg(w->windowTitle()));
}
}
return false;
}

View File

@ -74,6 +74,28 @@ QVector<qreal> GridSettings::getDashPattern(uint length) const {
}
}
QJsonObject GridSettings::toJson() const {
QJsonObject obj;
obj["width"] = static_cast<qint64>(this->width);
obj["height"] = static_cast<qint64>(this->height);
obj["offsetX"] = this->offsetX;
obj["offsetY"] = this->offsetY;
obj["style"] = getStyleName(this->style);
obj["color"] = this->color.name();
return obj;
}
GridSettings GridSettings::fromJson(const QJsonObject &obj) {
GridSettings settings;
settings.width = obj["width"].toInt();
settings.height = obj["height"].toInt();
settings.offsetX = obj["offsetX"].toInt();
settings.offsetY = obj["offsetY"].toInt();
settings.style = getStyleFromName(obj["style"].toString());
settings.color = QColor(obj["color"].toString());
return settings;
}
GridSettingsDialog::GridSettingsDialog(QWidget *parent) :

View File

@ -565,7 +565,9 @@ void MapImageExporter::updatePreview(bool forceUpdate) {
}
progress.close();
m_previewImage.setColorSpace(Util::toColorSpace(porymapConfig.imageExportColorSpaceId));
if (porymapConfig.imageExportColorSpace) {
m_previewImage.setColorSpace(QColorSpace(porymapConfig.imageExportColorSpace.value()));
}
m_preview->setPixmap(QPixmap::fromImage(m_previewImage));
m_scene->setSceneRect(m_scene->itemsBoundingRect());
scalePreview();

View File

@ -276,7 +276,9 @@ void MetatileImageExporter::updatePreview() {
m_layerOrder);
}
m_previewImage.setColorSpace(Util::toColorSpace(porymapConfig.imageExportColorSpaceId));
if (porymapConfig.imageExportColorSpace) {
m_previewImage.setColorSpace(QColorSpace(porymapConfig.imageExportColorSpace.value()));
}
m_preview->setPixmap(QPixmap::fromImage(m_previewImage));
m_scene->setSceneRect(m_scene->itemsBoundingRect());
m_previewUpdateQueued = false;

View File

@ -23,16 +23,17 @@ void MetatileLayersItem::setOrientation(Qt::Orientation orientation) {
// Generate a table of tile positions that allows us to map between
// the index of a tile in the metatile and its position in this layer view.
this->tilePositions.clear();
const int numLayers = Metatile::numLayers();
if (this->orientation == Qt::Horizontal) {
// Tiles are laid out horizontally, with the bottom layer on the left:
// 0 1 4 5 8 9
// 2 3 6 7 10 11
for (int layer = 0; layer < projectConfig.getNumLayersInMetatile(); layer++)
for (int layer = 0; layer < numLayers; layer++)
for (int y = 0; y < Metatile::tileHeight(); y++)
for (int x = 0; x < Metatile::tileWidth(); x++) {
this->tilePositions.append(QPoint(x + layer * Metatile::tileWidth(), y));
}
maxWidth *= projectConfig.getNumLayersInMetatile();
maxWidth *= numLayers;
} else if (this->orientation == Qt::Vertical) {
// Tiles are laid out vertically, with the bottom layer on the bottom:
// 8 9
@ -41,12 +42,12 @@ void MetatileLayersItem::setOrientation(Qt::Orientation orientation) {
// 6 7
// 0 1
// 2 3
for (int layer = projectConfig.getNumLayersInMetatile() - 1; layer >= 0; layer--)
for (int layer = numLayers - 1; layer >= 0; layer--)
for (int y = 0; y < Metatile::tileHeight(); y++)
for (int x = 0; x < Metatile::tileWidth(); x++) {
this->tilePositions.append(QPoint(x, y + layer * Metatile::tileHeight()));
}
maxHeight *= projectConfig.getNumLayersInMetatile();
maxHeight *= numLayers;
}
setMaxSelectionSize(maxWidth, maxHeight);
update();
@ -61,7 +62,7 @@ void MetatileLayersItem::draw() {
// Draw tile images
const Metatile* metatile = getMetatile();
int numTiles = qMin(projectConfig.getNumTilesInMetatile(), metatile ? metatile->tiles.length() : 0);
int numTiles = qMin(Metatile::maxTiles(), metatile ? metatile->tiles.length() : 0);
for (int i = 0; i < numTiles; i++) {
Tile tile = metatile->tiles.at(i);
QImage tileImage = getPalettedTileImage(tile.tileId,
@ -79,7 +80,7 @@ void MetatileLayersItem::draw() {
painter.setPen(Qt::white);
const int layerWidth = this->cellWidth * Metatile::tileWidth();
const int layerHeight = this->cellHeight * Metatile::tileHeight();
for (int i = 1; i < projectConfig.getNumLayersInMetatile(); i++) {
for (int i = 1; i < Metatile::numLayers(); i++) {
if (this->orientation == Qt::Vertical) {
int y = i * layerHeight;
painter.drawLine(0, y, layerWidth, y);

View File

@ -3,6 +3,7 @@
#include "ui_newlayoutdialog.h"
#include "config.h"
#include "validator.h"
#include "eventfilters.h"
#include <QMap>
#include <QSet>
@ -40,19 +41,12 @@ NewLayoutDialog::NewLayoutDialog(Project *project, const Layout *layoutToCopy, Q
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewLayoutDialog::dialogButtonClicked);
refresh();
if (porymapConfig.newLayoutDialogGeometry.isEmpty()){
// On first display resize to fit contents a little better
adjustSize();
} else {
restoreGeometry(porymapConfig.newLayoutDialogGeometry);
}
installEventFilter(new GeometrySaver(this));
ui->lineEdit_Name->setFocus();
}
NewLayoutDialog::~NewLayoutDialog()
{
porymapConfig.newLayoutDialogGeometry = saveGeometry();
saveSettings();
delete ui;
}

View File

@ -4,6 +4,7 @@
#include "ui_newmapdialog.h"
#include "config.h"
#include "validator.h"
#include "eventfilters.h"
#include <QMap>
#include <QSet>
@ -61,7 +62,7 @@ NewMapDialog::NewMapDialog(Project *project, const Map *mapToCopy, QWidget *pare
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewMapDialog::dialogButtonClicked);
refresh();
restoreGeometry(porymapConfig.newMapDialogGeometry);
installEventFilter(new GeometrySaver(this));
ui->lineEdit_Name->setFocus();
}
@ -91,7 +92,6 @@ NewMapDialog::NewMapDialog(Project *project, int mapListTab, const QString &mapL
NewMapDialog::~NewMapDialog()
{
porymapConfig.newMapDialogGeometry = saveGeometry();
saveSettings();
delete ui;
}

View File

@ -60,7 +60,7 @@ PaletteEditor::PaletteEditor(Project *project, Tileset *primaryTileset, Tileset
ui->actionRedo->setShortcuts({ui->actionRedo->shortcut(), QKeySequence("Ctrl+Shift+Z")});
refreshPaletteId();
restoreWindowState();
installEventFilter(new GeometrySaver(this));
}
PaletteEditor::~PaletteEditor() {
@ -159,13 +159,6 @@ void PaletteEditor::commitEditHistory(int paletteId) {
updateEditHistoryActions();
}
void PaletteEditor::restoreWindowState() {
logInfo("Restoring palette editor geometry from previous session.");
QMap<QString, QByteArray> geometry = porymapConfig.getPaletteEditorGeometry();
restoreGeometry(geometry.value("palette_editor_geometry"));
restoreState(geometry.value("palette_editor_state"));
}
void PaletteEditor::updateEditHistoryActions() {
int paletteId = currentPaletteId();
// We have an initial commit that shouldn't be available to Undo, so we ignore that.
@ -279,11 +272,6 @@ void PaletteEditor::setColorInputTitles(bool showUnused) {
}
void PaletteEditor::closeEvent(QCloseEvent*) {
porymapConfig.setPaletteEditorGeometry(
saveGeometry(),
saveState()
);
// Opening the color search window then closing the Palette Editor sets
// focus to the main editor window instead of the parent (Tileset Editor).
// Make sure the parent is active when we close.

View File

@ -20,7 +20,7 @@ const QString defaultFilepath = "prefabs.json";
void Prefab::loadPrefabs() {
this->items.clear();
QString filepath = projectConfig.prefabFilepath;
QString filepath = userConfig.prefabsFilepath;
if (filepath.isEmpty()) return;
ParseUtil parser;
@ -87,10 +87,10 @@ void Prefab::loadPrefabs() {
}
void Prefab::savePrefabs() {
if (projectConfig.prefabFilepath.isEmpty())
projectConfig.prefabFilepath = defaultFilepath;
if (userConfig.prefabsFilepath.isEmpty())
userConfig.prefabsFilepath = defaultFilepath;
QString filepath = projectConfig.prefabFilepath;
QString filepath = userConfig.prefabsFilepath;
QFileInfo info(filepath);
if (info.isRelative()) {
@ -283,9 +283,9 @@ void Prefab::addPrefab(MetatileSelection selection, Layout *layout, QString name
this->updatePrefabUi(layout);
}
bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, QString filepath) {
bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGame::Version version, QString filepath) {
// Ensure we have default prefabs for the project's game version.
if (version != BaseGameVersion::pokeruby && version != BaseGameVersion::pokeemerald && version != BaseGameVersion::pokefirered)
if (version != BaseGame::Version::pokeruby && version != BaseGame::Version::pokeemerald && version != BaseGame::Version::pokefirered)
return false;
if (filepath.isEmpty())
@ -316,17 +316,17 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version,
QMessageBox::question(parent,
QApplication::applicationName(),
QString("Would you like to import the default prefabs for %1? %2.")
.arg(projectConfig.getBaseGameVersionString(version))
.arg(BaseGame::versionToString(version))
.arg(fileWarning),
QMessageBox::Yes | QMessageBox::No);
bool acceptedImport = (prompt == QMessageBox::Yes);
if (acceptedImport) {
// Sets up the default prefabs.json filepath.
projectConfig.prefabFilepath = filepath;
userConfig.prefabsFilepath = filepath;
QFile prefabsFile(absFilepath);
if (!prefabsFile.open(QIODevice::WriteOnly)) {
projectConfig.prefabFilepath = QString();
userConfig.prefabsFilepath = QString();
logError(QString("Error: Could not open %1 for writing").arg(absFilepath));
QMessageBox messageBox(parent);
@ -340,13 +340,13 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version,
ParseUtil parser;
QString content;
switch (version) {
case BaseGameVersion::pokeruby:
case BaseGame::Version::pokeruby:
content = parser.readTextFile(":/text/prefabs_default_ruby.json");
break;
case BaseGameVersion::pokefirered:
case BaseGame::Version::pokefirered:
content = parser.readTextFile(":/text/prefabs_default_firered.json");
break;
case BaseGameVersion::pokeemerald:
case BaseGame::Version::pokeemerald:
content = parser.readTextFile(":/text/prefabs_default_emerald.json");
break;
default:
@ -359,7 +359,7 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version,
this->loadPrefabs();
}
projectConfig.prefabImportPrompted = true;
userConfig.prefabsImportPrompted = true;
return acceptedImport;
}

View File

@ -81,7 +81,11 @@ void PreferenceEditor::updateFields() {
} else if (porymapConfig.eventSelectionShapeMode == QGraphicsPixmapItem::BoundingRectShape) {
ui->radioButton_WithinRect->setChecked(true);
}
ui->comboBox_ColorSpace->setNumberItem(porymapConfig.imageExportColorSpaceId);
if (porymapConfig.imageExportColorSpace) {
ui->comboBox_ColorSpace->setNumberItem(porymapConfig.imageExportColorSpace.value());
} else {
ui->comboBox_ColorSpace->setNumberItem(0);
}
ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.textEditorOpenFolder);
ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.textEditorGotoLine);
ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.monitorFiles);
@ -103,7 +107,7 @@ void PreferenceEditor::updateFields() {
ui->checkBox_StatusWarnings->setChecked(porymapConfig.statusBarLogTypes.find(LogType::LOG_WARN) != logTypeEnd);
ui->checkBox_StatusInformation->setChecked(porymapConfig.statusBarLogTypes.find(LogType::LOG_INFO) != logTypeEnd);
if (/*porymapConfig.displayIdsHexadecimal*/ConfigDisplayIdsHexadecimal) {
if (porymapConfig.displayIdsHexadecimal) {
ui->radioButton_Hexadecimal->setChecked(true);
} else {
ui->radioButton_Decimal->setChecked(true);
@ -133,7 +137,12 @@ void PreferenceEditor::saveFields() {
emit scriptSettingsChanged(scriptAutocompleteMode);
}
porymapConfig.imageExportColorSpaceId = ui->comboBox_ColorSpace->currentData().toInt();
auto colorSpace = magic_enum::enum_cast<QColorSpace::NamedColorSpace>(ui->comboBox_ColorSpace->currentData().toInt());
if (colorSpace.has_value()) {
porymapConfig.imageExportColorSpace = colorSpace.value();
} else {
porymapConfig.imageExportColorSpace.reset();
}
porymapConfig.eventSelectionShapeMode = ui->radioButton_OnSprite->isChecked() ? QGraphicsPixmapItem::MaskShape : QGraphicsPixmapItem::BoundingRectShape;
porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text();
porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text();
@ -141,7 +150,7 @@ void PreferenceEditor::saveFields() {
porymapConfig.checkForUpdates = ui->checkBox_CheckForUpdates->isChecked();
porymapConfig.eventDeleteWarningDisabled = ui->checkBox_DisableEventWarning->isChecked();
porymapConfig.showProjectLoadingScreen = ui->checkBox_ShowProjectLoadingScreen->isChecked();
/*porymapConfig.displayIdsHexadecimal*/ConfigDisplayIdsHexadecimal = ui->radioButton_Hexadecimal->isChecked();
porymapConfig.displayIdsHexadecimal = ui->radioButton_Hexadecimal->isChecked();
porymapConfig.statusBarLogTypes.clear();
if (ui->checkBox_StatusErrors->isChecked()) porymapConfig.statusBarLogTypes.insert(LogType::LOG_ERROR);

View File

@ -4,6 +4,7 @@
#include "filedialog.h"
#include "newdefinedialog.h"
#include "utility.h"
#include "eventfilters.h"
#include <QAbstractButton>
#include <QFormLayout>
@ -27,9 +28,9 @@ ProjectSettingsEditor::ProjectSettingsEditor(QWidget *parent, Project *project)
this->initUi();
this->createProjectPathsTable();
this->createProjectIdentifiersTable();
this->installEventFilter(new GeometrySaver(this));
this->connectSignals();
this->refresh();
this->restoreWindowState();
}
ProjectSettingsEditor::~ProjectSettingsEditor()
@ -110,16 +111,19 @@ void ProjectSettingsEditor::initUi() {
ui->comboBox_IconSpecies->addItems(project->speciesNames);
ui->comboBox_WarpBehaviors->addItems(project->metatileBehaviorMap.keys());
}
ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings);
ui->comboBox_BaseGameVersion->addItem("Custom", BaseGame::Version::none);
ui->comboBox_BaseGameVersion->addItem(BaseGame::versionToString(BaseGame::Version::pokeruby), BaseGame::Version::pokeruby);
ui->comboBox_BaseGameVersion->addItem(BaseGame::versionToString(BaseGame::Version::pokefirered), BaseGame::Version::pokefirered);
ui->comboBox_BaseGameVersion->addItem(BaseGame::versionToString(BaseGame::Version::pokeemerald), BaseGame::Version::pokeemerald);
ui->comboBox_AttributesSize->addItems({"1", "2", "4"});
ui->comboBox_EventsTabIcon->addItem("Automatic", "");
ui->comboBox_EventsTabIcon->addItem("Brendan (Emerald)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeemerald, 0));
ui->comboBox_EventsTabIcon->addItem("Brendan (R/S)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeruby, 0));
ui->comboBox_EventsTabIcon->addItem("May (Emerald)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeemerald, 1));
ui->comboBox_EventsTabIcon->addItem("May (R/S)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeruby, 1));
ui->comboBox_EventsTabIcon->addItem("Red", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 0));
ui->comboBox_EventsTabIcon->addItem("Green", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 1));
ui->comboBox_EventsTabIcon->addItem("Brendan (Emerald)", BaseGame::getPlayerIconPath(BaseGame::Version::pokeemerald, 0));
ui->comboBox_EventsTabIcon->addItem("Brendan (R/S)", BaseGame::getPlayerIconPath(BaseGame::Version::pokeruby, 0));
ui->comboBox_EventsTabIcon->addItem("May (Emerald)", BaseGame::getPlayerIconPath(BaseGame::Version::pokeemerald, 1));
ui->comboBox_EventsTabIcon->addItem("May (R/S)", BaseGame::getPlayerIconPath(BaseGame::Version::pokeruby, 1));
ui->comboBox_EventsTabIcon->addItem("Red", BaseGame::getPlayerIconPath(BaseGame::Version::pokefirered, 0));
ui->comboBox_EventsTabIcon->addItem("Green", BaseGame::getPlayerIconPath(BaseGame::Version::pokefirered, 1));
ui->comboBox_EventsTabIcon->addItem("Custom", "Custom");
connect(ui->comboBox_EventsTabIcon, QOverload<int>::of(&NoScrollComboBox::currentIndexChanged), [this](int index) {
bool usingCustom = (index == ui->comboBox_EventsTabIcon->findText("Custom"));
@ -147,12 +151,12 @@ void ProjectSettingsEditor::initUi() {
ui->spinBox_Collision->setMaximum(Block::getMaxCollision());
ui->spinBox_MaxElevation->setMaximum(Block::getMaxElevation());
ui->spinBox_MaxCollision->setMaximum(Block::getMaxCollision());
ui->spinBox_MetatileIdMask->setMaximum(Block::maxValue);
ui->spinBox_CollisionMask->setMaximum(Block::maxValue);
ui->spinBox_ElevationMask->setMaximum(Block::maxValue);
ui->spinBox_UnusedTileNormal->setMaximum(Tile::maxValue);
ui->spinBox_UnusedTileCovered->setMaximum(Tile::maxValue);
ui->spinBox_UnusedTileSplit->setMaximum(Tile::maxValue);
ui->spinBox_MetatileIdMask->setMaximum(Block::MaxValue);
ui->spinBox_CollisionMask->setMaximum(Block::MaxValue);
ui->spinBox_ElevationMask->setMaximum(Block::MaxValue);
ui->spinBox_UnusedTileNormal->setMaximum(Tile::MaxValue);
ui->spinBox_UnusedTileCovered->setMaximum(Tile::MaxValue);
ui->spinBox_UnusedTileSplit->setMaximum(Tile::MaxValue);
ui->spinBox_MaxEvents->setMaximum(INT_MAX);
ui->spinBox_MapWidth->setMaximum(INT_MAX);
ui->spinBox_MapHeight->setMaximum(INT_MAX);
@ -201,6 +205,10 @@ bool ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString
return false;
}
BaseGame::Version ProjectSettingsEditor::getBaseGameVersion() const {
return static_cast<BaseGame::Version>(ui->comboBox_BaseGameVersion->currentData().toInt());
}
// Remember the current settings tab for future sessions
void ProjectSettingsEditor::on_mainTabs_tabBarClicked(int index) {
porymapConfig.projectSettingsTab = index;
@ -434,13 +442,6 @@ QString ProjectSettingsEditor::chooseProjectFile(const QString &defaultFilepath)
return path.remove(0, this->baseDir.length());
}
void ProjectSettingsEditor::restoreWindowState() {
logInfo("Restoring project settings editor geometry from previous session.");
const QMap<QString, QByteArray> geometry = porymapConfig.getProjectSettingsEditorGeometry();
this->restoreGeometry(geometry.value("project_settings_editor_geometry"));
this->restoreState(geometry.value("project_settings_editor_state"));
}
// Set UI states using config data
void ProjectSettingsEditor::refresh() {
this->refreshing = true; // Block signals
@ -448,12 +449,12 @@ void ProjectSettingsEditor::refresh() {
// Set combo box texts
ui->comboBox_DefaultPrimaryTileset->setTextItem(projectConfig.defaultPrimaryTileset);
ui->comboBox_DefaultSecondaryTileset->setTextItem(projectConfig.defaultSecondaryTileset);
ui->comboBox_BaseGameVersion->setTextItem(projectConfig.getBaseGameVersionString());
ui->comboBox_BaseGameVersion->setNumberItem(projectConfig.baseGameVersion);
ui->comboBox_AttributesSize->setTextItem(QString::number(projectConfig.metatileAttributesSize));
this->updateAttributeLimits(ui->comboBox_AttributesSize->currentText());
this->prevIconSpecies = QString();
this->editedPokemonIconPaths = projectConfig.getPokemonIconPaths();
this->editedPokemonIconPaths = projectConfig.pokemonIconPaths;
this->updatePokemonIconPath(ui->comboBox_IconSpecies->currentText());
// Set check box states
@ -511,13 +512,13 @@ void ProjectSettingsEditor::refresh() {
this->setBorderMetatileIds(true, projectConfig.newMapBorderMetatileIds);
// Set line edit texts
ui->lineEdit_PrefabsPath->setText(projectConfig.prefabFilepath);
ui->lineEdit_PrefabsPath->setText(userConfig.prefabsFilepath);
ui->lineEdit_CollisionGraphics->setText(projectConfig.collisionSheetPath);
ui->lineEdit_ObjectsIcon->setText(projectConfig.getEventIconPath(Event::Group::Object));
ui->lineEdit_WarpsIcon->setText(projectConfig.getEventIconPath(Event::Group::Warp));
ui->lineEdit_TriggersIcon->setText(projectConfig.getEventIconPath(Event::Group::Coord));
ui->lineEdit_BGsIcon->setText(projectConfig.getEventIconPath(Event::Group::Bg));
ui->lineEdit_HealLocationsIcon->setText(projectConfig.getEventIconPath(Event::Group::Heal));
ui->lineEdit_ObjectsIcon->setText(projectConfig.eventIconPaths.value(Event::Group::Object));
ui->lineEdit_WarpsIcon->setText(projectConfig.eventIconPaths.value(Event::Group::Warp));
ui->lineEdit_TriggersIcon->setText(projectConfig.eventIconPaths.value(Event::Group::Coord));
ui->lineEdit_BGsIcon->setText(projectConfig.eventIconPaths.value(Event::Group::Bg));
ui->lineEdit_HealLocationsIcon->setText(projectConfig.eventIconPaths.value(Event::Group::Heal));
for (auto lineEdit : ui->scrollAreaContents_ProjectPaths->findChildren<QLineEdit*>())
lineEdit->setText(projectConfig.getCustomFilePath(lineEdit->objectName()));
for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren<QLineEdit*>())
@ -556,7 +557,7 @@ void ProjectSettingsEditor::save() {
// Save combo box settings
projectConfig.defaultPrimaryTileset = ui->comboBox_DefaultPrimaryTileset->currentText();
projectConfig.defaultSecondaryTileset = ui->comboBox_DefaultSecondaryTileset->currentText();
projectConfig.baseGameVersion = projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText());
projectConfig.baseGameVersion = getBaseGameVersion();
projectConfig.metatileAttributesSize = ui->comboBox_AttributesSize->currentText().toInt();
// Save check box settings
@ -603,13 +604,13 @@ void ProjectSettingsEditor::save() {
projectConfig.metatileSelectorWidth = ui->spinBox_MetatileSelectorWidth->value();
// Save line edit settings
projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text();
userConfig.prefabsFilepath = ui->lineEdit_PrefabsPath->text();
projectConfig.collisionSheetPath = ui->lineEdit_CollisionGraphics->text();
projectConfig.setEventIconPath(Event::Group::Object, ui->lineEdit_ObjectsIcon->text());
projectConfig.setEventIconPath(Event::Group::Warp, ui->lineEdit_WarpsIcon->text());
projectConfig.setEventIconPath(Event::Group::Coord, ui->lineEdit_TriggersIcon->text());
projectConfig.setEventIconPath(Event::Group::Bg, ui->lineEdit_BGsIcon->text());
projectConfig.setEventIconPath(Event::Group::Heal, ui->lineEdit_HealLocationsIcon->text());
projectConfig.eventIconPaths[Event::Group::Object] = ui->lineEdit_ObjectsIcon->text();
projectConfig.eventIconPaths[Event::Group::Warp] = ui->lineEdit_WarpsIcon->text();
projectConfig.eventIconPaths[Event::Group::Coord] = ui->lineEdit_TriggersIcon->text();
projectConfig.eventIconPaths[Event::Group::Bg] = ui->lineEdit_BGsIcon->text();
projectConfig.eventIconPaths[Event::Group::Heal] = ui->lineEdit_HealLocationsIcon->text();
for (auto lineEdit : ui->scrollAreaContents_ProjectPaths->findChildren<QLineEdit*>())
projectConfig.setFilePath(lineEdit->objectName(), lineEdit->text());
for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren<QLineEdit*>())
@ -623,7 +624,7 @@ void ProjectSettingsEditor::save() {
projectConfig.warpBehaviors.clear();
const QStringList behaviorNames = this->getWarpBehaviorsList();
for (auto name : behaviorNames)
projectConfig.warpBehaviors.append(project->metatileBehaviorMap.value(name));
projectConfig.warpBehaviors.insert(project->metatileBehaviorMap.value(name));
// Save border metatile IDs
projectConfig.newMapBorderMetatileIds = this->getBorderMetatileIds(ui->checkBox_EnableCustomBorderSize->isChecked());
@ -633,7 +634,7 @@ void ProjectSettingsEditor::save() {
if (this->project->speciesNames.contains(species))
this->editedPokemonIconPaths.insert(species, ui->lineEdit_PokemonIcon->text());
for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++)
projectConfig.setPokemonIconPath(i.key(), i.value());
projectConfig.pokemonIconPaths[i.key()] = i.value();
QString eventsTabIconPath;
QVariant data = ui->comboBox_EventsTabIcon->currentData();
@ -773,9 +774,9 @@ QString ProjectSettingsEditor::stripProjectDir(QString s) {
void ProjectSettingsEditor::importDefaultPrefabsClicked(bool) {
// If the prompt is accepted the prefabs file will be created and its filepath will be saved in the config.
BaseGameVersion version = projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText());
BaseGame::Version version = getBaseGameVersion();
if (prefab.tryImportDefaultPrefabs(this, version, ui->lineEdit_PrefabsPath->text())) {
ui->lineEdit_PrefabsPath->setText(projectConfig.prefabFilepath); // Refresh with new filepath
ui->lineEdit_PrefabsPath->setText(userConfig.prefabsFilepath); // Refresh with new filepath
this->hasUnsavedChanges = true;
}
}
@ -805,18 +806,20 @@ bool ProjectSettingsEditor::promptSaveChanges() {
return true;
}
// TODO: Changing the base game version implicitly changes defaults.
// If the user declines this prompt, it should revert to the previous setting.
bool ProjectSettingsEditor::promptRestoreDefaults() {
if (this->refreshing)
return false;
const QString versionText = ui->comboBox_BaseGameVersion->currentText();
if (this->prompt(QString("Restore default config settings for %1?").arg(versionText)) == QMessageBox::No)
if (this->prompt(QString("Restore default config settings for %1?").arg(ui->comboBox_BaseGameVersion->currentText())) == QMessageBox::No)
return false;
// Restore defaults by resetting config in memory, refreshing the UI, then restoring the config.
// Don't want to save changes until user accepts them.
// TODO: Maybe give the project settings editor it's own copy of the config then.
ProjectConfig tempProject = projectConfig;
projectConfig.reset(projectConfig.stringToBaseGameVersion(versionText));
projectConfig.setVersionSpecificDefaults(getBaseGameVersion());
this->refresh();
projectConfig = tempProject;
@ -863,11 +866,6 @@ void ProjectSettingsEditor::closeEvent(QCloseEvent* event) {
return;
}
porymapConfig.setProjectSettingsEditorGeometry(
this->saveGeometry(),
this->saveState()
);
if (this->projectNeedsReload) {
// Note: Declining this prompt with changes that need a reload may cause problems
if (this->prompt("Settings saved, reload project to apply changes?") == QMessageBox::Yes)

View File

@ -7,6 +7,7 @@
#include "config.h"
#include "log.h"
#include "utility.h"
#include "eventfilters.h"
#include <QDir>
#include <QDialog>
@ -33,7 +34,7 @@ RegionMapEditor::RegionMapEditor(QWidget *parent, Project *project) :
this->configFilepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_porymap_cfg));
this->initShortcuts();
this->restoreWindowState();
this->installEventFilter(new GeometrySaver(this));
}
RegionMapEditor::~RegionMapEditor()
@ -56,13 +57,6 @@ RegionMapEditor::~RegionMapEditor()
delete ui;
}
void RegionMapEditor::restoreWindowState() {
logInfo("Restoring region map editor geometry from previous session.");
QMap<QString, QByteArray> geometry = porymapConfig.getRegionMapEditorGeometry();
this->restoreGeometry(geometry.value("region_map_editor_geometry"));
this->restoreState(geometry.value("region_map_editor_state"));
}
void RegionMapEditor::initShortcuts() {
auto *shortcut_RM_Options_delete = new Shortcut(
{QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(on_pushButton_RM_Options_delete_clicked()));
@ -80,7 +74,6 @@ void RegionMapEditor::initShortcuts() {
ui->menuEdit->addAction(undoAction);
ui->menuEdit->addAction(redoAction);
shortcutsConfig.load();
shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
applyUserShortcuts();
@ -122,42 +115,15 @@ bool RegionMapEditor::saveRegionMapEntries() {
return true;
}
void buildEmeraldDefaults(poryjson::Json &json) {
ParseUtil parser;
QString emeraldDefault = parser.readTextFile(":/text/region_map_default_emerald.json");
json = poryjson::Json::parse(emeraldDefault);
}
void buildRubyDefaults(poryjson::Json &json) {
ParseUtil parser;
QString emeraldDefault = parser.readTextFile(":/text/region_map_default_ruby.json");
json = poryjson::Json::parse(emeraldDefault);
}
void buildFireredDefaults(poryjson::Json &json) {
ParseUtil parser;
QString fireredDefault = parser.readTextFile(":/text/region_map_default_firered.json");
json = poryjson::Json::parse(fireredDefault);
}
poryjson::Json RegionMapEditor::buildDefaultJson() {
poryjson::Json defaultJson;
switch (projectConfig.baseGameVersion) {
case BaseGameVersion::pokeemerald:
buildEmeraldDefaults(defaultJson);
break;
case BaseGameVersion::pokeruby:
buildRubyDefaults(defaultJson);
break;
case BaseGameVersion::pokefirered:
buildFireredDefaults(defaultJson);
break;
default:
break;
}
return defaultJson;
static const QMap<BaseGame::Version, QString> versionToJsonPath = {
{BaseGame::Version::pokeemerald, QStringLiteral(":/text/region_map_default_emerald.json")},
{BaseGame::Version::pokeruby, QStringLiteral(":/text/region_map_default_ruby.json")},
{BaseGame::Version::pokefirered, QStringLiteral(":/text/region_map_default_firered.json")},
};
const QString path = versionToJsonPath.value(projectConfig.baseGameVersion);
if (path.isEmpty()) { Q_ASSERT(false); return OrderedJson::object(); }
return OrderedJson::parse(ParseUtil::readTextFile(path));
}
bool RegionMapEditor::buildConfigDialog() {
@ -283,18 +249,22 @@ bool RegionMapEditor::buildConfigDialog() {
// for sake of convenience, option to just use defaults for each basegame version
// TODO: Each version's default settings should be available regardless of the base game version,
// it can just suggest which default settings to use depending on the base game version.
QPushButton *config_useProjectDefault = nullptr;
switch (projectConfig.baseGameVersion) {
case BaseGameVersion::pokefirered:
case BaseGame::Version::pokefirered:
config_useProjectDefault = new QPushButton("\nUse pokefirered defaults\n");
break;
case BaseGameVersion::pokeemerald:
case BaseGame::Version::pokeemerald:
config_useProjectDefault = new QPushButton("\nUse pokeemerald defaults\n");
break;
case BaseGameVersion::pokeruby:
case BaseGame::Version::pokeruby:
config_useProjectDefault = new QPushButton("\nUse pokeruby defaults\n");
break;
default:
config_useProjectDefault = new QPushButton("\nNo default settings available\n");
config_useProjectDefault->setEnabled(false);
break;
}
form.addRow(config_useProjectDefault);
@ -1251,11 +1221,6 @@ void RegionMapEditor::closeEvent(QCloseEvent *event)
} else {
event->accept();
}
porymapConfig.setRegionMapEditorGeometry(
this->saveGeometry(),
this->saveState()
);
}
void RegionMapEditor::on_verticalSlider_Zoom_Map_Image_valueChanged(int val) {

View File

@ -4,6 +4,7 @@
#include "multikeyedit.h"
#include "message.h"
#include "log.h"
#include "eventfilters.h"
#include <QGroupBox>
#include <QFormLayout>
@ -22,6 +23,7 @@ ShortcutsEditor::ShortcutsEditor(QWidget *parent) :
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
installEventFilter(new GeometrySaver(this));
main_container = ui->scrollAreaWidgetContents_Shortcuts;
auto *main_layout = new QVBoxLayout(main_container);
main_layout->setSpacing(12);

View File

@ -37,7 +37,7 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent)
ActiveWindowFilter *filter = new ActiveWindowFilter(this);
connect(filter, &ActiveWindowFilter::activated, this, &TilesetEditor::onWindowActivated);
this->installEventFilter(filter);
installEventFilter(filter);
setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label);
@ -75,7 +75,7 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent)
initShortcuts();
setMetatileLayerOrientation(porymapConfig.tilesetEditorLayerOrientation);
this->metatileSelector->select(0);
restoreWindowState();
installEventFilter(new GeometrySaver(this));
}
TilesetEditor::~TilesetEditor()
@ -233,7 +233,7 @@ void TilesetEditor::setMetatileLayerOrientation(Qt::Orientation orientation) {
int numTilesWide = Metatile::tileWidth();
int numTilesTall = Metatile::tileHeight();
int numLayers = projectConfig.getNumLayersInMetatile();
int numLayers = Metatile::numLayers();
if (horizontal) {
numTilesWide *= numLayers;
} else {
@ -339,8 +339,6 @@ void TilesetEditor::initSelectedTileItem() {
void TilesetEditor::initShortcuts() {
initExtraShortcuts();
shortcutsConfig.load();
shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
applyUserShortcuts();
}
@ -379,14 +377,6 @@ void TilesetEditor::applyUserShortcuts() {
shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut));
}
void TilesetEditor::restoreWindowState() {
logInfo("Restoring tileset editor geometry from previous session.");
QMap<QString, QByteArray> geometry = porymapConfig.getTilesetEditorGeometry();
this->restoreGeometry(geometry.value("tileset_editor_geometry"));
this->restoreState(geometry.value("tileset_editor_state"));
this->ui->splitter->restoreState(geometry.value("tileset_editor_splitter_state"));
}
void TilesetEditor::onWindowActivated() {
// User may have made layout edits since window was last focused, so update counts
if (this->metatileSelector) {
@ -853,11 +843,6 @@ void TilesetEditor::closeEvent(QCloseEvent *event)
if (event->isAccepted()) {
if (this->paletteEditor) this->paletteEditor->close();
porymapConfig.setTilesetEditorGeometry(
this->saveGeometry(),
this->saveState(),
this->ui->splitter->saveState()
);
}
}
@ -930,7 +915,7 @@ bool TilesetEditor::replaceMetatile(uint16_t metatileId, const Metatile &src, QS
// Update tile usage if any tiles changed
if (this->tileSelector && this->tileSelector->showUnused) {
int numTiles = projectConfig.getNumTilesInMetatile();
int numTiles = qMin(src.tiles.length(), dest->tiles.length());
for (int i = 0; i < numTiles; i++) {
if (src.tiles[i].tileId != dest->tiles[i].tileId) {
this->tileSelector->usedTiles[src.tiles[i].tileId] += 1;
@ -995,7 +980,7 @@ void TilesetEditor::on_actionRedo_triggered() {
void TilesetEditor::on_actionCut_triggered()
{
this->copyMetatile(true);
this->pasteMetatile(Metatile(projectConfig.getNumTilesInMetatile()), "");
this->pasteMetatile(Metatile(Metatile::maxTiles()), "");
}
void TilesetEditor::on_actionCopy_triggered()

View File

@ -3,6 +3,7 @@
#include "ui_updatepromoter.h"
#include "log.h"
#include "config.h"
#include "version.h"
#include <QJsonDocument>
#include <QJsonArray>

View File

@ -4,6 +4,7 @@
#include "config.h"
#include "utility.h"
#include "message.h"
#include "eventfilters.h"
static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts");
@ -25,6 +26,7 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) :
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
setWindowFlags(Qt::Window);
installEventFilter(new GeometrySaver(this));
connect(ui->button_Help, &QAbstractButton::clicked, this, &WildMonChart::showHelpDialog);
@ -49,8 +51,6 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) :
porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText();
}
restoreGeometry(porymapConfig.wildMonChartGeometry);
setTable(table);
};
@ -461,9 +461,4 @@ void WildMonChart::showHelpDialog() {
InfoMessage::show(text, informativeText, this);
}
void WildMonChart::closeEvent(QCloseEvent *event) {
porymapConfig.wildMonChartGeometry = saveGeometry();
QWidget::closeEvent(event);
}
#endif // QT_CHARTS_LIB