mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
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:
parent
16d004c742
commit
7d3b80e1f7
10 changed files with 785 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
128
include/pipeline/wowee_creature_patrols.hpp
Normal file
128
include/pipeline/wowee_creature_patrols.hpp
Normal 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
|
||||
375
src/pipeline/wowee_creature_patrols.cpp
Normal file
375
src/pipeline/wowee_creature_patrols.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
251
tools/editor/cli_creature_patrols_catalog.cpp
Normal file
251
tools/editor/cli_creature_patrols_catalog.cpp
Normal 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
|
||||
12
tools/editor/cli_creature_patrols_catalog.hpp
Normal file
12
tools/editor/cli_creature_patrols_catalog.hpp
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue