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.
This commit is contained in:
Kelsi 2026-05-09 22:27:18 -07:00
parent 9a09831957
commit bf8d55cb3e
10 changed files with 661 additions and 0 deletions

View file

@ -666,6 +666,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_creature_difficulties.cpp
src/pipeline/wowee_item_materials.cpp
src/pipeline/wowee_player_spawn_profiles.cpp
src/pipeline/wowee_talent_tabs.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1490,6 +1491,7 @@ add_executable(wowee_editor
tools/editor/cli_creature_difficulties_catalog.cpp
tools/editor/cli_item_materials_catalog.cpp
tools/editor/cli_player_spawn_profiles_catalog.cpp
tools/editor/cli_talent_tabs_catalog.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1634,6 +1636,7 @@ add_executable(wowee_editor
src/pipeline/wowee_creature_difficulties.cpp
src/pipeline/wowee_item_materials.cpp
src/pipeline/wowee_player_spawn_profiles.cpp
src/pipeline/wowee_talent_tabs.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,110 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Talent Tab catalog (.wtle) — novel
// 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), display order in the talent UI,
// background artwork path, icon, and the class bitmask
// it belongs to.
//
// Distinct from WTAL (Talents) which defines individual
// talent points and their effects. 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 with previously-added formats:
// WCHC: classMask uses the same bit layout as WCHC
// class IDs.
// WTAL: talent entries reference tabId here.
//
// Binary layout (little-endian):
// magic[4] = "WTLE"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// tabId (uint32)
// nameLen + name
// descLen + description
// classMask (uint32)
// displayOrder (uint8) / roleHint (uint8) / pad[2]
// iconPathLen + iconPath
// backgroundFileLen + backgroundFile
// iconColorRGBA (uint32)
struct WoweeTalentTab {
enum RoleHint : uint8_t {
DPS = 0, // damage-dealing spec
Tank = 1, // mitigation spec
Healer = 2, // healing spec
Hybrid = 3, // can fill multiple roles
PetClass = 4, // hunter/warlock pet-focused tree
};
struct Entry {
uint32_t tabId = 0;
std::string name;
std::string description;
uint32_t classMask = 0;
uint8_t displayOrder = 0;
uint8_t roleHint = DPS;
uint8_t pad0 = 0;
uint8_t pad1 = 0;
std::string iconPath;
std::string backgroundFile;
uint32_t iconColorRGBA = 0xFFFFFFFFu;
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t tabId) const;
// Return all tabs for the given class, in displayOrder.
// The talent UI uses this to populate the three (or
// four, for druids) tab buttons.
std::vector<const Entry*> findByClass(uint32_t classBit) const;
static const char* roleHintName(uint8_t r);
};
class WoweeTalentTabLoader {
public:
static bool save(const WoweeTalentTab& cat,
const std::string& basePath);
static WoweeTalentTab load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-tle* variants.
//
// makeWarrior — 3 tabs (Arms DPS / Fury DPS /
// Protection Tank) with the
// canonical Warrior icon paths.
// makeMage — 3 tabs (Arcane / Fire / Frost),
// all DPS, with the canonical Mage
// icon paths.
// makePaladin — 3 tabs (Holy Healer / Protection
// Tank / Retribution DPS) covering
// all three roles in one preset.
static WoweeTalentTab makeWarrior(const std::string& catalogName);
static WoweeTalentTab makeMage(const std::string& catalogName);
static WoweeTalentTab makePaladin(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,270 @@
#include "pipeline/wowee_talent_tabs.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'T', 'L', 'E'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
void writeStr(std::ofstream& os, const std::string& s) {
uint32_t n = static_cast<uint32_t>(s.size());
writePOD(os, n);
if (n > 0) os.write(s.data(), n);
}
bool readStr(std::ifstream& is, std::string& s) {
uint32_t n = 0;
if (!readPOD(is, n)) return false;
if (n > (1u << 20)) return false;
s.resize(n);
if (n > 0) {
is.read(s.data(), n);
if (is.gcount() != static_cast<std::streamsize>(n)) {
s.clear();
return false;
}
}
return true;
}
std::string normalizePath(std::string base) {
if (base.size() < 5 || base.substr(base.size() - 5) != ".wtle") {
base += ".wtle";
}
return base;
}
uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) {
return (static_cast<uint32_t>(a) << 24) |
(static_cast<uint32_t>(b) << 16) |
(static_cast<uint32_t>(g) << 8) |
static_cast<uint32_t>(r);
}
constexpr uint32_t CLS_WARRIOR = 1u << 0;
constexpr uint32_t CLS_PALADIN = 1u << 1;
constexpr uint32_t CLS_MAGE = 1u << 7;
} // namespace
const WoweeTalentTab::Entry*
WoweeTalentTab::findById(uint32_t tabId) const {
for (const auto& e : entries)
if (e.tabId == tabId) return &e;
return nullptr;
}
std::vector<const WoweeTalentTab::Entry*>
WoweeTalentTab::findByClass(uint32_t classBit) const {
std::vector<const Entry*> out;
for (const auto& e : entries) {
if (e.classMask & classBit) out.push_back(&e);
}
std::sort(out.begin(), out.end(),
[](const Entry* a, const Entry* b) {
return a->displayOrder < b->displayOrder;
});
return out;
}
const char* WoweeTalentTab::roleHintName(uint8_t r) {
switch (r) {
case DPS: return "dps";
case Tank: return "tank";
case Healer: return "healer";
case Hybrid: return "hybrid";
case PetClass: return "pet";
default: return "unknown";
}
}
bool WoweeTalentTabLoader::save(const WoweeTalentTab& cat,
const std::string& basePath) {
std::ofstream os(normalizePath(basePath), std::ios::binary);
if (!os) return false;
os.write(kMagic, 4);
writePOD(os, kVersion);
writeStr(os, cat.name);
uint32_t entryCount = static_cast<uint32_t>(cat.entries.size());
writePOD(os, entryCount);
for (const auto& e : cat.entries) {
writePOD(os, e.tabId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.classMask);
writePOD(os, e.displayOrder);
writePOD(os, e.roleHint);
writePOD(os, e.pad0);
writePOD(os, e.pad1);
writeStr(os, e.iconPath);
writeStr(os, e.backgroundFile);
writePOD(os, e.iconColorRGBA);
}
return os.good();
}
WoweeTalentTab WoweeTalentTabLoader::load(
const std::string& basePath) {
WoweeTalentTab out;
std::ifstream is(normalizePath(basePath), std::ios::binary);
if (!is) return out;
char magic[4];
is.read(magic, 4);
if (std::memcmp(magic, kMagic, 4) != 0) return out;
uint32_t version = 0;
if (!readPOD(is, version) || version != kVersion) return out;
if (!readStr(is, out.name)) return out;
uint32_t entryCount = 0;
if (!readPOD(is, entryCount)) return out;
if (entryCount > (1u << 20)) return out;
out.entries.resize(entryCount);
for (auto& e : out.entries) {
if (!readPOD(is, e.tabId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.classMask) ||
!readPOD(is, e.displayOrder) ||
!readPOD(is, e.roleHint) ||
!readPOD(is, e.pad0) ||
!readPOD(is, e.pad1)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.iconPath) ||
!readStr(is, e.backgroundFile)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.iconColorRGBA)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeTalentTabLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeTalentTab WoweeTalentTabLoader::makeWarrior(
const std::string& catalogName) {
using T = WoweeTalentTab;
WoweeTalentTab c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t order,
uint8_t role, const char* icon, const char* bg,
const char* desc) {
T::Entry e;
e.tabId = id; e.name = name; e.description = desc;
e.classMask = CLS_WARRIOR;
e.displayOrder = order;
e.roleHint = role;
e.iconPath = icon;
e.backgroundFile = bg;
e.iconColorRGBA = packRgba(220, 60, 60); // warrior red
c.entries.push_back(e);
};
add(161, "Arms", 0, T::DPS,
"Interface\\Icons\\Ability_Rogue_Eviscerate",
"WarriorArms",
"Arms — two-handed weapon mastery, Mortal Strike DPS spec.");
add(164, "Fury", 1, T::DPS,
"Interface\\Icons\\Ability_Warrior_InnerRage",
"WarriorFury",
"Fury — dual-wield berserker DPS spec.");
add(163, "Protection", 2, T::Tank,
"Interface\\Icons\\INV_Shield_06",
"WarriorProtection",
"Protection — shield-wielding tank spec.");
return c;
}
WoweeTalentTab WoweeTalentTabLoader::makeMage(
const std::string& catalogName) {
using T = WoweeTalentTab;
WoweeTalentTab c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t order,
const char* icon, const char* bg,
const char* desc) {
T::Entry e;
e.tabId = id; e.name = name; e.description = desc;
e.classMask = CLS_MAGE;
e.displayOrder = order;
e.roleHint = T::DPS;
e.iconPath = icon;
e.backgroundFile = bg;
e.iconColorRGBA = packRgba(80, 140, 240); // mage blue
c.entries.push_back(e);
};
add(81, "Arcane", 0,
"Interface\\Icons\\Spell_Holy_MagicalSentry",
"MageArcane",
"Arcane — burst-mana spec around Arcane Blast scaling.");
add(41, "Fire", 1,
"Interface\\Icons\\Spell_Fire_FireBolt02",
"MageFire",
"Fire — crit-focused spec around Pyroblast / Combustion.");
add(61, "Frost", 2,
"Interface\\Icons\\Spell_Frost_FrostBolt02",
"MageFrost",
"Frost — control + sustained-damage spec.");
return c;
}
WoweeTalentTab WoweeTalentTabLoader::makePaladin(
const std::string& catalogName) {
using T = WoweeTalentTab;
WoweeTalentTab c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t order,
uint8_t role, const char* icon, const char* bg,
uint8_t r, uint8_t g, uint8_t b,
const char* desc) {
T::Entry e;
e.tabId = id; e.name = name; e.description = desc;
e.classMask = CLS_PALADIN;
e.displayOrder = order;
e.roleHint = role;
e.iconPath = icon;
e.backgroundFile = bg;
e.iconColorRGBA = packRgba(r, g, b);
c.entries.push_back(e);
};
add(382, "Holy", 0, T::Healer,
"Interface\\Icons\\Spell_Holy_HolyBolt",
"PaladinHoly",
240, 240, 200, "Holy — single-target healing spec.");
add(383, "Protection", 1, T::Tank,
"Interface\\Icons\\Spell_Holy_DevotionAura",
"PaladinProtection",
220, 220, 180, "Protection — shield + holy power tank spec.");
add(381, "Retribution", 2, T::DPS,
"Interface\\Icons\\Spell_Holy_AuraOfLight",
"PaladinRetribution",
240, 200, 100, "Retribution — two-handed melee DPS spec.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -237,6 +237,8 @@ const char* const kArgRequired[] = {
"--gen-psp", "--gen-psp-horde", "--gen-psp-dk",
"--info-wpsp", "--validate-wpsp",
"--export-wpsp-json", "--import-wpsp-json",
"--gen-tle", "--gen-tle-mage", "--gen-tle-paladin",
"--info-wtle", "--validate-wtle",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -117,6 +117,7 @@
#include "cli_creature_difficulties_catalog.hpp"
#include "cli_item_materials_catalog.hpp"
#include "cli_player_spawn_profiles_catalog.hpp"
#include "cli_talent_tabs_catalog.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -275,6 +276,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleCreatureDifficultiesCatalog,
handleItemMaterialsCatalog,
handlePlayerSpawnProfilesCatalog,
handleTalentTabsCatalog,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -80,6 +80,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','C','D','F'}, ".wcdf", "creatures", "--info-wcdf", "Creature difficulty variant catalog"},
{{'W','M','A','T'}, ".wmat", "items", "--info-wmat", "Item material catalog"},
{{'W','P','S','P'}, ".wpsp", "chars", "--info-wpsp", "Player spawn profile catalog"},
{{'W','T','L','E'}, ".wtle", "talents", "--info-wtle", "Talent tab / tree catalog"},
{{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"},
{{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"},
{{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"},

View file

@ -1833,6 +1833,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wpsp to a human-editable JSON sidecar (defaults to <base>.wpsp.json)\n");
std::printf(" --import-wpsp-json <json-path> [out-base]\n");
std::printf(" Import a .wpsp.json sidecar back into binary .wpsp (all per-entry fields preserved verbatim including spawn/bind coords and item/spell ids)\n");
std::printf(" --gen-tle <wtle-base> [name]\n");
std::printf(" Emit .wtle Warrior tabs: 3 trees (Arms DPS / Fury DPS / Protection Tank) with canonical icon and background art paths\n");
std::printf(" --gen-tle-mage <wtle-base> [name]\n");
std::printf(" Emit .wtle Mage tabs: 3 trees (Arcane / Fire / Frost), all DPS, with mage-blue icon color\n");
std::printf(" --gen-tle-paladin <wtle-base> [name]\n");
std::printf(" Emit .wtle Paladin tabs: 3 trees covering all 3 roles (Holy Healer / Protection Tank / Retribution DPS)\n");
std::printf(" --info-wtle <wtle-base> [--json]\n");
std::printf(" Print WTLE entries (id / classMask / displayOrder / role / name / backgroundFile)\n");
std::printf(" --validate-wtle <wtle-base> [--json]\n");
std::printf(" Static checks: id+name+classMask required, roleHint 0..4, no duplicate ids; warns on empty icon/background, displayOrder>3, and (classMask+order) UI position collisions\n");
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -102,6 +102,7 @@ constexpr FormatRow kFormats[] = {
{"WCDF", ".wcdf", "creatures", "CreatureDifficulty.dbc", "Creature difficulty variant catalog"},
{"WMAT", ".wmat", "items", "Material.dbc + ItemDisplayInfo", "Item material catalog"},
{"WPSP", ".wpsp", "chars", "playercreateinfo SQL + StartOutfit","Player spawn profile catalog"},
{"WTLE", ".wtle", "talents", "TalentTab.dbc", "Talent tab / tree catalog"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,250 @@
#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

View file

@ -0,0 +1,12 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
bool handleTalentTabsCatalog(int& i, int argc, char** argv,
int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee