From 4ce07d5ca948f6a53cee69852eb6f7e8f05aff24 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 03:08:27 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WPRG=20(PvP=20Ranking=20g?= =?UTF-8?q?rades)=20=E2=80=94=20122nd=20open=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CMakeLists.txt | 3 + include/pipeline/wowee_pvp_ranks.hpp | 121 +++++++++ src/pipeline/wowee_pvp_ranks.cpp | 336 +++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + tools/editor/cli_pvp_ranks_catalog.cpp | 295 ++++++++++++++++++++++ tools/editor/cli_pvp_ranks_catalog.hpp | 12 + 10 files changed, 783 insertions(+) create mode 100644 include/pipeline/wowee_pvp_ranks.hpp create mode 100644 src/pipeline/wowee_pvp_ranks.cpp create mode 100644 tools/editor/cli_pvp_ranks_catalog.cpp create mode 100644 tools/editor/cli_pvp_ranks_catalog.hpp 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