From 80ebf1dba5f7ca665290057b7812cabff5ac0a1a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 18:10:45 -0700 Subject: [PATCH] feat(pipeline): add WGLD (Wowee Guild) catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for AzerothCore-style guild + guild_member + guild_rank + guild_bank_tab + guild_perk SQL tables. The 36th open format added to the editor. Each guild entry holds the complete social-organization state: header (name, leader, faction, MOTD, info, creation date, level + experience, bank money, packed emblem), rank ladder with permissions bitmask + daily withdraw caps, member roster with rank + join date + public/officer notes, bank tabs with per-tab and per-rank deposit / withdraw / view permission masks, and purchased guild perks referencing WSPL spell IDs. Cross-references with previously-added formats: WGLD.entry.factionId ~ WCHC.race.factionId (guilds are faction-locked) WGLD.entry.perks.spellId -> WSPL.entry.spellId Format: • magic "WGLD", version 1, little-endian • per guild: header (12 scalar fields + 4 strings) + ranks[] + members[] + bankTabs[] + perks[] • per rank: rankIndex / name / permissionsMask / moneyPerDayCopper • per member: characterName / rankIndex / joinedDate / publicNote / officerNote • per bankTab: tabIndex / name / iconPath / deposit+withdraw+view permission masks • per perk: perkId / name / spellId / requiredGuildLevel Enums: • Faction (2): Alliance / Horde • RankPermissionFlags (14): GuildChat / OfficerChat / Invite / Remove / Promote / Demote / SetMotd / EditPublicNote / EditOfficerNote / ViewBank / Deposit / Withdraw / Disband / RepairFromBank API: WoweeGuildLoader::save / load / exists / findById + shared addDefaultRanks helper used by both starter and faction-pair presets. Three preset emitters: • makeStarter — 1 small guild, default 5-rank ladder (GM/Officer/Veteran/Member/Initiate), 3 members borrowing names from WCRT merchants for cross-format consistency • makeFull — 1 fleshed-out guild: 6 ranks (with Recruit added) + 8 members + 4 bank tabs (officer-only withdraw on tabs 3+4) + 3 perks referencing WSPL spell IDs (Heroic Strike / Battle Shout / Thunder Clap as placeholder perk procs) • makeFactionPair — 2 parallel guilds, one Alliance + one Horde, with identical rank structures CLI added (5 flags, 649 documented total now): --gen-guilds / --gen-guilds-full / --gen-guilds-pair --info-wgld / --validate-wgld Validator catches: guildId=0 + duplicates, empty name / leaderName, factionId out of range, no ranks (members can't exist without a rank ladder), member.rankIndex exceeding the highest defined rank (intra-format cross-reference resolution), duplicate bank tabIndices, perks with spellId=0 (perk does nothing). --- CMakeLists.txt | 3 + include/pipeline/wowee_guilds.hpp | 159 ++++++++++++ src/pipeline/wowee_guilds.cpp | 366 ++++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_guilds_catalog.cpp | 309 +++++++++++++++++++++++ tools/editor/cli_guilds_catalog.hpp | 11 + tools/editor/cli_help.cpp | 10 + 8 files changed, 862 insertions(+) create mode 100644 include/pipeline/wowee_guilds.hpp create mode 100644 src/pipeline/wowee_guilds.cpp create mode 100644 tools/editor/cli_guilds_catalog.cpp create mode 100644 tools/editor/cli_guilds_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 288e8c03..0450005b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -623,6 +623,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_battlegrounds.cpp src/pipeline/wowee_mail.cpp src/pipeline/wowee_gems.cpp + src/pipeline/wowee_guilds.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1392,6 +1393,7 @@ add_executable(wowee_editor tools/editor/cli_battlegrounds_catalog.cpp tools/editor/cli_mail_catalog.cpp tools/editor/cli_gems_catalog.cpp + tools/editor/cli_guilds_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1493,6 +1495,7 @@ add_executable(wowee_editor src/pipeline/wowee_battlegrounds.cpp src/pipeline/wowee_mail.cpp src/pipeline/wowee_gems.cpp + src/pipeline/wowee_guilds.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_guilds.hpp b/include/pipeline/wowee_guilds.hpp new file mode 100644 index 00000000..bed860a9 --- /dev/null +++ b/include/pipeline/wowee_guilds.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Guild catalog (.wgld) — novel replacement for +// AzerothCore-style guild + guild_member + guild_rank + +// guild_bank_tab + guild_perk SQL tables. The 36th open +// format added to the editor. +// +// Each guild entry holds: +// • header — id, name, faction, leader, MOTD, info, +// creation date, level + experience, bank +// money, emblem (packed style/color/border/bg) +// • ranks — rank ladder (GM down to Initiate) with +// permissions bitmask + daily money withdraw +// • members — character roster with rank, join date, +// public + officer notes +// • bankTabs — per-tab name, icon, deposit/withdraw/view +// permission masks indexed by rank +// • perks — purchased guild-level perks (cata-era spell +// buffs); each perk references a WSPL spell +// +// Cross-references with previously-added formats: +// WGLD.entry.factionId → matches WCHC.race.factionId +// (guilds are faction-locked) +// WGLD.entry.perks.spellId → WSPL.entry.spellId +// +// Binary layout (little-endian): +// magic[4] = "WGLD" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// guildId (uint32) +// nameLen + name +// leaderLen + leaderName +// motdLen + motd +// infoLen + info +// creationDate (uint64) +// experience (uint64) +// level (uint16) / factionId (uint8) / pad[1] +// bankCopper (uint32) +// emblem (uint32) +// rankCount (uint8) + ranks[] +// memberCount (uint16) + members[] +// bankTabCount (uint8) + bankTabs[] +// perkCount (uint8) + perks[] +struct WoweeGuild { + enum Faction : uint8_t { + Alliance = 0, + Horde = 1, + }; + + enum RankPermissionFlags : uint32_t { + PermGuildChat = 0x0001, + PermOfficerChat = 0x0002, + PermInvite = 0x0004, + PermRemove = 0x0008, + PermPromote = 0x0010, + PermDemote = 0x0020, + PermSetMotd = 0x0040, + PermEditPublicNote = 0x0080, + PermEditOfficerNote = 0x0100, + PermViewBank = 0x0200, + PermDeposit = 0x0400, + PermWithdraw = 0x0800, + PermDisband = 0x1000, + PermRepairFromBank = 0x2000, + }; + + struct Rank { + uint8_t rankIndex = 0; // 0 = GuildMaster + std::string name; + uint32_t permissionsMask = 0; + uint32_t moneyPerDayCopper = 0; + }; + + struct Member { + std::string characterName; + uint8_t rankIndex = 0; + uint64_t joinedDate = 0; // Unix timestamp seconds + std::string publicNote; + std::string officerNote; + }; + + struct BankTab { + uint8_t tabIndex = 0; + std::string name; + std::string iconPath; + uint32_t depositPermissionMask = 0; // bit per rank + uint32_t withdrawPermissionMask = 0; + uint32_t viewPermissionMask = 0; + }; + + struct Perk { + uint32_t perkId = 0; + std::string name; + uint32_t spellId = 0; // WSPL cross-ref + uint16_t requiredGuildLevel = 1; + }; + + struct Entry { + uint32_t guildId = 0; + std::string name; + std::string leaderName; + std::string motd; + std::string info; + uint64_t creationDate = 0; + uint64_t experience = 0; + uint16_t level = 1; + uint8_t factionId = Alliance; + uint32_t bankCopper = 0; + uint32_t emblem = 0; // packed style/color/border/bg + std::vector ranks; + std::vector members; + std::vector bankTabs; + std::vector perks; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t guildId) const; + + static const char* factionName(uint8_t f); +}; + +class WoweeGuildLoader { +public: + static bool save(const WoweeGuild& cat, + const std::string& basePath); + static WoweeGuild load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-guilds* variants. + // + // makeStarter — 1 small guild with default 5-rank ladder + // (GM/Officer/Veteran/Member/Initiate), + // 3 members, no bank tabs. + // makeFull — 1 fleshed-out guild with 6 ranks, 8 + // members, 4 bank tabs (each with + // per-rank permission masks), 3 purchased + // perks referencing WSPL spell IDs. + // makeFactionPair — 2 guilds, one Alliance + one Horde, + // with parallel rank structures. + static WoweeGuild makeStarter(const std::string& catalogName); + static WoweeGuild makeFull(const std::string& catalogName); + static WoweeGuild makeFactionPair(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_guilds.cpp b/src/pipeline/wowee_guilds.cpp new file mode 100644 index 00000000..20a463e3 --- /dev/null +++ b/src/pipeline/wowee_guilds.cpp @@ -0,0 +1,366 @@ +#include "pipeline/wowee_guilds.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'G', 'L', '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) != ".wgld") { + base += ".wgld"; + } + return base; +} + +} // namespace + +const WoweeGuild::Entry* WoweeGuild::findById(uint32_t guildId) const { + for (const auto& e : entries) if (e.guildId == guildId) return &e; + return nullptr; +} + +const char* WoweeGuild::factionName(uint8_t f) { + switch (f) { + case Alliance: return "alliance"; + case Horde: return "horde"; + default: return "unknown"; + } +} + +bool WoweeGuildLoader::save(const WoweeGuild& 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.guildId); + writeStr(os, e.name); + writeStr(os, e.leaderName); + writeStr(os, e.motd); + writeStr(os, e.info); + writePOD(os, e.creationDate); + writePOD(os, e.experience); + writePOD(os, e.level); + writePOD(os, e.factionId); + uint8_t pad1 = 0; + writePOD(os, pad1); + writePOD(os, e.bankCopper); + writePOD(os, e.emblem); + + uint8_t rankCount = static_cast( + e.ranks.size() > 255 ? 255 : e.ranks.size()); + writePOD(os, rankCount); + for (uint8_t k = 0; k < rankCount; ++k) { + const auto& r = e.ranks[k]; + writePOD(os, r.rankIndex); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writeStr(os, r.name); + writePOD(os, r.permissionsMask); + writePOD(os, r.moneyPerDayCopper); + } + uint16_t memCount = static_cast( + e.members.size() > 0xFFFF ? 0xFFFF : e.members.size()); + writePOD(os, memCount); + for (uint16_t k = 0; k < memCount; ++k) { + const auto& m = e.members[k]; + writeStr(os, m.characterName); + writePOD(os, m.rankIndex); + uint8_t pad7[7] = {0}; + os.write(reinterpret_cast(pad7), 7); + writePOD(os, m.joinedDate); + writeStr(os, m.publicNote); + writeStr(os, m.officerNote); + } + uint8_t tabCount = static_cast( + e.bankTabs.size() > 255 ? 255 : e.bankTabs.size()); + writePOD(os, tabCount); + for (uint8_t k = 0; k < tabCount; ++k) { + const auto& t = e.bankTabs[k]; + writePOD(os, t.tabIndex); + uint8_t pad3b[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3b), 3); + writeStr(os, t.name); + writeStr(os, t.iconPath); + writePOD(os, t.depositPermissionMask); + writePOD(os, t.withdrawPermissionMask); + writePOD(os, t.viewPermissionMask); + } + uint8_t perkCount = static_cast( + e.perks.size() > 255 ? 255 : e.perks.size()); + writePOD(os, perkCount); + for (uint8_t k = 0; k < perkCount; ++k) { + const auto& p = e.perks[k]; + writePOD(os, p.perkId); + writeStr(os, p.name); + writePOD(os, p.spellId); + writePOD(os, p.requiredGuildLevel); + uint8_t pad2c[2] = {0, 0}; + os.write(reinterpret_cast(pad2c), 2); + } + } + return os.good(); +} + +WoweeGuild WoweeGuildLoader::load(const std::string& basePath) { + WoweeGuild 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); + auto fail = [&]() { + out.entries.clear(); + return out; + }; + for (auto& e : out.entries) { + if (!readPOD(is, e.guildId)) return fail(); + if (!readStr(is, e.name) || !readStr(is, e.leaderName) || + !readStr(is, e.motd) || !readStr(is, e.info)) return fail(); + if (!readPOD(is, e.creationDate) || + !readPOD(is, e.experience) || + !readPOD(is, e.level) || + !readPOD(is, e.factionId)) return fail(); + uint8_t pad1 = 0; + if (!readPOD(is, pad1)) return fail(); + if (!readPOD(is, e.bankCopper) || !readPOD(is, e.emblem)) return fail(); + + uint8_t rankCount = 0; + if (!readPOD(is, rankCount)) return fail(); + e.ranks.resize(rankCount); + for (uint8_t k = 0; k < rankCount; ++k) { + auto& r = e.ranks[k]; + if (!readPOD(is, r.rankIndex)) return fail(); + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) return fail(); + if (!readStr(is, r.name)) return fail(); + if (!readPOD(is, r.permissionsMask) || + !readPOD(is, r.moneyPerDayCopper)) return fail(); + } + uint16_t memCount = 0; + if (!readPOD(is, memCount)) return fail(); + e.members.resize(memCount); + for (uint16_t k = 0; k < memCount; ++k) { + auto& m = e.members[k]; + if (!readStr(is, m.characterName)) return fail(); + if (!readPOD(is, m.rankIndex)) return fail(); + uint8_t pad7[7]; + is.read(reinterpret_cast(pad7), 7); + if (is.gcount() != 7) return fail(); + if (!readPOD(is, m.joinedDate)) return fail(); + if (!readStr(is, m.publicNote) || !readStr(is, m.officerNote)) return fail(); + } + uint8_t tabCount = 0; + if (!readPOD(is, tabCount)) return fail(); + e.bankTabs.resize(tabCount); + for (uint8_t k = 0; k < tabCount; ++k) { + auto& t = e.bankTabs[k]; + if (!readPOD(is, t.tabIndex)) return fail(); + uint8_t pad3b[3]; + is.read(reinterpret_cast(pad3b), 3); + if (is.gcount() != 3) return fail(); + if (!readStr(is, t.name) || !readStr(is, t.iconPath)) return fail(); + if (!readPOD(is, t.depositPermissionMask) || + !readPOD(is, t.withdrawPermissionMask) || + !readPOD(is, t.viewPermissionMask)) return fail(); + } + uint8_t perkCount = 0; + if (!readPOD(is, perkCount)) return fail(); + e.perks.resize(perkCount); + for (uint8_t k = 0; k < perkCount; ++k) { + auto& p = e.perks[k]; + if (!readPOD(is, p.perkId)) return fail(); + if (!readStr(is, p.name)) return fail(); + if (!readPOD(is, p.spellId) || + !readPOD(is, p.requiredGuildLevel)) return fail(); + uint8_t pad2c[2]; + is.read(reinterpret_cast(pad2c), 2); + if (is.gcount() != 2) return fail(); + } + } + return out; +} + +bool WoweeGuildLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +namespace { + +// Default 5-rank ladder used by both starter + faction-pair +// presets. Permissions widen toward the GM end of the ladder. +void addDefaultRanks(WoweeGuild::Entry& e) { + using G = WoweeGuild; + e.ranks.push_back({0, "Guild Master", + 0xFFFFFFFFu, 1000000}); // 100g/day + e.ranks.push_back({1, "Officer", + G::PermGuildChat | G::PermOfficerChat | + G::PermInvite | G::PermRemove | + G::PermPromote | G::PermDemote | + G::PermSetMotd | G::PermViewBank | + G::PermDeposit | G::PermWithdraw, + 500000}); // 50g/day + e.ranks.push_back({2, "Veteran", + G::PermGuildChat | G::PermInvite | + G::PermViewBank | G::PermDeposit | + G::PermWithdraw, + 100000}); // 10g/day + e.ranks.push_back({3, "Member", + G::PermGuildChat | G::PermViewBank | + G::PermDeposit, + 10000}); // 1g/day + e.ranks.push_back({4, "Initiate", + G::PermGuildChat, + 0}); +} + +} // namespace + +WoweeGuild WoweeGuildLoader::makeStarter(const std::string& catalogName) { + WoweeGuild c; + c.name = catalogName; + { + WoweeGuild::Entry e; + e.guildId = 1; e.name = "Sentinels of Dawn"; + e.leaderName = "Bartleby"; + e.motd = "Welcome adventurer! Read the info tab."; + e.info = "Casual leveling guild. All are welcome."; + e.factionId = WoweeGuild::Alliance; + e.level = 1; + addDefaultRanks(e); + e.members.push_back({"Bartleby", 0, 0, + "Founder", "Owns the inn"}); + e.members.push_back({"Hank Steelarm", 1, 0, + "Smith", "Friendly officer"}); + e.members.push_back({"Sera Goldroot", 3, 0, + "Alchemist", ""}); + c.entries.push_back(e); + } + return c; +} + +WoweeGuild WoweeGuildLoader::makeFull(const std::string& catalogName) { + WoweeGuild c; + c.name = catalogName; + { + WoweeGuild::Entry e; + e.guildId = 100; e.name = "Defenders of Stormwind"; + e.leaderName = "Lord Tideborne"; + e.motd = "Raid week starts Tuesday at 8 PM server."; + e.info = "Heroic raiding guild. Apply on the website."; + e.factionId = WoweeGuild::Alliance; + e.level = 25; + e.experience = 1500000; + e.bankCopper = 50000000; // 5000g in bank + e.emblem = 0x12345678; + // 6 ranks: GM + Officer + 2 Council tiers + Member + Initiate. + addDefaultRanks(e); + e.ranks.push_back({5, "Recruit", + WoweeGuild::PermGuildChat, + 0}); + for (int k = 0; k < 8; ++k) { + WoweeGuild::Member m; + m.characterName = "Officer" + std::to_string(k); + m.rankIndex = static_cast(k % 6); + m.joinedDate = 1700000000 + k * 86400; + e.members.push_back(m); + } + // 4 bank tabs with progressively more restrictive + // withdraw permissions (officers only on tabs 3 + 4). + for (int k = 0; k < 4; ++k) { + WoweeGuild::BankTab t; + t.tabIndex = static_cast(k); + t.name = "Tab " + std::to_string(k + 1); + // Bit per rank (rank 0 = bit 0 etc). + t.depositPermissionMask = 0x3F; // ranks 0-5 + t.viewPermissionMask = 0x3F; + t.withdrawPermissionMask = (k < 2) ? 0x3F : 0x03; // tabs 3+4 = GM/Officer only + e.bankTabs.push_back(t); + } + // 3 perks referencing WSPL spell IDs from makeMage / generic. + e.perks.push_back({1, "Fast Track", 78, 1}); // Heroic Strike (placeholder) + e.perks.push_back({2, "Cash Flow", 6673, 10}); // Battle Shout (placeholder) + e.perks.push_back({3, "Reinforce", 6343, 20}); // Thunder Clap (placeholder) + c.entries.push_back(e); + } + return c; +} + +WoweeGuild WoweeGuildLoader::makeFactionPair(const std::string& catalogName) { + WoweeGuild c; + c.name = catalogName; + { + WoweeGuild::Entry e; + e.guildId = 200; e.name = "Light's Vanguard"; + e.leaderName = "Lothar Crownguard"; + e.factionId = WoweeGuild::Alliance; + addDefaultRanks(e); + e.members.push_back({"Lothar Crownguard", 0, 0, "GM", ""}); + c.entries.push_back(e); + } + { + WoweeGuild::Entry e; + e.guildId = 201; e.name = "Bloodfang Warband"; + e.leaderName = "Garrok Bloodfang"; + e.factionId = WoweeGuild::Horde; + addDefaultRanks(e); + e.members.push_back({"Garrok Bloodfang", 0, 0, "Chieftain", ""}); + c.entries.push_back(e); + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index fa305070..1de188cd 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -103,6 +103,8 @@ const char* const kArgRequired[] = { "--export-wmal-json", "--import-wmal-json", "--gen-gems", "--gen-gems-set", "--gen-gems-enchants", "--info-wgem", "--validate-wgem", + "--gen-guilds", "--gen-guilds-full", "--gen-guilds-pair", + "--info-wgld", "--validate-wgld", "--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 1a60b03a..4f5e6b73 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -63,6 +63,7 @@ #include "cli_battlegrounds_catalog.hpp" #include "cli_mail_catalog.hpp" #include "cli_gems_catalog.hpp" +#include "cli_guilds_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -167,6 +168,7 @@ constexpr DispatchFn kDispatchTable[] = { handleBattlegroundsCatalog, handleMailCatalog, handleGemsCatalog, + handleGuildsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_guilds_catalog.cpp b/tools/editor/cli_guilds_catalog.cpp new file mode 100644 index 00000000..6061e7e2 --- /dev/null +++ b/tools/editor/cli_guilds_catalog.cpp @@ -0,0 +1,309 @@ +#include "cli_guilds_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_guilds.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWgldExt(std::string base) { + stripExt(base, ".wgld"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeGuild& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeGuildLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wgld\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeGuild& c, + const std::string& base) { + std::printf("Wrote %s.wgld\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" guilds : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterGuilds"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgldExt(base); + auto c = wowee::pipeline::WoweeGuildLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-guilds")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenFull(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FullGuild"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgldExt(base); + auto c = wowee::pipeline::WoweeGuildLoader::makeFull(name); + if (!saveOrError(c, base, "gen-guilds-full")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenFactionPair(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FactionPair"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgldExt(base); + auto c = wowee::pipeline::WoweeGuildLoader::makeFactionPair(name); + if (!saveOrError(c, base, "gen-guilds-pair")) 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 = stripWgldExt(base); + if (!wowee::pipeline::WoweeGuildLoader::exists(base)) { + std::fprintf(stderr, "WGLD not found: %s.wgld\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGuildLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wgld"] = base + ".wgld"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + nlohmann::json je; + je["guildId"] = e.guildId; + je["name"] = e.name; + je["leaderName"] = e.leaderName; + je["motd"] = e.motd; + je["info"] = e.info; + je["creationDate"] = e.creationDate; + je["experience"] = e.experience; + je["level"] = e.level; + je["factionId"] = e.factionId; + je["factionName"] = wowee::pipeline::WoweeGuild::factionName(e.factionId); + je["bankCopper"] = e.bankCopper; + je["emblem"] = e.emblem; + nlohmann::json ranks = nlohmann::json::array(); + for (const auto& r : e.ranks) { + ranks.push_back({ + {"rankIndex", r.rankIndex}, + {"name", r.name}, + {"permissionsMask", r.permissionsMask}, + {"moneyPerDayCopper", r.moneyPerDayCopper}, + }); + } + je["ranks"] = ranks; + nlohmann::json members = nlohmann::json::array(); + for (const auto& m : e.members) { + members.push_back({ + {"characterName", m.characterName}, + {"rankIndex", m.rankIndex}, + {"joinedDate", m.joinedDate}, + {"publicNote", m.publicNote}, + {"officerNote", m.officerNote}, + }); + } + je["members"] = members; + nlohmann::json tabs = nlohmann::json::array(); + for (const auto& t : e.bankTabs) { + tabs.push_back({ + {"tabIndex", t.tabIndex}, + {"name", t.name}, + {"iconPath", t.iconPath}, + {"depositPermissionMask", t.depositPermissionMask}, + {"withdrawPermissionMask", t.withdrawPermissionMask}, + {"viewPermissionMask", t.viewPermissionMask}, + }); + } + je["bankTabs"] = tabs; + nlohmann::json perks = nlohmann::json::array(); + for (const auto& p : e.perks) { + perks.push_back({ + {"perkId", p.perkId}, + {"name", p.name}, + {"spellId", p.spellId}, + {"requiredGuildLevel", p.requiredGuildLevel}, + }); + } + je["perks"] = perks; + arr.push_back(je); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WGLD: %s.wgld\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" guilds : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + for (const auto& e : c.entries) { + std::printf("\n guildId=%u faction=%s level=%u bank=%uc\n", + e.guildId, + wowee::pipeline::WoweeGuild::factionName(e.factionId), + e.level, e.bankCopper); + std::printf(" name : %s\n", e.name.c_str()); + std::printf(" leader : %s\n", e.leaderName.c_str()); + if (!e.motd.empty()) { + std::printf(" motd : %s\n", e.motd.c_str()); + } + std::printf(" ranks=%zu members=%zu tabs=%zu perks=%zu\n", + e.ranks.size(), e.members.size(), + e.bankTabs.size(), e.perks.size()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWgldExt(base); + if (!wowee::pipeline::WoweeGuildLoader::exists(base)) { + std::fprintf(stderr, + "validate-wgld: WGLD not found: %s.wgld\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGuildLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "guild " + std::to_string(k) + + " (id=" + std::to_string(e.guildId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.guildId == 0) errors.push_back(ctx + ": guildId is 0"); + if (e.name.empty()) errors.push_back(ctx + ": name is empty"); + if (e.leaderName.empty()) { + errors.push_back(ctx + ": leaderName is empty"); + } + if (e.factionId > wowee::pipeline::WoweeGuild::Horde) { + errors.push_back(ctx + ": factionId " + + std::to_string(e.factionId) + " not in 0..1"); + } + if (e.ranks.empty()) { + errors.push_back(ctx + ": no ranks (cannot have any members)"); + } + // Validate that each member's rankIndex resolves. + uint8_t maxRankIdx = 0; + for (const auto& r : e.ranks) { + if (r.rankIndex > maxRankIdx) maxRankIdx = r.rankIndex; + } + for (size_t mi = 0; mi < e.members.size(); ++mi) { + const auto& m = e.members[mi]; + if (m.rankIndex > maxRankIdx) { + errors.push_back(ctx + " member " + std::to_string(mi) + + " (" + m.characterName + "): rankIndex " + + std::to_string(m.rankIndex) + + " exceeds highest defined rank " + + std::to_string(maxRankIdx)); + } + if (m.characterName.empty()) { + errors.push_back(ctx + " member " + std::to_string(mi) + + ": characterName is empty"); + } + } + // Bank tab indices should be unique. + std::vector tabIdxSeen; + for (const auto& t : e.bankTabs) { + for (uint8_t prev : tabIdxSeen) { + if (prev == t.tabIndex) { + errors.push_back(ctx + + ": duplicate bank tabIndex " + + std::to_string(t.tabIndex)); + break; + } + } + tabIdxSeen.push_back(t.tabIndex); + } + // Perks should reference a non-zero spellId. + for (size_t pi = 0; pi < e.perks.size(); ++pi) { + const auto& p = e.perks[pi]; + if (p.spellId == 0) { + warnings.push_back(ctx + " perk " + std::to_string(pi) + + " (" + p.name + "): spellId is 0 (perk does nothing)"); + } + } + for (uint32_t prev : idsSeen) { + if (prev == e.guildId) { + errors.push_back(ctx + ": duplicate guildId"); + break; + } + } + idsSeen.push_back(e.guildId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wgld"] = base + ".wgld"; + 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-wgld: %s.wgld\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu guilds, all guildIds unique\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 handleGuildsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-guilds") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-guilds-full") == 0 && i + 1 < argc) { + outRc = handleGenFull(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-guilds-pair") == 0 && i + 1 < argc) { + outRc = handleGenFactionPair(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wgld") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wgld") == 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_guilds_catalog.hpp b/tools/editor/cli_guilds_catalog.hpp new file mode 100644 index 00000000..d1b775ef --- /dev/null +++ b/tools/editor/cli_guilds_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleGuildsCatalog(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 aac59ed7..a1d5ad8e 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1203,6 +1203,16 @@ void printUsage(const char* argv0) { std::printf(" Print WGEM gems (id / color / stat / item) + enchantments (id / slot / stat / duration / charges)\n"); std::printf(" --validate-wgem [--json]\n"); std::printf(" Static checks: ids>0+unique, name not empty, color/slot in range, stat-only entries need non-zero value\n"); + std::printf(" --gen-guilds [name]\n"); + std::printf(" Emit .wgld starter: 1 guild (Sentinels of Dawn) with default 5-rank ladder + 3 members\n"); + std::printf(" --gen-guilds-full [name]\n"); + std::printf(" Emit .wgld fleshed-out guild: 6 ranks + 8 members + 4 bank tabs (officer-only on tabs 3+4) + 3 perks\n"); + std::printf(" --gen-guilds-pair [name]\n"); + std::printf(" Emit .wgld 2 guilds: 1 Alliance (Light's Vanguard) + 1 Horde (Bloodfang Warband) with parallel ranks\n"); + std::printf(" --info-wgld [--json]\n"); + std::printf(" Print WGLD entries (id / faction / level / leader + motd / rank+member+tab+perk counts)\n"); + std::printf(" --validate-wgld [--json]\n"); + std::printf(" Static checks: id>0+unique, name+leader not empty, faction 0..1, members reference valid ranks, unique tab indices\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");