From 24bc52ab111145fc9e31d71f69ca8ef4df35537f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 15:20:05 -0700 Subject: [PATCH] feat(editor): add WLOT JSON round-trip authoring workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the WLOT open-format loop with --export-wlot-json / --import-wlot-json, mirroring the WOL/WOW/WOMX/WSND/WSPN/WIT JSON pairs. All 7 binary formats added since WOL now have full JSON round-trip authoring. Each loot table round-trips: • table-level: creatureId, flags (int + flagsList strings), dropCount, money min/max (copper) • per-drop: itemId, chancePercent (float), minQty / maxQty, flags (int + flagsList) Both flag fields emit dual int + named string-array forms. A hand-author can write ["quest", "always"] instead of having to remember that QuestRequired|AlwaysDrop = 5. Verified byte-identical round-trip on the boss preset (6 drops including the QuestRequired+AlwaysDrop combo on the guaranteed quest item, group-only epic at 5%, mass-loot trade goods at 90%). Adds 2 flags (495 documented total now). --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_help.cpp | 4 + tools/editor/cli_loot_catalog.cpp | 169 ++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index c8bfcfcc..639d7e8f 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -37,6 +37,7 @@ const char* const kArgRequired[] = { "--export-wit-json", "--import-wit-json", "--gen-loot", "--gen-loot-bandit", "--gen-loot-boss", "--info-wlot", "--validate-wlot", + "--export-wlot-json", "--import-wlot-json", "--gen-creatures", "--gen-creatures-bandit", "--gen-creatures-merchants", "--info-wcrt", "--validate-wcrt", "--gen-weather-temperate", "--gen-weather-arctic", diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 7580ac26..09db717f 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -891,6 +891,10 @@ void printUsage(const char* argv0) { std::printf(" Print WLOT loot tables (creatureId / dropCount / money range / per-drop chance + qty + flags)\n"); std::printf(" --validate-wlot [--json]\n"); std::printf(" Static checks: creatureId>0 + unique, chance in 0..100, minQty<=maxQty, money min<=max\n"); + std::printf(" --export-wlot-json [out.json]\n"); + std::printf(" Export binary .wlot to a human-editable JSON sidecar (defaults to .wlot.json)\n"); + std::printf(" --import-wlot-json [out-base]\n"); + std::printf(" Import a .wlot.json sidecar back into binary .wlot (accepts flag int OR flagsList strings)\n"); std::printf(" --gen-creatures [name]\n"); std::printf(" Emit .wcrt starter creature template: 1 friendly innkeeper (vendor + repair flags)\n"); std::printf(" --gen-creatures-bandit [name]\n"); diff --git a/tools/editor/cli_loot_catalog.cpp b/tools/editor/cli_loot_catalog.cpp index 8390c641..b3abc455 100644 --- a/tools/editor/cli_loot_catalog.cpp +++ b/tools/editor/cli_loot_catalog.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -170,6 +171,168 @@ int handleInfo(int& i, int argc, char** argv) { return 0; } +int handleExportJson(int& i, int argc, char** argv) { + // Mirrors WOL/WOW/WOMX/WSND/WSPN/WIT JSON pairs. Each + // table emits creatureId / dropCount / money range plus + // a sub-array of per-drop entries; flags also emit a + // string-array form so a hand-author can write + // ["quest", "always"] instead of "5". + std::string base = argv[++i]; + std::string outPath; + if (parseOptArg(i, argc, argv)) outPath = argv[++i]; + base = stripWlotExt(base); + if (outPath.empty()) outPath = base + ".wlot.json"; + if (!wowee::pipeline::WoweeLootLoader::exists(base)) { + std::fprintf(stderr, + "export-wlot-json: WLOT not found: %s.wlot\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLootLoader::load(base); + nlohmann::json j; + j["name"] = c.name; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string fs; + appendTableFlagsStr(fs, e.flags); + nlohmann::json je; + je["creatureId"] = e.creatureId; + je["flags"] = e.flags; + nlohmann::json fa = nlohmann::json::array(); + if (e.flags & wowee::pipeline::WoweeLoot::QuestOnly) fa.push_back("quest-only"); + if (e.flags & wowee::pipeline::WoweeLoot::GroupOnly) fa.push_back("group-only"); + if (e.flags & wowee::pipeline::WoweeLoot::Pickpocket) fa.push_back("pickpocket"); + je["flagsList"] = fa; + je["dropCount"] = e.dropCount; + je["moneyMinCopper"] = e.moneyMinCopper; + je["moneyMaxCopper"] = e.moneyMaxCopper; + nlohmann::json drops = nlohmann::json::array(); + for (const auto& d : e.itemDrops) { + nlohmann::json jd; + jd["itemId"] = d.itemId; + jd["chancePercent"] = d.chancePercent; + jd["minQty"] = d.minQty; + jd["maxQty"] = d.maxQty; + jd["flags"] = d.flags; + nlohmann::json dfa = nlohmann::json::array(); + if (d.flags & wowee::pipeline::WoweeLoot::QuestRequired) + dfa.push_back("quest"); + if (d.flags & wowee::pipeline::WoweeLoot::GroupRollOnly) + dfa.push_back("group"); + if (d.flags & wowee::pipeline::WoweeLoot::AlwaysDrop) + dfa.push_back("always"); + jd["flagsList"] = dfa; + drops.push_back(jd); + } + je["itemDrops"] = drops; + arr.push_back(je); + } + j["entries"] = arr; + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-wlot-json: cannot write %s\n", outPath.c_str()); + return 1; + } + out << j.dump(2) << "\n"; + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" source : %s.wlot\n", base.c_str()); + std::printf(" tables : %zu (%u drops total)\n", + c.entries.size(), totalDrops(c)); + return 0; +} + +int handleImportJson(int& i, int argc, char** argv) { + std::string jsonPath = argv[++i]; + std::string outBase; + if (parseOptArg(i, argc, argv)) outBase = argv[++i]; + if (outBase.empty()) { + outBase = jsonPath; + std::string suffix = ".wlot.json"; + if (outBase.size() > suffix.size() && + outBase.substr(outBase.size() - suffix.size()) == suffix) { + outBase = outBase.substr(0, outBase.size() - suffix.size()); + } else if (outBase.size() > 5 && + outBase.substr(outBase.size() - 5) == ".json") { + outBase = outBase.substr(0, outBase.size() - 5); + } + } + outBase = stripWlotExt(outBase); + std::ifstream in(jsonPath); + if (!in) { + std::fprintf(stderr, + "import-wlot-json: cannot read %s\n", jsonPath.c_str()); + return 1; + } + nlohmann::json j; + try { in >> j; } + catch (const std::exception& e) { + std::fprintf(stderr, + "import-wlot-json: bad JSON in %s: %s\n", + jsonPath.c_str(), e.what()); + return 1; + } + auto tableFlagFromName = [](const std::string& s) -> uint32_t { + if (s == "quest-only") return wowee::pipeline::WoweeLoot::QuestOnly; + if (s == "group-only") return wowee::pipeline::WoweeLoot::GroupOnly; + if (s == "pickpocket") return wowee::pipeline::WoweeLoot::Pickpocket; + return 0; + }; + auto dropFlagFromName = [](const std::string& s) -> uint8_t { + if (s == "quest") return wowee::pipeline::WoweeLoot::QuestRequired; + if (s == "group") return wowee::pipeline::WoweeLoot::GroupRollOnly; + if (s == "always") return wowee::pipeline::WoweeLoot::AlwaysDrop; + return 0; + }; + wowee::pipeline::WoweeLoot c; + c.name = j.value("name", std::string{}); + if (j.contains("entries") && j["entries"].is_array()) { + for (const auto& je : j["entries"]) { + wowee::pipeline::WoweeLoot::Entry e; + e.creatureId = je.value("creatureId", 0u); + if (je.contains("flags") && je["flags"].is_number_integer()) { + e.flags = je["flags"].get(); + } else if (je.contains("flagsList") && je["flagsList"].is_array()) { + for (const auto& f : je["flagsList"]) { + if (f.is_string()) e.flags |= tableFlagFromName(f.get()); + } + } + e.dropCount = static_cast(je.value("dropCount", 1)); + e.moneyMinCopper = je.value("moneyMinCopper", 0u); + e.moneyMaxCopper = je.value("moneyMaxCopper", 0u); + if (je.contains("itemDrops") && je["itemDrops"].is_array()) { + for (const auto& jd : je["itemDrops"]) { + wowee::pipeline::WoweeLoot::ItemDrop d; + d.itemId = jd.value("itemId", 0u); + d.chancePercent = jd.value("chancePercent", 100.0f); + d.minQty = static_cast(jd.value("minQty", 1)); + d.maxQty = static_cast(jd.value("maxQty", 1)); + if (jd.contains("flags") && jd["flags"].is_number_integer()) { + d.flags = static_cast(jd["flags"].get()); + } else if (jd.contains("flagsList") && jd["flagsList"].is_array()) { + for (const auto& f : jd["flagsList"]) { + if (f.is_string()) + d.flags |= dropFlagFromName(f.get()); + } + } + e.itemDrops.push_back(d); + } + } + c.entries.push_back(std::move(e)); + } + } + if (!wowee::pipeline::WoweeLootLoader::save(c, outBase)) { + std::fprintf(stderr, + "import-wlot-json: failed to save %s.wlot\n", outBase.c_str()); + return 1; + } + std::printf("Wrote %s.wlot\n", outBase.c_str()); + std::printf(" source : %s\n", jsonPath.c_str()); + std::printf(" tables : %zu (%u drops total)\n", + c.entries.size(), totalDrops(c)); + return 0; +} + int handleValidate(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); @@ -276,6 +439,12 @@ bool handleLootCatalog(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--validate-wlot") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--export-wlot-json") == 0 && i + 1 < argc) { + outRc = handleExportJson(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--import-wlot-json") == 0 && i + 1 < argc) { + outRc = handleImportJson(i, argc, argv); return true; + } return false; }