Preserve custom fields in wild_encounters.json

This commit is contained in:
GriffinR 2025-04-04 11:54:39 -04:00
parent 029e959bfe
commit f3a28848b9
10 changed files with 90 additions and 56 deletions

View File

@ -58,7 +58,7 @@ public:
QMap<QString, int> readCDefinesByRegex(const QString &filename, const QSet<QString> &regexList, QString *error = nullptr);
QMap<QString, int> readCDefinesByName(const QString &filename, const QSet<QString> &names, QString *error = nullptr);
QStringList readCDefineNames(const QString &filename, const QSet<QString> &regexList, QString *error = nullptr);
tsl::ordered_map<QString, QHash<QString, QString>> readCStructs(const QString &, const QString & = "", const QHash<int, QString>& = {});
OrderedMap<QString, QHash<QString, QString>> readCStructs(const QString &, const QString & = "", const QHash<int, QString>& = {});
QList<QStringList> getLabelMacros(const QList<QStringList>&, const QString&);
QStringList getLabelValues(const QList<QStringList>&, const QString&);
bool tryParseJsonFile(QJsonDocument *out, const QString &filepath, QString *error = nullptr);

View File

@ -3,7 +3,7 @@
#define GUARD_WILDMONINFO_H
#include <QtWidgets>
#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> wildPokemon;
OrderedJson::object customData;
};
struct WildPokemonHeader {
tsl::ordered_map<QString, WildMonInfo> wildMons;
OrderedMap<QString, WildMonInfo> wildMons;
OrderedJson::object customData;
};
struct EncounterField {
QString name; // Ex: "fishing_mons"
QVector<int> encounterRates;
tsl::ordered_map<QString, QVector<int>> groups; // Ex: "good_rod", {2, 3, 4}
OrderedMap<QString, QVector<int>> groups; // Ex: "good_rod", {2, 3, 4}
OrderedJson::object customData;
};
typedef QVector<EncounterField> EncounterFields;

View File

@ -99,7 +99,7 @@ public:
// Array and object typedefs
typedef QVector<Json> array;
typedef tsl::ordered_map<QString, Json> object;
typedef OrderedMap<QString, Json> 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.

View File

@ -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<class Key, class T>
using OrderedMap = tsl::ordered_map<Key, T>;
#endif

View File

@ -143,12 +143,11 @@ public:
QString getNewHealLocationName(const Map* map) const;
bool readWildMonData();
tsl::ordered_map<QString, tsl::ordered_map<QString, WildPokemonHeader>> wildMonData;
OrderedMap<QString, OrderedMap<QString, WildPokemonHeader>> wildMonData;
QString wildMonTableName;
QVector<EncounterField> wildMonFields;
QVector<QString> encounterGroupLabels;
QVector<poryjson::Json::object> 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

View File

@ -54,7 +54,7 @@ private:
Project *project;
RegionMap *region_map = nullptr;
tsl::ordered_map<QString, RegionMap *> region_maps;
OrderedMap<QString, RegionMap *> region_maps;
QString configFilepath;

View File

@ -610,12 +610,12 @@ bool ParseUtil::gameStringToBool(const QString &gameString, bool * ok) {
return gameStringToInt(gameString, ok) != 0;
}
tsl::ordered_map<QString, QHash<QString, QString>> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash<int, QString> &memberMap) {
OrderedMap<QString, QHash<QString, QString>> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash<int, QString> &memberMap) {
QString filePath = pathWithRoot(filename);
auto cParser = fex::Parser();
auto tokens = fex::Lexer().LexFile(filePath);
auto topLevelObjects = cParser.ParseTopLevelObjects(tokens);
tsl::ordered_map<QString, QHash<QString, QString>> structs;
OrderedMap<QString, QHash<QString, QString>> structs;
for (auto it = topLevelObjects.begin(); it != topLevelObjects.end(); it++) {
QString structLabel = QString::fromStdString(it->first);
if (structLabel.isEmpty()) continue;

View File

@ -596,7 +596,7 @@ void Editor::configureEncounterJSON(QWidget *window) {
if (newNameDialog.exec() == QDialog::Accepted) {
QString newFieldName = newNameEdit->text();
QVector<int> 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<QString, WildPokemonHeader> &encounterMap = project->wildMonData[map->constantName()];
OrderedMap<QString, WildPokemonHeader> &encounterMap = project->wildMonData[map->constantName()];
for (int groupIndex = 0; groupIndex < stack->count(); groupIndex++) {
MonTabWidget *tabWidget = static_cast<MonTabWidget *>(stack->widget(groupIndex));

View File

@ -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
*/

View File

@ -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<QString, QMap<int, int>> 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<int, int>());
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...