From 7af7534dc634bfb72f175cf81fb206b6f913e0b1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 08:16:52 -0700 Subject: [PATCH] refactor(editor): extract add-* append handlers into cli_add.cpp Moves the three add-* coordinate-based append handlers (--add-object, --add-creature, --add-item) out of main.cpp into a new cli_add.{hpp,cpp} module. All three append a single entry to a zone's JSON file with optional positional args after the required ones. --add-item handles raw nlohmann::json (no dedicated editor class) and auto-assigns the smallest unused id when the user passes 0 / nothing. main.cpp shrinks by 198 lines (5,986 to 5,788). --- CMakeLists.txt | 1 + tools/editor/cli_add.cpp | 249 +++++++++++++++++++++++++++++++++++++++ tools/editor/cli_add.hpp | 20 ++++ tools/editor/main.cpp | 207 +------------------------------- 4 files changed, 274 insertions(+), 203 deletions(-) create mode 100644 tools/editor/cli_add.cpp create mode 100644 tools/editor/cli_add.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cfc710f8..e7f7a5ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1340,6 +1340,7 @@ add_executable(wowee_editor tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp tools/editor/cli_remove.cpp + tools/editor/cli_add.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_add.cpp b/tools/editor/cli_add.cpp new file mode 100644 index 00000000..d2ecfc6e --- /dev/null +++ b/tools/editor/cli_add.cpp @@ -0,0 +1,249 @@ +#include "cli_add.hpp" + +#include "object_placer.hpp" +#include "npc_spawner.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleAddObject(int& i, int argc, char** argv) { + // Append a single object placement to a zone's objects.json. + // Args: [scale] + std::string zoneDir = argv[++i]; + std::string typeStr = argv[++i]; + std::string gamePath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir)) { + std::fprintf(stderr, "add-object: zone '%s' does not exist\n", + zoneDir.c_str()); + return 1; + } + wowee::editor::PlaceableType ptype; + if (typeStr == "m2") ptype = wowee::editor::PlaceableType::M2; + else if (typeStr == "wmo") ptype = wowee::editor::PlaceableType::WMO; + else { + std::fprintf(stderr, "add-object: type must be 'm2' or 'wmo'\n"); + return 1; + } + glm::vec3 pos; + try { + pos.x = std::stof(argv[++i]); + pos.y = std::stof(argv[++i]); + pos.z = std::stof(argv[++i]); + } catch (const std::exception& e) { + std::fprintf(stderr, "add-object: bad coordinate (%s)\n", e.what()); + return 1; + } + wowee::editor::ObjectPlacer placer; + std::string path = zoneDir + "/objects.json"; + if (fs::exists(path)) placer.loadFromFile(path); + placer.setActivePath(gamePath, ptype); + placer.placeObject(pos); + // Optional scale after coordinates. + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { + float scale = std::stof(argv[++i]); + if (std::isfinite(scale) && scale > 0.0f) { + // Set scale on the just-placed object (last in list). + placer.getObjects().back().scale = scale; + } + } catch (...) {} + } + if (!placer.saveToFile(path)) { + std::fprintf(stderr, "add-object: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Added %s '%s' to %s (now %zu total)\n", + typeStr.c_str(), gamePath.c_str(), path.c_str(), + placer.getObjects().size()); + return 0; +} + +int handleAddCreature(int& i, int argc, char** argv) { + // Append a single creature spawn to a zone's creatures.json. + // Args: [displayId] [level] + // Useful for batch-populating zones via shell script without + // launching the GUI placement tool. + std::string zoneDir = argv[++i]; + std::string name = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir)) { + std::fprintf(stderr, "add-creature: zone '%s' does not exist\n", + zoneDir.c_str()); + return 1; + } + wowee::editor::CreatureSpawn s; + s.name = name; + try { + s.position.x = std::stof(argv[++i]); + s.position.y = std::stof(argv[++i]); + s.position.z = std::stof(argv[++i]); + } catch (const std::exception& e) { + std::fprintf(stderr, "add-creature: bad coordinate (%s)\n", e.what()); + return 1; + } + // Optional displayId (positional, after coordinates). + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { + s.displayId = static_cast(std::stoul(argv[++i])); + } catch (...) { /* leave 0 → SQL exporter substitutes 11707 */ } + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { + s.level = static_cast(std::stoul(argv[++i])); + } catch (...) { /* leave default 1 */ } + } + // Load existing spawns (if any), append, save. + wowee::editor::NpcSpawner spawner; + std::string path = zoneDir + "/creatures.json"; + if (fs::exists(path)) spawner.loadFromFile(path); + spawner.placeCreature(s); + if (!spawner.saveToFile(path)) { + std::fprintf(stderr, "add-creature: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Added creature '%s' to %s (now %zu total)\n", + name.c_str(), path.c_str(), spawner.spawnCount()); + return 0; +} + +int handleAddItem(int& i, int argc, char** argv) { + // Append one item entry to /items.json. Inline + // JSON without a dedicated editor class — items.json is + // a simple {"items": [...]} array of records, and the + // schema is small enough that we don't need NpcSpawner- + // style infrastructure yet. + // + // Schema per item: + // id (uint32) — Item.dbc primary key (auto-increments + // from 1 if omitted) + // name (string) + // quality (uint8) — 0..6 (poor..artifact, default 1) + // displayId (uint32) — ItemDisplayInfo index (default 0) + // itemLevel (uint32) — default 1 + // stackable (uint32) — max stack size (default 1) + std::string zoneDir = argv[++i]; + std::string name = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir)) { + std::fprintf(stderr, + "add-item: zone '%s' does not exist\n", zoneDir.c_str()); + return 1; + } + uint32_t id = 0, displayId = 0, itemLevel = 1; + uint32_t quality = 1; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { id = static_cast(std::stoul(argv[++i])); } + catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { quality = static_cast(std::stoul(argv[++i])); } + catch (...) {} + if (quality > 6) quality = 1; + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { displayId = static_cast(std::stoul(argv[++i])); } + catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { itemLevel = static_cast(std::stoul(argv[++i])); } + catch (...) {} + } + std::string path = zoneDir + "/items.json"; + nlohmann::json doc = nlohmann::json::object({{"items", + nlohmann::json::array()}}); + if (fs::exists(path)) { + std::ifstream in(path); + try { in >> doc; } catch (...) { + std::fprintf(stderr, + "add-item: %s exists but is not valid JSON\n", + path.c_str()); + return 1; + } + if (!doc.contains("items") || !doc["items"].is_array()) { + doc["items"] = nlohmann::json::array(); + } + } + // Auto-assign id if user passed 0 / nothing — pick the + // smallest unused positive integer so the items.json + // numbering stays contiguous. + if (id == 0) { + std::set used; + for (const auto& it : doc["items"]) { + if (it.contains("id") && it["id"].is_number_unsigned()) { + used.insert(it["id"].get()); + } + } + id = 1; + while (used.count(id)) ++id; + } + // Reject duplicate id so the user notices a collision. + for (const auto& it : doc["items"]) { + if (it.contains("id") && it["id"].is_number_unsigned() && + it["id"].get() == id) { + std::fprintf(stderr, + "add-item: id %u already in use in %s\n", + id, path.c_str()); + return 1; + } + } + nlohmann::json item = { + {"id", id}, + {"name", name}, + {"quality", quality}, + {"displayId", displayId}, + {"itemLevel", itemLevel}, + {"stackable", 1}, + }; + doc["items"].push_back(item); + std::ofstream out(path); + if (!out) { + std::fprintf(stderr, + "add-item: failed to write %s\n", path.c_str()); + return 1; + } + out << doc.dump(2); + out.close(); + static const char* qualityNames[] = { + "poor", "common", "uncommon", "rare", "epic", + "legendary", "artifact" + }; + std::printf("Added item '%s' (id=%u, quality=%s, ilvl=%u) to %s (now %zu total)\n", + name.c_str(), id, + qualityNames[quality], itemLevel, + path.c_str(), doc["items"].size()); + return 0; +} + +} // namespace + +bool handleAdd(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--add-object") == 0 && i + 5 < argc) { + outRc = handleAddObject(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--add-creature") == 0 && i + 4 < argc) { + outRc = handleAddCreature(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--add-item") == 0 && i + 2 < argc) { + outRc = handleAddItem(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_add.hpp b/tools/editor/cli_add.hpp new file mode 100644 index 00000000..c8011f3f --- /dev/null +++ b/tools/editor/cli_add.hpp @@ -0,0 +1,20 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the add-* append-by-coords handlers — append a +// single creature spawn / object placement / item record to a +// zone's JSON files. Useful for batch-populating zones via +// shell scripts without launching the GUI placement tool. +// --add-object [scale] +// --add-creature [displayId] [level] +// --add-item [id] [quality] [displayId] [itemLevel] +// +// Returns true if matched; outRc holds the exit code. +bool handleAdd(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 1277b542..294bfcb3 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -41,6 +41,7 @@ #include "cli_quest_reward.hpp" #include "cli_clone.hpp" #include "cli_remove.hpp" +#include "cli_add.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -500,6 +501,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleRemove(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleAdd(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1428,209 +1432,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], "--add-object") == 0 && i + 5 < argc) { - // Append a single object placement to a zone's objects.json. - // Args: [scale] - std::string zoneDir = argv[++i]; - std::string typeStr = argv[++i]; - std::string gamePath = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir)) { - std::fprintf(stderr, "add-object: zone '%s' does not exist\n", - zoneDir.c_str()); - return 1; - } - wowee::editor::PlaceableType ptype; - if (typeStr == "m2") ptype = wowee::editor::PlaceableType::M2; - else if (typeStr == "wmo") ptype = wowee::editor::PlaceableType::WMO; - else { - std::fprintf(stderr, "add-object: type must be 'm2' or 'wmo'\n"); - return 1; - } - glm::vec3 pos; - try { - pos.x = std::stof(argv[++i]); - pos.y = std::stof(argv[++i]); - pos.z = std::stof(argv[++i]); - } catch (const std::exception& e) { - std::fprintf(stderr, "add-object: bad coordinate (%s)\n", e.what()); - return 1; - } - wowee::editor::ObjectPlacer placer; - std::string path = zoneDir + "/objects.json"; - if (fs::exists(path)) placer.loadFromFile(path); - placer.setActivePath(gamePath, ptype); - placer.placeObject(pos); - // Optional scale after coordinates. - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { - float scale = std::stof(argv[++i]); - if (std::isfinite(scale) && scale > 0.0f) { - // Set scale on the just-placed object (last in list). - placer.getObjects().back().scale = scale; - } - } catch (...) {} - } - if (!placer.saveToFile(path)) { - std::fprintf(stderr, "add-object: failed to write %s\n", path.c_str()); - return 1; - } - std::printf("Added %s '%s' to %s (now %zu total)\n", - typeStr.c_str(), gamePath.c_str(), path.c_str(), - placer.getObjects().size()); - return 0; - } else if (std::strcmp(argv[i], "--add-creature") == 0 && i + 4 < argc) { - // Append a single creature spawn to a zone's creatures.json. - // Args: [displayId] [level] - // Useful for batch-populating zones via shell script without - // launching the GUI placement tool. - std::string zoneDir = argv[++i]; - std::string name = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir)) { - std::fprintf(stderr, "add-creature: zone '%s' does not exist\n", - zoneDir.c_str()); - return 1; - } - wowee::editor::CreatureSpawn s; - s.name = name; - try { - s.position.x = std::stof(argv[++i]); - s.position.y = std::stof(argv[++i]); - s.position.z = std::stof(argv[++i]); - } catch (const std::exception& e) { - std::fprintf(stderr, "add-creature: bad coordinate (%s)\n", e.what()); - return 1; - } - // Optional displayId (positional, after coordinates). - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { - s.displayId = static_cast(std::stoul(argv[++i])); - } catch (...) { /* leave 0 → SQL exporter substitutes 11707 */ } - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { - s.level = static_cast(std::stoul(argv[++i])); - } catch (...) { /* leave default 1 */ } - } - // Load existing spawns (if any), append, save. - wowee::editor::NpcSpawner spawner; - std::string path = zoneDir + "/creatures.json"; - if (fs::exists(path)) spawner.loadFromFile(path); - spawner.placeCreature(s); - if (!spawner.saveToFile(path)) { - std::fprintf(stderr, "add-creature: failed to write %s\n", path.c_str()); - return 1; - } - std::printf("Added creature '%s' to %s (now %zu total)\n", - name.c_str(), path.c_str(), spawner.spawnCount()); - return 0; - } else if (std::strcmp(argv[i], "--add-item") == 0 && i + 2 < argc) { - // Append one item entry to /items.json. Inline - // JSON without a dedicated editor class — items.json is - // a simple {"items": [...]} array of records, and the - // schema is small enough that we don't need NpcSpawner- - // style infrastructure yet. - // - // Schema per item: - // id (uint32) — Item.dbc primary key (auto-increments - // from 1 if omitted) - // name (string) - // quality (uint8) — 0..6 (poor..artifact, default 1) - // displayId (uint32) — ItemDisplayInfo index (default 0) - // itemLevel (uint32) — default 1 - // stackable (uint32) — max stack size (default 1) - std::string zoneDir = argv[++i]; - std::string name = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir)) { - std::fprintf(stderr, - "add-item: zone '%s' does not exist\n", zoneDir.c_str()); - return 1; - } - uint32_t id = 0, displayId = 0, itemLevel = 1; - uint32_t quality = 1; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { id = static_cast(std::stoul(argv[++i])); } - catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { quality = static_cast(std::stoul(argv[++i])); } - catch (...) {} - if (quality > 6) quality = 1; - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { displayId = static_cast(std::stoul(argv[++i])); } - catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { itemLevel = static_cast(std::stoul(argv[++i])); } - catch (...) {} - } - std::string path = zoneDir + "/items.json"; - nlohmann::json doc = nlohmann::json::object({{"items", - nlohmann::json::array()}}); - if (fs::exists(path)) { - std::ifstream in(path); - try { in >> doc; } catch (...) { - std::fprintf(stderr, - "add-item: %s exists but is not valid JSON\n", - path.c_str()); - return 1; - } - if (!doc.contains("items") || !doc["items"].is_array()) { - doc["items"] = nlohmann::json::array(); - } - } - // Auto-assign id if user passed 0 / nothing — pick the - // smallest unused positive integer so the items.json - // numbering stays contiguous. - if (id == 0) { - std::set used; - for (const auto& it : doc["items"]) { - if (it.contains("id") && it["id"].is_number_unsigned()) { - used.insert(it["id"].get()); - } - } - id = 1; - while (used.count(id)) ++id; - } - // Reject duplicate id so the user notices a collision. - for (const auto& it : doc["items"]) { - if (it.contains("id") && it["id"].is_number_unsigned() && - it["id"].get() == id) { - std::fprintf(stderr, - "add-item: id %u already in use in %s\n", - id, path.c_str()); - return 1; - } - } - nlohmann::json item = { - {"id", id}, - {"name", name}, - {"quality", quality}, - {"displayId", displayId}, - {"itemLevel", itemLevel}, - {"stackable", 1}, - }; - doc["items"].push_back(item); - std::ofstream out(path); - if (!out) { - std::fprintf(stderr, - "add-item: failed to write %s\n", path.c_str()); - return 1; - } - out << doc.dump(2); - out.close(); - static const char* qualityNames[] = { - "poor", "common", "uncommon", "rare", "epic", - "legendary", "artifact" - }; - std::printf("Added item '%s' (id=%u, quality=%s, ilvl=%u) to %s (now %zu total)\n", - name.c_str(), id, - qualityNames[quality], itemLevel, - path.c_str(), doc["items"].size()); - return 0; } else if (std::strcmp(argv[i], "--random-populate-zone") == 0 && i + 1 < argc) { // Randomly add creatures and/or objects to a zone for // playtest scenarios. Reads the zone manifest's tile