Kelsidavis-WoWee/tools/editor/cli_spell_cooldowns_catalog.cpp
Kelsi 493db026dd 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.
2026-05-09 21:49:13 -07:00

259 lines
9.4 KiB
C++

#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