From 83d20180c334ec6f7a1795dca4c48c1e9a8c1bef Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 11:35:07 -0700 Subject: [PATCH] feat(editor): add --add-creature CLI for headless creature placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appends a single creature spawn to a zone's creatures.json. First real authoring tool that doesn't need the GUI placement system — useful for batch-populating zones via shell script: for npc in goblin spider wolf; do wowee_editor --add-creature "$zone" "$npc" 100 200 50 done Args: [displayId] [level] - displayId 0 → SQL exporter substitutes 11707 (generic humanoid) - level defaults to 1 - Coordinates are render-space (renderX=wowY, renderY=wowX) Loads any existing creatures.json first then appends, so multiple invocations build up the file. The standard NPC spawner caps (50k creatures) protect against runaway scripts. --- tools/editor/main.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 1dab0b43..2e4bf14b 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -32,6 +32,8 @@ static void printUsage(const char* argv0) { std::printf(" --convert-wmo Convert WMO building to WOB open format (no GUI)\n"); std::printf(" --list-zones [--json] List discovered custom zones and exit\n"); std::printf(" --scaffold-zone [tx ty] Create a blank zone in custom_zones// and exit\n"); + std::printf(" --add-creature [displayId] [level]\n"); + std::printf(" Append one creature spawn to /creatures.json and exit\n"); std::printf(" --build-woc Generate a WOC collision mesh from WHM/WOT and exit\n"); std::printf(" --regen-collision Rebuild every WOC under a zone dir and exit\n"); std::printf(" --fix-zone Re-parse + re-save zone JSONs to apply latest scrubs/caps and exit\n"); @@ -83,7 +85,8 @@ int main(int argc, char* argv[]) { "--info-extract", "--info-zone", "--info-wcp", "--list-wcp", "--unpack-wcp", "--pack-wcp", "--validate", "--zone-summary", - "--scaffold-zone", "--build-woc", "--regen-collision", "--fix-zone", + "--scaffold-zone", "--add-creature", + "--build-woc", "--regen-collision", "--fix-zone", "--export-png", "--convert-m2", "--convert-wmo", }; @@ -102,6 +105,11 @@ int main(int argc, char* argv[]) { std::fprintf(stderr, "--diff-wcp requires two paths\n"); return 1; } + if (std::strcmp(argv[i], "--add-creature") == 0 && i + 5 >= argc) { + std::fprintf(stderr, + "--add-creature requires \n"); + return 1; + } } for (int i = 1; i < argc; i++) { @@ -1093,6 +1101,52 @@ 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-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], "--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.