From 2d78dd57a71a2b1fa21d4c8e8cfffb5e78ea6fca Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 01:15:26 -0700 Subject: [PATCH] feat(editor): add WBAB JSON round-trip (--export/--import-wbab-json) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dual encoding for both WBAB enums on import: statBonusKind (int 0..9 OR 255 OR token "stamina" / "intellect" / "spirit" / "allstats" / "armor" / "spellpower" / "attackpower" / "critrating" / "hasterating" / "manaregen" / "other"), and a NEW "+"-joined bitmask string form for targetTypeMask ("self+party+raid+friendly" parsed by splitting on '+' then OR-ing the bits). The "+" syntax matches what targetMaskString emits on info display so the round- trip uses identical syntax. Per-bit token form is more useful than raw bitfield ints for hand-edited JSON — operators can read "self+raid" and immediately know the buff hits self plus all raid members without doing 0x05 & flag math. All 3 presets (mage/druid/raid) byte-identical roundtrip OK. Token-form import smoke-tested with spellpower + self+raid+friendly together. CLI flag count 1139 -> 1141. --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_buff_book_catalog.cpp | 213 +++++++++++++++++++++++++ tools/editor/cli_help.cpp | 4 + 3 files changed, 218 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index d20ce485..d25b2816 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -315,6 +315,7 @@ const char* const kArgRequired[] = { "--export-wemo-json", "--import-wemo-json", "--gen-bab", "--gen-bab-druid", "--gen-bab-raid", "--info-wbab", "--validate-wbab", + "--export-wbab-json", "--import-wbab-json", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_buff_book_catalog.cpp b/tools/editor/cli_buff_book_catalog.cpp index 7c3793a0..fa82e419 100644 --- a/tools/editor/cli_buff_book_catalog.cpp +++ b/tools/editor/cli_buff_book_catalog.cpp @@ -164,6 +164,213 @@ int handleInfo(int& i, int argc, char** argv) { return 0; } +// Token parser for statBonusKind. Returns -1 if unknown. +int parseStatBonusKindToken(const std::string& s) { + using B = wowee::pipeline::WoweeBuffBook; + if (s == "stamina") return B::Stamina; + if (s == "intellect") return B::Intellect; + if (s == "spirit") return B::Spirit; + if (s == "allstats") return B::AllStats; + if (s == "armor") return B::Armor; + if (s == "spellpower") return B::SpellPower; + if (s == "attackpower") return B::AttackPower; + if (s == "critrating") return B::CritRating; + if (s == "hasterating") return B::HasteRating; + if (s == "manaregen") return B::ManaRegen; + if (s == "other") return B::Other; + return -1; +} + +// Parse a "self+party+raid" style bitmask string into the +// targetTypeMask bits. Empty / "none" returns 0; unknown +// tokens return -1 with no partial result. The "+" form +// is what targetMaskString emits on export so the round +// trip uses the same syntax. +int parseTargetMaskString(const std::string& s) { + using B = wowee::pipeline::WoweeBuffBook; + if (s.empty() || s == "none") return 0; + int mask = 0; + size_t pos = 0; + while (pos < s.size()) { + size_t plus = s.find('+', pos); + std::string tok = (plus == std::string::npos) + ? s.substr(pos) : s.substr(pos, plus - pos); + if (tok == "self") mask |= B::TargetSelf; + else if (tok == "party") mask |= B::TargetParty; + else if (tok == "raid") mask |= B::TargetRaid; + else if (tok == "friendly") mask |= B::TargetFriendly; + else return -1; + if (plus == std::string::npos) break; + pos = plus + 1; + } + return mask; +} + +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 = stripWbabExt(base); + if (out.empty()) out = base + ".wbab.json"; + if (!wowee::pipeline::WoweeBuffBookLoader::exists(base)) { + std::fprintf(stderr, + "export-wbab-json: WBAB not found: %s.wbab\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeBuffBookLoader::load(base); + nlohmann::json j; + j["magic"] = "WBAB"; + j["version"] = 1; + j["name"] = c.name; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"buffId", e.buffId}, + {"name", e.name}, + {"description", e.description}, + {"spellId", e.spellId}, + {"castClassMask", e.castClassMask}, + {"targetTypeMask", e.targetTypeMask}, + {"targetTypeNames", targetMaskString(e.targetTypeMask)}, + {"statBonusKind", e.statBonusKind}, + {"statBonusKindName", + statBonusKindName(e.statBonusKind)}, + {"rank", e.rank}, + {"maxStackCount", e.maxStackCount}, + {"statBonusAmount", e.statBonusAmount}, + {"duration", e.duration}, + {"previousRankId", e.previousRankId}, + {"nextRankId", e.nextRankId}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::ofstream os(out); + if (!os) { + std::fprintf(stderr, + "export-wbab-json: failed to open %s for write\n", + out.c_str()); + return 1; + } + os << j.dump(2) << "\n"; + std::printf("Wrote %s (%zu buffs)\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) == ".wbab.json") { + outBase.resize(outBase.size() - 10); + } else { + stripExt(outBase, ".json"); + stripExt(outBase, ".wbab"); + } + } + std::ifstream is(in); + if (!is) { + std::fprintf(stderr, + "import-wbab-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-wbab-json: JSON parse error: %s\n", ex.what()); + return 1; + } + wowee::pipeline::WoweeBuffBook c; + c.name = j.value("name", std::string{}); + if (!j.contains("entries") || !j["entries"].is_array()) { + std::fprintf(stderr, + "import-wbab-json: missing or non-array 'entries'\n"); + return 1; + } + for (const auto& je : j["entries"]) { + wowee::pipeline::WoweeBuffBook::Entry e; + e.buffId = je.value("buffId", 0u); + e.name = je.value("name", std::string{}); + e.description = je.value("description", std::string{}); + e.spellId = je.value("spellId", 0u); + e.castClassMask = je.value("castClassMask", 0u); + // targetTypeMask: int OR "+"-joined token string. + if (je.contains("targetTypeMask")) { + const auto& tm = je["targetTypeMask"]; + if (tm.is_string()) { + int parsed = parseTargetMaskString(tm.get()); + if (parsed < 0) { + std::fprintf(stderr, + "import-wbab-json: unknown targetTypeMask " + "token in '%s' on entry id=%u\n", + tm.get().c_str(), e.buffId); + return 1; + } + e.targetTypeMask = static_cast(parsed); + } else if (tm.is_number_integer()) { + e.targetTypeMask = static_cast( + tm.get()); + } + } else if (je.contains("targetTypeNames") && + je["targetTypeNames"].is_string()) { + int parsed = parseTargetMaskString( + je["targetTypeNames"].get()); + if (parsed >= 0) + e.targetTypeMask = static_cast(parsed); + } + // statBonusKind: int OR token string. + if (je.contains("statBonusKind")) { + const auto& sk = je["statBonusKind"]; + if (sk.is_string()) { + int parsed = parseStatBonusKindToken( + sk.get()); + if (parsed < 0) { + std::fprintf(stderr, + "import-wbab-json: unknown statBonusKind " + "token '%s' on entry id=%u\n", + sk.get().c_str(), e.buffId); + return 1; + } + e.statBonusKind = static_cast(parsed); + } else if (sk.is_number_integer()) { + e.statBonusKind = static_cast( + sk.get()); + } + } else if (je.contains("statBonusKindName") && + je["statBonusKindName"].is_string()) { + int parsed = parseStatBonusKindToken( + je["statBonusKindName"].get()); + if (parsed >= 0) + e.statBonusKind = static_cast(parsed); + } + e.rank = static_cast(je.value("rank", 1u)); + e.maxStackCount = static_cast( + je.value("maxStackCount", 1u)); + e.statBonusAmount = je.value("statBonusAmount", 0); + e.duration = je.value("duration", 0u); + e.previousRankId = je.value("previousRankId", 0u); + e.nextRankId = je.value("nextRankId", 0u); + e.iconColorRGBA = je.value("iconColorRGBA", 0xFFFFFFFFu); + c.entries.push_back(e); + } + if (!wowee::pipeline::WoweeBuffBookLoader::save(c, outBase)) { + std::fprintf(stderr, + "import-wbab-json: failed to save %s.wbab\n", + outBase.c_str()); + return 1; + } + std::printf("Wrote %s.wbab (%zu buffs)\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); @@ -333,6 +540,12 @@ bool handleBuffBookCatalog(int& i, int argc, char** argv, if (std::strcmp(argv[i], "--validate-wbab") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--export-wbab-json") == 0 && i + 1 < argc) { + outRc = handleExportJson(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--import-wbab-json") == 0 && i + 1 < argc) { + outRc = handleImportJson(i, argc, argv); return true; + } return false; } diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index d8a410b3..82bce91b 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2191,6 +2191,10 @@ void printUsage(const char* argv0) { std::printf(" Print WBAB entries (id / spellId / classMask / target mask / stat kind / rank / amount / duration / prev+next rank ids / name)\n"); std::printf(" --validate-wbab [--json]\n"); std::printf(" Static checks: id+name+spellId+castClassMask+targetTypeMask required, statBonusKind 0..9 OR 255, no duplicate ids, no self-referencing rank edges, all next/prev IDs resolve to existing entries, AND back-edges symmetric (A.next=B implies B.prev=A); warns on rank=0, maxStackCount=0\n"); + std::printf(" --export-wbab-json [out.json]\n"); + std::printf(" Export binary .wbab to a human-editable JSON sidecar (defaults to .wbab.json; emits statBonusKind as int+name and targetTypeMask as both int AND \"self+party+raid\" join string)\n"); + std::printf(" --import-wbab-json [out-base]\n"); + std::printf(" Import a .wbab.json sidecar back into binary .wbab (statBonusKind int OR \"stamina\"/\"intellect\"/\"spirit\"/\"allstats\"/\"armor\"/\"spellpower\"/\"attackpower\"/\"critrating\"/\"hasterating\"/\"manaregen\"/\"other\"; targetTypeMask int OR \"+\"-joined tokens \"self\"/\"party\"/\"raid\"/\"friendly\")\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");