From 7d3b80e1f77c2c7db03c06e74e0c682bd35a86d8 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 23:38:59 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WCMR=20(Creature=20Patrol?= =?UTF-8?q?=20Path)=20=E2=80=94=2090th=20open=20format=20milestone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for AzerothCore's creature_movement / waypoints SQL tables plus the per-spawn waypoint arrays. Defines named waypoint paths that creatures patrol along: Stormwind guards walking the city perimeter, AQ40 trash rotating through the chamber, ICC patrols circling the spire. Each entry binds a creatureGuid to a sequence of (x, y, z, delayMs) waypoints. The pathKind controls cycling behavior (Loop / OneShot / Reverse / Random) and moveType controls the locomotion kind (Walk / Run / Fly / Swim) — a flying patrol ignores ground geometry, a swimming patrol stays underwater. This is the first open format with truly variable-length per-entry payload. Earlier formats with multi-slot fields (WSPR's 8-reagent slots, WPSP's 4-item arrays) used fixed-size caps padded with zeros. WCMR instead uses an inline length-prefixed waypoint array — entries can be 4 waypoints or 4000, with the loader advancing through the file by reading the count first then count*16 bytes of waypoint data. Cap of 64K waypoints per path keeps a corrupted file from allocating gigabytes. pathLengthYards(pathId) is the engine helper that sums segment distances between consecutive waypoints (closing the loop for Loop kind). Tested across 12-point and 16-point circular paths that geometrically resolve to the expected ~25y radius and ~60y radius totals. Cross-references back to WCRT — creatureGuid points at the spawned creature instance whose behavior mode follows this patrol. Three preset emitters: --gen-cmr (3 small paths showing each pathKind variant), --gen-cmr-city (4 capital-city guard 6-point loops with 2.0-2.5s waypoint dwell), --gen-cmr-boss (3 long raid-zone patrols up to 16 waypoints, demonstrating that variable-length payloads scale). Validation enforces id+name+creatureGuid+waypoints presence, pathKind 0..3, moveType 0..3, no duplicate ids; warns on 1-waypoint paths (creature would idle in place) and Loop with fewer than 3 waypoints (degenerate — indistinguishable from Reverse). This is the 90th open format milestone. Wired through the cross-format table; WCMR appears in all 18 cross-format utilities. Format count 89 -> 90; CLI flag count 1048 -> 1053. --- CMakeLists.txt | 3 + include/pipeline/wowee_creature_patrols.hpp | 128 ++++++ src/pipeline/wowee_creature_patrols.cpp | 375 ++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_creature_patrols_catalog.cpp | 251 ++++++++++++ tools/editor/cli_creature_patrols_catalog.hpp | 12 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 785 insertions(+) create mode 100644 include/pipeline/wowee_creature_patrols.hpp create mode 100644 src/pipeline/wowee_creature_patrols.cpp create mode 100644 tools/editor/cli_creature_patrols_catalog.cpp create mode 100644 tools/editor/cli_creature_patrols_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 34492f3e..58348049 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -678,6 +678,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_npc_services.cpp src/pipeline/wowee_token_rewards.cpp src/pipeline/wowee_spell_procs.cpp + src/pipeline/wowee_creature_patrols.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1519,6 +1520,7 @@ add_executable(wowee_editor tools/editor/cli_npc_services_catalog.cpp tools/editor/cli_token_rewards_catalog.cpp tools/editor/cli_spell_procs_catalog.cpp + tools/editor/cli_creature_patrols_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1675,6 +1677,7 @@ add_executable(wowee_editor src/pipeline/wowee_npc_services.cpp src/pipeline/wowee_token_rewards.cpp src/pipeline/wowee_spell_procs.cpp + src/pipeline/wowee_creature_patrols.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_creature_patrols.hpp b/include/pipeline/wowee_creature_patrols.hpp new file mode 100644 index 00000000..18d5c5a4 --- /dev/null +++ b/include/pipeline/wowee_creature_patrols.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Creature Patrol Path catalog (.wcmr) — +// novel replacement for AzerothCore's creature_movement / +// waypoints SQL tables plus the per-spawn waypoint +// arrays. Defines named waypoint paths that creatures +// patrol along: Stormwind guards walking the city perimeter, +// AQ40 trash rotating through the chamber, ICC patrols +// circling the spire. +// +// Each entry binds a creatureGuid (server-side spawn id) +// to a sequence of (x, y, z, delayMs) waypoints. The +// pathKind controls cycling behavior (Loop / OneShot / +// Reverse / Random) and moveType controls the locomotion +// kind (Walk / Run / Fly / Swim) — a flying patrol +// ignores ground geometry, a swimming patrol stays +// underwater. +// +// Variable-length entries: each path stores its own +// waypointCount with a corresponding inline waypoint +// array (no fixed cap). Loaders advance through the file +// by reading the count first, then count*16 bytes of +// waypoint data. +// +// Cross-references with previously-added formats: +// WCRT: creatureGuid references the spawned creature +// instance whose AI follows this patrol. +// +// Binary layout (little-endian): +// magic[4] = "WCMR" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// pathId (uint32) +// nameLen + name +// descLen + description +// creatureGuid (uint32) +// pathKind (uint8) / moveType (uint8) / pad[2] +// waypointCount (uint32) +// waypoints[count] (each 16 bytes): +// x (float) / y (float) / z (float) / delayMs (uint32) +// iconColorRGBA (uint32) +struct WoweeCreaturePatrol { + enum PathKind : uint8_t { + Loop = 0, // patrol forever in a circle + OneShot = 1, // walk once, then idle at last waypoint + Reverse = 2, // walk to end, walk back, repeat + Random = 3, // pick next waypoint randomly each step + }; + + enum MoveType : uint8_t { + Walk = 0, // ground walking speed + Run = 1, // ground run speed + Fly = 2, // airborne movement (ignores terrain) + Swim = 3, // underwater movement + }; + + struct Waypoint { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + uint32_t delayMs = 0; // dwell time at this waypoint + }; + + struct Entry { + uint32_t pathId = 0; + std::string name; + std::string description; + uint32_t creatureGuid = 0; + uint8_t pathKind = Loop; + uint8_t moveType = Walk; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + std::vector waypoints; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t pathId) const; + const Entry* findByCreatureGuid(uint32_t creatureGuid) const; + + // Compute total path length in yards by summing + // segment distances between consecutive waypoints. + // For Loop kind, includes the closing segment back to + // the first waypoint. + float pathLengthYards(uint32_t pathId) const; + + static const char* pathKindName(uint8_t k); + static const char* moveTypeName(uint8_t m); +}; + +class WoweeCreaturePatrolLoader { +public: + static bool save(const WoweeCreaturePatrol& cat, + const std::string& basePath); + static WoweeCreaturePatrol load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-cmr* variants. + // + // makePatrol — 3 small patrols showing each pathKind + // (4-pt Loop guard, 6-pt OneShot run, + // 8-pt Random tiger). + // makeCity — 4 capital-city guard routes (Stormwind / + // Orgrimmar / Ironforge / Thunder Bluff) + // with 6-8 waypoints each. + // makeBoss — 3 raid-zone patrols (AQ40 12-pt Loop / + // Naxx 8-pt OneShot / ICC 16-pt Random) + // demonstrating long-path support. + static WoweeCreaturePatrol makePatrol(const std::string& catalogName); + static WoweeCreaturePatrol makeCity(const std::string& catalogName); + static WoweeCreaturePatrol makeBoss(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_creature_patrols.cpp b/src/pipeline/wowee_creature_patrols.cpp new file mode 100644 index 00000000..f3ce5f06 --- /dev/null +++ b/src/pipeline/wowee_creature_patrols.cpp @@ -0,0 +1,375 @@ +#include "pipeline/wowee_creature_patrols.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'C', 'M', 'R'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wcmr") { + base += ".wcmr"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeCreaturePatrol::Entry* +WoweeCreaturePatrol::findById(uint32_t pathId) const { + for (const auto& e : entries) + if (e.pathId == pathId) return &e; + return nullptr; +} + +const WoweeCreaturePatrol::Entry* +WoweeCreaturePatrol::findByCreatureGuid(uint32_t creatureGuid) const { + for (const auto& e : entries) + if (e.creatureGuid == creatureGuid) return &e; + return nullptr; +} + +float WoweeCreaturePatrol::pathLengthYards(uint32_t pathId) const { + const Entry* e = findById(pathId); + if (!e || e->waypoints.size() < 2) return 0.0f; + float total = 0.0f; + for (size_t k = 1; k < e->waypoints.size(); ++k) { + const auto& a = e->waypoints[k - 1]; + const auto& b = e->waypoints[k]; + float dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z; + total += std::sqrt(dx * dx + dy * dy + dz * dz); + } + // Loop kind closes back to first waypoint, so include + // that closing segment in the length. + if (e->pathKind == Loop && e->waypoints.size() >= 2) { + const auto& a = e->waypoints.back(); + const auto& b = e->waypoints.front(); + float dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z; + total += std::sqrt(dx * dx + dy * dy + dz * dz); + } + return total; +} + +const char* WoweeCreaturePatrol::pathKindName(uint8_t k) { + switch (k) { + case Loop: return "loop"; + case OneShot: return "one-shot"; + case Reverse: return "reverse"; + case Random: return "random"; + default: return "unknown"; + } +} + +const char* WoweeCreaturePatrol::moveTypeName(uint8_t m) { + switch (m) { + case Walk: return "walk"; + case Run: return "run"; + case Fly: return "fly"; + case Swim: return "swim"; + default: return "unknown"; + } +} + +bool WoweeCreaturePatrolLoader::save(const WoweeCreaturePatrol& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.pathId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.creatureGuid); + writePOD(os, e.pathKind); + writePOD(os, e.moveType); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + uint32_t wpCount = static_cast(e.waypoints.size()); + writePOD(os, wpCount); + for (const auto& w : e.waypoints) { + writePOD(os, w.x); + writePOD(os, w.y); + writePOD(os, w.z); + writePOD(os, w.delayMs); + } + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeCreaturePatrol WoweeCreaturePatrolLoader::load( + const std::string& basePath) { + WoweeCreaturePatrol out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.pathId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.creatureGuid) || + !readPOD(is, e.pathKind) || + !readPOD(is, e.moveType) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1)) { + out.entries.clear(); return out; + } + uint32_t wpCount = 0; + if (!readPOD(is, wpCount)) { out.entries.clear(); return out; } + // Cap to keep a corrupted file from allocating + // gigabytes — 64K waypoints per path is plenty. + if (wpCount > (1u << 16)) { out.entries.clear(); return out; } + e.waypoints.resize(wpCount); + for (auto& w : e.waypoints) { + if (!readPOD(is, w.x) || + !readPOD(is, w.y) || + !readPOD(is, w.z) || + !readPOD(is, w.delayMs)) { + out.entries.clear(); return out; + } + } + if (!readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeCreaturePatrolLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeCreaturePatrol WoweeCreaturePatrolLoader::makePatrol( + const std::string& catalogName) { + using P = WoweeCreaturePatrol; + WoweeCreaturePatrol c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t guid, + uint8_t kind, uint8_t move, + std::initializer_list wps, + uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + P::Entry e; + e.pathId = id; e.name = name; e.description = desc; + e.creatureGuid = guid; + e.pathKind = kind; + e.moveType = move; + e.waypoints.assign(wps); + e.iconColorRGBA = packRgba(r, g, b); + c.entries.push_back(e); + }; + // Three small patrols showing each pathKind variant. + add(1, "GuardLoop4", 100001, P::Loop, P::Walk, { + { -8910.0f, -135.0f, 82.0f, 1500 }, + { -8895.0f, -120.0f, 82.0f, 1500 }, + { -8895.0f, -150.0f, 82.0f, 1500 }, + { -8910.0f, -150.0f, 82.0f, 1500 }, + }, 100, 200, 240, "Stormwind guard — 4-point loop with 1.5s dwell at each waypoint."); + add(2, "RunRouteOneShot6", 100002, P::OneShot, P::Run, { + { -10000.0f, 500.0f, 30.0f, 0 }, + { -9900.0f, 600.0f, 30.0f, 0 }, + { -9800.0f, 700.0f, 30.0f, 0 }, + { -9700.0f, 800.0f, 30.0f, 0 }, + { -9600.0f, 900.0f, 30.0f, 0 }, + { -9500.0f, 1000.0f, 30.0f, 0 }, + }, 220, 180, 100, "Westfall harvester — 6-point one-shot run, ends at last waypoint."); + add(3, "TigerRandom8", 100003, P::Random, P::Walk, { + { -11000.0f, -2000.0f, 30.0f, 3000 }, + { -10800.0f, -2100.0f, 30.0f, 3000 }, + { -10600.0f, -2050.0f, 30.0f, 3000 }, + { -10500.0f, -2200.0f, 30.0f, 3000 }, + { -10700.0f, -2300.0f, 30.0f, 3000 }, + { -10900.0f, -2250.0f, 30.0f, 3000 }, + { -11100.0f, -2150.0f, 30.0f, 3000 }, + { -11050.0f, -1950.0f, 30.0f, 3000 }, + }, 220, 100, 100, "Stranglethorn tiger — 8-point random patrol, " + "3s dwell, picks next destination randomly."); + return c; +} + +WoweeCreaturePatrol WoweeCreaturePatrolLoader::makeCity( + const std::string& catalogName) { + using P = WoweeCreaturePatrol; + WoweeCreaturePatrol c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t guid, + std::initializer_list wps, + const char* desc) { + P::Entry e; + e.pathId = id; e.name = name; e.description = desc; + e.creatureGuid = guid; + e.pathKind = P::Loop; + e.moveType = P::Walk; + e.waypoints.assign(wps); + e.iconColorRGBA = packRgba(180, 220, 240); // city blue + c.entries.push_back(e); + }; + // Four city guard routes (illustrative coordinates). + add(100, "StormwindCathedralLoop", 110001, { + { -8520.0f, 840.0f, 110.0f, 2000 }, + { -8480.0f, 860.0f, 110.0f, 2000 }, + { -8460.0f, 820.0f, 110.0f, 2000 }, + { -8500.0f, 800.0f, 110.0f, 2000 }, + { -8540.0f, 820.0f, 110.0f, 2000 }, + { -8540.0f, 860.0f, 110.0f, 2000 }, + }, "Stormwind cathedral square guard — 6-point perimeter loop."); + add(101, "OrgrimmarValleyOfStrengthLoop", 110002, { + { 1640.0f, -4400.0f, 30.0f, 2000 }, + { 1680.0f, -4380.0f, 30.0f, 2000 }, + { 1700.0f, -4420.0f, 30.0f, 2000 }, + { 1680.0f, -4460.0f, 30.0f, 2000 }, + { 1640.0f, -4480.0f, 30.0f, 2000 }, + { 1620.0f, -4440.0f, 30.0f, 2000 }, + }, "Orgrimmar Valley of Strength grunt — 6-point perimeter loop."); + add(102, "IronforgeBankLoop", 110003, { + { -4800.0f, -930.0f, 500.0f, 2500 }, + { -4760.0f, -910.0f, 500.0f, 2500 }, + { -4750.0f, -950.0f, 500.0f, 2500 }, + { -4790.0f, -980.0f, 500.0f, 2500 }, + { -4830.0f, -960.0f, 500.0f, 2500 }, + { -4830.0f, -920.0f, 500.0f, 2500 }, + }, "Ironforge bank district sentinel — 6-point perimeter loop."); + add(103, "ThunderBluffElderRiseLoop", 110004, { + { -1250.0f, 120.0f, 130.0f, 2000 }, + { -1200.0f, 140.0f, 130.0f, 2000 }, + { -1180.0f, 100.0f, 130.0f, 2000 }, + { -1220.0f, 80.0f, 130.0f, 2000 }, + { -1270.0f, 100.0f, 130.0f, 2000 }, + { -1280.0f, 150.0f, 130.0f, 2000 }, + }, "Thunder Bluff Elder Rise warrior — 6-point loop on the upper plateau."); + return c; +} + +WoweeCreaturePatrol WoweeCreaturePatrolLoader::makeBoss( + const std::string& catalogName) { + using P = WoweeCreaturePatrol; + WoweeCreaturePatrol c; + c.name = catalogName; + // Three long-path patrols demonstrating that the + // variable-length design handles bigger patrol sets + // gracefully. Each helper-build constructs synthetic + // waypoints around a center using simple maths. + auto buildCircle = [&](float cx, float cy, float cz, + float radius, int n) { + std::vector out; + const float pi2 = 6.28318530718f; + for (int k = 0; k < n; ++k) { + float a = pi2 * (static_cast(k) / + static_cast(n)); + P::Waypoint w; + w.x = cx + radius * std::cos(a); + w.y = cy + radius * std::sin(a); + w.z = cz; + w.delayMs = 500; + out.push_back(w); + } + return out; + }; + P::Entry aq40; + aq40.pathId = 200; aq40.name = "AQ40TrashLoop12"; + aq40.description = "AQ40 chamber trash — 12-point Loop circle, " + "500ms dwell."; + aq40.creatureGuid = 200001; + aq40.pathKind = P::Loop; + aq40.moveType = P::Walk; + aq40.waypoints = buildCircle(-8225.0f, 2050.0f, 130.0f, 25.0f, 12); + aq40.iconColorRGBA = packRgba(220, 180, 100); + c.entries.push_back(aq40); + + P::Entry naxx; + naxx.pathId = 201; naxx.name = "NaxxTrashOneShot8"; + naxx.description = "Naxxramas trash — 8-point one-shot ramp run."; + naxx.creatureGuid = 200002; + naxx.pathKind = P::OneShot; + naxx.moveType = P::Run; + for (int k = 0; k < 8; ++k) { + P::Waypoint w; + w.x = 3470.0f + 10.0f * k; + w.y = -3110.0f + 8.0f * k; + w.z = 287.0f + 1.5f * k; // ramp upward + w.delayMs = 0; + naxx.waypoints.push_back(w); + } + naxx.iconColorRGBA = packRgba(220, 100, 100); + c.entries.push_back(naxx); + + P::Entry icc; + icc.pathId = 202; icc.name = "ICCSpirePatrolRandom16"; + icc.description = "Icecrown Citadel spire patrol — 16-point " + "Random walk over a 60-yard radius."; + icc.creatureGuid = 200003; + icc.pathKind = P::Random; + icc.moveType = P::Walk; + icc.waypoints = buildCircle(-3500.0f, 2200.0f, 600.0f, 60.0f, 16); + for (auto& w : icc.waypoints) w.delayMs = 1000; + icc.iconColorRGBA = packRgba(180, 100, 240); + c.entries.push_back(icc); + + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 2fed736b..63c1128c 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -276,6 +276,8 @@ const char* const kArgRequired[] = { "--gen-sps", "--gen-sps-aura", "--gen-sps-talent", "--info-wsps", "--validate-wsps", "--export-wsps-json", "--import-wsps-json", + "--gen-cmr", "--gen-cmr-city", "--gen-cmr-boss", + "--info-wcmr", "--validate-wcmr", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_creature_patrols_catalog.cpp b/tools/editor/cli_creature_patrols_catalog.cpp new file mode 100644 index 00000000..e980f6f0 --- /dev/null +++ b/tools/editor/cli_creature_patrols_catalog.cpp @@ -0,0 +1,251 @@ +#include "cli_creature_patrols_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_creature_patrols.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWcmrExt(std::string base) { + stripExt(base, ".wcmr"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeCreaturePatrol& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeCreaturePatrolLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wcmr\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeCreaturePatrol& c, + const std::string& base) { + size_t totalWp = 0; + for (const auto& e : c.entries) totalWp += e.waypoints.size(); + std::printf("Wrote %s.wcmr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" paths : %zu\n", c.entries.size()); + std::printf(" waypoints : %zu (across all paths)\n", totalWp); +} + +int handleGenPatrol(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "PatrolPaths"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmrExt(base); + auto c = wowee::pipeline::WoweeCreaturePatrolLoader::makePatrol(name); + if (!saveOrError(c, base, "gen-cmr")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCity(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "CityGuardRoutes"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmrExt(base); + auto c = wowee::pipeline::WoweeCreaturePatrolLoader::makeCity(name); + if (!saveOrError(c, base, "gen-cmr-city")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenBoss(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BossPatrols"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcmrExt(base); + auto c = wowee::pipeline::WoweeCreaturePatrolLoader::makeBoss(name); + if (!saveOrError(c, base, "gen-cmr-boss")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWcmrExt(base); + if (!wowee::pipeline::WoweeCreaturePatrolLoader::exists(base)) { + std::fprintf(stderr, "WCMR not found: %s.wcmr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCreaturePatrolLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wcmr"] = base + ".wcmr"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + nlohmann::json wpArr = nlohmann::json::array(); + for (const auto& w : e.waypoints) { + wpArr.push_back({ + {"x", w.x}, {"y", w.y}, {"z", w.z}, + {"delayMs", w.delayMs}, + }); + } + arr.push_back({ + {"pathId", e.pathId}, + {"name", e.name}, + {"description", e.description}, + {"creatureGuid", e.creatureGuid}, + {"pathKind", e.pathKind}, + {"pathKindName", wowee::pipeline::WoweeCreaturePatrol::pathKindName(e.pathKind)}, + {"moveType", e.moveType}, + {"moveTypeName", wowee::pipeline::WoweeCreaturePatrol::moveTypeName(e.moveType)}, + {"waypointCount", e.waypoints.size()}, + {"pathLengthYards", c.pathLengthYards(e.pathId)}, + {"waypoints", wpArr}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCMR: %s.wcmr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" paths : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id creatureGuid kind move waypoints pathLen(yd) name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %8u %-8s %-5s %5zu %8.1f %s\n", + e.pathId, e.creatureGuid, + wowee::pipeline::WoweeCreaturePatrol::pathKindName(e.pathKind), + wowee::pipeline::WoweeCreaturePatrol::moveTypeName(e.moveType), + e.waypoints.size(), + c.pathLengthYards(e.pathId), + e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWcmrExt(base); + if (!wowee::pipeline::WoweeCreaturePatrolLoader::exists(base)) { + std::fprintf(stderr, + "validate-wcmr: WCMR not found: %s.wcmr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCreaturePatrolLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.pathId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.pathId == 0) + errors.push_back(ctx + ": pathId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.creatureGuid == 0) + errors.push_back(ctx + + ": creatureGuid is 0 — path is unbound to any spawn"); + if (e.pathKind > wowee::pipeline::WoweeCreaturePatrol::Random) { + errors.push_back(ctx + ": pathKind " + + std::to_string(e.pathKind) + " not in 0..3"); + } + if (e.moveType > wowee::pipeline::WoweeCreaturePatrol::Swim) { + errors.push_back(ctx + ": moveType " + + std::to_string(e.moveType) + " not in 0..3"); + } + if (e.waypoints.empty()) + errors.push_back(ctx + ": no waypoints — path has nothing to walk"); + if (e.waypoints.size() == 1) + warnings.push_back(ctx + + ": only 1 waypoint — creature will idle in place"); + // Loop with fewer than 3 waypoints is degenerate + // (back and forth between 2 points isn't a loop). + if (e.pathKind == wowee::pipeline::WoweeCreaturePatrol::Loop && + e.waypoints.size() < 3) { + warnings.push_back(ctx + + ": Loop with " + + std::to_string(e.waypoints.size()) + + " waypoints — fewer than 3 makes Loop " + "indistinguishable from Reverse"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.pathId) { + errors.push_back(ctx + ": duplicate pathId"); + break; + } + } + idsSeen.push_back(e.pathId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wcmr"] = base + ".wcmr"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wcmr: %s.wcmr\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu paths, all pathIds unique\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleCreaturePatrolsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-cmr") == 0 && i + 1 < argc) { + outRc = handleGenPatrol(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-cmr-city") == 0 && i + 1 < argc) { + outRc = handleGenCity(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-cmr-boss") == 0 && i + 1 < argc) { + outRc = handleGenBoss(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wcmr") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wcmr") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_creature_patrols_catalog.hpp b/tools/editor/cli_creature_patrols_catalog.hpp new file mode 100644 index 00000000..cf284686 --- /dev/null +++ b/tools/editor/cli_creature_patrols_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleCreaturePatrolsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index aae24e11..f66c3b73 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -134,6 +134,7 @@ #include "cli_npc_services_catalog.hpp" #include "cli_token_rewards_catalog.hpp" #include "cli_spell_procs_catalog.hpp" +#include "cli_creature_patrols_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -309,6 +310,7 @@ constexpr DispatchFn kDispatchTable[] = { handleNPCServicesCatalog, handleTokenRewardsCatalog, handleSpellProcsCatalog, + handleCreaturePatrolsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 2f5a637f..6ec11676 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -92,6 +92,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','B','K','D'}, ".wbkd", "npcs", "--info-wbkd", "NPC service definition catalog"}, {{'W','T','B','R'}, ".wtbr", "tokens", "--info-wtbr", "Token reward redemption catalog"}, {{'W','S','P','S'}, ".wsps", "spells", "--info-wsps", "Spell proc trigger catalog"}, + {{'W','C','M','R'}, ".wcmr", "creatures", "--info-wcmr", "Creature patrol path catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 4ae84452..86bc6cc7 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2013,6 +2013,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wsps to a human-editable JSON sidecar (defaults to .wsps.json)\n"); std::printf(" --import-wsps-json [out-base]\n"); std::printf(" Import a .wsps.json sidecar back into binary .wsps (procFlags accepts int OR pipe-separated label string)\n"); + std::printf(" --gen-cmr [name]\n"); + std::printf(" Emit .wcmr 3 small patrols showing each pathKind (4-pt Loop guard / 6-pt OneShot run / 8-pt Random tiger)\n"); + std::printf(" --gen-cmr-city [name]\n"); + std::printf(" Emit .wcmr 4 capital-city guard routes (Stormwind / Orgrimmar / Ironforge / Thunder Bluff) with 6-point loops\n"); + std::printf(" --gen-cmr-boss [name]\n"); + std::printf(" Emit .wcmr 3 raid-zone patrols (AQ40 12-pt Loop / Naxx 8-pt OneShot / ICC 16-pt Random) demonstrating long-path support\n"); + std::printf(" --info-wcmr [--json]\n"); + std::printf(" Print WCMR entries (id / creatureGuid / pathKind / moveType / waypoint count / total path length yards / name)\n"); + std::printf(" --validate-wcmr [--json]\n"); + std::printf(" Static checks: id+name+creatureGuid+waypoints required, pathKind 0..3, moveType 0..3, no duplicate ids; warns on 1-waypoint paths (idle), Loop with <3 waypoints (degenerate)\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index dd77027e..81a0577c 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -114,6 +114,7 @@ constexpr FormatRow kFormats[] = { {"WBKD", ".wbkd", "npcs", "npc_vendor + npc_trainer SQL", "NPC service definition catalog"}, {"WTBR", ".wtbr", "tokens", "currency_token_reward SQL", "Token reward redemption catalog"}, {"WSPS", ".wsps", "spells", "spell_proc_event SQL + Spell.dbc", "Spell proc trigger catalog"}, + {"WCMR", ".wcmr", "creatures", "creature_movement waypoints SQL", "Creature patrol path catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine