feat(editor): add WSTC (Hunter Stable Slot) open catalog format

Open replacement for the hardcoded hunter pet stable slot
progression. Defines each stable slot's display order in the
stable UI, the character level at which the slot becomes
available, the gold cost to unlock, and whether it's a premium
/ donator-only slot.

In WoW 3.3.5a hunters get 5 stable slots total: the active pet
plus 4 stabled (slots 1-4 unlocking at hunter levels 10/20/30/40
with escalating gold costs 0/10s/50s/2g/10g). Cataclysm raised
the cap to 5 stabled slots, and server-custom expansions go
higher with donator-only "premium" slots that bypass the level
gate. This catalog parameterizes the entire progression instead
of editing engine source.

Consumed directly by the StableMaster service in WBKD entries.
unlockedSlotCount(characterLevel) is the engine helper used by
the stable master frame to decide how many slot tabs to render.

Three preset emitters: --gen-stc (5 canonical slots matching
WoW 3.3.5a), --gen-stc-cata (6 Cata-style slots with slot 5
unlocking at lvl 60 for 25g), --gen-stc-premium (4 server-custom
donator slots with no level/gold gate).

The info renderer pretty-prints copperCost as "free" / "10s 0c" /
"2g 0s 0c" — matches how server admins think about pricing.

Validation enforces id+name presence, no duplicate ids; warns
on:
  - minLevelToUnlock > 80 (unreachable at WotLK cap)
  - Premium slot with non-zero copperCost (donor slots are
    typically free; the gate is donor status, not gold)
  - duplicate displayOrder (stable UI position collision —
    only the first slot would render)

Wired through the cross-format table; WSTC appears in all 18
cross-format utilities. Format count 92 -> 93; CLI flag count
1069 -> 1074.
This commit is contained in:
Kelsi 2026-05-09 23:58:49 -07:00
parent 3f65e63ca1
commit 8f6f6ac91e
10 changed files with 603 additions and 0 deletions

View file

@ -681,6 +681,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_creature_patrols.cpp
src/pipeline/wowee_boss_encounters.cpp
src/pipeline/wowee_instance_lockouts.cpp
src/pipeline/wowee_stable_slots.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1525,6 +1526,7 @@ add_executable(wowee_editor
tools/editor/cli_creature_patrols_catalog.cpp
tools/editor/cli_boss_encounters_catalog.cpp
tools/editor/cli_instance_lockouts_catalog.cpp
tools/editor/cli_stable_slots_catalog.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1684,6 +1686,7 @@ add_executable(wowee_editor
src/pipeline/wowee_creature_patrols.cpp
src/pipeline/wowee_boss_encounters.cpp
src/pipeline/wowee_instance_lockouts.cpp
src/pipeline/wowee_stable_slots.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,94 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Hunter Stable Slot catalog (.wstc) — novel
// replacement for the hardcoded hunter pet stable slot
// progression. Defines each stable slot's display order
// in the stable UI, the character level at which the
// slot becomes available, the gold cost to unlock, and
// whether it's a premium / donator-only slot.
//
// In WoW 3.3.5a hunters get 5 stable slots total: the
// active pet plus 4 stabled (slots 1-4 unlocking at
// hunter levels 10/20/30/40 with escalating gold costs).
// Cataclysm raised the cap to 5 stabled slots, and
// server-custom expansions go higher. This catalog lets
// admins parameterize the entire progression instead of
// editing engine source.
//
// Cross-references with previously-added formats:
// None — this catalog is consumed directly by the
// stable master service in WBKD entries with
// serviceKind=StableMaster.
//
// Binary layout (little-endian):
// magic[4] = "WSTC"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// slotId (uint32)
// nameLen + name
// descLen + description
// displayOrder (uint8) / minLevelToUnlock (uint8)
// isPremium (uint8) / pad (uint8)
// copperCost (uint32)
// iconColorRGBA (uint32)
struct WoweeStableSlot {
struct Entry {
uint32_t slotId = 0;
std::string name;
std::string description;
uint8_t displayOrder = 0;
uint8_t minLevelToUnlock = 1;
uint8_t isPremium = 0; // 0/1 bool
uint8_t pad0 = 0;
uint32_t copperCost = 0; // 1g = 10000c
uint32_t iconColorRGBA = 0xFFFFFFFFu;
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t slotId) const;
// Returns the count of slots a hunter at the given
// character level has unlocked. Used by the stable
// master frame to decide how many slot tabs to render.
int unlockedSlotCount(uint8_t characterLevel) const;
};
class WoweeStableSlotLoader {
public:
static bool save(const WoweeStableSlot& cat,
const std::string& basePath);
static WoweeStableSlot load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-stc* variants.
//
// makeStandard — 5 canonical slots matching WoW
// 3.3.5a (Active + 4 stabled,
// unlocking at lvl 10/20/30/40 with
// 10s/50s/2g/10g costs).
// makeCata — 6 Cata-style slots (Active + 5
// stabled with later unlock at
// lvl 50, 25g cost for slot 5).
// makePremium — 4 server-custom premium slots
// (donator-only, marked premium=1,
// no level gate, no gold cost).
static WoweeStableSlot makeStandard(const std::string& catalogName);
static WoweeStableSlot makeCata(const std::string& catalogName);
static WoweeStableSlot makePremium(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,229 @@
#include "pipeline/wowee_stable_slots.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'S', 'T', 'C'};
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) != ".wstc") {
base += ".wstc";
}
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 WoweeStableSlot::Entry*
WoweeStableSlot::findById(uint32_t slotId) const {
for (const auto& e : entries)
if (e.slotId == slotId) return &e;
return nullptr;
}
int WoweeStableSlot::unlockedSlotCount(uint8_t characterLevel) const {
int n = 0;
for (const auto& e : entries) {
if (characterLevel >= e.minLevelToUnlock) ++n;
}
return n;
}
bool WoweeStableSlotLoader::save(const WoweeStableSlot& 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.slotId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.displayOrder);
writePOD(os, e.minLevelToUnlock);
writePOD(os, e.isPremium);
writePOD(os, e.pad0);
writePOD(os, e.copperCost);
writePOD(os, e.iconColorRGBA);
}
return os.good();
}
WoweeStableSlot WoweeStableSlotLoader::load(const std::string& basePath) {
WoweeStableSlot 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.slotId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.displayOrder) ||
!readPOD(is, e.minLevelToUnlock) ||
!readPOD(is, e.isPremium) ||
!readPOD(is, e.pad0) ||
!readPOD(is, e.copperCost) ||
!readPOD(is, e.iconColorRGBA)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeStableSlotLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeStableSlot WoweeStableSlotLoader::makeStandard(
const std::string& catalogName) {
using S = WoweeStableSlot;
WoweeStableSlot c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t order,
uint8_t lvl, uint32_t cop, const char* desc) {
S::Entry e;
e.slotId = id; e.name = name; e.description = desc;
e.displayOrder = order;
e.minLevelToUnlock = lvl;
e.copperCost = cop;
e.iconColorRGBA = packRgba(140, 100, 60); // stable brown
c.entries.push_back(e);
};
// 5 canonical slots: active + 4 stabled. Active is
// always unlocked (lvl 10 = hunter trainable), then
// stabled slots open at 20/30/40/50 with escalating
// gold costs.
add(1, "ActivePet", 0, 10, 0,
"Active pet slot — auto-unlocked at hunter lvl 10.");
add(2, "StableSlot1", 1, 20, 1000,
"Stable slot 1 — unlocks at lvl 20 for 10 silver.");
add(3, "StableSlot2", 2, 30, 5000,
"Stable slot 2 — unlocks at lvl 30 for 50 silver.");
add(4, "StableSlot3", 3, 40, 20000,
"Stable slot 3 — unlocks at lvl 40 for 2 gold.");
add(5, "StableSlot4", 4, 50, 100000,
"Stable slot 4 — unlocks at lvl 50 for 10 gold.");
return c;
}
WoweeStableSlot WoweeStableSlotLoader::makeCata(
const std::string& catalogName) {
using S = WoweeStableSlot;
WoweeStableSlot c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t order,
uint8_t lvl, uint32_t cop, const char* desc) {
S::Entry e;
e.slotId = id; e.name = name; e.description = desc;
e.displayOrder = order;
e.minLevelToUnlock = lvl;
e.copperCost = cop;
e.iconColorRGBA = packRgba(160, 120, 80);
c.entries.push_back(e);
};
// Cata-era 6-slot layout: active + 5 stabled.
add(100, "ActivePet", 0, 10, 0,
"Active pet — auto-unlocked at lvl 10.");
add(101, "CataStableSlot1", 1, 20, 1000,
"Stable slot 1 — lvl 20, 10s.");
add(102, "CataStableSlot2", 2, 30, 5000,
"Stable slot 2 — lvl 30, 50s.");
add(103, "CataStableSlot3", 3, 40, 20000,
"Stable slot 3 — lvl 40, 2g.");
add(104, "CataStableSlot4", 4, 50, 100000,
"Stable slot 4 — lvl 50, 10g.");
add(105, "CataStableSlot5", 5, 60, 250000,
"Stable slot 5 — lvl 60, 25g (Cataclysm extension).");
return c;
}
WoweeStableSlot WoweeStableSlotLoader::makePremium(
const std::string& catalogName) {
using S = WoweeStableSlot;
WoweeStableSlot c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t order,
const char* desc) {
S::Entry e;
e.slotId = id; e.name = name; e.description = desc;
e.displayOrder = order;
e.minLevelToUnlock = 1;
e.isPremium = 1;
e.copperCost = 0;
e.iconColorRGBA = packRgba(240, 180, 240); // donor pink
c.entries.push_back(e);
};
// Server-custom donator-only slots — no level gate,
// no gold cost; access controlled by external donor
// status check.
add(200, "DonatorSlot1", 6, "Donator slot 1 — premium, no level/gold gate.");
add(201, "DonatorSlot2", 7, "Donator slot 2 — premium, no level/gold gate.");
add(202, "DonatorSlot3", 8, "Donator slot 3 — premium, no level/gold gate.");
add(203, "AnniversarySlot", 9, "Anniversary slot — premium, server event reward.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -285,6 +285,8 @@ const char* const kArgRequired[] = {
"--gen-hld", "--gen-hld-dungeon", "--gen-hld-event",
"--info-whld", "--validate-whld",
"--export-whld-json", "--import-whld-json",
"--gen-stc", "--gen-stc-cata", "--gen-stc-premium",
"--info-wstc", "--validate-wstc",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -137,6 +137,7 @@
#include "cli_creature_patrols_catalog.hpp"
#include "cli_boss_encounters_catalog.hpp"
#include "cli_instance_lockouts_catalog.hpp"
#include "cli_stable_slots_catalog.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -315,6 +316,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleCreaturePatrolsCatalog,
handleBossEncountersCatalog,
handleInstanceLockoutsCatalog,
handleStableSlotsCatalog,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -95,6 +95,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','C','M','R'}, ".wcmr", "creatures", "--info-wcmr", "Creature patrol path catalog"},
{{'W','B','O','S'}, ".wbos", "raid", "--info-wbos", "Boss encounter definition catalog"},
{{'W','H','L','D'}, ".whld", "raid", "--info-whld", "Instance lockout schedule catalog"},
{{'W','S','T','C'}, ".wstc", "pets", "--info-wstc", "Hunter stable slot 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

@ -2055,6 +2055,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .whld to a human-editable JSON sidecar (defaults to <base>.whld.json)\n");
std::printf(" --import-whld-json <json-path> [out-base]\n");
std::printf(" Import a .whld.json sidecar back into binary .whld (accepts raidLockoutKind int OR raidLockoutKindName string)\n");
std::printf(" --gen-stc <wstc-base> [name]\n");
std::printf(" Emit .wstc 5 canonical hunter stable slots (Active + 4 stabled, unlocking lvl 10/20/30/40/50 with 0/10s/50s/2g/10g costs)\n");
std::printf(" --gen-stc-cata <wstc-base> [name]\n");
std::printf(" Emit .wstc 6 Cata-style slots (Active + 5 stabled), with slot 5 unlocking at lvl 60 for 25g\n");
std::printf(" --gen-stc-premium <wstc-base> [name]\n");
std::printf(" Emit .wstc 4 server-custom donator-only slots (premium=1, no level/gold gate)\n");
std::printf(" --info-wstc <wstc-base> [--json]\n");
std::printf(" Print WSTC entries (id / displayOrder / minLevelToUnlock / cost (formatted) / premium flag / name)\n");
std::printf(" --validate-wstc <wstc-base> [--json]\n");
std::printf(" Static checks: id+name required, no duplicate ids; warns on lvl>80 (unreachable), Premium+nonzero cost (donor slots are free), duplicate displayOrder (UI collision)\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

@ -117,6 +117,7 @@ constexpr FormatRow kFormats[] = {
{"WCMR", ".wcmr", "creatures", "creature_movement waypoints SQL", "Creature patrol path catalog"},
{"WBOS", ".wbos", "raid", "instance_encounter SQL", "Boss encounter definition catalog"},
{"WHLD", ".whld", "raid", "InstanceTemplate.dbc reset fields","Instance lockout schedule catalog"},
{"WSTC", ".wstc", "pets", "stable_slot SQL + hunter UI", "Hunter stable slot catalog"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,249 @@
#include "cli_stable_slots_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_stable_slots.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 stripWstcExt(std::string base) {
stripExt(base, ".wstc");
return base;
}
bool saveOrError(const wowee::pipeline::WoweeStableSlot& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeStableSlotLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wstc\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeStableSlot& c,
const std::string& base) {
std::printf("Wrote %s.wstc\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" slots : %zu\n", c.entries.size());
}
int handleGenStandard(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "StandardStableSlots";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWstcExt(base);
auto c = wowee::pipeline::WoweeStableSlotLoader::makeStandard(name);
if (!saveOrError(c, base, "gen-stc")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenCata(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "CataStableSlots";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWstcExt(base);
auto c = wowee::pipeline::WoweeStableSlotLoader::makeCata(name);
if (!saveOrError(c, base, "gen-stc-cata")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenPremium(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "PremiumStableSlots";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWstcExt(base);
auto c = wowee::pipeline::WoweeStableSlotLoader::makePremium(name);
if (!saveOrError(c, base, "gen-stc-premium")) return 1;
printGenSummary(c, base);
return 0;
}
void formatGold(uint32_t copper, char* buf, size_t bufSize) {
uint32_t g = copper / 10000;
uint32_t s = (copper % 10000) / 100;
uint32_t cop = copper % 100;
if (copper == 0) std::snprintf(buf, bufSize, "free");
else if (g > 0) std::snprintf(buf, bufSize, "%ug %us %uc", g, s, cop);
else if (s > 0) std::snprintf(buf, bufSize, "%us %uc", s, cop);
else std::snprintf(buf, bufSize, "%uc", cop);
}
int handleInfo(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWstcExt(base);
if (!wowee::pipeline::WoweeStableSlotLoader::exists(base)) {
std::fprintf(stderr, "WSTC not found: %s.wstc\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeStableSlotLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wstc"] = base + ".wstc";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"slotId", e.slotId},
{"name", e.name},
{"description", e.description},
{"displayOrder", e.displayOrder},
{"minLevelToUnlock", e.minLevelToUnlock},
{"isPremium", e.isPremium != 0},
{"copperCost", e.copperCost},
{"iconColorRGBA", e.iconColorRGBA},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WSTC: %s.wstc\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" slots : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id ord unlockLvl cost premium name\n");
for (const auto& e : c.entries) {
char goldBuf[32];
formatGold(e.copperCost, goldBuf, sizeof(goldBuf));
std::printf(" %4u %u %3u %-13s %s %s\n",
e.slotId, e.displayOrder, e.minLevelToUnlock,
goldBuf, e.isPremium ? "yes" : "no ",
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 = stripWstcExt(base);
if (!wowee::pipeline::WoweeStableSlotLoader::exists(base)) {
std::fprintf(stderr,
"validate-wstc: WSTC not found: %s.wstc\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeStableSlotLoader::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;
std::vector<uint8_t> ordersSeen;
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.slotId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.slotId == 0)
errors.push_back(ctx + ": slotId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.minLevelToUnlock > 80) {
warnings.push_back(ctx +
": minLevelToUnlock " +
std::to_string(e.minLevelToUnlock) +
" > 80 — slot unreachable at WotLK cap");
}
// Premium slot with non-zero cost is contradictory —
// donator slots should be free (status-gated, not
// gold-gated).
if (e.isPremium && e.copperCost > 0) {
warnings.push_back(ctx +
": Premium slot with copperCost=" +
std::to_string(e.copperCost) +
" — donator slots are typically free; the gate "
"is donor status, not gold");
}
for (uint32_t prev : idsSeen) {
if (prev == e.slotId) {
errors.push_back(ctx + ": duplicate slotId");
break;
}
}
idsSeen.push_back(e.slotId);
// Two slots with the same displayOrder collide in
// the stable UI — only the first would render.
for (uint8_t prevOrd : ordersSeen) {
if (prevOrd == e.displayOrder) {
warnings.push_back(ctx +
": duplicate displayOrder " +
std::to_string(e.displayOrder) +
" — stable UI position collision");
break;
}
}
ordersSeen.push_back(e.displayOrder);
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wstc"] = base + ".wstc";
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-wstc: %s.wstc\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu slots, all slotIds unique, no UI collisions\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 handleStableSlotsCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-stc") == 0 && i + 1 < argc) {
outRc = handleGenStandard(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-stc-cata") == 0 && i + 1 < argc) {
outRc = handleGenCata(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-stc-premium") == 0 && i + 1 < argc) {
outRc = handleGenPremium(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wstc") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wstc") == 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 handleStableSlotsCatalog(int& i, int argc, char** argv,
int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee