From 695c22b274f30da05eab947ee8f07f9d5b58d51e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 02:59:53 -0700 Subject: [PATCH] feat(editor): add WCFG JSON round-trip (--export/--import-wcfg-json) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dual encoding for both WCFG enums via the readEnumField template: configKind (int 0..7 OR 255 OR token "xprate"/ "droprate"/"honorrate"/"restedxp"/"realmtype"/ "worldflag"/"performance"/"security"/"misc") and valueKind (int 0..3 OR token "float"/"int"/"bool"/ "string"). restartRequired accepts bool or int. The polymorphic value field is preserved on disk by emitting ALL THREE value carriers (floatValue, intValue, strValue) regardless of valueKind — only the matching one is meaningful at runtime, but the others must round-trip byte-identically. JSON also includes the activeValue derived field rendering only the meaningful one per kind, so operators editing the sidecar see the right value front-and-center. intValue uses int64_t for the trade-gold-cap and other high-magnitude configs that exceed uint32 range. All 3 presets (rates/perf/security with mixed value kinds: Float, Int, Bool, String) byte-identical roundtrip OK. CLI flag count 1267 -> 1269. --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_help.cpp | 4 + tools/editor/cli_server_config_catalog.cpp | 187 +++++++++++++++++++++ 3 files changed, 192 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 988366df..7e3685e3 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -370,6 +370,7 @@ const char* const kArgRequired[] = { "--export-wskp-json", "--import-wskp-json", "--gen-cfg", "--gen-cfg-perf", "--gen-cfg-sec", "--info-wcfg", "--validate-wcfg", + "--export-wcfg-json", "--import-wcfg-json", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 3791846c..0f694e37 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2443,6 +2443,10 @@ void printUsage(const char* argv0) { std::printf(" Print WCFG entries (id / configKind / valueKind / restart-required / active value rendered per kind / name)\n"); std::printf(" --validate-wcfg [--json]\n"); std::printf(" Static checks: id+name required, configKind 0..7 OR 255 Misc, valueKind 0..3, no duplicate configIds OR config names (server name-based lookups would be ambiguous), Bool valueKind requires intValue 0/1; warns on cross-field bleed (Float kind with non-zero intValue, etc.)\n"); + std::printf(" --export-wcfg-json [out.json]\n"); + std::printf(" Export binary .wcfg to a human-editable JSON sidecar (defaults to .wcfg.json; emits configKind + valueKind as int+name; ALL three value carriers (floatValue/intValue/strValue) plus an activeValue derived field rendering only the meaningful one per kind)\n"); + std::printf(" --import-wcfg-json [out-base]\n"); + std::printf(" Import a .wcfg.json sidecar back into binary .wcfg (configKind int OR \"xprate\"/\"droprate\"/\"honorrate\"/\"restedxp\"/\"realmtype\"/\"worldflag\"/\"performance\"/\"security\"/\"misc\"; valueKind int OR \"float\"/\"int\"/\"bool\"/\"string\"; restartRequired bool OR int)\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_server_config_catalog.cpp b/tools/editor/cli_server_config_catalog.cpp index 9ac6e839..9728cf1c 100644 --- a/tools/editor/cli_server_config_catalog.cpp +++ b/tools/editor/cli_server_config_catalog.cpp @@ -178,6 +178,187 @@ int handleInfo(int& i, int argc, char** argv) { return 0; } +int parseConfigKindToken(const std::string& s) { + using C = wowee::pipeline::WoweeServerConfig; + if (s == "xprate") return C::XPRate; + if (s == "droprate") return C::DropRate; + if (s == "honorrate") return C::HonorRate; + if (s == "restedxp") return C::RestedXP; + if (s == "realmtype") return C::RealmType; + if (s == "worldflag") return C::WorldFlag; + if (s == "performance") return C::Performance; + if (s == "security") return C::Security; + if (s == "misc") return C::Misc; + return -1; +} + +int parseValueKindToken(const std::string& s) { + using C = wowee::pipeline::WoweeServerConfig; + if (s == "float") return C::Float; + if (s == "int") return C::Int; + if (s == "bool") return C::Bool; + if (s == "string") return C::String; + return -1; +} + +template +bool readEnumField(const nlohmann::json& je, + const char* intKey, + const char* nameKey, + ParseFn parseFn, + const char* label, + uint32_t entryId, + uint8_t& outValue) { + if (je.contains(intKey)) { + const auto& v = je[intKey]; + if (v.is_string()) { + int parsed = parseFn(v.get()); + if (parsed < 0) { + std::fprintf(stderr, + "import-wcfg-json: unknown %s token " + "'%s' on entry id=%u\n", + label, v.get().c_str(), + entryId); + return false; + } + outValue = static_cast(parsed); + return true; + } + if (v.is_number_integer()) { + outValue = static_cast(v.get()); + return true; + } + } + if (je.contains(nameKey) && je[nameKey].is_string()) { + int parsed = parseFn(je[nameKey].get()); + if (parsed >= 0) { + outValue = static_cast(parsed); + return true; + } + } + return true; +} + +int handleExportJson(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string out; + if (parseOptArg(i, argc, argv)) out = argv[++i]; + base = stripWcfgExt(base); + if (out.empty()) out = base + ".wcfg.json"; + if (!wowee::pipeline::WoweeServerConfigLoader::exists(base)) { + std::fprintf(stderr, + "export-wcfg-json: WCFG not found: %s.wcfg\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeServerConfigLoader::load(base); + nlohmann::json j; + j["magic"] = "WCFG"; + j["version"] = 1; + j["name"] = c.name; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"configId", e.configId}, + {"name", e.name}, + {"description", e.description}, + {"configKind", e.configKind}, + {"configKindName", configKindName(e.configKind)}, + {"valueKind", e.valueKind}, + {"valueKindName", valueKindName(e.valueKind)}, + {"restartRequired", e.restartRequired != 0}, + {"floatValue", e.floatValue}, + {"intValue", e.intValue}, + {"strValue", e.strValue}, + {"activeValue", activeValueString(e)}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::ofstream os(out); + if (!os) { + std::fprintf(stderr, + "export-wcfg-json: failed to open %s for write\n", + out.c_str()); + return 1; + } + os << j.dump(2) << "\n"; + std::printf("Wrote %s (%zu configs)\n", + out.c_str(), c.entries.size()); + return 0; +} + +int handleImportJson(int& i, int argc, char** argv) { + std::string in = argv[++i]; + std::string outBase; + if (parseOptArg(i, argc, argv)) outBase = argv[++i]; + if (outBase.empty()) { + outBase = in; + if (outBase.size() >= 10 && + outBase.substr(outBase.size() - 10) == ".wcfg.json") { + outBase.resize(outBase.size() - 10); + } else { + stripExt(outBase, ".json"); + stripExt(outBase, ".wcfg"); + } + } + std::ifstream is(in); + if (!is) { + std::fprintf(stderr, + "import-wcfg-json: cannot open %s\n", in.c_str()); + return 1; + } + nlohmann::json j; + try { + is >> j; + } catch (const std::exception& ex) { + std::fprintf(stderr, + "import-wcfg-json: JSON parse error: %s\n", ex.what()); + return 1; + } + wowee::pipeline::WoweeServerConfig c; + c.name = j.value("name", std::string{}); + if (!j.contains("entries") || !j["entries"].is_array()) { + std::fprintf(stderr, + "import-wcfg-json: missing or non-array 'entries'\n"); + return 1; + } + for (const auto& je : j["entries"]) { + wowee::pipeline::WoweeServerConfig::Entry e; + e.configId = je.value("configId", 0u); + e.name = je.value("name", std::string{}); + e.description = je.value("description", std::string{}); + if (!readEnumField(je, "configKind", "configKindName", + parseConfigKindToken, "configKind", + e.configId, e.configKind)) return 1; + if (!readEnumField(je, "valueKind", "valueKindName", + parseValueKindToken, "valueKind", + e.configId, e.valueKind)) return 1; + if (je.contains("restartRequired")) { + const auto& v = je["restartRequired"]; + if (v.is_boolean()) + e.restartRequired = v.get() ? 1 : 0; + else if (v.is_number_integer()) + e.restartRequired = static_cast( + v.get() != 0 ? 1 : 0); + } + e.floatValue = je.value("floatValue", 0.0f); + e.intValue = je.value("intValue", int64_t{0}); + e.strValue = je.value("strValue", std::string{}); + e.iconColorRGBA = je.value("iconColorRGBA", 0xFFFFFFFFu); + c.entries.push_back(e); + } + if (!wowee::pipeline::WoweeServerConfigLoader::save(c, outBase)) { + std::fprintf(stderr, + "import-wcfg-json: failed to save %s.wcfg\n", + outBase.c_str()); + return 1; + } + std::printf("Wrote %s.wcfg (%zu configs)\n", + outBase.c_str(), c.entries.size()); + return 0; +} + int handleValidate(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); @@ -311,6 +492,12 @@ bool handleServerConfigCatalog(int& i, int argc, char** argv, if (std::strcmp(argv[i], "--validate-wcfg") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--export-wcfg-json") == 0 && i + 1 < argc) { + outRc = handleExportJson(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--import-wcfg-json") == 0 && i + 1 < argc) { + outRc = handleImportJson(i, argc, argv); return true; + } return false; }