diff --git a/include/config.h b/include/config.h index 4907014f..b96c17b8 100644 --- a/include/config.h +++ b/include/config.h @@ -17,8 +17,19 @@ #include #include +#include "converter.h" #include "events.h" #include "gridsettings.h" +#include "scriptsettings.h" +#include "log.h" +#include "basegame.h" +#include "orderedset.h" +#include "block.h" + +// TODO: Go through and re-test the default window geometries. +// TODO: Value validation? Make sure anything no longer validated can't cause problems. +// TODO: Documentation +// TODO: File splits extern const QVersionNumber porymapVersion; @@ -26,8 +37,6 @@ extern const QVersionNumber porymapVersion; #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, @@ -37,159 +46,241 @@ enum ScriptAutocompleteMode { 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) + 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); + // Writes the contents of the config to disk. + // Returns true if saving was successful, false otherwise. + virtual bool save(); + + // Loads the contents of the config from disk. + // The file to load from depends on the file name given to + // the constructor, and the root given to setRoot, if any. + // A successful load includes initializing an empty or non-existing file. + virtual bool load(); + + virtual QJsonObject toJson() const; + 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(); } + + struct FieldManager { + QJsonValue (*get)(const KeyValueConfigBase*); + QStringList (*set)(KeyValueConfigBase*, const QJsonValue&); + }; + template + static FieldManager makeFieldManager() { + // Deduce the type from the pointer to the member variable. + using T = std::remove_reference_t().*Member)>; + return FieldManager{ + .get = [](const KeyValueConfigBase* base) -> QJsonValue { + auto c = static_cast(base); + return Converter::toJson(c->*Member); + }, + .set = [](KeyValueConfigBase* base, const QJsonValue& json) { + QStringList errors; + const T value = Converter::fromJson(json, &errors); + if (errors.isEmpty()) static_cast(base)->*Member = value; + return errors; + } + }; + } + virtual const QHash& registeredFields() const { + static QHash empty; + return empty; + } + + 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(); + + // TODO: Make this setting accessible somewhere + bool m_saveAllFields = true; }; 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(); + 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; - 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; + 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; - int projectSettingsTab; - ScriptAutocompleteMode scriptAutocompleteMode; - bool warpBehaviorWarningDisabled; - bool eventDeleteWarningDisabled; - bool eventOverlayEnabled; - bool checkForUpdates; - bool showProjectLoadingScreen; + 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; - QByteArray wildMonChartGeometry; - QByteArray newMapDialogGeometry; - QByteArray newLayoutDialogGeometry; - bool shownInGameReloadMessage; + QGraphicsPixmapItem::ShapeMode eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; + bool shownInGameReloadMessage = false; GridSettings gridSettings; - // Prefer over QSet to prevent shuffling elements when writing the config file. - std::set statusBarLogTypes; + OrderedSet statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; QFont applicationFont; QFont mapListFont; - int imageExportColorSpaceId; +#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. + int 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. + int imageExportColorSpaceId = 0; +#endif QMap trustedScriptHashes; +#define REGISTER(key, member) {QStringLiteral(key), makeFieldManager()} + const QHash& registeredFields() const override { + static const QHash fields = { + REGISTER("reopen_on_launch", reopenOnLaunch), + REGISTER("project_manually_closed", projectManuallyClosed), + REGISTER("map_list_tab", mapListTab), + REGISTER("map_list_edit_groups_enabled", mapListEditGroupsEnabled), + REGISTER("map_list_tabs_hiding_empty_folders", mapListTabsHidingEmptyFolders), + REGISTER("map_list_layouts_sorted", mapListLayoutsSorted), + REGISTER("map_list_locations_sorted", mapListLocationsSorted), + REGISTER("pretty_cursors", prettyCursors), + REGISTER("mirror_connecting_maps", mirrorConnectingMaps), + REGISTER("show_dive_emerge_maps", showDiveEmergeMaps), + REGISTER("dive_emerge_map_opacity", diveEmergeMapOpacity), + REGISTER("dive_map_opacity", diveMapOpacity), + REGISTER("emerge_map_opacity", emergeMapOpacity), + REGISTER("collision_opacity", collisionOpacity), + REGISTER("collision_zoom", collisionZoom), + REGISTER("metatiles_zoom", metatilesZoom), + REGISTER("tileset_editor_metatiles_zoom", tilesetEditorMetatilesZoom), + REGISTER("tileset_editor_tiles_zoom", tilesetEditorTilesZoom), + REGISTER("tileset_editor_layer_orientation", tilesetEditorLayerOrientation), + REGISTER("show_player_view", showPlayerView), + REGISTER("show_cursor_tile", showCursorTile), + REGISTER("show_border", showBorder), + REGISTER("show_grid", showGrid), + REGISTER("show_tileset_editor_metatile_grid", showTilesetEditorMetatileGrid), + REGISTER("show_tileset_editor_layer_grid", showTilesetEditorLayerGrid), + REGISTER("show_tileset_editor_divider", showTilesetEditorDivider), + REGISTER("show_tileset_editor_raw_attributes", showTilesetEditorRawAttributes), + REGISTER("show_palette_editor_unused_colors", showPaletteEditorUnusedColors), + REGISTER("monitor_files", monitorFiles), + REGISTER("tileset_checkerboard_fill", tilesetCheckerboardFill), + REGISTER("new_map_header_section_expanded", newMapHeaderSectionExpanded), + REGISTER("display_ids_hexadecimal", displayIdsHexadecimal), + REGISTER("theme", theme), + REGISTER("wild_mon_chart_theme", wildMonChartTheme), + REGISTER("text_editor_open_folder", textEditorOpenFolder), + REGISTER("text_editor_goto_line", textEditorGotoLine), + REGISTER("palette_editor_bit_depth", paletteEditorBitDepth), + REGISTER("project_settings_tab", projectSettingsTab), + REGISTER("script_autocomplete_mode", scriptAutocompleteMode), + REGISTER("warp_behavior_warning_disabled", warpBehaviorWarningDisabled), + REGISTER("event_delete_warning_disabled", eventDeleteWarningDisabled), + REGISTER("event_overlay_enabled", eventOverlayEnabled), + REGISTER("check_for_updates", checkForUpdates), + REGISTER("show_project_loading_screen", showProjectLoadingScreen), + REGISTER("last_update_check_time", lastUpdateCheckTime), + REGISTER("last_update_check_version", lastUpdateCheckVersion), + REGISTER("rate_limit_times", rateLimitTimes), + REGISTER("event_selection_shape_mode", eventSelectionShapeMode), + REGISTER("shown_in_game_reload_message", shownInGameReloadMessage), + REGISTER("map_grid", gridSettings), + REGISTER("status_bar_log_types", statusBarLogTypes), + REGISTER("application_font", applicationFont), + REGISTER("map_list_font", mapListFont), + REGISTER("image_export_color_space_id", imageExportColorSpaceId), + REGISTER("trusted_script_hashes", trustedScriptHashes), + + REGISTER("recent_projects", recentProjects), + REGISTER("geometry", savedGeometryMap), + REGISTER("geometry_version", geometryVersion), + }; + return fields; + }; +#undef REGISTER + protected: - virtual void parseConfigKeyValue(QString key, QString value) override; - virtual QMap getKeyValueMap() override; - virtual void init() override {}; - virtual void setUnreadKeys() override {}; + virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; + virtual QJsonObject getDefaultJson() const 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; + QMap savedGeometryMap; + int geometryVersion = 0; }; extern PorymapConfig porymapConfig; -enum BaseGameVersion { - none, - pokeruby, - pokefirered, - pokeemerald, -}; - enum ProjectIdentifier { symbol_facing_directions, symbol_obj_event_gfx_pointers, @@ -301,134 +392,154 @@ enum ProjectFilePath { 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(); + ProjectConfig(const QString& root = QString()) : KeyValueConfigBase(QStringLiteral("porymap.project.json")) { + setRoot(root); + } + ProjectConfig(BaseGame::Version version, const QString& root = QString()) : ProjectConfig(root) { + setVersionSpecificDefaults(version); } - 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); + virtual bool save() override; + + virtual void loadFromJson(const QJsonObject& obj) override; 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(); + void setVersionSpecificDefaults(BaseGame::Version baseGameVersion); - 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; + 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; + QString defaultPrimaryTileset = QStringLiteral("gTileset_General"); 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; + 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; - QMargins playerViewDistance; - QList warpBehaviors; - int maxEventsPerGroup; - int forcedMajorVersion; - int metatileSelectorWidth; + 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; - -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; + QList customScripts; QMap eventIconPaths; QMap pokemonIconPaths; + QVersionNumber minimumVersion; + +#define REGISTER(key, member) {QStringLiteral(key), makeFieldManager()} + const QHash& registeredFields() const override { + static const QHash fields = { + REGISTER("base_game_version", baseGameVersion), + REGISTER("use_poryscript", usePoryScript), + REGISTER("use_custom_border_size", useCustomBorderSize), + REGISTER("enable_event_weather_trigger", eventWeatherTriggerEnabled), + REGISTER("enable_event_secret_base", eventSecretBaseEnabled), + REGISTER("enable_hidden_item_quantity", hiddenItemQuantityEnabled), + REGISTER("enable_hidden_item_requires_itemfinder", hiddenItemRequiresItemfinderEnabled), + REGISTER("enable_heal_location_respawn_data", healLocationRespawnDataEnabled), + REGISTER("enable_event_clone_object", eventCloneObjectEnabled), + REGISTER("enable_floor_number", floorNumberEnabled), + REGISTER("create_map_text_file", createMapTextFileEnabled), + REGISTER("enable_triple_layer_metatiles", tripleLayerMetatilesEnabled), + REGISTER("default_metatile_id", defaultMetatileId), + REGISTER("default_elevation", defaultElevation), + REGISTER("default_collision", defaultCollision), + REGISTER("default_map_size", defaultMapSize), + REGISTER("new_map_border_metatiles", newMapBorderMetatileIds), + REGISTER("default_primary_tileset", defaultPrimaryTileset), + REGISTER("default_secondary_tileset", defaultSecondaryTileset), + REGISTER("tilesets_have_callback", tilesetsHaveCallback), + REGISTER("tilesets_have_is_compressed", tilesetsHaveIsCompressed), + REGISTER("transparency_color", transparencyColor), + REGISTER("preserve_matching_only_data", preserveMatchingOnlyData), + REGISTER("metatile_attributes_size", metatileAttributesSize), + REGISTER("metatile_behavior_mask", metatileBehaviorMask), + REGISTER("metatile_terrain_type_mask", metatileTerrainTypeMask), + REGISTER("metatile_encounter_type_mask", metatileEncounterTypeMask), + REGISTER("metatile_layer_type_mask", metatileLayerTypeMask), + REGISTER("block_metatile_id_mask", blockMetatileIdMask), + REGISTER("block_collision_mask", blockCollisionMask), + REGISTER("block_elevation_mask", blockElevationMask), + REGISTER("unused_tile_normal", unusedTileNormal), + REGISTER("unused_tile_covered", unusedTileCovered), + REGISTER("unused_tile_split", unusedTileSplit), + REGISTER("enable_map_allow_flags", mapAllowFlagsEnabled), + REGISTER("events_tab_icon_path", eventsTabIconPath), + REGISTER("collision_sheet_path", collisionSheetPath), + REGISTER("collision_sheet_size", collisionSheetSize), + REGISTER("player_view_distance", playerViewDistance), + REGISTER("warp_behaviors", warpBehaviors), + REGISTER("max_events_per_group", maxEventsPerGroup), + REGISTER("metatile_selector_width", metatileSelectorWidth), + REGISTER("global_constants_filepaths", globalConstantsFilepaths), + REGISTER("global_constants", globalConstants), + REGISTER("custom_scripts", customScripts), + REGISTER("event_icon_paths", eventIconPaths), + REGISTER("pokemon_icon_paths", pokemonIconPaths), + REGISTER("minimum_version", minimumVersion), + + REGISTER("custom_identifiers", identifiers), + REGISTER("custom_file_paths", filePaths), + }; + return fields; + } +#undef REGISTER + +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); + + QMap identifiers; + QMap filePaths; }; extern ProjectConfig projectConfig; @@ -436,36 +547,36 @@ 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(); + 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() - void parseCustomScripts(QString input); - QString outputCustomScripts(); - void setCustomScripts(QStringList scripts, QList enabled); - QStringList getCustomScriptPaths(); - QList getCustomScriptsEnabled(); QString recentMapOrLayout; - bool useEncounterJson; + QString prefabsFilepath; + bool prefabsImportPrompted = false; + bool useEncounterJson = true; + QList customScripts; 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 + virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; + virtual QJsonObject getDefaultJson() const override; -private: - QStringList readKeys; - QMap customScripts; +#define REGISTER(key, member) {QStringLiteral(key), makeFieldManager()} + const QHash& registeredFields() const override { + static const QHash fields = { + REGISTER("recent_map_or_layout", recentMapOrLayout), + REGISTER("prefabs_filepath", prefabsFilepath), + REGISTER("prefabs_import_prompted", prefabsImportPrompted), + REGISTER("use_encounter_json", useEncounterJson), + REGISTER("custom_scripts", customScripts), + }; + return fields; + } +#undef REGISTER }; extern UserConfig userConfig; @@ -476,26 +587,24 @@ class Shortcut; class ShortcutsConfig : public KeyValueConfigBase { public: - ShortcutsConfig(); - - virtual void reset() override { + ShortcutsConfig() : KeyValueConfigBase(QStringLiteral("shortcuts.json")) { setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - user_shortcuts.clear(); } + virtual QJsonObject toJson() const 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); + void setDefaultShortcuts(const QObjectList& objects); QList defaultShortcuts(const QObject *object) const; - void setUserShortcuts(const QObjectList &objects); - void setUserShortcuts(const QMultiMap &objects_keySequences); + 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 { }; + virtual bool parseLegacyKeyValue(const QString& key, const QString& value) override; + virtual QJsonObject getDefaultJson() const override; private: QMultiMap user_shortcuts; @@ -509,11 +618,11 @@ private: QString cfgKey(const QObject *object) const; QList currentShortcuts(const QObject *object) const; - void storeShortcutsFromList(StoreType storeType, const QObjectList &objects); + void storeShortcutsFromList(StoreType storeType, const QObjectList& objects); void storeShortcuts( StoreType storeType, - const QString &cfgKey, - const QList &keySequences); + const QString& cfgKey, + const QList& keySequences); }; extern ShortcutsConfig shortcutsConfig; diff --git a/include/core/basegame.h b/include/core/basegame.h new file mode 100644 index 00000000..38387027 --- /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, // TODO: Go through and make sure this is a valid state + 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..a193386f --- /dev/null +++ b/include/core/converter.h @@ -0,0 +1,342 @@ +#pragma once +#ifndef CONVERTER_H +#define CONVERTER_H + +#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. + +*/ + + +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;} +}; + +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 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 (auto& elem : arr) list.append(Converter::fromJson(elem, errors)); + return list; + } +}; + +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 (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; + } +}; + +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; + } +}; + +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/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..717c94f4 --- /dev/null +++ b/include/core/orderedset.h @@ -0,0 +1,30 @@ +#pragma once +#ifndef ORDERED_SET_H +#define ORDERED_SET_H + +#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..54e53305 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -21,6 +21,19 @@ namespace Util { 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/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 12cdf3f5..17204686 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..f14d9b3b 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_wasShown; +}; 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..f14e74cf 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*); 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..dff010c7 100644 --- a/porymap.pro +++ b/porymap.pro @@ -47,8 +47,9 @@ VERSION = 6.3.0 DEFINES += PORYMAP_VERSION=\\\"$$VERSION\\\" SOURCES += src/core/advancemapparser.cpp \ + src/config/legacy.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 \ @@ -113,7 +114,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 \ @@ -168,6 +169,7 @@ HEADERS += 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 \ @@ -233,7 +235,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 \ diff --git a/src/config.cpp b/src/config.cpp index 067e199e..b55d1b8f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,5 +1,4 @@ #include "config.h" -#include "log.h" #include "shortcut.h" #include "map.h" #include "validator.h" @@ -17,64 +16,11 @@ #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"}}, @@ -187,994 +133,353 @@ const QMap> ProjectConfig::defaultPaths {ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}}, }; -ProjectIdentifier reverseDefaultIdentifier(QString str) { - for (auto i = ProjectConfig::defaultIdentifiers.cbegin(), end = ProjectConfig::defaultIdentifiers.cend(); i != end; i++) { +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 reverseDefaultPaths(QString str) { - for (auto it = ProjectConfig::defaultPaths.constKeyValueBegin(); it != ProjectConfig::defaultPaths.constKeyValueEnd(); ++it) { +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 KeyValueConfigBase::setRoot(const QString &root) { +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(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(); +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 (line.length() == 0) { - continue; + 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; } - - QRegularExpressionMatch match = re.match(line); - if (!match.hasMatch()) { - logWarn(QString("Invalid config line in %1: '%2'").arg(this->filepath()).arg(line)); - continue; + 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; } - - this->parseConfigKeyValue(match.captured("key").trimmed(), match.captured("value").trimmed()); + 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(); } - this->setUnreadKeys(); - - file.close(); 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) { + const auto fields = registeredFields(); + auto it = fields.find(key); + if (it == fields.end()) return false; + + // Recognized 'key' as a registered field. Let the FieldManager try to assign the value. + const QStringList errors = it.value().set(this, 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() const { + QJsonObject obj; + const auto fields = registeredFields(); + for (auto it = fields.begin(); it != fields.end(); it++) { + obj[it.key()] = it.value().get(this); + } + return obj; +} + 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()); + QFile file(filepath()); if (!file.open(QIODevice::WriteOnly)) { - logError(QString("Could not open config file '%1' for writing: ").arg(this->filepath()) + file.errorString()); + 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(); + } + } + } - file.write(text.toUtf8()); - file.close(); + QJsonDocument doc(savedObject); + file.write(doc.toJson()); 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(); +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::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::loadFromJson(const QJsonObject& obj) { + KeyValueConfigBase::loadFromJson(obj); -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)); + // 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(); } } -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; +QJsonObject PorymapConfig::getDefaultJson() const { + PorymapConfig defaultConfig; + return defaultConfig.toJson(); } -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) { +void PorymapConfig::addRecentProject(const QString& project) { this->recentProjects.removeOne(project); this->recentProjects.prepend(project); } -void PorymapConfig::setRecentProjects(QStringList projects) { +void PorymapConfig::setRecentProjects(const QStringList& projects) { this->recentProjects = projects; } -QString PorymapConfig::getRecentProject() { +QString PorymapConfig::getRecentProject() const { return this->recentProjects.value(0); } -QStringList PorymapConfig::getRecentProjects() { +const QStringList& PorymapConfig::getRecentProjects() const { 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::saveGeometry(const QWidget* widget, const QString& keyPrefix, bool recursive) { + if (!widget || widget->objectName().isEmpty()) return; -void PorymapConfig::setTilesetEditorGeometry(QByteArray tilesetEditorGeometry_, QByteArray tilesetEditorState_, - QByteArray tilesetEditorSplitterState_) { - this->tilesetEditorGeometry = tilesetEditorGeometry_; - this->tilesetEditorState = tilesetEditorState_; - this->tilesetEditorSplitterState = tilesetEditorSplitterState_; -} + const QString key = keyPrefix + widget->objectName(); + this->savedGeometryMap.insert(key, widget->saveGeometry()); -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; - } + // 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); } } - // 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); +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()); } - 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)); + if (recursive) { + for (const auto splitter : widget->findChildren()) { + restoreGeometry(splitter, key + "_", false); + } + } + return true; } +// TODO: This should eventually be contained by each individual Project instance. ProjectConfig projectConfig; -ProjectConfig::ProjectConfig() : KeyValueConfigBase(QStringLiteral("porymap.project.cfg")) { - reset(); +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 + }; + if (this->baseGameVersion == BaseGame::Version::pokeruby) { + this->mapAllowFlagsEnabled = false; + } + } } -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); +bool ProjectConfig::save() { + // Clean out empty paths + Util::removeEmptyStrings(&this->globalConstants); + Util::removeEmptyStrings(&this->eventIconPaths); + Util::removeEmptyStrings(&this->pokemonIconPaths); + + return KeyValueConfigBase::save(); } -// Restore config to version-specific defaults -void::ProjectConfig::reset(BaseGameVersion baseGameVersion) { - this->reset(); - this->baseGameVersion = baseGameVersion; - this->setUnreadKeys(); +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; } -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; +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(); } -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) { +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(getBaseGameVersionString(version))); + logInfo(QString("Auto-detected base_game_version as '%1'").arg(BaseGame::versionToString(version))); } else { QDialog dialog(nullptr, Qt::WindowTitleHint); dialog.setWindowTitle("Project Configuration"); @@ -1183,9 +488,10 @@ void ProjectConfig::init() { QFormLayout form(&dialog); QComboBox *baseGameVersionComboBox = new QComboBox(); - baseGameVersionComboBox->addItem("pokeruby", BaseGameVersion::pokeruby); - baseGameVersionComboBox->addItem("pokefirered", BaseGameVersion::pokefirered); - baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); + // TODO: Populate dynamically, same as project settings editor + baseGameVersionComboBox->addItem("pokeruby", BaseGame::Version::pokeruby); + baseGameVersionComboBox->addItem("pokefirered", BaseGame::Version::pokefirered); + baseGameVersionComboBox->addItem("pokeemerald", BaseGame::Version::pokeemerald); form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); // TODO: Add an 'Advanced' button to open the project settings window (with some settings disabled) @@ -1195,15 +501,14 @@ void ProjectConfig::init() { form.addRow(&buttonBox); if (dialog.exec() == QDialog::Accepted) { - this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); + this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); } else { - logWarn(QString("No base_game_version selected, using default '%1'").arg(getBaseGameVersionString(this->baseGameVersion))); + logWarn(QString("No base_game_version selected, using default '%1'").arg(BaseGame::versionToString(this->baseGameVersion))); } } - this->setUnreadKeys(); // Initialize version-specific options } -void ProjectConfig::setFilePath(ProjectFilePath pathId, const QString &path) { +void ProjectConfig::setFilePath(ProjectFilePath pathId, const QString& path) { if (!defaultPaths.contains(pathId)) return; if (path.isEmpty()) { this->filePaths.remove(pathId); @@ -1212,7 +517,7 @@ void ProjectConfig::setFilePath(ProjectFilePath pathId, const QString &path) { } } -void ProjectConfig::setFilePath(const QString &pathId, const QString &path) { +void ProjectConfig::setFilePath(const QString& pathId, const QString& path) { this->setFilePath(reverseDefaultPaths(pathId), path); } @@ -1220,7 +525,7 @@ QString ProjectConfig::getCustomFilePath(ProjectFilePath pathId) { return QDir::cleanPath(this->filePaths.value(pathId)); } -QString ProjectConfig::getCustomFilePath(const QString &pathId) { +QString ProjectConfig::getCustomFilePath(const QString& pathId) { return this->getCustomFilePath(reverseDefaultPaths(pathId)); } @@ -1239,10 +544,9 @@ QString ProjectConfig::getFilePath(ProjectFilePath pathId) { } } return defaultPaths.contains(pathId) ? defaultPaths[pathId].second : QString(); - } -void ProjectConfig::setIdentifier(ProjectIdentifier id, QString text) { +void ProjectConfig::setIdentifier(ProjectIdentifier id, const QString& text) { if (!defaultIdentifiers.contains(id)) return; @@ -1262,7 +566,7 @@ void ProjectConfig::setIdentifier(ProjectIdentifier id, QString text) { } } -void ProjectConfig::setIdentifier(const QString &id, const QString &text) { +void ProjectConfig::setIdentifier(const QString& id, const QString& text) { this->setIdentifier(reverseDefaultIdentifier(id), text); } @@ -1270,7 +574,7 @@ QString ProjectConfig::getCustomIdentifier(ProjectIdentifier id) { return this->identifiers.value(id); } -QString ProjectConfig::getCustomIdentifier(const QString &id) { +QString ProjectConfig::getCustomIdentifier(const QString& id) { return this->getCustomIdentifier(reverseDefaultIdentifier(id)); } @@ -1281,152 +585,36 @@ QString ProjectConfig::getIdentifier(ProjectIdentifier id) { 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; -} - +// TODO: This should eventually be contained by each individual Project instance. UserConfig userConfig; -UserConfig::UserConfig() : KeyValueConfigBase(QStringLiteral("porymap.user.cfg")) { - reset(); +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; } -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(); +QJsonObject UserConfig::getDefaultJson() const { + UserConfig defaultConfig; + return defaultConfig.toJson(); } ShortcutsConfig shortcutsConfig; -ShortcutsConfig::ShortcutsConfig() : KeyValueConfigBase(QStringLiteral("porymap.shortcuts.cfg")), - user_shortcuts({ }), - default_shortcuts({ }) { +void ShortcutsConfig::loadFromJson(const QJsonObject& obj) { + this->user_shortcuts = Converter>::fromJson(obj); } -void ShortcutsConfig::parseConfigKeyValue(QString key, QString value) { - QStringList keySequences = value.split(' '); - for (auto keySequence : keySequences) - user_shortcuts.insert(key, keySequence); +QJsonObject ShortcutsConfig::toJson() const { + return Converter>::toJson(this->user_shortcuts); } -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; +QJsonObject ShortcutsConfig::getDefaultJson() const { + return Converter>::toJson(this->default_shortcuts); } -void ShortcutsConfig::setDefaultShortcuts(const QObjectList &objects) { +void ShortcutsConfig::setDefaultShortcuts(const QObjectList& objects) { storeShortcutsFromList(StoreType::Default, objects); } @@ -1434,11 +622,11 @@ QList ShortcutsConfig::defaultShortcuts(const QObject *object) con return default_shortcuts.values(cfgKey(object)); } -void ShortcutsConfig::setUserShortcuts(const QObjectList &objects) { +void ShortcutsConfig::setUserShortcuts(const QObjectList& objects) { storeShortcutsFromList(StoreType::User, objects); } -void ShortcutsConfig::setUserShortcuts(const QMultiMap &objects_keySequences) { +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)); @@ -1448,7 +636,7 @@ QList ShortcutsConfig::userShortcuts(const QObject *object) const return user_shortcuts.values(cfgKey(object)); } -void ShortcutsConfig::storeShortcutsFromList(StoreType storeType, const QObjectList &objects) { +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)); @@ -1456,8 +644,8 @@ void ShortcutsConfig::storeShortcutsFromList(StoreType storeType, const QObjectL void ShortcutsConfig::storeShortcuts( StoreType storeType, - const QString &cfgKey, - const QList &keySequences) + const QString& cfgKey, + const QList& keySequences) { bool storeUser = (storeType == User) || !user_shortcuts.contains(cfgKey); diff --git a/src/config/legacy.cpp b/src/config/legacy.cpp new file mode 100644 index 00000000..810a93b7 --- /dev/null +++ b/src/config/legacy.cpp @@ -0,0 +1,423 @@ +#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()/* && !file.remove()*/) { // TODO: Once non-legacy loading is complete, we can uncomment this. + //logWarn(QString("Failed to delete old 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 == "tileset_editor_geometry") { + } 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") { + this->paletteEditorBitDepth = toInt(value, 15, 24, 24); + if (this->paletteEditorBitDepth != 15 && this->paletteEditorBitDepth != 24){ + this->paletteEditorBitDepth = 24; + } + } 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 = 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 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 if (key == "image_export_color_space_id") { + this->imageExportColorSpaceId = toInt(value, 0, 8); + } 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/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..0646a357 --- /dev/null +++ b/src/core/basegame.cpp @@ -0,0 +1,63 @@ +#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()); + BaseGame::Version version = BaseGame::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 != BaseGame::Version::none) { + // The given string matches multiple versions, so we can't be sure which it is. + return BaseGame::Version::none; + } + version = it.key(); + break; + } + } + } + // We finished checking the names for each version; the name either matched 1 version or none. + return version; +} + +// TODO: Make sure empty string is ok everywhere this is used +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 == BaseGame::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 == BaseGame::Version::pokefirered) { + static const QStringList paths = { QStringLiteral(":/icons/player/red.ico"), + QStringLiteral(":/icons/player/green.ico"), }; + return paths.value(character); + } else if (version == BaseGame::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/mainwindow.cpp b/src/mainwindow.cpp index 52ea6720..6d6056e9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -81,6 +81,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 +93,7 @@ void MainWindow::initialize() { if (porymapConfig.checkForUpdates) this->checkForUpdates(false); - this->restoreWindowState(); + this->resizeWithinScreen(); this->show(); } @@ -109,13 +110,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 +179,7 @@ void MainWindow::setWindowDisabled(bool disabled) { } void MainWindow::initWindow() { + porymapConfig = PorymapConfig(); porymapConfig.load(); this->initLogStatusBar(); this->initCustomUI(); @@ -227,6 +222,7 @@ void MainWindow::initWindow() { void MainWindow::initShortcuts() { initExtraShortcuts(); + shortcutsConfig = ShortcutsConfig(); shortcutsConfig.load(); shortcutsConfig.setDefaultShortcuts(shortcutableObjects()); applyUserShortcuts(); @@ -303,9 +299,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 +315,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 +586,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 +596,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 +727,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 +787,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 +832,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 +883,24 @@ 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()) { + 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()) { // We were unable to find the necessary changes for Porymap's current major version. // Unless they have explicitly suppressed this message, warn the user that this might mean their project is missing breaking changes. - // Note: Do not report 'projectVersion' to the user in this message. We've already logged it for troubleshooting. + // 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 +913,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 +923,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 +1475,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 +1484,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 +1508,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 +2243,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 +3283,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 4f7b466c..1beeea3b 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 a version of -1. + // This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check // is not important enough to significantly slow down project launch, we'd rather just timeout. - 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); @@ -3205,7 +3211,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; @@ -3297,7 +3303,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; @@ -3338,7 +3344,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..4489641a 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -22,11 +22,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 +103,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..17482fe7 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(); +} + +void CustomScriptsListItem::setSettings(const ScriptSettings& settings) { + setPath(settings.path); + setScriptEnabled(settings.enabled); + // TODO: Read userOnly +} + +ScriptSettings CustomScriptsListItem::getSettings() const { + return { + .path = path(), + .enabled = scriptEnabled(), + .userOnly = true, // TODO + }; +} diff --git a/src/ui/eventfilters.cpp b/src/ui/eventfilters.cpp index 1f3d94f1..1cda13b8 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,40 @@ 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_wasShown.insert(object); + } else if (event->type() == QEvent::Close && m_wasShown.contains(object)) { + // 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've checked to make sure the widget was displayed before proceeding. + porymapConfig.saveGeometry(w); + if (m_loggingEnabled && !w->windowTitle().isEmpty()) { + logInfo(QString("Closing window: %1").arg(w->windowTitle())); + } + } + return false; +} \ No newline at end of file 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/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..c12ba566 100644 --- a/src/ui/newlayoutdialog.cpp +++ b/src/ui/newlayoutdialog.cpp @@ -41,18 +41,16 @@ NewLayoutDialog::NewLayoutDialog(Project *project, const Layout *layoutToCopy, Q refresh(); - if (porymapConfig.newLayoutDialogGeometry.isEmpty()){ + if (!porymapConfig.restoreGeometry(this)) { // On first display resize to fit contents a little better adjustSize(); - } else { - restoreGeometry(porymapConfig.newLayoutDialogGeometry); } ui->lineEdit_Name->setFocus(); } NewLayoutDialog::~NewLayoutDialog() { - porymapConfig.newLayoutDialogGeometry = saveGeometry(); + porymapConfig.saveGeometry(this); 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..5cb9c731 100644 --- a/src/ui/preferenceeditor.cpp +++ b/src/ui/preferenceeditor.cpp @@ -103,7 +103,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); @@ -141,7 +141,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..5afcf81d 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,21 @@ void ProjectSettingsEditor::initUi() { ui->comboBox_IconSpecies->addItems(project->speciesNames); ui->comboBox_WarpBehaviors->addItems(project->metatileBehaviorMap.keys()); } - ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings); + // TODO: We don't need to keep converting these to/from strings, just include the value as data. + ui->comboBox_BaseGameVersion->addItems({ + BaseGame::versionToString(BaseGame::Version::pokeruby), + BaseGame::versionToString(BaseGame::Version::pokefirered), + BaseGame::versionToString(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 +153,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); @@ -434,13 +440,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 +447,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->setTextItem(BaseGame::versionToString(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 +510,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 +555,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 = BaseGame::stringToVersion(ui->comboBox_BaseGameVersion->currentText()); projectConfig.metatileAttributesSize = ui->comboBox_AttributesSize->currentText().toInt(); // Save check box settings @@ -603,13 +602,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 +622,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 +632,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 +772,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 = BaseGame::stringToVersion(ui->comboBox_BaseGameVersion->currentText()); 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; } } @@ -815,8 +814,9 @@ bool ProjectSettingsEditor::promptRestoreDefaults() { // 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(BaseGame::stringToVersion(versionText)); this->refresh(); projectConfig = tempProject; @@ -863,11 +863,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..ce0fa0ce 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(); @@ -144,13 +137,13 @@ void buildFireredDefaults(poryjson::Json &json) { poryjson::Json RegionMapEditor::buildDefaultJson() { poryjson::Json defaultJson; switch (projectConfig.baseGameVersion) { - case BaseGameVersion::pokeemerald: + case BaseGame::Version::pokeemerald: buildEmeraldDefaults(defaultJson); break; - case BaseGameVersion::pokeruby: + case BaseGame::Version::pokeruby: buildRubyDefaults(defaultJson); break; - case BaseGameVersion::pokefirered: + case BaseGame::Version::pokefirered: buildFireredDefaults(defaultJson); break; default: @@ -285,13 +278,13 @@ bool RegionMapEditor::buildConfigDialog() { // for sake of convenience, option to just use defaults for each basegame 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: @@ -1251,11 +1244,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/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