diff --git a/CMakeLists.txt b/CMakeLists.txt index 104aa5fb..79b3d880 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -648,6 +648,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_lfg.cpp src/pipeline/wowee_macros.cpp src/pipeline/wowee_char_features.cpp + src/pipeline/wowee_pvp.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1450,6 +1451,7 @@ add_executable(wowee_editor tools/editor/cli_catalog_grep.cpp tools/editor/cli_macros_catalog.cpp tools/editor/cli_char_features_catalog.cpp + tools/editor/cli_pvp_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1576,6 +1578,7 @@ add_executable(wowee_editor src/pipeline/wowee_lfg.cpp src/pipeline/wowee_macros.cpp src/pipeline/wowee_char_features.cpp + src/pipeline/wowee_pvp.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_pvp.hpp b/include/pipeline/wowee_pvp.hpp new file mode 100644 index 00000000..a8430512 --- /dev/null +++ b/include/pipeline/wowee_pvp.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open PvP Honor / Rank catalog (.wpvp) — novel +// replacement for the AzerothCore-style PvP rank / +// arena-tier tables plus the vanilla honor-rank reward +// chains. The 60th open format added to the editor. +// +// Defines PvP progression rungs: vanilla honor ranks +// (Private through Grand Marshal / High Warlord), arena +// rating brackets (Combatant / Challenger / Rival / +// Duelist / Gladiator), and battleground rated tiers. +// Each entry has alliance / horde alternate names, an +// honor or rating threshold, an optional title award +// (cross-ref WTTL), and gear cross-refs (chest, gloves, +// shoulders) into WIT for the matching PvP set. +// +// Cross-references with previously-added formats: +// WPVP.entry.titleId → WTTL.titleId +// WPVP.entry.chestItemId → WIT.itemId +// WPVP.entry.glovesItemId → WIT.itemId +// WPVP.entry.shouldersItemId→ WIT.itemId +// WPVP.entry.bracketBgId → WBGD.bgId (battleground- +// bracket gating) +// +// Binary layout (little-endian): +// magic[4] = "WPVP" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// rankId (uint32) +// nameLen + name +// allyLen + factionAllianceName +// hordeLen + factionHordeName +// descLen + description +// rankKind (uint8) / minBracketLevel (uint8) / +// maxBracketLevel (uint8) / pad[1] +// minHonorOrRating (uint32) +// rewardEmblems (uint16) / pad[2] +// titleId (uint32) +// chestItemId (uint32) +// glovesItemId (uint32) +// shouldersItemId (uint32) +// bracketBgId (uint32) +struct WoweePVPRank { + enum RankKind : uint8_t { + VanillaHonor = 0, // Private / Knight / etc — uses + // minHonor (kill points) + ArenaRating = 1, // 1500-2400+ rating-based + BattlegroundRated = 2, // 10v10 rated BG bracket + WorldPvP = 3, // Wintergrasp / Tol Barad rank + ConquestPoint = 4, // currency-based threshold + }; + + struct Entry { + uint32_t rankId = 0; + std::string name; + std::string factionAllianceName; // alt name for Alliance + std::string factionHordeName; // alt name for Horde + std::string description; + uint8_t rankKind = VanillaHonor; + uint8_t minBracketLevel = 1; // for level bracket gating + uint8_t maxBracketLevel = 80; + uint32_t minHonorOrRating = 0; // kills or rating points + uint16_t rewardEmblems = 0; // bonus emblem currency + uint32_t titleId = 0; // WTTL cross-ref + uint32_t chestItemId = 0; // WIT cross-ref + uint32_t glovesItemId = 0; // WIT cross-ref + uint32_t shouldersItemId = 0; // WIT cross-ref + uint32_t bracketBgId = 0; // WBGD cross-ref (optional) + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t rankId) const; + + static const char* rankKindName(uint8_t k); +}; + +class WoweePVPRankLoader { +public: + static bool save(const WoweePVPRank& cat, + const std::string& basePath); + static WoweePVPRank load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-pvp* variants. + // + // makeStarter — 3 vanilla honor entry tiers + // (Private/Knight, Sergeant/Stone + // Guard, Knight-Lieutenant/Blood + // Guard) showing the alliance-vs- + // horde alternate-name pattern. + // makeAllianceFull — 7 alliance vanilla ranks (R6-R14: + // Knight-Captain through Grand + // Marshal) with chest/gloves/ + // shoulders cross-refs into WIT. + // makeArenaTiers — 5 arena rating brackets + // (Combatant 1500 / Challenger 1750 + // / Rival 2000 / Duelist 2200 / + // Gladiator 2400) with title + + // emblem rewards. + static WoweePVPRank makeStarter(const std::string& catalogName); + static WoweePVPRank makeAllianceFull(const std::string& catalogName); + static WoweePVPRank makeArenaTiers(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_pvp.cpp b/src/pipeline/wowee_pvp.cpp new file mode 100644 index 00000000..c9954452 --- /dev/null +++ b/src/pipeline/wowee_pvp.cpp @@ -0,0 +1,274 @@ +#include "pipeline/wowee_pvp.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'P', 'V', 'P'}; +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) != ".wpvp") { + base += ".wpvp"; + } + return base; +} + +} // namespace + +const WoweePVPRank::Entry* +WoweePVPRank::findById(uint32_t rankId) const { + for (const auto& e : entries) if (e.rankId == rankId) return &e; + return nullptr; +} + +const char* WoweePVPRank::rankKindName(uint8_t k) { + switch (k) { + case VanillaHonor: return "vanilla-honor"; + case ArenaRating: return "arena"; + case BattlegroundRated: return "rated-bg"; + case WorldPvP: return "world-pvp"; + case ConquestPoint: return "conquest"; + default: return "unknown"; + } +} + +bool WoweePVPRankLoader::save(const WoweePVPRank& 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.factionAllianceName); + writeStr(os, e.factionHordeName); + writeStr(os, e.description); + writePOD(os, e.rankKind); + writePOD(os, e.minBracketLevel); + writePOD(os, e.maxBracketLevel); + uint8_t pad = 0; + writePOD(os, pad); + writePOD(os, e.minHonorOrRating); + writePOD(os, e.rewardEmblems); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, e.titleId); + writePOD(os, e.chestItemId); + writePOD(os, e.glovesItemId); + writePOD(os, e.shouldersItemId); + writePOD(os, e.bracketBgId); + } + return os.good(); +} + +WoweePVPRank WoweePVPRankLoader::load(const std::string& basePath) { + WoweePVPRank 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.factionAllianceName) || + !readStr(is, e.factionHordeName) || + !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.rankKind) || + !readPOD(is, e.minBracketLevel) || + !readPOD(is, e.maxBracketLevel)) { + out.entries.clear(); return out; + } + uint8_t pad = 0; + if (!readPOD(is, pad)) { out.entries.clear(); return out; } + if (!readPOD(is, e.minHonorOrRating) || + !readPOD(is, e.rewardEmblems)) { + out.entries.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.entries.clear(); return out; } + if (!readPOD(is, e.titleId) || + !readPOD(is, e.chestItemId) || + !readPOD(is, e.glovesItemId) || + !readPOD(is, e.shouldersItemId) || + !readPOD(is, e.bracketBgId)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweePVPRankLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweePVPRank WoweePVPRankLoader::makeStarter( + const std::string& catalogName) { + WoweePVPRank c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, const char* ally, + const char* horde, uint32_t honor, + const char* desc) { + WoweePVPRank::Entry e; + e.rankId = id; e.name = name; + e.factionAllianceName = ally; + e.factionHordeName = horde; + e.description = desc; + e.rankKind = WoweePVPRank::VanillaHonor; + e.minHonorOrRating = honor; + c.entries.push_back(e); + }; + add(1, "Rank2", "Private", "Scout", + 2000, "Vanilla rank 2 — first PvP title."); + add(2, "Rank3", "Corporal", "Grunt", + 5000, "Vanilla rank 3."); + add(3, "Rank4", "Sergeant", "Sergeant", + 10000, + "Vanilla rank 4 — same name on both factions."); + return c; +} + +WoweePVPRank WoweePVPRankLoader::makeAllianceFull( + const std::string& catalogName) { + WoweePVPRank c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* ally, const char* horde, + uint32_t honor, uint32_t titleId, + uint32_t chest, uint32_t gloves, uint32_t shoulders, + const char* desc) { + WoweePVPRank::Entry e; + e.rankId = id; + e.name = std::string("Rank") + std::to_string(id); + e.factionAllianceName = ally; + e.factionHordeName = horde; + e.description = desc; + e.rankKind = WoweePVPRank::VanillaHonor; + e.minHonorOrRating = honor; + e.titleId = titleId; + e.chestItemId = chest; + e.glovesItemId = gloves; + e.shouldersItemId = shoulders; + c.entries.push_back(e); + }; + // Vanilla ranks 6-14 with WTTL + WIT cross-refs. Honor + // values ramp exponentially toward the Grand Marshal cap. + add(6, "Knight", "Stone Guard", + 50000, 3, 16462, 16472, 16482, + "Rank 6 — first epic gear unlock."); + add(7, "Knight-Lieutenant", "Blood Guard", + 70000, 4, 16463, 16473, 16483, + "Rank 7."); + add(8, "Knight-Captain", "Legionnaire", + 90000, 5, 16464, 16474, 16484, + "Rank 8."); + add(9, "Knight-Champion", "Centurion", + 110000, 6, 16465, 16475, 16485, + "Rank 9."); + add(10, "Lieutenant Commander", "Champion", + 130000, 7, 16466, 16476, 16486, + "Rank 10."); + add(11, "Commander", "Lieutenant General", + 160000, 8, 16467, 16477, 16487, + "Rank 11."); + add(12, "Marshal", "General", + 190000, 9, 16468, 16478, 16488, + "Rank 12."); + add(13, "Field Marshal", "Warlord", + 220000, 10, 16469, 16479, 16489, + "Rank 13."); + add(14, "Grand Marshal", "High Warlord", + 260000, 11, 16470, 16480, 16490, + "Rank 14 — pinnacle. 'Grand Marshal' / 'High Warlord' " + "title + full epic PvP set."); + return c; +} + +WoweePVPRank WoweePVPRankLoader::makeArenaTiers( + const std::string& catalogName) { + WoweePVPRank c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t rating, + uint16_t emblems, uint32_t titleId, + const char* desc) { + WoweePVPRank::Entry e; + e.rankId = id; e.name = name; + e.factionAllianceName = name; // arena names match + e.factionHordeName = name; + e.description = desc; + e.rankKind = WoweePVPRank::ArenaRating; + e.minHonorOrRating = rating; + e.rewardEmblems = emblems; + e.titleId = titleId; + e.minBracketLevel = 80; e.maxBracketLevel = 80; + c.entries.push_back(e); + }; + add(100, "Combatant", 1500, 10, 0, + "Arena bracket — minimum entry rating."); + add(101, "Challenger", 1750, 20, 44, + "Arena bracket — 'Challenger' title earned."); + add(102, "Rival", 2000, 40, 45, + "Arena bracket — 'Rival' title earned."); + add(103, "Duelist", 2200, 80, 46, + "Arena bracket — 'Duelist' title earned."); + add(104, "Gladiator", 2400, 160, 47, + "Arena bracket — 'Gladiator' title + season mount."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 0901d627..d92f0f52 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -182,6 +182,8 @@ const char* const kArgRequired[] = { "--gen-chf", "--gen-chf-bloodelf", "--gen-chf-tauren", "--info-wchf", "--validate-wchf", "--export-wchf-json", "--import-wchf-json", + "--gen-pvp", "--gen-pvp-alliance", "--gen-pvp-arena", + "--info-wpvp", "--validate-wpvp", "--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 82dd21c0..1ca09821 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -95,6 +95,7 @@ #include "cli_catalog_grep.hpp" #include "cli_macros_catalog.hpp" #include "cli_char_features_catalog.hpp" +#include "cli_pvp_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -231,6 +232,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCatalogGrep, handleMacrosCatalog, handleCharFeaturesCatalog, + handlePVPCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 2629ea5f..822c7aff 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -62,6 +62,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','L','F','G'}, ".wlfg", "social", "--info-wlfg", "LFG / Dungeon Finder catalog"}, {{'W','M','A','C'}, ".wmac", "ui", "--info-wmac", "Macro / slash command catalog"}, {{'W','C','H','F'}, ".wchf", "chars", "--info-wchf", "Character hair / face customization catalog"}, + {{'W','P','V','P'}, ".wpvp", "pvp", "--info-wpvp", "PvP honor rank + arena tier 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 202cffeb..dc355533 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1573,6 +1573,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wchf to a human-editable JSON sidecar (defaults to .wchf.json)\n"); std::printf(" --import-wchf-json [out-base]\n"); std::printf(" Import a .wchf.json sidecar back into binary .wchf (accepts featureKind/sexId/requiresExpansion int OR name string)\n"); + std::printf(" --gen-pvp [name]\n"); + std::printf(" Emit .wpvp starter: 3 vanilla honor entry tiers (Rank2-4 — Private/Scout, Corporal/Grunt, Sergeant)\n"); + std::printf(" --gen-pvp-alliance [name]\n"); + std::printf(" Emit .wpvp 9 vanilla ranks 6-14 (Knight through Grand Marshal) with WTTL+WIT cross-refs for chest/gloves/shoulders\n"); + std::printf(" --gen-pvp-arena [name]\n"); + std::printf(" Emit .wpvp 5 arena rating brackets (Combatant 1500 / Challenger 1750 / Rival 2000 / Duelist 2200 / Gladiator 2400)\n"); + std::printf(" --info-wpvp [--json]\n"); + std::printf(" Print WPVP entries (id / kind / threshold / emblem reward / title+chest cross-refs / alliance / horde names)\n"); + std::printf(" --validate-wpvp [--json]\n"); + std::printf(" Static checks: id+name required, kind 0..4, level range valid, faction alt names paired, threshold monotonic within kind, arena>=1500\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 7684f742..2c71e1ea 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -84,6 +84,7 @@ constexpr FormatRow kFormats[] = { {"WLFG", ".wlfg", "social", "LFGDungeons.dbc + LFG rewards", "LFG / Dungeon Finder catalog"}, {"WMAC", ".wmac", "ui", "(client-side macro storage)", "Macro / slash command catalog"}, {"WCHF", ".wchf", "chars", "CharHairGeosets + CharFacialHair", "Character hair / face customization catalog"}, + {"WPVP", ".wpvp", "pvp", "honor / arena rank tables", "PvP honor rank + arena tier catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_pvp_catalog.cpp b/tools/editor/cli_pvp_catalog.cpp new file mode 100644 index 00000000..7040ddca --- /dev/null +++ b/tools/editor/cli_pvp_catalog.cpp @@ -0,0 +1,279 @@ +#include "cli_pvp_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_pvp.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWpvpExt(std::string base) { + stripExt(base, ".wpvp"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweePVPRank& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweePVPRankLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wpvp\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweePVPRank& c, + const std::string& base) { + std::printf("Wrote %s.wpvp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" ranks : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterPvPRanks"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpvpExt(base); + auto c = wowee::pipeline::WoweePVPRankLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-pvp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAllianceFull(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AllianceVanillaRanks"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpvpExt(base); + auto c = wowee::pipeline::WoweePVPRankLoader::makeAllianceFull(name); + if (!saveOrError(c, base, "gen-pvp-alliance")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenArena(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ArenaTiers"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpvpExt(base); + auto c = wowee::pipeline::WoweePVPRankLoader::makeArenaTiers(name); + if (!saveOrError(c, base, "gen-pvp-arena")) 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 = stripWpvpExt(base); + if (!wowee::pipeline::WoweePVPRankLoader::exists(base)) { + std::fprintf(stderr, "WPVP not found: %s.wpvp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePVPRankLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wpvp"] = base + ".wpvp"; + 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}, + {"factionAllianceName", e.factionAllianceName}, + {"factionHordeName", e.factionHordeName}, + {"description", e.description}, + {"rankKind", e.rankKind}, + {"rankKindName", wowee::pipeline::WoweePVPRank::rankKindName(e.rankKind)}, + {"minBracketLevel", e.minBracketLevel}, + {"maxBracketLevel", e.maxBracketLevel}, + {"minHonorOrRating", e.minHonorOrRating}, + {"rewardEmblems", e.rewardEmblems}, + {"titleId", e.titleId}, + {"chestItemId", e.chestItemId}, + {"glovesItemId", e.glovesItemId}, + {"shouldersItemId", e.shouldersItemId}, + {"bracketBgId", e.bracketBgId}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WPVP: %s.wpvp\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 kind threshold emblem title chest alliance / horde \n"); + for (const auto& e : c.entries) { + std::string both = e.factionAllianceName + + (e.factionAllianceName != e.factionHordeName + ? std::string(" / ") + e.factionHordeName + : std::string()); + std::printf(" %4u %-15s %8u %3u %3u %5u %s\n", + e.rankId, + wowee::pipeline::WoweePVPRank::rankKindName(e.rankKind), + e.minHonorOrRating, e.rewardEmblems, + e.titleId, e.chestItemId, both.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWpvpExt(base); + if (!wowee::pipeline::WoweePVPRankLoader::exists(base)) { + std::fprintf(stderr, + "validate-wpvp: WPVP not found: %s.wpvp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePVPRankLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + // Track threshold monotonicity within a single rankKind — + // arena ratings should ascend (1500 < 1750 < ...), so a + // catalog with two arena entries at the same rating or + // a higher-id entry below a lower-id entry is suspicious. + uint32_t prevHonorByKind[5] = {0, 0, 0, 0, 0}; + bool prevHonorSeen[5] = {false, false, false, false, false}; + 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.rankKind > wowee::pipeline::WoweePVPRank::ConquestPoint) { + errors.push_back(ctx + ": rankKind " + + std::to_string(e.rankKind) + " not in 0..4"); + } + if (e.minBracketLevel > e.maxBracketLevel) { + errors.push_back(ctx + ": minBracketLevel " + + std::to_string(e.minBracketLevel) + + " > maxBracketLevel " + + std::to_string(e.maxBracketLevel)); + } + // Vanilla honor ranks must use VanillaHonor kind and + // have minHonor > 0 (rank 1 is the implicit baseline). + if (e.rankKind == wowee::pipeline::WoweePVPRank::VanillaHonor && + e.minHonorOrRating == 0) { + warnings.push_back(ctx + + ": VanillaHonor kind with minHonor=0 " + "(rank 1 baseline — verify intentional)"); + } + // Arena ratings below 1500 don't unlock any reward — + // 1500 is the WoW arena floor. + if (e.rankKind == wowee::pipeline::WoweePVPRank::ArenaRating && + e.minHonorOrRating < 1500) { + warnings.push_back(ctx + + ": ArenaRating with rating " + + std::to_string(e.minHonorOrRating) + + " below 1500 floor"); + } + // Faction alternate names — vanilla ranks have + // distinct alliance / horde names; arena tiers share + // the same name on both factions. Either is valid; an + // empty alliance name + non-empty horde (or vice + // versa) is a typo signal. + if (e.factionAllianceName.empty() != + e.factionHordeName.empty()) { + warnings.push_back(ctx + + ": only one faction-alternate name set " + "(alliance='" + e.factionAllianceName + + "', horde='" + e.factionHordeName + "')"); + } + // Threshold monotonicity within rankKind. + if (e.rankKind < 5) { + if (prevHonorSeen[e.rankKind] && + e.minHonorOrRating < prevHonorByKind[e.rankKind]) { + warnings.push_back(ctx + + ": threshold " + + std::to_string(e.minHonorOrRating) + + " below previous " + + std::to_string(prevHonorByKind[e.rankKind]) + + " in same rankKind (non-monotonic)"); + } + prevHonorByKind[e.rankKind] = e.minHonorOrRating; + prevHonorSeen[e.rankKind] = true; + } + for (uint32_t prev : idsSeen) { + if (prev == e.rankId) { + errors.push_back(ctx + ": duplicate rankId"); + break; + } + } + idsSeen.push_back(e.rankId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wpvp"] = base + ".wpvp"; + 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-wpvp: %s.wpvp\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu ranks, all rankIds unique, all thresholds monotonic\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 handlePVPCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-pvp") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-pvp-alliance") == 0 && i + 1 < argc) { + outRc = handleGenAllianceFull(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-pvp-arena") == 0 && i + 1 < argc) { + outRc = handleGenArena(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wpvp") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wpvp") == 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_catalog.hpp b/tools/editor/cli_pvp_catalog.hpp new file mode 100644 index 00000000..5067c131 --- /dev/null +++ b/tools/editor/cli_pvp_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handlePVPCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee