diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 8e0dfa33..67bba635 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -92,6 +92,7 @@ In addition to these files, there are some specific symbol and macro names that ``symbol_spawn_npcs``, ``u8 sWhiteoutRespawnHealerNpcIds``, the type and table name for Heal Location ``Respawn NPC`` values ``symbol_attribute_table``, ``sMetatileAttrMasks``, optionally read to get settings on ``Tilesets`` tab ``symbol_tilesets_prefix``, ``gTileset_``, for new tileset names and to extract base tileset names + ``symbol_dynamic_map_name``, ``Dynamic``, reserved map name to display for ``define_map_dynamic`` ``define_obj_event_count``, ``OBJECT_EVENT_TEMPLATES_COUNT``, to limit total Object Events ``define_min_level``, ``MIN_LEVEL``, minimum wild encounters level ``define_max_level``, ``MAX_LEVEL``, maximum wild encounters level diff --git a/include/config.h b/include/config.h index 0dcbfe66..0655c0bd 100644 --- a/include/config.h +++ b/include/config.h @@ -187,6 +187,7 @@ enum ProjectIdentifier { symbol_spawn_npcs, symbol_attribute_table, symbol_tilesets_prefix, + symbol_dynamic_map_name, define_obj_event_count, define_min_level, define_max_level, @@ -323,7 +324,7 @@ public: QString getCustomFilePath(ProjectFilePath pathId); QString getCustomFilePath(const QString &pathId); QString getFilePath(ProjectFilePath pathId); - void setIdentifier(ProjectIdentifier id, const QString &text); + void setIdentifier(ProjectIdentifier id, QString text); void setIdentifier(const QString &id, const QString &text); QString getCustomIdentifier(ProjectIdentifier id); QString getCustomIdentifier(const QString &id); diff --git a/include/project.h b/include/project.h index 1632d38f..c76fcd72 100644 --- a/include/project.h +++ b/include/project.h @@ -18,10 +18,6 @@ #include #include -// TODO: Expose to config -// The displayed name of the special map value used by warps with multiple potential destinations -static QString DYNAMIC_MAP_NAME = "Dynamic"; - class Project : public QObject { Q_OBJECT @@ -80,6 +76,7 @@ public: QMap modifiedFileTimestamps; bool usingAsmTilesets; QSet disabledSettingsNames; + QSet topLevelMapFields; int pokemonMinLevel; int pokemonMaxLevel; int maxEncounterRate; @@ -145,7 +142,7 @@ public: bool hasUnsavedChanges(); bool hasUnsavedDataChanges = false; - QSet getTopLevelMapFields(); + void initTopLevelMapFields(); bool readMapJson(const QString &mapName, QJsonDocument * out); bool loadMapData(Map*); bool readMapLayouts(); @@ -219,7 +216,6 @@ public: QString getDefaultPrimaryTilesetLabel(); QString getDefaultSecondaryTilesetLabel(); - QString getDynamicMapDefineName(); void updateTilesetMetatileLabels(Tileset *tileset); QString buildMetatileLabelsText(const QMap defines); QString findMetatileLabelsTileset(QString label); @@ -227,6 +223,8 @@ public: static QString getExistingFilepath(QString filepath); void applyParsedLimits(); + static QString getDynamicMapDefineName(); + static QString getDynamicMapName(); static int getNumTilesPrimary(); static int getNumTilesTotal(); static int getNumMetatilesPrimary(); diff --git a/src/config.cpp b/src/config.cpp index 316c9ea3..234a0a33 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -81,6 +81,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::symbol_spawn_npcs, {"symbol_spawn_npcs", "u8 sWhiteoutRespawnHealerNpcIds"}}, {ProjectIdentifier::symbol_attribute_table, {"symbol_attribute_table", "sMetatileAttrMasks"}}, {ProjectIdentifier::symbol_tilesets_prefix, {"symbol_tilesets_prefix", "gTileset_"}}, + {ProjectIdentifier::symbol_dynamic_map_name, {"symbol_dynamic_map_name", "Dynamic"}}, // Defines {ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}}, {ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}}, @@ -941,13 +942,25 @@ QString ProjectConfig::getFilePath(ProjectFilePath pathId) { } -void ProjectConfig::setIdentifier(ProjectIdentifier id, const QString &text) { - if (!defaultIdentifiers.contains(id)) return; - QString copy(text); - if (copy.isEmpty()) { +void ProjectConfig::setIdentifier(ProjectIdentifier id, QString text) { + if (!defaultIdentifiers.contains(id)) + return; + + if (text.isEmpty()) { this->identifiers.remove(id); } else { - this->identifiers[id] = copy; + const QString idName = defaultIdentifiers.value(id).first; + if (idName.startsWith("define_") || idName.startsWith("symbol_")) { + // Validate the input for the identifier, depending on the type. + static const QRegularExpression re("[A-Za-z_]+[\\w]*"); + auto validator = QRegularExpressionValidator(re); + int temp = 0; + if (validator.validate(text, temp) != QValidator::Acceptable) { + logError(QString("The name '%1' for project identifier '%2' is invalid. It must only contain word characters, and cannot start with a digit.").arg(text).arg(idName)); + return; + } + } + this->identifiers[id] = text; } } diff --git a/src/core/events.cpp b/src/core/events.cpp index 94030d03..0673a652 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -404,17 +404,11 @@ bool CloneObjectEvent::loadFromJson(QJsonObject json, Project *project) { this->setGfx(ParseUtil::jsonToQString(json["graphics_id"])); this->setTargetID(ParseUtil::jsonToInt(json["target_local_id"])); - // Ensure the target map constant is valid before adding it to the events. - const QString dynamicMapConstant = project->getDynamicMapDefineName(); - QString mapConstant = ParseUtil::jsonToQString(json["target_map"]); - if (project->mapConstantsToMapNames.contains(mapConstant)) { - this->setTargetMap(project->mapConstantsToMapNames.value(mapConstant)); - } else if (mapConstant == dynamicMapConstant) { - this->setTargetMap(DYNAMIC_MAP_NAME); - } else { - logWarn(QString("Target Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(dynamicMapConstant)); - this->setTargetMap(DYNAMIC_MAP_NAME); - } + // Log a warning if "target_map" isn't a known map ID, but don't overwrite user data. + const QString mapConstant = ParseUtil::jsonToQString(json["target_map"]); + if (!project->mapConstantsToMapNames.contains(mapConstant)) + logWarn(QString("Target Map constant '%1' is invalid.").arg(mapConstant)); + this->setTargetMap(project->mapConstantsToMapNames.value(mapConstant, mapConstant)); this->readCustomValues(json); @@ -516,17 +510,11 @@ bool WarpEvent::loadFromJson(QJsonObject json, Project *project) { this->setElevation(ParseUtil::jsonToInt(json["elevation"])); this->setDestinationWarpID(ParseUtil::jsonToQString(json["dest_warp_id"])); - // Ensure the warp destination map constant is valid before adding it to the warps. - const QString dynamicMapConstant = project->getDynamicMapDefineName(); - QString mapConstant = ParseUtil::jsonToQString(json["dest_map"]); - if (project->mapConstantsToMapNames.contains(mapConstant)) { - this->setDestinationMap(project->mapConstantsToMapNames.value(mapConstant)); - } else if (mapConstant == dynamicMapConstant) { - this->setDestinationMap(DYNAMIC_MAP_NAME); - } else { - logWarn(QString("Destination Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(dynamicMapConstant)); - this->setDestinationMap(DYNAMIC_MAP_NAME); - } + // Log a warning if "dest_map" isn't a known map ID, but don't overwrite user data. + const QString mapConstant = ParseUtil::jsonToQString(json["dest_map"]); + if (!project->mapConstantsToMapNames.contains(mapConstant)) + logWarn(QString("Destination Map constant '%1' is invalid.").arg(mapConstant)); + this->setDestinationMap(project->mapConstantsToMapNames.value(mapConstant, mapConstant)); this->readCustomValues(json); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 50c4b4d8..eaa98f11 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -850,10 +850,10 @@ bool MainWindow::userSetMap(QString map_name) { if (editor->map && editor->map->name() == map_name) return true; // Already set - if (map_name == DYNAMIC_MAP_NAME) { + if (map_name == editor->project->getDynamicMapName()) { QMessageBox msgBox(this); QString errorMsg = QString("The map '%1' can't be opened, it's a placeholder to indicate the specified map will be set programmatically.").arg(map_name); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); + msgBox.warning(nullptr, "Cannot Open Map", errorMsg); return false; } @@ -870,14 +870,13 @@ bool MainWindow::userSetMap(QString map_name) { } bool MainWindow::setMap(QString map_name) { - if (map_name.isEmpty() || map_name == DYNAMIC_MAP_NAME) { - logInfo(QString("Cannot set map to '%1'").arg(map_name)); + if (!editor || !editor->project || map_name.isEmpty() || map_name == editor->project->getDynamicMapName()) { + logWarn(QString("Ignored setting map to '%1'").arg(map_name)); return false; } logInfo(QString("Setting map to '%1'").arg(map_name)); - - if (!editor || !editor->setMap(map_name)) { + if (!editor->setMap(map_name)) { logWarn(QString("Failed to set map to '%1'").arg(map_name)); return false; } @@ -996,12 +995,6 @@ void MainWindow::refreshMapScene() { } void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_group) { - // Ensure valid destination map name. - if (!editor->project->mapNames.contains(map_name)) { - logError(QString("Invalid map name '%1'").arg(map_name)); - return; - } - // Open the destination map. if (!userSetMap(map_name)) return; @@ -2780,15 +2773,11 @@ void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { } void MainWindow::on_button_OpenDiveMap_clicked() { - const QString mapName = ui->comboBox_DiveMap->currentText(); - if (editor->project->mapNames.contains(mapName)) - userSetMap(mapName); + userSetMap(ui->comboBox_DiveMap->currentText()); } void MainWindow::on_button_OpenEmergeMap_clicked() { - const QString mapName = ui->comboBox_EmergeMap->currentText(); - if (editor->project->mapNames.contains(mapName)) - userSetMap(mapName); + userSetMap(ui->comboBox_EmergeMap->currentText()); } void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { diff --git a/src/project.cpp b/src/project.cpp index f36f7543..03f7b4df 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -135,7 +135,7 @@ void Project::clearTilesetCache() { } Map* Project::loadMap(QString mapName) { - if (mapName == DYNAMIC_MAP_NAME) + if (mapName == getDynamicMapName()) return nullptr; Map *map; @@ -148,7 +148,6 @@ Map* Project::loadMap(QString mapName) { } else { map = new Map; map->setName(mapName); - map->setConstantName(this->mapNamesToMapConstants.value(mapName)); // TODO: How should we handle if !mapNamesToMapConstants.contains(mapName) here } if (!(loadMapData(map) && loadMapLayout(map))){ @@ -161,38 +160,36 @@ Map* Project::loadMap(QString mapName) { return map; } -const QSet defaultTopLevelMapFields = { - "id", - "name", - "layout", - "music", - "region_map_section", - "requires_flash", - "weather", - "map_type", - "show_map_name", - "battle_scene", - "connections", - "object_events", - "warp_events", - "coord_events", - "bg_events", - "shared_events_map", - "shared_scripts_map", -}; - -QSet Project::getTopLevelMapFields() { - QSet topLevelMapFields = defaultTopLevelMapFields; +void Project::initTopLevelMapFields() { + static const QSet defaultTopLevelMapFields = { + "id", + "name", + "layout", + "music", + "region_map_section", + "requires_flash", + "weather", + "map_type", + "show_map_name", + "battle_scene", + "connections", + "object_events", + "warp_events", + "coord_events", + "bg_events", + "heal_locations", + "shared_events_map", + "shared_scripts_map", + }; + this->topLevelMapFields = defaultTopLevelMapFields; if (projectConfig.mapAllowFlagsEnabled) { - topLevelMapFields.insert("allow_cycling"); - topLevelMapFields.insert("allow_escaping"); - topLevelMapFields.insert("allow_running"); + this->topLevelMapFields.insert("allow_cycling"); + this->topLevelMapFields.insert("allow_escaping"); + this->topLevelMapFields.insert("allow_running"); } - if (projectConfig.floorNumberEnabled) { - topLevelMapFields.insert("floor_number"); + this->topLevelMapFields.insert("floor_number"); } - return topLevelMapFields; } bool Project::readMapJson(const QString &mapName, QJsonDocument * out) { @@ -215,6 +212,11 @@ bool Project::loadMapData(Map* map) { QJsonObject mapObj = mapDoc.object(); + // We should already know the map constant ID from the initial project launch, but we'll ensure it's correct here anyway. + map->setConstantName(ParseUtil::jsonToQString(mapObj["id"])); + this->mapNamesToMapConstants.insert(map->name(), map->constantName()); + this->mapConstantsToMapNames.insert(map->constantName(), map->name()); + map->setSong(ParseUtil::jsonToQString(mapObj["music"])); map->setLayoutId(ParseUtil::jsonToQString(mapObj["layout"])); map->setLocation(ParseUtil::jsonToQString(mapObj["region_map_section"])); @@ -353,9 +355,8 @@ bool Project::loadMapData(Map* map) { // Check for custom fields /* // TODO: Re-enable - QSet baseFields = this->getTopLevelMapFields(); for (QString key : mapObj.keys()) { - if (!baseFields.contains(key)) { + if (!this->topLevelMapFields.contains(key)) { map->customHeaders.insert(key, mapObj[key]); } } @@ -1822,6 +1823,8 @@ bool Project::readMapGroups() { this->groupedMapNames.clear(); this->mapNames.clear(); + this->initTopLevelMapFields(); + const QString filepath = root + "/" + projectConfig.getFilePath(ProjectFilePath::json_map_groups); fileWatcher.addPath(filepath); QJsonDocument mapGroupsDoc; @@ -1832,14 +1835,20 @@ bool Project::readMapGroups() { QJsonObject mapGroupsObj = mapGroupsDoc.object(); QJsonArray mapGroupOrder = mapGroupsObj["group_order"].toArray(); + + const QString dynamicMapName = getDynamicMapName(); + + // Process the map group lists for (int groupIndex = 0; groupIndex < mapGroupOrder.size(); groupIndex++) { const QString groupName = ParseUtil::jsonToQString(mapGroupOrder.at(groupIndex)); const QJsonArray mapNamesJson = mapGroupsObj.value(groupName).toArray(); this->groupedMapNames.append(QStringList()); this->groupNames.append(groupName); + + // Process the names in this map group for (int j = 0; j < mapNamesJson.size(); j++) { const QString mapName = ParseUtil::jsonToQString(mapNamesJson.at(j)); - if (mapName == DYNAMIC_MAP_NAME) { + if (mapName == dynamicMapName) { logWarn(QString("Ignoring map with reserved name '%1'.").arg(mapName)); continue; } @@ -1877,7 +1886,7 @@ bool Project::readMapGroups() { this->mapGroups.insert(mapName, groupIndex); this->mapConstantsToMapNames.insert(mapConstant, mapName); this->mapNamesToMapConstants.insert(mapName, mapConstant); - // TODO: Keep these updated + // TODO: Either verify that these are known IDs, or make sure nothing breaks when they're unknown. this->mapNameToLayoutId.insert(mapName, ParseUtil::jsonToQString(mapObj["layout"])); this->mapNameToMapSectionName.insert(mapName, ParseUtil::jsonToQString(mapObj["region_map_section"])); } @@ -1892,10 +1901,11 @@ bool Project::readMapGroups() { return false; } + // Save special "Dynamic" constant const QString defineName = this->getDynamicMapDefineName(); - this->mapConstantsToMapNames.insert(defineName, DYNAMIC_MAP_NAME); - this->mapNamesToMapConstants.insert(DYNAMIC_MAP_NAME, defineName); - this->mapNames.append(DYNAMIC_MAP_NAME); + this->mapConstantsToMapNames.insert(defineName, dynamicMapName); + this->mapNamesToMapConstants.insert(dynamicMapName, defineName); + this->mapNames.append(dynamicMapName); return true; } @@ -2941,6 +2951,10 @@ QString Project::getDynamicMapDefineName() { return prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_dynamic); } +QString Project::getDynamicMapName() { + return projectConfig.getIdentifier(ProjectIdentifier::symbol_dynamic_map_name); +} + // If the provided filepath is an absolute path to an existing file, return filepath. // If not, and the provided filepath is a relative path from the project dir to an existing file, return the relative path. // Otherwise return empty string. diff --git a/src/ui/newmapdialog.cpp b/src/ui/newmapdialog.cpp index ff4cb209..da10c26a 100644 --- a/src/ui/newmapdialog.cpp +++ b/src/ui/newmapdialog.cpp @@ -23,7 +23,6 @@ NewMapDialog::NewMapDialog(QWidget *parent, Project *project) : this->importedMap = false; // Map names and IDs can only contain word characters, and cannot start with a digit. - // TODO: Also validate this when we read ProjectIdentifier::define_map_prefix from the config static const QRegularExpression re("[A-Za-z_]+[\\w]*"); auto validator = new QRegularExpressionValidator(re, this); ui->lineEdit_Name->setValidator(validator);