From 62e370545f3633320260d767282a9ce41ca5145c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 15:33:21 -0700 Subject: [PATCH] feat(editor): add WQT JSON round-trip authoring workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the WQT open-format loop with --export-wqt-json / --import-wqt-json, mirroring the WOL/WOW/WOMX/WSND/WSPN/ WIT/WLOT/WCRT JSON pairs. All 9 binary formats added since WOL now have full JSON round-trip authoring. Each quest round-trips: • 13 scalar fields (id, level range, masks, chain links, giver/turnin, xp + money reward, flags) • 3 string fields (title, objective, description) • objectives array with dual int + name kindName • rewardItems array with dual int + name pickFlagsList The flag bitset emits string-array form so a hand-author can write ["daily", "repeatable", "auto-accept"] instead of having to remember the bit math. The objective kindName makes "visit/collect/kill" obvious without needing to know that kind=3 means VisitArea. Verified byte-identical round-trip on the 3-quest chain preset (full feature exercise: prev/next chain links, mixed objective kinds, AutoComplete bridge quest, player- choice rewards). Adds 2 flags (509 documented total now). --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_help.cpp | 4 + tools/editor/cli_quests_catalog.cpp | 224 ++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 8bed7605..a0c7f96a 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -43,6 +43,7 @@ const char* const kArgRequired[] = { "--export-wcrt-json", "--import-wcrt-json", "--gen-quests", "--gen-quests-chain", "--gen-quests-daily", "--info-wqt", "--validate-wqt", + "--export-wqt-json", "--import-wqt-json", "--gen-objects", "--gen-objects-dungeon", "--gen-objects-gather", "--info-wgot", "--validate-wgot", "--gen-weather-temperate", "--gen-weather-arctic", diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 53650272..bdbac94b 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -919,6 +919,10 @@ void printUsage(const char* argv0) { std::printf(" Print WQT entries (questId / level / giver / objectives / rewards / chain links)\n"); std::printf(" --validate-wqt [--json]\n"); std::printf(" Static checks: questId>0+unique, level>0+min<=max, title not empty, no rewards warning, daily needs repeatable\n"); + std::printf(" --export-wqt-json [out.json]\n"); + std::printf(" Export binary .wqt to a human-editable JSON sidecar (defaults to .wqt.json)\n"); + std::printf(" --import-wqt-json [out-base]\n"); + std::printf(" Import a .wqt.json sidecar back into binary .wqt (accepts kind/flag int OR name forms)\n"); std::printf(" --gen-objects [name]\n"); std::printf(" Emit .wgot starter object catalog: 1 chest + 1 mailbox + 1 sign\n"); std::printf(" --gen-objects-dungeon [name]\n"); diff --git a/tools/editor/cli_quests_catalog.cpp b/tools/editor/cli_quests_catalog.cpp index ae469811..70ef942f 100644 --- a/tools/editor/cli_quests_catalog.cpp +++ b/tools/editor/cli_quests_catalog.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -191,6 +192,223 @@ int handleInfo(int& i, int argc, char** argv) { return 0; } +int handleExportJson(int& i, int argc, char** argv) { + // Mirrors the JSON pairs added for every other novel + // open format. Each quest emits all 14 scalar fields plus + // the variable-length objectives + rewards arrays. + std::string base = argv[++i]; + std::string outPath; + if (parseOptArg(i, argc, argv)) outPath = argv[++i]; + base = stripWqtExt(base); + if (outPath.empty()) outPath = base + ".wqt.json"; + if (!wowee::pipeline::WoweeQuestLoader::exists(base)) { + std::fprintf(stderr, + "export-wqt-json: WQT not found: %s.wqt\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeQuestLoader::load(base); + nlohmann::json j; + j["name"] = c.name; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + nlohmann::json je; + je["questId"] = e.questId; + je["title"] = e.title; + je["objective"] = e.objective; + je["description"] = e.description; + je["minLevel"] = e.minLevel; + je["questLevel"] = e.questLevel; + je["maxLevel"] = e.maxLevel; + je["requiredClassMask"] = e.requiredClassMask; + je["requiredRaceMask"] = e.requiredRaceMask; + je["prevQuestId"] = e.prevQuestId; + je["nextQuestId"] = e.nextQuestId; + je["giverCreatureId"] = e.giverCreatureId; + je["turninCreatureId"] = e.turninCreatureId; + nlohmann::json oa = nlohmann::json::array(); + for (const auto& o : e.objectives) { + oa.push_back({ + {"kind", o.kind}, + {"kindName", wowee::pipeline::WoweeQuest::objectiveKindName(o.kind)}, + {"targetId", o.targetId}, + {"quantity", o.quantity}, + }); + } + je["objectives"] = oa; + je["xpReward"] = e.xpReward; + je["moneyCopperReward"] = e.moneyCopperReward; + nlohmann::json ra = nlohmann::json::array(); + for (const auto& r : e.rewardItems) { + nlohmann::json jr; + jr["itemId"] = r.itemId; + jr["qty"] = r.qty; + jr["pickFlags"] = r.pickFlags; + nlohmann::json pa = nlohmann::json::array(); + if (r.pickFlags & wowee::pipeline::WoweeQuest::AutoGiven) + pa.push_back("auto"); + if (r.pickFlags & wowee::pipeline::WoweeQuest::PlayerChoice) + pa.push_back("choice"); + jr["pickFlagsList"] = pa; + ra.push_back(jr); + } + je["rewardItems"] = ra; + je["flags"] = e.flags; + nlohmann::json fa = nlohmann::json::array(); + if (e.flags & wowee::pipeline::WoweeQuest::Daily) fa.push_back("daily"); + if (e.flags & wowee::pipeline::WoweeQuest::Weekly) fa.push_back("weekly"); + if (e.flags & wowee::pipeline::WoweeQuest::Raid) fa.push_back("raid"); + if (e.flags & wowee::pipeline::WoweeQuest::Group) fa.push_back("group"); + if (e.flags & wowee::pipeline::WoweeQuest::AutoComplete) fa.push_back("auto-complete"); + if (e.flags & wowee::pipeline::WoweeQuest::AutoAccept) fa.push_back("auto-accept"); + if (e.flags & wowee::pipeline::WoweeQuest::Repeatable) fa.push_back("repeatable"); + if (e.flags & wowee::pipeline::WoweeQuest::ClassQuest) fa.push_back("class"); + if (e.flags & wowee::pipeline::WoweeQuest::Pvp) fa.push_back("pvp"); + je["flagsList"] = fa; + arr.push_back(je); + } + j["entries"] = arr; + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-wqt-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.wqt\n", base.c_str()); + std::printf(" quests : %zu\n", c.entries.size()); + 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 = ".wqt.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 = stripWqtExt(outBase); + std::ifstream in(jsonPath); + if (!in) { + std::fprintf(stderr, + "import-wqt-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-wqt-json: bad JSON in %s: %s\n", + jsonPath.c_str(), e.what()); + return 1; + } + auto kindFromName = [](const std::string& s) -> uint8_t { + if (s == "kill") return wowee::pipeline::WoweeQuest::KillCreature; + if (s == "collect") return wowee::pipeline::WoweeQuest::CollectItem; + if (s == "interact") return wowee::pipeline::WoweeQuest::InteractObject; + if (s == "visit") return wowee::pipeline::WoweeQuest::VisitArea; + if (s == "escort") return wowee::pipeline::WoweeQuest::EscortNpc; + if (s == "cast") return wowee::pipeline::WoweeQuest::SpellCast; + return wowee::pipeline::WoweeQuest::KillCreature; + }; + auto pickFlagFromName = [](const std::string& s) -> uint8_t { + if (s == "auto") return wowee::pipeline::WoweeQuest::AutoGiven; + if (s == "choice") return wowee::pipeline::WoweeQuest::PlayerChoice; + return 0; + }; + auto questFlagFromName = [](const std::string& s) -> uint32_t { + if (s == "daily") return wowee::pipeline::WoweeQuest::Daily; + if (s == "weekly") return wowee::pipeline::WoweeQuest::Weekly; + if (s == "raid") return wowee::pipeline::WoweeQuest::Raid; + if (s == "group") return wowee::pipeline::WoweeQuest::Group; + if (s == "auto-complete") return wowee::pipeline::WoweeQuest::AutoComplete; + if (s == "auto-accept") return wowee::pipeline::WoweeQuest::AutoAccept; + if (s == "repeatable") return wowee::pipeline::WoweeQuest::Repeatable; + if (s == "class") return wowee::pipeline::WoweeQuest::ClassQuest; + if (s == "pvp") return wowee::pipeline::WoweeQuest::Pvp; + return 0; + }; + wowee::pipeline::WoweeQuest c; + c.name = j.value("name", std::string{}); + if (j.contains("entries") && j["entries"].is_array()) { + for (const auto& je : j["entries"]) { + wowee::pipeline::WoweeQuest::Entry e; + e.questId = je.value("questId", 0u); + e.title = je.value("title", std::string{}); + e.objective = je.value("objective", std::string{}); + e.description = je.value("description", std::string{}); + e.minLevel = static_cast(je.value("minLevel", 1)); + e.questLevel = static_cast(je.value("questLevel", 1)); + e.maxLevel = static_cast(je.value("maxLevel", 0)); + e.requiredClassMask = je.value("requiredClassMask", 0u); + e.requiredRaceMask = je.value("requiredRaceMask", 0u); + e.prevQuestId = je.value("prevQuestId", 0u); + e.nextQuestId = je.value("nextQuestId", 0u); + e.giverCreatureId = je.value("giverCreatureId", 0u); + e.turninCreatureId = je.value("turninCreatureId", 0u); + if (je.contains("objectives") && je["objectives"].is_array()) { + for (const auto& jo : je["objectives"]) { + wowee::pipeline::WoweeQuest::Objective o; + if (jo.contains("kind") && jo["kind"].is_number_integer()) { + o.kind = static_cast(jo["kind"].get()); + } else if (jo.contains("kindName") && jo["kindName"].is_string()) { + o.kind = kindFromName(jo["kindName"].get()); + } + o.targetId = jo.value("targetId", 0u); + o.quantity = static_cast(jo.value("quantity", 1)); + e.objectives.push_back(o); + } + } + e.xpReward = je.value("xpReward", 0u); + e.moneyCopperReward = je.value("moneyCopperReward", 0u); + if (je.contains("rewardItems") && je["rewardItems"].is_array()) { + for (const auto& jr : je["rewardItems"]) { + wowee::pipeline::WoweeQuest::RewardItem r; + r.itemId = jr.value("itemId", 0u); + r.qty = static_cast(jr.value("qty", 1)); + if (jr.contains("pickFlags") && jr["pickFlags"].is_number_integer()) { + r.pickFlags = static_cast(jr["pickFlags"].get()); + } else if (jr.contains("pickFlagsList") && jr["pickFlagsList"].is_array()) { + r.pickFlags = 0; + for (const auto& f : jr["pickFlagsList"]) { + if (f.is_string()) + r.pickFlags |= pickFlagFromName(f.get()); + } + } + e.rewardItems.push_back(r); + } + } + 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 |= questFlagFromName(f.get()); + } + } + c.entries.push_back(std::move(e)); + } + } + if (!wowee::pipeline::WoweeQuestLoader::save(c, outBase)) { + std::fprintf(stderr, + "import-wqt-json: failed to save %s.wqt\n", outBase.c_str()); + return 1; + } + std::printf("Wrote %s.wqt\n", outBase.c_str()); + std::printf(" source : %s\n", jsonPath.c_str()); + std::printf(" quests : %zu\n", c.entries.size()); + return 0; +} + int handleValidate(int& i, int argc, char** argv) { std::string base = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); @@ -322,6 +540,12 @@ bool handleQuestsCatalog(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--validate-wqt") == 0 && i + 1 < argc) { outRc = handleValidate(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--export-wqt-json") == 0 && i + 1 < argc) { + outRc = handleExportJson(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--import-wqt-json") == 0 && i + 1 < argc) { + outRc = handleImportJson(i, argc, argv); return true; + } return false; }