feat(editor): add WTBD (Tabard Design / Heraldry) — 103rd open format

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.
This commit is contained in:
Kelsi 2026-05-10 01:24:46 -07:00
parent 2d78dd57a7
commit 0f4c619b49
12 changed files with 863 additions and 2 deletions

View file

@ -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 <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <set>
#include <string>
#include <vector>
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<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::set<uint32_t> 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