diff --git a/CMakeLists.txt b/CMakeLists.txt index 512d92f0..ee6b2fa7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -695,6 +695,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_spell_markers.cpp src/pipeline/wowee_learning_notifications.cpp src/pipeline/wowee_creature_resists.cpp + src/pipeline/wowee_pet_talents.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1553,6 +1554,7 @@ add_executable(wowee_editor tools/editor/cli_spell_markers_catalog.cpp tools/editor/cli_learning_notifications_catalog.cpp tools/editor/cli_creature_resists_catalog.cpp + tools/editor/cli_pet_talents_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1729,6 +1731,7 @@ add_executable(wowee_editor src/pipeline/wowee_spell_markers.cpp src/pipeline/wowee_learning_notifications.cpp src/pipeline/wowee_creature_resists.cpp + src/pipeline/wowee_pet_talents.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_pet_talents.hpp b/include/pipeline/wowee_pet_talents.hpp new file mode 100644 index 00000000..b4155e6d --- /dev/null +++ b/include/pipeline/wowee_pet_talents.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Pet Talent Tree catalog (.wptt) — novel +// replacement for the PetTalent.dbc + PetTalentTab.dbc +// pair that defined the Hunter pet talent system added +// in WotLK. Each entry is one talent in one of the three +// pet trees (Cunning / Ferocity / Tenacity), placed at a +// (tier, column) grid position with a per-rank spell ID +// array, an optional prerequisite-talent edge, and a +// legacy loyalty-level requirement carried over from +// Vanilla pet happiness mechanics. +// +// Combines three patterns previously seen separately: +// - variable-length payload (spellIdsByRank[], like +// WCMR's members[]) +// - graph edge (prerequisiteTalentId, like WBAB's +// previousRankId) +// - grid placement (tier+column, novel — first format +// with explicit 2D layout coordinates) +// +// Cross-references with previously-added formats: +// WSPL: spellIdsByRank entries reference the WSPL +// spell catalog. +// WPTT: prerequisiteTalentId references ANOTHER entry +// in the same WPTT catalog — internal graph edge. +// WPET: pet families that can train this tree are +// filtered via the WPET catalog's treeKind field +// (lookup external). +// +// Binary layout (little-endian): +// magic[4] = "WPTT" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// talentId (uint32) +// nameLen + name +// descLen + description +// treeKind (uint8) — Cunning / Ferocity / +// Tenacity +// tier (uint8) — 0..6 (7 tiers in +// standard pet tree) +// column (uint8) — 0..2 (3 columns per +// tier) +// maxRank (uint8) — typically 1, 2, 3, or +// 5 talent points +// prerequisiteTalentId (uint32) — 0 if no prereq +// requiredLoyalty (uint8) — Vanilla loyalty +// (0 = always); +// cosmetic in WotLK+ +// pad0 (uint8) / pad1 (uint8) / pad2 (uint8) +// iconColorRGBA (uint32) +// spellCountByRank (uint32) — = maxRank, captured +// explicitly so reader +// can validate the +// array size matches +// spellIdsByRank (count × uint32) +struct WoweePetTalents { + enum TreeKind : uint8_t { + Cunning = 0, // utility — Roar of Recovery, + // Dash, Heart of the Phoenix + Ferocity = 1, // damage — Cobra Reflexes, + // Spiked Collar, Rabid + Tenacity = 2, // tank — Charge, Thunder- + // stomp, Last Stand + }; + + struct Entry { + uint32_t talentId = 0; + std::string name; + std::string description; + uint8_t treeKind = Cunning; + uint8_t tier = 0; + uint8_t column = 0; + uint8_t maxRank = 1; + uint32_t prerequisiteTalentId = 0; + uint8_t requiredLoyalty = 0; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint8_t pad2 = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + std::vector spellIdsByRank; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t talentId) const; + + // Returns all talents in one tree (used by the pet + // talent UI to populate the tree-switching tabs). + std::vector findByTree(uint8_t treeKind) const; + + // Returns the talent (if any) at the given (tier, + // column) of the given tree. Used by the talent grid + // renderer to look up "what occupies this cell?" + const Entry* findAtCell(uint8_t treeKind, uint8_t tier, + uint8_t column) const; +}; + +class WoweePetTalentsLoader { +public: + static bool save(const WoweePetTalents& cat, + const std::string& basePath); + static WoweePetTalents load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-ptt* variants. + // + // makeFerocity — 6 Ferocity (DPS) tree talents + // spanning tiers 0-3 with prereq + // chains (Cobra Reflexes / + // Spiked Collar / Boar's Speed / + // Spider's Bite / Rabid / + // Wolverine Bite). + // makeCunning — 5 Cunning (utility) tree talents + // (Dash / Roar of Recovery / + // Heart of the Phoenix / Owl's + // Focus / Cornered). + // makeTenacity — 5 Tenacity (tank) tree talents + // (Charge / Thunderstomp / Last + // Stand / Taunt / Roar of + // Sacrifice). + static WoweePetTalents makeFerocity(const std::string& catalogName); + static WoweePetTalents makeCunning(const std::string& catalogName); + static WoweePetTalents makeTenacity(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_pet_talents.cpp b/src/pipeline/wowee_pet_talents.cpp new file mode 100644 index 00000000..3d725859 --- /dev/null +++ b/src/pipeline/wowee_pet_talents.cpp @@ -0,0 +1,325 @@ +#include "pipeline/wowee_pet_talents.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'P', 'T', 'T'}; +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) != ".wptt") { + base += ".wptt"; + } + 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); +} + +} // namespace + +const WoweePetTalents::Entry* +WoweePetTalents::findById(uint32_t talentId) const { + for (const auto& e : entries) + if (e.talentId == talentId) return &e; + return nullptr; +} + +std::vector +WoweePetTalents::findByTree(uint8_t treeKind) const { + std::vector out; + for (const auto& e : entries) + if (e.treeKind == treeKind) out.push_back(&e); + return out; +} + +const WoweePetTalents::Entry* +WoweePetTalents::findAtCell(uint8_t treeKind, uint8_t tier, + uint8_t column) const { + for (const auto& e : entries) { + if (e.treeKind == treeKind && e.tier == tier && + e.column == column) return &e; + } + return nullptr; +} + +bool WoweePetTalentsLoader::save(const WoweePetTalents& 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.talentId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.treeKind); + writePOD(os, e.tier); + writePOD(os, e.column); + writePOD(os, e.maxRank); + writePOD(os, e.prerequisiteTalentId); + writePOD(os, e.requiredLoyalty); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.pad2); + writePOD(os, e.iconColorRGBA); + uint32_t spellCount = static_cast( + e.spellIdsByRank.size()); + writePOD(os, spellCount); + for (uint32_t s : e.spellIdsByRank) writePOD(os, s); + } + return os.good(); +} + +WoweePetTalents WoweePetTalentsLoader::load( + const std::string& basePath) { + WoweePetTalents 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.talentId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.treeKind) || + !readPOD(is, e.tier) || + !readPOD(is, e.column) || + !readPOD(is, e.maxRank) || + !readPOD(is, e.prerequisiteTalentId) || + !readPOD(is, e.requiredLoyalty) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.pad2) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + uint32_t spellCount = 0; + if (!readPOD(is, spellCount)) { + out.entries.clear(); return out; + } + if (spellCount > 16) { + out.entries.clear(); return out; + } + e.spellIdsByRank.resize(spellCount); + for (uint32_t k = 0; k < spellCount; ++k) { + if (!readPOD(is, e.spellIdsByRank[k])) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweePetTalentsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweePetTalents WoweePetTalentsLoader::makeFerocity( + const std::string& catalogName) { + using P = WoweePetTalents; + WoweePetTalents c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t tier, uint8_t column, + uint8_t maxRank, + std::vector spells, + uint32_t prereq, const char* desc) { + P::Entry e; + e.talentId = id; e.name = name; e.description = desc; + e.treeKind = P::Ferocity; + e.tier = tier; e.column = column; + e.maxRank = maxRank; + e.spellIdsByRank = std::move(spells); + e.prerequisiteTalentId = prereq; + e.iconColorRGBA = packRgba(220, 60, 60); // ferocity red + c.entries.push_back(e); + }; + add(1, "CobraReflexes", 0, 0, 2, + { 61682, 61683 }, 0, + "Tier 0 col 0 — Increases pet attack speed by " + "15%/30%, reduces damage by 15%. Two ranks. No " + "prerequisite (root of Ferocity tree)."); + add(2, "SerpentSwiftness", 0, 1, 5, + { 16093, 16094, 16095, 16096, 16097 }, 0, + "Tier 0 col 1 — Increases pet attack speed by " + "1%/2%/3%/4%/5%. Five ranks. Root talent."); + add(3, "SpikedCollar", 1, 0, 3, + { 19582, 19583, 19584 }, 1, + "Tier 1 col 0 — Increases pet damage by 1%/2%/3%. " + "Requires CobraReflexes (talentId=1) as prereq."); + add(4, "BoarsSpeed", 2, 1, 1, + { 19596 }, 2, + "Tier 2 col 1 — Increases pet movement speed by " + "30%. Requires SerpentSwiftness rank 2+ (modeled " + "by talentId=2 prereq)."); + add(5, "SpidersBite", 2, 2, 3, + { 19589, 19591, 19592 }, 3, + "Tier 2 col 2 — Increases pet melee crit chance " + "by 3%/6%/9%. Requires SpikedCollar (talentId=3) " + "as prereq."); + add(6, "Rabid", 3, 1, 1, + { 53401 }, 4, + "Tier 3 col 1 — Active ability. Pet enrages, " + "increasing damage by 5% per stack (max 5). Top-" + "tier Ferocity talent. Requires BoarsSpeed " + "(talentId=4)."); + return c; +} + +WoweePetTalents WoweePetTalentsLoader::makeCunning( + const std::string& catalogName) { + using P = WoweePetTalents; + WoweePetTalents c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t tier, uint8_t column, + uint8_t maxRank, + std::vector spells, + uint32_t prereq, const char* desc) { + P::Entry e; + e.talentId = id; e.name = name; e.description = desc; + e.treeKind = P::Cunning; + e.tier = tier; e.column = column; + e.maxRank = maxRank; + e.spellIdsByRank = std::move(spells); + e.prerequisiteTalentId = prereq; + e.iconColorRGBA = packRgba(140, 200, 255); // cunning blue + c.entries.push_back(e); + }; + add(100, "Dash", 0, 0, 3, + { 61684, 61685, 61686 }, 0, + "Tier 0 col 0 — Pet sprint. Increases run speed " + "by 30%/40%/50% for 16 sec. 32-sec cooldown. Root " + "talent."); + add(101, "OwlsFocus", 1, 1, 5, + { 53513, 53514, 53515, 53516, 53517 }, 0, + "Tier 1 col 1 — Pet special-attack damage chance " + "increased by 4%/8%/12%/16%/20%. Root talent."); + add(102, "RoarOfRecovery", 2, 0, 1, + { 53517 }, 100, + "Tier 2 col 0 — Active. Restores 30% of hunter's " + "maximum mana over 9 sec. 3-min cooldown. " + "Requires Dash (talentId=100)."); + add(103, "Cornered", 2, 2, 2, + { 53497, 53499 }, 101, + "Tier 2 col 2 — Pet damage increased by 5%/10% " + "and crit by 5%/10% when below 35% health. " + "Requires OwlsFocus (talentId=101)."); + add(104, "HeartOfThePhoenix", 3, 1, 1, + { 55709 }, 102, + "Tier 3 col 1 — Active. Pet self-resurrects if " + "killed in combat. 10-min cooldown. Top-tier " + "Cunning talent. Requires RoarOfRecovery " + "(talentId=102)."); + return c; +} + +WoweePetTalents WoweePetTalentsLoader::makeTenacity( + const std::string& catalogName) { + using P = WoweePetTalents; + WoweePetTalents c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t tier, uint8_t column, + uint8_t maxRank, + std::vector spells, + uint32_t prereq, const char* desc) { + P::Entry e; + e.talentId = id; e.name = name; e.description = desc; + e.treeKind = P::Tenacity; + e.tier = tier; e.column = column; + e.maxRank = maxRank; + e.spellIdsByRank = std::move(spells); + e.prerequisiteTalentId = prereq; + e.iconColorRGBA = packRgba(160, 220, 80); // tenacity green + c.entries.push_back(e); + }; + add(200, "Charge", 0, 0, 1, + { 61685 }, 0, + "Tier 0 col 0 — Active. Pet charges target, " + "stunning for 1 sec and increasing next attack " + "by 25%. 25-yd range. Root talent."); + add(201, "GreatStamina", 0, 1, 3, + { 61686, 61687, 61688 }, 0, + "Tier 0 col 1 — Increases pet stamina by " + "4%/8%/12%. Root talent — most Tenacity builds " + "max this first."); + add(202, "Thunderstomp", 1, 1, 1, + { 63900 }, 200, + "Tier 1 col 1 — Active AoE. Pet stomps the " + "ground, dealing nature damage to nearby enemies " + "and threat. Requires Charge (talentId=200)."); + add(203, "Taunt", 2, 0, 1, + { 53477 }, 202, + "Tier 2 col 0 — Active. Forces target to attack " + "the pet for 3 sec. Requires Thunderstomp " + "(talentId=202)."); + add(204, "LastStand", 3, 1, 1, + { 53478 }, 203, + "Tier 3 col 1 — Active. Pet temporarily gains " + "30% maximum health for 20 sec. 10-min cooldown. " + "Top-tier Tenacity talent. Requires Taunt " + "(talentId=203)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index bf12c380..97a857e0 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -328,6 +328,8 @@ const char* const kArgRequired[] = { "--gen-cre", "--gen-cre-elites", "--gen-cre-immune", "--info-wcre", "--validate-wcre", "--export-wcre-json", "--import-wcre-json", + "--gen-ptt", "--gen-ptt-cunning", "--gen-ptt-tenacity", + "--info-wptt", "--validate-wptt", "--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 6c970853..c8adfd12 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -151,6 +151,7 @@ #include "cli_spell_markers_catalog.hpp" #include "cli_learning_notifications_catalog.hpp" #include "cli_creature_resists_catalog.hpp" +#include "cli_pet_talents_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -346,6 +347,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpellMarkersCatalog, handleLearningNotificationsCatalog, handleCreatureResistsCatalog, + handlePetTalentsCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index ac066e95..1273993c 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -109,6 +109,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','P','M'}, ".wspm", "spellfx", "--info-wspm", "Spell persistent marker catalog"}, {{'W','L','D','N'}, ".wldn", "server", "--info-wldn", "Learning notification catalog"}, {{'W','C','R','E'}, ".wcre", "creatures", "--info-wcre", "Creature resist + immunity catalog"}, + {{'W','P','T','T'}, ".wptt", "pets", "--info-wptt", "Hunter pet talent 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 e7146bf3..765ae01b 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2251,6 +2251,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcre to a human-editable JSON sidecar (defaults to .wcre.json; emits ccImmunityMask as both raw uint16 AND \"+\"-joined name string \"root+stun+fear\" / \"all\" / \"none\")\n"); std::printf(" --import-wcre-json [out-base]\n"); std::printf(" Import a .wcre.json sidecar back into binary .wcre (ccImmunityMask int OR token string \"all\"/\"none\"/\"+\"-joined from {root,snare,stun,fear,sleep,silence,charm,disarm,polymorph,banish,knockback,interrupt,taunt,bleed})\n"); + std::printf(" --gen-ptt [name]\n"); + std::printf(" Emit .wptt 6 Ferocity (DPS) Hunter pet talents with grid placement + prereq chains (CobraReflexes / SerpentSwiftness / SpikedCollar / BoarsSpeed / SpidersBite / Rabid)\n"); + std::printf(" --gen-ptt-cunning [name]\n"); + std::printf(" Emit .wptt 5 Cunning (utility) Hunter pet talents (Dash / OwlsFocus / RoarOfRecovery / Cornered / HeartOfThePhoenix)\n"); + std::printf(" --gen-ptt-tenacity [name]\n"); + std::printf(" Emit .wptt 5 Tenacity (tank) Hunter pet talents (Charge / GreatStamina / Thunderstomp / Taunt / LastStand)\n"); + std::printf(" --info-wptt [--json]\n"); + std::printf(" Print WPTT entries (id / tree / tier / column / max ranks / prereq talentId / loyalty req / name) plus per-talent rank-spell IDs\n"); + std::printf(" --validate-wptt [--json]\n"); + std::printf(" Static checks: id+name required, treeKind 0..2, tier 0..6, column 0..2, maxRank 1..5, no duplicate talentIds, no two talents in same (tree,tier,col) cell, no self-referencing prereqs, prereqs resolve to existing entries IN SAME TREE at EARLIER TIER, spellIdsByRank.size() == maxRank, no zero-spell-id within array\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 20fdf46f..df7b28cc 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -131,6 +131,7 @@ constexpr FormatRow kFormats[] = { {"WSPM", ".wspm", "spellfx", "AreaTrigger.dbc + decal blob", "Spell persistent marker catalog (AoE ground decals)"}, {"WLDN", ".wldn", "server", "TutorialPopup + LevelMilestone msgs","Learning notification catalog (level-up milestones)"}, {"WCRE", ".wcre", "creatures", "creature_template resist + immunity","Creature resist + CC-immunity profile catalog"}, + {"WPTT", ".wptt", "pets", "PetTalent.dbc + PetTalentTab.dbc", "Hunter pet talent tree catalog (3 trees, grid+graph)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_pet_talents_catalog.cpp b/tools/editor/cli_pet_talents_catalog.cpp new file mode 100644 index 00000000..a32070d9 --- /dev/null +++ b/tools/editor/cli_pet_talents_catalog.cpp @@ -0,0 +1,345 @@ +#include "cli_pet_talents_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_pet_talents.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWpttExt(std::string base) { + stripExt(base, ".wptt"); + return base; +} + +const char* treeKindName(uint8_t k) { + using P = wowee::pipeline::WoweePetTalents; + switch (k) { + case P::Cunning: return "cunning"; + case P::Ferocity: return "ferocity"; + case P::Tenacity: return "tenacity"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweePetTalents& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweePetTalentsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wptt\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweePetTalents& c, + const std::string& base) { + size_t totalSpells = 0; + for (const auto& e : c.entries) + totalSpells += e.spellIdsByRank.size(); + std::printf("Wrote %s.wptt\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" talents : %zu (%zu rank-spells total)\n", + c.entries.size(), totalSpells); +} + +int handleGenFerocity(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FerocityPetTree"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpttExt(base); + auto c = wowee::pipeline::WoweePetTalentsLoader::makeFerocity(name); + if (!saveOrError(c, base, "gen-ptt")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCunning(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "CunningPetTree"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpttExt(base); + auto c = wowee::pipeline::WoweePetTalentsLoader::makeCunning(name); + if (!saveOrError(c, base, "gen-ptt-cunning")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenTenacity(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "TenacityPetTree"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpttExt(base); + auto c = wowee::pipeline::WoweePetTalentsLoader::makeTenacity(name); + if (!saveOrError(c, base, "gen-ptt-tenacity")) 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 = stripWpttExt(base); + if (!wowee::pipeline::WoweePetTalentsLoader::exists(base)) { + std::fprintf(stderr, "WPTT not found: %s.wptt\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePetTalentsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wptt"] = base + ".wptt"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"talentId", e.talentId}, + {"name", e.name}, + {"description", e.description}, + {"treeKind", e.treeKind}, + {"treeKindName", treeKindName(e.treeKind)}, + {"tier", e.tier}, + {"column", e.column}, + {"maxRank", e.maxRank}, + {"prerequisiteTalentId", e.prerequisiteTalentId}, + {"requiredLoyalty", e.requiredLoyalty}, + {"iconColorRGBA", e.iconColorRGBA}, + {"spellIdsByRank", e.spellIdsByRank}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WPTT: %s.wptt\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" talents : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id tree tier col ranks prereq loyalty name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-8s %2u %2u %2u %5u %3u %s\n", + e.talentId, treeKindName(e.treeKind), + e.tier, e.column, e.maxRank, + e.prerequisiteTalentId, + e.requiredLoyalty, e.name.c_str()); + if (!e.spellIdsByRank.empty()) { + std::printf(" rank-spells:"); + for (uint32_t s : e.spellIdsByRank) + std::printf(" %u", s); + std::printf("\n"); + } + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWpttExt(base); + if (!wowee::pipeline::WoweePetTalentsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wptt: WPTT not found: %s.wptt\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePetTalentsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + // Track (tree, tier, column) cell occupancy — two + // talents in the same cell would render on top of + // each other. + std::set cellsSeen; + auto cellKey = [](uint8_t tree, uint8_t tier, uint8_t col) { + return static_cast(tree) << 16 | + static_cast(tier) << 8 | + static_cast(col); + }; + 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.talentId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.talentId == 0) + errors.push_back(ctx + ": talentId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.treeKind > 2) { + errors.push_back(ctx + ": treeKind " + + std::to_string(e.treeKind) + + " out of range (must be 0..2)"); + } + if (e.tier > 6) { + errors.push_back(ctx + ": tier " + + std::to_string(e.tier) + + " > 6 — pet trees have 7 tiers (0-6)"); + } + if (e.column > 2) { + errors.push_back(ctx + ": column " + + std::to_string(e.column) + + " > 2 — pet trees have 3 columns (0-2)"); + } + if (e.maxRank == 0 || e.maxRank > 5) { + errors.push_back(ctx + ": maxRank " + + std::to_string(e.maxRank) + + " out of range (must be 1..5)"); + } + // spellIdsByRank size must equal maxRank. + if (e.spellIdsByRank.size() != + static_cast(e.maxRank)) { + errors.push_back(ctx + + ": spellIdsByRank.size() = " + + std::to_string(e.spellIdsByRank.size()) + + " does not match maxRank " + + std::to_string(e.maxRank)); + } + // No spell ID may be 0 within the array. + for (size_t s = 0; s < e.spellIdsByRank.size(); ++s) { + if (e.spellIdsByRank[s] == 0) { + errors.push_back(ctx + + ": spellIdsByRank[" + std::to_string(s) + + "] = 0 (rank " + std::to_string(s + 1) + + " has no spell)"); + } + } + // Self-reference check for prereq. + if (e.prerequisiteTalentId == e.talentId) { + errors.push_back(ctx + + ": prerequisiteTalentId equals talentId — " + "would create a 1-element prereq cycle"); + } + // Cell occupancy uniqueness. + if (e.tier <= 6 && e.column <= 2 && e.treeKind <= 2) { + uint32_t key = cellKey(e.treeKind, e.tier, e.column); + if (!cellsSeen.insert(key).second) { + errors.push_back(ctx + + ": cell (tree=" + + std::string(treeKindName(e.treeKind)) + + ", tier=" + std::to_string(e.tier) + + ", column=" + std::to_string(e.column) + + ") already occupied by another talent"); + } + } + if (!idsSeen.insert(e.talentId).second) { + errors.push_back(ctx + ": duplicate talentId"); + } + } + // Cross-entry: prereq must resolve to an existing + // talent in the same tree (you can't prereq a Ferocity + // talent from a Cunning slot). + auto findEntry = [&](uint32_t id) -> const + wowee::pipeline::WoweePetTalents::Entry* { + for (const auto& e : c.entries) { + if (e.talentId == id) return &e; + } + return nullptr; + }; + for (const auto& e : c.entries) { + if (e.prerequisiteTalentId == 0) continue; + auto* pre = findEntry(e.prerequisiteTalentId); + if (!pre) { + errors.push_back("entry id=" + + std::to_string(e.talentId) + + " (" + e.name + "): prerequisiteTalentId=" + + std::to_string(e.prerequisiteTalentId) + + " references missing entry"); + continue; + } + if (pre->treeKind != e.treeKind) { + errors.push_back("entry id=" + + std::to_string(e.talentId) + + " (" + e.name + "): prereq " + + std::to_string(e.prerequisiteTalentId) + + " (" + pre->name + ") is in tree '" + + std::string(treeKindName(pre->treeKind)) + + "' but this talent is in tree '" + + std::string(treeKindName(e.treeKind)) + + "' — prereq must be in same tree"); + } + if (pre->tier >= e.tier) { + errors.push_back("entry id=" + + std::to_string(e.talentId) + + " (" + e.name + "): prereq tier " + + std::to_string(pre->tier) + + " >= this talent's tier " + + std::to_string(e.tier) + + " — prereqs must be in earlier tiers"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wptt"] = base + ".wptt"; + 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-wptt: %s.wptt\n", base.c_str()); + if (ok && warnings.empty()) { + size_t totalSpells = 0; + for (const auto& e : c.entries) + totalSpells += e.spellIdsByRank.size(); + std::printf(" OK — %zu talents, %zu rank-spells, " + "all talentIds + cells unique, prereqs " + "valid + earlier-tier\n", + c.entries.size(), totalSpells); + 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 handlePetTalentsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-ptt") == 0 && i + 1 < argc) { + outRc = handleGenFerocity(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-ptt-cunning") == 0 && + i + 1 < argc) { + outRc = handleGenCunning(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-ptt-tenacity") == 0 && + i + 1 < argc) { + outRc = handleGenTenacity(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wptt") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wptt") == 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_pet_talents_catalog.hpp b/tools/editor/cli_pet_talents_catalog.hpp new file mode 100644 index 00000000..ef98c6ff --- /dev/null +++ b/tools/editor/cli_pet_talents_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handlePetTalentsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee