From a10497a026897a7873693f1b83c7f35f28ca6a60 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 05:24:26 -0700 Subject: [PATCH] feat(pipeline): WBRD battleground reward stages catalog (140th open format) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel replacement for the per-BG per-bracket reward configuration vanilla WoW carried in BattlemasterList.dbc + the hard-coded honor table in the server's BattlegroundMgr (the "Mark of Honor" item granted on win/loss was hard-coded per-BG type with no formal data-driven scaling). Each WBRD entry binds one (battlegroundId, levelBracket) pair to its win/loss honor amounts, win/loss marks, the mark itemId, an optional weekly-bonus item, and a minimum-players-to-start gate. Three presets covering canonical vanilla BGs: --gen-brd-av Alterac Valley reward ladder for brackets 5-6 (51-69 only — AV was endgame). 20 players/side, Mark of AV item 17502 --gen-brd-wsg Warsong Gulch ladder for all 6 brackets (10-69), 10 players/side, Mark of WSG item 20558, monotonically scaling honor (50/100/200/350/500/750) --gen-brd-ab Arathi Basin ladder for brackets 2-6 (20-69), 15 players/side, Mark of AB item 20559, includes weekly bonus quest token (placeholder itemId 20560) Validator catches: id+battlegroundId required, bracketIndex 1..6 (vanilla), no duplicate rewardIds, no duplicate (bgId,bracket) pairs (runtime reward-lookup tie), bonusItemCount > 0 requires non-zero bonusItemId, minPlayers ToStart > 0 (else BG queue would never start a match). CRITICAL: lossHonor <= winHonor (else losing rewards more than winning, no incentive to play to win — would degenerate into AFK farming). Warns on winMarks=0 with markItemId set (vanilla wins always granted at least 1 mark). Format count 139 -> 140. CLI flag count 1454 -> 1461. --- CMakeLists.txt | 3 + .../pipeline/wowee_battleground_rewards.hpp | 128 ++++++++ src/pipeline/wowee_battleground_rewards.cpp | 245 +++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + .../cli_battleground_rewards_catalog.cpp | 297 ++++++++++++++++++ .../cli_battleground_rewards_catalog.hpp | 12 + 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 + 10 files changed, 701 insertions(+) create mode 100644 include/pipeline/wowee_battleground_rewards.hpp create mode 100644 src/pipeline/wowee_battleground_rewards.cpp create mode 100644 tools/editor/cli_battleground_rewards_catalog.cpp create mode 100644 tools/editor/cli_battleground_rewards_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dc3d7209..604dcd99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -728,6 +728,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_random_property.cpp src/pipeline/wowee_spell_proc_rules.cpp src/pipeline/wowee_auction_houses.cpp + src/pipeline/wowee_battleground_rewards.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1619,6 +1620,7 @@ add_executable(wowee_editor tools/editor/cli_random_property_catalog.cpp tools/editor/cli_spell_proc_rules_catalog.cpp tools/editor/cli_auction_houses_catalog.cpp + tools/editor/cli_battleground_rewards_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1829,6 +1831,7 @@ add_executable(wowee_editor src/pipeline/wowee_random_property.cpp src/pipeline/wowee_spell_proc_rules.cpp src/pipeline/wowee_auction_houses.cpp + src/pipeline/wowee_battleground_rewards.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_battleground_rewards.hpp b/include/pipeline/wowee_battleground_rewards.hpp new file mode 100644 index 00000000..56e7579e --- /dev/null +++ b/include/pipeline/wowee_battleground_rewards.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Battleground Reward Stages catalog +// (.wbrd) — novel replacement for the per-BG per- +// bracket reward configuration vanilla WoW carried +// in BattlemasterList.dbc + the hard-coded honor +// table in the server's BattlegroundMgr (the +// "Mark of Honor" item granted on win/loss was +// hard-coded per-BG type with no formal data- +// driven scaling). Each WBRD entry binds one +// (battlegroundId, levelBracket) pair to its win/ +// loss honor amounts, win/loss marks, the mark +// itemId, an optional weekly-bonus item, and a +// minimum-players-to-start gate. +// +// Cross-references with previously-added formats: +// WBGD: battlegroundId references the existing +// WBGD battleground catalog (1=AV, 2=WSG, +// 3=AB). +// WIT: markItemId and bonusItemId both reference +// the WIT item catalog (Mark of Honor IDs +// are 17502 AV / 20558 WSG / 20559 AB). +// +// Binary layout (little-endian): +// magic[4] = "WBRD" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// rewardId (uint32) +// battlegroundId (uint16) — WBGD ref +// (1=AV/2=WSG/ +// 3=AB) +// bracketIndex (uint8) — 1=10-19 / +// 2=20-29 / ... +// / 6=60-69 +// minPlayersToStart (uint8) — AV=20 / WSG=10 +// / AB=15 etc. +// winHonor (uint32) +// lossHonor (uint32) +// markItemId (uint32) — WIT ref +// winMarks (uint16) +// lossMarks (uint16) +// bonusItemId (uint32) — weekly bonus +// quest token +// (0 = no bonus) +// bonusItemCount (uint16) +// pad0 (uint16) +struct WoweeBattlegroundRewards { + static constexpr uint8_t kMaxBracketIndex = 6; + + struct Entry { + uint32_t rewardId = 0; + uint16_t battlegroundId = 0; + uint8_t bracketIndex = 0; + uint8_t minPlayersToStart = 0; + uint32_t winHonor = 0; + uint32_t lossHonor = 0; + uint32_t markItemId = 0; + uint16_t winMarks = 0; + uint16_t lossMarks = 0; + uint32_t bonusItemId = 0; + uint16_t bonusItemCount = 0; + uint16_t pad0 = 0; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t rewardId) const; + + // Resolve the reward stage for a (BG, level + // bracket) pair — the canonical lookup the + // post-match handler uses to credit honor + + // marks to each participant. + const Entry* find(uint16_t bgId, + uint8_t bracketIndex) const; + + // Returns all reward entries for a single + // battleground — used by the BG queue UI to + // show per-bracket reward previews. + std::vector findByBg(uint16_t bgId) const; +}; + +class WoweeBattlegroundRewardsLoader { +public: + static bool save(const WoweeBattlegroundRewards& cat, + const std::string& basePath); + static WoweeBattlegroundRewards load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-brd* variants. + // + // makeAlteracValley — AV (bgId=1) reward + // ladder for brackets 5 + // and 6 (51-60 / 61-69). + // AV requires 20 players + // per side. High honor + + // Mark of AV (itemId + // 17502). + // makeWarsong — WSG (bgId=2) reward + // ladder for brackets + // 1..6 (10-19 through + // 61-69). WSG min 10 + // players. Mark of WSG + // (20558). + // makeArathiBasin — AB (bgId=3) reward + // ladder for brackets 2..6 + // (20-29 through 61-69). + // Min 15 players. Mark of + // AB (20559) + weekly + // bonus quest token. + static WoweeBattlegroundRewards makeAlteracValley(const std::string& catalogName); + static WoweeBattlegroundRewards makeWarsong(const std::string& catalogName); + static WoweeBattlegroundRewards makeArathiBasin(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_battleground_rewards.cpp b/src/pipeline/wowee_battleground_rewards.cpp new file mode 100644 index 00000000..6cd50fde --- /dev/null +++ b/src/pipeline/wowee_battleground_rewards.cpp @@ -0,0 +1,245 @@ +#include "pipeline/wowee_battleground_rewards.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'B', 'R', 'D'}; +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) != ".wbrd") { + base += ".wbrd"; + } + return base; +} + +} // namespace + +const WoweeBattlegroundRewards::Entry* +WoweeBattlegroundRewards::findById(uint32_t rewardId) const { + for (const auto& e : entries) + if (e.rewardId == rewardId) return &e; + return nullptr; +} + +const WoweeBattlegroundRewards::Entry* +WoweeBattlegroundRewards::find(uint16_t bgId, + uint8_t bracketIndex) const { + for (const auto& e : entries) { + if (e.battlegroundId == bgId && + e.bracketIndex == bracketIndex) + return &e; + } + return nullptr; +} + +std::vector +WoweeBattlegroundRewards::findByBg(uint16_t bgId) const { + std::vector out; + for (const auto& e : entries) + if (e.battlegroundId == bgId) out.push_back(&e); + return out; +} + +bool WoweeBattlegroundRewardsLoader::save( + const WoweeBattlegroundRewards& 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.rewardId); + writePOD(os, e.battlegroundId); + writePOD(os, e.bracketIndex); + writePOD(os, e.minPlayersToStart); + writePOD(os, e.winHonor); + writePOD(os, e.lossHonor); + writePOD(os, e.markItemId); + writePOD(os, e.winMarks); + writePOD(os, e.lossMarks); + writePOD(os, e.bonusItemId); + writePOD(os, e.bonusItemCount); + writePOD(os, e.pad0); + } + return os.good(); +} + +WoweeBattlegroundRewards WoweeBattlegroundRewardsLoader::load( + const std::string& basePath) { + WoweeBattlegroundRewards 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.rewardId) || + !readPOD(is, e.battlegroundId) || + !readPOD(is, e.bracketIndex) || + !readPOD(is, e.minPlayersToStart) || + !readPOD(is, e.winHonor) || + !readPOD(is, e.lossHonor) || + !readPOD(is, e.markItemId) || + !readPOD(is, e.winMarks) || + !readPOD(is, e.lossMarks) || + !readPOD(is, e.bonusItemId) || + !readPOD(is, e.bonusItemCount) || + !readPOD(is, e.pad0)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeBattlegroundRewardsLoader::exists( + const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +namespace { + +WoweeBattlegroundRewards::Entry makeStage( + uint32_t rewardId, uint16_t bgId, + uint8_t bracketIndex, uint8_t minPlayers, + uint32_t winHonor, uint32_t lossHonor, + uint32_t markItemId, + uint16_t winMarks, uint16_t lossMarks, + uint32_t bonusItemId = 0, + uint16_t bonusItemCount = 0) { + WoweeBattlegroundRewards::Entry e; + e.rewardId = rewardId; + e.battlegroundId = bgId; + e.bracketIndex = bracketIndex; + e.minPlayersToStart = minPlayers; + e.winHonor = winHonor; + e.lossHonor = lossHonor; + e.markItemId = markItemId; + e.winMarks = winMarks; + e.lossMarks = lossMarks; + e.bonusItemId = bonusItemId; + e.bonusItemCount = bonusItemCount; + return e; +} + +} // namespace + +WoweeBattlegroundRewards WoweeBattlegroundRewardsLoader::makeAlteracValley( + const std::string& catalogName) { + WoweeBattlegroundRewards c; + c.name = catalogName; + // AV bgId=1, requires 20/side. Brackets 5 + // (51-60) and 6 (61-69 — endgame). Mark of AV = + // itemId 17502. Win 3 marks / loss 1 mark + // baseline. + c.entries.push_back(makeStage( + 1, 1, 5, 20, + 750 /* winHonor */, 250 /* lossHonor */, + 17502 /* Mark of Honor: AV */, + 3, 1)); + c.entries.push_back(makeStage( + 2, 1, 6, 20, + 1000, 350, + 17502, + 3, 1)); + return c; +} + +WoweeBattlegroundRewards WoweeBattlegroundRewardsLoader::makeWarsong( + const std::string& catalogName) { + WoweeBattlegroundRewards c; + c.name = catalogName; + // WSG bgId=2, requires 10/side. All 6 brackets. + // Mark of WSG = itemId 20558. + c.entries.push_back(makeStage( + 10, 2, 1, 10, 50, 25, 20558, 1, 0)); + c.entries.push_back(makeStage( + 11, 2, 2, 10, 100, 50, 20558, 1, 1)); + c.entries.push_back(makeStage( + 12, 2, 3, 10, 200, 100, 20558, 2, 1)); + c.entries.push_back(makeStage( + 13, 2, 4, 10, 350, 175, 20558, 2, 1)); + c.entries.push_back(makeStage( + 14, 2, 5, 10, 500, 250, 20558, 3, 1)); + c.entries.push_back(makeStage( + 15, 2, 6, 10, 750, 375, 20558, 3, 1)); + return c; +} + +WoweeBattlegroundRewards WoweeBattlegroundRewardsLoader::makeArathiBasin( + const std::string& catalogName) { + WoweeBattlegroundRewards c; + c.name = catalogName; + // AB bgId=3, requires 15/side. Brackets 2..6. + // Mark of AB = itemId 20559. Weekly bonus quest + // turns in 3 marks for an extra Token of the + // Triumvirate (placeholder itemId 20560). + c.entries.push_back(makeStage( + 20, 3, 2, 15, 80, 40, 20559, 1, 0, + 20560 /* weekly bonus */, 1)); + c.entries.push_back(makeStage( + 21, 3, 3, 15, 175, 90, 20559, 2, 1, + 20560, 1)); + c.entries.push_back(makeStage( + 22, 3, 4, 15, 300, 150, 20559, 2, 1, + 20560, 1)); + c.entries.push_back(makeStage( + 23, 3, 5, 15, 450, 225, 20559, 3, 1, + 20560, 1)); + c.entries.push_back(makeStage( + 24, 3, 6, 15, 700, 350, 20559, 3, 1, + 20560, 1)); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 6e36a565..c47708de 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -428,6 +428,8 @@ const char* const kArgRequired[] = { "--gen-auh-stormwind", "--gen-auh-orgrimmar", "--gen-auh-bootybay", "--info-wauh", "--validate-wauh", "--export-wauh-json", "--import-wauh-json", + "--gen-brd-av", "--gen-brd-wsg", "--gen-brd-ab", + "--info-wbrd", "--validate-wbrd", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_battleground_rewards_catalog.cpp b/tools/editor/cli_battleground_rewards_catalog.cpp new file mode 100644 index 00000000..cc63309d --- /dev/null +++ b/tools/editor/cli_battleground_rewards_catalog.cpp @@ -0,0 +1,297 @@ +#include "cli_battleground_rewards_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_battleground_rewards.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWbrdExt(std::string base) { + stripExt(base, ".wbrd"); + return base; +} + +const char* bgName(uint16_t bgId) { + switch (bgId) { + case 1: return "AV"; + case 2: return "WSG"; + case 3: return "AB"; + default: return "?"; + } +} + +bool saveOrError( + const wowee::pipeline::WoweeBattlegroundRewards& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeBattlegroundRewardsLoader::save( + c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wbrd\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary( + const wowee::pipeline::WoweeBattlegroundRewards& c, + const std::string& base) { + std::printf("Wrote %s.wbrd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" stages : %zu\n", c.entries.size()); +} + +int handleGenAV(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AlteracValleyRewards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWbrdExt(base); + auto c = wowee::pipeline::WoweeBattlegroundRewardsLoader:: + makeAlteracValley(name); + if (!saveOrError(c, base, "gen-brd-av")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWSG(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WarsongRewards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWbrdExt(base); + auto c = wowee::pipeline::WoweeBattlegroundRewardsLoader:: + makeWarsong(name); + if (!saveOrError(c, base, "gen-brd-wsg")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAB(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ArathiBasinRewards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWbrdExt(base); + auto c = wowee::pipeline::WoweeBattlegroundRewardsLoader:: + makeArathiBasin(name); + if (!saveOrError(c, base, "gen-brd-ab")) 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 = stripWbrdExt(base); + if (!wowee::pipeline::WoweeBattlegroundRewardsLoader::exists(base)) { + std::fprintf(stderr, "WBRD not found: %s.wbrd\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeBattlegroundRewardsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wbrd"] = base + ".wbrd"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"rewardId", e.rewardId}, + {"battlegroundId", e.battlegroundId}, + {"battlegroundName", bgName(e.battlegroundId)}, + {"bracketIndex", e.bracketIndex}, + {"minPlayersToStart", e.minPlayersToStart}, + {"winHonor", e.winHonor}, + {"lossHonor", e.lossHonor}, + {"markItemId", e.markItemId}, + {"winMarks", e.winMarks}, + {"lossMarks", e.lossMarks}, + {"bonusItemId", e.bonusItemId}, + {"bonusItemCount", e.bonusItemCount}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WBRD: %s.wbrd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" stages : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id bg bracket minPl winHonor lossHonor markId winM lossM bonusId bonusN\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-3s %5u %3u %5u %5u %5u %2u %3u %5u %3u\n", + e.rewardId, bgName(e.battlegroundId), + e.bracketIndex, e.minPlayersToStart, + e.winHonor, e.lossHonor, + e.markItemId, e.winMarks, e.lossMarks, + e.bonusItemId, e.bonusItemCount); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWbrdExt(base); + if (!wowee::pipeline::WoweeBattlegroundRewardsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wbrd: WBRD not found: %s.wbrd\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeBattlegroundRewardsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + using B = wowee::pipeline::WoweeBattlegroundRewards; + std::set idsSeen; + using Pair = std::pair; + std::set bgBracketPairs; + 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.rewardId) + + " " + bgName(e.battlegroundId) + + " bracket=" + + std::to_string(e.bracketIndex) + ")"; + if (e.rewardId == 0) + errors.push_back(ctx + ": rewardId is 0"); + if (e.battlegroundId == 0) + errors.push_back(ctx + + ": battlegroundId is 0"); + if (e.bracketIndex == 0 || + e.bracketIndex > B::kMaxBracketIndex) { + errors.push_back(ctx + ": bracketIndex " + + std::to_string(e.bracketIndex) + + " out of range (1..6 vanilla)"); + } + // CRITICAL invariant: minPlayersToStart > 0, + // else BG queue would never start a match. + if (e.minPlayersToStart == 0) { + errors.push_back(ctx + + ": minPlayersToStart is 0 — BG queue " + "would never start a match"); + } + // Loss honor should be < win honor (winning + // should always be more rewarding than losing, + // else there's no incentive to play to win). + if (e.lossHonor > e.winHonor) { + errors.push_back(ctx + + ": lossHonor=" + + std::to_string(e.lossHonor) + + " > winHonor=" + + std::to_string(e.winHonor) + + " — losing rewards more than winning " + "(no win incentive)"); + } + // Mark = 0 on win is unusual — every BG win + // grants at least 1 mark in vanilla. Warn. + if (e.winMarks == 0 && e.markItemId != 0) { + warnings.push_back(ctx + + ": winMarks=0 but markItemId is set " + "— win grants no marks; verify " + "intentional (vanilla wins always " + "gave at least 1 mark)"); + } + // bonusItemCount > 0 with bonusItemId = 0 is + // a contradiction (count of nothing). + if (e.bonusItemCount > 0 && e.bonusItemId == 0) { + errors.push_back(ctx + + ": bonusItemCount=" + + std::to_string(e.bonusItemCount) + + " but bonusItemId is 0 — count of " + "nothing"); + } + // (battlegroundId, bracketIndex) MUST be + // unique — runtime dispatch by this pair + // would tie. + Pair p{e.battlegroundId, e.bracketIndex}; + if (!bgBracketPairs.insert(p).second) { + errors.push_back(ctx + + ": duplicate (bgId=" + + std::to_string(e.battlegroundId) + + ", bracket=" + + std::to_string(e.bracketIndex) + + ") — runtime reward-lookup tie"); + } + if (!idsSeen.insert(e.rewardId).second) { + errors.push_back(ctx + ": duplicate rewardId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wbrd"] = base + ".wbrd"; + 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-wbrd: %s.wbrd\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu stages, all rewardIds + " + "(bgId,bracket) pairs unique, " + "bracketIndex 1..6, minPlayersToStart " + "> 0, winHonor >= lossHonor, no " + "bonus-count without bonus-itemId\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 handleBattlegroundRewardsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-brd-av") == 0 && + i + 1 < argc) { + outRc = handleGenAV(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-brd-wsg") == 0 && + i + 1 < argc) { + outRc = handleGenWSG(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-brd-ab") == 0 && + i + 1 < argc) { + outRc = handleGenAB(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wbrd") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wbrd") == 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_battleground_rewards_catalog.hpp b/tools/editor/cli_battleground_rewards_catalog.hpp new file mode 100644 index 00000000..d6b3113c --- /dev/null +++ b/tools/editor/cli_battleground_rewards_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleBattlegroundRewardsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 7f1f0806..9e3ed084 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -184,6 +184,7 @@ #include "cli_random_property_catalog.hpp" #include "cli_spell_proc_rules_catalog.hpp" #include "cli_auction_houses_catalog.hpp" +#include "cli_battleground_rewards_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -413,6 +414,7 @@ constexpr DispatchFn kDispatchTable[] = { handleRandomPropertyCatalog, handleSpellProcRulesCatalog, handleAuctionHousesCatalog, + handleBattlegroundRewardsCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index c1f065ca..62946f9b 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -142,6 +142,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','I','R','C'}, ".wirc", "loot", "--info-wirc", "Item random-property pool catalog"}, {{'W','P','R','C'}, ".wprc", "spells", "--info-wprc", "Spell proc rules catalog"}, {{'W','A','U','H'}, ".wauh", "economy", "--info-wauh", "Auction house config catalog"}, + {{'W','B','R','D'}, ".wbrd", "pvp", "--info-wbrd", "Battleground reward stages 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 aba98c40..17fec9e1 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2713,6 +2713,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wauh to a human-editable JSON sidecar (defaults to .wauh.json; emits factionAccess as int + name string)\n"); std::printf(" --import-wauh-json [out-base]\n"); std::printf(" Import a .wauh.json sidecar back into binary .wauh (factionAccess int OR \"both\"/\"alliance\"/\"horde\"/\"neutral\" — round-trips per-AH config byte-identical)\n"); + std::printf(" --gen-brd-av [name]\n"); + std::printf(" Emit .wbrd Alterac Valley reward ladder (bgId=1, brackets 5-6 / 51-69 only, 20 players/side, Mark of AV item 17502)\n"); + std::printf(" --gen-brd-wsg [name]\n"); + std::printf(" Emit .wbrd Warsong Gulch reward ladder (bgId=2, all 6 brackets 10-69, 10 players/side, Mark of WSG item 20558, scaling honor 50/100/200/350/500/750)\n"); + std::printf(" --gen-brd-ab [name]\n"); + std::printf(" Emit .wbrd Arathi Basin reward ladder (bgId=3, brackets 2-6 / 20-69, 15 players/side, Mark of AB item 20559, weekly bonus quest token included)\n"); + std::printf(" --info-wbrd [--json]\n"); + std::printf(" Print WBRD entries (id / bg / bracket / minPlayers / win+loss honor / markId / win+loss marks / bonusItem)\n"); + std::printf(" --validate-wbrd [--json]\n"); + std::printf(" Static checks: id+battlegroundId required, bracketIndex 1..6 (vanilla), minPlayersToStart > 0 (else queue would never start a match), no duplicate rewardIds, no duplicate (bgId,bracket) pairs (runtime reward-lookup tie), bonusItemCount > 0 requires bonusItemId; CRITICAL: lossHonor <= winHonor (else losing rewards more than winning, no incentive to play to win). Warns on winMarks=0 with markItemId set (vanilla wins always granted at least 1 mark)\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 a2a8bb94..d84d5b4c 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -164,6 +164,7 @@ constexpr FormatRow kFormats[] = { {"WIRC", ".wirc", "loot", "ItemRandomProperties.dbc + LootMgr", "Item random-property pool catalog (weighted suffix enchants)"}, {"WPRC", ".wprc", "spells", "SpellProcEvents + per-spell procFlags","Spell proc rules catalog (event triggers + ICD + self-loop guard)"}, {"WAUH", ".wauh", "economy", "AuctionHouse.dbc + AuctionMgr", "Auction house config catalog (deposit/cut rates + duration tiers)"}, + {"WBRD", ".wbrd", "pvp", "BattlemasterList.dbc + BattlegroundMgr","Battleground reward stages catalog (per-bracket honor + marks)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine