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