feat(editor): add WSCD (Spell Cooldown Category) open catalog format

Open replacement for SpellCooldown.dbc plus the per-spell
category-cooldown fields in Spell.dbc. Defines the shared-cooldown
buckets that related spells reference: casting one spell triggers
a cooldown on every other spell in the same bucket. Mage Polymorph
variants (Sheep / Pig / Turtle / Cat) all share one bucket so
morphing a target locks all variants at once. Healing potions and
mana potions share the SharedWithItems bucket so consuming one
locks the other.

Distinct from WSDR (which times how long an aura stays on a
target) — WSCD times how long before a spell can be cast again.
The global cooldown (GCD) is itself just one bucket of this kind,
flagged with OnGCDStart so the engine triggers it at cast start
rather than cast finish.

Three preset emitters: --gen-cdb (4 baseline buckets including
GCD), --gen-cdb-class (5 mage-specific class cooldowns including
the Polymorph family), --gen-cdb-items (5 item cooldowns
including the heal/mana potion shared bucket and the 60min
Hearthstone family). Validation enforces id+name presence,
bucketKind 0..4, no duplicate ids, and warns on Global without
OnGCDStart (engine wouldn't trigger on cast start) and Spell
kind with SharedWithItems (contradictory).

categoryFlags is a bitfield (AffectedByHaste / SharedWithItems /
OnGCDStart / IgnoresCooldownReduction); --info-wscd decodes the
bits to label list. Wired through the cross-format table; WSCD
appears automatically in all 9 cross-format utilities. Format
count 70 -> 71; CLI flag count 907 -> 912.
This commit is contained in:
Kelsi 2026-05-09 21:49:13 -07:00
parent 824b6ebf53
commit 493db026dd
10 changed files with 645 additions and 0 deletions

View file

@ -659,6 +659,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_spell_ranges.cpp
src/pipeline/wowee_spell_cast_times.cpp
src/pipeline/wowee_spell_durations.cpp
src/pipeline/wowee_spell_cooldowns.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1474,6 +1475,7 @@ add_executable(wowee_editor
tools/editor/cli_spell_ranges_catalog.cpp
tools/editor/cli_spell_cast_times_catalog.cpp
tools/editor/cli_spell_durations_catalog.cpp
tools/editor/cli_spell_cooldowns_catalog.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1611,6 +1613,7 @@ add_executable(wowee_editor
src/pipeline/wowee_spell_ranges.cpp
src/pipeline/wowee_spell_cast_times.cpp
src/pipeline/wowee_spell_durations.cpp
src/pipeline/wowee_spell_cooldowns.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,111 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Spell Cooldown Category catalog (.wscd) —
// novel replacement for Blizzard's SpellCooldown.dbc plus
// the per-spell category-cooldown fields in Spell.dbc.
// Defines the shared-cooldown buckets that related spells
// reference, so casting one spell triggers a cooldown on
// every other spell in the same bucket. Examples: every
// Mage Polymorph variant (Sheep / Pig / Turtle / Cat)
// shares a single bucket so polymorphing a target locks
// all the morph spells, not just the one cast. Healing
// potions, mana potions, and similar consumables work the
// same way.
//
// Distinct from WSDR (Spell Duration), which times how
// long an aura stays on a target. WSCD times how long
// before a spell can be cast again, applied to everyone in
// the bucket — the global cooldown (GCD) is the most
// common bucket of all.
//
// Cross-references with previously-added formats:
// None — this catalog is consumed directly by the spell
// engine. WSPL spell entries reference cooldownBucketId.
//
// Binary layout (little-endian):
// magic[4] = "WSCD"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// bucketId (uint32)
// nameLen + name
// descLen + description
// bucketKind (uint8) / pad[3]
// cooldownMs (uint32)
// categoryFlags (uint32)
// iconColorRGBA (uint32)
struct WoweeSpellCooldown {
enum BucketKind : uint8_t {
Spell = 0, // spell-only cooldown bucket
Item = 1, // item-only cooldown bucket
Class = 2, // class-shared bucket (e.g. all mage AOE)
Global = 3, // global cooldown — all combat spells
Misc = 4, // catch-all (engineering trinkets, etc.)
};
enum CategoryFlag : uint32_t {
AffectedByHaste = 1u << 0, // cooldown shrinks with haste
SharedWithItems = 1u << 1, // spells + items share bucket
OnGCDStart = 1u << 2, // triggers at GCD start, not cast finish
IgnoresCooldownReduction = 1u << 3, // not affected by CDR talents
};
struct Entry {
uint32_t bucketId = 0;
std::string name;
std::string description;
uint8_t bucketKind = Spell;
uint32_t cooldownMs = 0;
uint32_t categoryFlags = 0;
uint32_t iconColorRGBA = 0xFFFFFFFFu;
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t bucketId) const;
static const char* bucketKindName(uint8_t k);
};
class WoweeSpellCooldownLoader {
public:
static bool save(const WoweeSpellCooldown& cat,
const std::string& basePath);
static WoweeSpellCooldown load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-cdb* variants.
//
// makeStarter — 4 baseline buckets (GlobalCooldown
// 1.5s, ShortItem 5s, MediumItem 30s,
// LongItem 60s) covering the most
// common cooldown tiers including the
// GCD itself.
// makeClass — 5 mage-specific buckets for spells
// that share cooldowns within a class
// (PolymorphFamily 0s — instant on hit
// but exclusive, AlterTime 90s,
// Counterspell 24s, Blink 15s,
// IceBlock 5min).
// makeItems — 5 item-cooldown buckets (HealingPot
// 60s, ManaPot 60s, ManaJade 1.5s
// GCD-only, EngineerTrinket 60s,
// HearthstoneFamily 60min).
static WoweeSpellCooldown makeStarter(const std::string& catalogName);
static WoweeSpellCooldown makeClass(const std::string& catalogName);
static WoweeSpellCooldown makeItems(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,244 @@
#include "pipeline/wowee_spell_cooldowns.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'S', 'C', 'D'};
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) != ".wscd") {
base += ".wscd";
}
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 WoweeSpellCooldown::Entry*
WoweeSpellCooldown::findById(uint32_t bucketId) const {
for (const auto& e : entries)
if (e.bucketId == bucketId) return &e;
return nullptr;
}
const char* WoweeSpellCooldown::bucketKindName(uint8_t k) {
switch (k) {
case Spell: return "spell";
case Item: return "item";
case Class: return "class";
case Global: return "global";
case Misc: return "misc";
default: return "unknown";
}
}
bool WoweeSpellCooldownLoader::save(const WoweeSpellCooldown& 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.bucketId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.bucketKind);
uint8_t pad3[3] = {0, 0, 0};
os.write(reinterpret_cast<const char*>(pad3), 3);
writePOD(os, e.cooldownMs);
writePOD(os, e.categoryFlags);
writePOD(os, e.iconColorRGBA);
}
return os.good();
}
WoweeSpellCooldown WoweeSpellCooldownLoader::load(
const std::string& basePath) {
WoweeSpellCooldown 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.bucketId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.bucketKind)) {
out.entries.clear(); return out;
}
uint8_t pad3[3];
is.read(reinterpret_cast<char*>(pad3), 3);
if (is.gcount() != 3) { out.entries.clear(); return out; }
if (!readPOD(is, e.cooldownMs) ||
!readPOD(is, e.categoryFlags) ||
!readPOD(is, e.iconColorRGBA)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeSpellCooldownLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeSpellCooldown WoweeSpellCooldownLoader::makeStarter(
const std::string& catalogName) {
WoweeSpellCooldown c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t kind,
uint32_t cdMs, uint32_t flags,
uint8_t r, uint8_t g, uint8_t b,
const char* desc) {
WoweeSpellCooldown::Entry e;
e.bucketId = id; e.name = name; e.description = desc;
e.bucketKind = kind;
e.cooldownMs = cdMs;
e.categoryFlags = flags;
e.iconColorRGBA = packRgba(r, g, b);
c.entries.push_back(e);
};
add(1, "GlobalCooldown", WoweeSpellCooldown::Global,
1500,
WoweeSpellCooldown::AffectedByHaste |
WoweeSpellCooldown::OnGCDStart,
220, 220, 220, "Global cooldown — 1.5s, hasted, applies to "
"every combat spell cast.");
add(2, "ShortItemCD", WoweeSpellCooldown::Item,
5000, 0,
180, 240, 180, "Short item cooldown — 5s (low-tier consumables).");
add(3, "MediumItemCD", WoweeSpellCooldown::Item,
30000, 0,
180, 240, 100, "Medium item cooldown — 30s (mid-tier "
"consumables / wands).");
add(4, "LongItemCD", WoweeSpellCooldown::Item,
60000, WoweeSpellCooldown::SharedWithItems,
240, 220, 100, "Long item cooldown — 60s, shared between "
"healing/mana potions.");
return c;
}
WoweeSpellCooldown WoweeSpellCooldownLoader::makeClass(
const std::string& catalogName) {
WoweeSpellCooldown c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint32_t cdMs,
uint32_t flags, const char* desc) {
WoweeSpellCooldown::Entry e;
e.bucketId = id; e.name = name; e.description = desc;
e.bucketKind = WoweeSpellCooldown::Class;
e.cooldownMs = cdMs;
e.categoryFlags = flags;
e.iconColorRGBA = packRgba(100, 200, 240); // mage blue
c.entries.push_back(e);
};
add(100, "PolymorphFamily", 0,
0, "Mage Polymorph variants (Sheep / Pig / Turtle / Cat) — "
"0ms cooldown but exclusive: only one variant active per "
"target.");
add(101, "AlterTime", 90000,
WoweeSpellCooldown::AffectedByHaste,
"Alter Time — 90s, hasted by spell haste.");
add(102, "Counterspell", 24000,
0, "Counterspell — 24s, fixed cooldown.");
add(103, "Blink", 15000,
0, "Blink — 15s, fixed cooldown.");
add(104, "IceBlock", 300000,
WoweeSpellCooldown::IgnoresCooldownReduction,
"Ice Block — 5min, not affected by Cold Snap or CDR.");
return c;
}
WoweeSpellCooldown WoweeSpellCooldownLoader::makeItems(
const std::string& catalogName) {
WoweeSpellCooldown c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint32_t cdMs,
uint32_t flags, const char* desc) {
WoweeSpellCooldown::Entry e;
e.bucketId = id; e.name = name; e.description = desc;
e.bucketKind = WoweeSpellCooldown::Item;
e.cooldownMs = cdMs;
e.categoryFlags = flags;
e.iconColorRGBA = packRgba(240, 200, 100); // gold for items
c.entries.push_back(e);
};
add(200, "HealingPotion", 60000,
WoweeSpellCooldown::SharedWithItems,
"Healing potion family — 60s shared with mana potions.");
add(201, "ManaPotion", 60000,
WoweeSpellCooldown::SharedWithItems,
"Mana potion family — 60s shared with healing potions.");
add(202, "ManaJade", 1500,
WoweeSpellCooldown::OnGCDStart,
"Mana Jade / oil flasks — GCD-only, no item cooldown.");
add(203, "EngineerTrinket", 60000, 0,
"Engineer trinket — 60s standalone bucket.");
add(204, "HearthstoneFamily", 3600000, 0,
"Hearthstone — 60min, exclusive across alt-bind variants.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -215,6 +215,8 @@ const char* const kArgRequired[] = {
"--gen-sdr", "--gen-sdr-buffs", "--gen-sdr-dot",
"--info-wsdr", "--validate-wsdr",
"--export-wsdr-json", "--import-wsdr-json",
"--gen-cdb", "--gen-cdb-class", "--gen-cdb-items",
"--info-wscd", "--validate-wscd",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -108,6 +108,7 @@
#include "cli_spell_ranges_catalog.hpp"
#include "cli_spell_cast_times_catalog.hpp"
#include "cli_spell_durations_catalog.hpp"
#include "cli_spell_cooldowns_catalog.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -257,6 +258,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleSpellRangesCatalog,
handleSpellCastTimesCatalog,
handleSpellDurationsCatalog,
handleSpellCooldownsCatalog,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -73,6 +73,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','S','R','G'}, ".wsrg", "spells", "--info-wsrg", "Spell range bucket catalog"},
{{'W','S','C','T'}, ".wsct", "spells", "--info-wsct", "Spell cast time bucket catalog"},
{{'W','S','D','R'}, ".wsdr", "spells", "--info-wsdr", "Spell duration bucket catalog"},
{{'W','S','C','D'}, ".wscd", "spells", "--info-wscd", "Spell cooldown category 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

@ -1731,6 +1731,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wsdr to a human-editable JSON sidecar (defaults to <base>.wsdr.json)\n");
std::printf(" --import-wsdr-json <json-path> [out-base]\n");
std::printf(" Import a .wsdr.json sidecar back into binary .wsdr (accepts durationKind int OR durationKindName string)\n");
std::printf(" --gen-cdb <wscd-base> [name]\n");
std::printf(" Emit .wscd starter: 4 baseline cooldown buckets (GlobalCooldown 1.5s / ShortItem 5s / MediumItem 30s / LongItem 60s shared)\n");
std::printf(" --gen-cdb-class <wscd-base> [name]\n");
std::printf(" Emit .wscd 5 mage class buckets (Polymorph family / AlterTime 90s / Counterspell 24s / Blink 15s / IceBlock 5min)\n");
std::printf(" --gen-cdb-items <wscd-base> [name]\n");
std::printf(" Emit .wscd 5 item buckets (HealingPot+ManaPot 60s shared / ManaJade GCD-only / EngineerTrinket 60s / Hearthstone 60min)\n");
std::printf(" --info-wscd <wscd-base> [--json]\n");
std::printf(" Print WSCD entries (id / kind / cooldownMs / category flags / name) — flags decoded as label list\n");
std::printf(" --validate-wscd <wscd-base> [--json]\n");
std::printf(" Static checks: id+name required, bucketKind 0..4, no duplicate ids; warns on Global without OnGCDStart and Spell with SharedWithItems\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

@ -95,6 +95,7 @@ constexpr FormatRow kFormats[] = {
{"WSRG", ".wsrg", "spells", "SpellRange.dbc + per-spell range", "Spell range bucket catalog"},
{"WSCT", ".wsct", "spells", "SpellCastTimes.dbc + cast scaling","Spell cast time bucket catalog"},
{"WSDR", ".wsdr", "spells", "SpellDuration.dbc + per-spell dur","Spell duration bucket catalog"},
{"WSCD", ".wscd", "spells", "SpellCooldown.dbc + shared cd grp","Spell cooldown category catalog"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,259 @@
#include "cli_spell_cooldowns_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_spell_cooldowns.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 stripWscdExt(std::string base) {
stripExt(base, ".wscd");
return base;
}
bool saveOrError(const wowee::pipeline::WoweeSpellCooldown& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeSpellCooldownLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wscd\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeSpellCooldown& c,
const std::string& base) {
std::printf("Wrote %s.wscd\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" buckets : %zu\n", c.entries.size());
}
int handleGenStarter(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "StarterCooldowns";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWscdExt(base);
auto c = wowee::pipeline::WoweeSpellCooldownLoader::makeStarter(name);
if (!saveOrError(c, base, "gen-cdb")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenClass(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "MageClassCooldowns";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWscdExt(base);
auto c = wowee::pipeline::WoweeSpellCooldownLoader::makeClass(name);
if (!saveOrError(c, base, "gen-cdb-class")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenItems(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "ItemCooldowns";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWscdExt(base);
auto c = wowee::pipeline::WoweeSpellCooldownLoader::makeItems(name);
if (!saveOrError(c, base, "gen-cdb-items")) return 1;
printGenSummary(c, base);
return 0;
}
void appendFlagNames(uint32_t flags, std::string& out) {
using F = wowee::pipeline::WoweeSpellCooldown;
auto add = [&](const char* n) {
if (!out.empty()) out += "|";
out += n;
};
if (flags & F::AffectedByHaste) add("AffectedByHaste");
if (flags & F::SharedWithItems) add("SharedWithItems");
if (flags & F::OnGCDStart) add("OnGCDStart");
if (flags & F::IgnoresCooldownReduction) add("IgnoresCooldownReduction");
if (out.empty()) out = "-";
}
int handleInfo(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWscdExt(base);
if (!wowee::pipeline::WoweeSpellCooldownLoader::exists(base)) {
std::fprintf(stderr, "WSCD not found: %s.wscd\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeSpellCooldownLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wscd"] = base + ".wscd";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
std::string flagNames;
appendFlagNames(e.categoryFlags, flagNames);
arr.push_back({
{"bucketId", e.bucketId},
{"name", e.name},
{"description", e.description},
{"bucketKind", e.bucketKind},
{"bucketKindName", wowee::pipeline::WoweeSpellCooldown::bucketKindName(e.bucketKind)},
{"cooldownMs", e.cooldownMs},
{"categoryFlags", e.categoryFlags},
{"categoryFlagsLabels", flagNames},
{"iconColorRGBA", e.iconColorRGBA},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WSCD: %s.wscd\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" buckets : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id kind cooldownMs flags name\n");
for (const auto& e : c.entries) {
std::string flagNames;
appendFlagNames(e.categoryFlags, flagNames);
std::printf(" %4u %-7s %10u %-32s %s\n",
e.bucketId,
wowee::pipeline::WoweeSpellCooldown::bucketKindName(e.bucketKind),
e.cooldownMs,
flagNames.c_str(),
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 = stripWscdExt(base);
if (!wowee::pipeline::WoweeSpellCooldownLoader::exists(base)) {
std::fprintf(stderr,
"validate-wscd: WSCD not found: %s.wscd\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeSpellCooldownLoader::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;
constexpr uint32_t kKnownFlagMask =
wowee::pipeline::WoweeSpellCooldown::AffectedByHaste |
wowee::pipeline::WoweeSpellCooldown::SharedWithItems |
wowee::pipeline::WoweeSpellCooldown::OnGCDStart |
wowee::pipeline::WoweeSpellCooldown::IgnoresCooldownReduction;
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.bucketId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.bucketId == 0)
errors.push_back(ctx + ": bucketId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.bucketKind > wowee::pipeline::WoweeSpellCooldown::Misc) {
errors.push_back(ctx + ": bucketKind " +
std::to_string(e.bucketKind) + " not in 0..4");
}
if (e.categoryFlags & ~kKnownFlagMask) {
warnings.push_back(ctx +
": categoryFlags has bits outside known mask " +
"(0x" + std::to_string(e.categoryFlags & ~kKnownFlagMask) +
") — engine will ignore unknown flags");
}
// Global bucket should be GCD-marked. Otherwise the
// engine wouldn't trigger it on cast start.
if (e.bucketKind == wowee::pipeline::WoweeSpellCooldown::Global &&
!(e.categoryFlags & wowee::pipeline::WoweeSpellCooldown::OnGCDStart)) {
warnings.push_back(ctx +
": Global kind without OnGCDStart flag — "
"engine will not trigger this on cast start");
}
// SharedWithItems on a Spell-only bucket is
// contradictory.
if (e.bucketKind == wowee::pipeline::WoweeSpellCooldown::Spell &&
(e.categoryFlags & wowee::pipeline::WoweeSpellCooldown::SharedWithItems)) {
warnings.push_back(ctx +
": Spell kind with SharedWithItems flag — "
"switch kind to Item or Misc, or drop the flag");
}
for (uint32_t prev : idsSeen) {
if (prev == e.bucketId) {
errors.push_back(ctx + ": duplicate bucketId");
break;
}
}
idsSeen.push_back(e.bucketId);
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wscd"] = base + ".wscd";
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-wscd: %s.wscd\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu buckets, all bucketIds 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 handleSpellCooldownsCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-cdb") == 0 && i + 1 < argc) {
outRc = handleGenStarter(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-cdb-class") == 0 && i + 1 < argc) {
outRc = handleGenClass(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-cdb-items") == 0 && i + 1 < argc) {
outRc = handleGenItems(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wscd") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wscd") == 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 handleSpellCooldownsCatalog(int& i, int argc, char** argv,
int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee