mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
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.
249 lines
8.6 KiB
C++
249 lines
8.6 KiB
C++
#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
|