diff --git a/CMakeLists.txt b/CMakeLists.txt index a92eb9d9..a30e9a4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -710,6 +710,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_sky_params.cpp src/pipeline/wowee_server_config.cpp src/pipeline/wowee_anniversary_events.cpp + src/pipeline/wowee_pvp_ranks.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1583,6 +1584,7 @@ add_executable(wowee_editor tools/editor/cli_sky_params_catalog.cpp tools/editor/cli_server_config_catalog.cpp tools/editor/cli_anniversary_events_catalog.cpp + tools/editor/cli_pvp_ranks_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1775,6 +1777,7 @@ add_executable(wowee_editor src/pipeline/wowee_sky_params.cpp src/pipeline/wowee_server_config.cpp src/pipeline/wowee_anniversary_events.cpp + src/pipeline/wowee_pvp_ranks.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_pvp_ranks.hpp b/include/pipeline/wowee_pvp_ranks.hpp new file mode 100644 index 00000000..c106f858 --- /dev/null +++ b/include/pipeline/wowee_pvp_ranks.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open PvP Ranking Grades catalog (.wprg) — +// novel replacement for the hardcoded 14-rank PvP +// ladder vanilla WoW shipped (Private through Grand +// Marshal for Alliance, Scout through High Warlord for +// 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, and tier-set gear reward. +// +// Cross-references with previously-added formats: +// WCHC: factionFilter uses the WCHC faction-mask +// convention (1=Alliance, 2=Horde). +// WIT: gearItemId references the WIT item catalog +// (the rank-tier set piece — typically the +// legendary battlegear shoulders unlocked at +// high ranks). +// WPVP: WPRG supersedes the older WPVP simpler PvP +// currency catalog where rank-progression +// semantics matter. +// +// Binary layout (little-endian): +// magic[4] = "WPRG" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// rankId (uint32) +// nameLen + name +// descLen + description +// factionFilter (uint8) — 1=Alliance, 2=Horde +// tier (uint8) — 1..14 vanilla rank +// ladder +// pad0 (uint8) / pad1 (uint8) +// honorRequiredWeekly (uint32) — RP threshold per +// week to maintain +// honorRequiredAchieve (uint32) — total RP for +// first-time +// achievement +// prefixLen + titlePrefix — e.g. "Sergeant" +// gearItemId (uint32) — 0 if no gear +// reward at this +// tier +// iconColorRGBA (uint32) +struct WoweePvPRanks { + enum FactionFilter : uint8_t { + AllianceOnly = 1, + HordeOnly = 2, + }; + + struct Entry { + uint32_t rankId = 0; + std::string name; + std::string description; + uint8_t factionFilter = AllianceOnly; + uint8_t tier = 1; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint32_t honorRequiredWeekly = 0; + uint32_t honorRequiredAchieve = 0; + std::string titlePrefix; + uint32_t gearItemId = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t rankId) const; + + // Returns all entries for one faction sorted by + // tier. Used by the rank-progression UI to render + // the ladder. + std::vector findByFaction(uint8_t faction) const; + + // Returns the entry for a specific (faction, tier) + // combination. Used by the weekly-honor processor + // to look up "what's the threshold for tier 7?" + const Entry* findByTier(uint8_t faction, + uint8_t tier) const; +}; + +class WoweePvPRanksLoader { +public: + static bool save(const WoweePvPRanks& cat, + const std::string& basePath); + static WoweePvPRanks load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-prg* variants. + // + // makeAllianceRanks — 7 lower-tier Alliance ranks + // (Private through Knight- + // Lieutenant, tiers 1-7). + // makeHordeRanks — 7 lower-tier Horde ranks + // (Scout through Blood Guard, + // tiers 1-7) with mirrored + // honor thresholds. + // makeHighRanks — 4 high-tier Alliance ranks + // (Knight-Captain through + // Commander, tiers 8-11). + // Plus 4 mirrored Horde + // (Legionnaire through Lt. + // Commander). + static WoweePvPRanks makeAllianceRanks(const std::string& catalogName); + static WoweePvPRanks makeHordeRanks(const std::string& catalogName); + static WoweePvPRanks makeHighRanks(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_pvp_ranks.cpp b/src/pipeline/wowee_pvp_ranks.cpp new file mode 100644 index 00000000..502d76b6 --- /dev/null +++ b/src/pipeline/wowee_pvp_ranks.cpp @@ -0,0 +1,336 @@ +#include "pipeline/wowee_pvp_ranks.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'P', 'R', 'G'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(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(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wprg") { + base += ".wprg"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweePvPRanks::Entry* +WoweePvPRanks::findById(uint32_t rankId) const { + for (const auto& e : entries) + if (e.rankId == rankId) return &e; + return nullptr; +} + +std::vector +WoweePvPRanks::findByFaction(uint8_t faction) const { + std::vector out; + for (const auto& e : entries) + if (e.factionFilter == faction) out.push_back(&e); + std::sort(out.begin(), out.end(), + [](const Entry* a, const Entry* b) { + return a->tier < b->tier; + }); + return out; +} + +const WoweePvPRanks::Entry* +WoweePvPRanks::findByTier(uint8_t faction, uint8_t tier) const { + for (const auto& e : entries) { + if (e.factionFilter == faction && e.tier == tier) + return &e; + } + return nullptr; +} + +bool WoweePvPRanksLoader::save(const WoweePvPRanks& 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(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.rankId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.factionFilter); + writePOD(os, e.tier); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.honorRequiredWeekly); + writePOD(os, e.honorRequiredAchieve); + writeStr(os, e.titlePrefix); + writePOD(os, e.gearItemId); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweePvPRanks WoweePvPRanksLoader::load( + const std::string& basePath) { + WoweePvPRanks 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.rankId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.factionFilter) || + !readPOD(is, e.tier) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.honorRequiredWeekly) || + !readPOD(is, e.honorRequiredAchieve)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.titlePrefix)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.gearItemId) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweePvPRanksLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweePvPRanks WoweePvPRanksLoader::makeAllianceRanks( + const std::string& catalogName) { + using P = WoweePvPRanks; + WoweePvPRanks c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t tier, uint32_t weekly, + uint32_t achieve, const char* title, + uint32_t gearId, const char* desc) { + P::Entry e; + e.rankId = id; e.name = name; e.description = desc; + e.factionFilter = P::AllianceOnly; + e.tier = tier; + e.honorRequiredWeekly = weekly; + e.honorRequiredAchieve = achieve; + e.titlePrefix = title; + e.gearItemId = gearId; + e.iconColorRGBA = packRgba(140, 200, 255); // alliance blue + c.entries.push_back(e); + }; + // Vanilla 1.x PvP rank thresholds (RP per week). + // Actual values are approximate — tuned for the + // exponential decay curve. + add(1, "Private", 1, 200, 0, "Private", + 0, + "Tier 1 — entry rank. Earn any honor in a " + "week to enter and progress."); + add(2, "Corporal", 2, 1000, 2000, "Corporal", + 0, + "Tier 2 — first promotion. 1000 RP/week to " + "maintain, 2000 lifetime to first-time " + "achieve."); + add(3, "Sergeant", 3, 2500, 5000, "Sergeant", + 0, + "Tier 3 — squad leader. Begin gaining minor " + "vendor discounts at this tier."); + add(4, "MasterSergeant", 4, 5000, 12000, "Master Sergeant", + 18837, + "Tier 4 — Master Sergeant. First gear unlock " + "(item 18837 — Knight's Pauldrons placeholder)."); + add(5, "SergeantMajor", 5, 9000, 25000, "Sergeant Major", + 18838, + "Tier 5 — Senior NCO rank. Tier 5 set piece " + "unlocks."); + add(6, "Knight", 6, 14000, 50000, "Knight", + 18839, + "Tier 6 — first officer rank. Knighthood " + "ceremony at Stormwind Cathedral."); + add(7, "KnightLieutenant", 7, 22000, 100000, "Knight-Lieutenant", + 18840, + "Tier 7 — Knight-Lieutenant. Lower-tier " + "officer; eligible for raid-officer hall access."); + return c; +} + +WoweePvPRanks WoweePvPRanksLoader::makeHordeRanks( + const std::string& catalogName) { + using P = WoweePvPRanks; + WoweePvPRanks c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t tier, uint32_t weekly, + uint32_t achieve, const char* title, + uint32_t gearId, const char* desc) { + P::Entry e; + e.rankId = id; e.name = name; e.description = desc; + e.factionFilter = P::HordeOnly; + e.tier = tier; + e.honorRequiredWeekly = weekly; + e.honorRequiredAchieve = achieve; + e.titlePrefix = title; + e.gearItemId = gearId; + e.iconColorRGBA = packRgba(220, 80, 80); // horde red + c.entries.push_back(e); + }; + // Mirrored Horde ladder — same honor thresholds, + // distinct titles. + add(100, "Scout", 1, 200, 0, "Scout", + 0, + "Tier 1 — Horde entry rank. Mirrors Alliance " + "Private."); + add(101, "Grunt", 2, 1000, 2000, "Grunt", + 0, + "Tier 2 — first promotion. Mirrors Corporal."); + add(102, "HordeSergeant", 3, 2500, 5000, "Sergeant", + 0, + "Tier 3 — squad leader. Same title as Alliance " + "Sergeant (factionFilter disambiguates)."); + add(103, "SeniorSergeant", 4, 5000, 12000, "Senior Sergeant", + 18857, + "Tier 4 — first gear unlock for Horde. " + "Mirrors Master Sergeant."); + add(104, "FirstSergeant", 5, 9000, 25000, "First Sergeant", + 18858, + "Tier 5 — Senior Horde NCO."); + add(105, "StoneGuard", 6, 14000, 50000, "Stone Guard", + 18859, + "Tier 6 — first Horde officer rank. Mirrors " + "Knight."); + add(106, "BloodGuard", 7, 22000, 100000, "Blood Guard", + 18860, + "Tier 7 — Lower-tier Horde officer."); + return c; +} + +WoweePvPRanks WoweePvPRanksLoader::makeHighRanks( + const std::string& catalogName) { + using P = WoweePvPRanks; + WoweePvPRanks c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t faction, uint8_t tier, + uint32_t weekly, uint32_t achieve, + const char* title, uint32_t gearId, + uint32_t color, const char* desc) { + P::Entry e; + e.rankId = id; e.name = name; e.description = desc; + e.factionFilter = faction; + e.tier = tier; + e.honorRequiredWeekly = weekly; + e.honorRequiredAchieve = achieve; + e.titlePrefix = title; + e.gearItemId = gearId; + e.iconColorRGBA = color; + c.entries.push_back(e); + }; + // Tiers 8-11. Tier 14 (Grand Marshal / High Warlord) + // is intentionally omitted — it's the legendary + // top-rank that historically required dedicated + // 24/7 grinding of months. Catalog can be extended. + add(200, "KnightCaptain", P::AllianceOnly, 8, 35000, + 200000, "Knight-Captain", 18841, + packRgba(140, 200, 255), + "Tier 8 Alliance — Knight-Captain. First high-" + "tier rank requiring dedicated effort."); + add(201, "KnightChampion", P::AllianceOnly, 9, 50000, + 400000, "Knight-Champion", 18842, + packRgba(140, 200, 255), + "Tier 9 Alliance — Knight-Champion. Mounts " + "and legendary battlegear shoulders unlock."); + add(202, "LtCommander", P::AllianceOnly, 10, 75000, + 650000, "Lieutenant Commander", 18843, + packRgba(140, 200, 255), + "Tier 10 Alliance — Lt. Commander. Unlocks " + "the Battlemaster's Aegis hall keys."); + add(203, "Commander", P::AllianceOnly, 11, 100000, + 1000000, "Commander", 18844, + packRgba(140, 200, 255), + "Tier 11 Alliance — Commander. Officer's " + "battlegear chestpiece unlock."); + add(210, "Legionnaire", P::HordeOnly, 8, 35000, + 200000, "Legionnaire", 18861, + packRgba(220, 80, 80), + "Tier 8 Horde — Legionnaire. Mirrors " + "Knight-Captain."); + add(211, "Centurion", P::HordeOnly, 9, 50000, + 400000, "Centurion", 18862, + packRgba(220, 80, 80), + "Tier 9 Horde — Centurion."); + add(212, "Champion", P::HordeOnly, 10, 75000, + 650000, "Champion", 18863, + packRgba(220, 80, 80), + "Tier 10 Horde — Champion."); + add(213, "LtCommanderHorde", P::HordeOnly, 11, 100000, + 1000000, "Lieutenant Commander", 18864, + packRgba(220, 80, 80), + "Tier 11 Horde — Lieutenant Commander. " + "Unlocks the Warlord's hall keys."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 42256abf..28ef2de0 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -374,6 +374,8 @@ const char* const kArgRequired[] = { "--gen-anv", "--gen-anv-bonus", "--gen-anv-launch", "--info-wanv", "--validate-wanv", "--export-wanv-json", "--import-wanv-json", + "--gen-prg", "--gen-prg-horde", "--gen-prg-high", + "--info-wprg", "--validate-wprg", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 59eaeb46..7de43d3d 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -166,6 +166,7 @@ #include "cli_sky_params_catalog.hpp" #include "cli_server_config_catalog.hpp" #include "cli_anniversary_events_catalog.hpp" +#include "cli_pvp_ranks_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -377,6 +378,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSkyParamsCatalog, handleServerConfigCatalog, handleAnniversaryEventsCatalog, + handlePvPRanksCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index b2791172..75849832 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -124,6 +124,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','K','P'}, ".wskp", "world", "--info-wskp", "Sky parameters catalog"}, {{'W','C','F','G'}, ".wcfg", "server", "--info-wcfg", "Server config catalog"}, {{'W','A','N','V'}, ".wanv", "events", "--info-wanv", "Anniversary & recurring event catalog"}, + {{'W','P','R','G'}, ".wprg", "pvp", "--info-wprg", "PvP ranking grades 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"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 07448ea6..16e04f7f 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2461,6 +2461,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wanv to a human-editable JSON sidecar (defaults to .wanv.json; emits both eventKind and recurrenceKind as int + name string)\n"); std::printf(" --import-wanv-json [out-base]\n"); std::printf(" Import a .wanv.json sidecar back into binary .wanv (eventKind int OR \"holiday\"/\"anniversary\"/\"doublexp\"/\"doublehonor\"/\"petbattle\"/\"bgbonus\"/\"seasonalquest\"/\"misc\"; recurrenceKind int OR \"yearly\"/\"monthly\"/\"weekly\"/\"oneoff\")\n"); + std::printf(" --gen-prg [name]\n"); + std::printf(" Emit .wprg 7 lower-tier Alliance PvP ranks (Private through Knight-Lieutenant) with weekly/lifetime honor thresholds and tier-set gear bindings\n"); + std::printf(" --gen-prg-horde [name]\n"); + std::printf(" Emit .wprg 7 lower-tier Horde PvP ranks (Scout through Blood Guard) mirroring Alliance honor thresholds with distinct titles\n"); + std::printf(" --gen-prg-high [name]\n"); + std::printf(" Emit .wprg 8 high-tier ranks (Alliance Knight-Captain through Commander, Horde Legionnaire through Lt. Commander — tiers 8-11)\n"); + std::printf(" --info-wprg [--json]\n"); + std::printf(" Print WPRG entries (id / faction / tier / weekly RP / lifetime RP / gear / title / name)\n"); + std::printf(" --validate-wprg [--json]\n"); + std::printf(" Static checks: id+name required, factionFilter 1=Alliance OR 2=Horde, tier 1..14 (vanilla ladder), no duplicate rankIds, no two ranks at same (faction, tier) tuple (runtime lookup tie); warns on empty titlePrefix, per-faction honor threshold non-monotonic (higher tier should require more honor)\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 6c2aa118..e06820f2 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -146,6 +146,7 @@ constexpr FormatRow kFormats[] = { {"WSKP", ".wskp", "world", "LightParams.dbc + Light.dbc diurnal","Sky parameters catalog (per-zone keyframes)"}, {"WCFG", ".wcfg", "server", "worldserver.conf flat-text config", "Server config catalog (polymorphic Float/Int/Bool/String values)"}, {"WANV", ".wanv", "events", "GameEvent SQL + per-holiday script", "Anniversary & recurring event catalog (cron-like scheduling)"}, + {"WPRG", ".wprg", "pvp", "vanilla 14-rank PvP ladder ladder", "PvP ranking grades catalog (faction + tier + honor thresholds)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_pvp_ranks_catalog.cpp b/tools/editor/cli_pvp_ranks_catalog.cpp new file mode 100644 index 00000000..bd74a513 --- /dev/null +++ b/tools/editor/cli_pvp_ranks_catalog.cpp @@ -0,0 +1,295 @@ +#include "cli_pvp_ranks_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_pvp_ranks.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set 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 factionTierSeen; + auto factionTierKey = [](uint8_t faction, uint8_t tier) { + return static_cast( + (static_cast(faction) << 8) | tier); + }; + // Per-faction monotonicity: honorRequiredAchieve + // should be non-decreasing as tier increases. + std::map> + 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; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_pvp_ranks_catalog.hpp b/tools/editor/cli_pvp_ranks_catalog.hpp new file mode 100644 index 00000000..1475a43e --- /dev/null +++ b/tools/editor/cli_pvp_ranks_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handlePvPRanksCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee