mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
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:
parent
3f65e63ca1
commit
8f6f6ac91e
10 changed files with 603 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
94
include/pipeline/wowee_stable_slots.hpp
Normal file
94
include/pipeline/wowee_stable_slots.hpp
Normal 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
|
||||
229
src/pipeline/wowee_stable_slots.cpp
Normal file
229
src/pipeline/wowee_stable_slots.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
249
tools/editor/cli_stable_slots_catalog.cpp
Normal file
249
tools/editor/cli_stable_slots_catalog.cpp
Normal 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
|
||||
12
tools/editor/cli_stable_slots_catalog.hpp
Normal file
12
tools/editor/cli_stable_slots_catalog.hpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue