diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index 7c5452b1..8673a4a6 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -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 diff --git a/forms/newlayoutdialog.ui b/forms/newlayoutdialog.ui index 980b5351..303e11a1 100644 --- a/forms/newlayoutdialog.ui +++ b/forms/newlayoutdialog.ui @@ -6,8 +6,8 @@ 0 0 - 264 - 173 + 387 + 357 @@ -36,8 +36,8 @@ 0 0 - 240 - 109 + 363 + 293 diff --git a/forms/newlayoutform.ui b/forms/newlayoutform.ui index b3c0649b..86107fb6 100644 --- a/forms/newlayoutform.ui +++ b/forms/newlayoutform.ui @@ -2,6 +2,14 @@ NewLayoutForm + + + 0 + 0 + 196 + 331 + + Form @@ -168,6 +176,9 @@ QComboBox::InsertPolicy::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon + @@ -204,6 +215,9 @@ QComboBox::InsertPolicy::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon + diff --git a/forms/newmapdialog.ui b/forms/newmapdialog.ui index ec8c2612..9479b6dd 100644 --- a/forms/newmapdialog.ui +++ b/forms/newmapdialog.ui @@ -6,8 +6,8 @@ 0 0 - 559 - 614 + 467 + 428 @@ -39,8 +39,8 @@ 0 0 - 535 - 550 + 443 + 364 @@ -129,6 +129,9 @@ QComboBox::InsertPolicy::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon + @@ -171,6 +174,9 @@ QComboBox::InsertPolicy::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon + diff --git a/include/config.h b/include/config.h deleted file mode 100644 index 4907014f..00000000 --- a/include/config.h +++ /dev/null @@ -1,521 +0,0 @@ -#pragma once -#ifndef CONFIG_H -#define CONFIG_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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 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 getMainGeometry(); - QMap getTilesetEditorGeometry(); - QMap getPaletteEditorGeometry(); - QMap getRegionMapEditorGeometry(); - QMap getProjectSettingsEditorGeometry(); - QMap getCustomScriptsEditorGeometry(); - - static QFont defaultMapListFont() { return QFontDatabase::systemFont(QFontDatabase::FixedFont); } - - bool reopenOnLaunch; - bool projectManuallyClosed; - int mapListTab; - bool mapListEditGroupsEnabled; - QMap 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 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 statusBarLogTypes; - QFont applicationFont; - QFont mapListFont; - int imageExportColorSpaceId; - QMap trustedScriptHashes; - -protected: - virtual void parseConfigKeyValue(QString key, QString value) override; - virtual QMap 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> defaultIdentifiers; - static const QMap> 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 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 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 warpBehaviors; - int maxEventsPerGroup; - int forcedMajorVersion; - int metatileSelectorWidth; - QStringList globalConstantsFilepaths; - QMap globalConstants; - -protected: - virtual void parseConfigKeyValue(QString key, QString value) override; - virtual QMap getKeyValueMap() override; - virtual void init() override; - virtual void setUnreadKeys() override; - -private: - QStringList readKeys; - QMap identifiers; - QMap filePaths; - QMap eventIconPaths; - QMap 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 enabled); - QStringList getCustomScriptPaths(); - QList getCustomScriptsEnabled(); - - QString recentMapOrLayout; - bool useEncounterJson; - -protected: - virtual void parseConfigKeyValue(QString key, QString value) override; - virtual QMap getKeyValueMap() override; - virtual void init() override; - virtual void setUnreadKeys() override; -#ifdef CONFIG_BACKWARDS_COMPATABILITY - friend class ProjectConfig; -#endif - -private: - QStringList readKeys; - QMap 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 defaultShortcuts(const QObject *object) const; - - void setUserShortcuts(const QObjectList &objects); - void setUserShortcuts(const QMultiMap &objects_keySequences); - QList userShortcuts(const QObject *object) const; - -protected: - virtual void parseConfigKeyValue(QString key, QString value) override; - virtual QMap getKeyValueMap() override; - virtual void init() override { }; - virtual void setUnreadKeys() override { }; - -private: - QMultiMap user_shortcuts; - QMultiMap default_shortcuts; - - enum StoreType { - User, - Default - }; - - QString cfgKey(const QObject *object) const; - QList currentShortcuts(const QObject *object) const; - - void storeShortcutsFromList(StoreType storeType, const QObjectList &objects); - void storeShortcuts( - StoreType storeType, - const QString &cfgKey, - const QList &keySequences); -}; - -extern ShortcutsConfig shortcutsConfig; - -#endif // CONFIG_H diff --git a/include/config/config.h b/include/config/config.h new file mode 100644 index 00000000..723f59cc --- /dev/null +++ b/include/config/config.h @@ -0,0 +1,5 @@ +#pragma once +#include "porymapconfig.h" +#include "projectconfig.h" +#include "userconfig.h" +#include "shortcutsconfig.h" diff --git a/include/config/keyvalueconfigbase.h b/include/config/keyvalueconfigbase.h new file mode 100644 index 00000000..f7db0d96 --- /dev/null +++ b/include/config/keyvalueconfigbase.h @@ -0,0 +1,48 @@ +#pragma once +#ifndef KEYVALUECONFIGBASE_H +#define KEYVALUECONFIGBASE_H + +#include +#include + +#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 diff --git a/include/config/porymapconfig.h b/include/config/porymapconfig.h new file mode 100644 index 00000000..3e1bf5d7 --- /dev/null +++ b/include/config/porymapconfig.h @@ -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 +#include +#include +#include + +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 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 rateLimitTimes; + QGraphicsPixmapItem::ShapeMode eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; + bool shownInGameReloadMessage = false; + GridSettings gridSettings; + OrderedSet 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 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 imageExportColorSpace = {}; +#endif + QMap trustedScriptHashes; + + FieldManager* getFieldManager() override { + if (!m_fm) { + m_fm = std::make_shared(); + 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 m_fm = nullptr; + QStringList recentProjects; + QMap savedGeometryMap; + int geometryVersion = 0; +}; + +extern PorymapConfig porymapConfig; + +#endif // PORYMAPCONFIG_H diff --git a/include/config/projectconfig.h b/include/config/projectconfig.h new file mode 100644 index 00000000..8e5e1f8d --- /dev/null +++ b/include/config/projectconfig.h @@ -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> defaultIdentifiers; + static const QMap> 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 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 warpBehaviors; + int maxEventsPerGroup = 255; + int metatileSelectorWidth = 8; + QStringList globalConstantsFilepaths; + QMap globalConstants; + QList customScripts; + QMap eventIconPaths; + QMap pokemonIconPaths; + QVersionNumber minimumVersion; + + FieldManager* getFieldManager() override { + if (!m_fm) { + m_fm = std::make_shared(); + 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(&this->defaultMetatileId, "default_metatile_id", 0, Block::MaxValue); + m_fm->addField(&this->defaultElevation, "default_elevation", 0, Block::MaxValue); + m_fm->addField(&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(&this->blockMetatileIdMask, "block_metatile_id_mask", 0, Block::MaxValue); + m_fm->addField(&this->blockCollisionMask, "block_collision_mask", 0, Block::MaxValue); + m_fm->addField(&this->blockElevationMask, "block_elevation_mask", 0, Block::MaxValue); + m_fm->addField(&this->unusedTileNormal, "unused_tile_normal", 0, Tile::MaxValue); + m_fm->addField(&this->unusedTileCovered, "unused_tile_covered", 0, Tile::MaxValue); + m_fm->addField(&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 m_fm = nullptr; + QMap identifiers; + QMap filePaths; +}; + +extern ProjectConfig projectConfig; + +#endif // PROJECTCONFIG_H diff --git a/include/config/shortcutsconfig.h b/include/config/shortcutsconfig.h new file mode 100644 index 00000000..5cf67348 --- /dev/null +++ b/include/config/shortcutsconfig.h @@ -0,0 +1,55 @@ +#pragma once +#ifndef SHORTCUTSCONFIG_H +#define SHORTCUTSCONFIG_H + +#include "keyvalueconfigbase.h" + +#include + +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 defaultShortcuts(const QObject *object) const; + + void setUserShortcuts(const QObjectList& objects); + void setUserShortcuts(const QMultiMap& objects_keySequences); + QList userShortcuts(const QObject *object) const; + +protected: + virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; + virtual QJsonObject getDefaultJson() const override; + +private: + QMultiMap user_shortcuts; + QMultiMap default_shortcuts; + + enum StoreType { + User, + Default + }; + + QString cfgKey(const QObject *object) const; + QList currentShortcuts(const QObject *object) const; + + void storeShortcutsFromList(StoreType storeType, const QObjectList& objects); + void storeShortcuts( + StoreType storeType, + const QString& cfgKey, + const QList& keySequences); +}; + +extern ShortcutsConfig shortcutsConfig; + +#endif // SHORTCUTSCONFIG_H diff --git a/include/config/userconfig.h b/include/config/userconfig.h new file mode 100644 index 00000000..01433fc1 --- /dev/null +++ b/include/config/userconfig.h @@ -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 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(); + 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 m_fm = nullptr; +}; + +extern UserConfig userConfig; + +#endif // USERCONFIG_H diff --git a/include/core/basegame.h b/include/core/basegame.h new file mode 100644 index 00000000..06732847 --- /dev/null +++ b/include/core/basegame.h @@ -0,0 +1,22 @@ +#pragma once +#ifndef BASEGAMEVERSION_H +#define BASEGAMEVERSION_H + +#include +#include + +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 diff --git a/include/core/block.h b/include/core/block.h index 94c674c3..5735add1 100644 --- a/include/core/block.h +++ b/include/core/block.h @@ -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; diff --git a/include/core/converter.h b/include/core/converter.h new file mode 100644 index 00000000..5a5e86b5 --- /dev/null +++ b/include/core/converter.h @@ -0,0 +1,401 @@ +#pragma once +#ifndef CONVERTER_H +#define CONVERTER_H + +#include +#include +#include +#include +#include + +#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::toJson(size); + QSize sameSize = Converter::fromJson(json); + + + ## Adding a new conversion ## + To add a new type conversion, add a new 'Converter' template: + + template <> + struct Converter : DefaultConverter { + // 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 +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()) { + 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(); + } + + // 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 +struct Converter : DefaultConverter {}; + +// 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 +struct DefaultStringConverter : DefaultConverter { + static QJsonValue toJson(const T& value) { + return Converter::toJson(Converter::toString(value)); + } + static T fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto string = Converter::fromJson(json, errors); + return Converter::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 : DefaultStringConverter {}; + +template <> +struct Converter : DefaultStringConverter {}; + +template <> +struct Converter : DefaultConverter { + // Constructing a QJsonValue from uint32_t is ambiguous, so we need an explicit cast. + static QJsonValue toJson(uint32_t value) { + return QJsonValue{static_cast(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 +struct Converter>> : DefaultStringConverter { + 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(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(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::fromString(json.toString()); + auto value = Converter::fromJson(json, errors); + auto e = magic_enum::enum_cast(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(0); + } + return e.value(); + } +}; + +template <> +struct Converter : DefaultStringConverter { + static QVersionNumber fromString(const QString& string, QStringList* = nullptr) { + return QVersionNumber::fromString(string); + } +}; + +template <> +struct Converter : DefaultStringConverter { + 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 : DefaultStringConverter { + 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 : DefaultStringConverter { + 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 : DefaultStringConverter { + 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 +struct Converter> : DefaultConverter> { + static QJsonValue toJson(const std::optional& optional) { + return optional.has_value() ? Converter::toJson(optional.value()) : QJsonValue(); + } + static std::optional fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + if (json.isNull()) return {}; + return Converter::fromJson(json, errors); + } +}; + +template +struct ListConverter : DefaultConverter> { + static QJsonValue toJson(const QList& list) { + QJsonArray arr; + for (auto& elem : list) arr.append(Converter::toJson(elem)); + return arr; + } + static QList fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto arr = Converter::fromJson(json, errors); + QList list; + for (const auto& elem : arr) list.append(Converter::fromJson(elem, errors)); + return list; + } +}; + +template +struct Converter> : ListConverter {}; + +// Only needed for Qt5 +template <> +struct Converter : ListConverter {}; + +template +struct Converter> : DefaultConverter> { + static QJsonObject toJson(const QMap& map) { + QJsonObject obj; + for (auto it = map.begin(); it != map.end(); it++) { + const QString key = Converter::toString(it.key()); + obj[key] = Converter::toJson(it.value()); + } + return obj; + } + static QMap fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto obj = Converter::fromJson(json, errors); + QMap map; + for (auto it = obj.begin(); it != obj.end(); it++) { + const auto key = Converter::fromString(it.key(), errors); + map.insert(key, Converter::fromJson(it.value(), errors)); + } + return map; + } +}; + +template +struct Converter> : DefaultConverter> { + static QJsonObject toJson(const QMultiMap& map) { + QJsonObject obj; + for (const auto& uniqueKey : map.uniqueKeys()) { + const QString key = Converter::toString(uniqueKey); + obj[key] = Converter>::toJson(map.values(uniqueKey)); + } + return obj; + } + static QMultiMap fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto obj = Converter::fromJson(json, errors); + QMultiMap map; + for (auto it = obj.begin(); it != obj.end(); it++) { + const auto key = Converter::fromString(it.key(), errors); + const auto values = Converter>::fromJson(it.value(), errors); + for (const auto& value : values) map.insert(key, value); + } + return map; + } +}; + +template +struct Converter> : DefaultConverter> { + static QJsonValue toJson(const OrderedSet& set) { + QJsonArray arr; + for (auto& elem : set) arr.append(Converter::toJson(elem)); + return arr; + } + static OrderedSet fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto arr = Converter::fromJson(json, errors); + OrderedSet set; + for (const auto& elem : arr) set.insert(Converter::fromJson(elem, errors)); + return set; + } +}; + +template <> +struct Converter : DefaultConverter { + 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::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 : DefaultConverter { + 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::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 : DefaultConverter { + 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::fromJson(json, errors); + ScriptSettings settings; + settings.path = obj.value("path").toString(); + settings.enabled = obj.value("enabled").toBool(); + return settings; + } +}; + +template <> +struct Converter : DefaultConverter { + static QJsonValue toJson(const GridSettings& value) { + return value.toJson(); + } + static GridSettings fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto obj = Converter::fromJson(json, errors); + return GridSettings::fromJson(obj); + } +}; + +template <> +struct Converter : DefaultConverter { + static QJsonValue toJson(const QByteArray& value) { + return QString::fromLocal8Bit(value.toBase64()); + } + static QByteArray fromJson(const QJsonValue& json, QStringList* errors = nullptr) { + const auto s = Converter::fromJson(json, errors); + return QByteArray::fromBase64(s.toLocal8Bit()); + } +}; + +#endif // CONVERTER_H diff --git a/include/core/fieldmanager.h b/include/core/fieldmanager.h new file mode 100644 index 00000000..ed2fcccd --- /dev/null +++ b/include/core/fieldmanager.h @@ -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 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 +class BasicFieldInterface : public FieldInterface { +public: + BasicFieldInterface(T* field) : m_field(field) { + Q_ASSERT(m_field); + }; + virtual ~BasicFieldInterface() {}; + virtual QJsonValue get() const override {return Converter::toJson(*m_field);} + virtual QStringList set(const QJsonValue& json) const override { + QStringList errors; + auto value = Converter::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 +static FieldInterface* makeFieldInterface(T* field) { + return new BasicFieldInterface(field); +} + +// Create a regular FieldInterface, but override 'set' to use the given min/max. +template +static FieldInterface* makeFieldInterface(T* field, const T& min, const T& max) { + class BoundedFieldInterface : public BasicFieldInterface { + public: + BoundedFieldInterface(T* field, const T& min, const T& max) + : BasicFieldInterface(field), m_min(min), m_max(max) {}; + virtual ~BoundedFieldInterface() {}; + virtual QStringList set(const QJsonValue& json) const override { + QStringList errors; + auto value = Converter::fromJson(json, &errors); + if (errors.isEmpty()) { // Don't bother changing the value if conversion failed + value = Converter::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 +static FieldInterface* makeFieldInterface(T* field, const QList& acceptableValues) { + Q_ASSERT(!acceptableValues.isEmpty()); + class BoundedFieldInterface : public BasicFieldInterface { + public: + BoundedFieldInterface(T* field, const QList& acceptableValues) + : BasicFieldInterface(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::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 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 + void addField(T* field, const QString& key) { + Q_ASSERT(!m_fields.contains(key)); + m_fields.insert(key, makeFieldInterface(field)); + } + template + void addField(T* field, const QString& key, const T& min, const T& max) { + Q_ASSERT(!m_fields.contains(key)); + m_fields.insert(key, makeFieldInterface(field, min, max)); + } + template + void addField(T* field, const QString& key, const QList& acceptableValues) { + Q_ASSERT(!m_fields.contains(key)); + m_fields.insert(key, makeFieldInterface(field, acceptableValues)); + } + +private: + QHash m_fields; +}; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index 3095574b..1e9f6030 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -8,6 +8,7 @@ #include #include #include +#include class Map; class LayoutPixmapItem; diff --git a/include/core/metatile.h b/include/core/metatile.h index eba4afed..250e3fa4 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -3,11 +3,13 @@ #define METATILE_H #include "tile.h" -#include "config.h" +#include "basegame.h" #include "bitpacker.h" #include #include #include +#include +#include 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 &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()); } diff --git a/include/core/orderedset.h b/include/core/orderedset.h new file mode 100644 index 00000000..8f5bb185 --- /dev/null +++ b/include/core/orderedset.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef ORDERED_SET_H +#define ORDERED_SET_H + +#include +#include + +template +class OrderedSet : public std::set +{ + using std::set::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 toQSet() const { + return QSet(this->begin(), this->end()); + } + static QSet fromQSet(const QSet& set) { + return OrderedSet(set.begin(), set.end()); + } + bool isEmpty() const { + return this->empty(); + } +}; + +#endif // ORDERED_SET_H diff --git a/include/core/regionmap.h b/include/core/regionmap.h index c8afb1b2..0fb81add 100644 --- a/include/core/regionmap.h +++ b/include/core/regionmap.h @@ -5,6 +5,7 @@ #include "map.h" #include "tilemaptileselector.h" #include "history.h" +#include "config.h" #include #include diff --git a/include/core/scriptsettings.h b/include/core/scriptsettings.h new file mode 100644 index 00000000..dd8e4f4f --- /dev/null +++ b/include/core/scriptsettings.h @@ -0,0 +1,27 @@ +#pragma once +#ifndef SCRIPTSETTINGS_H +#define SCRIPTSETTINGS_H + +#include +#include + +// 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& scripts) { + QStringList paths; + for (auto& script : scripts) { + if (script.enabled) paths.append(script.path); + } + return paths; + } +}; + +#endif // SCRIPTSETTINGS_H diff --git a/include/core/tile.h b/include/core/tile.h index b4960dc9..81a1dd02 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -5,9 +5,6 @@ #include #include -// 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; } diff --git a/include/core/utility.h b/include/core/utility.h index ccb3cf24..40dd1423 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -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, erases all entries with empty strings. + // Returns the number of entries erased. + template + int removeEmptyStrings(QMap *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 diff --git a/include/core/version.h b/include/core/version.h new file mode 100644 index 00000000..876f6890 --- /dev/null +++ b/include/core/version.h @@ -0,0 +1,9 @@ +#pragma once +#ifndef VERSION_H +#define VERSION_H + +#include + +extern const QVersionNumber porymapVersion; + +#endif // VERSION_H diff --git a/include/lib/magic_enum.hpp b/include/lib/magic_enum.hpp new file mode 100644 index 00000000..7ab8bbf3 --- /dev/null +++ b/include/lib/magic_enum.hpp @@ -0,0 +1,1549 @@ +// __ __ _ ______ _____ +// | \/ | (_) | ____| / ____|_ _ +// | \ / | __ _ __ _ _ ___ | |__ _ __ _ _ _ __ ___ | | _| |_ _| |_ +// | |\/| |/ _` |/ _` | |/ __| | __| | '_ \| | | | '_ ` _ \ | | |_ _|_ _| +// | | | | (_| | (_| | | (__ | |____| | | | |_| | | | | | | | |____|_| |_| +// |_| |_|\__,_|\__, |_|\___| |______|_| |_|\__,_|_| |_| |_| \_____| +// __/ | https://github.com/Neargye/magic_enum +// |___/ version 0.9.7 +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2019 - 2024 Daniil Goncharov . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef NEARGYE_MAGIC_ENUM_HPP +#define NEARGYE_MAGIC_ENUM_HPP + +#define MAGIC_ENUM_VERSION_MAJOR 0 +#define MAGIC_ENUM_VERSION_MINOR 9 +#define MAGIC_ENUM_VERSION_PATCH 7 + +#ifndef MAGIC_ENUM_USE_STD_MODULE +#include +#include +#include +#include +#include +#include +#include +#endif + +#if defined(MAGIC_ENUM_CONFIG_FILE) +# include MAGIC_ENUM_CONFIG_FILE +#endif + +#ifndef MAGIC_ENUM_USE_STD_MODULE +#if !defined(MAGIC_ENUM_USING_ALIAS_OPTIONAL) +# include +#endif +#if !defined(MAGIC_ENUM_USING_ALIAS_STRING) +# include +#endif +#if !defined(MAGIC_ENUM_USING_ALIAS_STRING_VIEW) +# include +#endif +#endif + +#if defined(MAGIC_ENUM_NO_ASSERT) +# define MAGIC_ENUM_ASSERT(...) static_cast(0) +#elif !defined(MAGIC_ENUM_ASSERT) +# include +# define MAGIC_ENUM_ASSERT(...) assert((__VA_ARGS__)) +#endif + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunknown-warning-option" +# pragma clang diagnostic ignored "-Wenum-constexpr-conversion" +# pragma clang diagnostic ignored "-Wuseless-cast" // suppresses 'static_cast('\0')' for char_type = char (common on Linux). +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wmaybe-uninitialized" // May be used uninitialized 'return {};'. +# pragma GCC diagnostic ignored "-Wuseless-cast" // suppresses 'static_cast('\0')' for char_type = char (common on Linux). +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 26495) // Variable 'static_str::chars_' is uninitialized. +# pragma warning(disable : 28020) // Arithmetic overflow: Using operator '-' on a 4 byte value and then casting the result to a 8 byte value. +# pragma warning(disable : 26451) // The expression '0<=_Param_(1)&&_Param_(1)<=1-1' is not true at this call. +# pragma warning(disable : 4514) // Unreferenced inline function has been removed. +#endif + +// Checks magic_enum compiler compatibility. +#if defined(__clang__) && __clang_major__ >= 5 || defined(__GNUC__) && __GNUC__ >= 9 || defined(_MSC_VER) && _MSC_VER >= 1910 || defined(__RESHARPER__) +# undef MAGIC_ENUM_SUPPORTED +# define MAGIC_ENUM_SUPPORTED 1 +#endif + +// Checks magic_enum compiler aliases compatibility. +#if defined(__clang__) && __clang_major__ >= 5 || defined(__GNUC__) && __GNUC__ >= 9 || defined(_MSC_VER) && _MSC_VER >= 1920 +# undef MAGIC_ENUM_SUPPORTED_ALIASES +# define MAGIC_ENUM_SUPPORTED_ALIASES 1 +#endif + +// Specify the calling convention for compilers that need it in order to get reliable mangled names under different +// compiler flags. In particular, MSVC allows changing the default calling convention on x86. +#if defined(__clang__) || defined(__GNUC__) +#define MAGIC_ENUM_CALLING_CONVENTION +#elif defined(_MSC_VER) +#define MAGIC_ENUM_CALLING_CONVENTION __cdecl +#endif + +// Enum value must be greater or equals than MAGIC_ENUM_RANGE_MIN. By default MAGIC_ENUM_RANGE_MIN = -128. +// If need another min range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN. +#if !defined(MAGIC_ENUM_RANGE_MIN) +# define MAGIC_ENUM_RANGE_MIN -128 +#endif + +// Enum value must be less or equals than MAGIC_ENUM_RANGE_MAX. By default MAGIC_ENUM_RANGE_MAX = 127. +// If need another max range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MAX. +#if !defined(MAGIC_ENUM_RANGE_MAX) +# define MAGIC_ENUM_RANGE_MAX 127 +#endif + +// Improve ReSharper C++ intellisense performance with builtins, avoiding unnecessary template instantiations. +#if defined(__RESHARPER__) +# undef MAGIC_ENUM_GET_ENUM_NAME_BUILTIN +# undef MAGIC_ENUM_GET_TYPE_NAME_BUILTIN +# if __RESHARPER__ >= 20230100 +# define MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V) __rscpp_enumerator_name(V) +# define MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(T) __rscpp_type_name() +# else +# define MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V) nullptr +# define MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(T) nullptr +# endif +#endif + +namespace magic_enum { + +// If need another optional type, define the macro MAGIC_ENUM_USING_ALIAS_OPTIONAL. +#if defined(MAGIC_ENUM_USING_ALIAS_OPTIONAL) +MAGIC_ENUM_USING_ALIAS_OPTIONAL +#else +using std::optional; +#endif + +// If need another string_view type, define the macro MAGIC_ENUM_USING_ALIAS_STRING_VIEW. +#if defined(MAGIC_ENUM_USING_ALIAS_STRING_VIEW) +MAGIC_ENUM_USING_ALIAS_STRING_VIEW +#else +using std::string_view; +#endif + +// If need another string type, define the macro MAGIC_ENUM_USING_ALIAS_STRING. +#if defined(MAGIC_ENUM_USING_ALIAS_STRING) +MAGIC_ENUM_USING_ALIAS_STRING +#else +using std::string; +#endif + +using char_type = string_view::value_type; +static_assert(std::is_same_v, "magic_enum::customize requires same string_view::value_type and string::value_type"); +static_assert([] { + if constexpr (std::is_same_v) { + constexpr const char c[] = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789|"; + constexpr const wchar_t wc[] = L"abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789|"; + static_assert(std::size(c) == std::size(wc), "magic_enum::customize identifier characters are multichars in wchar_t."); + + for (std::size_t i = 0; i < std::size(c); ++i) { + if (c[i] != wc[i]) { + return false; + } + } + } + return true; +} (), "magic_enum::customize wchar_t is not compatible with ASCII."); + +namespace customize { + template + struct enum_range; +} + +namespace detail { + template + constexpr inline std::size_t prefix_length_or_zero = 0; + + template + constexpr inline auto prefix_length_or_zero::prefix_length)>> = std::size_t{customize::enum_range::prefix_length}; +} + +namespace customize { + +template +struct adl_info_holder { + constexpr static int max = Max; + constexpr static int min = Min; + constexpr static bool is_flags = IsFlags; + constexpr static std::size_t prefix_length = PrefixLength; + + template + constexpr static adl_info_holder minmax() { return {}; } + + template + constexpr static adl_info_holder flag() { return {}; } + + template + constexpr static adl_info_holder prefix() { return {}; } +}; + +constexpr adl_info_holder<> adl_info() { return {}; } + +// Enum value must be in range [MAGIC_ENUM_RANGE_MIN, MAGIC_ENUM_RANGE_MAX]. By default MAGIC_ENUM_RANGE_MIN = -128, MAGIC_ENUM_RANGE_MAX = 127. +// If need another range for all enum types by default, redefine the macro MAGIC_ENUM_RANGE_MIN and MAGIC_ENUM_RANGE_MAX. +// If need another range for specific enum type, add specialization enum_range for necessary enum type. +template +struct enum_range { + static constexpr int min = MAGIC_ENUM_RANGE_MIN; + static constexpr int max = MAGIC_ENUM_RANGE_MAX; +}; + +template +struct enum_range : decltype(magic_enum_define_range_adl(E{})) {}; + +static_assert(MAGIC_ENUM_RANGE_MAX > MAGIC_ENUM_RANGE_MIN, "MAGIC_ENUM_RANGE_MAX must be greater than MAGIC_ENUM_RANGE_MIN."); + +namespace detail { + +enum class customize_tag { + default_tag, + invalid_tag, + custom_tag +}; + +} // namespace magic_enum::customize::detail + +class customize_t : public std::pair { + public: + constexpr customize_t(string_view srt) : std::pair{detail::customize_tag::custom_tag, srt} {} + constexpr customize_t(const char_type* srt) : customize_t{string_view{srt}} {} + constexpr customize_t(detail::customize_tag tag) : std::pair{tag, string_view{}} { + MAGIC_ENUM_ASSERT(tag != detail::customize_tag::custom_tag); + } +}; + +// Default customize. +inline constexpr auto default_tag = customize_t{detail::customize_tag::default_tag}; +// Invalid customize. +inline constexpr auto invalid_tag = customize_t{detail::customize_tag::invalid_tag}; + +// If need custom names for enum, add specialization enum_name for necessary enum type. +template +constexpr customize_t enum_name(E) noexcept { + return default_tag; +} + +// If need custom type name for enum, add specialization enum_type_name for necessary enum type. +template +constexpr customize_t enum_type_name() noexcept { + return default_tag; +} + +} // namespace magic_enum::customize + +namespace detail { + +template +struct supported +#if defined(MAGIC_ENUM_SUPPORTED) || defined(MAGIC_ENUM_NO_CHECK_SUPPORT) + : std::true_type {}; +#else + : std::false_type {}; +#endif + +template , std::enable_if_t, int> = 0> +using enum_constant = std::integral_constant; + +template +inline constexpr bool always_false_v = false; + +template +struct has_is_flags : std::false_type {}; + +template +struct has_is_flags::is_flags)>> : std::bool_constant::is_flags)>>> {}; + +template +struct range_min : std::integral_constant {}; + +template +struct range_min::min)>> : std::integral_constant::min), customize::enum_range::min> {}; + +template +struct range_max : std::integral_constant {}; + +template +struct range_max::max)>> : std::integral_constant::max), customize::enum_range::max> {}; + +struct str_view { + const char* str_ = nullptr; + std::size_t size_ = 0; +}; + +template +class static_str { + public: + constexpr explicit static_str(str_view str) noexcept : static_str{str.str_, std::make_integer_sequence{}} { + MAGIC_ENUM_ASSERT(str.size_ == N); + } + + constexpr explicit static_str(const char* const str) noexcept : static_str{ str, std::make_integer_sequence{} } { + } + + constexpr explicit static_str(string_view str) noexcept : static_str{str.data(), std::make_integer_sequence{}} { + MAGIC_ENUM_ASSERT(str.size() == N); + } + + constexpr const char_type* data() const noexcept { return chars_; } + + constexpr std::uint16_t size() const noexcept { return N; } + + constexpr string_view str() const noexcept { return string_view(data(), size()); } + + private: + template + constexpr static_str(const char* str, std::integer_sequence) noexcept : chars_{static_cast(str[I])..., static_cast('\0')} {} + + template + constexpr static_str(string_view str, std::integer_sequence) noexcept : chars_{str[I]..., static_cast('\0')} {} + + char_type chars_[static_cast(N) + 1]; +}; + +template <> +class static_str<0> { + public: + constexpr static_str() noexcept = default; + + constexpr static_str(str_view) noexcept {} + + constexpr static_str(string_view) noexcept {} + + constexpr const char_type* data() const noexcept { return chars_; } + + constexpr std::uint16_t size() const noexcept { return 0; } + + constexpr string_view str() const noexcept { return string_view(data(), size()); } + +private: + static constexpr char_type chars_[1] = {}; +}; + +template > +class case_insensitive { + static constexpr char_type to_lower(char_type c) noexcept { + return (c >= static_cast('A') && c <= static_cast('Z')) ? static_cast(c + (static_cast('a') - static_cast('A'))) : c; + } + + public: + template + constexpr auto operator()(L lhs, R rhs) const noexcept -> std::enable_if_t, char_type> && std::is_same_v, char_type>, bool> { + return Op{}(to_lower(lhs), to_lower(rhs)); + } +}; + +constexpr std::size_t find(string_view str, char_type c) noexcept { +#if defined(__clang__) && __clang_major__ < 9 && defined(__GLIBCXX__) || defined(_MSC_VER) && _MSC_VER < 1920 && !defined(__clang__) +// https://stackoverflow.com/questions/56484834/constexpr-stdstring-viewfind-last-of-doesnt-work-on-clang-8-with-libstdc +// https://developercommunity.visualstudio.com/content/problem/360432/vs20178-regression-c-failed-in-test.html + constexpr bool workaround = true; +#else + constexpr bool workaround = false; +#endif + + if constexpr (workaround) { + for (std::size_t i = 0; i < str.size(); ++i) { + if (str[i] == c) { + return i; + } + } + + return string_view::npos; + } else { + return str.find(c); + } +} + +template +inline constexpr bool is_default_predicate_v = std::is_same_v, std::equal_to> || std::is_same_v, std::equal_to<>>; + + +template +inline constexpr bool is_nothrow_invocable_v = is_default_predicate_v || std::is_nothrow_invocable_r_v; + + +template +constexpr bool cmp_equal(string_view lhs, string_view rhs, [[maybe_unused]] BinaryPredicate&& p) noexcept(is_nothrow_invocable_v) { +#if defined(_MSC_VER) && _MSC_VER < 1920 && !defined(__clang__) + // https://developercommunity.visualstudio.com/content/problem/360432/vs20178-regression-c-failed-in-test.html + // https://developercommunity.visualstudio.com/content/problem/232218/c-constexpr-string-view.html + constexpr bool workaround = true; +#else + constexpr bool workaround = false; +#endif + + if constexpr (!is_default_predicate_v || workaround) { + if (lhs.size() != rhs.size()) { + return false; + } + + const auto size = lhs.size(); + for (std::size_t i = 0; i < size; ++i) { + if (!p(lhs[i], rhs[i])) { + return false; + } + } + + return true; + } else { + return lhs == rhs; + } +} + +template +constexpr bool cmp_less(L lhs, R rhs) noexcept { + static_assert(std::is_integral_v && std::is_integral_v, "magic_enum::detail::cmp_less requires integral type."); + + if constexpr (std::is_signed_v == std::is_signed_v) { + // If same signedness (both signed or both unsigned). + return lhs < rhs; + } else if constexpr (std::is_same_v) { // bool special case + return static_cast(lhs) < rhs; + } else if constexpr (std::is_same_v) { // bool special case + return lhs < static_cast(rhs); + } else if constexpr (std::is_signed_v) { + // If 'right' is negative, then result is 'false', otherwise cast & compare. + return rhs > 0 && lhs < static_cast>(rhs); + } else { + // If 'left' is negative, then result is 'true', otherwise cast & compare. + return lhs < 0 || static_cast>(lhs) < rhs; + } +} + +template +constexpr I log2(I value) noexcept { + static_assert(std::is_integral_v, "magic_enum::detail::log2 requires integral type."); + + if constexpr (std::is_same_v) { // bool special case + return MAGIC_ENUM_ASSERT(false), value; + } else { + auto ret = I{0}; + for (; value > I{1}; value >>= I{1}, ++ret) {} + + return ret; + } +} + +#if defined(__cpp_lib_array_constexpr) && __cpp_lib_array_constexpr >= 201603L +# define MAGIC_ENUM_ARRAY_CONSTEXPR 1 +#else +template +constexpr std::array, N> to_array(T (&a)[N], std::index_sequence) noexcept { + return {{a[I]...}}; +} +#endif + +template +inline constexpr bool is_enum_v = std::is_enum_v && std::is_same_v>; + +template +constexpr auto MAGIC_ENUM_CALLING_CONVENTION n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + + if constexpr (supported::value) { +#if defined(MAGIC_ENUM_GET_TYPE_NAME_BUILTIN) + constexpr auto name_ptr = MAGIC_ENUM_GET_TYPE_NAME_BUILTIN(E); + constexpr auto name = name_ptr ? str_view{name_ptr, std::char_traits::length(name_ptr)} : str_view{}; +#elif defined(__clang__) + str_view name; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else { + name.size_ = sizeof(__PRETTY_FUNCTION__) - 36; + name.str_ = __PRETTY_FUNCTION__ + 34; + } +#elif defined(__GNUC__) + auto name = str_view{__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 1}; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else if (name.str_[name.size_ - 1] == ']') { + name.size_ -= 50; + name.str_ += 49; + } else { + name.size_ -= 40; + name.str_ += 37; + } +#elif defined(_MSC_VER) + // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284). + str_view name; + name.str_ = __FUNCSIG__; + name.str_ += 40; + name.size_ += sizeof(__FUNCSIG__) - 57; +#else + auto name = str_view{}; +#endif + std::size_t p = 0; + for (std::size_t i = name.size_; i > 0; --i) { + if (name.str_[i] == ':') { + p = i + 1; + break; + } + } + if (p > 0) { + name.size_ -= p; + name.str_ += p; + } + return name; + } else { + return str_view{}; // Unsupported compiler or Invalid customize. + } +} + +template +constexpr auto type_name() noexcept { + [[maybe_unused]] constexpr auto custom = customize::enum_type_name(); + static_assert(std::is_same_v, customize::customize_t>, "magic_enum::customize requires customize_t type."); + if constexpr (custom.first == customize::detail::customize_tag::custom_tag) { + constexpr auto name = custom.second; + static_assert(!name.empty(), "magic_enum::customize requires not empty string."); + return static_str{name}; + } else if constexpr (custom.first == customize::detail::customize_tag::invalid_tag) { + return static_str<0>{}; + } else if constexpr (custom.first == customize::detail::customize_tag::default_tag) { + constexpr auto name = n(); + return static_str{name}; + } else { + static_assert(always_false_v, "magic_enum::customize invalid."); + } +} + +template +inline constexpr auto type_name_v = type_name(); + +template +constexpr auto MAGIC_ENUM_CALLING_CONVENTION n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + + if constexpr (supported::value) { +#if defined(MAGIC_ENUM_GET_ENUM_NAME_BUILTIN) + constexpr auto name_ptr = MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V); + auto name = name_ptr ? str_view{name_ptr, std::char_traits::length(name_ptr)} : str_view{}; +#elif defined(__clang__) + str_view name; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else { + name.size_ = sizeof(__PRETTY_FUNCTION__) - 36; + name.str_ = __PRETTY_FUNCTION__ + 34; + } + if (name.size_ > 22 && name.str_[0] == '(' && name.str_[1] == 'a' && name.str_[10] == ' ' && name.str_[22] == ':') { + name.size_ -= 23; + name.str_ += 23; + } + if (name.str_[0] == '(' || name.str_[0] == '-' || (name.str_[0] >= '0' && name.str_[0] <= '9')) { + name = str_view{}; + } +#elif defined(__GNUC__) + auto name = str_view{__PRETTY_FUNCTION__, sizeof(__PRETTY_FUNCTION__) - 1}; + if constexpr (sizeof(__PRETTY_FUNCTION__) == sizeof(__FUNCTION__)) { + static_assert(always_false_v, "magic_enum::detail::n requires __PRETTY_FUNCTION__."); + return str_view{}; + } else if (name.str_[name.size_ - 1] == ']') { + name.size_ -= 55; + name.str_ += 54; + } else { + name.size_ -= 40; + name.str_ += 37; + } + if (name.str_[0] == '(') { + name = str_view{}; + } +#elif defined(_MSC_VER) + str_view name; + if ((__FUNCSIG__[5] == '_' && __FUNCSIG__[35] != '(') || (__FUNCSIG__[5] == 'c' && __FUNCSIG__[41] != '(')) { + // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284). + name.str_ = __FUNCSIG__; + name.str_ += 35; + name.size_ = sizeof(__FUNCSIG__) - 52; + } +#else + auto name = str_view{}; +#endif + std::size_t p = 0; + for (std::size_t i = name.size_; i > 0; --i) { + if (name.str_[i] == ':') { + p = i + 1; + break; + } + } + if (p > 0) { + name.size_ -= p; + name.str_ += p; + } + return name; + } else { + return str_view{}; // Unsupported compiler or Invalid customize. + } +} + +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1920 +# define MAGIC_ENUM_VS_2017_WORKAROUND 1 +#endif + +#if defined(MAGIC_ENUM_VS_2017_WORKAROUND) +template +constexpr auto MAGIC_ENUM_CALLING_CONVENTION n() noexcept { + static_assert(is_enum_v, "magic_enum::detail::n requires enum type."); + +# if defined(MAGIC_ENUM_GET_ENUM_NAME_BUILTIN) + constexpr auto name_ptr = MAGIC_ENUM_GET_ENUM_NAME_BUILTIN(V); + auto name = name_ptr ? str_view{name_ptr, std::char_traits::length(name_ptr)} : str_view{}; +# else + // CLI/C++ workaround (see https://github.com/Neargye/magic_enum/issues/284). + str_view name; + name.str_ = __FUNCSIG__; + name.size_ = sizeof(__FUNCSIG__) - 17; + std::size_t p = 0; + for (std::size_t i = name.size_; i > 0; --i) { + if (name.str_[i] == ',' || name.str_[i] == ':') { + p = i + 1; + break; + } + } + if (p > 0) { + name.size_ -= p; + name.str_ += p; + } + if (name.str_[0] == '(' || name.str_[0] == '-' || (name.str_[0] >= '0' && name.str_[0] <= '9')) { + name = str_view{}; + } + return name; +# endif +} +#endif + +template +constexpr auto enum_name() noexcept { + [[maybe_unused]] constexpr auto custom = customize::enum_name(V); + static_assert(std::is_same_v, customize::customize_t>, "magic_enum::customize requires customize_t type."); + if constexpr (custom.first == customize::detail::customize_tag::custom_tag) { + constexpr auto name = custom.second; + static_assert(!name.empty(), "magic_enum::customize requires not empty string."); + return static_str{name}; + } else if constexpr (custom.first == customize::detail::customize_tag::invalid_tag) { + return static_str<0>{}; + } else if constexpr (custom.first == customize::detail::customize_tag::default_tag) { +#if defined(MAGIC_ENUM_VS_2017_WORKAROUND) + constexpr auto name = n(); +#else + constexpr auto name = n(); +#endif + return static_str>{name.str_ + prefix_length_or_zero}; + } else { + static_assert(always_false_v, "magic_enum::customize invalid."); + } +} + +template +inline constexpr auto enum_name_v = enum_name(); + +// CWG1766: Values outside the range of the values of an enumeration +// https://reviews.llvm.org/D130058, https://reviews.llvm.org/D131307 +#if defined(__clang__) && __clang_major__ >= 16 +template +inline constexpr bool is_enum_constexpr_static_cast_valid = false; +template +inline constexpr bool is_enum_constexpr_static_cast_valid(V)>>> = true; +#else +template +inline constexpr bool is_enum_constexpr_static_cast_valid = true; +#endif + +template +constexpr bool is_valid() noexcept { + if constexpr (is_enum_constexpr_static_cast_valid) { + constexpr E v = static_cast(V); + [[maybe_unused]] constexpr auto custom = customize::enum_name(v); + static_assert(std::is_same_v, customize::customize_t>, "magic_enum::customize requires customize_t type."); + if constexpr (custom.first == customize::detail::customize_tag::custom_tag) { + constexpr auto name = custom.second; + static_assert(!name.empty(), "magic_enum::customize requires not empty string."); + return name.size() != 0; + } else if constexpr (custom.first == customize::detail::customize_tag::default_tag) { +#if defined(MAGIC_ENUM_VS_2017_WORKAROUND) + return n().size_ != 0; +#else + return n().size_ != 0; +#endif + } else { + return false; + } + } else { + return false; + } +} + +enum class enum_subtype { + common, + flags +}; + +template > +constexpr U ualue(std::size_t i) noexcept { + if constexpr (std::is_same_v) { // bool special case + static_assert(O == 0, "magic_enum::detail::ualue requires valid offset."); + + return static_cast(i); + } else if constexpr (S == enum_subtype::flags) { + return static_cast(U{1} << static_cast(static_cast(i) + O)); + } else { + return static_cast(static_cast(i) + O); + } +} + +template > +constexpr E value(std::size_t i) noexcept { + return static_cast(ualue(i)); +} + +template > +constexpr int reflected_min() noexcept { + if constexpr (S == enum_subtype::flags) { + return 0; + } else { + constexpr auto lhs = range_min::value; + constexpr auto rhs = (std::numeric_limits::min)(); + + if constexpr (cmp_less(rhs, lhs)) { + return lhs; + } else { + return rhs; + } + } +} + +template > +constexpr int reflected_max() noexcept { + if constexpr (S == enum_subtype::flags) { + return std::numeric_limits::digits - 1; + } else { + constexpr auto lhs = range_max::value; + constexpr auto rhs = (std::numeric_limits::max)(); + + if constexpr (cmp_less(lhs, rhs)) { + return lhs; + } else { + return rhs; + } + } +} + +#define MAGIC_ENUM_FOR_EACH_256(T) \ + T( 0)T( 1)T( 2)T( 3)T( 4)T( 5)T( 6)T( 7)T( 8)T( 9)T( 10)T( 11)T( 12)T( 13)T( 14)T( 15)T( 16)T( 17)T( 18)T( 19)T( 20)T( 21)T( 22)T( 23)T( 24)T( 25)T( 26)T( 27)T( 28)T( 29)T( 30)T( 31) \ + T( 32)T( 33)T( 34)T( 35)T( 36)T( 37)T( 38)T( 39)T( 40)T( 41)T( 42)T( 43)T( 44)T( 45)T( 46)T( 47)T( 48)T( 49)T( 50)T( 51)T( 52)T( 53)T( 54)T( 55)T( 56)T( 57)T( 58)T( 59)T( 60)T( 61)T( 62)T( 63) \ + T( 64)T( 65)T( 66)T( 67)T( 68)T( 69)T( 70)T( 71)T( 72)T( 73)T( 74)T( 75)T( 76)T( 77)T( 78)T( 79)T( 80)T( 81)T( 82)T( 83)T( 84)T( 85)T( 86)T( 87)T( 88)T( 89)T( 90)T( 91)T( 92)T( 93)T( 94)T( 95) \ + T( 96)T( 97)T( 98)T( 99)T(100)T(101)T(102)T(103)T(104)T(105)T(106)T(107)T(108)T(109)T(110)T(111)T(112)T(113)T(114)T(115)T(116)T(117)T(118)T(119)T(120)T(121)T(122)T(123)T(124)T(125)T(126)T(127) \ + T(128)T(129)T(130)T(131)T(132)T(133)T(134)T(135)T(136)T(137)T(138)T(139)T(140)T(141)T(142)T(143)T(144)T(145)T(146)T(147)T(148)T(149)T(150)T(151)T(152)T(153)T(154)T(155)T(156)T(157)T(158)T(159) \ + T(160)T(161)T(162)T(163)T(164)T(165)T(166)T(167)T(168)T(169)T(170)T(171)T(172)T(173)T(174)T(175)T(176)T(177)T(178)T(179)T(180)T(181)T(182)T(183)T(184)T(185)T(186)T(187)T(188)T(189)T(190)T(191) \ + T(192)T(193)T(194)T(195)T(196)T(197)T(198)T(199)T(200)T(201)T(202)T(203)T(204)T(205)T(206)T(207)T(208)T(209)T(210)T(211)T(212)T(213)T(214)T(215)T(216)T(217)T(218)T(219)T(220)T(221)T(222)T(223) \ + T(224)T(225)T(226)T(227)T(228)T(229)T(230)T(231)T(232)T(233)T(234)T(235)T(236)T(237)T(238)T(239)T(240)T(241)T(242)T(243)T(244)T(245)T(246)T(247)T(248)T(249)T(250)T(251)T(252)T(253)T(254)T(255) + +template +constexpr void valid_count(bool* valid, std::size_t& count) noexcept { +#define MAGIC_ENUM_V(O) \ + if constexpr ((I + O) < Size) { \ + if constexpr (is_valid(I + O)>()) { \ + valid[I + O] = true; \ + ++count; \ + } \ + } + + MAGIC_ENUM_FOR_EACH_256(MAGIC_ENUM_V) + + if constexpr ((I + 256) < Size) { + valid_count(valid, count); + } +#undef MAGIC_ENUM_V +} + +template +struct valid_count_t { + std::size_t count = 0; + bool valid[N] = {}; +}; + +template +constexpr auto valid_count() noexcept { + valid_count_t vc; + valid_count(vc.valid, vc.count); + return vc; +} + +template +constexpr auto values() noexcept { + constexpr auto vc = valid_count(); + + if constexpr (vc.count > 0) { +#if defined(MAGIC_ENUM_ARRAY_CONSTEXPR) + std::array values = {}; +#else + E values[vc.count] = {}; +#endif + for (std::size_t i = 0, v = 0; v < vc.count; ++i) { + if (vc.valid[i]) { + values[v++] = value(i); + } + } +#if defined(MAGIC_ENUM_ARRAY_CONSTEXPR) + return values; +#else + return to_array(values, std::make_index_sequence{}); +#endif + } else { + return std::array{}; + } +} + +template > +constexpr auto values() noexcept { + constexpr auto min = reflected_min(); + constexpr auto max = reflected_max(); + constexpr auto range_size = max - min + 1; + static_assert(range_size > 0, "magic_enum::enum_range requires valid size."); + + return values(); +} + +template > +constexpr enum_subtype subtype(std::true_type) noexcept { + if constexpr (std::is_same_v) { // bool special case + return enum_subtype::common; + } else if constexpr (has_is_flags::value) { + return customize::enum_range::is_flags ? enum_subtype::flags : enum_subtype::common; + } else { + return enum_subtype::common; + } +} + +template +constexpr enum_subtype subtype(std::false_type) noexcept { + // For non-enum type return default common subtype. + return enum_subtype::common; +} + +template > +inline constexpr auto subtype_v = subtype(std::is_enum{}); + +template +inline constexpr auto values_v = values(); + +template > +using values_t = decltype((values_v)); + +template +inline constexpr auto count_v = values_v.size(); + +template > +inline constexpr auto min_v = (count_v > 0) ? static_cast(values_v.front()) : U{0}; + +template > +inline constexpr auto max_v = (count_v > 0) ? static_cast(values_v.back()) : U{0}; + +template +constexpr auto names(std::index_sequence) noexcept { + constexpr auto names = std::array{{enum_name_v[I]>.str()...}}; + return names; +} + +template +inline constexpr auto names_v = names(std::make_index_sequence>{}); + +template > +using names_t = decltype((names_v)); + +template +constexpr auto entries(std::index_sequence) noexcept { + constexpr auto entries = std::array, sizeof...(I)>{{{values_v[I], enum_name_v[I]>.str()}...}}; + return entries; +} + +template +inline constexpr auto entries_v = entries(std::make_index_sequence>{}); + +template > +using entries_t = decltype((entries_v)); + +template > +constexpr bool is_sparse() noexcept { + if constexpr (count_v == 0) { + return false; + } else if constexpr (std::is_same_v) { // bool special case + return false; + } else { + constexpr auto max = (S == enum_subtype::flags) ? log2(max_v) : max_v; + constexpr auto min = (S == enum_subtype::flags) ? log2(min_v) : min_v; + constexpr auto range_size = max - min + 1; + + return range_size != count_v; + } +} + +template > +inline constexpr bool is_sparse_v = is_sparse(); + +template +struct is_reflected +#if defined(MAGIC_ENUM_NO_CHECK_REFLECTED_ENUM) + : std::true_type {}; +#else + : std::bool_constant && (count_v != 0)> {}; +#endif + +template +inline constexpr bool is_reflected_v = is_reflected, S>{}; + +template +struct enable_if_enum {}; + +template +struct enable_if_enum { + using type = R; + static_assert(supported::value, "magic_enum unsupported compiler (https://github.com/Neargye/magic_enum#compiler-compatibility)."); +}; + +template , typename D = std::decay_t> +using enable_if_t = typename enable_if_enum && std::is_invocable_r_v, R>::type; + +template >, int> = 0> +using enum_concept = T; + +template > +struct is_scoped_enum : std::false_type {}; + +template +struct is_scoped_enum : std::bool_constant>> {}; + +template > +struct is_unscoped_enum : std::false_type {}; + +template +struct is_unscoped_enum : std::bool_constant>> {}; + +template >> +struct underlying_type {}; + +template +struct underlying_type : std::underlying_type> {}; + +#if defined(MAGIC_ENUM_ENABLE_HASH) || defined(MAGIC_ENUM_ENABLE_HASH_SWITCH) + +template +struct constexpr_hash_t; + +template +struct constexpr_hash_t>> { + constexpr auto operator()(Value value) const noexcept { + using U = typename underlying_type::type; + if constexpr (std::is_same_v) { // bool special case + return static_cast(value); + } else { + return static_cast(value); + } + } + using secondary_hash = constexpr_hash_t; +}; + +template +struct constexpr_hash_t>> { + static constexpr std::uint32_t crc_table[256] { + 0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L, 0x706af48fL, 0xe963a535L, 0x9e6495a3L, + 0x0edb8832L, 0x79dcb8a4L, 0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L, 0x90bf1d91L, + 0x1db71064L, 0x6ab020f2L, 0xf3b97148L, 0x84be41deL, 0x1adad47dL, 0x6ddde4ebL, 0xf4d4b551L, 0x83d385c7L, + 0x136c9856L, 0x646ba8c0L, 0xfd62f97aL, 0x8a65c9ecL, 0x14015c4fL, 0x63066cd9L, 0xfa0f3d63L, 0x8d080df5L, + 0x3b6e20c8L, 0x4c69105eL, 0xd56041e4L, 0xa2677172L, 0x3c03e4d1L, 0x4b04d447L, 0xd20d85fdL, 0xa50ab56bL, + 0x35b5a8faL, 0x42b2986cL, 0xdbbbc9d6L, 0xacbcf940L, 0x32d86ce3L, 0x45df5c75L, 0xdcd60dcfL, 0xabd13d59L, + 0x26d930acL, 0x51de003aL, 0xc8d75180L, 0xbfd06116L, 0x21b4f4b5L, 0x56b3c423L, 0xcfba9599L, 0xb8bda50fL, + 0x2802b89eL, 0x5f058808L, 0xc60cd9b2L, 0xb10be924L, 0x2f6f7c87L, 0x58684c11L, 0xc1611dabL, 0xb6662d3dL, + 0x76dc4190L, 0x01db7106L, 0x98d220bcL, 0xefd5102aL, 0x71b18589L, 0x06b6b51fL, 0x9fbfe4a5L, 0xe8b8d433L, + 0x7807c9a2L, 0x0f00f934L, 0x9609a88eL, 0xe10e9818L, 0x7f6a0dbbL, 0x086d3d2dL, 0x91646c97L, 0xe6635c01L, + 0x6b6b51f4L, 0x1c6c6162L, 0x856530d8L, 0xf262004eL, 0x6c0695edL, 0x1b01a57bL, 0x8208f4c1L, 0xf50fc457L, + 0x65b0d9c6L, 0x12b7e950L, 0x8bbeb8eaL, 0xfcb9887cL, 0x62dd1ddfL, 0x15da2d49L, 0x8cd37cf3L, 0xfbd44c65L, + 0x4db26158L, 0x3ab551ceL, 0xa3bc0074L, 0xd4bb30e2L, 0x4adfa541L, 0x3dd895d7L, 0xa4d1c46dL, 0xd3d6f4fbL, + 0x4369e96aL, 0x346ed9fcL, 0xad678846L, 0xda60b8d0L, 0x44042d73L, 0x33031de5L, 0xaa0a4c5fL, 0xdd0d7cc9L, + 0x5005713cL, 0x270241aaL, 0xbe0b1010L, 0xc90c2086L, 0x5768b525L, 0x206f85b3L, 0xb966d409L, 0xce61e49fL, + 0x5edef90eL, 0x29d9c998L, 0xb0d09822L, 0xc7d7a8b4L, 0x59b33d17L, 0x2eb40d81L, 0xb7bd5c3bL, 0xc0ba6cadL, + 0xedb88320L, 0x9abfb3b6L, 0x03b6e20cL, 0x74b1d29aL, 0xead54739L, 0x9dd277afL, 0x04db2615L, 0x73dc1683L, + 0xe3630b12L, 0x94643b84L, 0x0d6d6a3eL, 0x7a6a5aa8L, 0xe40ecf0bL, 0x9309ff9dL, 0x0a00ae27L, 0x7d079eb1L, + 0xf00f9344L, 0x8708a3d2L, 0x1e01f268L, 0x6906c2feL, 0xf762575dL, 0x806567cbL, 0x196c3671L, 0x6e6b06e7L, + 0xfed41b76L, 0x89d32be0L, 0x10da7a5aL, 0x67dd4accL, 0xf9b9df6fL, 0x8ebeeff9L, 0x17b7be43L, 0x60b08ed5L, + 0xd6d6a3e8L, 0xa1d1937eL, 0x38d8c2c4L, 0x4fdff252L, 0xd1bb67f1L, 0xa6bc5767L, 0x3fb506ddL, 0x48b2364bL, + 0xd80d2bdaL, 0xaf0a1b4cL, 0x36034af6L, 0x41047a60L, 0xdf60efc3L, 0xa867df55L, 0x316e8eefL, 0x4669be79L, + 0xcb61b38cL, 0xbc66831aL, 0x256fd2a0L, 0x5268e236L, 0xcc0c7795L, 0xbb0b4703L, 0x220216b9L, 0x5505262fL, + 0xc5ba3bbeL, 0xb2bd0b28L, 0x2bb45a92L, 0x5cb36a04L, 0xc2d7ffa7L, 0xb5d0cf31L, 0x2cd99e8bL, 0x5bdeae1dL, + 0x9b64c2b0L, 0xec63f226L, 0x756aa39cL, 0x026d930aL, 0x9c0906a9L, 0xeb0e363fL, 0x72076785L, 0x05005713L, + 0x95bf4a82L, 0xe2b87a14L, 0x7bb12baeL, 0x0cb61b38L, 0x92d28e9bL, 0xe5d5be0dL, 0x7cdcefb7L, 0x0bdbdf21L, + 0x86d3d2d4L, 0xf1d4e242L, 0x68ddb3f8L, 0x1fda836eL, 0x81be16cdL, 0xf6b9265bL, 0x6fb077e1L, 0x18b74777L, + 0x88085ae6L, 0xff0f6a70L, 0x66063bcaL, 0x11010b5cL, 0x8f659effL, 0xf862ae69L, 0x616bffd3L, 0x166ccf45L, + 0xa00ae278L, 0xd70dd2eeL, 0x4e048354L, 0x3903b3c2L, 0xa7672661L, 0xd06016f7L, 0x4969474dL, 0x3e6e77dbL, + 0xaed16a4aL, 0xd9d65adcL, 0x40df0b66L, 0x37d83bf0L, 0xa9bcae53L, 0xdebb9ec5L, 0x47b2cf7fL, 0x30b5ffe9L, + 0xbdbdf21cL, 0xcabac28aL, 0x53b39330L, 0x24b4a3a6L, 0xbad03605L, 0xcdd70693L, 0x54de5729L, 0x23d967bfL, + 0xb3667a2eL, 0xc4614ab8L, 0x5d681b02L, 0x2a6f2b94L, 0xb40bbe37L, 0xc30c8ea1L, 0x5a05df1bL, 0x2d02ef8dL + }; + constexpr std::uint32_t operator()(string_view value) const noexcept { + auto crc = static_cast(0xffffffffL); + for (const auto c : value) { + crc = (crc >> 8) ^ crc_table[(crc ^ static_cast(c)) & 0xff]; + } + return crc ^ 0xffffffffL; + } + + struct secondary_hash { + constexpr std::uint32_t operator()(string_view value) const noexcept { + auto acc = static_cast(2166136261ULL); + for (const auto c : value) { + acc = ((acc ^ static_cast(c)) * static_cast(16777619ULL)) & (std::numeric_limits::max)(); + } + return static_cast(acc); + } + }; +}; + +template +inline constexpr Hash hash_v{}; + +template +constexpr auto calculate_cases(std::size_t Page) noexcept { + constexpr std::array values = *GlobValues; + constexpr std::size_t size = values.size(); + + using switch_t = std::invoke_result_t; + static_assert(std::is_integral_v && !std::is_same_v); + const std::size_t values_to = (std::min)(static_cast(256), size - Page); + + std::array result{}; + auto fill = result.begin(); + { + auto first = values.begin() + static_cast(Page); + auto last = values.begin() + static_cast(Page + values_to); + while (first != last) { + *fill++ = hash_v(*first++); + } + } + + // dead cases, try to avoid case collisions + for (switch_t last_value = result[values_to - 1]; fill != result.end() && last_value != (std::numeric_limits::max)(); *fill++ = ++last_value) { + } + + { + auto it = result.begin(); + auto last_value = (std::numeric_limits::min)(); + for (; fill != result.end(); *fill++ = last_value++) { + while (last_value == *it) { + ++last_value, ++it; + } + } + } + + return result; +} + +template +constexpr R invoke_r(F&& f, Args&&... args) noexcept(std::is_nothrow_invocable_r_v) { + if constexpr (std::is_void_v) { + std::forward(f)(std::forward(args)...); + } else { + return static_cast(std::forward(f)(std::forward(args)...)); + } +} + +enum class case_call_t { + index, + value +}; + +template +inline constexpr auto default_result_type_lambda = []() noexcept(std::is_nothrow_default_constructible_v) { return T{}; }; + +template <> +inline constexpr auto default_result_type_lambda = []() noexcept {}; + +template +constexpr bool has_duplicate() noexcept { + using value_t = std::decay_t; + using hash_value_t = std::invoke_result_t; + std::arraysize()> hashes{}; + std::size_t size = 0; + for (auto elem : *Arr) { + hashes[size] = hash_v(elem); + for (auto i = size++; i > 0; --i) { + if (hashes[i] < hashes[i - 1]) { + auto tmp = hashes[i]; + hashes[i] = hashes[i - 1]; + hashes[i - 1] = tmp; + } else if (hashes[i] == hashes[i - 1]) { + return false; + } else { + break; + } + } + } + return true; +} + +#define MAGIC_ENUM_CASE(val) \ + case cases[val]: \ + if constexpr ((val) + Page < size) { \ + if (!pred(values[val + Page], searched)) { \ + break; \ + } \ + if constexpr (CallValue == case_call_t::index) { \ + if constexpr (std::is_invocable_r_v>) { \ + return detail::invoke_r(std::forward(lambda), std::integral_constant{}); \ + } else if constexpr (std::is_invocable_v>) { \ + MAGIC_ENUM_ASSERT(false && "magic_enum::detail::constexpr_switch wrong result type."); \ + } \ + } else if constexpr (CallValue == case_call_t::value) { \ + if constexpr (std::is_invocable_r_v>) { \ + return detail::invoke_r(std::forward(lambda), enum_constant{}); \ + } else if constexpr (std::is_invocable_r_v>) { \ + MAGIC_ENUM_ASSERT(false && "magic_enum::detail::constexpr_switch wrong result type."); \ + } \ + } \ + break; \ + } else [[fallthrough]]; + +template ::value_type>, + typename BinaryPredicate = std::equal_to<>, + typename Lambda, + typename ResultGetterType> +constexpr decltype(auto) constexpr_switch( + Lambda&& lambda, + typename std::decay_t::value_type searched, + ResultGetterType&& def, + BinaryPredicate&& pred = {}) { + using result_t = std::invoke_result_t; + using hash_t = std::conditional_t(), Hash, typename Hash::secondary_hash>; + static_assert(has_duplicate(), "magic_enum::detail::constexpr_switch duplicated hash found, please report it: https://github.com/Neargye/magic_enum/issues."); + constexpr std::array values = *GlobValues; + constexpr std::size_t size = values.size(); + constexpr std::array cases = calculate_cases(Page); + + switch (hash_v(searched)) { + MAGIC_ENUM_FOR_EACH_256(MAGIC_ENUM_CASE) + default: + if constexpr (size > 256 + Page) { + return constexpr_switch(std::forward(lambda), searched, std::forward(def)); + } + break; + } + return def(); +} + +#undef MAGIC_ENUM_CASE + +#endif + +} // namespace magic_enum::detail + +// Checks is magic_enum supported compiler. +inline constexpr bool is_magic_enum_supported = detail::supported::value; + +template +using Enum = detail::enum_concept; + +// Checks whether T is an Unscoped enumeration type. +// Provides the member constant value which is equal to true, if T is an [Unscoped enumeration](https://en.cppreference.com/w/cpp/language/enum#Unscoped_enumeration) type. Otherwise, value is equal to false. +template +struct is_unscoped_enum : detail::is_unscoped_enum {}; + +template +inline constexpr bool is_unscoped_enum_v = is_unscoped_enum::value; + +// Checks whether T is an Scoped enumeration type. +// Provides the member constant value which is equal to true, if T is an [Scoped enumeration](https://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations) type. Otherwise, value is equal to false. +template +struct is_scoped_enum : detail::is_scoped_enum {}; + +template +inline constexpr bool is_scoped_enum_v = is_scoped_enum::value; + +// If T is a complete enumeration type, provides a member typedef type that names the underlying type of T. +// Otherwise, if T is not an enumeration type, there is no member type. Otherwise (T is an incomplete enumeration type), the program is ill-formed. +template +struct underlying_type : detail::underlying_type {}; + +template +using underlying_type_t = typename underlying_type::type; + +template +using enum_constant = detail::enum_constant; + +// Returns type name of enum. +template +[[nodiscard]] constexpr auto enum_type_name() noexcept -> detail::enable_if_t { + constexpr string_view name = detail::type_name_v>.str(); + static_assert(!name.empty(), "magic_enum::enum_type_name enum type does not have a name."); + + return name; +} + +// Returns number of enum values. +template > +[[nodiscard]] constexpr auto enum_count() noexcept -> detail::enable_if_t { + return detail::count_v, S>; +} + +// Returns enum value at specified index. +// No bounds checking is performed: the behavior is undefined if index >= number of enum values. +template > +[[nodiscard]] constexpr auto enum_value(std::size_t index) noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v) { + return MAGIC_ENUM_ASSERT(index < detail::count_v), detail::values_v[index]; + } else { + constexpr auto min = (S == detail::enum_subtype::flags) ? detail::log2(detail::min_v) : detail::min_v; + + return MAGIC_ENUM_ASSERT(index < detail::count_v), detail::value(index); + } +} + +// Returns enum value at specified index. +template > +[[nodiscard]] constexpr auto enum_value() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + static_assert(I < detail::count_v, "magic_enum::enum_value out of range."); + + return enum_value(I); +} + +// Returns std::array with enum values, sorted by enum value. +template > +[[nodiscard]] constexpr auto enum_values() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return detail::values_v; +} + +// Returns integer value from enum value. +template +[[nodiscard]] constexpr auto enum_integer(E value) noexcept -> underlying_type_t { + return static_cast>(value); +} + +// Returns underlying value from enum value. +template +[[nodiscard]] constexpr auto enum_underlying(E value) noexcept -> detail::enable_if_t> { + return static_cast>(value); +} + +// Obtains index in enum values from enum value. +// Returns optional with index. +template > +[[nodiscard]] constexpr auto enum_index(E value) noexcept -> detail::enable_if_t> { + using D = std::decay_t; + using U = underlying_type_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v || (S == detail::enum_subtype::flags)) { +#if defined(MAGIC_ENUM_ENABLE_HASH) + return detail::constexpr_switch<&detail::values_v, detail::case_call_t::index>( + [](std::size_t i) { return optional{i}; }, + value, + detail::default_result_type_lambda>); +#else + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (enum_value(i) == value) { + return i; + } + } + return {}; // Invalid value or out of range. +#endif + } else { + const auto v = static_cast(value); + if (v >= detail::min_v && v <= detail::max_v) { + return static_cast(v - detail::min_v); + } + return {}; // Invalid value or out of range. + } +} + +// Obtains index in enum values from enum value. +// Returns optional with index. +template +[[nodiscard]] constexpr auto enum_index(E value) noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return enum_index(value); +} + +// Obtains index in enum values from static storage enum variable. +template >> +[[nodiscard]] constexpr auto enum_index() noexcept -> detail::enable_if_t {\ + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + constexpr auto index = enum_index(V); + static_assert(index, "magic_enum::enum_index enum value does not have a index."); + + return *index; +} + +// Returns name from static storage enum variable. +// This version is much lighter on the compile times and is not restricted to the enum_range limitation. +template +[[nodiscard]] constexpr auto enum_name() noexcept -> detail::enable_if_t { + constexpr string_view name = detail::enum_name_v, V>.str(); + static_assert(!name.empty(), "magic_enum::enum_name enum value does not have a name."); + + return name; +} + +// Returns name from enum value. +// If enum value does not have name or value out of range, returns empty string. +template > +[[nodiscard]] constexpr auto enum_name(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if (const auto i = enum_index(value)) { + return detail::names_v[*i]; + } + return detail::static_str<0>{}.str(); +} + +// Returns name from enum value. +// If enum value does not have name or value out of range, returns empty string. +template +[[nodiscard]] constexpr auto enum_name(E value) -> detail::enable_if_t { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return enum_name(value); +} + +// Returns std::array with names, sorted by enum value. +template > +[[nodiscard]] constexpr auto enum_names() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return detail::names_v; +} + +// Returns std::array with pairs (value, name), sorted by enum value. +template > +[[nodiscard]] constexpr auto enum_entries() noexcept -> detail::enable_if_t> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + return detail::entries_v; +} + +// Allows you to write magic_enum::enum_cast("bar", magic_enum::case_insensitive); +inline constexpr auto case_insensitive = detail::case_insensitive<>{}; + +// Obtains enum value from integer value. +// Returns optional with enum value. +template > +[[nodiscard]] constexpr auto enum_cast(underlying_type_t value) noexcept -> detail::enable_if_t>> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + + if constexpr (detail::is_sparse_v || (S == detail::enum_subtype::flags)) { +#if defined(MAGIC_ENUM_ENABLE_HASH) + return detail::constexpr_switch<&detail::values_v, detail::case_call_t::value>( + [](D v) { return optional{v}; }, + static_cast(value), + detail::default_result_type_lambda>); +#else + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (value == static_cast>(enum_value(i))) { + return static_cast(value); + } + } + return {}; // Invalid value or out of range. +#endif + } else { + if (value >= detail::min_v && value <= detail::max_v) { + return static_cast(value); + } + return {}; // Invalid value or out of range. + } +} + +// Obtains enum value from name. +// Returns optional with enum value. +template , typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr auto enum_cast(string_view value, [[maybe_unused]] BinaryPredicate p = {}) noexcept(detail::is_nothrow_invocable_v) -> detail::enable_if_t>, BinaryPredicate> { + using D = std::decay_t; + static_assert(detail::is_reflected_v, "magic_enum requires enum implementation and valid max and min."); + +#if defined(MAGIC_ENUM_ENABLE_HASH) + if constexpr (detail::is_default_predicate_v) { + return detail::constexpr_switch<&detail::names_v, detail::case_call_t::index>( + [](std::size_t i) { return optional{detail::values_v[i]}; }, + value, + detail::default_result_type_lambda>, + [&p](string_view lhs, string_view rhs) { return detail::cmp_equal(lhs, rhs, p); }); + } +#endif + for (std::size_t i = 0; i < detail::count_v; ++i) { + if (detail::cmp_equal(value, detail::names_v[i], p)) { + return enum_value(i); + } + } + return {}; // Invalid value or out of range. +} + +// Checks whether enum contains value with such value. +template > +[[nodiscard]] constexpr auto enum_contains(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + using U = underlying_type_t; + + return static_cast(enum_cast(static_cast(value))); +} + +// Checks whether enum contains value with such value. +template +[[nodiscard]] constexpr auto enum_contains(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + using U = underlying_type_t; + + return static_cast(enum_cast(static_cast(value))); +} + +// Checks whether enum contains value with such integer value. +template > +[[nodiscard]] constexpr auto enum_contains(underlying_type_t value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + return static_cast(enum_cast(value)); +} + +// Checks whether enum contains enumerator with such name. +template , typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr auto enum_contains(string_view value, BinaryPredicate p = {}) noexcept(detail::is_nothrow_invocable_v) -> detail::enable_if_t { + using D = std::decay_t; + + return static_cast(enum_cast(value, std::move(p))); +} + +// Returns true if the enum integer value is in the range of values that can be reflected. +template > +[[nodiscard]] constexpr auto enum_reflected(underlying_type_t value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + if constexpr (detail::is_reflected_v) { + constexpr auto min = detail::reflected_min(); + constexpr auto max = detail::reflected_max(); + return value >= min && value <= max; + } else { + return false; + } +} + +// Returns true if the enum value is in the range of values that can be reflected. +template > +[[nodiscard]] constexpr auto enum_reflected(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + return enum_reflected(static_cast>(value)); +} + +// Returns true if the enum value is in the range of values that can be reflected. +template +[[nodiscard]] constexpr auto enum_reflected(E value) noexcept -> detail::enable_if_t { + using D = std::decay_t; + + return enum_reflected(value); +} + +template +inline constexpr auto as_flags = AsFlags ? detail::enum_subtype::flags : detail::enum_subtype::common; + +template +inline constexpr auto as_common = AsCommon ? detail::enum_subtype::common : detail::enum_subtype::flags; + +namespace bitwise_operators { + +template = 0> +constexpr E operator~(E rhs) noexcept { + return static_cast(~static_cast>(rhs)); +} + +template = 0> +constexpr E operator|(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) | static_cast>(rhs)); +} + +template = 0> +constexpr E operator&(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) & static_cast>(rhs)); +} + +template = 0> +constexpr E operator^(E lhs, E rhs) noexcept { + return static_cast(static_cast>(lhs) ^ static_cast>(rhs)); +} + +template = 0> +constexpr E& operator|=(E& lhs, E rhs) noexcept { + return lhs = (lhs | rhs); +} + +template = 0> +constexpr E& operator&=(E& lhs, E rhs) noexcept { + return lhs = (lhs & rhs); +} + +template = 0> +constexpr E& operator^=(E& lhs, E rhs) noexcept { + return lhs = (lhs ^ rhs); +} + +} // namespace magic_enum::bitwise_operators + +} // namespace magic_enum + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#undef MAGIC_ENUM_GET_ENUM_NAME_BUILTIN +#undef MAGIC_ENUM_GET_TYPE_NAME_BUILTIN +#undef MAGIC_ENUM_VS_2017_WORKAROUND +#undef MAGIC_ENUM_ARRAY_CONSTEXPR +#undef MAGIC_ENUM_FOR_EACH_256 + +#endif // NEARGYE_MAGIC_ENUM_HPP diff --git a/include/mainwindow.h b/include/mainwindow.h index a4d3a731..ba4d173d 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -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); diff --git a/include/project.h b/include/project.h index 0af3af32..3f4fde19 100644 --- a/include/project.h +++ b/include/project.h @@ -105,7 +105,7 @@ public: void clearHealLocations(); bool sanityCheck(); - int getSupportedMajorVersion(QString *errorOut = nullptr); + QVersionNumber getMinimumVersion(QString *errorOut = nullptr) const; bool load(); QMap tilesetCache; diff --git a/include/scriptutility.h b/include/scriptutility.h index b21f329c..33b9658e 100644 --- a/include/scriptutility.h +++ b/include/scriptutility.h @@ -50,7 +50,7 @@ public: Q_INVOKABLE bool getBorderVisibility(); Q_INVOKABLE void setSmartPathsEnabled(bool visible); Q_INVOKABLE bool getSmartPathsEnabled(); - Q_INVOKABLE QList getCustomScripts(); + static Q_INVOKABLE QList getCustomScripts(); Q_INVOKABLE QList getMetatileLayerOrder(); Q_INVOKABLE void setMetatileLayerOrder(const QList &order); Q_INVOKABLE QList getMetatileLayerOpacity(); diff --git a/include/ui/citymappixmapitem.h b/include/ui/citymappixmapitem.h deleted file mode 100644 index 5fe285de..00000000 --- a/include/ui/citymappixmapitem.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef CITYMAPPIXMAPITEM_H -#define CITYMAPPIXMAPITEM_H - -#include "tilemaptileselector.h" - -#include -#include -#include - -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 getTiles(); - void setTiles(QVector); - -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 diff --git a/include/ui/customscriptseditor.h b/include/ui/customscriptseditor.h index 8d261570..141ebae8 100644 --- a/include/ui/customscriptseditor.h +++ b/include/ui/customscriptseditor.h @@ -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(); diff --git a/include/ui/customscriptslistitem.h b/include/ui/customscriptslistitem.h index d166db8a..9aa7be7d 100644 --- a/include/ui/customscriptslistitem.h +++ b/include/ui/customscriptslistitem.h @@ -2,6 +2,7 @@ #define CUSTOMSCRIPTSLISTITEM_H #include +#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; }; diff --git a/include/ui/eventfilters.h b/include/ui/eventfilters.h index 96260e10..d39e8474 100644 --- a/include/ui/eventfilters.h +++ b/include/ui/eventfilters.h @@ -1,5 +1,6 @@ #include #include +#include /// 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 m_shown; +}; diff --git a/include/ui/gridsettings.h b/include/ui/gridsettings.h index 807a0685..0e322e9e 100644 --- a/include/ui/gridsettings.h +++ b/include/ui/gridsettings.h @@ -3,10 +3,12 @@ #include #include +#include +#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 getHorizontalDashPattern() const { return this->getDashPattern(this->width); } QVector 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: diff --git a/include/ui/metatileimageexporter.h b/include/ui/metatileimageexporter.h index 20077d80..527d7478 100644 --- a/include/ui/metatileimageexporter.h +++ b/include/ui/metatileimageexporter.h @@ -84,7 +84,12 @@ private: QImage m_previewImage; bool m_previewUpdateQueued = false; QList m_layerOrder; - ProjectConfig m_savedConfig; + struct SavedConfigSettings { + QColor transparencyColor; + uint16_t unusedTileNormal = 0; + uint16_t unusedTileCovered = 0; + uint16_t unusedTileSplit = 0; + } m_savedConfig; QList m_transparencyButtons; void populate(const Settings &settings); diff --git a/include/ui/paletteeditor.h b/include/ui/paletteeditor.h index 2d26cf71..c46621b6 100644 --- a/include/ui/paletteeditor.h +++ b/include/ui/paletteeditor.h @@ -54,7 +54,6 @@ private: void commitEditHistory(); void commitEditHistory(int paletteId); void updateEditHistoryActions(); - void restoreWindowState(); void invalidateCache(); void closeEvent(QCloseEvent*); void setColorInputTitles(bool show); diff --git a/include/ui/prefab.h b/include/ui/prefab.h index 2d107402..7d68ec48 100644 --- a/include/ui/prefab.h +++ b/include/ui/prefab.h @@ -25,7 +25,7 @@ public: void addPrefab(MetatileSelection selection, Layout *layout, QString name); void updatePrefabUi(QPointer layout); void clearPrefabUi(); - bool tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, QString filepath = ""); + bool tryImportDefaultPrefabs(QWidget * parent, BaseGame::Version version, QString filepath = ""); private: QPointer selector; diff --git a/include/ui/projectsettingseditor.h b/include/ui/projectsettingseditor.h index dd9b2b8c..34eb4207 100644 --- a/include/ui/projectsettingseditor.h +++ b/include/ui/projectsettingseditor.h @@ -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 getGlobalConstants(); + BaseGame::Version getBaseGameVersion() const; private slots: void dialogButtonClicked(QAbstractButton *button); diff --git a/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h index 432eaab6..86a38198 100644 --- a/include/ui/regionmapeditor.h +++ b/include/ui/regionmapeditor.h @@ -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); diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index 49b0ec91..8e3a7826 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -114,7 +114,6 @@ private: void initMetatileLayersItem(); void initShortcuts(); void initExtraShortcuts(); - void restoreWindowState(); void initMetatileHistory(); void setTilesets(QString primaryTilesetLabel, QString secondaryTilesetLabel); void reset(); diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 860ce564..0ebc26f0 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -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(); diff --git a/porymap.pro b/porymap.pro index 27c11ba5..2cf2251d 100644 --- a/porymap.pro +++ b/porymap.pro @@ -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 diff --git a/src/config.cpp b/src/config.cpp deleted file mode 100644 index 067e199e..00000000 --- a/src/config.cpp +++ /dev/null @@ -1,1518 +0,0 @@ -#include "config.h" -#include "log.h" -#include "shortcut.h" -#include "map.h" -#include "validator.h" -#include "utility.h" -#include "metatile.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const QVersionNumber porymapVersion = QVersionNumber::fromString(PORYMAP_VERSION); - -// In both versions the default new map border is a generic tree -const QList defaultBorder_RSE = {0x1D4, 0x1D5, 0x1DC, 0x1DD}; -const QList defaultBorder_FRLG = {0x14, 0x15, 0x1C, 0x1D}; - -const QList defaultWarpBehaviors_RSE = { - 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 -}; - -const QList defaultWarpBehaviors_FRLG = { - 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 -}; - -const QMap> 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> 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 reverseDefaultIdentifier(QString str) { - for (auto i = ProjectConfig::defaultIdentifiers.cbegin(), end = ProjectConfig::defaultIdentifiers.cend(); i != end; i++) { - if (i.value().first == str) return i.key(); - } - return static_cast(-1); -} - -ProjectFilePath reverseDefaultPaths(QString str) { - for (auto it = ProjectConfig::defaultPaths.constKeyValueBegin(); it != ProjectConfig::defaultPaths.constKeyValueEnd(); ++it) { - if ((*it).second.first == str) return (*it).first; - } - return static_cast(-1); -} - -void KeyValueConfigBase::setRoot(const QString &root) { - m_root = root; - QDir dir(m_root); - if (!m_root.isEmpty() && !dir.exists()) { - dir.mkpath(m_root); - } - m_filepath = dir.absoluteFilePath(m_filename); -} - -bool KeyValueConfigBase::load(const QString &root) { - if (!root.isEmpty()) { - setRoot(root); - } - reset(); - QFile file(this->filepath()); - if (file.exists() && !file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open config file '%1': ").arg(this->filepath()) + file.errorString()); - return false; - } else if (file.size() == 0) { - this->init(); - } - - QTextStream in(&file); - static const QRegularExpression re("^(?[^=]+)=(?.*)$"); - 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()) { - logWarn(QString("Invalid config line in %1: '%2'").arg(this->filepath()).arg(line)); - continue; - } - - this->parseConfigKeyValue(match.captured("key").trimmed(), match.captured("value").trimmed()); - } - this->setUnreadKeys(); - - file.close(); - return true; -} - -bool KeyValueConfigBase::save() { - QString text = ""; - QMap map = this->getKeyValueMap(); - for (QMap::iterator it = map.begin(); it != map.end(); it++) { - text += QString("%1=%2\n").arg(it.key()).arg(it.value()); - } - - QFile file(this->filepath()); - if (!file.open(QIODevice::WriteOnly)) { - logError(QString("Could not open config file '%1' for writing: ").arg(this->filepath()) + file.errorString()); - return false; - } - - file.write(text.toUtf8()); - file.close(); - return true; -} - -bool KeyValueConfigBase::getConfigBool(const QString &key, const QString &value) { - bool ok; - int result = value.toInt(&ok, 0); - if (!ok || (result != 0 && result != 1)) { - logWarn(QString("Invalid config value for %1: '%2'. Must be 0 or 1.").arg(key).arg(value)); - } - return (result != 0); -} - -int KeyValueConfigBase::getConfigInteger(const QString &key, const QString &value, int min, int max, int defaultValue) { - bool ok; - int result = value.toInt(&ok, 0); - if (!ok) { - logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); - result = defaultValue; - } - return qBound(min, result, max); -} - -uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &value, uint32_t min, uint32_t max, uint32_t defaultValue) { - bool ok; - uint32_t result = value.toUInt(&ok, 0); - if (!ok) { - logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); - result = defaultValue; - } - return qBound(min, result, max); -} - -QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &value, const QColor &defaultValue) { - if (value.isEmpty()) - return QColor(); - - QColor color = QColor("#" + value); - if (!color.isValid()) { - logWarn(QString("Invalid config value for %1: '%2'. Must be a color in the format 'RRGGBB'. Using default value '%3'.").arg(key).arg(value).arg(defaultValue.name())); - color = defaultValue; - } - return color; -} - -QString KeyValueConfigBase::toConfigColor(const QColor &color) { - return color.isValid() ? color.name().remove("#") : QString(); // Our text config treats '#' as the start of a comment. -} - -PorymapConfig porymapConfig; - -PorymapConfig::PorymapConfig() : KeyValueConfigBase(QStringLiteral("porymap.cfg")) { - reset(); -} - -void PorymapConfig::reset() { - setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - this->recentProjects.clear(); - this->projectManuallyClosed = false; - this->reopenOnLaunch = true; - this->mapListTab = 0; - this->mapListEditGroupsEnabled = false; - this->mapListHideEmptyEnabled.clear(); - this->mapListLayoutsSorted = true; - this->mapListLocationsSorted = true; - this->prettyCursors = true; - this->mirrorConnectingMaps = true; - this->showDiveEmergeMaps = false; - this->diveEmergeMapOpacity = 30; - this->diveMapOpacity = 15; - this->emergeMapOpacity = 15; - this->collisionOpacity = 50; - this->collisionZoom = 30; - this->metatilesZoom = 30; - this->tilesetEditorMetatilesZoom = 30; - this->tilesetEditorTilesZoom = 30; - this->tilesetEditorLayerOrientation = Qt::Vertical; - this->showPlayerView = false; - this->showCursorTile = true; - this->showBorder = true; - this->showGrid = false; - this->showTilesetEditorMetatileGrid = false; - this->showTilesetEditorLayerGrid = true; - this->showTilesetEditorDivider = false; - this->showTilesetEditorRawAttributes = false; - this->showPaletteEditorUnusedColors = false; - this->monitorFiles = true; - this->tilesetCheckerboardFill = true; - this->newMapHeaderSectionExpanded = false; - this->theme = "default"; - this->wildMonChartTheme = ""; - this->textEditorOpenFolder = ""; - this->textEditorGotoLine = ""; - this->paletteEditorBitDepth = 24; - this->projectSettingsTab = 0; - this->scriptAutocompleteMode = ScriptAutocompleteMode::MapOnly; - this->warpBehaviorWarningDisabled = false; - this->eventDeleteWarningDisabled = false; - this->eventOverlayEnabled = false; - this->checkForUpdates = true; - this->showProjectLoadingScreen = true; - this->lastUpdateCheckTime = QDateTime(); - this->lastUpdateCheckVersion = porymapVersion; - this->rateLimitTimes.clear(); - this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; - this->shownInGameReloadMessage = false; - this->gridSettings = GridSettings(); - this->gridSettings.width = Metatile::pixelWidth(); - this->gridSettings.height = Metatile::pixelHeight(); - this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; - this->applicationFont = QFont(); - this->mapListFont = PorymapConfig::defaultMapListFont(); -#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. - this->imageExportColorSpaceId = static_cast(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. - this->imageExportColorSpaceId = 0; -#endif - this->trustedScriptHashes.clear(); -} - -void PorymapConfig::parseConfigKeyValue(QString key, QString value) { - if (key == "recent_project") { - this->recentProjects = value.split(",", Qt::SkipEmptyParts); - this->recentProjects.removeDuplicates(); - } else if (key == "project_manually_closed") { - this->projectManuallyClosed = getConfigBool(key, value); - } else if (key == "reopen_on_launch") { - this->reopenOnLaunch = getConfigBool(key, value); - } else if (key == "pretty_cursors") { - this->prettyCursors = getConfigBool(key, value); - } else if (key == "map_list_tab") { - this->mapListTab = getConfigInteger(key, value, 0, 2, 0); - } else if (key == "map_list_edit_groups_enabled") { - this->mapListEditGroupsEnabled = getConfigBool(key, 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) { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); - return; - } - this->mapListHideEmptyEnabled.insert(tab, getConfigBool(key, value)); - } else if (key == "map_list_layouts_sorted") { - this->mapListLayoutsSorted = getConfigBool(key, value); - } else if (key == "map_list_locations_sorted") { - this->mapListLocationsSorted = getConfigBool(key, value); - } else if (key == "main_window_geometry") { - this->mainWindowGeometry = bytesFromString(value); - } else if (key == "main_window_state") { - this->mainWindowState = bytesFromString(value); - } else if (key == "map_splitter_state") { - this->mapSplitterState = bytesFromString(value); - } else if (key == "main_splitter_state") { - this->mainSplitterState = bytesFromString(value); - } else if (key == "metatiles_splitter_state") { - this->metatilesSplitterState = bytesFromString(value); - } else if (key == "mirror_connecting_maps") { - this->mirrorConnectingMaps = getConfigBool(key, value); - } else if (key == "show_dive_emerge_maps") { - this->showDiveEmergeMaps = getConfigBool(key, value); - } else if (key == "dive_emerge_map_opacity") { - this->diveEmergeMapOpacity = getConfigInteger(key, value, 10, 90, 30); - } else if (key == "dive_map_opacity") { - this->diveMapOpacity = getConfigInteger(key, value, 10, 90, 15); - } else if (key == "emerge_map_opacity") { - this->emergeMapOpacity = getConfigInteger(key, value, 10, 90, 15); - } else if (key == "collision_opacity") { - this->collisionOpacity = getConfigInteger(key, value, 0, 100, 50); - } else if (key == "tileset_editor_geometry") { - this->tilesetEditorGeometry = bytesFromString(value); - } else if (key == "tileset_editor_state") { - this->tilesetEditorState = bytesFromString(value); - } else if (key == "tileset_editor_splitter_state") { - this->tilesetEditorSplitterState = bytesFromString(value); - } else if (key == "palette_editor_geometry") { - this->paletteEditorGeometry = bytesFromString(value); - } else if (key == "palette_editor_state") { - this->paletteEditorState = bytesFromString(value); - } else if (key == "region_map_editor_geometry") { - this->regionMapEditorGeometry = bytesFromString(value); - } else if (key == "region_map_editor_state") { - this->regionMapEditorState = bytesFromString(value); - } else if (key == "project_settings_editor_geometry") { - this->projectSettingsEditorGeometry = bytesFromString(value); - } else if (key == "project_settings_editor_state") { - this->projectSettingsEditorState = bytesFromString(value); - } else if (key == "custom_scripts_editor_geometry") { - this->customScriptsEditorGeometry = bytesFromString(value); - } else if (key == "custom_scripts_editor_state") { - this->customScriptsEditorState = bytesFromString(value); - } else if (key == "wild_mon_chart_geometry") { - this->wildMonChartGeometry = bytesFromString(value); - } else if (key == "new_map_dialog_geometry") { - this->newMapDialogGeometry = bytesFromString(value); - } else if (key == "new_layout_dialog_geometry") { - this->newLayoutDialogGeometry = bytesFromString(value); - } else if (key == "metatiles_zoom") { - this->metatilesZoom = getConfigInteger(key, value, 10, 100, 30); - } else if (key == "collision_zoom") { - this->collisionZoom = getConfigInteger(key, value, 10, 100, 30); - } else if (key == "tileset_editor_metatiles_zoom") { - this->tilesetEditorMetatilesZoom = getConfigInteger(key, value, 10, 100, 30); - } else if (key == "tileset_editor_tiles_zoom") { - this->tilesetEditorTilesZoom = getConfigInteger(key, value, 10, 100, 30); - } else if (key == "tileset_editor_layer_orientation") { - // Being explicit here to avoid casting out-of-range values. - this->tilesetEditorLayerOrientation = (getConfigInteger(key, value) == static_cast(Qt::Horizontal)) ? Qt::Horizontal : Qt::Vertical; - } else if (key == "show_player_view") { - this->showPlayerView = getConfigBool(key, value); - } else if (key == "show_cursor_tile") { - this->showCursorTile = getConfigBool(key, value); - } else if (key == "show_border") { - this->showBorder = getConfigBool(key, value); - } else if (key == "show_grid") { - this->showGrid = getConfigBool(key, value); - } else if (key == "show_tileset_editor_metatile_grid") { - this->showTilesetEditorMetatileGrid = getConfigBool(key, value); - } else if (key == "show_tileset_editor_layer_grid") { - this->showTilesetEditorLayerGrid = getConfigBool(key, value); - } else if (key == "show_tileset_editor_divider") { - this->showTilesetEditorDivider = getConfigBool(key, value); - } else if (key == "show_tileset_editor_raw_attributes") { - this->showTilesetEditorRawAttributes = getConfigBool(key, value); - } else if (key == "show_palette_editor_unused_colors") { - this->showPaletteEditorUnusedColors = getConfigBool(key, value); - } else if (key == "monitor_files") { - this->monitorFiles = getConfigBool(key, value); - } else if (key == "tileset_checkerboard_fill") { - this->tilesetCheckerboardFill = getConfigBool(key, value); - } else if (key == "new_map_header_section_expanded") { - this->newMapHeaderSectionExpanded = getConfigBool(key, 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") { - this->paletteEditorBitDepth = getConfigInteger(key, value, 15, 24, 24); - if (this->paletteEditorBitDepth != 15 && this->paletteEditorBitDepth != 24){ - this->paletteEditorBitDepth = 24; - } - } else if (key == "project_settings_tab") { - this->projectSettingsTab = getConfigInteger(key, value, 0); -#ifdef CONFIG_BACKWARDS_COMPATABILITY - // Old setting replaced by script_autocomplete_mode - } else if (key == "load_all_event_scripts") { - this->scriptAutocompleteMode = getConfigBool(key, value) ? ScriptAutocompleteMode::All : ScriptAutocompleteMode::MapOnly; -#endif - } else if (key == "script_autocomplete_mode") { - this->scriptAutocompleteMode = static_cast(getConfigInteger(key, value, ScriptAutocompleteMode::MapOnly, ScriptAutocompleteMode::All)); - } else if (key == "warp_behavior_warning_disabled") { - this->warpBehaviorWarningDisabled = getConfigBool(key, value); - } else if (key == "event_delete_warning_disabled") { - this->eventDeleteWarningDisabled = getConfigBool(key, value); - } else if (key == "event_overlay_enabled") { - this->eventOverlayEnabled = getConfigBool(key, value); - } else if (key == "check_for_updates") { - this->checkForUpdates = getConfigBool(key, value); - } else if (key == "show_project_loading_screen") { - this->showProjectLoadingScreen = getConfigBool(key, 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) { - logWarn(QString("Invalid config value for %1: '%2'. Must be 3 numbers separated by '.'").arg(key).arg(value)); - this->lastUpdateCheckVersion = porymapVersion; - } else { - this->lastUpdateCheckVersion = version; - } - } else if (key.startsWith("rate_limit_time/")) { - static const QRegularExpression regex("\\brate_limit_time/(?.+)"); - 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 { - logWarn(QString("Invalid config value for %1: '%2'. Must be 'mask' or 'bounding_rect'.").arg(key).arg(value)); - } - } else if (key == "shown_in_game_reload_message") { - this->shownInGameReloadMessage = getConfigBool(key, value); - } else if (key == "grid_width") { - this->gridSettings.width = getConfigUint32(key, value); - } else if (key == "grid_height") { - this->gridSettings.height = getConfigUint32(key, value); - } else if (key == "grid_x") { - this->gridSettings.offsetX = getConfigInteger(key, value, 0, 999); - } else if (key == "grid_y") { - this->gridSettings.offsetY = getConfigInteger(key, value, 0, 999); - } else if (key == "grid_style") { - this->gridSettings.style = GridSettings::getStyleFromName(value); - } else if (key == "grid_color") { - this->gridSettings.color = getConfigColor(key, 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(getConfigInteger(key, typeString, 0, 2)); - this->statusBarLogTypes.insert(type); - } - } else if (key.startsWith("trusted_script_hash/")) { - this->trustedScriptHashes.insert(key.mid(QStringLiteral("trusted_script_hash/").length()), value); - } 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 if (key == "image_export_color_space_id") { - this->imageExportColorSpaceId = getConfigInteger(key, value, 0, 8); - } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); - } -} - -QMap PorymapConfig::getKeyValueMap() { - QMap map; - map.insert("recent_project", this->recentProjects.join(",")); - map.insert("project_manually_closed", this->projectManuallyClosed ? "1" : "0"); - map.insert("reopen_on_launch", this->reopenOnLaunch ? "1" : "0"); - map.insert("pretty_cursors", this->prettyCursors ? "1" : "0"); - map.insert("map_list_tab", QString::number(this->mapListTab)); - map.insert("map_list_edit_groups_enabled", this->mapListEditGroupsEnabled ? "1" : "0"); - for (auto i = this->mapListHideEmptyEnabled.constBegin(); i != this->mapListHideEmptyEnabled.constEnd(); i++) { - map.insert(QStringLiteral("map_list_hide_empty_enabled/") + QString::number(i.key()), i.value() ? "1" : "0"); - } - map.insert("map_list_layouts_sorted", this->mapListLayoutsSorted ? "1" : "0"); - map.insert("map_list_locations_sorted", this->mapListLocationsSorted ? "1" : "0"); - map.insert("main_window_geometry", stringFromByteArray(this->mainWindowGeometry)); - map.insert("main_window_state", stringFromByteArray(this->mainWindowState)); - map.insert("map_splitter_state", stringFromByteArray(this->mapSplitterState)); - map.insert("main_splitter_state", stringFromByteArray(this->mainSplitterState)); - map.insert("metatiles_splitter_state", stringFromByteArray(this->metatilesSplitterState)); - map.insert("tileset_editor_geometry", stringFromByteArray(this->tilesetEditorGeometry)); - map.insert("tileset_editor_state", stringFromByteArray(this->tilesetEditorState)); - map.insert("tileset_editor_splitter_state", stringFromByteArray(this->tilesetEditorSplitterState)); - map.insert("palette_editor_geometry", stringFromByteArray(this->paletteEditorGeometry)); - map.insert("palette_editor_state", stringFromByteArray(this->paletteEditorState)); - map.insert("region_map_editor_geometry", stringFromByteArray(this->regionMapEditorGeometry)); - map.insert("region_map_editor_state", stringFromByteArray(this->regionMapEditorState)); - map.insert("project_settings_editor_geometry", stringFromByteArray(this->projectSettingsEditorGeometry)); - map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState)); - map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); - map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); - map.insert("wild_mon_chart_geometry", stringFromByteArray(this->wildMonChartGeometry)); - map.insert("new_map_dialog_geometry", stringFromByteArray(this->newMapDialogGeometry)); - map.insert("new_layout_dialog_geometry", stringFromByteArray(this->newLayoutDialogGeometry)); - map.insert("mirror_connecting_maps", this->mirrorConnectingMaps ? "1" : "0"); - map.insert("show_dive_emerge_maps", this->showDiveEmergeMaps ? "1" : "0"); - map.insert("dive_emerge_map_opacity", QString::number(this->diveEmergeMapOpacity)); - map.insert("dive_map_opacity", QString::number(this->diveMapOpacity)); - map.insert("emerge_map_opacity", QString::number(this->emergeMapOpacity)); - map.insert("collision_opacity", QString::number(this->collisionOpacity)); - map.insert("collision_zoom", QString::number(this->collisionZoom)); - map.insert("metatiles_zoom", QString::number(this->metatilesZoom)); - map.insert("tileset_editor_metatiles_zoom", QString::number(this->tilesetEditorMetatilesZoom)); - map.insert("tileset_editor_tiles_zoom", QString::number(this->tilesetEditorTilesZoom)); - map.insert("tileset_editor_layer_orientation", QString::number(this->tilesetEditorLayerOrientation)); - map.insert("show_player_view", this->showPlayerView ? "1" : "0"); - map.insert("show_cursor_tile", this->showCursorTile ? "1" : "0"); - map.insert("show_border", this->showBorder ? "1" : "0"); - map.insert("show_grid", this->showGrid ? "1" : "0"); - map.insert("show_tileset_editor_metatile_grid", this->showTilesetEditorMetatileGrid ? "1" : "0"); - map.insert("show_tileset_editor_layer_grid", this->showTilesetEditorLayerGrid ? "1" : "0"); - map.insert("show_tileset_editor_divider", this->showTilesetEditorDivider ? "1" : "0"); - map.insert("show_tileset_editor_raw_attributes", this->showTilesetEditorRawAttributes ? "1" : "0"); - map.insert("show_palette_editor_unused_colors", this->showPaletteEditorUnusedColors ? "1" : "0"); - map.insert("monitor_files", this->monitorFiles ? "1" : "0"); - map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0"); - map.insert("new_map_header_section_expanded", this->newMapHeaderSectionExpanded ? "1" : "0"); - map.insert("theme", this->theme); - map.insert("wild_mon_chart_theme", this->wildMonChartTheme); - map.insert("text_editor_open_directory", this->textEditorOpenFolder); - map.insert("text_editor_goto_line", this->textEditorGotoLine); - map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth)); - map.insert("project_settings_tab", QString::number(this->projectSettingsTab)); - map.insert("script_autocomplete_mode", QString::number(this->scriptAutocompleteMode)); - map.insert("warp_behavior_warning_disabled", QString::number(this->warpBehaviorWarningDisabled)); - map.insert("event_delete_warning_disabled", QString::number(this->eventDeleteWarningDisabled)); - map.insert("event_overlay_enabled", QString::number(this->eventOverlayEnabled)); - map.insert("check_for_updates", QString::number(this->checkForUpdates)); - map.insert("show_project_loading_screen", QString::number(this->showProjectLoadingScreen)); - map.insert("last_update_check_time", this->lastUpdateCheckTime.toUTC().toString()); - map.insert("last_update_check_version", this->lastUpdateCheckVersion.toString()); - for (auto i = this->rateLimitTimes.cbegin(), end = this->rateLimitTimes.cend(); i != end; i++){ - // Only include rate limit times that are still active (i.e., in the future) - const QDateTime time = i.value(); - if (!time.isNull() && time > QDateTime::currentDateTime()) - map.insert("rate_limit_time/" + i.key().toString(), time.toUTC().toString()); - } - map.insert("event_selection_shape_mode", (this->eventSelectionShapeMode == QGraphicsPixmapItem::MaskShape) ? "mask" : "bounding_rect"); - map.insert("shown_in_game_reload_message", this->shownInGameReloadMessage ? "1" : "0"); - map.insert("grid_width", QString::number(this->gridSettings.width)); - map.insert("grid_height", QString::number(this->gridSettings.height)); - map.insert("grid_x", QString::number(this->gridSettings.offsetX)); - map.insert("grid_y", QString::number(this->gridSettings.offsetY)); - map.insert("grid_style", GridSettings::getStyleName(this->gridSettings.style)); - map.insert("grid_color", toConfigColor(this->gridSettings.color)); - - QStringList logTypesStrings; - for (const auto &type : this->statusBarLogTypes) { - logTypesStrings.append(QString::number(type)); - } - map.insert("status_bar_log_types", logTypesStrings.join(",")); - map.insert("application_font", this->applicationFont.toString()); - map.insert("map_list_font", this->mapListFont.toString()); - map.insert("image_export_color_space_id", QString::number(this->imageExportColorSpaceId)); - for (auto it = this->trustedScriptHashes.constBegin(); it != this->trustedScriptHashes.constEnd(); it++) { - if (it.value().isEmpty() || it.key().isEmpty()) continue; - map.insert("trusted_script_hash/" + it.key(), it.value()); - } - return map; -} - -QString PorymapConfig::stringFromByteArray(const QByteArray &bytearray) { - QString ret; - for (const auto &ch : bytearray) { - ret += QString::number(static_cast(ch)) + ":"; - } - return ret; -} - -QByteArray PorymapConfig::bytesFromString(const QString &in) { - QByteArray ba; - QStringList split = in.split(":", Qt::SkipEmptyParts); - for (const auto &ch : split) { - ba.append(static_cast(ch.toInt())); - } - return ba; -} - -void PorymapConfig::addRecentProject(QString project) { - this->recentProjects.removeOne(project); - this->recentProjects.prepend(project); -} - -void PorymapConfig::setRecentProjects(QStringList projects) { - this->recentProjects = projects; -} - -QString PorymapConfig::getRecentProject() { - return this->recentProjects.value(0); -} - -QStringList PorymapConfig::getRecentProjects() { - return this->recentProjects; -} - -void PorymapConfig::setMainGeometry(QByteArray mainWindowGeometry_, QByteArray mainWindowState_, - QByteArray mapSplitterState_, QByteArray mainSplitterState_, QByteArray metatilesSplitterState_) { - this->mainWindowGeometry = mainWindowGeometry_; - this->mainWindowState = mainWindowState_; - this->mapSplitterState = mapSplitterState_; - this->mainSplitterState = mainSplitterState_; - this->metatilesSplitterState = metatilesSplitterState_; -} - -void PorymapConfig::setTilesetEditorGeometry(QByteArray tilesetEditorGeometry_, QByteArray tilesetEditorState_, - QByteArray tilesetEditorSplitterState_) { - this->tilesetEditorGeometry = tilesetEditorGeometry_; - this->tilesetEditorState = tilesetEditorState_; - this->tilesetEditorSplitterState = tilesetEditorSplitterState_; -} - -void PorymapConfig::setPaletteEditorGeometry(QByteArray paletteEditorGeometry_, QByteArray paletteEditorState_) { - this->paletteEditorGeometry = paletteEditorGeometry_; - this->paletteEditorState = paletteEditorState_; -} - -void PorymapConfig::setRegionMapEditorGeometry(QByteArray regionMapEditorGeometry_, QByteArray regionMapEditorState_) { - this->regionMapEditorGeometry = regionMapEditorGeometry_; - this->regionMapEditorState = regionMapEditorState_; -} - -void PorymapConfig::setProjectSettingsEditorGeometry(QByteArray projectSettingsEditorGeometry_, QByteArray projectSettingsEditorState_) { - this->projectSettingsEditorGeometry = projectSettingsEditorGeometry_; - this->projectSettingsEditorState = projectSettingsEditorState_; -} - -void PorymapConfig::setCustomScriptsEditorGeometry(QByteArray customScriptsEditorGeometry_, QByteArray customScriptsEditorState_) { - this->customScriptsEditorGeometry = customScriptsEditorGeometry_; - this->customScriptsEditorState = customScriptsEditorState_; -} - -QMap PorymapConfig::getMainGeometry() { - QMap geometry; - - geometry.insert("main_window_geometry", this->mainWindowGeometry); - geometry.insert("main_window_state", this->mainWindowState); - geometry.insert("map_splitter_state", this->mapSplitterState); - geometry.insert("main_splitter_state", this->mainSplitterState); - geometry.insert("metatiles_splitter_state", this->metatilesSplitterState); - - return geometry; -} - -QMap PorymapConfig::getTilesetEditorGeometry() { - QMap geometry; - - geometry.insert("tileset_editor_geometry", this->tilesetEditorGeometry); - geometry.insert("tileset_editor_state", this->tilesetEditorState); - geometry.insert("tileset_editor_splitter_state", this->tilesetEditorSplitterState); - - return geometry; -} - -QMap PorymapConfig::getPaletteEditorGeometry() { - QMap geometry; - - geometry.insert("palette_editor_geometry", this->paletteEditorGeometry); - geometry.insert("palette_editor_state", this->paletteEditorState); - - return geometry; -} - -QMap PorymapConfig::getRegionMapEditorGeometry() { - QMap geometry; - - geometry.insert("region_map_editor_geometry", this->regionMapEditorGeometry); - geometry.insert("region_map_editor_state", this->regionMapEditorState); - - return geometry; -} - -QMap PorymapConfig::getProjectSettingsEditorGeometry() { - QMap geometry; - - geometry.insert("project_settings_editor_geometry", this->projectSettingsEditorGeometry); - geometry.insert("project_settings_editor_state", this->projectSettingsEditorState); - - return geometry; -} - -QMap PorymapConfig::getCustomScriptsEditorGeometry() { - QMap geometry; - - geometry.insert("custom_scripts_editor_geometry", this->customScriptsEditorGeometry); - geometry.insert("custom_scripts_editor_state", this->customScriptsEditorState); - - return geometry; -} - -const QStringList ProjectConfig::versionStrings = { - "pokeruby", - "pokefirered", - "pokeemerald", -}; - -const QMap baseGameVersionMap = { - {BaseGameVersion::pokeruby, ProjectConfig::versionStrings[0]}, - {BaseGameVersion::pokefirered, ProjectConfig::versionStrings[1]}, - {BaseGameVersion::pokeemerald, ProjectConfig::versionStrings[2]}, -}; - -const QMap versionDetectNames = { - {BaseGameVersion::pokeruby, {"ruby", "sapphire"}}, - {BaseGameVersion::pokefirered, {"firered", "leafgreen"}}, - {BaseGameVersion::pokeemerald, {"emerald"}}, -}; - -// 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. -BaseGameVersion ProjectConfig::stringToBaseGameVersion(const QString &string) { - BaseGameVersion version = BaseGameVersion::none; - for (auto i = versionDetectNames.cbegin(), end = versionDetectNames.cend(); i != end; i++) { - // Compare the given string to all the possible names for this game version - const QStringList names = i.value(); - for (auto name : names) { - if (string.contains(name)) { - if (version != BaseGameVersion::none) { - // The given string matches multiple versions, so we can't be sure which it is. - return BaseGameVersion::none; - } - version = i.key(); - break; - } - } - } - // We finished checking the names for each version; the name either matched 1 version or none. - return version; -} - -QString ProjectConfig::getPlayerIconPath(BaseGameVersion baseGameVersion, int character) { - switch (baseGameVersion) { - case BaseGameVersion::pokeemerald: { - static const QStringList paths = { QStringLiteral(":/icons/player/brendan_em.ico"), - QStringLiteral(":/icons/player/may_em.ico"), }; - return paths.value(character); - } - case BaseGameVersion::pokefirered: { - static const QStringList paths = { QStringLiteral(":/icons/player/red.ico"), - QStringLiteral(":/icons/player/green.ico"), }; - return paths.value(character); - } - case BaseGameVersion::pokeruby: { - static const QStringList paths = { QStringLiteral(":/icons/player/brendan_rs.ico"), - QStringLiteral(":/icons/player/may_rs.ico"), }; - return paths.value(character); - } - default: break; - } - return QString(); -} - -QIcon ProjectConfig::getPlayerIcon(BaseGameVersion baseGameVersion, int character) { - return QIcon(getPlayerIconPath(baseGameVersion, character)); -} - -ProjectConfig projectConfig; - -ProjectConfig::ProjectConfig() : KeyValueConfigBase(QStringLiteral("porymap.project.cfg")) { - reset(); -} - -void ProjectConfig::parseConfigKeyValue(QString key, QString value) { - if (key == "base_game_version") { - this->baseGameVersion = this->stringToBaseGameVersion(value.toLower()); - if (this->baseGameVersion == BaseGameVersion::none) { - logWarn(QString("Invalid config value for base_game_version: '%1'. Must be 'pokeruby', 'pokefirered' or 'pokeemerald'.").arg(value)); - this->baseGameVersion = BaseGameVersion::pokeemerald; - } - } else if (key == "use_poryscript") { - this->usePoryScript = getConfigBool(key, value); - } else if (key == "use_custom_border_size") { - this->useCustomBorderSize = getConfigBool(key, value); - } else if (key == "enable_event_weather_trigger") { - this->eventWeatherTriggerEnabled = getConfigBool(key, value); - } else if (key == "enable_event_secret_base") { - this->eventSecretBaseEnabled = getConfigBool(key, value); - } else if (key == "enable_hidden_item_quantity") { - this->hiddenItemQuantityEnabled = getConfigBool(key, value); - } else if (key == "enable_hidden_item_requires_itemfinder") { - this->hiddenItemRequiresItemfinderEnabled = getConfigBool(key, value); - } else if (key == "enable_heal_location_respawn_data") { - this->healLocationRespawnDataEnabled = getConfigBool(key, value); - } else if (key == "enable_event_clone_object" || key == "enable_object_event_in_connection") { - this->eventCloneObjectEnabled = getConfigBool(key, value); - key = "enable_event_clone_object"; // Backwards compatibiliy, replace old name above - } else if (key == "enable_floor_number") { - this->floorNumberEnabled = getConfigBool(key, value); - } else if (key == "create_map_text_file") { - this->createMapTextFileEnabled = getConfigBool(key, value); - } else if (key == "enable_triple_layer_metatiles") { - this->tripleLayerMetatilesEnabled = getConfigBool(key, value); - } else if (key == "default_metatile") { - this->defaultMetatileId = getConfigUint32(key, value, 0, Block::maxValue); - } else if (key == "default_elevation") { - this->defaultElevation = getConfigUint32(key, value, 0, Block::maxValue); - } else if (key == "default_collision") { - this->defaultCollision = getConfigUint32(key, value, 0, Block::maxValue); - } else if (key == "default_map_width") { - this->defaultMapSize.setWidth(getConfigInteger(key, value, 1)); - } else if (key == "default_map_height") { - this->defaultMapSize.setHeight(getConfigInteger(key, value, 1)); - } else if (key == "new_map_border_metatiles") { - this->newMapBorderMetatileIds.clear(); - QList metatileIds = value.split(","); - for (int i = 0; i < metatileIds.size(); i++) { - int metatileId = getConfigUint32(key, 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 = getConfigInteger(key, value, 1, 4, 2); - if (size & (size - 1)) { - logWarn(QString("Invalid config value for %1: must be 1, 2, or 4").arg(key)); - return; // Don't set a default now, it will be set later based on the base game version - } - this->metatileAttributesSize = size; - } else if (key == "metatile_behavior_mask") { - this->metatileBehaviorMask = getConfigUint32(key, value); - } else if (key == "metatile_terrain_type_mask") { - this->metatileTerrainTypeMask = getConfigUint32(key, value); - } else if (key == "metatile_encounter_type_mask") { - this->metatileEncounterTypeMask = getConfigUint32(key, value); - } else if (key == "metatile_layer_type_mask") { - this->metatileLayerTypeMask = getConfigUint32(key, value); - } else if (key == "block_metatile_id_mask") { - this->blockMetatileIdMask = getConfigUint32(key, value, 0, Block::maxValue); - } else if (key == "block_collision_mask") { - this->blockCollisionMask = getConfigUint32(key, value, 0, Block::maxValue); - } else if (key == "block_elevation_mask") { - this->blockElevationMask = getConfigUint32(key, value, 0, Block::maxValue); - } else if (key == "unused_tile_normal") { - this->unusedTileNormal = getConfigUint32(key, value, 0, Tile::maxValue); - } else if (key == "unused_tile_covered") { - this->unusedTileCovered = getConfigUint32(key, value, 0, Tile::maxValue); - } else if (key == "unused_tile_split") { - this->unusedTileSplit = getConfigUint32(key, value, 0, Tile::maxValue); - } else if (key == "enable_map_allow_flags") { - this->mapAllowFlagsEnabled = getConfigBool(key, value); -#ifdef CONFIG_BACKWARDS_COMPATABILITY - } else if (key == "recent_map_or_layout") { - userConfig.recentMapOrLayout = value; - } else if (key == "use_encounter_json") { - userConfig.useEncounterJson = getConfigBool(key, value); - } else if (key == "custom_scripts") { - userConfig.parseCustomScripts(value); -#endif - } else if (key.startsWith("path/")) { - auto k = reverseDefaultPaths(key.mid(QStringLiteral("path/").length())); - if (k != static_cast(-1)) { - this->setFilePath(k, value); - } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); - } - } else if (key.startsWith("ident/")) { - auto identifierId = reverseDefaultIdentifier(key.mid(QStringLiteral("ident/").length())); - if (identifierId != static_cast(-1)) { - this->setIdentifier(identifierId, value); - } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); - } - } 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 == "prefabs_filepath") { - this->prefabFilepath = value; - } else if (key == "prefabs_import_prompted") { - this->prefabImportPrompted = getConfigBool(key, value); - } else if (key == "tilesets_have_callback") { - this->tilesetsHaveCallback = getConfigBool(key, value); - } else if (key == "tilesets_have_is_compressed") { - this->tilesetsHaveIsCompressed = getConfigBool(key, value); -#ifdef CONFIG_BACKWARDS_COMPATABILITY - // Old setting replaced by transparency_color - } else if (key == "set_transparent_pixels_black") { - this->transparencyColor = getConfigBool(key, value) ? QColor(Qt::black) : QColor(); -#endif - } else if (key == "transparency_color") { - this->transparencyColor = getConfigColor(key, value); - } else if (key == "preserve_matching_only_data") { - this->preserveMatchingOnlyData = getConfigBool(key, 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(getConfigInteger(key, value, 1, Block::maxValue)); - } else if (key == "collision_sheet_height") { - this->collisionSheetSize.setHeight(getConfigInteger(key, value, 1, Block::maxValue)); - } else if (key == "player_view_north") { - this->playerViewDistance.setTop(getConfigInteger(key, value, 0, INT_MAX, GBA_V_DIST_TO_CENTER)); - } else if (key == "player_view_south") { - this->playerViewDistance.setBottom(getConfigInteger(key, value, 0, INT_MAX, GBA_V_DIST_TO_CENTER)); - } else if (key == "player_view_west") { - this->playerViewDistance.setLeft(getConfigInteger(key, value, 0, INT_MAX, GBA_H_DIST_TO_CENTER)); - } else if (key == "player_view_east") { - this->playerViewDistance.setRight(getConfigInteger(key, 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.append(getConfigUint32(key, s)); - } else if (key == "max_events_per_group") { - this->maxEventsPerGroup = getConfigInteger(key, value, 1, INT_MAX, 255); - } else if (key == "forced_major_version") { - this->forcedMajorVersion = getConfigInteger(key, value); - } else if (key == "metatile_selector_width") { - this->metatileSelectorWidth = getConfigInteger(key, value, 1, INT_MAX, 8); - } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); - } - readKeys.append(key); -} - -// Restore config to version-specific defaults -void::ProjectConfig::reset(BaseGameVersion baseGameVersion) { - this->reset(); - this->baseGameVersion = baseGameVersion; - this->setUnreadKeys(); -} - -void ProjectConfig::setUnreadKeys() { - // Set game-version specific defaults for any config field that wasn't read - bool isPokefirered = this->baseGameVersion == BaseGameVersion::pokefirered; - if (!readKeys.contains("use_custom_border_size")) this->useCustomBorderSize = isPokefirered; - if (!readKeys.contains("enable_event_weather_trigger")) this->eventWeatherTriggerEnabled = !isPokefirered; - if (!readKeys.contains("enable_event_secret_base")) this->eventSecretBaseEnabled = !isPokefirered; - if (!readKeys.contains("enable_hidden_item_quantity")) this->hiddenItemQuantityEnabled = isPokefirered; - if (!readKeys.contains("enable_hidden_item_requires_itemfinder")) this->hiddenItemRequiresItemfinderEnabled = isPokefirered; - if (!readKeys.contains("enable_heal_location_respawn_data")) this->healLocationRespawnDataEnabled = isPokefirered; - if (!readKeys.contains("enable_event_clone_object")) this->eventCloneObjectEnabled = isPokefirered; - if (!readKeys.contains("enable_floor_number")) this->floorNumberEnabled = isPokefirered; - if (!readKeys.contains("create_map_text_file")) this->createMapTextFileEnabled = (this->baseGameVersion != BaseGameVersion::pokeemerald); - if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? defaultBorder_FRLG : defaultBorder_RSE; - if (!readKeys.contains("default_secondary_tileset")) this->defaultSecondaryTileset = isPokefirered ? "gTileset_PalletTown" : "gTileset_Petalburg"; - if (!readKeys.contains("metatile_attributes_size")) this->metatileAttributesSize = Metatile::getDefaultAttributesSize(this->baseGameVersion); - if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::Behavior); - if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::TerrainType); - if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::EncounterType); - if (!readKeys.contains("metatile_layer_type_mask")) this->metatileLayerTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::LayerType); - if (!readKeys.contains("enable_map_allow_flags")) this->mapAllowFlagsEnabled = (this->baseGameVersion != BaseGameVersion::pokeruby); - if (!readKeys.contains("warp_behaviors")) this->warpBehaviors = isPokefirered ? defaultWarpBehaviors_FRLG : defaultWarpBehaviors_RSE; -} - -QMap ProjectConfig::getKeyValueMap() { - QMap map; - map.insert("base_game_version", baseGameVersionMap.value(this->baseGameVersion)); - map.insert("use_poryscript", QString::number(this->usePoryScript)); - map.insert("use_custom_border_size", QString::number(this->useCustomBorderSize)); - map.insert("enable_event_weather_trigger", QString::number(this->eventWeatherTriggerEnabled)); - map.insert("enable_event_secret_base", QString::number(this->eventSecretBaseEnabled)); - map.insert("enable_hidden_item_quantity", QString::number(this->hiddenItemQuantityEnabled)); - map.insert("enable_hidden_item_requires_itemfinder", QString::number(this->hiddenItemRequiresItemfinderEnabled)); - map.insert("enable_heal_location_respawn_data", QString::number(this->healLocationRespawnDataEnabled)); - map.insert("enable_event_clone_object", QString::number(this->eventCloneObjectEnabled)); - map.insert("enable_floor_number", QString::number(this->floorNumberEnabled)); - map.insert("create_map_text_file", QString::number(this->createMapTextFileEnabled)); - map.insert("enable_triple_layer_metatiles", QString::number(this->tripleLayerMetatilesEnabled)); - map.insert("default_metatile", Metatile::getMetatileIdString(this->defaultMetatileId)); - map.insert("default_elevation", QString::number(this->defaultElevation)); - map.insert("default_collision", QString::number(this->defaultCollision)); - map.insert("default_map_width", QString::number(this->defaultMapSize.width())); - map.insert("default_map_height", QString::number(this->defaultMapSize.height())); - map.insert("new_map_border_metatiles", Metatile::getMetatileIdStrings(this->newMapBorderMetatileIds)); - map.insert("default_primary_tileset", this->defaultPrimaryTileset); - map.insert("default_secondary_tileset", this->defaultSecondaryTileset); - map.insert("prefabs_filepath", this->prefabFilepath); - map.insert("prefabs_import_prompted", QString::number(this->prefabImportPrompted)); - for (auto it = this->filePaths.constKeyValueBegin(); it != this->filePaths.constKeyValueEnd(); ++it) { - map.insert("path/"+defaultPaths[(*it).first].first, (*it).second); - } - map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback)); - map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed)); - map.insert("transparency_color", toConfigColor(this->transparencyColor)); - map.insert("preserve_matching_only_data", QString::number(this->preserveMatchingOnlyData)); - map.insert("metatile_attributes_size", QString::number(this->metatileAttributesSize)); - map.insert("metatile_behavior_mask", Util::toHexString(this->metatileBehaviorMask)); - map.insert("metatile_terrain_type_mask", Util::toHexString(this->metatileTerrainTypeMask)); - map.insert("metatile_encounter_type_mask", Util::toHexString(this->metatileEncounterTypeMask)); - map.insert("metatile_layer_type_mask", Util::toHexString(this->metatileLayerTypeMask)); - map.insert("block_metatile_id_mask", Util::toHexString(this->blockMetatileIdMask)); - map.insert("block_collision_mask", Util::toHexString(this->blockCollisionMask)); - map.insert("block_elevation_mask", Util::toHexString(this->blockElevationMask)); - map.insert("unused_tile_normal", Util::toHexString(this->unusedTileNormal)); - map.insert("unused_tile_covered", Util::toHexString(this->unusedTileCovered)); - map.insert("unused_tile_split", Util::toHexString(this->unusedTileSplit)); - map.insert("enable_map_allow_flags", QString::number(this->mapAllowFlagsEnabled)); - map.insert("event_icon_path_object", this->eventIconPaths[Event::Group::Object]); - map.insert("event_icon_path_warp", this->eventIconPaths[Event::Group::Warp]); - map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]); - map.insert("event_icon_path_bg", this->eventIconPaths[Event::Group::Bg]); - map.insert("event_icon_path_heal", this->eventIconPaths[Event::Group::Heal]); - for (auto it = this->pokemonIconPaths.constBegin(); it != this->pokemonIconPaths.constEnd(); it++) { - const QString path = it.value(); - if (!path.isEmpty()) map.insert("pokemon_icon_path/" + it.key(), path); - } - for (auto it = this->globalConstants.constBegin(); it != this->globalConstants.constEnd(); it++) { - map.insert("global_constant/" + it.key(), it.value()); - } - map.insert("global_constants_filepaths", this->globalConstantsFilepaths.join(",")); - for (auto it = this->identifiers.constBegin(); it != this->identifiers.constEnd(); it++) { - map.insert("ident/"+defaultIdentifiers.value(it.key()).first, it.value()); - } - map.insert("events_tab_icon_path", this->eventsTabIconPath); - map.insert("collision_sheet_path", this->collisionSheetPath); - map.insert("collision_sheet_width", QString::number(this->collisionSheetSize.width())); - map.insert("collision_sheet_height", QString::number(this->collisionSheetSize.height())); - map.insert("player_view_north", QString::number(this->playerViewDistance.top())); - map.insert("player_view_south", QString::number(this->playerViewDistance.bottom())); - map.insert("player_view_west", QString::number(this->playerViewDistance.left())); - map.insert("player_view_east", QString::number(this->playerViewDistance.right())); - QStringList warpBehaviorStrs; - for (const auto &value : this->warpBehaviors) - warpBehaviorStrs.append("0x" + QString("%1").arg(value, 2, 16, QChar('0')).toUpper()); - map.insert("warp_behaviors", warpBehaviorStrs.join(",")); - map.insert("max_events_per_group", QString::number(this->maxEventsPerGroup)); - map.insert("forced_major_version", QString::number(this->forcedMajorVersion)); - map.insert("metatile_selector_width", QString::number(this->metatileSelectorWidth)); - - return map; -} - -void ProjectConfig::init() { - QString dirName = QDir(this->projectDir()).dirName().toLower(); - - BaseGameVersion version = stringToBaseGameVersion(dirName); - if (version != BaseGameVersion::none) { - this->baseGameVersion = version; - logInfo(QString("Auto-detected base_game_version as '%1'").arg(getBaseGameVersionString(version))); - } else { - QDialog dialog(nullptr, Qt::WindowTitleHint); - dialog.setWindowTitle("Project Configuration"); - dialog.setWindowModality(Qt::NonModal); - - QFormLayout form(&dialog); - - QComboBox *baseGameVersionComboBox = new QComboBox(); - baseGameVersionComboBox->addItem("pokeruby", BaseGameVersion::pokeruby); - baseGameVersionComboBox->addItem("pokefirered", BaseGameVersion::pokefirered); - baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); - form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); - - // TODO: Add an 'Advanced' button to open the project settings window (with some settings disabled) - - 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(baseGameVersionComboBox->currentData().toInt()); - } else { - logWarn(QString("No base_game_version selected, using default '%1'").arg(getBaseGameVersionString(this->baseGameVersion))); - } - } - this->setUnreadKeys(); // Initialize version-specific options -} - -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, 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(); -} - -QString ProjectConfig::getBaseGameVersionString(BaseGameVersion version) { - if (!baseGameVersionMap.contains(version)) { - version = BaseGameVersion::pokeemerald; - } - return baseGameVersionMap.value(version); -} - -QString ProjectConfig::getBaseGameVersionString() { - return this->getBaseGameVersionString(this->baseGameVersion); -} - -int ProjectConfig::getNumLayersInMetatile() { - return this->tripleLayerMetatilesEnabled ? 3 : 2; -} - -int ProjectConfig::getNumTilesInMetatile() { - return getNumLayersInMetatile() * Metatile::tilesPerLayer(); -} - -void ProjectConfig::setEventIconPath(Event::Group group, const QString &path) { - this->eventIconPaths[group] = path; -} - -QString ProjectConfig::getEventIconPath(Event::Group group) { - return this->eventIconPaths.value(group); -} - -void ProjectConfig::setPokemonIconPath(const QString &species, const QString &path) { - this->pokemonIconPaths[species] = path; -} - -QString ProjectConfig::getPokemonIconPath(const QString &species) { - return this->pokemonIconPaths.value(species); -} - -QMap ProjectConfig::getPokemonIconPaths() { - return this->pokemonIconPaths; -} - -UserConfig userConfig; - -UserConfig::UserConfig() : KeyValueConfigBase(QStringLiteral("porymap.user.cfg")) { - reset(); -} - -void UserConfig::parseConfigKeyValue(QString key, QString value) { - if (key == "recent_map_or_layout") { - this->recentMapOrLayout = value; - } else if (key == "use_encounter_json") { - this->useEncounterJson = getConfigBool(key, value); - } else if (key == "custom_scripts") { - this->parseCustomScripts(value); - } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); - } - readKeys.append(key); -} - -void UserConfig::setUnreadKeys() { -} - -QMap UserConfig::getKeyValueMap() { - QMap map; - map.insert("recent_map_or_layout", this->recentMapOrLayout); - map.insert("use_encounter_json", QString::number(this->useEncounterJson)); - map.insert("custom_scripts", this->outputCustomScripts()); - return map; -} - -void UserConfig::init() { - this->useEncounterJson = true; - this->customScripts.clear(); -} - -// 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. -void UserConfig::parseCustomScripts(QString input) { - this->customScripts.clear(); - QList 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. - this->customScripts.insert(path, enabled); - } - } -} - -// Inverse of UserConfig::parseCustomScripts -QString UserConfig::outputCustomScripts() { - QStringList list; - QMapIterator i(this->customScripts); - while (i.hasNext()) { - i.next(); - list.append(QString("%1:%2").arg(i.key()).arg(i.value() ? "1" : "0")); - } - return list.join(","); -} - -void UserConfig::setCustomScripts(QStringList scripts, QList enabled) { - this->customScripts.clear(); - size_t size = qMin(scripts.length(), enabled.length()); - for (size_t i = 0; i < size; i++) - this->customScripts.insert(scripts.at(i), enabled.at(i)); -} - -QStringList UserConfig::getCustomScriptPaths() { - return this->customScripts.keys(); -} - -QList UserConfig::getCustomScriptsEnabled() { - return this->customScripts.values(); -} - -ShortcutsConfig shortcutsConfig; - -ShortcutsConfig::ShortcutsConfig() : KeyValueConfigBase(QStringLiteral("porymap.shortcuts.cfg")), - user_shortcuts({ }), - default_shortcuts({ }) { -} - -void ShortcutsConfig::parseConfigKeyValue(QString key, QString value) { - QStringList keySequences = value.split(' '); - for (auto keySequence : keySequences) - user_shortcuts.insert(key, keySequence); -} - -QMap ShortcutsConfig::getKeyValueMap() { - QMap map; - for (auto cfg_key : user_shortcuts.uniqueKeys()) { - auto keySequences = user_shortcuts.values(cfg_key); - QStringList keySequenceStrings; - for (auto keySequence : keySequences) - keySequenceStrings.append(keySequence.toString()); - map.insert(cfg_key, keySequenceStrings.join(' ')); - } - return map; -} - -void ShortcutsConfig::setDefaultShortcuts(const QObjectList &objects) { - storeShortcutsFromList(StoreType::Default, objects); -} - -QList 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 &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 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 &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(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 ShortcutsConfig::currentShortcuts(const QObject *object) const { - if (object->inherits("QAction")) { - const auto *action = qobject_cast(object); - return action->shortcuts(); - } else if (object->inherits("Shortcut")) { - const auto *shortcut = qobject_cast(object); - return shortcut->keys(); - } else if (object->inherits("QShortcut")) { - const auto *qshortcut = qobject_cast(object); - return { qshortcut->key() }; - } else if (object->property("shortcut").isValid()) { - return { object->property("shortcut").value() }; - } else { - return { }; - } -} diff --git a/src/config/keyvalueconfigbase.cpp b/src/config/keyvalueconfigbase.cpp new file mode 100644 index 00000000..d398ed57 --- /dev/null +++ b/src/config/keyvalueconfigbase.cpp @@ -0,0 +1,105 @@ +#include "keyvalueconfigbase.h" +#include "log.h" + +#include +#include +#include + +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; +} diff --git a/src/config/legacy.cpp b/src/config/legacy.cpp new file mode 100644 index 00000000..f1530b90 --- /dev/null +++ b/src/config/legacy.cpp @@ -0,0 +1,419 @@ +#include "config.h" +#include "log.h" +#include "tile.h" +#include "block.h" + +#include +#include +#include + + +QString getLegacyFilename(const QString &newFilename) { + static const QMap 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("^(?[^=]+)=(?.*)$"); + 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(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(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/(?.+)"); + 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(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 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(-1)) { + this->setFilePath(k, value); + } + } else if (key.startsWith("ident/")) { + auto identifierId = reverseDefaultIdentifier(key.mid(QStringLiteral("ident/").length())); + if (identifierId != static_cast(-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 parseCustomScripts(const QString &input) { + QMap customScripts; + const QList 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 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; +} diff --git a/src/config/porymapconfig.cpp b/src/config/porymapconfig.cpp new file mode 100644 index 00000000..b93d32f0 --- /dev/null +++ b/src/config/porymapconfig.cpp @@ -0,0 +1,106 @@ +#include "porymapconfig.h" + +#include +#include + +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(widget); + if (mainWindow) this->savedGeometryMap.insert(stateKey, mainWindow->saveState()); + else { + auto splitter = qobject_cast(widget); + if (splitter) this->savedGeometryMap.insert(stateKey, splitter->saveState()); + } + if (recursive) { + for (const auto splitter : widget->findChildren()) { + 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(widget); + if (mainWindow) mainWindow->restoreState(it.value()); + else { + auto splitter = qobject_cast(widget); + if (splitter) splitter->restoreState(it.value()); + } + } + if (recursive) { + for (const auto splitter : widget->findChildren()) { + restoreGeometry(splitter, key + "_", false); + } + } + return true; +} diff --git a/src/config/projectconfig.cpp b/src/config/projectconfig.cpp new file mode 100644 index 00000000..b7eb0321 --- /dev/null +++ b/src/config/projectconfig.cpp @@ -0,0 +1,368 @@ +#include "projectconfig.h" +#include "utility.h" +#include "validator.h" + +#include +#include +#include +#include +#include + +// TODO: This should eventually be contained by each individual Project instance. +ProjectConfig projectConfig; + +const QMap> 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> 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(-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(-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(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(); +} diff --git a/src/config/shortcutsconfig.cpp b/src/config/shortcutsconfig.cpp new file mode 100644 index 00000000..772a62a2 --- /dev/null +++ b/src/config/shortcutsconfig.cpp @@ -0,0 +1,110 @@ +#include "shortcutsconfig.h" +#include "shortcut.h" + +#include +#include + +ShortcutsConfig shortcutsConfig; + +void ShortcutsConfig::loadFromJson(const QJsonObject& obj) { + this->user_shortcuts = Converter>::fromJson(obj); +} + +QJsonObject ShortcutsConfig::toJson() { + return Converter>::toJson(this->user_shortcuts); +} + +QJsonObject ShortcutsConfig::getDefaultJson() const { + return Converter>::toJson(this->default_shortcuts); +} + +void ShortcutsConfig::setDefaultShortcuts(const QObjectList& objects) { + storeShortcutsFromList(StoreType::Default, objects); +} + +QList 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& 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 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& 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(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 ShortcutsConfig::currentShortcuts(const QObject *object) const { + if (object->inherits("QAction")) { + const auto *action = qobject_cast(object); + return action->shortcuts(); + } else if (object->inherits("Shortcut")) { + const auto *shortcut = qobject_cast(object); + return shortcut->keys(); + } else if (object->inherits("QShortcut")) { + const auto *qshortcut = qobject_cast(object); + return { qshortcut->key() }; + } else if (object->property("shortcut").isValid()) { + return { object->property("shortcut").value() }; + } else { + return { }; + } +} diff --git a/src/config/userconfig.cpp b/src/config/userconfig.cpp new file mode 100644 index 00000000..c0f24bef --- /dev/null +++ b/src/config/userconfig.cpp @@ -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(); +} diff --git a/src/core/advancemapparser.cpp b/src/core/advancemapparser.cpp index 04348e96..f020208c 100644 --- a/src/core/advancemapparser.cpp +++ b/src/core/advancemapparser.cpp @@ -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 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'.")); diff --git a/src/core/basegame.cpp b/src/core/basegame.cpp new file mode 100644 index 00000000..0eecb123 --- /dev/null +++ b/src/core/basegame.cpp @@ -0,0 +1,62 @@ +#include "basegame.h" + +#include +#include + +// 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 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 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)); +} diff --git a/src/core/block.cpp b/src/core/block.cpp index fc4fe774..38505018 100644 --- a/src/core/block.cpp +++ b/src/core/block.cpp @@ -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), diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 7cd17cec..539bde5b 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -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(); } diff --git a/src/core/tile.cpp b/src/core/tile.cpp index b8a6217d..1b1a28a8 100644 --- a/src/core/tile.cpp +++ b/src/core/tile.cpp @@ -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); } diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index 34868cbe..4293a729 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -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 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(); @@ -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(); diff --git a/src/core/utility.cpp b/src/core/utility.cpp index 77cf8e3e..b6434c4a 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -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(QColorSpace::SRgb); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)) - // Qt 6.8.0 introduced additional color spaces - int max = static_cast(QColorSpace::Bt2100Hlg); -#else - int max = static_cast(QColorSpace::ProPhotoRgb); -#endif - - if (colorSpaceInt >= min && colorSpaceInt <= max) { - return QColorSpace(static_cast(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) { diff --git a/src/core/version.cpp b/src/core/version.cpp new file mode 100644 index 00000000..07ebbbde --- /dev/null +++ b/src/core/version.cpp @@ -0,0 +1,3 @@ +#include "version.h" + +const QVersionNumber porymapVersion = QVersionNumber::fromString(PORYMAP_VERSION); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 52ea6720..1e03cc78 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -29,6 +29,7 @@ #include "newmapgroupdialog.h" #include "newlocationdialog.h" #include "loadingscreen.h" +#include "version.h" #include #include @@ -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(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 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 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.
" @@ -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); diff --git a/src/project.cpp b/src/project.cpp index f305b572..d1aa6579 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -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(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); } diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index dcd4fc33..7702faa4 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -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) diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index ad861b11..8a196e6c 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -198,7 +198,12 @@ bool ScriptUtility::getSmartPathsEnabled() { } QList ScriptUtility::getCustomScripts() { - return userConfig.getCustomScriptPaths(); + QList paths; + for (const auto& settings : userConfig.customScripts) + paths.append(settings.path); + for (const auto& settings : projectConfig.customScripts) + paths.append(settings.path); + return paths; } QList ScriptUtility::getMetatileLayerOrder() { diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index c7154d90..5b4bca25 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -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 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(); diff --git a/src/ui/citymappixmapitem.cpp b/src/ui/citymappixmapitem.cpp deleted file mode 100644 index c0a96717..00000000 --- a/src/ui/citymappixmapitem.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "citymappixmapitem.h" -#include "imageproviders.h" -#include "config.h" -#include "log.h" - -#include -#include -#include - -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(pos.x()) / 8; - int y = static_cast(pos.y()) / 8; - int index = getIndexAt(x, y); - data[index] = static_cast(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(pos.x()) / 8; - int y = static_cast(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 CityMapPixmapItem::getTiles() { - QVector tiles; - for (auto tile : data) { - tiles.append(tile); - } - return tiles; -} - -void CityMapPixmapItem::setTiles(QVector 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_; -} diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 0b3dca1e..22c13258 100644 --- a/src/ui/customscriptseditor.cpp +++ b/src/ui/customscriptseditor.cpp @@ -5,6 +5,7 @@ #include "editor.h" #include "shortcut.h" #include "filedialog.h" +#include "eventfilters.h" #include @@ -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 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 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(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(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(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 enabledStates; + QList userScripts; + QList 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(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() - ); } diff --git a/src/ui/customscriptslistitem.cpp b/src/ui/customscriptslistitem.cpp index e1452425..ab00ef0b 100644 --- a/src/ui/customscriptslistitem.cpp +++ b/src/ui/customscriptslistitem.cpp @@ -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, + }; +} diff --git a/src/ui/eventfilters.cpp b/src/ui/eventfilters.cpp index 1f3d94f1..d32c5b01 100644 --- a/src/ui/eventfilters.cpp +++ b/src/ui/eventfilters.cpp @@ -1,4 +1,6 @@ #include "eventfilters.h" +#include "config.h" +#include "log.h" #include @@ -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(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; +} diff --git a/src/ui/gridsettings.cpp b/src/ui/gridsettings.cpp index 4e36b60d..d621fb9f 100644 --- a/src/ui/gridsettings.cpp +++ b/src/ui/gridsettings.cpp @@ -74,6 +74,28 @@ QVector GridSettings::getDashPattern(uint length) const { } } +QJsonObject GridSettings::toJson() const { + QJsonObject obj; + obj["width"] = static_cast(this->width); + obj["height"] = static_cast(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) : diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index ab44566e..e507c50e 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -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(); diff --git a/src/ui/metatileimageexporter.cpp b/src/ui/metatileimageexporter.cpp index f7e99fa0..8261dad5 100644 --- a/src/ui/metatileimageexporter.cpp +++ b/src/ui/metatileimageexporter.cpp @@ -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; diff --git a/src/ui/metatilelayersitem.cpp b/src/ui/metatilelayersitem.cpp index 63b1fc54..33c44426 100644 --- a/src/ui/metatilelayersitem.cpp +++ b/src/ui/metatilelayersitem.cpp @@ -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); diff --git a/src/ui/newlayoutdialog.cpp b/src/ui/newlayoutdialog.cpp index de20bb29..16a82645 100644 --- a/src/ui/newlayoutdialog.cpp +++ b/src/ui/newlayoutdialog.cpp @@ -3,6 +3,7 @@ #include "ui_newlayoutdialog.h" #include "config.h" #include "validator.h" +#include "eventfilters.h" #include #include @@ -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; } diff --git a/src/ui/newmapdialog.cpp b/src/ui/newmapdialog.cpp index c8a02802..ea043999 100644 --- a/src/ui/newmapdialog.cpp +++ b/src/ui/newmapdialog.cpp @@ -4,6 +4,7 @@ #include "ui_newmapdialog.h" #include "config.h" #include "validator.h" +#include "eventfilters.h" #include #include @@ -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; } diff --git a/src/ui/paletteeditor.cpp b/src/ui/paletteeditor.cpp index ebc66cde..72a53eb2 100644 --- a/src/ui/paletteeditor.cpp +++ b/src/ui/paletteeditor.cpp @@ -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 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. diff --git a/src/ui/prefab.cpp b/src/ui/prefab.cpp index 7a1e96d2..43da720a 100644 --- a/src/ui/prefab.cpp +++ b/src/ui/prefab.cpp @@ -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; } diff --git a/src/ui/preferenceeditor.cpp b/src/ui/preferenceeditor.cpp index d34a37c6..638fde9b 100644 --- a/src/ui/preferenceeditor.cpp +++ b/src/ui/preferenceeditor.cpp @@ -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(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); diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 3e7123fd..714ad326 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -4,6 +4,7 @@ #include "filedialog.h" #include "newdefinedialog.h" #include "utility.h" +#include "eventfilters.h" #include #include @@ -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::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(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 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()) lineEdit->setText(projectConfig.getCustomFilePath(lineEdit->objectName())); for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren()) @@ -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()) projectConfig.setFilePath(lineEdit->objectName(), lineEdit->text()); for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren()) @@ -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) diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 46b5de0f..5f4cded0 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -7,6 +7,7 @@ #include "config.h" #include "log.h" #include "utility.h" +#include "eventfilters.h" #include #include @@ -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 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 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) { diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp index f2f8083b..8b33d0c9 100644 --- a/src/ui/shortcutseditor.cpp +++ b/src/ui/shortcutseditor.cpp @@ -4,6 +4,7 @@ #include "multikeyedit.h" #include "message.h" #include "log.h" +#include "eventfilters.h" #include #include @@ -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); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 3abbd95a..dc709759 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -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 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() diff --git a/src/ui/updatepromoter.cpp b/src/ui/updatepromoter.cpp index 4b66fbff..cea5b753 100644 --- a/src/ui/updatepromoter.cpp +++ b/src/ui/updatepromoter.cpp @@ -3,6 +3,7 @@ #include "ui_updatepromoter.h" #include "log.h" #include "config.h" +#include "version.h" #include #include diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index bc45415a..5419f6f4 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -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