From 267d525fe7c8333ddc1a55ccad6b327c18fb5d9c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 01:31:49 -0700 Subject: [PATCH] feat(editor): add WSPM JSON round-trip (--export/--import-wspm-json) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dual encoding for edgeFadeMode (int 0..2 OR token "hard" / "softedge" / "pulse"). stackable and destroyOnCancel accept both bool and int. Float fields (radius, duration) serialize as JSON floats; tickIntervalMs and decalColor as plain unsigned integers — operators editing JSON can hand-write 0xAARRGGBB hex colors via Python int() prep. All 3 presets (mage/raid/env) byte-identical roundtrip OK. Token-form import smoke-tested with mixed pulse + stackable + destroyOnCancel false. CLI flag count 1153 -> 1155. --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_help.cpp | 4 + tools/editor/cli_spell_markers_catalog.cpp | 170 +++++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index d65129b5..752a30cc 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -321,6 +321,7 @@ const char* const kArgRequired[] = { "--export-wtbd-json", "--import-wtbd-json", "--gen-spm", "--gen-spm-raid", "--gen-spm-env", "--info-wspm", "--validate-wspm", + "--export-wspm-json", "--import-wspm-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 4eafdfbf..2ef80114 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2219,6 +2219,10 @@ void printUsage(const char* argv0) { std::printf(" Print WSPM entries (id / spell / radius / duration / tick interval / fade mode / stack-flag / destroy-on-cancel / name)\n"); std::printf(" --validate-wspm [--json]\n"); std::printf(" Static checks: id+name+spellId+texturePath required, radius>0, edgeFadeMode 0..2, no duplicate markerIds, no duplicate spellIds (spell-cast lookup ambiguity); warns on radius>100yd, tickIntervalMs<100ms (perf risk for stackable), decalColor alpha=0 (invisible)\n"); + std::printf(" --export-wspm-json [out.json]\n"); + std::printf(" Export binary .wspm to a human-editable JSON sidecar (defaults to .wspm.json; emits edgeFadeMode as both int AND name string; radius+duration as floats; decalColor as 0xAARRGGBB uint32)\n"); + std::printf(" --import-wspm-json [out-base]\n"); + std::printf(" Import a .wspm.json sidecar back into binary .wspm (edgeFadeMode int OR \"hard\"/\"softedge\"/\"pulse\"; stackable/destroyOnCancel 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_spell_markers_catalog.cpp b/tools/editor/cli_spell_markers_catalog.cpp index 2e231b3d..eaa6da86 100644 --- a/tools/editor/cli_spell_markers_catalog.cpp +++ b/tools/editor/cli_spell_markers_catalog.cpp @@ -140,6 +140,170 @@ int handleInfo(int& i, int argc, char** argv) { return 0; } +int parseEdgeFadeModeToken(const std::string& s) { + using S = wowee::pipeline::WoweeSpellMarkers; + if (s == "hard") return S::Hard; + if (s == "softedge") return S::SoftEdge; + if (s == "pulse") return S::Pulse; + return -1; +} + +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 = stripWspmExt(base); + if (out.empty()) out = base + ".wspm.json"; + if (!wowee::pipeline::WoweeSpellMarkersLoader::exists(base)) { + std::fprintf(stderr, + "export-wspm-json: WSPM not found: %s.wspm\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellMarkersLoader::load(base); + nlohmann::json j; + j["magic"] = "WSPM"; + j["version"] = 1; + j["name"] = c.name; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"markerId", e.markerId}, + {"name", e.name}, + {"description", e.description}, + {"spellId", e.spellId}, + {"groundTexturePath", e.groundTexturePath}, + {"radius", e.radius}, + {"duration", e.duration}, + {"tickIntervalMs", e.tickIntervalMs}, + {"decalColor", e.decalColor}, + {"edgeFadeMode", e.edgeFadeMode}, + {"edgeFadeModeName", + edgeFadeModeName(e.edgeFadeMode)}, + {"stackable", e.stackable != 0}, + {"destroyOnCancel", e.destroyOnCancel != 0}, + {"tickSoundId", e.tickSoundId}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::ofstream os(out); + if (!os) { + std::fprintf(stderr, + "export-wspm-json: failed to open %s for write\n", + out.c_str()); + return 1; + } + os << j.dump(2) << "\n"; + std::printf("Wrote %s (%zu markers)\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) == ".wspm.json") { + outBase.resize(outBase.size() - 10); + } else { + stripExt(outBase, ".json"); + stripExt(outBase, ".wspm"); + } + } + std::ifstream is(in); + if (!is) { + std::fprintf(stderr, + "import-wspm-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-wspm-json: JSON parse error: %s\n", ex.what()); + return 1; + } + wowee::pipeline::WoweeSpellMarkers c; + c.name = j.value("name", std::string{}); + if (!j.contains("entries") || !j["entries"].is_array()) { + std::fprintf(stderr, + "import-wspm-json: missing or non-array 'entries'\n"); + return 1; + } + for (const auto& je : j["entries"]) { + wowee::pipeline::WoweeSpellMarkers::Entry e; + e.markerId = je.value("markerId", 0u); + e.name = je.value("name", std::string{}); + e.description = je.value("description", std::string{}); + e.spellId = je.value("spellId", 0u); + e.groundTexturePath = je.value("groundTexturePath", + std::string{}); + e.radius = je.value("radius", 0.0f); + e.duration = je.value("duration", 0.0f); + e.tickIntervalMs = je.value("tickIntervalMs", 0u); + e.decalColor = je.value("decalColor", 0xFFFFFFFFu); + if (je.contains("edgeFadeMode")) { + const auto& v = je["edgeFadeMode"]; + if (v.is_string()) { + int parsed = parseEdgeFadeModeToken( + v.get()); + if (parsed < 0) { + std::fprintf(stderr, + "import-wspm-json: unknown " + "edgeFadeMode token '%s' on entry " + "id=%u\n", + v.get().c_str(), + e.markerId); + return 1; + } + e.edgeFadeMode = static_cast(parsed); + } else if (v.is_number_integer()) { + e.edgeFadeMode = static_cast( + v.get()); + } + } else if (je.contains("edgeFadeModeName") && + je["edgeFadeModeName"].is_string()) { + int parsed = parseEdgeFadeModeToken( + je["edgeFadeModeName"].get()); + if (parsed >= 0) + e.edgeFadeMode = static_cast(parsed); + } + if (je.contains("stackable")) { + const auto& v = je["stackable"]; + if (v.is_boolean()) + e.stackable = v.get() ? 1 : 0; + else if (v.is_number_integer()) + e.stackable = static_cast( + v.get() != 0 ? 1 : 0); + } + if (je.contains("destroyOnCancel")) { + const auto& v = je["destroyOnCancel"]; + if (v.is_boolean()) + e.destroyOnCancel = v.get() ? 1 : 0; + else if (v.is_number_integer()) + e.destroyOnCancel = static_cast( + v.get() != 0 ? 1 : 0); + } + e.tickSoundId = je.value("tickSoundId", 0u); + e.iconColorRGBA = je.value("iconColorRGBA", 0xFFFFFFFFu); + c.entries.push_back(e); + } + if (!wowee::pipeline::WoweeSpellMarkersLoader::save(c, outBase)) { + std::fprintf(stderr, + "import-wspm-json: failed to save %s.wspm\n", + outBase.c_str()); + return 1; + } + std::printf("Wrote %s.wspm (%zu markers)\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); @@ -272,6 +436,12 @@ bool handleSpellMarkersCatalog(int& i, int argc, char** argv, if (std::strcmp(argv[i], "--validate-wspm") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--export-wspm-json") == 0 && i + 1 < argc) { + outRc = handleExportJson(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--import-wspm-json") == 0 && i + 1 < argc) { + outRc = handleImportJson(i, argc, argv); return true; + } return false; }