Kelsidavis-WoWee/tools/editor/cli_tabards_catalog.cpp

488 lines
17 KiB
C++
Raw Normal View History

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.
2026-05-10 01:24:46 -07:00
#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 parseBackgroundPatternToken(const std::string& s) {
using T = wowee::pipeline::WoweeTabards;
if (s == "solid") return T::Solid;
if (s == "gradient") return T::Gradient;
if (s == "chevron") return T::Chevron;
if (s == "quartered") return T::Quartered;
if (s == "starburst") return T::Starburst;
return -1;
}
int parseBorderPatternToken(const std::string& s) {
using T = wowee::pipeline::WoweeTabards;
if (s == "none") return T::BorderNone;
if (s == "thin") return T::BorderThin;
if (s == "thick") return T::BorderThick;
if (s == "decorative") return T::BorderDecorative;
return -1;
}
template <typename ParseFn>
bool readEnumField(const nlohmann::json& je,
const char* intKey,
const char* nameKey,
ParseFn parseFn,
const char* label,
uint32_t entryId,
uint8_t& outValue) {
if (je.contains(intKey)) {
const auto& v = je[intKey];
if (v.is_string()) {
int parsed = parseFn(v.get<std::string>());
if (parsed < 0) {
std::fprintf(stderr,
"import-wtbd-json: unknown %s token "
"'%s' on entry id=%u\n",
label, v.get<std::string>().c_str(),
entryId);
return false;
}
outValue = static_cast<uint8_t>(parsed);
return true;
}
if (v.is_number_integer()) {
outValue = static_cast<uint8_t>(v.get<int>());
return true;
}
}
if (je.contains(nameKey) && je[nameKey].is_string()) {
int parsed = parseFn(je[nameKey].get<std::string>());
if (parsed >= 0) {
outValue = static_cast<uint8_t>(parsed);
return true;
}
}
return true;
}
int handleExportJson(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string out;
if (parseOptArg(i, argc, argv)) out = argv[++i];
base = stripWtbdExt(base);
if (out.empty()) out = base + ".wtbd.json";
if (!wowee::pipeline::WoweeTabardsLoader::exists(base)) {
std::fprintf(stderr,
"export-wtbd-json: WTBD not found: %s.wtbd\n",
base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTabardsLoader::load(base);
nlohmann::json j;
j["magic"] = "WTBD";
j["version"] = 1;
j["name"] = c.name;
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::ofstream os(out);
if (!os) {
std::fprintf(stderr,
"export-wtbd-json: failed to open %s for write\n",
out.c_str());
return 1;
}
os << j.dump(2) << "\n";
std::printf("Wrote %s (%zu tabards)\n",
out.c_str(), c.entries.size());
return 0;
}
int handleImportJson(int& i, int argc, char** argv) {
std::string in = argv[++i];
std::string outBase;
if (parseOptArg(i, argc, argv)) outBase = argv[++i];
if (outBase.empty()) {
outBase = in;
if (outBase.size() >= 10 &&
outBase.substr(outBase.size() - 10) == ".wtbd.json") {
outBase.resize(outBase.size() - 10);
} else {
stripExt(outBase, ".json");
stripExt(outBase, ".wtbd");
}
}
std::ifstream is(in);
if (!is) {
std::fprintf(stderr,
"import-wtbd-json: cannot open %s\n", in.c_str());
return 1;
}
nlohmann::json j;
try {
is >> j;
} catch (const std::exception& ex) {
std::fprintf(stderr,
"import-wtbd-json: JSON parse error: %s\n", ex.what());
return 1;
}
wowee::pipeline::WoweeTabards c;
c.name = j.value("name", std::string{});
if (!j.contains("entries") || !j["entries"].is_array()) {
std::fprintf(stderr,
"import-wtbd-json: missing or non-array 'entries'\n");
return 1;
}
for (const auto& je : j["entries"]) {
wowee::pipeline::WoweeTabards::Entry e;
e.tabardId = je.value("tabardId", 0u);
e.name = je.value("name", std::string{});
e.description = je.value("description", std::string{});
if (!readEnumField(je, "backgroundPattern",
"backgroundPatternName",
parseBackgroundPatternToken,
"backgroundPattern",
e.tabardId,
e.backgroundPattern)) return 1;
if (!readEnumField(je, "borderPattern",
"borderPatternName",
parseBorderPatternToken,
"borderPattern",
e.tabardId,
e.borderPattern)) return 1;
e.emblemId = static_cast<uint16_t>(
je.value("emblemId", 0u));
e.backgroundColor = je.value("backgroundColor",
0xFF000000u);
e.borderColor = je.value("borderColor", 0xFFFFFFFFu);
e.emblemColor = je.value("emblemColor", 0xFFFFFFFFu);
e.guildId = je.value("guildId", 0u);
e.creatorPlayerId = je.value("creatorPlayerId", 0u);
if (je.contains("isApproved")) {
const auto& a = je["isApproved"];
if (a.is_boolean())
e.isApproved = a.get<bool>() ? 1 : 0;
else if (a.is_number_integer())
e.isApproved = static_cast<uint8_t>(
a.get<int>() != 0 ? 1 : 0);
}
e.iconColorRGBA = je.value("iconColorRGBA", 0xFFFFFFFFu);
c.entries.push_back(e);
}
if (!wowee::pipeline::WoweeTabardsLoader::save(c, outBase)) {
std::fprintf(stderr,
"import-wtbd-json: failed to save %s.wtbd\n",
outBase.c_str());
return 1;
}
std::printf("Wrote %s.wtbd (%zu tabards)\n",
outBase.c_str(), c.entries.size());
return 0;
}
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.
2026-05-10 01:24:46 -07:00
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;
}
if (std::strcmp(argv[i], "--export-wtbd-json") == 0 && i + 1 < argc) {
outRc = handleExportJson(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--import-wtbd-json") == 0 && i + 1 < argc) {
outRc = handleImportJson(i, argc, argv); return true;
}
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.
2026-05-10 01:24:46 -07:00
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee