Kelsidavis-WoWee/tools/editor/cli_talent_tabs_catalog.cpp
Kelsi bf8d55cb3e feat(editor): add WTLE (Talent Tab) open catalog format
Open replacement for Blizzard's TalentTab.dbc plus the per-tab
fields in Spell.dbc / Talent.dbc. Defines the three talent trees
that each class has — Warrior: Arms / Fury / Protection;
Mage: Arcane / Fire / Frost; Paladin: Holy / Protection /
Retribution; etc.

Each tab carries its own name, role hint (DPS / Tank / Healer /
Hybrid / PetClass), display order in the talent UI, background
artwork path (e.g. "WarriorArms" for the parchment background),
icon path, and the class bitmask it belongs to.

Distinct from WTAL (which defines individual talent points) —
WTLE says "the Arms tree exists for Warriors, displays in tab 1,
is a DPS spec"; WTAL says "Mortal Strike is a 1-point talent in
the Arms tree, row 7, requires Improved Charge as a prerequisite".

Cross-references back to WCHC (classMask uses the same bit
layout) and forward to WTAL (talent entries reference tabId
here). findByClass(classBit) returns all tabs for a class
sorted by displayOrder — the talent UI uses this directly to
populate its tab buttons.

Three preset emitters: --gen-tle (Warrior 3 tabs with two DPS +
one Tank), --gen-tle-mage (Mage 3 DPS tabs), --gen-tle-paladin
(Paladin 3 tabs covering all three roles in one preset).

Validation enforces id+name+classMask presence (classMask=0
means no class can use the tab — usually a config bug),
roleHint 0..4, no duplicate ids; warns on empty iconPath
(missing-texture render), empty backgroundFile (no panel art),
displayOrder>3 (UI shows at most 4 tabs), and (classMask +
displayOrder) collisions for overlapping classes (two tabs
claiming the same UI slot for the same class).

Wired through the cross-format table; WTLE appears automatically
in all 12 cross-format utilities. Format count 77 -> 78; CLI flag
count 958 -> 963.
2026-05-09 22:27:18 -07:00

250 lines
8.8 KiB
C++

#include "cli_talent_tabs_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_talent_tabs.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
std::string stripWtleExt(std::string base) {
stripExt(base, ".wtle");
return base;
}
bool saveOrError(const wowee::pipeline::WoweeTalentTab& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeTalentTabLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wtle\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeTalentTab& c,
const std::string& base) {
std::printf("Wrote %s.wtle\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" tabs : %zu\n", c.entries.size());
}
int handleGenWarrior(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "WarriorTalentTabs";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtleExt(base);
auto c = wowee::pipeline::WoweeTalentTabLoader::makeWarrior(name);
if (!saveOrError(c, base, "gen-tle")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenMage(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "MageTalentTabs";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtleExt(base);
auto c = wowee::pipeline::WoweeTalentTabLoader::makeMage(name);
if (!saveOrError(c, base, "gen-tle-mage")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenPaladin(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "PaladinTalentTabs";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtleExt(base);
auto c = wowee::pipeline::WoweeTalentTabLoader::makePaladin(name);
if (!saveOrError(c, base, "gen-tle-paladin")) 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 = stripWtleExt(base);
if (!wowee::pipeline::WoweeTalentTabLoader::exists(base)) {
std::fprintf(stderr, "WTLE not found: %s.wtle\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTalentTabLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wtle"] = base + ".wtle";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"tabId", e.tabId},
{"name", e.name},
{"description", e.description},
{"classMask", e.classMask},
{"displayOrder", e.displayOrder},
{"roleHint", e.roleHint},
{"roleHintName", wowee::pipeline::WoweeTalentTab::roleHintName(e.roleHint)},
{"iconPath", e.iconPath},
{"backgroundFile", e.backgroundFile},
{"iconColorRGBA", e.iconColorRGBA},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WTLE: %s.wtle\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" tabs : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id classMask ord role name backgroundFile\n");
for (const auto& e : c.entries) {
std::printf(" %4u 0x%08x %u %-7s %-15s %s\n",
e.tabId, e.classMask,
e.displayOrder,
wowee::pipeline::WoweeTalentTab::roleHintName(e.roleHint),
e.name.c_str(),
e.backgroundFile.c_str());
}
return 0;
}
int handleValidate(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWtleExt(base);
if (!wowee::pipeline::WoweeTalentTabLoader::exists(base)) {
std::fprintf(stderr,
"validate-wtle: WTLE not found: %s.wtle\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTalentTabLoader::load(base);
std::vector<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::vector<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.tabId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.tabId == 0)
errors.push_back(ctx + ": tabId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.classMask == 0)
errors.push_back(ctx +
": classMask is 0 — no class can use this tab");
if (e.roleHint > wowee::pipeline::WoweeTalentTab::PetClass) {
errors.push_back(ctx + ": roleHint " +
std::to_string(e.roleHint) + " not in 0..4");
}
if (e.displayOrder > 3) {
warnings.push_back(ctx +
": displayOrder " +
std::to_string(e.displayOrder) +
" > 3 — talent UI shows at most 4 tabs");
}
if (e.iconPath.empty())
warnings.push_back(ctx +
": iconPath is empty — tab will render with "
"the missing-texture placeholder");
if (e.backgroundFile.empty())
warnings.push_back(ctx +
": backgroundFile is empty — talent tree "
"panel will have no background art");
for (uint32_t prev : idsSeen) {
if (prev == e.tabId) {
errors.push_back(ctx + ": duplicate tabId");
break;
}
}
idsSeen.push_back(e.tabId);
}
// Cross-entry: detect duplicate (classMask, displayOrder)
// for overlapping classMasks — two tabs can't share a UI
// slot for the same class.
for (size_t a = 0; a < c.entries.size(); ++a) {
for (size_t b = a + 1; b < c.entries.size(); ++b) {
const auto& ea = c.entries[a];
const auto& eb = c.entries[b];
if (ea.displayOrder != eb.displayOrder) continue;
if ((ea.classMask & eb.classMask) == 0) continue;
warnings.push_back(
"entries " + std::to_string(a) + " (" +
ea.name + ") and " + std::to_string(b) + " (" +
eb.name + ") share displayOrder " +
std::to_string(ea.displayOrder) +
" for overlapping classMask — tab UI position collision");
}
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wtle"] = base + ".wtle";
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-wtle: %s.wtle\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu tabs, all tabIds unique, no UI overlaps\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 handleTalentTabsCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-tle") == 0 && i + 1 < argc) {
outRc = handleGenWarrior(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-tle-mage") == 0 && i + 1 < argc) {
outRc = handleGenMage(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-tle-paladin") == 0 && i + 1 < argc) {
outRc = handleGenPaladin(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wtle") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wtle") == 0 && i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee