From 6430a3f55474c8d9b8b54e03de2b2dd8de3997dd Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 08:33:59 -0700 Subject: [PATCH] refactor(editor): extract items.json mutation handlers into cli_items_mutate.cpp Moves the three items.json mutation handlers (--set-item, --copy-zone-items, --clone-item) out of main.cpp into a new cli_items_mutate.{hpp,cpp} module. All three operate on the same {"items": [...]} schema; --set-item supports both id-lookup ("123") and index-lookup ("#0") with strict "only changed fields are written" semantics, --copy-zone-items has replace and --merge modes, --clone-item auto-assigns the smallest unused id. main.cpp shrinks by 316 lines (5,087 to 4,771) and finally drops below 5K. items.json now has its full mutation surface in one focused module instead of scattered through the megafile. --- CMakeLists.txt | 1 + tools/editor/cli_items_mutate.cpp | 364 ++++++++++++++++++++++++++++++ tools/editor/cli_items_mutate.hpp | 18 ++ tools/editor/main.cpp | 324 +------------------------- 4 files changed, 387 insertions(+), 320 deletions(-) create mode 100644 tools/editor/cli_items_mutate.cpp create mode 100644 tools/editor/cli_items_mutate.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 50a8f497..7d81258b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1343,6 +1343,7 @@ add_executable(wowee_editor tools/editor/cli_add.cpp tools/editor/cli_random.cpp tools/editor/cli_items_export.cpp + tools/editor/cli_items_mutate.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_items_mutate.cpp b/tools/editor/cli_items_mutate.cpp new file mode 100644 index 00000000..5973fe98 --- /dev/null +++ b/tools/editor/cli_items_mutate.cpp @@ -0,0 +1,364 @@ +#include "cli_items_mutate.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleSetItem(int& i, int argc, char** argv) { + // Edit fields on an existing item in place. Lookup is by + // id by default; '#N' for index lookup. Only specified + // flags are changed; everything else is preserved + // verbatim — including any extra fields added by hand. + // + // Supported flags: --name, --quality, --displayId, + // --itemLevel, --stackable. Each takes one positional + // argument that follows the flag. + std::string zoneDir = argv[++i]; + std::string lookup = argv[++i]; + namespace fs = std::filesystem; + std::string path = zoneDir + "/items.json"; + if (!fs::exists(path)) { + std::fprintf(stderr, + "set-item: %s has no items.json\n", zoneDir.c_str()); + return 1; + } + nlohmann::json doc; + try { + std::ifstream in(path); + in >> doc; + } catch (...) { + std::fprintf(stderr, + "set-item: %s is not valid JSON\n", path.c_str()); + return 1; + } + if (!doc.contains("items") || !doc["items"].is_array()) { + std::fprintf(stderr, + "set-item: %s has no 'items' array\n", path.c_str()); + return 1; + } + auto& items = doc["items"]; + int foundIdx = -1; + if (!lookup.empty() && lookup[0] == '#') { + try { + int idx = std::stoi(lookup.substr(1)); + if (idx >= 0 && static_cast(idx) < items.size()) + foundIdx = idx; + } catch (...) {} + } else { + uint32_t targetId = 0; + try { targetId = static_cast(std::stoul(lookup)); } + catch (...) { + std::fprintf(stderr, + "set-item: lookup '%s' is not a number\n", + lookup.c_str()); + return 1; + } + for (size_t k = 0; k < items.size(); ++k) { + if (items[k].contains("id") && + items[k]["id"].is_number_unsigned() && + items[k]["id"].get() == targetId) { + foundIdx = static_cast(k); + break; + } + } + } + if (foundIdx < 0) { + std::fprintf(stderr, + "set-item: no match for '%s' in %s\n", + lookup.c_str(), path.c_str()); + return 1; + } + auto& it = items[foundIdx]; + std::vector changes; + // Walk the remaining args looking for known --field value + // pairs. Anything unrecognized is reported and aborts so + // typos don't silently no-op. + while (i + 2 < argc) { + std::string flag = argv[i + 1]; + std::string val = argv[i + 2]; + if (flag.size() < 2 || flag[0] != '-' || flag[1] != '-') break; + if (flag == "--name") { + it["name"] = val; + changes.push_back("name=" + val); + } else if (flag == "--quality") { + try { + uint32_t q = static_cast(std::stoul(val)); + if (q > 6) { + std::fprintf(stderr, + "set-item: quality %u out of range (0..6)\n", q); + return 1; + } + it["quality"] = q; + changes.push_back("quality=" + val); + } catch (...) { + std::fprintf(stderr, + "set-item: --quality needs a number\n"); + return 1; + } + } else if (flag == "--displayId") { + try { + it["displayId"] = static_cast(std::stoul(val)); + changes.push_back("displayId=" + val); + } catch (...) { + std::fprintf(stderr, + "set-item: --displayId needs a number\n"); + return 1; + } + } else if (flag == "--itemLevel") { + try { + it["itemLevel"] = static_cast(std::stoul(val)); + changes.push_back("itemLevel=" + val); + } catch (...) { + std::fprintf(stderr, + "set-item: --itemLevel needs a number\n"); + return 1; + } + } else if (flag == "--stackable") { + try { + uint32_t s = static_cast(std::stoul(val)); + if (s == 0 || s > 1000) { + std::fprintf(stderr, + "set-item: stackable %u out of range (1..1000)\n", s); + return 1; + } + it["stackable"] = s; + changes.push_back("stackable=" + val); + } catch (...) { + std::fprintf(stderr, + "set-item: --stackable needs a number\n"); + return 1; + } + } else { + std::fprintf(stderr, + "set-item: unknown flag '%s' (typo?)\n", flag.c_str()); + return 1; + } + i += 2; + } + if (changes.empty()) { + std::fprintf(stderr, + "set-item: no field flags supplied — nothing to change\n"); + return 1; + } + std::ofstream out(path); + if (!out) { + std::fprintf(stderr, + "set-item: failed to write %s\n", path.c_str()); + return 1; + } + out << doc.dump(2); + out.close(); + std::printf("Updated item %d in %s:\n", foundIdx, path.c_str()); + for (const auto& c : changes) { + std::printf(" %s\n", c.c_str()); + } + return 0; +} + +int handleCopyZoneItems(int& i, int argc, char** argv) { + // Copy items from one zone to another. Default mode + // replaces the destination items.json wholesale; --merge + // appends each source item to the existing destination + // list, re-id'ing on collision so the destination's + // existing IDs are preserved and the source's new + // entries get fresh ones. + std::string fromZone = argv[++i]; + std::string toZone = argv[++i]; + bool mergeMode = false; + if (i + 1 < argc && std::strcmp(argv[i + 1], "--merge") == 0) { + mergeMode = true; i++; + } + namespace fs = std::filesystem; + std::string srcPath = fromZone + "/items.json"; + if (!fs::exists(srcPath)) { + std::fprintf(stderr, + "copy-zone-items: %s has no items.json\n", fromZone.c_str()); + return 1; + } + if (!fs::exists(toZone) || !fs::is_directory(toZone)) { + std::fprintf(stderr, + "copy-zone-items: dest %s is not a directory\n", + toZone.c_str()); + return 1; + } + nlohmann::json src; + try { + std::ifstream in(srcPath); + in >> src; + } catch (...) { + std::fprintf(stderr, + "copy-zone-items: %s is not valid JSON\n", srcPath.c_str()); + return 1; + } + if (!src.contains("items") || !src["items"].is_array()) { + std::fprintf(stderr, + "copy-zone-items: %s has no 'items' array\n", + srcPath.c_str()); + return 1; + } + std::string dstPath = toZone + "/items.json"; + nlohmann::json dst = nlohmann::json::object({{"items", + nlohmann::json::array()}}); + int copied = 0, reIded = 0; + if (mergeMode && fs::exists(dstPath)) { + try { + std::ifstream in(dstPath); + in >> dst; + } catch (...) {} + if (!dst.contains("items") || !dst["items"].is_array()) { + dst["items"] = nlohmann::json::array(); + } + std::set usedIds; + for (const auto& it : dst["items"]) { + if (it.contains("id") && it["id"].is_number_unsigned()) { + usedIds.insert(it["id"].get()); + } + } + for (const auto& it : src["items"]) { + nlohmann::json newItem = it; + uint32_t srcId = it.value("id", 0u); + if (srcId == 0 || usedIds.count(srcId)) { + // Pick the next free id. + uint32_t fresh = 1; + while (usedIds.count(fresh)) ++fresh; + newItem["id"] = fresh; + usedIds.insert(fresh); + if (srcId != 0) reIded++; + } else { + usedIds.insert(srcId); + } + dst["items"].push_back(newItem); + copied++; + } + } else { + // Replace mode: destination becomes a verbatim copy of + // the source items array. + dst["items"] = src["items"]; + copied = static_cast(src["items"].size()); + } + std::ofstream out(dstPath); + if (!out) { + std::fprintf(stderr, + "copy-zone-items: failed to write %s\n", dstPath.c_str()); + return 1; + } + out << dst.dump(2); + out.close(); + std::printf("Copied %d item(s) from %s to %s\n", + copied, fromZone.c_str(), toZone.c_str()); + std::printf(" mode : %s\n", + mergeMode ? "merge (append + re-id)" : "replace"); + std::printf(" dst total : %zu\n", dst["items"].size()); + if (reIded > 0) { + std::printf(" re-ided : %d (id collisions)\n", reIded); + } + return 0; +} + +int handleCloneItem(int& i, int argc, char** argv) { + // Duplicate the item at given 0-based index. Auto-assigns + // the smallest unused positive id; optional + // overrides the cloned name (without it the new entry + // gets " (copy)" appended). + std::string zoneDir = argv[++i]; + int idx = -1; + try { idx = std::stoi(argv[++i]); } + catch (...) { + std::fprintf(stderr, + "clone-item: index must be an integer\n"); + return 1; + } + std::string newName; + if (i + 1 < argc && argv[i + 1][0] != '-') newName = argv[++i]; + namespace fs = std::filesystem; + std::string path = zoneDir + "/items.json"; + if (!fs::exists(path)) { + std::fprintf(stderr, + "clone-item: %s has no items.json\n", zoneDir.c_str()); + return 1; + } + nlohmann::json doc; + try { + std::ifstream in(path); + in >> doc; + } catch (...) { + std::fprintf(stderr, + "clone-item: %s is not valid JSON\n", path.c_str()); + return 1; + } + if (!doc.contains("items") || !doc["items"].is_array()) { + std::fprintf(stderr, + "clone-item: %s has no 'items' array\n", path.c_str()); + return 1; + } + auto& items = doc["items"]; + if (idx < 0 || static_cast(idx) >= items.size()) { + std::fprintf(stderr, + "clone-item: index %d out of range (have %zu)\n", + idx, items.size()); + return 1; + } + // Pick the next free id. + std::set used; + for (const auto& it : items) { + if (it.contains("id") && it["id"].is_number_unsigned()) { + used.insert(it["id"].get()); + } + } + uint32_t newId = 1; + while (used.count(newId)) ++newId; + nlohmann::json clone = items[idx]; + clone["id"] = newId; + if (!newName.empty()) { + clone["name"] = newName; + } else { + std::string oldName = clone.value("name", std::string("(unnamed)")); + clone["name"] = oldName + " (copy)"; + } + items.push_back(clone); + std::ofstream out(path); + if (!out) { + std::fprintf(stderr, + "clone-item: failed to write %s\n", path.c_str()); + return 1; + } + out << doc.dump(2); + out.close(); + std::printf("Cloned item idx %d to '%s' (id=%u) in %s (now %zu total)\n", + idx, clone["name"].get().c_str(), + newId, path.c_str(), items.size()); + return 0; +} + + +} // namespace + +bool handleItemsMutate(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--set-item") == 0 && i + 2 < argc) { + outRc = handleSetItem(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--copy-zone-items") == 0 && i + 2 < argc) { + outRc = handleCopyZoneItems(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--clone-item") == 0 && i + 2 < argc) { + outRc = handleCloneItem(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_items_mutate.hpp b/tools/editor/cli_items_mutate.hpp new file mode 100644 index 00000000..47302a4e --- /dev/null +++ b/tools/editor/cli_items_mutate.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the items.json mutation handlers — edit / duplicate / +// transfer item records in a zone's items.json. +// --set-item edit fields on an existing item by id or #idx +// --copy-zone-items copy items between zones (replace or merge) +// --clone-item duplicate an item with a fresh id +// +// Returns true if matched; outRc holds the exit code. +bool handleItemsMutate(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 4d17fc50..34131e2d 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -44,6 +44,7 @@ #include "cli_add.hpp" #include "cli_random.hpp" #include "cli_items_export.hpp" +#include "cli_items_mutate.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -512,6 +513,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleItemsExport(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleItemsMutate(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1440,326 +1444,6 @@ int main(int argc, char* argv[]) { outPath.c_str(), col.triangles.size(), col.walkableCount(), col.steepCount()); return 0; - } else if (std::strcmp(argv[i], "--set-item") == 0 && i + 2 < argc) { - // Edit fields on an existing item in place. Lookup is by - // id by default; '#N' for index lookup. Only specified - // flags are changed; everything else is preserved - // verbatim — including any extra fields added by hand. - // - // Supported flags: --name, --quality, --displayId, - // --itemLevel, --stackable. Each takes one positional - // argument that follows the flag. - std::string zoneDir = argv[++i]; - std::string lookup = argv[++i]; - namespace fs = std::filesystem; - std::string path = zoneDir + "/items.json"; - if (!fs::exists(path)) { - std::fprintf(stderr, - "set-item: %s has no items.json\n", zoneDir.c_str()); - return 1; - } - nlohmann::json doc; - try { - std::ifstream in(path); - in >> doc; - } catch (...) { - std::fprintf(stderr, - "set-item: %s is not valid JSON\n", path.c_str()); - return 1; - } - if (!doc.contains("items") || !doc["items"].is_array()) { - std::fprintf(stderr, - "set-item: %s has no 'items' array\n", path.c_str()); - return 1; - } - auto& items = doc["items"]; - int foundIdx = -1; - if (!lookup.empty() && lookup[0] == '#') { - try { - int idx = std::stoi(lookup.substr(1)); - if (idx >= 0 && static_cast(idx) < items.size()) - foundIdx = idx; - } catch (...) {} - } else { - uint32_t targetId = 0; - try { targetId = static_cast(std::stoul(lookup)); } - catch (...) { - std::fprintf(stderr, - "set-item: lookup '%s' is not a number\n", - lookup.c_str()); - return 1; - } - for (size_t k = 0; k < items.size(); ++k) { - if (items[k].contains("id") && - items[k]["id"].is_number_unsigned() && - items[k]["id"].get() == targetId) { - foundIdx = static_cast(k); - break; - } - } - } - if (foundIdx < 0) { - std::fprintf(stderr, - "set-item: no match for '%s' in %s\n", - lookup.c_str(), path.c_str()); - return 1; - } - auto& it = items[foundIdx]; - std::vector changes; - // Walk the remaining args looking for known --field value - // pairs. Anything unrecognized is reported and aborts so - // typos don't silently no-op. - while (i + 2 < argc) { - std::string flag = argv[i + 1]; - std::string val = argv[i + 2]; - if (flag.size() < 2 || flag[0] != '-' || flag[1] != '-') break; - if (flag == "--name") { - it["name"] = val; - changes.push_back("name=" + val); - } else if (flag == "--quality") { - try { - uint32_t q = static_cast(std::stoul(val)); - if (q > 6) { - std::fprintf(stderr, - "set-item: quality %u out of range (0..6)\n", q); - return 1; - } - it["quality"] = q; - changes.push_back("quality=" + val); - } catch (...) { - std::fprintf(stderr, - "set-item: --quality needs a number\n"); - return 1; - } - } else if (flag == "--displayId") { - try { - it["displayId"] = static_cast(std::stoul(val)); - changes.push_back("displayId=" + val); - } catch (...) { - std::fprintf(stderr, - "set-item: --displayId needs a number\n"); - return 1; - } - } else if (flag == "--itemLevel") { - try { - it["itemLevel"] = static_cast(std::stoul(val)); - changes.push_back("itemLevel=" + val); - } catch (...) { - std::fprintf(stderr, - "set-item: --itemLevel needs a number\n"); - return 1; - } - } else if (flag == "--stackable") { - try { - uint32_t s = static_cast(std::stoul(val)); - if (s == 0 || s > 1000) { - std::fprintf(stderr, - "set-item: stackable %u out of range (1..1000)\n", s); - return 1; - } - it["stackable"] = s; - changes.push_back("stackable=" + val); - } catch (...) { - std::fprintf(stderr, - "set-item: --stackable needs a number\n"); - return 1; - } - } else { - std::fprintf(stderr, - "set-item: unknown flag '%s' (typo?)\n", flag.c_str()); - return 1; - } - i += 2; - } - if (changes.empty()) { - std::fprintf(stderr, - "set-item: no field flags supplied — nothing to change\n"); - return 1; - } - std::ofstream out(path); - if (!out) { - std::fprintf(stderr, - "set-item: failed to write %s\n", path.c_str()); - return 1; - } - out << doc.dump(2); - out.close(); - std::printf("Updated item %d in %s:\n", foundIdx, path.c_str()); - for (const auto& c : changes) { - std::printf(" %s\n", c.c_str()); - } - return 0; - } else if (std::strcmp(argv[i], "--copy-zone-items") == 0 && i + 2 < argc) { - // Copy items from one zone to another. Default mode - // replaces the destination items.json wholesale; --merge - // appends each source item to the existing destination - // list, re-id'ing on collision so the destination's - // existing IDs are preserved and the source's new - // entries get fresh ones. - std::string fromZone = argv[++i]; - std::string toZone = argv[++i]; - bool mergeMode = false; - if (i + 1 < argc && std::strcmp(argv[i + 1], "--merge") == 0) { - mergeMode = true; i++; - } - namespace fs = std::filesystem; - std::string srcPath = fromZone + "/items.json"; - if (!fs::exists(srcPath)) { - std::fprintf(stderr, - "copy-zone-items: %s has no items.json\n", fromZone.c_str()); - return 1; - } - if (!fs::exists(toZone) || !fs::is_directory(toZone)) { - std::fprintf(stderr, - "copy-zone-items: dest %s is not a directory\n", - toZone.c_str()); - return 1; - } - nlohmann::json src; - try { - std::ifstream in(srcPath); - in >> src; - } catch (...) { - std::fprintf(stderr, - "copy-zone-items: %s is not valid JSON\n", srcPath.c_str()); - return 1; - } - if (!src.contains("items") || !src["items"].is_array()) { - std::fprintf(stderr, - "copy-zone-items: %s has no 'items' array\n", - srcPath.c_str()); - return 1; - } - std::string dstPath = toZone + "/items.json"; - nlohmann::json dst = nlohmann::json::object({{"items", - nlohmann::json::array()}}); - int copied = 0, reIded = 0; - if (mergeMode && fs::exists(dstPath)) { - try { - std::ifstream in(dstPath); - in >> dst; - } catch (...) {} - if (!dst.contains("items") || !dst["items"].is_array()) { - dst["items"] = nlohmann::json::array(); - } - std::set usedIds; - for (const auto& it : dst["items"]) { - if (it.contains("id") && it["id"].is_number_unsigned()) { - usedIds.insert(it["id"].get()); - } - } - for (const auto& it : src["items"]) { - nlohmann::json newItem = it; - uint32_t srcId = it.value("id", 0u); - if (srcId == 0 || usedIds.count(srcId)) { - // Pick the next free id. - uint32_t fresh = 1; - while (usedIds.count(fresh)) ++fresh; - newItem["id"] = fresh; - usedIds.insert(fresh); - if (srcId != 0) reIded++; - } else { - usedIds.insert(srcId); - } - dst["items"].push_back(newItem); - copied++; - } - } else { - // Replace mode: destination becomes a verbatim copy of - // the source items array. - dst["items"] = src["items"]; - copied = static_cast(src["items"].size()); - } - std::ofstream out(dstPath); - if (!out) { - std::fprintf(stderr, - "copy-zone-items: failed to write %s\n", dstPath.c_str()); - return 1; - } - out << dst.dump(2); - out.close(); - std::printf("Copied %d item(s) from %s to %s\n", - copied, fromZone.c_str(), toZone.c_str()); - std::printf(" mode : %s\n", - mergeMode ? "merge (append + re-id)" : "replace"); - std::printf(" dst total : %zu\n", dst["items"].size()); - if (reIded > 0) { - std::printf(" re-ided : %d (id collisions)\n", reIded); - } - return 0; - } else if (std::strcmp(argv[i], "--clone-item") == 0 && i + 2 < argc) { - // Duplicate the item at given 0-based index. Auto-assigns - // the smallest unused positive id; optional - // overrides the cloned name (without it the new entry - // gets " (copy)" appended). - std::string zoneDir = argv[++i]; - int idx = -1; - try { idx = std::stoi(argv[++i]); } - catch (...) { - std::fprintf(stderr, - "clone-item: index must be an integer\n"); - return 1; - } - std::string newName; - if (i + 1 < argc && argv[i + 1][0] != '-') newName = argv[++i]; - namespace fs = std::filesystem; - std::string path = zoneDir + "/items.json"; - if (!fs::exists(path)) { - std::fprintf(stderr, - "clone-item: %s has no items.json\n", zoneDir.c_str()); - return 1; - } - nlohmann::json doc; - try { - std::ifstream in(path); - in >> doc; - } catch (...) { - std::fprintf(stderr, - "clone-item: %s is not valid JSON\n", path.c_str()); - return 1; - } - if (!doc.contains("items") || !doc["items"].is_array()) { - std::fprintf(stderr, - "clone-item: %s has no 'items' array\n", path.c_str()); - return 1; - } - auto& items = doc["items"]; - if (idx < 0 || static_cast(idx) >= items.size()) { - std::fprintf(stderr, - "clone-item: index %d out of range (have %zu)\n", - idx, items.size()); - return 1; - } - // Pick the next free id. - std::set used; - for (const auto& it : items) { - if (it.contains("id") && it["id"].is_number_unsigned()) { - used.insert(it["id"].get()); - } - } - uint32_t newId = 1; - while (used.count(newId)) ++newId; - nlohmann::json clone = items[idx]; - clone["id"] = newId; - if (!newName.empty()) { - clone["name"] = newName; - } else { - std::string oldName = clone.value("name", std::string("(unnamed)")); - clone["name"] = oldName + " (copy)"; - } - items.push_back(clone); - std::ofstream out(path); - if (!out) { - std::fprintf(stderr, - "clone-item: failed to write %s\n", path.c_str()); - return 1; - } - out << doc.dump(2); - out.close(); - std::printf("Cloned item idx %d to '%s' (id=%u) in %s (now %zu total)\n", - idx, clone["name"].get().c_str(), - newId, path.c_str(), items.size()); - return 0; } else if (std::strcmp(argv[i], "--scaffold-zone") == 0 && i + 1 < argc) { // Generate a minimal valid empty zone — useful for kickstarting // a new authoring session without needing to launch the GUI.