mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
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:
parent
2d78dd57a7
commit
0f4c619b49
12 changed files with 863 additions and 2 deletions
289
tools/editor/cli_tabards_catalog.cpp
Normal file
289
tools/editor/cli_tabards_catalog.cpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue