diff --git a/CMakeLists.txt b/CMakeLists.txt index 5292671f..191819c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/pipeline/wowee_talent_tabs.hpp b/include/pipeline/wowee_talent_tabs.hpp new file mode 100644 index 00000000..19c53d57 --- /dev/null +++ b/include/pipeline/wowee_talent_tabs.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include + +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 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 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 diff --git a/src/pipeline/wowee_talent_tabs.cpp b/src/pipeline/wowee_talent_tabs.cpp new file mode 100644 index 00000000..c9320169 --- /dev/null +++ b/src/pipeline/wowee_talent_tabs.cpp @@ -0,0 +1,270 @@ +#include "pipeline/wowee_talent_tabs.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'L', 'E'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(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(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(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(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 +WoweeTalentTab::findByClass(uint32_t classBit) const { + std::vector 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(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 diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index abf3d5cf..a212634d 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -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", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 8314169e..d327154d 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -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, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 573ff607..60b620fa 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -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"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 2581769f..33044b3a 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1833,6 +1833,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wpsp to a human-editable JSON sidecar (defaults to .wpsp.json)\n"); std::printf(" --import-wpsp-json [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 [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 [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 [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 [--json]\n"); + std::printf(" Print WTLE entries (id / classMask / displayOrder / role / name / backgroundFile)\n"); + std::printf(" --validate-wtle [--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 [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 0096c8ba..73dbadab 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -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 diff --git a/tools/editor/cli_talent_tabs_catalog.cpp b/tools/editor/cli_talent_tabs_catalog.cpp new file mode 100644 index 00000000..980c4d11 --- /dev/null +++ b/tools/editor/cli_talent_tabs_catalog.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +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 errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector 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 diff --git a/tools/editor/cli_talent_tabs_catalog.hpp b/tools/editor/cli_talent_tabs_catalog.hpp new file mode 100644 index 00000000..5be36a92 --- /dev/null +++ b/tools/editor/cli_talent_tabs_catalog.hpp @@ -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