From 62e793800c75caf0c36dba6801017a0f7c776e49 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 18:27:02 -0700 Subject: [PATCH] feat(pipeline): add WPET (Wowee Pet System) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for AzerothCore-style pet_template + pet_levelstats SQL + the pet-related subsets of CreatureFamily.dbc + SpellFamilyName.dbc. The 38th open format added to the editor. Defines two related kinds of player-controlled NPCs in one catalog: • Pet families — hunter pet families (Wolf / Cat / Bear / Boar / Raptor / Spider / etc.) with per-family ability sets, base stat multipliers, and diet preferences • Warlock minions — Imp / Voidwalker / Succubus / Felhunter / Felguard, each with their own summon spell, creature template, and ability list Cross-references with previously-added formats: WPET.family.familyId -> WCRT.entry.familyId (matches creature family) WPET.family.abilities.spellId -> WSPL.entry.spellId WPET.minion.summonSpellId -> WSPL.entry.spellId WPET.minion.creatureId -> WCRT.entry.creatureId (used for stat scaling) WPET.minion.abilities.spellId -> WSPL.entry.spellId The starter preset's familyIds (1=Wolf, 2=Cat) match WCRT::FamilyId enum values, so a hunter taming a wolf via WCRT links straight through to WPET ability sets. Format: • magic "WPET", version 1, little-endian • families[]: familyId / name / description / icon / petType / baseAttackSpeed / damageMultiplier / armorMultiplier / dietMask / abilities[] • minions[]: minionId / name / summonSpellId / creatureId / abilities[] (each: spellId / rank / autocastDefault) Enums: • PetType (3): Cunning / Ferocity / Tenacity (WotLK+ talent tree categorization) • DietFlags: Meat / Fish / Bread / Cheese / Fruit / Fungus API: WoweePetLoader::save / load / exists + WoweePet::findFamily / findMinion + dietMaskName helper that decodes a dietMask into a "meat+fish" string. Three preset emitters showcase typical pet catalogs: • makeStarter — 2 hunter families (Wolf + Cat) with full 3-ability sets + 1 warlock Imp • makeHunter — 8 classic hunter families covering all 3 petType categories with appropriate diet masks • makeWarlock — 5 warlock minions each with summon spell ID and creatureId pointing into WCRT CLI added (5 flags, 663 documented total now): --gen-pets / --gen-pets-hunter / --gen-pets-warlock --info-wpet / --validate-wpet Validator catches: ids=0 + duplicates, empty name, petType out of range, baseAttackSpeed<=0 (would divide by zero in DPS calc), dietMask=0 (pet cannot be fed for happiness), minion missing summonSpellId / creatureId. --- CMakeLists.txt | 3 + include/pipeline/wowee_pets.hpp | 147 ++++++++++++++ src/pipeline/wowee_pets.cpp | 308 ++++++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_pets_catalog.cpp | 288 ++++++++++++++++++++++++++++ tools/editor/cli_pets_catalog.hpp | 11 ++ 8 files changed, 771 insertions(+) create mode 100644 include/pipeline/wowee_pets.hpp create mode 100644 src/pipeline/wowee_pets.cpp create mode 100644 tools/editor/cli_pets_catalog.cpp create mode 100644 tools/editor/cli_pets_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c1e63d80..e4ab7295 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -625,6 +625,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_gems.cpp src/pipeline/wowee_guilds.cpp src/pipeline/wowee_conditions.cpp + src/pipeline/wowee_pets.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1396,6 +1397,7 @@ add_executable(wowee_editor tools/editor/cli_gems_catalog.cpp tools/editor/cli_guilds_catalog.cpp tools/editor/cli_conditions_catalog.cpp + tools/editor/cli_pets_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1499,6 +1501,7 @@ add_executable(wowee_editor src/pipeline/wowee_gems.cpp src/pipeline/wowee_guilds.cpp src/pipeline/wowee_conditions.cpp + src/pipeline/wowee_pets.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_pets.hpp b/include/pipeline/wowee_pets.hpp new file mode 100644 index 00000000..48cea8fb --- /dev/null +++ b/include/pipeline/wowee_pets.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Pet System catalog (.wpet) — novel replacement +// for AzerothCore-style pet_template + pet_levelstats SQL +// + the pet-related subsets of CreatureFamily.dbc and +// SpellFamilyName.dbc. The 38th open format added to the +// editor. +// +// Defines two related kinds of player-controlled NPCs: +// • Pet families — hunter pet families (Wolf / Cat / Bear / +// Boar / Raptor / Spider / etc.) with +// per-family ability sets, base stat +// multipliers, and diet preferences +// • Warlock minions — Imp / Voidwalker / Succubus / +// Felhunter / Felguard, each with +// their own summon spell and +// ability list +// +// Cross-references with previously-added formats: +// WPET.family.familyId → WCRT.entry.familyId +// (links to creature family) +// WPET.family.abilities.spellId → WSPL.entry.spellId +// WPET.minion.summonSpellId → WSPL.entry.spellId +// WPET.minion.creatureId → WCRT.entry.creatureId +// WPET.minion.abilities.spellId → WSPL.entry.spellId +// +// Binary layout (little-endian): +// magic[4] = "WPET" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// familyCount (uint32) +// families (each): +// familyId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// petType (uint8) / pad[3] +// baseAttackSpeed (float) / damageMultiplier (float) +// armorMultiplier (float) +// dietMask (uint32) +// abilityCount (uint8) + pad[3] + +// abilities (each: spellId(4) + learnedAtLevel(2) + rank(1) + pad) +// minionCount (uint32) +// minions (each): +// minionId (uint32) +// nameLen + name +// summonSpellId (uint32) / creatureId (uint32) +// abilityCount (uint8) + pad[3] + +// abilities (each: spellId(4) + rank(1) + autocast(1) + pad[2]) +struct WoweePet { + enum PetType : uint8_t { + Cunning = 0, + Ferocity = 1, + Tenacity = 2, + }; + + enum DietFlags : uint32_t { + DietMeat = 0x01, + DietFish = 0x02, + DietBread = 0x04, + DietCheese = 0x08, + DietFruit = 0x10, + DietFungus = 0x20, + }; + + struct FamilyAbility { + uint32_t spellId = 0; + uint16_t learnedAtLevel = 1; + uint8_t rank = 1; + }; + + struct Family { + uint32_t familyId = 0; + std::string name; + std::string description; + std::string iconPath; + uint8_t petType = Cunning; + float baseAttackSpeed = 2.0f; + float damageMultiplier = 1.0f; + float armorMultiplier = 1.0f; + uint32_t dietMask = 0; + std::vector abilities; + }; + + struct MinionAbility { + uint32_t spellId = 0; + uint8_t rank = 1; + uint8_t autocastDefault = 0; // 1 = autocast enabled by default + }; + + struct Minion { + uint32_t minionId = 0; + std::string name; + uint32_t summonSpellId = 0; + uint32_t creatureId = 0; // WCRT cross-ref for stats + std::vector abilities; + }; + + std::string name; + std::vector families; + std::vector minions; + + bool isValid() const { return !families.empty() || !minions.empty(); } + + const Family* findFamily(uint32_t familyId) const; + const Minion* findMinion(uint32_t minionId) const; + + static const char* petTypeName(uint8_t t); + // Decode a dietMask into a short string ("meat+fish"). + static std::string dietMaskName(uint32_t mask); +}; + +class WoweePetLoader { +public: + static bool save(const WoweePet& cat, + const std::string& basePath); + static WoweePet load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-pets* variants. + // + // makeStarter — 2 hunter families (Wolf + Cat) with + // 3 abilities each + 1 warlock minion + // (Imp). + // makeHunter — full beast family set (8 classic + // families: Wolf / Cat / Bear / Boar / + // Raptor / Hyena / Spider / Bat) with + // per-type Cunning/Ferocity/Tenacity + // classification. + // makeWarlock — 5 minions (Imp / Voidwalker / + // Succubus / Felhunter / Felguard) with + // summon spell + creature template + // cross-refs. + static WoweePet makeStarter(const std::string& catalogName); + static WoweePet makeHunter(const std::string& catalogName); + static WoweePet makeWarlock(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_pets.cpp b/src/pipeline/wowee_pets.cpp new file mode 100644 index 00000000..d130db04 --- /dev/null +++ b/src/pipeline/wowee_pets.cpp @@ -0,0 +1,308 @@ +#include "pipeline/wowee_pets.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'P', 'E', '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) != ".wpet") { + base += ".wpet"; + } + return base; +} + +} // namespace + +const WoweePet::Family* WoweePet::findFamily(uint32_t familyId) const { + for (const auto& f : families) if (f.familyId == familyId) return &f; + return nullptr; +} + +const WoweePet::Minion* WoweePet::findMinion(uint32_t minionId) const { + for (const auto& m : minions) if (m.minionId == minionId) return &m; + return nullptr; +} + +const char* WoweePet::petTypeName(uint8_t t) { + switch (t) { + case Cunning: return "cunning"; + case Ferocity: return "ferocity"; + case Tenacity: return "tenacity"; + default: return "unknown"; + } +} + +std::string WoweePet::dietMaskName(uint32_t mask) { + std::string s; + if (mask & DietMeat) s += "meat+"; + if (mask & DietFish) s += "fish+"; + if (mask & DietBread) s += "bread+"; + if (mask & DietCheese) s += "cheese+"; + if (mask & DietFruit) s += "fruit+"; + if (mask & DietFungus) s += "fungus+"; + if (s.empty()) return "-"; + s.pop_back(); // drop trailing '+' + return s; +} + +bool WoweePetLoader::save(const WoweePet& 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 famCount = static_cast(cat.families.size()); + writePOD(os, famCount); + for (const auto& f : cat.families) { + writePOD(os, f.familyId); + writeStr(os, f.name); + writeStr(os, f.description); + writeStr(os, f.iconPath); + writePOD(os, f.petType); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, f.baseAttackSpeed); + writePOD(os, f.damageMultiplier); + writePOD(os, f.armorMultiplier); + writePOD(os, f.dietMask); + uint8_t abCount = static_cast( + f.abilities.size() > 255 ? 255 : f.abilities.size()); + writePOD(os, abCount); + os.write(reinterpret_cast(pad3), 3); + for (uint8_t k = 0; k < abCount; ++k) { + const auto& a = f.abilities[k]; + writePOD(os, a.spellId); + writePOD(os, a.learnedAtLevel); + writePOD(os, a.rank); + uint8_t pad = 0; + writePOD(os, pad); + } + } + uint32_t minCount = static_cast(cat.minions.size()); + writePOD(os, minCount); + for (const auto& m : cat.minions) { + writePOD(os, m.minionId); + writeStr(os, m.name); + writePOD(os, m.summonSpellId); + writePOD(os, m.creatureId); + uint8_t abCount = static_cast( + m.abilities.size() > 255 ? 255 : m.abilities.size()); + writePOD(os, abCount); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + for (uint8_t k = 0; k < abCount; ++k) { + const auto& a = m.abilities[k]; + writePOD(os, a.spellId); + writePOD(os, a.rank); + writePOD(os, a.autocastDefault); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + } + } + return os.good(); +} + +WoweePet WoweePetLoader::load(const std::string& basePath) { + WoweePet 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; + auto fail = [&]() { + out.families.clear(); out.minions.clear(); + return out; + }; + uint32_t famCount = 0; + if (!readPOD(is, famCount)) return out; + if (famCount > (1u << 20)) return out; + out.families.resize(famCount); + for (auto& f : out.families) { + if (!readPOD(is, f.familyId)) return fail(); + if (!readStr(is, f.name) || !readStr(is, f.description) || + !readStr(is, f.iconPath)) return fail(); + if (!readPOD(is, f.petType)) return fail(); + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) return fail(); + if (!readPOD(is, f.baseAttackSpeed) || + !readPOD(is, f.damageMultiplier) || + !readPOD(is, f.armorMultiplier) || + !readPOD(is, f.dietMask)) return fail(); + uint8_t abCount = 0; + if (!readPOD(is, abCount)) return fail(); + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) return fail(); + f.abilities.resize(abCount); + for (uint8_t k = 0; k < abCount; ++k) { + auto& a = f.abilities[k]; + if (!readPOD(is, a.spellId) || + !readPOD(is, a.learnedAtLevel) || + !readPOD(is, a.rank)) return fail(); + uint8_t pad = 0; + if (!readPOD(is, pad)) return fail(); + } + } + uint32_t minCount = 0; + if (!readPOD(is, minCount)) return fail(); + if (minCount > (1u << 20)) return fail(); + out.minions.resize(minCount); + for (auto& m : out.minions) { + if (!readPOD(is, m.minionId)) return fail(); + if (!readStr(is, m.name)) return fail(); + if (!readPOD(is, m.summonSpellId) || + !readPOD(is, m.creatureId)) return fail(); + uint8_t abCount = 0; + if (!readPOD(is, abCount)) return fail(); + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) return fail(); + m.abilities.resize(abCount); + for (uint8_t k = 0; k < abCount; ++k) { + auto& a = m.abilities[k]; + if (!readPOD(is, a.spellId) || + !readPOD(is, a.rank) || + !readPOD(is, a.autocastDefault)) return fail(); + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) return fail(); + } + } + return out; +} + +bool WoweePetLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweePet WoweePetLoader::makeStarter(const std::string& catalogName) { + WoweePet c; + c.name = catalogName; + { + // familyId=1 matches WCRT::FamWolf. + WoweePet::Family f; + f.familyId = 1; f.name = "Wolf"; + f.description = "Pack hunter; favors meat."; + f.petType = WoweePet::Ferocity; + f.dietMask = WoweePet::DietMeat; + f.abilities.push_back({27050, 1, 1}); // Bite r1 + f.abilities.push_back({27047, 16, 1}); // Furious Howl + f.abilities.push_back({27049, 30, 1}); // Dash + c.families.push_back(f); + } + { + // familyId=2 matches WCRT::FamCat. + WoweePet::Family f; + f.familyId = 2; f.name = "Cat"; + f.description = "Stealthy hunter; favors meat or fish."; + f.petType = WoweePet::Ferocity; + f.dietMask = WoweePet::DietMeat | WoweePet::DietFish; + f.abilities.push_back({27049, 1, 1}); // Claw r1 + f.abilities.push_back({16827, 12, 1}); // Prowl + f.abilities.push_back({26064, 24, 1}); // Dash + c.families.push_back(f); + } + { + WoweePet::Minion m; + m.minionId = 1; m.name = "Imp"; + m.summonSpellId = 688; // canonical Summon Imp + m.creatureId = 416; // canonical Imp creatureId + m.abilities.push_back({3110, 1, 1}); // Firebolt r1 + m.abilities.push_back({7813, 1, 0}); // Blood Pact (autocast off) + c.minions.push_back(m); + } + return c; +} + +WoweePet WoweePetLoader::makeHunter(const std::string& catalogName) { + WoweePet c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t type, + uint32_t diet) { + WoweePet::Family f; + f.familyId = id; f.name = name; + f.petType = type; f.dietMask = diet; + c.families.push_back(f); + }; + // familyId values match WCRT::FamilyId enum (1..9). + add(1, "Wolf", WoweePet::Ferocity, WoweePet::DietMeat); + add(2, "Cat", WoweePet::Ferocity, + WoweePet::DietMeat | WoweePet::DietFish); + add(3, "Bear", WoweePet::Tenacity, + WoweePet::DietMeat | WoweePet::DietFish | WoweePet::DietFruit); + add(4, "Boar", WoweePet::Tenacity, + WoweePet::DietMeat | WoweePet::DietFungus | WoweePet::DietBread); + add(5, "Raptor", WoweePet::Cunning, WoweePet::DietMeat); + add(6, "Hyena", WoweePet::Cunning, WoweePet::DietMeat); + add(7, "Spider", WoweePet::Cunning, + WoweePet::DietMeat | WoweePet::DietFungus); + add(9, "Crab", WoweePet::Tenacity, + WoweePet::DietMeat | WoweePet::DietFish); + return c; +} + +WoweePet WoweePetLoader::makeWarlock(const std::string& catalogName) { + WoweePet c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t summonSpell, uint32_t creatureId) { + WoweePet::Minion m; + m.minionId = id; m.name = name; + m.summonSpellId = summonSpell; m.creatureId = creatureId; + c.minions.push_back(m); + }; + add(1, "Imp", 688, 416); + add(2, "Voidwalker", 697, 1860); + add(3, "Succubus", 712, 1863); + add(4, "Felhunter", 691, 417); + add(5, "Felguard", 30146, 17252); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 3c59ae1d..7dcc8f17 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -109,6 +109,8 @@ const char* const kArgRequired[] = { "--export-wgld-json", "--import-wgld-json", "--gen-conditions", "--gen-conditions-gated", "--gen-conditions-event", "--info-wpcd", "--validate-wpcd", + "--gen-pets", "--gen-pets-hunter", "--gen-pets-warlock", + "--info-wpet", "--validate-wpet", "--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 adc8cb0f..cd7bb82f 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -65,6 +65,7 @@ #include "cli_gems_catalog.hpp" #include "cli_guilds_catalog.hpp" #include "cli_conditions_catalog.hpp" +#include "cli_pets_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -171,6 +172,7 @@ constexpr DispatchFn kDispatchTable[] = { handleGemsCatalog, handleGuildsCatalog, handleConditionsCatalog, + handlePetsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 870af7c0..dc63ea17 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1231,6 +1231,16 @@ void printUsage(const char* argv0) { std::printf(" Print WPCD entries (id / group / kind / aggregator / negated / target / min..max value / name)\n"); std::printf(" --validate-wpcd [--json]\n"); std::printf(" Static checks: id>0+unique, kind in 0..16, aggregator 0..1, kinds that need targetId have non-zero target\n"); + std::printf(" --gen-pets [name]\n"); + std::printf(" Emit .wpet starter: 2 hunter families (Wolf + Cat) + 1 warlock minion (Imp) with WSPL ability refs\n"); + std::printf(" --gen-pets-hunter [name]\n"); + std::printf(" Emit .wpet 8 classic hunter families (Wolf/Cat/Bear/Boar/Raptor/Hyena/Spider/Crab) with diet+petType\n"); + std::printf(" --gen-pets-warlock [name]\n"); + std::printf(" Emit .wpet 5 warlock minions (Imp/Voidwalker/Succubus/Felhunter/Felguard) with summon spell + WCRT ref\n"); + std::printf(" --info-wpet [--json]\n"); + std::printf(" Print WPET families (id / petType / atkSpd / dmg+arm mult / diet) + minions (id / summon / creatureId)\n"); + std::printf(" --validate-wpet [--json]\n"); + std::printf(" Static checks: ids>0+unique, name not empty, petType 0..2, atkSpeed>0, minion needs summon+creatureId\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_pets_catalog.cpp b/tools/editor/cli_pets_catalog.cpp new file mode 100644 index 00000000..fdedba73 --- /dev/null +++ b/tools/editor/cli_pets_catalog.cpp @@ -0,0 +1,288 @@ +#include "cli_pets_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_pets.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWpetExt(std::string base) { + stripExt(base, ".wpet"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweePet& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweePetLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wpet\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweePet& c, + const std::string& base) { + std::printf("Wrote %s.wpet\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" families : %zu minions : %zu\n", + c.families.size(), c.minions.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterPets"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpetExt(base); + auto c = wowee::pipeline::WoweePetLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-pets")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenHunter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "HunterPets"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpetExt(base); + auto c = wowee::pipeline::WoweePetLoader::makeHunter(name); + if (!saveOrError(c, base, "gen-pets-hunter")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWarlock(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WarlockMinions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpetExt(base); + auto c = wowee::pipeline::WoweePetLoader::makeWarlock(name); + if (!saveOrError(c, base, "gen-pets-warlock")) 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 = stripWpetExt(base); + if (!wowee::pipeline::WoweePetLoader::exists(base)) { + std::fprintf(stderr, "WPET not found: %s.wpet\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePetLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wpet"] = base + ".wpet"; + j["name"] = c.name; + j["familyCount"] = c.families.size(); + j["minionCount"] = c.minions.size(); + nlohmann::json fa = nlohmann::json::array(); + for (const auto& f : c.families) { + nlohmann::json jf; + jf["familyId"] = f.familyId; + jf["name"] = f.name; + jf["description"] = f.description; + jf["iconPath"] = f.iconPath; + jf["petType"] = f.petType; + jf["petTypeName"] = wowee::pipeline::WoweePet::petTypeName(f.petType); + jf["baseAttackSpeed"] = f.baseAttackSpeed; + jf["damageMultiplier"] = f.damageMultiplier; + jf["armorMultiplier"] = f.armorMultiplier; + jf["dietMask"] = f.dietMask; + jf["dietMaskName"] = wowee::pipeline::WoweePet::dietMaskName(f.dietMask); + nlohmann::json abs = nlohmann::json::array(); + for (const auto& a : f.abilities) { + abs.push_back({ + {"spellId", a.spellId}, + {"learnedAtLevel", a.learnedAtLevel}, + {"rank", a.rank}, + }); + } + jf["abilities"] = abs; + fa.push_back(jf); + } + j["families"] = fa; + nlohmann::json ma = nlohmann::json::array(); + for (const auto& m : c.minions) { + nlohmann::json jm; + jm["minionId"] = m.minionId; + jm["name"] = m.name; + jm["summonSpellId"] = m.summonSpellId; + jm["creatureId"] = m.creatureId; + nlohmann::json abs = nlohmann::json::array(); + for (const auto& a : m.abilities) { + abs.push_back({ + {"spellId", a.spellId}, + {"rank", a.rank}, + {"autocastDefault", a.autocastDefault}, + }); + } + jm["abilities"] = abs; + ma.push_back(jm); + } + j["minions"] = ma; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WPET: %s.wpet\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" families : %zu minions : %zu\n", + c.families.size(), c.minions.size()); + if (!c.families.empty()) { + std::printf("\n Families:\n"); + std::printf(" id type atkSpd dmg arm diet name\n"); + for (const auto& f : c.families) { + std::printf(" %4u %-8s %4.1f %4.2f %4.2f %-13s %s (%zu abilities)\n", + f.familyId, + wowee::pipeline::WoweePet::petTypeName(f.petType), + f.baseAttackSpeed, f.damageMultiplier, + f.armorMultiplier, + wowee::pipeline::WoweePet::dietMaskName(f.dietMask).c_str(), + f.name.c_str(), f.abilities.size()); + } + } + if (!c.minions.empty()) { + std::printf("\n Minions:\n"); + std::printf(" id summonSpell creatureId name\n"); + for (const auto& m : c.minions) { + std::printf(" %4u %5u %5u %s (%zu abilities)\n", + m.minionId, m.summonSpellId, m.creatureId, + m.name.c_str(), m.abilities.size()); + } + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWpetExt(base); + if (!wowee::pipeline::WoweePetLoader::exists(base)) { + std::fprintf(stderr, + "validate-wpet: WPET not found: %s.wpet\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePetLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.families.empty() && c.minions.empty()) { + warnings.push_back("catalog has zero families and zero minions"); + } + std::vector famIds; + for (size_t k = 0; k < c.families.size(); ++k) { + const auto& f = c.families[k]; + std::string ctx = "family " + std::to_string(k) + + " (id=" + std::to_string(f.familyId); + if (!f.name.empty()) ctx += " " + f.name; + ctx += ")"; + if (f.familyId == 0) errors.push_back(ctx + ": familyId is 0"); + if (f.name.empty()) errors.push_back(ctx + ": name is empty"); + if (f.petType > wowee::pipeline::WoweePet::Tenacity) { + errors.push_back(ctx + ": petType " + + std::to_string(f.petType) + " not in 0..2"); + } + if (f.baseAttackSpeed <= 0) { + errors.push_back(ctx + ": baseAttackSpeed must be > 0"); + } + if (f.dietMask == 0) { + warnings.push_back(ctx + + ": dietMask=0 (pet cannot be fed for happiness)"); + } + for (uint32_t prev : famIds) { + if (prev == f.familyId) { + errors.push_back(ctx + ": duplicate familyId"); + break; + } + } + famIds.push_back(f.familyId); + } + std::vector minIds; + for (size_t k = 0; k < c.minions.size(); ++k) { + const auto& m = c.minions[k]; + std::string ctx = "minion " + std::to_string(k) + + " (id=" + std::to_string(m.minionId); + if (!m.name.empty()) ctx += " " + m.name; + ctx += ")"; + if (m.minionId == 0) errors.push_back(ctx + ": minionId is 0"); + if (m.name.empty()) errors.push_back(ctx + ": name is empty"); + if (m.summonSpellId == 0) { + errors.push_back(ctx + ": summonSpellId is 0 (cannot summon)"); + } + if (m.creatureId == 0) { + errors.push_back(ctx + + ": creatureId is 0 (no WCRT template for stats)"); + } + for (uint32_t prev : minIds) { + if (prev == m.minionId) { + errors.push_back(ctx + ": duplicate minionId"); + break; + } + } + minIds.push_back(m.minionId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wpet"] = base + ".wpet"; + 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-wpet: %s.wpet\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu families, %zu minions, all IDs unique\n", + c.families.size(), c.minions.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 handlePetsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-pets") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-pets-hunter") == 0 && i + 1 < argc) { + outRc = handleGenHunter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-pets-warlock") == 0 && i + 1 < argc) { + outRc = handleGenWarlock(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wpet") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wpet") == 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_pets_catalog.hpp b/tools/editor/cli_pets_catalog.hpp new file mode 100644 index 00000000..d6ba43f1 --- /dev/null +++ b/tools/editor/cli_pets_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handlePetsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee