From 98316b48aca3c0881870c3d0e3ec95ddd65f8e66 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 04:15:20 -0700 Subject: [PATCH] feat(pipeline): WGBK guild bank tabs catalog (131st open format) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel format providing what vanilla WoW lacked entirely: a guild-level shared storage facility (Blizzard added guild banks in TBC, but the Wowee project provides this format from day one for the Classic-1.12 server flavor as well as later expansions). Each WGBK entry binds one guild bank tab to its display label, slot count (1..98 vanilla cap), deposit-only flag, icon, and a fixed-size per-guild-rank withdrawal limit array (slots/day cap per rank 0..7, where rank 0 is GuildMaster, kUnlimited = 0xFFFFFFFF). Three presets: --gen-gbk Standard 4-tab bank (General/Materials/ Consumables/Officer) for guildId 1 with progressive per-rank caps --gen-gbk-raid 5-tab raid guild (Tier1_BWL/Tier2_AQ40/ Tier3_Naxx + Consumables + Officer) — tier tabs strictly officer-only with 4-slot/day cap on rank 1 --gen-gbk-small 2-tab small guild (General + Officer) with tight 5-slot/day caps below officer rank Validator catches: id+guildId+tabName required, slotCount 1..98 (vanilla cap), GM withdrawal limit > 0 (rank 0 cannot be locked out — almost certainly a typo), per-rank monotonicity (lower rank cannot exceed higher rank's cap — kUnlimited treated as infinity for compare), no duplicate tabIds, no duplicate (guildId,tabName) pairs (UI dispatch tie). Warns on depositOnly flag set with non-zero rank-0 limit (self-contradiction — flag overrides at runtime but data is contradictory). Format count 130 -> 131. CLI flag count 1373 -> 1380. --- CMakeLists.txt | 3 + include/pipeline/wowee_guild_bank.hpp | 110 +++++++++ src/pipeline/wowee_guild_bank.cpp | 239 ++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_guild_bank_catalog.cpp | 310 ++++++++++++++++++++++++ tools/editor/cli_guild_bank_catalog.hpp | 12 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 690 insertions(+) create mode 100644 include/pipeline/wowee_guild_bank.hpp create mode 100644 src/pipeline/wowee_guild_bank.cpp create mode 100644 tools/editor/cli_guild_bank_catalog.cpp create mode 100644 tools/editor/cli_guild_bank_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b7730255..53f8cf72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -719,6 +719,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_transit_schedule.cpp src/pipeline/wowee_mage_portals.cpp src/pipeline/wowee_combat_stats.cpp + src/pipeline/wowee_guild_bank.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1601,6 +1602,7 @@ add_executable(wowee_editor tools/editor/cli_transit_schedule_catalog.cpp tools/editor/cli_mage_portals_catalog.cpp tools/editor/cli_combat_stats_catalog.cpp + tools/editor/cli_guild_bank_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1802,6 +1804,7 @@ add_executable(wowee_editor src/pipeline/wowee_transit_schedule.cpp src/pipeline/wowee_mage_portals.cpp src/pipeline/wowee_combat_stats.cpp + src/pipeline/wowee_guild_bank.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_guild_bank.hpp b/include/pipeline/wowee_guild_bank.hpp new file mode 100644 index 00000000..6007b2cf --- /dev/null +++ b/include/pipeline/wowee_guild_bank.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Guild Bank Tabs catalog (.wgbk) — +// novel format providing what vanilla WoW lacked +// entirely: a guild-level shared storage facility +// (Blizzard added guild banks in TBC, but the +// Wowee project predates a TBC-specific binding — +// WGBK provides the catalog format from day one for +// the Classic-1.12 server flavor as well as later +// expansions). Each WGBK entry binds one guild bank +// tab to its display label, slot count, deposit- +// only flag, icon, and a per-guild-rank withdrawal +// limit array (slots/day cap per rank 0..7, where +// rank 0 is GuildMaster). +// +// Cross-references with previously-added formats: +// WGLD: guildId references the WGLD guilds catalog. +// WIT: inventoried itemIds in tabs are looked up +// against WIT (the runtime structure for tab +// contents lives elsewhere — WGBK only +// describes the tab schema, not the items). +// +// Binary layout (little-endian): +// magic[4] = "WGBK" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// tabId (uint32) — surrogate primary key +// guildId (uint32) — owning guild +// tabNameLen + tabName +// iconIndex (uint32) — ItemDisplayInfo row id +// depositOnly (uint8) — 0/1 bool — non-rank-0 +// can deposit but not +// withdraw +// pad0 (uint8) +// slotCount (uint16) — 1..98 (vanilla cap) +// perRankWithdrawalLimit[8] (uint32) — slots/day +// per rank 0..7, +// 0xFFFFFFFF = +// unlimited, 0 = no +// withdraw access. +// Index 0 is GuildMaster. +struct WoweeGuildBank { + static constexpr uint32_t kRankCount = 8; + static constexpr uint32_t kUnlimited = 0xFFFFFFFFu; + static constexpr uint16_t kMaxSlots = 98; + + struct Entry { + uint32_t tabId = 0; + uint32_t guildId = 0; + std::string tabName; + uint32_t iconIndex = 0; + uint8_t depositOnly = 0; + uint8_t pad0 = 0; + uint16_t slotCount = 0; + uint32_t perRankWithdrawalLimit[kRankCount] = {0,0,0,0,0,0,0,0}; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t tabId) const; + + // Returns all bank tabs owned by a guild — used + // by the guild-bank UI to populate the per-guild + // tab strip. + std::vector findByGuild(uint32_t guildId) const; +}; + +class WoweeGuildBankLoader { +public: + static bool save(const WoweeGuildBank& cat, + const std::string& basePath); + static WoweeGuildBank load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-gbk* variants. + // + // makeStandardBank — 4 tabs for guildId 1 + // (General/Materials/ + // Consumables/Officer). + // Officer tab only + // withdrawable by ranks + // 0-2. + // makeRaidGuild — 5 tabs (Tier1/Tier2/Tier3 + // gear sets, Consumables, + // Officer). High slot + // counts on tier tabs. + // makeSmallGuild — 2 tabs (General + Officer) + // with tight per-rank + // withdrawal limits — all + // non-officer ranks capped + // at 5 slots/day. + static WoweeGuildBank makeStandardBank(const std::string& catalogName); + static WoweeGuildBank makeRaidGuild(const std::string& catalogName); + static WoweeGuildBank makeSmallGuild(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_guild_bank.cpp b/src/pipeline/wowee_guild_bank.cpp new file mode 100644 index 00000000..7207b450 --- /dev/null +++ b/src/pipeline/wowee_guild_bank.cpp @@ -0,0 +1,239 @@ +#include "pipeline/wowee_guild_bank.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'G', 'B', 'K'}; +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) != ".wgbk") { + base += ".wgbk"; + } + return base; +} + +} // namespace + +const WoweeGuildBank::Entry* +WoweeGuildBank::findById(uint32_t tabId) const { + for (const auto& e : entries) + if (e.tabId == tabId) return &e; + return nullptr; +} + +std::vector +WoweeGuildBank::findByGuild(uint32_t guildId) const { + std::vector out; + for (const auto& e : entries) + if (e.guildId == guildId) out.push_back(&e); + return out; +} + +bool WoweeGuildBankLoader::save(const WoweeGuildBank& 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.tabId); + writePOD(os, e.guildId); + writeStr(os, e.tabName); + writePOD(os, e.iconIndex); + writePOD(os, e.depositOnly); + writePOD(os, e.pad0); + writePOD(os, e.slotCount); + for (uint32_t r = 0; + r < WoweeGuildBank::kRankCount; ++r) { + writePOD(os, e.perRankWithdrawalLimit[r]); + } + } + return os.good(); +} + +WoweeGuildBank WoweeGuildBankLoader::load( + const std::string& basePath) { + WoweeGuildBank 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.tabId) || + !readPOD(is, e.guildId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.tabName)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.iconIndex) || + !readPOD(is, e.depositOnly) || + !readPOD(is, e.pad0) || + !readPOD(is, e.slotCount)) { + out.entries.clear(); return out; + } + for (uint32_t r = 0; + r < WoweeGuildBank::kRankCount; ++r) { + if (!readPOD(is, e.perRankWithdrawalLimit[r])) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeGuildBankLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +namespace { + +WoweeGuildBank::Entry makeTab(uint32_t tabId, uint32_t guildId, + const char* tabName, + uint32_t iconIndex, + uint8_t depositOnly, + uint16_t slotCount, + std::initializer_list limits) { + WoweeGuildBank::Entry e; + e.tabId = tabId; e.guildId = guildId; + e.tabName = tabName; + e.iconIndex = iconIndex; + e.depositOnly = depositOnly; + e.slotCount = slotCount; + uint32_t r = 0; + for (uint32_t v : limits) { + if (r >= WoweeGuildBank::kRankCount) break; + e.perRankWithdrawalLimit[r++] = v; + } + return e; +} + +} // namespace + +WoweeGuildBank WoweeGuildBankLoader::makeStandardBank( + const std::string& catalogName) { + using G = WoweeGuildBank; + WoweeGuildBank c; + c.name = catalogName; + // Per-rank limits: rank 0=GM, rank 1=Officer, + // rank 2..7=members. GM always Unlimited; lower + // ranks get progressively tighter caps. + // General: open-access tab. + c.entries.push_back(makeTab( + 1, 1, "General", 1392, 0, 98, + {G::kUnlimited, 50, 30, 20, 15, 10, 5, 0})); + // Materials: cloth/herb/leather pool — modest + // caps for ranks 1-4, none below. + c.entries.push_back(makeTab( + 2, 1, "Materials", 5765, 0, 98, + {G::kUnlimited, 100, 50, 25, 10, 0, 0, 0})); + // Consumables: pots/scrolls/elixirs — generous + // cap for raiders (rank 1-3), nothing for casuals. + c.entries.push_back(makeTab( + 3, 1, "Consumables", 5764, 0, 98, + {G::kUnlimited, 75, 50, 25, 0, 0, 0, 0})); + // Officer: ranks 0-2 only. Used for raid drops + // pre-distribution. + c.entries.push_back(makeTab( + 4, 1, "Officer", 7079, 0, 56, + {G::kUnlimited, 20, 10, 0, 0, 0, 0, 0})); + return c; +} + +WoweeGuildBank WoweeGuildBankLoader::makeRaidGuild( + const std::string& catalogName) { + using G = WoweeGuildBank; + WoweeGuildBank c; + c.name = catalogName; + // Tier set tabs: high slot count, withdrawal + // strictly officer-only for the rare items. + c.entries.push_back(makeTab( + 10, 2, "Tier1_BWL", 18811, 0, 98, + {G::kUnlimited, 4, 2, 0, 0, 0, 0, 0})); + c.entries.push_back(makeTab( + 11, 2, "Tier2_AQ40", 21221, 0, 98, + {G::kUnlimited, 4, 2, 0, 0, 0, 0, 0})); + c.entries.push_back(makeTab( + 12, 2, "Tier3_Naxx", 22349, 0, 98, + {G::kUnlimited, 4, 2, 0, 0, 0, 0, 0})); + // Consumables: any raider can pull. + c.entries.push_back(makeTab( + 13, 2, "Consumables", 5764, 0, 98, + {G::kUnlimited, 50, 50, 50, 25, 10, 0, 0})); + // Officer: drops awaiting distribution. + c.entries.push_back(makeTab( + 14, 2, "Officer", 7079, 0, 56, + {G::kUnlimited, 25, 10, 0, 0, 0, 0, 0})); + return c; +} + +WoweeGuildBank WoweeGuildBankLoader::makeSmallGuild( + const std::string& catalogName) { + using G = WoweeGuildBank; + WoweeGuildBank c; + c.name = catalogName; + // Small guild: tight controls — most ranks + // capped at 5 slots/day for the General tab, + // Officer tab is GM+officer only. + c.entries.push_back(makeTab( + 20, 3, "General", 1392, 0, 28, + {G::kUnlimited, 10, 5, 5, 5, 5, 5, 0})); + c.entries.push_back(makeTab( + 21, 3, "Officer", 7079, 0, 14, + {G::kUnlimited, 5, 0, 0, 0, 0, 0, 0})); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 98cbbd1d..4925d58a 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -401,6 +401,8 @@ const char* const kArgRequired[] = { "--gen-cst-warrior", "--gen-cst-mage", "--gen-cst-starting", "--info-wcst", "--validate-wcst", "--export-wcst-json", "--import-wcst-json", + "--gen-gbk", "--gen-gbk-raid", "--gen-gbk-small", + "--info-wgbk", "--validate-wgbk", "--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 ee00ed3e..f9da6154 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -175,6 +175,7 @@ #include "cli_transit_schedule_catalog.hpp" #include "cli_mage_portals_catalog.hpp" #include "cli_combat_stats_catalog.hpp" +#include "cli_guild_bank_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -395,6 +396,7 @@ constexpr DispatchFn kDispatchTable[] = { handleTransitScheduleCatalog, handleMagePortalsCatalog, handleCombatStatsCatalog, + handleGuildBankCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 035774b2..317fc366 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -133,6 +133,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','T','S','C'}, ".wtsc", "transit", "--info-wtsc", "Transit schedule catalog"}, {{'W','P','R','T'}, ".wprt", "portals", "--info-wprt", "Mage portal destinations catalog"}, {{'W','C','S','T'}, ".wcst", "stats", "--info-wcst", "Combat stats baseline catalog"}, + {{'W','G','B','K'}, ".wgbk", "guild", "--info-wgbk", "Guild bank tabs 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_guild_bank_catalog.cpp b/tools/editor/cli_guild_bank_catalog.cpp new file mode 100644 index 00000000..32f7f7af --- /dev/null +++ b/tools/editor/cli_guild_bank_catalog.cpp @@ -0,0 +1,310 @@ +#include "cli_guild_bank_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_guild_bank.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWgbkExt(std::string base) { + stripExt(base, ".wgbk"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeGuildBank& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeGuildBankLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wgbk\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeGuildBank& c, + const std::string& base) { + std::printf("Wrote %s.wgbk\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tabs : %zu\n", c.entries.size()); +} + +int handleGenStandard(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StandardGuildBank"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgbkExt(base); + auto c = wowee::pipeline::WoweeGuildBankLoader:: + makeStandardBank(name); + if (!saveOrError(c, base, "gen-gbk")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRaid(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RaidGuildBank"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgbkExt(base); + auto c = wowee::pipeline::WoweeGuildBankLoader:: + makeRaidGuild(name); + if (!saveOrError(c, base, "gen-gbk-raid")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenSmall(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "SmallGuildBank"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgbkExt(base); + auto c = wowee::pipeline::WoweeGuildBankLoader:: + makeSmallGuild(name); + if (!saveOrError(c, base, "gen-gbk-small")) return 1; + printGenSummary(c, base); + return 0; +} + +std::string formatLimit(uint32_t v) { + using G = wowee::pipeline::WoweeGuildBank; + if (v == G::kUnlimited) return "INF"; + return std::to_string(v); +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWgbkExt(base); + if (!wowee::pipeline::WoweeGuildBankLoader::exists(base)) { + std::fprintf(stderr, "WGBK not found: %s.wgbk\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGuildBankLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wgbk"] = base + ".wgbk"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + nlohmann::json limits = nlohmann::json::array(); + for (uint32_t r = 0; + r < wowee::pipeline::WoweeGuildBank:: + kRankCount; ++r) { + limits.push_back(e.perRankWithdrawalLimit[r]); + } + arr.push_back({ + {"tabId", e.tabId}, + {"guildId", e.guildId}, + {"tabName", e.tabName}, + {"iconIndex", e.iconIndex}, + {"depositOnly", e.depositOnly != 0}, + {"slotCount", e.slotCount}, + {"perRankWithdrawalLimit", limits}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WGBK: %s.wgbk\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tabs : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id guild slots dep R0 R1 R2 R3 R4 R5 R6 R7 tabName\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %5u %s ", + e.tabId, e.guildId, e.slotCount, + e.depositOnly ? "Y" : "n"); + for (uint32_t r = 0; + r < wowee::pipeline::WoweeGuildBank:: + kRankCount; ++r) { + std::printf(" %4s", + formatLimit(e.perRankWithdrawalLimit[r]) + .c_str()); + } + std::printf(" %s\n", e.tabName.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWgbkExt(base); + if (!wowee::pipeline::WoweeGuildBankLoader::exists(base)) { + std::fprintf(stderr, + "validate-wgbk: WGBK not found: %s.wgbk\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGuildBankLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + using G = wowee::pipeline::WoweeGuildBank; + std::set idsSeen; + using Pair = std::pair; + std::set guildTabPairs; + 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.tabId) + + " guildId=" + std::to_string(e.guildId); + if (!e.tabName.empty()) ctx += " " + e.tabName; + ctx += ")"; + if (e.tabId == 0) + errors.push_back(ctx + ": tabId is 0"); + if (e.guildId == 0) + errors.push_back(ctx + ": guildId is 0"); + if (e.tabName.empty()) + errors.push_back(ctx + ": tabName is empty"); + if (e.slotCount == 0) { + errors.push_back(ctx + + ": slotCount is 0 — empty tab is unusable"); + } + if (e.slotCount > G::kMaxSlots) { + errors.push_back(ctx + ": slotCount " + + std::to_string(e.slotCount) + + " exceeds vanilla cap of " + + std::to_string(G::kMaxSlots)); + } + // GuildMaster (rank 0) must have at least + // some withdrawal access — usually unlimited. + // A 0 here means even the GM can't withdraw, + // which is almost certainly a bug. + if (e.perRankWithdrawalLimit[0] == 0) { + errors.push_back(ctx + + ": perRankWithdrawalLimit[0]=0 — GuildMaster" + " cannot withdraw, almost certainly a typo"); + } + // Per-rank monotonicity: a lower rank should + // not have a HIGHER limit than a higher rank. + // Walk pairs (rank R+1 vs rank R) and flag. + // Treat kUnlimited as "infinity" for compare. + auto rankVal = [&](uint32_t r) -> uint64_t { + if (e.perRankWithdrawalLimit[r] == G::kUnlimited) + return UINT64_MAX; + return e.perRankWithdrawalLimit[r]; + }; + for (uint32_t r = 0; r + 1 < G::kRankCount; ++r) { + uint64_t hi = rankVal(r); + uint64_t lo = rankVal(r + 1); + if (lo > hi) { + errors.push_back(ctx + + ": perRankWithdrawalLimit[" + + std::to_string(r + 1) + + "]=" + + formatLimit(e.perRankWithdrawalLimit[r + 1]) + + " > rank[" + std::to_string(r) + "]=" + + formatLimit(e.perRankWithdrawalLimit[r]) + + " — lower rank cannot exceed higher rank's" + " withdrawal cap"); + } + } + // depositOnly + non-zero withdrawal limit on + // rank 0 = self-contradiction. WGBK semantics: + // depositOnly is a tab-wide flag overriding + // the per-rank table. Warn. + if (e.depositOnly && + e.perRankWithdrawalLimit[0] != 0) { + warnings.push_back(ctx + + ": depositOnly flag set but rank 0 has " + "withdrawal limit " + + formatLimit(e.perRankWithdrawalLimit[0]) + + " — flag will override the table at " + "runtime, but the data is contradictory"); + } + // Duplicate (guildId, tabName) — UI dispatch + // would tie. + if (e.guildId != 0 && !e.tabName.empty()) { + Pair p{e.guildId, e.tabName}; + if (!guildTabPairs.insert(p).second) { + errors.push_back(ctx + + ": duplicate (guildId=" + + std::to_string(e.guildId) + + ", tabName=" + e.tabName + + ") — UI tab dispatch ambiguous"); + } + } + if (!idsSeen.insert(e.tabId).second) { + errors.push_back(ctx + ": duplicate tabId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wgbk"] = base + ".wgbk"; + 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-wgbk: %s.wgbk\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu tabs, all tabIds + " + "(guildId,tabName) unique, slotCount " + "1..98, GM withdrawal > 0, per-rank " + "monotonicity (lower rank <= higher " + "rank cap)\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 handleGuildBankCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-gbk") == 0 && i + 1 < argc) { + outRc = handleGenStandard(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-gbk-raid") == 0 && + i + 1 < argc) { + outRc = handleGenRaid(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-gbk-small") == 0 && + i + 1 < argc) { + outRc = handleGenSmall(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wgbk") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wgbk") == 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_guild_bank_catalog.hpp b/tools/editor/cli_guild_bank_catalog.hpp new file mode 100644 index 00000000..8683d631 --- /dev/null +++ b/tools/editor/cli_guild_bank_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleGuildBankCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 346b3093..f6a0b646 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2587,6 +2587,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcst to a human-editable JSON sidecar (defaults to .wcst.json; emits classId as int + className string for readability)\n"); std::printf(" --import-wcst-json [out-base]\n"); std::printf(" Import a .wcst.json sidecar back into binary .wcst (className field is informational; classId int is authoritative — round-trips per-class per-level stat tables byte-identical)\n"); + std::printf(" --gen-gbk [name]\n"); + std::printf(" Emit .wgbk standard 4-tab guild bank for guildId 1 (General/Materials/Consumables/Officer) with progressive per-rank withdrawal limits (GM unlimited, lower ranks tighter caps)\n"); + std::printf(" --gen-gbk-raid [name]\n"); + std::printf(" Emit .wgbk 5-tab raid guild bank for guildId 2 (Tier1_BWL/Tier2_AQ40/Tier3_Naxx tier-set tabs + Consumables + Officer) — tier tabs strictly officer-only\n"); + std::printf(" --gen-gbk-small [name]\n"); + std::printf(" Emit .wgbk 2-tab small guild bank for guildId 3 (General + Officer) with tight 5-slot/day per-rank caps below officer\n"); + std::printf(" --info-wgbk [--json]\n"); + std::printf(" Print WGBK entries (id / guildId / slotCount / depositOnly / per-rank-0..7 withdrawal limits / tabName)\n"); + std::printf(" --validate-wgbk [--json]\n"); + std::printf(" Static checks: id+guildId+tabName required, slotCount 1..98 (vanilla cap), GM withdrawal limit > 0 (rank 0 cannot be locked out — almost certainly a typo), per-rank monotonicity (lower rank cannot exceed higher rank's cap), no duplicate tabIds, no duplicate (guildId,tabName) pairs (UI tab dispatch tie); warns on depositOnly flag set with non-zero rank-0 limit (self-contradiction — flag overrides at runtime but data is contradictory)\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 c82a88c7..3bb100a0 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -155,6 +155,7 @@ constexpr FormatRow kFormats[] = { {"WTSC", ".wtsc", "transit", "TaxiNodes + zeppelin GO scripts", "Transit schedule catalog (taxi/zeppelin/boat scheduled departures)"}, {"WPRT", ".wprt", "portals", "SpellEffect TELEPORT_UNITS + AreaTrigger","Mage portal destinations catalog (spellId -> coords binding)"}, {"WCST", ".wcst", "stats", "CharBaseInfo + GtChanceTo*.dbc + StatSystem","Combat stats baseline catalog (per-class per-level base stats)"}, + {"WGBK", ".wgbk", "guild", "(absent in vanilla, TBC GuildBankTab)","Guild bank tabs catalog (per-rank withdrawal limits)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine