feat(editor): add WCMR (Creature Patrol Path) — 90th open format milestone

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.
This commit is contained in:
Kelsi 2026-05-09 23:38:59 -07:00
parent 16d004c742
commit 7d3b80e1f7
10 changed files with 785 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,128 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
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<Waypoint> waypoints;
uint32_t iconColorRGBA = 0xFFFFFFFFu;
};
std::string name;
std::vector<Entry> 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

View file

@ -0,0 +1,375 @@
#include "pipeline/wowee_creature_patrols.hpp"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'C', 'M', 'R'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
void writeStr(std::ofstream& os, const std::string& s) {
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(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<uint32_t>(a) << 24) |
(static_cast<uint32_t>(b) << 16) |
(static_cast<uint32_t>(g) << 8) |
static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<P::Waypoint> 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<P::Waypoint> 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<P::Waypoint> out;
const float pi2 = 6.28318530718f;
for (int k = 0; k < n; ++k) {
float a = pi2 * (static_cast<float>(k) /
static_cast<float>(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

View file

@ -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",

View file

@ -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 <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
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<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::vector<uint32_t> 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

View file

@ -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

View file

@ -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,

View file

@ -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"},

View file

@ -2013,6 +2013,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wsps to a human-editable JSON sidecar (defaults to <base>.wsps.json)\n");
std::printf(" --import-wsps-json <json-path> [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 <wcmr-base> [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 <wcmr-base> [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 <wcmr-base> [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 <wcmr-base> [--json]\n");
std::printf(" Print WCMR entries (id / creatureGuid / pathKind / moveType / waypoint count / total path length yards / name)\n");
std::printf(" --validate-wcmr <wcmr-base> [--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 <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -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