Kelsidavis-WoWee/tools/editor/cli_pvp_ranks_catalog.cpp

443 lines
16 KiB
C++
Raw Normal View History

feat(editor): add WPRG (PvP Ranking grades) — 122nd open format Novel replacement for the hardcoded 14-rank vanilla WoW PvP ladder (Private through Grand Marshal Alliance, Scout through High Warlord Horde). Each entry binds one (factionFilter, tier) combination to its display name, weekly RP threshold to maintain rank, lifetime honor for first-time achievement, title prefix for player- name display, and tier-set gear reward. The vanilla rank-ladder system used a weekly RP-decay mechanic that punished any week without play with rank- loss; this catalog stores both the weekly threshold (maintenance) and the lifetime threshold (achievement) since both are needed for accurate rank-progression simulation. Three preset emitters spanning the rank ladder: makeAllianceRanks (7 lower-tier ranks Private through Knight-Lieutenant), makeHordeRanks (7 mirrored Horde titles Scout through Blood Guard with identical honor thresholds — factionFilter disambiguates the shared "Sergeant" title), makeHighRanks (8 high-tier ranks across both factions Knight-Captain through Lt. Commander, tiers 8-11 with the iconic legendary battlegear shoulder unlocks). Tier 14 (Grand Marshal / High Warlord) intentionally omitted from presets — it's the legendary top-rank that historically required dedicated 24/7 grinding. Catalog supports tiers 1..14 in the schema; consumers extend as needed. Validator's most novel checks: per-(faction, tier) tuple uniqueness — two ranks at the same tier for the same faction would tie at runtime when the rank- progression UI looks up "what's tier 5 for Alliance?" Plus per-faction honor-threshold monotonicity — a higher tier requiring less honor than a lower tier would let players "downrank" by gaining honor, which is a content authoring bug. Format count 121 -> 122. CLI flag count 1276 -> 1281.
2026-05-10 03:08:27 -07:00
#include "cli_pvp_ranks_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_pvp_ranks.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
std::string stripWprgExt(std::string base) {
stripExt(base, ".wprg");
return base;
}
const char* factionFilterName(uint8_t f) {
using P = wowee::pipeline::WoweePvPRanks;
switch (f) {
case P::AllianceOnly: return "alliance";
case P::HordeOnly: return "horde";
default: return "unknown";
}
}
bool saveOrError(const wowee::pipeline::WoweePvPRanks& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweePvPRanksLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wprg\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweePvPRanks& c,
const std::string& base) {
std::printf("Wrote %s.wprg\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" ranks : %zu\n", c.entries.size());
}
int handleGenAlliance(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "AllianceLowerRanks";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWprgExt(base);
auto c = wowee::pipeline::WoweePvPRanksLoader::makeAllianceRanks(name);
if (!saveOrError(c, base, "gen-prg")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenHorde(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "HordeLowerRanks";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWprgExt(base);
auto c = wowee::pipeline::WoweePvPRanksLoader::makeHordeRanks(name);
if (!saveOrError(c, base, "gen-prg-horde")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenHigh(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "HighRanks";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWprgExt(base);
auto c = wowee::pipeline::WoweePvPRanksLoader::makeHighRanks(name);
if (!saveOrError(c, base, "gen-prg-high")) return 1;
printGenSummary(c, base);
return 0;
}
int handleInfo(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWprgExt(base);
if (!wowee::pipeline::WoweePvPRanksLoader::exists(base)) {
std::fprintf(stderr, "WPRG not found: %s.wprg\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweePvPRanksLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wprg"] = base + ".wprg";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"rankId", e.rankId},
{"name", e.name},
{"description", e.description},
{"factionFilter", e.factionFilter},
{"factionFilterName",
factionFilterName(e.factionFilter)},
{"tier", e.tier},
{"honorRequiredWeekly", e.honorRequiredWeekly},
{"honorRequiredAchieve", e.honorRequiredAchieve},
{"titlePrefix", e.titlePrefix},
{"gearItemId", e.gearItemId},
{"iconColorRGBA", e.iconColorRGBA},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WPRG: %s.wprg\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" ranks : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id faction tier weekly RP total RP gear title name\n");
for (const auto& e : c.entries) {
std::printf(" %4u %-8s %3u %9u %9u %5u %-15s %s\n",
e.rankId, factionFilterName(e.factionFilter),
e.tier, e.honorRequiredWeekly,
e.honorRequiredAchieve, e.gearItemId,
e.titlePrefix.c_str(), e.name.c_str());
}
return 0;
}
int parseFactionFilterToken(const std::string& s) {
using P = wowee::pipeline::WoweePvPRanks;
if (s == "alliance") return P::AllianceOnly;
if (s == "horde") return P::HordeOnly;
return -1;
}
int handleExportJson(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string out;
if (parseOptArg(i, argc, argv)) out = argv[++i];
base = stripWprgExt(base);
if (out.empty()) out = base + ".wprg.json";
if (!wowee::pipeline::WoweePvPRanksLoader::exists(base)) {
std::fprintf(stderr,
"export-wprg-json: WPRG not found: %s.wprg\n",
base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweePvPRanksLoader::load(base);
nlohmann::json j;
j["magic"] = "WPRG";
j["version"] = 1;
j["name"] = c.name;
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"rankId", e.rankId},
{"name", e.name},
{"description", e.description},
{"factionFilter", e.factionFilter},
{"factionFilterName",
factionFilterName(e.factionFilter)},
{"tier", e.tier},
{"honorRequiredWeekly", e.honorRequiredWeekly},
{"honorRequiredAchieve", e.honorRequiredAchieve},
{"titlePrefix", e.titlePrefix},
{"gearItemId", e.gearItemId},
{"iconColorRGBA", e.iconColorRGBA},
});
}
j["entries"] = arr;
std::ofstream os(out);
if (!os) {
std::fprintf(stderr,
"export-wprg-json: failed to open %s for write\n",
out.c_str());
return 1;
}
os << j.dump(2) << "\n";
std::printf("Wrote %s (%zu ranks)\n",
out.c_str(), c.entries.size());
return 0;
}
int handleImportJson(int& i, int argc, char** argv) {
std::string in = argv[++i];
std::string outBase;
if (parseOptArg(i, argc, argv)) outBase = argv[++i];
if (outBase.empty()) {
outBase = in;
if (outBase.size() >= 10 &&
outBase.substr(outBase.size() - 10) == ".wprg.json") {
outBase.resize(outBase.size() - 10);
} else {
stripExt(outBase, ".json");
stripExt(outBase, ".wprg");
}
}
std::ifstream is(in);
if (!is) {
std::fprintf(stderr,
"import-wprg-json: cannot open %s\n", in.c_str());
return 1;
}
nlohmann::json j;
try {
is >> j;
} catch (const std::exception& ex) {
std::fprintf(stderr,
"import-wprg-json: JSON parse error: %s\n", ex.what());
return 1;
}
wowee::pipeline::WoweePvPRanks c;
c.name = j.value("name", std::string{});
if (!j.contains("entries") || !j["entries"].is_array()) {
std::fprintf(stderr,
"import-wprg-json: missing or non-array 'entries'\n");
return 1;
}
for (const auto& je : j["entries"]) {
wowee::pipeline::WoweePvPRanks::Entry e;
e.rankId = je.value("rankId", 0u);
e.name = je.value("name", std::string{});
e.description = je.value("description", std::string{});
if (je.contains("factionFilter")) {
const auto& v = je["factionFilter"];
if (v.is_string()) {
int parsed = parseFactionFilterToken(
v.get<std::string>());
if (parsed < 0) {
std::fprintf(stderr,
"import-wprg-json: unknown "
"factionFilter token '%s' on "
"entry id=%u\n",
v.get<std::string>().c_str(),
e.rankId);
return 1;
}
e.factionFilter = static_cast<uint8_t>(parsed);
} else if (v.is_number_integer()) {
e.factionFilter = static_cast<uint8_t>(
v.get<int>());
}
} else if (je.contains("factionFilterName") &&
je["factionFilterName"].is_string()) {
int parsed = parseFactionFilterToken(
je["factionFilterName"].get<std::string>());
if (parsed >= 0)
e.factionFilter = static_cast<uint8_t>(parsed);
}
e.tier = static_cast<uint8_t>(je.value("tier", 1u));
e.honorRequiredWeekly = je.value("honorRequiredWeekly", 0u);
e.honorRequiredAchieve = je.value("honorRequiredAchieve",
0u);
e.titlePrefix = je.value("titlePrefix", std::string{});
e.gearItemId = je.value("gearItemId", 0u);
e.iconColorRGBA = je.value("iconColorRGBA", 0xFFFFFFFFu);
c.entries.push_back(e);
}
if (!wowee::pipeline::WoweePvPRanksLoader::save(c, outBase)) {
std::fprintf(stderr,
"import-wprg-json: failed to save %s.wprg\n",
outBase.c_str());
return 1;
}
std::printf("Wrote %s.wprg (%zu ranks)\n",
outBase.c_str(), c.entries.size());
return 0;
}
feat(editor): add WPRG (PvP Ranking grades) — 122nd open format Novel replacement for the hardcoded 14-rank vanilla WoW PvP ladder (Private through Grand Marshal Alliance, Scout through High Warlord Horde). Each entry binds one (factionFilter, tier) combination to its display name, weekly RP threshold to maintain rank, lifetime honor for first-time achievement, title prefix for player- name display, and tier-set gear reward. The vanilla rank-ladder system used a weekly RP-decay mechanic that punished any week without play with rank- loss; this catalog stores both the weekly threshold (maintenance) and the lifetime threshold (achievement) since both are needed for accurate rank-progression simulation. Three preset emitters spanning the rank ladder: makeAllianceRanks (7 lower-tier ranks Private through Knight-Lieutenant), makeHordeRanks (7 mirrored Horde titles Scout through Blood Guard with identical honor thresholds — factionFilter disambiguates the shared "Sergeant" title), makeHighRanks (8 high-tier ranks across both factions Knight-Captain through Lt. Commander, tiers 8-11 with the iconic legendary battlegear shoulder unlocks). Tier 14 (Grand Marshal / High Warlord) intentionally omitted from presets — it's the legendary top-rank that historically required dedicated 24/7 grinding. Catalog supports tiers 1..14 in the schema; consumers extend as needed. Validator's most novel checks: per-(faction, tier) tuple uniqueness — two ranks at the same tier for the same faction would tie at runtime when the rank- progression UI looks up "what's tier 5 for Alliance?" Plus per-faction honor-threshold monotonicity — a higher tier requiring less honor than a lower tier would let players "downrank" by gaining honor, which is a content authoring bug. Format count 121 -> 122. CLI flag count 1276 -> 1281.
2026-05-10 03:08:27 -07:00
int handleValidate(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWprgExt(base);
if (!wowee::pipeline::WoweePvPRanksLoader::exists(base)) {
std::fprintf(stderr,
"validate-wprg: WPRG not found: %s.wprg\n",
base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweePvPRanksLoader::load(base);
std::vector<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::set<uint32_t> idsSeen;
// Per-(faction, tier) tuple uniqueness — two ranks
// at the same tier for the same faction would tie
// at runtime when the rank-progression UI looks up
// "what's tier 5 for Alliance?"
std::set<uint16_t> factionTierSeen;
auto factionTierKey = [](uint8_t faction, uint8_t tier) {
return static_cast<uint16_t>(
(static_cast<uint16_t>(faction) << 8) | tier);
};
// Per-faction monotonicity: honorRequiredAchieve
// should be non-decreasing as tier increases.
std::map<uint8_t, std::vector<
const wowee::pipeline::WoweePvPRanks::Entry*>>
byFaction;
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.rankId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.rankId == 0)
errors.push_back(ctx + ": rankId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.factionFilter != 1 && e.factionFilter != 2) {
errors.push_back(ctx + ": factionFilter " +
std::to_string(e.factionFilter) +
" out of range (must be 1=Alliance or "
"2=Horde)");
}
if (e.tier < 1 || e.tier > 14) {
errors.push_back(ctx + ": tier " +
std::to_string(e.tier) +
" out of range (must be 1..14 — vanilla "
"ladder)");
}
if (e.titlePrefix.empty()) {
warnings.push_back(ctx +
": titlePrefix is empty — UI rank-name "
"display would render blank");
}
if (e.tier <= 14 && (e.factionFilter == 1 ||
e.factionFilter == 2)) {
uint16_t key = factionTierKey(e.factionFilter,
e.tier);
if (!factionTierSeen.insert(key).second) {
errors.push_back(ctx +
": (faction=" +
std::string(factionFilterName(e.factionFilter)) +
", tier=" + std::to_string(e.tier) +
") slot already occupied by another "
"rank — runtime lookup would tie");
}
}
if (!idsSeen.insert(e.rankId).second) {
errors.push_back(ctx + ": duplicate rankId");
}
byFaction[e.factionFilter].push_back(&e);
}
// Per-faction monotonicity check.
for (auto& [faction, ranks] : byFaction) {
if (ranks.size() < 2) continue;
std::sort(ranks.begin(), ranks.end(),
[](auto* a, auto* b) {
return a->tier < b->tier;
});
for (size_t k = 1; k < ranks.size(); ++k) {
if (ranks[k]->honorRequiredAchieve <
ranks[k-1]->honorRequiredAchieve) {
warnings.push_back("faction " +
std::string(factionFilterName(faction)) +
" has decreasing honor threshold: tier " +
std::to_string(ranks[k-1]->tier) +
" (" + ranks[k-1]->name + ") requires " +
std::to_string(ranks[k-1]->honorRequiredAchieve) +
" > tier " + std::to_string(ranks[k]->tier) +
" (" + ranks[k]->name + ") requiring " +
std::to_string(ranks[k]->honorRequiredAchieve) +
" — higher tier should require more "
"honor, not less");
}
}
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wprg"] = base + ".wprg";
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-wprg: %s.wprg\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu ranks, all rankIds + "
"(faction,tier) tuples unique, honor "
"thresholds monotonic per faction\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 handlePvPRanksCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-prg") == 0 && i + 1 < argc) {
outRc = handleGenAlliance(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-prg-horde") == 0 &&
i + 1 < argc) {
outRc = handleGenHorde(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-prg-high") == 0 &&
i + 1 < argc) {
outRc = handleGenHigh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wprg") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wprg") == 0 && i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--export-wprg-json") == 0 && i + 1 < argc) {
outRc = handleExportJson(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--import-wprg-json") == 0 && i + 1 < argc) {
outRc = handleImportJson(i, argc, argv); return true;
}
feat(editor): add WPRG (PvP Ranking grades) — 122nd open format Novel replacement for the hardcoded 14-rank vanilla WoW PvP ladder (Private through Grand Marshal Alliance, Scout through High Warlord Horde). Each entry binds one (factionFilter, tier) combination to its display name, weekly RP threshold to maintain rank, lifetime honor for first-time achievement, title prefix for player- name display, and tier-set gear reward. The vanilla rank-ladder system used a weekly RP-decay mechanic that punished any week without play with rank- loss; this catalog stores both the weekly threshold (maintenance) and the lifetime threshold (achievement) since both are needed for accurate rank-progression simulation. Three preset emitters spanning the rank ladder: makeAllianceRanks (7 lower-tier ranks Private through Knight-Lieutenant), makeHordeRanks (7 mirrored Horde titles Scout through Blood Guard with identical honor thresholds — factionFilter disambiguates the shared "Sergeant" title), makeHighRanks (8 high-tier ranks across both factions Knight-Captain through Lt. Commander, tiers 8-11 with the iconic legendary battlegear shoulder unlocks). Tier 14 (Grand Marshal / High Warlord) intentionally omitted from presets — it's the legendary top-rank that historically required dedicated 24/7 grinding. Catalog supports tiers 1..14 in the schema; consumers extend as needed. Validator's most novel checks: per-(faction, tier) tuple uniqueness — two ranks at the same tier for the same faction would tie at runtime when the rank- progression UI looks up "what's tier 5 for Alliance?" Plus per-faction honor-threshold monotonicity — a higher tier requiring less honor than a lower tier would let players "downrank" by gaining honor, which is a content authoring bug. Format count 121 -> 122. CLI flag count 1276 -> 1281.
2026-05-10 03:08:27 -07:00
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee