From f3a28848b9dcd226688b01805deb5a8d45e3ac08 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 4 Apr 2025 11:54:39 -0400 Subject: [PATCH] Preserve custom fields in wild_encounters.json --- include/core/parseutil.h | 2 +- include/core/wildmoninfo.h | 10 +++-- include/lib/orderedjson.h | 19 +++++++-- include/lib/orderedmap.h | 11 ++++++ include/project.h | 6 ++- include/ui/regionmapeditor.h | 2 +- src/core/parseutil.cpp | 4 +- src/editor.cpp | 4 +- src/lib/orderedjson.cpp | 12 ------ src/project.cpp | 76 ++++++++++++++++++++++-------------- 10 files changed, 90 insertions(+), 56 deletions(-) diff --git a/include/core/parseutil.h b/include/core/parseutil.h index 4c19a27c..e02d0504 100644 --- a/include/core/parseutil.h +++ b/include/core/parseutil.h @@ -58,7 +58,7 @@ public: QMap readCDefinesByRegex(const QString &filename, const QSet ®exList, QString *error = nullptr); QMap readCDefinesByName(const QString &filename, const QSet &names, QString *error = nullptr); QStringList readCDefineNames(const QString &filename, const QSet ®exList, QString *error = nullptr); - tsl::ordered_map> readCStructs(const QString &, const QString & = "", const QHash& = {}); + OrderedMap> readCStructs(const QString &, const QString & = "", const QHash& = {}); QList getLabelMacros(const QList&, const QString&); QStringList getLabelValues(const QList&, const QString&); bool tryParseJsonFile(QJsonDocument *out, const QString &filepath, QString *error = nullptr); diff --git a/include/core/wildmoninfo.h b/include/core/wildmoninfo.h index 3c94fb17..a3665b7d 100644 --- a/include/core/wildmoninfo.h +++ b/include/core/wildmoninfo.h @@ -3,7 +3,7 @@ #define GUARD_WILDMONINFO_H #include -#include "orderedmap.h" +#include "orderedjson.h" class WildPokemon { public: @@ -13,22 +13,26 @@ public: int minLevel; int maxLevel; QString species; + OrderedJson::object customData; }; struct WildMonInfo { bool active = false; int encounterRate = 0; QVector wildPokemon; + OrderedJson::object customData; }; struct WildPokemonHeader { - tsl::ordered_map wildMons; + OrderedMap wildMons; + OrderedJson::object customData; }; struct EncounterField { QString name; // Ex: "fishing_mons" QVector encounterRates; - tsl::ordered_map> groups; // Ex: "good_rod", {2, 3, 4} + OrderedMap> groups; // Ex: "good_rod", {2, 3, 4} + OrderedJson::object customData; }; typedef QVector EncounterFields; diff --git a/include/lib/orderedjson.h b/include/lib/orderedjson.h index 73422a2b..cb5139d3 100644 --- a/include/lib/orderedjson.h +++ b/include/lib/orderedjson.h @@ -99,7 +99,7 @@ public: // Array and object typedefs typedef QVector array; - typedef tsl::ordered_map object; + typedef OrderedMap object; // Constructors for the various types of JSON value. Json() noexcept; // NUL @@ -133,8 +133,21 @@ public: Json(const V & v) : Json(array(v.begin(), v.end())) {} static Json fromQJsonValue(const QJsonValue &value); - static void append(Json::array *array, const QJsonArray &qArray); - static void append(Json::object *object, const QJsonObject &qObject); + + static void append(Json::array *array, const QJsonArray &addendum) { + for (const auto &i : addendum) array->push_back(fromQJsonValue(i)); + } + static void append(Json::array *array, const Json::array &addendum) { + for (const auto &i : addendum) array->push_back(i); + } + static void append(Json::object *object, const QJsonObject &addendum) { + for (auto it = addendum.constBegin(); it != addendum.constEnd(); it++) + (*object)[it.key()] = fromQJsonValue(it.value()); + } + static void append(Json::object *object, const Json::object &addendum) { + for (auto it = addendum.cbegin(); it != addendum.cend(); it++) + (*object)[it.key()] = it.value(); + } // This prevents Json(some_pointer) from accidentally producing a bool. Use // Json(bool(some_pointer)) if that behavior is desired. diff --git a/include/lib/orderedmap.h b/include/lib/orderedmap.h index 40882d9f..33fcfc50 100644 --- a/include/lib/orderedmap.h +++ b/include/lib/orderedmap.h @@ -1977,6 +1977,14 @@ public: size_type erase(const K& key, std::size_t precalculated_hash) { return m_ht.erase(key, precalculated_hash); } + + // Naive solution for take, should probably be replaced with one that does a single lookup and no unnecessary insertion. + // We want to mirror the behavior of QMap::take, which returns a default-constructed value if the key is not present. + T take(const key_type& key) { + typename ValueSelect::value_type value = m_ht[key]; + m_ht.erase(key); + return value; + } @@ -2404,4 +2412,7 @@ private: } // end namespace tsl +template +using OrderedMap = tsl::ordered_map; + #endif diff --git a/include/project.h b/include/project.h index 5f37c5e7..bcc53ae7 100644 --- a/include/project.h +++ b/include/project.h @@ -143,12 +143,11 @@ public: QString getNewHealLocationName(const Map* map) const; bool readWildMonData(); - tsl::ordered_map> wildMonData; + OrderedMap> wildMonData; QString wildMonTableName; QVector wildMonFields; QVector encounterGroupLabels; - QVector extraEncounterGroups; bool readSpeciesIconPaths(); QString getDefaultSpeciesIconPath(const QString &species); @@ -274,6 +273,9 @@ private: QJsonObject customMapSectionsData; QJsonObject customMapGroupsData; QJsonObject customHealLocationsData; + OrderedJson::object customWildMonData; + OrderedJson::object customWildMonGroupData; + OrderedJson::array extraEncounterGroups; // Maps/layouts represented in these sets have been fully loaded from the project. // If a valid map name / layout id is not in these sets, a Map / Layout object exists diff --git a/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h index 490324af..8ecf0a76 100644 --- a/include/ui/regionmapeditor.h +++ b/include/ui/regionmapeditor.h @@ -54,7 +54,7 @@ private: Project *project; RegionMap *region_map = nullptr; - tsl::ordered_map region_maps; + OrderedMap region_maps; QString configFilepath; diff --git a/src/core/parseutil.cpp b/src/core/parseutil.cpp index e172ea42..da2a8f8e 100644 --- a/src/core/parseutil.cpp +++ b/src/core/parseutil.cpp @@ -610,12 +610,12 @@ bool ParseUtil::gameStringToBool(const QString &gameString, bool * ok) { return gameStringToInt(gameString, ok) != 0; } -tsl::ordered_map> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash &memberMap) { +OrderedMap> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash &memberMap) { QString filePath = pathWithRoot(filename); auto cParser = fex::Parser(); auto tokens = fex::Lexer().LexFile(filePath); auto topLevelObjects = cParser.ParseTopLevelObjects(tokens); - tsl::ordered_map> structs; + OrderedMap> structs; for (auto it = topLevelObjects.begin(); it != topLevelObjects.end(); it++) { QString structLabel = QString::fromStdString(it->first); if (structLabel.isEmpty()) continue; diff --git a/src/editor.cpp b/src/editor.cpp index b29a47bc..19acbe60 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -596,7 +596,7 @@ void Editor::configureEncounterJSON(QWidget *window) { if (newNameDialog.exec() == QDialog::Accepted) { QString newFieldName = newNameEdit->text(); QVector newFieldRates(1, 100); - tempFields.append({newFieldName, newFieldRates, {}}); + tempFields.append({newFieldName, newFieldRates, {}, {}}); fieldChoices->addItem(newFieldName); fieldChoices->setCurrentIndex(fieldChoices->count() - 1); } @@ -675,7 +675,7 @@ void Editor::saveEncounterTabData() { if (!stack->count()) return; - tsl::ordered_map &encounterMap = project->wildMonData[map->constantName()]; + OrderedMap &encounterMap = project->wildMonData[map->constantName()]; for (int groupIndex = 0; groupIndex < stack->count(); groupIndex++) { MonTabWidget *tabWidget = static_cast(stack->widget(groupIndex)); diff --git a/src/lib/orderedjson.cpp b/src/lib/orderedjson.cpp index c501a596..d6ba4dbd 100644 --- a/src/lib/orderedjson.cpp +++ b/src/lib/orderedjson.cpp @@ -331,18 +331,6 @@ Json Json::fromQJsonValue(const QJsonValue &value) { } } -void Json::append(Json::array *array, const QJsonArray &qArray) { - for (const auto &i: qArray) { - array->push_back(fromQJsonValue(i)); - } -} - -void Json::append(Json::object *object, const QJsonObject &qObject) { - for (auto it = qObject.constBegin(); it != qObject.constEnd(); it++) { - (*object)[it.key()] = fromQJsonValue(it.value()); - } -} - /* * * * * * * * * * * * * * * * * * * * * Comparison */ diff --git a/src/project.cpp b/src/project.cpp index a942d6fc..871b8e2c 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -747,7 +747,7 @@ void Project::saveWildMonData() { monHeadersObject["for_maps"] = true; OrderedJson::array fieldsInfoArray; - for (EncounterField fieldInfo : wildMonFields) { + for (EncounterField fieldInfo : this->wildMonFields) { OrderedJson::object fieldObject; OrderedJson::array rateArray; @@ -770,48 +770,52 @@ void Project::saveWildMonData() { } if (!groupsObject.empty()) fieldObject["groups"] = groupsObject; + OrderedJson::append(&fieldObject, fieldInfo.customData); fieldsInfoArray.append(fieldObject); } monHeadersObject["fields"] = fieldsInfoArray; OrderedJson::array encountersArray; - for (auto keyPair : wildMonData) { + for (auto keyPair : this->wildMonData) { QString key = keyPair.first; - for (auto grouplLabelPair : wildMonData[key]) { + for (auto grouplLabelPair : this->wildMonData[key]) { QString groupLabel = grouplLabelPair.first; OrderedJson::object encounterObject; encounterObject["map"] = key; encounterObject["base_label"] = groupLabel; - WildPokemonHeader encounterHeader = wildMonData[key][groupLabel]; + WildPokemonHeader encounterHeader = this->wildMonData[key][groupLabel]; for (auto fieldNamePair : encounterHeader.wildMons) { QString fieldName = fieldNamePair.first; - OrderedJson::object fieldObject; + OrderedJson::object monInfoObject; WildMonInfo monInfo = encounterHeader.wildMons[fieldName]; - fieldObject["encounter_rate"] = monInfo.encounterRate; + monInfoObject["encounter_rate"] = monInfo.encounterRate; OrderedJson::array monArray; for (WildPokemon wildMon : monInfo.wildPokemon) { OrderedJson::object monEntry; monEntry["min_level"] = wildMon.minLevel; monEntry["max_level"] = wildMon.maxLevel; monEntry["species"] = wildMon.species; + OrderedJson::append(&monEntry, wildMon.customData); monArray.push_back(monEntry); } - fieldObject["mons"] = monArray; - encounterObject[fieldName] = fieldObject; + monInfoObject["mons"] = monArray; + OrderedJson::append(&monInfoObject, monInfo.customData); + + encounterObject[fieldName] = monInfoObject; + OrderedJson::append(&encounterObject, encounterHeader.customData); } encountersArray.push_back(encounterObject); } } monHeadersObject["encounters"] = encountersArray; - wildEncounterGroups.push_back(monHeadersObject); + OrderedJson::append(&monHeadersObject, this->customWildMonGroupData); - // add extra Json objects that are not associated with maps to the file - for (auto extraObject : extraEncounterGroups) { - wildEncounterGroups.push_back(extraObject); - } + wildEncounterGroups.push_back(monHeadersObject); + OrderedJson::append(&wildEncounterGroups, this->extraEncounterGroups); wildEncountersObject["wild_encounter_groups"] = wildEncounterGroups; + OrderedJson::append(&wildEncountersObject, this->customWildMonData); ignoreWatchedFileTemporarily(wildEncountersJsonFilepath); OrderedJson encounterJson(wildEncountersObject); @@ -1607,6 +1611,8 @@ bool Project::readWildMonData() { this->pokemonMaxLevel = 100; this->maxEncounterRate = 2880/16; this->wildEncountersLoaded = false; + this->customWildMonData = OrderedJson::object(); + this->customWildMonGroupData = OrderedJson::object(); if (!userConfig.useEncounterJson) { return true; } @@ -1652,7 +1658,8 @@ bool Project::readWildMonData() { QMap> encounterRateFrequencyMaps; // Parse "wild_encounter_groups". This is the main object array containing all the data in this file. - for (OrderedJson mainArrayJson : wildMonObj["wild_encounter_groups"].array_items()) { + OrderedJson::array mainArray = wildMonObj.take("wild_encounter_groups").array_items(); + for (const OrderedJson &mainArrayJson : mainArray) { OrderedJson::object mainArrayObject = mainArrayJson.object_items(); // We're only interested in wild encounter data that's associated with maps ("for_maps" == true). @@ -1661,10 +1668,14 @@ bool Project::readWildMonData() { if (!mainArrayObject["for_maps"].bool_value()) { this->extraEncounterGroups.push_back(mainArrayObject); continue; + } else { + // Note: We don't call 'take' above, we don't want to strip data from extraEncounterGroups. + // We do want to strip it from the main group, because it shouldn't be treated as custom data. + mainArrayObject.erase("for_maps"); } // If multiple "for_maps" data sets are found they will be collapsed into a single set. - QString label = mainArrayObject["label"].string_value(); + QString label = mainArrayObject.take("label").string_value(); if (this->wildMonTableName.isEmpty()) { this->wildMonTableName = label; } else { @@ -1677,24 +1688,25 @@ bool Project::readWildMonData() { // Each element describes a type of wild encounter Porymap can expect to find, and we represent this data with an EncounterField. // They should contain a name ("type"), the number of encounter slots and the ratio at which they occur ("encounter_rates"), // and whether the encounters are divided into groups (like fishing rods). - for (const OrderedJson &fieldJson : mainArrayObject["fields"].array_items()) { + for (const OrderedJson &fieldJson : mainArrayObject.take("fields").array_items()) { OrderedJson::object fieldObject = fieldJson.object_items(); EncounterField encounterField; - encounterField.name = fieldObject["type"].string_value(); + encounterField.name = fieldObject.take("type").string_value(); - for (auto val : fieldObject["encounter_rates"].array_items()) { + for (auto val : fieldObject.take("encounter_rates").array_items()) { encounterField.encounterRates.append(val.int_value()); } // Each element of the "groups" array is an object with the group name as the key (e.g. "old_rod") // and an array of slot numbers indicating which encounter slots in this encounter type belong to that group. - for (auto groupPair : fieldObject["groups"].object_items()) { + for (auto groupPair : fieldObject.take("groups").object_items()) { const QString groupName = groupPair.first; for (auto slotNum : groupPair.second.array_items()) { encounterField.groups[groupName].append(slotNum.int_value()); } } + encounterField.customData = fieldObject; encounterRateFrequencyMaps.insert(encounterField.name, QMap()); this->wildMonFields.append(encounterField); @@ -1704,7 +1716,7 @@ bool Project::readWildMonData() { // Each element is an object that will tell us which map it's associated with, // its symbol name (which we will display in the Groups dropdown) and a list of // pokémon associated with any of the encounter types described by the data we parsed above. - for (const auto &encounterJson : mainArrayObject["encounters"].array_items()) { + for (const auto &encounterJson : mainArrayObject.take("encounters").array_items()) { OrderedJson::object encounterObj = encounterJson.object_items(); WildPokemonHeader header; @@ -1712,29 +1724,31 @@ bool Project::readWildMonData() { // Check for each possible encounter type. for (const EncounterField &monField : this->wildMonFields) { const QString field = monField.name; - if (encounterObj[field].is_null()) { + if (!encounterObj.contains(field)) { // Encounter type isn't present continue; } - OrderedJson::object encounterFieldObj = encounterObj[field].object_items(); + OrderedJson::object encounterFieldObj = encounterObj.take(field).object_items(); WildMonInfo monInfo; monInfo.active = true; // Read encounter rate - monInfo.encounterRate = encounterFieldObj["encounter_rate"].int_value(); + monInfo.encounterRate = encounterFieldObj.take("encounter_rate").int_value(); encounterRateFrequencyMaps[field][monInfo.encounterRate]++; // Read wild pokémon list - for (auto monJson : encounterFieldObj["mons"].array_items()) { + for (const auto &monJson : encounterFieldObj.take("mons").array_items()) { OrderedJson::object monObj = monJson.object_items(); WildPokemon newMon; - newMon.minLevel = monObj["min_level"].int_value(); - newMon.maxLevel = monObj["max_level"].int_value(); - newMon.species = monObj["species"].string_value(); + newMon.minLevel = monObj.take("min_level").int_value(); + newMon.maxLevel = monObj.take("max_level").int_value(); + newMon.species = monObj.take("species").string_value(); + newMon.customData = monObj; monInfo.wildPokemon.append(newMon); } + monInfo.customData = encounterFieldObj; // If the user supplied too few pokémon for this group then we fill in the rest with default values. for (int i = monInfo.wildPokemon.length(); i < monField.encounterRates.length(); i++) { @@ -1742,13 +1756,15 @@ bool Project::readWildMonData() { } header.wildMons[field] = monInfo; } - - const QString mapConstant = encounterObj["map"].string_value(); - const QString baseLabel = encounterObj["base_label"].string_value(); + const QString mapConstant = encounterObj.take("map").string_value(); + const QString baseLabel = encounterObj.take("base_label").string_value(); + header.customData = encounterObj; this->wildMonData[mapConstant].insert({baseLabel, header}); this->encounterGroupLabels.append(baseLabel); } + this->customWildMonGroupData = mainArrayObject; } + this->customWildMonData = wildMonObj; // For each encounter type, set default encounter rate to most common value. // Iterate over map of encounter type names to frequency maps...