diff --git a/CMakeLists.txt b/CMakeLists.txt index b21ed476..febc1e5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -612,6 +612,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_trainers.cpp src/pipeline/wowee_gossip.cpp src/pipeline/wowee_taxi.cpp + src/pipeline/wowee_talents.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1370,6 +1371,7 @@ add_executable(wowee_editor tools/editor/cli_trainers_catalog.cpp tools/editor/cli_gossip_catalog.cpp tools/editor/cli_taxi_catalog.cpp + tools/editor/cli_talents_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1460,6 +1462,7 @@ add_executable(wowee_editor src/pipeline/wowee_trainers.cpp src/pipeline/wowee_gossip.cpp src/pipeline/wowee_taxi.cpp + src/pipeline/wowee_talents.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_talents.hpp b/include/pipeline/wowee_talents.hpp new file mode 100644 index 00000000..419edfe7 --- /dev/null +++ b/include/pipeline/wowee_talents.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Talent catalog (.wtal) — novel replacement for +// Blizzard's TalentTab.dbc + Talent.dbc + the AzerothCore- +// style talent_progression SQL tables. The 25th open format +// added to the editor. +// +// Defines class talent specialization trees: a per-class set +// of named tabs (Arms / Fury / Protection for warrior, Fire +// / Frost / Arcane for mage, etc.), each with up to N +// talents arranged in a row/column grid, each talent having +// up to 5 ranks and an optional prerequisite chain. +// +// Cross-references with previously-added formats: +// WTAL.talent.prereqTalentId → WTAL.talent.talentId +// (intra-format chain) +// WTAL.talent.rankSpellIds[] → WSPL.entry.spellId +// (spell granted at each rank) +// +// Binary layout (little-endian): +// magic[4] = "WTAL" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// treeCount (uint32) +// trees (each): +// treeId (uint32) +// nameLen + name +// iconLen + iconPath +// requiredClassMask (uint32) +// talentCount (uint16) + pad[2] +// talents (talentCount × { +// talentId (uint32) +// row (uint8) / col (uint8) / maxRank (uint8) / pad[1] +// prereqTalentId (uint32) / prereqRank (uint8) / pad[3] +// rankSpellIds[5] (uint32 each, zero-padded for unused ranks) +// }) +struct WoweeTalent { + static constexpr int kMaxRanks = 5; + + // Class IDs follow Blizzard's CharClasses.dbc canonical + // bit positions (bit N corresponds to classId N+1): + // 1=Warrior 2=Paladin 3=Hunter 4=Rogue 5=Priest + // 6=DK 7=Shaman 8=Mage 9=Warlock 10=- 11=Druid + enum ClassMask : uint32_t { + ClassWarrior = 1u << 0, + ClassPaladin = 1u << 1, + ClassHunter = 1u << 2, + ClassRogue = 1u << 3, + ClassPriest = 1u << 4, + ClassDK = 1u << 5, + ClassShaman = 1u << 6, + ClassMage = 1u << 7, + ClassWarlock = 1u << 8, + ClassDruid = 1u << 10, + }; + + struct Talent { + uint32_t talentId = 0; + uint8_t row = 0; + uint8_t col = 0; + uint8_t maxRank = 1; + uint32_t prereqTalentId = 0; + uint8_t prereqRank = 0; + uint32_t rankSpellIds[kMaxRanks] = {0, 0, 0, 0, 0}; + }; + + struct Tree { + uint32_t treeId = 0; + std::string name; + std::string iconPath; + uint32_t requiredClassMask = 0; + std::vector talents; + }; + + std::string name; + std::vector trees; + + bool isValid() const { return !trees.empty(); } + + const Tree* findTree(uint32_t treeId) const; + // Talent lookup is global across all trees (talentIds are + // expected to be unique within a single .wtal catalog). + const Talent* findTalent(uint32_t talentId) const; +}; + +class WoweeTalentLoader { +public: + static bool save(const WoweeTalent& cat, + const std::string& basePath); + static WoweeTalent load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-talents* variants. + // + // makeStarter — 1 small tree (3 talents, no prereqs). + // makeWarrior — 3 trees (Arms / Fury / Protection) each + // with a handful of talents, prerequisite + // chains, and rankSpellIds wired to WSPL + // warrior preset spells where applicable. + // makeMage — 3 trees (Arcane / Fire / Frost) with + // links to WSPL mage preset spell IDs. + static WoweeTalent makeStarter(const std::string& catalogName); + static WoweeTalent makeWarrior(const std::string& catalogName); + static WoweeTalent makeMage(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_talents.cpp b/src/pipeline/wowee_talents.cpp new file mode 100644 index 00000000..587ec0ca --- /dev/null +++ b/src/pipeline/wowee_talents.cpp @@ -0,0 +1,287 @@ +#include "pipeline/wowee_talents.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'A', 'L'}; +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) != ".wtal") { + base += ".wtal"; + } + return base; +} + +} // namespace + +const WoweeTalent::Tree* WoweeTalent::findTree(uint32_t treeId) const { + for (const auto& t : trees) if (t.treeId == treeId) return &t; + return nullptr; +} + +const WoweeTalent::Talent* WoweeTalent::findTalent(uint32_t talentId) const { + for (const auto& t : trees) { + for (const auto& a : t.talents) { + if (a.talentId == talentId) return &a; + } + } + return nullptr; +} + +bool WoweeTalentLoader::save(const WoweeTalent& 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 treeCount = static_cast(cat.trees.size()); + writePOD(os, treeCount); + for (const auto& t : cat.trees) { + writePOD(os, t.treeId); + writeStr(os, t.name); + writeStr(os, t.iconPath); + writePOD(os, t.requiredClassMask); + uint16_t talentCount = static_cast( + t.talents.size() > 0xFFFF ? 0xFFFF : t.talents.size()); + writePOD(os, talentCount); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + for (uint16_t k = 0; k < talentCount; ++k) { + const auto& a = t.talents[k]; + writePOD(os, a.talentId); + writePOD(os, a.row); + writePOD(os, a.col); + writePOD(os, a.maxRank); + uint8_t pad1 = 0; + writePOD(os, pad1); + writePOD(os, a.prereqTalentId); + writePOD(os, a.prereqRank); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + for (int r = 0; r < WoweeTalent::kMaxRanks; ++r) { + writePOD(os, a.rankSpellIds[r]); + } + } + } + return os.good(); +} + +WoweeTalent WoweeTalentLoader::load(const std::string& basePath) { + WoweeTalent 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 treeCount = 0; + if (!readPOD(is, treeCount)) return out; + if (treeCount > (1u << 20)) return out; + out.trees.resize(treeCount); + for (auto& t : out.trees) { + if (!readPOD(is, t.treeId)) { out.trees.clear(); return out; } + if (!readStr(is, t.name) || !readStr(is, t.iconPath)) { + out.trees.clear(); return out; + } + if (!readPOD(is, t.requiredClassMask)) { + out.trees.clear(); return out; + } + uint16_t talentCount = 0; + if (!readPOD(is, talentCount)) { + out.trees.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.trees.clear(); return out; } + t.talents.resize(talentCount); + for (uint16_t k = 0; k < talentCount; ++k) { + auto& a = t.talents[k]; + if (!readPOD(is, a.talentId) || + !readPOD(is, a.row) || + !readPOD(is, a.col) || + !readPOD(is, a.maxRank)) { + out.trees.clear(); return out; + } + uint8_t pad1 = 0; + if (!readPOD(is, pad1)) { + out.trees.clear(); return out; + } + if (!readPOD(is, a.prereqTalentId) || + !readPOD(is, a.prereqRank)) { + out.trees.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { out.trees.clear(); return out; } + for (int r = 0; r < WoweeTalent::kMaxRanks; ++r) { + if (!readPOD(is, a.rankSpellIds[r])) { + out.trees.clear(); return out; + } + } + } + } + return out; +} + +bool WoweeTalentLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeTalent WoweeTalentLoader::makeStarter(const std::string& catalogName) { + WoweeTalent c; + c.name = catalogName; + { + WoweeTalent::Tree t; + t.treeId = 1; t.name = "Starter Tree"; + t.requiredClassMask = WoweeTalent::ClassWarrior; + // Tier 0: free entry talent. + WoweeTalent::Talent a1; + a1.talentId = 1; a1.row = 0; a1.col = 0; a1.maxRank = 5; + t.talents.push_back(a1); + // Tier 1: depends on a1 at rank 5. + WoweeTalent::Talent a2; + a2.talentId = 2; a2.row = 1; a2.col = 0; a2.maxRank = 3; + a2.prereqTalentId = 1; a2.prereqRank = 5; + t.talents.push_back(a2); + // Tier 2: capstone, single-rank. + WoweeTalent::Talent a3; + a3.talentId = 3; a3.row = 2; a3.col = 0; a3.maxRank = 1; + a3.prereqTalentId = 2; a3.prereqRank = 3; + t.talents.push_back(a3); + c.trees.push_back(t); + } + return c; +} + +WoweeTalent WoweeTalentLoader::makeWarrior(const std::string& catalogName) { + WoweeTalent c; + c.name = catalogName; + auto addTree = [&](uint32_t id, const char* name) -> WoweeTalent::Tree* { + WoweeTalent::Tree t; + t.treeId = id; t.name = name; + t.requiredClassMask = WoweeTalent::ClassWarrior; + c.trees.push_back(t); + return &c.trees.back(); + }; + auto addTalent = [&](WoweeTalent::Tree* t, uint32_t id, + uint8_t row, uint8_t col, uint8_t maxRank, + uint32_t prereq = 0, uint8_t prereqRank = 0, + uint32_t rankSpell0 = 0) { + WoweeTalent::Talent a; + a.talentId = id; a.row = row; a.col = col; a.maxRank = maxRank; + a.prereqTalentId = prereq; a.prereqRank = prereqRank; + a.rankSpellIds[0] = rankSpell0; + t->talents.push_back(a); + }; + { + // Arms: 4 talents in a vertical chain. + auto* t = addTree(101, "Arms"); + addTalent(t, 1001, 0, 1, 5); // Improved Heroic Strike + addTalent(t, 1002, 1, 0, 3, 1001, 5); // Anger Management + addTalent(t, 1003, 2, 1, 5, 1002, 3); // Two-Handed Spec + addTalent(t, 1004, 3, 1, 1, 1003, 5, 12294); // Mortal Strike capstone -> WSPL spellId 12294 + } + { + // Fury: 4 talents. + auto* t = addTree(102, "Fury"); + addTalent(t, 1101, 0, 1, 5); // Cruelty + addTalent(t, 1102, 0, 2, 3); // Booming Voice + addTalent(t, 1103, 1, 1, 5, 1101, 5); // Improved Battle Shout -> WSPL 6673 + addTalent(t, 1104, 2, 1, 1, 1103, 5); // Bloodthirst capstone + } + { + // Protection: 3 talents. + auto* t = addTree(103, "Protection"); + addTalent(t, 1201, 0, 0, 5); // Improved Bloodrage + addTalent(t, 1202, 1, 1, 3, 1201, 5); // Improved Thunder Clap -> WSPL 6343 + addTalent(t, 1203, 2, 1, 1, 1202, 3); // Shield Slam capstone + } + return c; +} + +WoweeTalent WoweeTalentLoader::makeMage(const std::string& catalogName) { + WoweeTalent c; + c.name = catalogName; + auto addTree = [&](uint32_t id, const char* name) -> WoweeTalent::Tree* { + WoweeTalent::Tree t; + t.treeId = id; t.name = name; + t.requiredClassMask = WoweeTalent::ClassMage; + c.trees.push_back(t); + return &c.trees.back(); + }; + auto addTalent = [&](WoweeTalent::Tree* t, uint32_t id, + uint8_t row, uint8_t col, uint8_t maxRank, + uint32_t prereq = 0, uint8_t prereqRank = 0, + uint32_t rankSpell0 = 0) { + WoweeTalent::Talent a; + a.talentId = id; a.row = row; a.col = col; a.maxRank = maxRank; + a.prereqTalentId = prereq; a.prereqRank = prereqRank; + a.rankSpellIds[0] = rankSpell0; + t->talents.push_back(a); + }; + { + auto* t = addTree(201, "Arcane"); + addTalent(t, 2001, 0, 1, 5); // Arcane Focus + addTalent(t, 2002, 1, 1, 3, 2001, 5); // Improved Arcane Intellect -> WSPL 1459 + addTalent(t, 2003, 2, 1, 1, 2002, 3, 1953); // Improved Blink -> WSPL 1953 + } + { + auto* t = addTree(202, "Fire"); + addTalent(t, 2101, 0, 1, 5); // Improved Fireball + addTalent(t, 2102, 1, 1, 5, 2101, 5); // Critical Mass + addTalent(t, 2103, 2, 1, 1, 2102, 5, 133); // Pyroblast (Fireball ref) -> WSPL 133 + } + { + auto* t = addTree(203, "Frost"); + addTalent(t, 2201, 0, 1, 5); // Frost Channeling + addTalent(t, 2202, 1, 1, 5, 2201, 5); // Improved Frostbolt + addTalent(t, 2203, 2, 1, 1, 2202, 5, 116); // Ice Shards (Frostbolt ref) -> WSPL 116 + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 52293588..616c6b69 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -70,6 +70,8 @@ const char* const kArgRequired[] = { "--export-wgsp-json", "--import-wgsp-json", "--gen-taxi", "--gen-taxi-region", "--gen-taxi-continent", "--info-wtax", "--validate-wtax", + "--gen-talents", "--gen-talents-warrior", "--gen-talents-mage", + "--info-wtal", "--validate-wtal", "--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 0c83f03b..2b39d424 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -52,6 +52,7 @@ #include "cli_trainers_catalog.hpp" #include "cli_gossip_catalog.hpp" #include "cli_taxi_catalog.hpp" +#include "cli_talents_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -145,6 +146,7 @@ constexpr DispatchFn kDispatchTable[] = { handleTrainersCatalog, handleGossipCatalog, handleTaxiCatalog, + handleTalentsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 653a345e..d3d8ab45 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1047,6 +1047,16 @@ void printUsage(const char* argv0) { std::printf(" Print WTAX nodes (id / map / position / name) + paths (id / from->to / cost / waypoint count)\n"); std::printf(" --validate-wtax [--json]\n"); std::printf(" Static checks: ids>0+unique, finite positions, paths reference real nodes, no self-loop, non-negative delays\n"); + std::printf(" --gen-talents [name]\n"); + std::printf(" Emit .wtal starter: 1 small tree (3 talents in chain) for class warrior\n"); + std::printf(" --gen-talents-warrior [name]\n"); + std::printf(" Emit .wtal warrior trees: Arms (4 talents) + Fury (4) + Protection (3) with WSPL spell cross-refs\n"); + std::printf(" --gen-talents-mage [name]\n"); + std::printf(" Emit .wtal mage trees: Arcane (3 talents) + Fire (3) + Frost (3) with WSPL Frostbolt/Fireball/Blink refs\n"); + std::printf(" --info-wtal [--json]\n"); + std::printf(" Print WTAL trees + per-talent grid position / max rank / prereq chain / rank-1 spellId\n"); + std::printf(" --validate-wtal [--json]\n"); + std::printf(" Static checks: tree+talent ids>0+unique, maxRank 1..5, prereq references resolve, no self-prereq\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_talents_catalog.cpp b/tools/editor/cli_talents_catalog.cpp new file mode 100644 index 00000000..b26bd352 --- /dev/null +++ b/tools/editor/cli_talents_catalog.cpp @@ -0,0 +1,304 @@ +#include "cli_talents_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_talents.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWtalExt(std::string base) { + stripExt(base, ".wtal"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeTalent& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeTalentLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wtal\n", + cmd, base.c_str()); + return false; + } + return true; +} + +uint32_t totalTalents(const wowee::pipeline::WoweeTalent& c) { + uint32_t n = 0; + for (const auto& t : c.trees) n += static_cast(t.talents.size()); + return n; +} + +void printGenSummary(const wowee::pipeline::WoweeTalent& c, + const std::string& base) { + std::printf("Wrote %s.wtal\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" trees : %zu (%u talents total)\n", + c.trees.size(), totalTalents(c)); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterTalents"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtalExt(base); + auto c = wowee::pipeline::WoweeTalentLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-talents")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWarrior(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WarriorTalents"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtalExt(base); + auto c = wowee::pipeline::WoweeTalentLoader::makeWarrior(name); + if (!saveOrError(c, base, "gen-talents-warrior")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMage(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MageTalents"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtalExt(base); + auto c = wowee::pipeline::WoweeTalentLoader::makeMage(name); + if (!saveOrError(c, base, "gen-talents-mage")) 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 = stripWtalExt(base); + if (!wowee::pipeline::WoweeTalentLoader::exists(base)) { + std::fprintf(stderr, "WTAL not found: %s.wtal\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTalentLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wtal"] = base + ".wtal"; + j["name"] = c.name; + j["treeCount"] = c.trees.size(); + j["totalTalents"] = totalTalents(c); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& t : c.trees) { + nlohmann::json jt; + jt["treeId"] = t.treeId; + jt["name"] = t.name; + jt["iconPath"] = t.iconPath; + jt["requiredClassMask"] = t.requiredClassMask; + nlohmann::json ta = nlohmann::json::array(); + for (const auto& a : t.talents) { + nlohmann::json ja; + ja["talentId"] = a.talentId; + ja["row"] = a.row; + ja["col"] = a.col; + ja["maxRank"] = a.maxRank; + ja["prereqTalentId"] = a.prereqTalentId; + ja["prereqRank"] = a.prereqRank; + nlohmann::json sa = nlohmann::json::array(); + for (int r = 0; r < wowee::pipeline::WoweeTalent::kMaxRanks; ++r) { + sa.push_back(a.rankSpellIds[r]); + } + ja["rankSpellIds"] = sa; + ta.push_back(ja); + } + jt["talents"] = ta; + arr.push_back(jt); + } + j["trees"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WTAL: %s.wtal\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" trees : %zu (%u talents total)\n", + c.trees.size(), totalTalents(c)); + if (c.trees.empty()) return 0; + for (const auto& t : c.trees) { + std::printf("\n treeId=%u classMask=0x%x %s (%zu talents)\n", + t.treeId, t.requiredClassMask, + t.name.c_str(), t.talents.size()); + if (t.talents.empty()) { + std::printf(" *no talents*\n"); + continue; + } + std::printf(" id row col maxRank prereq spellAtR1\n"); + for (const auto& a : t.talents) { + std::printf(" %5u %u %u %u %5u/%u %u\n", + a.talentId, a.row, a.col, a.maxRank, + a.prereqTalentId, a.prereqRank, + a.rankSpellIds[0]); + } + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWtalExt(base); + if (!wowee::pipeline::WoweeTalentLoader::exists(base)) { + std::fprintf(stderr, + "validate-wtal: WTAL not found: %s.wtal\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTalentLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.trees.empty()) { + warnings.push_back("catalog has zero trees"); + } + std::vector treeIdsSeen; + std::vector talentIdsSeen; + for (size_t k = 0; k < c.trees.size(); ++k) { + const auto& t = c.trees[k]; + std::string ctx = "tree " + std::to_string(k) + + " (id=" + std::to_string(t.treeId); + if (!t.name.empty()) ctx += " " + t.name; + ctx += ")"; + if (t.treeId == 0) errors.push_back(ctx + ": treeId is 0"); + if (t.name.empty()) errors.push_back(ctx + ": name is empty"); + if (t.requiredClassMask == 0) { + warnings.push_back(ctx + + ": requiredClassMask=0 (every class would see this tree)"); + } + for (uint32_t prev : treeIdsSeen) { + if (prev == t.treeId) { + errors.push_back(ctx + ": duplicate treeId"); + break; + } + } + treeIdsSeen.push_back(t.treeId); + for (size_t ti = 0; ti < t.talents.size(); ++ti) { + const auto& a = t.talents[ti]; + std::string actx = ctx + " talent " + std::to_string(ti) + + " (id=" + std::to_string(a.talentId) + ")"; + if (a.talentId == 0) { + errors.push_back(actx + ": talentId is 0"); + } + if (a.maxRank == 0 || + a.maxRank > wowee::pipeline::WoweeTalent::kMaxRanks) { + errors.push_back(actx + ": maxRank " + + std::to_string(a.maxRank) + " not in 1..5"); + } + // prereqRank check moved into the second pass below + // where we have the prereq talent in hand to compare + // against its actual maxRank. + if (a.prereqTalentId == a.talentId) { + errors.push_back(actx + + ": talent lists itself as prerequisite"); + } + // Active spell talents typically have rankSpellIds[0] + // set even at rank 1 — a passive (stat-modifier) talent + // may legitimately leave them all 0. Just check for + // ascending non-zero ordering: if rank N has a spell, + // rank N-1 should too. + for (int r = 1; r < wowee::pipeline::WoweeTalent::kMaxRanks; ++r) { + if (a.rankSpellIds[r] != 0 && a.rankSpellIds[r - 1] == 0) { + warnings.push_back(actx + + ": rankSpellIds[" + std::to_string(r) + + "] set but rank " + std::to_string(r - 1) + + " is empty (gap in rank progression)"); + break; + } + } + for (uint32_t prev : talentIdsSeen) { + if (prev == a.talentId) { + errors.push_back(actx + ": duplicate talentId"); + break; + } + } + talentIdsSeen.push_back(a.talentId); + } + } + // Second pass: verify prereqTalentId references resolve + // and prereqRank fits within the prereq talent's own + // maxRank. + for (const auto& t : c.trees) { + for (const auto& a : t.talents) { + if (a.prereqTalentId == 0) continue; + const auto* prereq = c.findTalent(a.prereqTalentId); + if (!prereq) { + errors.push_back("talent " + std::to_string(a.talentId) + + ": prereqTalentId " + + std::to_string(a.prereqTalentId) + + " does not exist in this catalog"); + continue; + } + if (a.prereqRank == 0 || a.prereqRank > prereq->maxRank) { + errors.push_back("talent " + std::to_string(a.talentId) + + ": prereqRank " + std::to_string(a.prereqRank) + + " not in 1.." + std::to_string(prereq->maxRank) + + " (prereq talent's maxRank)"); + } + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wtal"] = base + ".wtal"; + 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-wtal: %s.wtal\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu trees, %u talents, all IDs unique\n", + c.trees.size(), totalTalents(c)); + 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 handleTalentsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-talents") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-talents-warrior") == 0 && i + 1 < argc) { + outRc = handleGenWarrior(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-talents-mage") == 0 && i + 1 < argc) { + outRc = handleGenMage(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wtal") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wtal") == 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_talents_catalog.hpp b/tools/editor/cli_talents_catalog.hpp new file mode 100644 index 00000000..5ad475b4 --- /dev/null +++ b/tools/editor/cli_talents_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleTalentsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee