mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
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.
355 lines
11 KiB
C++
355 lines
11 KiB
C++
#include "cli_catalog_find.hpp"
|
|
#include "cli_arg_parse.hpp"
|
|
#include "cli_format_table.hpp"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace wowee {
|
|
namespace editor {
|
|
namespace cli {
|
|
|
|
namespace {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
std::string shellQuote(const std::string& s) {
|
|
std::string out;
|
|
out.reserve(s.size() + 2);
|
|
out.push_back('\'');
|
|
for (char c : s) {
|
|
if (c == '\'') out += "'\"'\"'";
|
|
else out.push_back(c);
|
|
}
|
|
out.push_back('\'');
|
|
return out;
|
|
}
|
|
|
|
bool peekMagic(const fs::path& path, char magic[4]) {
|
|
std::ifstream is(path, std::ios::binary);
|
|
if (!is) return false;
|
|
if (!is.read(magic, 4) || is.gcount() != 4) return false;
|
|
return true;
|
|
}
|
|
|
|
// Same external-ref filter as cli_catalog_pluck. Kept in
|
|
// sync — when a new format adds a foreign-key suffix that
|
|
// the old filter misses, both files must be updated.
|
|
// Future cleanup: share via cli_catalog_pluck.hpp once
|
|
// either utility needs a third common helper.
|
|
bool isExternalRefField(const std::string& k) {
|
|
static const char* kExternals[] = {
|
|
"mapId", "areaId", "zoneId", "subAreaId",
|
|
"spellId", "itemId", "npcId", "creatureId",
|
|
"objectId", "gameObjectId",
|
|
"factionId", "factionTemplateId",
|
|
"difficultyId", "instanceId",
|
|
"raceId", "classId", "classMask", "raceMask",
|
|
"skillLineId", "questId", "talentId",
|
|
"achievementId", "criteriaId", "lootId",
|
|
"soundId", "movieId", "displayId", "modelId",
|
|
"iconId", "textureId", "auraId",
|
|
"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;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::pair<bool, uint64_t>
|
|
findEntryPrimaryKey(const nlohmann::json& entry) {
|
|
if (!entry.is_object()) return {false, 0};
|
|
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
|
const std::string& k = it.key();
|
|
if (k.size() >= 2 &&
|
|
k.compare(k.size() - 2, 2, "Id") == 0 &&
|
|
it.value().is_number_integer() &&
|
|
!isExternalRefField(k)) {
|
|
return {true, it.value().get<uint64_t>()};
|
|
}
|
|
}
|
|
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
|
const std::string& k = it.key();
|
|
if (k.size() >= 2 &&
|
|
k.compare(k.size() - 2, 2, "Id") == 0 &&
|
|
it.value().is_number_integer()) {
|
|
return {true, it.value().get<uint64_t>()};
|
|
}
|
|
}
|
|
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
|
if (it.value().is_number_integer()) {
|
|
return {true, it.value().get<uint64_t>()};
|
|
}
|
|
}
|
|
return {false, 0};
|
|
}
|
|
|
|
std::string findEntryPrimaryKeyName(const nlohmann::json& entry) {
|
|
if (!entry.is_object()) return {};
|
|
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
|
const std::string& k = it.key();
|
|
if (k.size() >= 2 &&
|
|
k.compare(k.size() - 2, 2, "Id") == 0 &&
|
|
it.value().is_number_integer() &&
|
|
!isExternalRefField(k)) {
|
|
return k;
|
|
}
|
|
}
|
|
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
|
const std::string& k = it.key();
|
|
if (k.size() >= 2 &&
|
|
k.compare(k.size() - 2, 2, "Id") == 0 &&
|
|
it.value().is_number_integer()) {
|
|
return k;
|
|
}
|
|
}
|
|
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
|
if (it.value().is_number_integer()) return it.key();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::string runAndCapture(const std::string& cmd, int& outRc) {
|
|
std::string buf;
|
|
FILE* pipe = popen(cmd.c_str(), "r");
|
|
if (!pipe) {
|
|
outRc = 127;
|
|
return buf;
|
|
}
|
|
char chunk[4096];
|
|
while (std::fgets(chunk, sizeof(chunk), pipe) != nullptr) {
|
|
buf += chunk;
|
|
}
|
|
int rc = pclose(pipe);
|
|
#ifdef WEXITSTATUS
|
|
outRc = (rc != -1) ? WEXITSTATUS(rc) : rc;
|
|
#else
|
|
outRc = rc;
|
|
#endif
|
|
return buf;
|
|
}
|
|
|
|
struct Hit {
|
|
fs::path path;
|
|
std::string magic; // 4-char as string
|
|
std::string primaryKeyField;
|
|
std::string entryName;
|
|
nlohmann::json entry;
|
|
};
|
|
|
|
int handleFind(int& i, int argc, char** argv) {
|
|
if (i + 2 >= argc) {
|
|
std::fprintf(stderr,
|
|
"catalog-find: usage: --catalog-find "
|
|
"<directory> <id> [--magic <WXXX>] [--json]\n");
|
|
return 1;
|
|
}
|
|
std::string dir = argv[++i];
|
|
std::string idArg = argv[++i];
|
|
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
|
// Optional --magic <WXXX> filter to limit search to
|
|
// one format. Useful when an id is a primary key in
|
|
// multiple format families and you only want hits from
|
|
// one (e.g. id 100 matches both WGRP comp 100 and
|
|
// WSCB broadcast 100 — --magic WGRP narrows it).
|
|
std::string magicFilter;
|
|
while (i + 1 < argc && std::strcmp(argv[i + 1], "--magic") == 0 &&
|
|
i + 2 < argc) {
|
|
++i;
|
|
magicFilter = argv[++i];
|
|
}
|
|
|
|
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
|
std::fprintf(stderr,
|
|
"catalog-find: not a directory: %s\n", dir.c_str());
|
|
return 1;
|
|
}
|
|
|
|
uint64_t searchId = 0;
|
|
try {
|
|
searchId = std::stoull(idArg);
|
|
} catch (...) {
|
|
std::fprintf(stderr,
|
|
"catalog-find: <id> must be a numeric literal "
|
|
"(got '%s')\n", idArg.c_str());
|
|
return 1;
|
|
}
|
|
|
|
std::vector<Hit> hits;
|
|
size_t scanned = 0;
|
|
size_t skippedNoFlag = 0;
|
|
size_t skippedUnknownMagic = 0;
|
|
|
|
// skip_permission_denied prevents the iterator from
|
|
// throwing on unreadable subdirectories (common when
|
|
// walking /tmp or system trees that contain other-user
|
|
// files). Errors are swallowed silently — catalog-find
|
|
// is a best-effort search, not an audit.
|
|
std::error_code walkEc;
|
|
fs::recursive_directory_iterator it(
|
|
dir, fs::directory_options::skip_permission_denied,
|
|
walkEc);
|
|
fs::recursive_directory_iterator end;
|
|
if (walkEc) {
|
|
std::fprintf(stderr,
|
|
"catalog-find: cannot open directory '%s': %s\n",
|
|
dir.c_str(), walkEc.message().c_str());
|
|
return 1;
|
|
}
|
|
for (; it != end; it.increment(walkEc)) {
|
|
if (walkEc) {
|
|
// A subdirectory failed mid-walk; clear and
|
|
// continue. The skip_permission_denied option
|
|
// covers most cases but defensive code stays
|
|
// safer.
|
|
walkEc.clear();
|
|
continue;
|
|
}
|
|
const auto& dirent = *it;
|
|
if (!dirent.is_regular_file(walkEc)) {
|
|
walkEc.clear();
|
|
continue;
|
|
}
|
|
char magic[4]{};
|
|
if (!peekMagic(dirent.path(), magic)) continue;
|
|
const FormatMagicEntry* fmt = findFormatByMagic(magic);
|
|
if (!fmt) {
|
|
++skippedUnknownMagic;
|
|
continue;
|
|
}
|
|
if (!magicFilter.empty()) {
|
|
std::string m(magic, 4);
|
|
// Pad / strip trailing space — table magics
|
|
// include space chars (e.g. "WOM ").
|
|
if (m != magicFilter) continue;
|
|
}
|
|
if (!fmt->infoFlag) {
|
|
++skippedNoFlag;
|
|
continue;
|
|
}
|
|
++scanned;
|
|
// Strip extension to get the base path the
|
|
// per-format inspect handler expects.
|
|
std::string base = dirent.path().string();
|
|
if (fmt->extension && *fmt->extension) {
|
|
size_t extLen = std::strlen(fmt->extension);
|
|
if (base.size() >= extLen &&
|
|
base.compare(base.size() - extLen, extLen,
|
|
fmt->extension) == 0) {
|
|
base.resize(base.size() - extLen);
|
|
}
|
|
}
|
|
std::string cmd = shellQuote(argv[0]) + " " +
|
|
fmt->infoFlag + " " +
|
|
shellQuote(base) + " --json 2>/dev/null";
|
|
int rc = 0;
|
|
std::string out = runAndCapture(cmd, rc);
|
|
if (rc != 0 || out.empty()) continue;
|
|
nlohmann::json doc;
|
|
try {
|
|
doc = nlohmann::json::parse(out);
|
|
} catch (...) {
|
|
continue;
|
|
}
|
|
if (!doc.contains("entries") ||
|
|
!doc["entries"].is_array()) continue;
|
|
for (const auto& entry : doc["entries"]) {
|
|
auto [ok, key] = findEntryPrimaryKey(entry);
|
|
if (!ok || key != searchId) continue;
|
|
Hit h;
|
|
h.path = dirent.path();
|
|
h.magic = std::string(magic, 4);
|
|
h.primaryKeyField = findEntryPrimaryKeyName(entry);
|
|
if (entry.is_object() && entry.contains("name") &&
|
|
entry["name"].is_string()) {
|
|
h.entryName = entry["name"].get<std::string>();
|
|
}
|
|
h.entry = entry;
|
|
hits.push_back(h);
|
|
}
|
|
}
|
|
|
|
if (jsonOut) {
|
|
nlohmann::json out;
|
|
out["directory"] = dir;
|
|
out["searchId"] = searchId;
|
|
if (!magicFilter.empty()) out["magicFilter"] = magicFilter;
|
|
out["scanned"] = scanned;
|
|
out["hits"] = nlohmann::json::array();
|
|
for (const auto& h : hits) {
|
|
out["hits"].push_back({
|
|
{"file", h.path.string()},
|
|
{"magic", h.magic},
|
|
{"primaryKey", h.primaryKeyField},
|
|
{"name", h.entryName},
|
|
{"entry", h.entry},
|
|
});
|
|
}
|
|
std::printf("%s\n", out.dump(2).c_str());
|
|
return hits.empty() ? 1 : 0;
|
|
}
|
|
|
|
std::printf("catalog-find: searched %zu catalog files "
|
|
"in '%s' for id=%llu",
|
|
scanned, dir.c_str(),
|
|
static_cast<unsigned long long>(searchId));
|
|
if (!magicFilter.empty()) {
|
|
std::printf(" (magic=%s)", magicFilter.c_str());
|
|
}
|
|
std::printf("\n");
|
|
if (skippedNoFlag > 0) {
|
|
std::printf(" (skipped %zu files: format has no "
|
|
"--info-* surface)\n", skippedNoFlag);
|
|
}
|
|
if (skippedUnknownMagic > 0) {
|
|
std::printf(" (skipped %zu files: unknown magic)\n",
|
|
skippedUnknownMagic);
|
|
}
|
|
if (hits.empty()) {
|
|
std::printf(" no hits — id %llu is not a primary "
|
|
"key in any catalog under this tree\n",
|
|
static_cast<unsigned long long>(searchId));
|
|
return 1;
|
|
}
|
|
std::printf(" hits (%zu):\n", hits.size());
|
|
for (const auto& h : hits) {
|
|
std::printf(" [%s] %s:%s=%llu",
|
|
h.magic.c_str(), h.path.string().c_str(),
|
|
h.primaryKeyField.c_str(),
|
|
static_cast<unsigned long long>(searchId));
|
|
if (!h.entryName.empty()) {
|
|
std::printf(" \"%s\"", h.entryName.c_str());
|
|
}
|
|
std::printf("\n");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool handleCatalogFind(int& i, int argc, char** argv, int& outRc) {
|
|
if (std::strcmp(argv[i], "--catalog-find") == 0 &&
|
|
i + 2 < argc) {
|
|
outRc = handleFind(i, argc, argv); return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace cli
|
|
} // namespace editor
|
|
} // namespace wowee
|