mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
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:
parent
9a09831957
commit
bf8d55cb3e
10 changed files with 661 additions and 0 deletions
270
src/pipeline/wowee_talent_tabs.cpp
Normal file
270
src/pipeline/wowee_talent_tabs.cpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue