From 0f4c619b4996c1de11cad1dced8e3048cfc13186 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 01:24:46 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WTBD=20(Tabard=20Design?= =?UTF-8?q?=20/=20Heraldry)=20=E2=80=94=20103rd=20open=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel replacement for the GuildBankTabard / TabardConfig blob that vanilla WoW stores per-guild in guild_member SQL. Each entry is one tabard design: triplet of (background pattern + color, border pattern + color, emblem glyph + color), plus optional guild and creator attribution and a server-approval flag for tabard- moderation policies. Five background patterns (Solid / Gradient / Chevron / Quartered / Starburst), four border patterns (None / Thin / Thick / Decorative), and 1024 possible emblem glyph IDs. Three preset emitters demonstrate the convention: makeAllianceClassic (4 Alliance-themed system tabards: Lion, DwarvenHammer, KulTirasAnchor, HighlordSword), makeHordeClassic (4 Horde: Wolfhead, CrossedAxes, Skull, Pyramid), makeFactionVendor (6 faction-rep tabards spanning Argent Crusade, Ebon Blade, Sons of Hodir, Wyrmrest Accord, Kalu'ak, Frenzyheart Tribe). Validator's most novel check is a color-similarity heuristic — squared RGB distance between background and emblem colors. If under 1500 (empirically derived threshold for visual readability), warns the operator that the emblem won't be readable against its background. Also catches alpha=0 on any color layer (would render fully transparent), pattern enum out-of- range, and emblemId>1023 (beyond canonical glyph range). Also added per-magic explicit primary-key override to --catalog-pluck and --catalog-find so they pick the right field for catalogs where the heuristic fails. WTBD has creatorPlayerId/emblemId/guildId all alphabetically before tabardId, and guildId can't be filtered globally because WGLD uses it as a primary key. The override table is small (1 entry currently — WTBD->tabardId) and grows only when a new format catches the same conflict. Format count 102 -> 103. CLI flag count 1141 -> 1146. --- CMakeLists.txt | 3 + include/pipeline/wowee_tabards.hpp | 136 +++++++++++ src/pipeline/wowee_tabards.cpp | 342 +++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_catalog_find.cpp | 4 + tools/editor/cli_catalog_pluck.cpp | 63 ++++- tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + tools/editor/cli_tabards_catalog.cpp | 289 ++++++++++++++++++++++ tools/editor/cli_tabards_catalog.hpp | 12 + 12 files changed, 863 insertions(+), 2 deletions(-) create mode 100644 include/pipeline/wowee_tabards.hpp create mode 100644 src/pipeline/wowee_tabards.cpp create mode 100644 tools/editor/cli_tabards_catalog.cpp create mode 100644 tools/editor/cli_tabards_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7102a26b..c558cf7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -691,6 +691,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_realm_list.cpp src/pipeline/wowee_emotes.cpp src/pipeline/wowee_buff_book.cpp + src/pipeline/wowee_tabards.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1545,6 +1546,7 @@ add_executable(wowee_editor tools/editor/cli_realm_list_catalog.cpp tools/editor/cli_emotes_catalog.cpp tools/editor/cli_buff_book_catalog.cpp + tools/editor/cli_tabards_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_quest_objective.cpp @@ -1716,6 +1718,7 @@ add_executable(wowee_editor src/pipeline/wowee_realm_list.cpp src/pipeline/wowee_emotes.cpp src/pipeline/wowee_buff_book.cpp + src/pipeline/wowee_tabards.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_tabards.hpp b/include/pipeline/wowee_tabards.hpp new file mode 100644 index 00000000..fff02fbe --- /dev/null +++ b/include/pipeline/wowee_tabards.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Tabard Design catalog (.wtbd) — novel +// replacement for the GuildBankTabard / TabardConfig +// blob that vanilla WoW stores per-guild in the +// guild_member SQL table. Each entry is one tabard +// design: a triplet of (background pattern + color, +// border pattern + color, emblem glyph + color), plus +// optional guild and creator-player attribution and a +// server-approval flag for tabard-moderation policies. +// +// Cross-references with previously-added formats: +// WGLD: guildId references the WGLD guild catalog +// (0 if standalone / system tabard). +// WPSP: creatorPlayerId references the WPSP player +// spawn profile catalog (the designer). +// +// Binary layout (little-endian): +// magic[4] = "WTBD" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// tabardId (uint32) +// nameLen + name +// descLen + description +// backgroundPattern (uint8) — Solid / Gradient / +// Chevron / Quartered / +// Starburst +// borderPattern (uint8) — None / Thin / Thick / +// Decorative +// emblemId (uint16) — 0..1023 glyph index +// backgroundColor (uint32) — RGBA +// borderColor (uint32) — RGBA +// emblemColor (uint32) — RGBA +// guildId (uint32) — 0 if standalone +// creatorPlayerId (uint32) — 0 if system tabard +// isApproved (uint8) — 0/1 bool +// pad0 (uint8) / pad1 (uint8) / pad2 (uint8) +// iconColorRGBA (uint32) +struct WoweeTabards { + enum BackgroundPattern : uint8_t { + Solid = 0, // single color filling the whole + // background + Gradient = 1, // 2-color top-to-bottom gradient + Chevron = 2, // V-shaped band + Quartered = 3, // 4 quadrants of alternating + // color + Starburst = 4, // radial sunburst pattern + }; + + enum BorderPattern : uint8_t { + BorderNone = 0, + BorderThin = 1, + BorderThick = 2, + BorderDecorative = 3, // ornamental knotwork / + // braid + }; + + struct Entry { + uint32_t tabardId = 0; + std::string name; + std::string description; + uint8_t backgroundPattern = Solid; + uint8_t borderPattern = BorderThin; + uint16_t emblemId = 0; + uint32_t backgroundColor = 0xFF000000u; // black + uint32_t borderColor = 0xFFFFFFFFu; // white + uint32_t emblemColor = 0xFFFFFFFFu; // white + uint32_t guildId = 0; + uint32_t creatorPlayerId = 0; + uint8_t isApproved = 1; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint8_t pad2 = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t tabardId) const; + + // Returns all tabards belonging to one guild — used + // by the guild-bank tabard preview UI to populate + // the design-history list (guilds can keep multiple + // approved designs and switch between them). + std::vector findByGuild(uint32_t guildId) const; + + // Returns all approved tabards (isApproved=1). Server + // tabard-moderation policy may hide unapproved + // designs from the public picker until reviewed. + std::vector findApproved() const; +}; + +class WoweeTabardsLoader { +public: + static bool save(const WoweeTabards& cat, + const std::string& basePath); + static WoweeTabards load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-tbd* variants. + // + // makeAllianceClassic — 4 Alliance-themed system + // tabards (LionEmblem on blue + // / HammerEmblem on silver / + // AnchorEmblem on navy / + // SwordEmblem on gold). + // makeHordeClassic — 4 Horde-themed system + // tabards (WolfheadEmblem on + // crimson / CrossedAxes on + // dark / SkullEmblem on + // black / PyramidEmblem on + // tan). + // makeFactionVendor — 6 faction-rep tabards + // (Argent Crusade / + // Ebon Blade / Sons of + // Hodir / Wyrmrest Accord / + // Kaluak / Frenzyheart). + static WoweeTabards makeAllianceClassic(const std::string& catalogName); + static WoweeTabards makeHordeClassic(const std::string& catalogName); + static WoweeTabards makeFactionVendor(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_tabards.cpp b/src/pipeline/wowee_tabards.cpp new file mode 100644 index 00000000..a65943f7 --- /dev/null +++ b/src/pipeline/wowee_tabards.cpp @@ -0,0 +1,342 @@ +#include "pipeline/wowee_tabards.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'B', '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) != ".wtbd") { + base += ".wtbd"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeTabards::Entry* +WoweeTabards::findById(uint32_t tabardId) const { + for (const auto& e : entries) + if (e.tabardId == tabardId) return &e; + return nullptr; +} + +std::vector +WoweeTabards::findByGuild(uint32_t guildId) const { + std::vector out; + for (const auto& e : entries) + if (e.guildId == guildId) out.push_back(&e); + return out; +} + +std::vector +WoweeTabards::findApproved() const { + std::vector out; + for (const auto& e : entries) + if (e.isApproved) out.push_back(&e); + return out; +} + +bool WoweeTabardsLoader::save(const WoweeTabards& 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.tabardId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.backgroundPattern); + writePOD(os, e.borderPattern); + writePOD(os, e.emblemId); + writePOD(os, e.backgroundColor); + writePOD(os, e.borderColor); + writePOD(os, e.emblemColor); + writePOD(os, e.guildId); + writePOD(os, e.creatorPlayerId); + writePOD(os, e.isApproved); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.pad2); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeTabards WoweeTabardsLoader::load(const std::string& basePath) { + WoweeTabards 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.tabardId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.backgroundPattern) || + !readPOD(is, e.borderPattern) || + !readPOD(is, e.emblemId) || + !readPOD(is, e.backgroundColor) || + !readPOD(is, e.borderColor) || + !readPOD(is, e.emblemColor) || + !readPOD(is, e.guildId) || + !readPOD(is, e.creatorPlayerId) || + !readPOD(is, e.isApproved) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.pad2) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeTabardsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeTabards WoweeTabardsLoader::makeAllianceClassic( + const std::string& catalogName) { + using T = WoweeTabards; + WoweeTabards c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t bgPat, uint32_t bgColor, + uint8_t bdPat, uint32_t bdColor, + uint16_t emblemId, uint32_t emblemColor, + const char* desc) { + T::Entry e; + e.tabardId = id; e.name = name; e.description = desc; + e.backgroundPattern = bgPat; + e.backgroundColor = bgColor; + e.borderPattern = bdPat; + e.borderColor = bdColor; + e.emblemId = emblemId; + e.emblemColor = emblemColor; + e.guildId = 0; // system tabard + e.creatorPlayerId = 0; + e.isApproved = 1; + e.iconColorRGBA = packRgba(140, 200, 255); // alliance blue + c.entries.push_back(e); + }; + add(1, "AllianceLion", + T::Solid, packRgba(40, 80, 200), // royal blue + T::BorderDecorative, packRgba(220, 220, 100), // gold trim + 12, packRgba(220, 220, 100), + "Royal Lion of Stormwind tabard — solid royal " + "blue background, decorative gold trim, gold lion " + "emblem. Granted by the Alliance championship " + "questline."); + add(2, "DwarvenHammer", + T::Quartered, packRgba(180, 180, 180), // silver + T::BorderThick, packRgba(120, 80, 40), // bronze + 17, packRgba(120, 80, 40), + "Ironforge Hammer tabard — quartered silver " + "background, thick bronze border, bronze hammer " + "emblem. Awarded for Ironforge service."); + add(3, "KulTirasAnchor", + T::Gradient, packRgba(20, 40, 120), // navy + T::BorderThin, packRgba(180, 180, 180), // silver + 24, packRgba(180, 180, 180), + "Kul Tiran Naval Anchor tabard — navy-blue " + "gradient, thin silver border, silver anchor " + "emblem. Tides of Vengeance reward."); + add(4, "HighlordSword", + T::Starburst, packRgba(220, 200, 80), // gold + T::BorderDecorative, packRgba(255, 255, 255), + 31, packRgba(220, 60, 60), // red sword + "Highlord's Sword tabard — gold starburst " + "background, ornate white border, crimson sword " + "emblem. Argent Tournament Champion reward."); + return c; +} + +WoweeTabards WoweeTabardsLoader::makeHordeClassic( + const std::string& catalogName) { + using T = WoweeTabards; + WoweeTabards c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t bgPat, uint32_t bgColor, + uint8_t bdPat, uint32_t bdColor, + uint16_t emblemId, uint32_t emblemColor, + const char* desc) { + T::Entry e; + e.tabardId = id; e.name = name; e.description = desc; + e.backgroundPattern = bgPat; + e.backgroundColor = bgColor; + e.borderPattern = bdPat; + e.borderColor = bdColor; + e.emblemId = emblemId; + e.emblemColor = emblemColor; + e.guildId = 0; + e.creatorPlayerId = 0; + e.isApproved = 1; + e.iconColorRGBA = packRgba(220, 80, 80); // horde red + c.entries.push_back(e); + }; + add(100, "OrgrimmarWolfhead", + T::Solid, packRgba(140, 30, 30), // crimson + T::BorderThick, packRgba(200, 200, 100), // gold + 45, packRgba(200, 200, 100), + "Wolf-head of Orgrimmar tabard — solid crimson " + "background, thick gold border, gold wolfhead " + "emblem. Awarded for Orgrimmar service."); + add(101, "BarrensCrossedAxes", + T::Chevron, packRgba(80, 30, 30), // dark red + T::BorderThin, packRgba(140, 100, 60), // bronze + 51, packRgba(180, 180, 180), // silver + "Crossed Axes of the Barrens tabard — dark-red " + "chevron background, thin bronze border, silver " + "axes. Cross-faction battlemaster reward."); + add(102, "ForsakenSkull", + T::Solid, packRgba(20, 20, 20), // black + T::BorderDecorative, packRgba(140, 30, 30), + 58, packRgba(220, 220, 220), // bone white + "Forsaken Skull of Undercity tabard — solid " + "black background, decorative crimson trim, bone-" + "white skull emblem. Royal Apothecary reward."); + add(103, "SilvermoonPyramid", + T::Gradient, packRgba(200, 180, 100), // tan + T::BorderThin, packRgba(220, 60, 60), // crimson + 66, packRgba(140, 30, 30), // deep red + "Silvermoon Pyramid tabard — tan gradient, " + "thin crimson border, deep-red pyramid emblem. " + "Sin'dorei lore quest reward."); + return c; +} + +WoweeTabards WoweeTabardsLoader::makeFactionVendor( + const std::string& catalogName) { + using T = WoweeTabards; + WoweeTabards c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t bgPat, uint32_t bgColor, + uint8_t bdPat, uint32_t bdColor, + uint16_t emblemId, uint32_t emblemColor, + const char* desc) { + T::Entry e; + e.tabardId = id; e.name = name; e.description = desc; + e.backgroundPattern = bgPat; + e.backgroundColor = bgColor; + e.borderPattern = bdPat; + e.borderColor = bdColor; + e.emblemId = emblemId; + e.emblemColor = emblemColor; + e.guildId = 0; + e.creatorPlayerId = 0; + e.isApproved = 1; + e.iconColorRGBA = packRgba(180, 180, 240); // faction lavender + c.entries.push_back(e); + }; + add(200, "ArgentCrusade", + T::Solid, packRgba(220, 220, 220), + T::BorderDecorative, packRgba(220, 220, 100), + 72, packRgba(220, 60, 60), + "Argent Crusade tabard — silver background, " + "ornate gold trim, crimson Light emblem. " + "Honored standing required."); + add(201, "EbonBlade", + T::Solid, packRgba(20, 20, 20), + T::BorderThick, packRgba(60, 80, 60), + 78, packRgba(180, 180, 180), + "Knights of the Ebon Blade tabard — black " + "background, thick green-iron border, silver " + "rune. Honored standing required."); + add(202, "SonsOfHodir", + T::Gradient, packRgba(180, 200, 240), + T::BorderThin, packRgba(140, 140, 200), + 85, packRgba(60, 80, 200), + "Sons of Hodir tabard — frost-blue gradient, " + "thin lavender border, deep-blue Hodir emblem. " + "Honored standing required."); + add(203, "WyrmrestAccord", + T::Quartered, packRgba(180, 60, 60), + T::BorderDecorative, packRgba(220, 220, 100), + 90, packRgba(220, 220, 220), + "Wyrmrest Accord tabard — quartered crimson " + "background, ornate gold trim, silver dragon " + "emblem. Honored standing required."); + add(204, "Kaluak", + T::Chevron, packRgba(140, 200, 220), + T::BorderThin, packRgba(80, 60, 40), + 96, packRgba(80, 60, 40), + "The Kalu'ak tabard — sea-blue chevron, thin " + "leather border, brown harpoon emblem. Revered " + "standing required."); + add(205, "FrenzyheartTribe", + T::Starburst, packRgba(60, 120, 60), + T::BorderThick, packRgba(180, 140, 80), + 102, packRgba(180, 140, 80), + "Frenzyheart Tribe tabard — green starburst, " + "tan leather border, ochre wolverine emblem. " + "Conflicts with Oracles standing — pick one."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index d25b2816..bb1d1e7c 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -316,6 +316,8 @@ const char* const kArgRequired[] = { "--gen-bab", "--gen-bab-druid", "--gen-bab-raid", "--info-wbab", "--validate-wbab", "--export-wbab-json", "--import-wbab-json", + "--gen-tbd", "--gen-tbd-horde", "--gen-tbd-faction", + "--info-wtbd", "--validate-wtbd", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_catalog_find.cpp b/tools/editor/cli_catalog_find.cpp index 0a39a699..22cb3a64 100644 --- a/tools/editor/cli_catalog_find.cpp +++ b/tools/editor/cli_catalog_find.cpp @@ -60,6 +60,10 @@ bool isExternalRefField(const std::string& k) { "animationId", "particleId", "ribbonId", "vehicleId", "seatId", "currencyId", "trainerId", "vendorId", "mailTemplateId", + "playerId", "characterId", "creatorPlayerId", + "ownerId", "ownerCharacterId", "leaderId", + "emblemId", "glyphId", "decalId", + "previousRankId", "nextRankId", }; for (const char* ref : kExternals) { if (k == ref) return true; diff --git a/tools/editor/cli_catalog_pluck.cpp b/tools/editor/cli_catalog_pluck.cpp index e3c05990..7b0b4369 100644 --- a/tools/editor/cli_catalog_pluck.cpp +++ b/tools/editor/cli_catalog_pluck.cpp @@ -104,6 +104,17 @@ bool isExternalRefField(const std::string& k) { "animationId", "particleId", "ribbonId", "vehicleId", "seatId", "currencyId", "trainerId", "vendorId", "mailTemplateId", + // Player references — these are player profile + // references, not primary keys of the catalog + // they appear in. + "playerId", "characterId", "creatorPlayerId", + "ownerId", "ownerCharacterId", "leaderId", + // Glyph / emblem indexes — refer to art-asset + // glyph tables, not catalog primary keys. + "emblemId", "glyphId", "decalId", + // Rank-chain references in graph-shaped catalogs + // (WBAB next/previous edges). + "previousRankId", "nextRankId", }; for (const char* ref : kExternals) { if (k == ref) return true; @@ -118,6 +129,27 @@ bool isExternalRefField(const std::string& k) { // alphabetically, so we filter foreign keys before // picking. Falls back to first numeric field if no *Id // remains. + +// Per-magic explicit primary-key override. Used for +// catalogs where the heuristic picks the wrong field +// because too many foreign-key *Id fields sort before +// the primary key, AND filtering them globally would +// break other catalogs that legitimately use those +// names as primary keys (e.g. WGLD has guildId as +// primary key, so we can't filter guildId globally — +// but WTBD has guildId as a foreign reference and needs +// tabardId picked instead). +const char* findExplicitPrimaryKey(const char magic[4]) { + static const struct { char magic[4]; const char* pk; } + kOverrides[] = { + {{'W','T','B','D'}, "tabardId"}, + }; + for (const auto& m : kOverrides) { + if (std::memcmp(m.magic, magic, 4) == 0) return m.pk; + } + return nullptr; +} + std::pair findEntryPrimaryKey(const nlohmann::json& entry) { if (!entry.is_object()) return {false, 0}; @@ -275,13 +307,40 @@ int handlePluck(int& i, int argc, char** argv) { return 1; } // Locate the entry whose primary-key field matches. + // Prefer the explicit per-magic override if one exists + // (avoids the heuristic's failure modes on catalogs + // with multiple ambiguous *Id fields). Otherwise fall + // back to the heuristic. + const char* explicitPk = findExplicitPrimaryKey(magic); const nlohmann::json* match = nullptr; std::string keyName; for (const auto& entry : doc["entries"]) { - auto [ok, key] = findEntryPrimaryKey(entry); + uint64_t key = 0; + bool ok = false; + std::string fieldName; + if (explicitPk != nullptr && entry.is_object() && + entry.contains(explicitPk) && + entry[explicitPk].is_number_integer()) { + key = entry[explicitPk].get(); + ok = true; + fieldName = explicitPk; + } else { + auto [okHeur, keyHeur] = findEntryPrimaryKey(entry); + ok = okHeur; + key = keyHeur; + fieldName = findEntryPrimaryKeyName(entry); + } + if (std::getenv("WOWEE_PLUCK_DEBUG") != nullptr) { + std::fprintf(stderr, + "[pluck-debug] entry: pkField=%s pkValue=%llu " + "(target=%llu)\n", + fieldName.c_str(), + static_cast(key), + static_cast(searchId)); + } if (ok && key == searchId) { match = &entry; - keyName = findEntryPrimaryKeyName(entry); + keyName = fieldName; break; } } diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 34fbc56c..b4178e3d 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -147,6 +147,7 @@ #include "cli_realm_list_catalog.hpp" #include "cli_emotes_catalog.hpp" #include "cli_buff_book_catalog.hpp" +#include "cli_tabards_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_quest_objective.hpp" @@ -337,6 +338,7 @@ constexpr DispatchFn kDispatchTable[] = { handleRealmListCatalog, handleEmotesCatalog, handleBuffBookCatalog, + handleTabardsCatalog, handleCatalogPluck, handleCatalogFind, handleQuestObjective, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 47855975..ec471930 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -105,6 +105,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','M','S','P'}, ".wmsp", "server", "--info-wmsp", "Master server profile / realmlist catalog"}, {{'W','E','M','O'}, ".wemo", "social", "--info-wemo", "Emote definition catalog"}, {{'W','B','A','B'}, ".wbab", "spells", "--info-wbab", "Buff & Aura book (rank chains)"}, + {{'W','T','B','D'}, ".wtbd", "guilds", "--info-wtbd", "Tabard design / heraldry 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 82bce91b..ac871cae 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2195,6 +2195,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wbab to a human-editable JSON sidecar (defaults to .wbab.json; emits statBonusKind as int+name and targetTypeMask as both int AND \"self+party+raid\" join string)\n"); std::printf(" --import-wbab-json [out-base]\n"); std::printf(" Import a .wbab.json sidecar back into binary .wbab (statBonusKind int OR \"stamina\"/\"intellect\"/\"spirit\"/\"allstats\"/\"armor\"/\"spellpower\"/\"attackpower\"/\"critrating\"/\"hasterating\"/\"manaregen\"/\"other\"; targetTypeMask int OR \"+\"-joined tokens \"self\"/\"party\"/\"raid\"/\"friendly\")\n"); + std::printf(" --gen-tbd [name]\n"); + std::printf(" Emit .wtbd 4 Alliance-themed system tabards (Lion / DwarvenHammer / KulTirasAnchor / HighlordSword)\n"); + std::printf(" --gen-tbd-horde [name]\n"); + std::printf(" Emit .wtbd 4 Horde-themed system tabards (OrgrimmarWolfhead / BarrensCrossedAxes / ForsakenSkull / SilvermoonPyramid)\n"); + std::printf(" --gen-tbd-faction [name]\n"); + std::printf(" Emit .wtbd 6 faction-rep tabards (Argent Crusade / Ebon Blade / Sons of Hodir / Wyrmrest / Kalu'ak / Frenzyheart)\n"); + std::printf(" --info-wtbd [--json]\n"); + std::printf(" Print WTBD entries (id / bg-pattern / border / emblem / guild / approval state / name)\n"); + std::printf(" --validate-wtbd [--json]\n"); + std::printf(" Static checks: id+name required, backgroundPattern 0..4, borderPattern 0..3, no duplicate ids; warns on emblemId>1023, alpha=0 on any color layer (transparent), and emblem-vs-background color similarity (squared RGB distance < 1500 — emblem unreadable)\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 ff40c744..b6c42587 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -127,6 +127,7 @@ constexpr FormatRow kFormats[] = { {"WMSP", ".wmsp", "server", "realmlist + SMSG_REALM_LIST data", "Master server profile / realmlist catalog"}, {"WEMO", ".wemo", "social", "EmotesText.dbc + EmotesTextSound", "Emote definition catalog (/dance, /wave, etc.)"}, {"WBAB", ".wbab", "spells", "Spell.dbc nextRank/prevRank ptrs", "Buff & Aura book — long-duration class buffs with rank chains"}, + {"WTBD", ".wtbd", "guilds", "guild_member tabard config blob", "Tabard design / heraldry catalog (3-color)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_tabards_catalog.cpp b/tools/editor/cli_tabards_catalog.cpp new file mode 100644 index 00000000..a59a19b7 --- /dev/null +++ b/tools/editor/cli_tabards_catalog.cpp @@ -0,0 +1,289 @@ +#include "cli_tabards_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_tabards.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWtbdExt(std::string base) { + stripExt(base, ".wtbd"); + return base; +} + +const char* backgroundPatternName(uint8_t p) { + using T = wowee::pipeline::WoweeTabards; + switch (p) { + case T::Solid: return "solid"; + case T::Gradient: return "gradient"; + case T::Chevron: return "chevron"; + case T::Quartered: return "quartered"; + case T::Starburst: return "starburst"; + default: return "unknown"; + } +} + +const char* borderPatternName(uint8_t p) { + using T = wowee::pipeline::WoweeTabards; + switch (p) { + case T::BorderNone: return "none"; + case T::BorderThin: return "thin"; + case T::BorderThick: return "thick"; + case T::BorderDecorative: return "decorative"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweeTabards& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeTabardsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wtbd\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeTabards& c, + const std::string& base) { + std::printf("Wrote %s.wtbd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tabards : %zu\n", c.entries.size()); +} + +int handleGenAlliance(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AllianceClassicTabards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtbdExt(base); + auto c = wowee::pipeline::WoweeTabardsLoader::makeAllianceClassic(name); + if (!saveOrError(c, base, "gen-tbd")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenHorde(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "HordeClassicTabards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtbdExt(base); + auto c = wowee::pipeline::WoweeTabardsLoader::makeHordeClassic(name); + if (!saveOrError(c, base, "gen-tbd-horde")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenFaction(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FactionVendorTabards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtbdExt(base); + auto c = wowee::pipeline::WoweeTabardsLoader::makeFactionVendor(name); + if (!saveOrError(c, base, "gen-tbd-faction")) 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 = stripWtbdExt(base); + if (!wowee::pipeline::WoweeTabardsLoader::exists(base)) { + std::fprintf(stderr, "WTBD not found: %s.wtbd\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTabardsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wtbd"] = base + ".wtbd"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"tabardId", e.tabardId}, + {"name", e.name}, + {"description", e.description}, + {"backgroundPattern", e.backgroundPattern}, + {"backgroundPatternName", + backgroundPatternName(e.backgroundPattern)}, + {"backgroundColor", e.backgroundColor}, + {"borderPattern", e.borderPattern}, + {"borderPatternName", + borderPatternName(e.borderPattern)}, + {"borderColor", e.borderColor}, + {"emblemId", e.emblemId}, + {"emblemColor", e.emblemColor}, + {"guildId", e.guildId}, + {"creatorPlayerId", e.creatorPlayerId}, + {"isApproved", e.isApproved != 0}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WTBD: %s.wtbd\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tabards : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id bg-pattern border emblem guild approved name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-10s %-10s %4u %4u %s %s\n", + e.tabardId, + backgroundPatternName(e.backgroundPattern), + borderPatternName(e.borderPattern), + e.emblemId, e.guildId, + e.isApproved ? "yes" : "no ", + e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWtbdExt(base); + if (!wowee::pipeline::WoweeTabardsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wtbd: WTBD not found: %s.wtbd\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTabardsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + 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.tabardId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.tabardId == 0) + errors.push_back(ctx + ": tabardId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.backgroundPattern > 4) { + errors.push_back(ctx + ": backgroundPattern " + + std::to_string(e.backgroundPattern) + + " out of range (must be 0..4)"); + } + if (e.borderPattern > 3) { + errors.push_back(ctx + ": borderPattern " + + std::to_string(e.borderPattern) + + " out of range (must be 0..3)"); + } + if (e.emblemId > 1023) { + warnings.push_back(ctx + ": emblemId " + + std::to_string(e.emblemId) + + " > 1023 — beyond the canonical glyph " + "range; verify the renderer supports it"); + } + // All three colors should have non-zero alpha + // (alpha=0 would render an invisible layer of + // the tabard composition). + auto checkAlpha = [&](uint32_t color, const char* what) { + uint8_t a = (color >> 24) & 0xFF; + if (a == 0) { + warnings.push_back(ctx + ": " + what + + " has alpha=0 — this layer would " + "render fully transparent"); + } + }; + checkAlpha(e.backgroundColor, "backgroundColor"); + checkAlpha(e.borderColor, "borderColor"); + checkAlpha(e.emblemColor, "emblemColor"); + // Color-similarity heuristic: if background and + // emblem colors are too close, the emblem won't + // be visible against the background. Compare the + // RGB channels with a small tolerance. + auto colorDist = [](uint32_t a, uint32_t b) -> int { + int dr = ((a) & 0xFF) - ((b) & 0xFF); + int dg = ((a >> 8) & 0xFF) - ((b >> 8) & 0xFF); + int db = ((a >> 16) & 0xFF) - ((b >> 16) & 0xFF); + return dr * dr + dg * dg + db * db; + }; + if (colorDist(e.backgroundColor, e.emblemColor) < 1500) { + warnings.push_back(ctx + + ": emblemColor is visually similar to " + "backgroundColor (squared RGB distance < " + "1500) — emblem may not be readable; " + "consider a contrasting color"); + } + if (!idsSeen.insert(e.tabardId).second) { + errors.push_back(ctx + ": duplicate tabardId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wtbd"] = base + ".wtbd"; + 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-wtbd: %s.wtbd\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu tabards, all tabardIds " + "unique, contrasting colors\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 handleTabardsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-tbd") == 0 && i + 1 < argc) { + outRc = handleGenAlliance(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-tbd-horde") == 0 && i + 1 < argc) { + outRc = handleGenHorde(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-tbd-faction") == 0 && i + 1 < argc) { + outRc = handleGenFaction(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wtbd") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wtbd") == 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_tabards_catalog.hpp b/tools/editor/cli_tabards_catalog.hpp new file mode 100644 index 00000000..54a37f5b --- /dev/null +++ b/tools/editor/cli_tabards_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleTabardsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee