diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b5418b2..4dee0c09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -699,6 +699,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_heroic_scaling.cpp src/pipeline/wowee_reputation_rewards.cpp src/pipeline/wowee_minimap_levels.cpp + src/pipeline/wowee_pet_care.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1561,6 +1562,7 @@ add_executable(wowee_editor tools/editor/cli_heroic_scaling_catalog.cpp tools/editor/cli_reputation_rewards_catalog.cpp tools/editor/cli_minimap_levels_catalog.cpp + tools/editor/cli_pet_care_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1742,6 +1744,7 @@ add_executable(wowee_editor src/pipeline/wowee_heroic_scaling.cpp src/pipeline/wowee_reputation_rewards.cpp src/pipeline/wowee_minimap_levels.cpp + src/pipeline/wowee_pet_care.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_pet_care.hpp b/include/pipeline/wowee_pet_care.hpp new file mode 100644 index 00000000..42a4ab8d --- /dev/null +++ b/include/pipeline/wowee_pet_care.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Pet Care & Action catalog (.wpcr) — novel +// replacement for the implicit pet-management action +// rules vanilla WoW scattered across spell_template +// (Revive Pet / Mend Pet / Feed Pet / Dismiss Pet +// definitions), npc_text (stable master gossip), and +// per-class trainer SQL. Each entry binds one pet +// management action (Revive, Feed, Stable Slot, +// Untrain, etc.) to its dispatching spell, gold cost, +// reagent requirement, cast time, cooldown, and +// pet/NPC pre-conditions. +// +// Cross-references with previously-added formats: +// WSPL: spellId references the WSPL spell catalog +// (the actual spell the client casts when the +// action button is pressed). +// WCHC: classFilter uses the WCHC class-bit +// convention (4=Hunter, 256=Warlock, 64=Shaman +// for elementals, etc.). +// WIT: reagentItemId references the WIT item catalog +// (Mend Pet has no reagent, Feed Pet requires +// food matching the pet's diet, Tame Beast has +// no reagent but consumes a tame slot). +// WPET: actions operate on the active pet from the +// WPET pet catalog. +// +// Binary layout (little-endian): +// magic[4] = "WPCR" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// actionId (uint32) +// nameLen + name +// descLen + description +// spellId (uint32) +// classFilter (uint32) +// actionKind (uint8) — Revive / Mend / Feed +// / Dismiss / Tame / +// BeastLore / Stable / +// Untrain / Rename / +// Abandon / Summon +// happinessRestore (int8) — typically +10 (Feed), +// -10 (Abandon), +// 0 (no effect) +// requiresPet (uint8) — 0/1 bool +// requiresStableNPC (uint8) — 0/1 bool +// costCopper (uint32) — 100 = 1 silver, +// 10000 = 1 gold +// reagentItemId (uint32) — 0 if no reagent +// castTimeMs (uint32) +// cooldownSec (uint32) +// iconColorRGBA (uint32) +struct WoweePetCare { + enum ActionKind : uint8_t { + Revive = 0, // spell 982 + Mend = 1, // spell 136 (channeled HoT) + Feed = 2, // spell 6991 (happiness +) + Dismiss = 3, // spell 2641 (instant despawn) + Tame = 4, // spell 1515 (channeled tame) + BeastLore = 5, // spell 1462 (inspect) + Stable = 6, // 50s per slot up to 5 + Untrain = 7, // gold ramp 10c..10g + Rename = 8, // free, instant + Abandon = 9, // permanent, free + Summon = 10, // Warlock minion summons + }; + + struct Entry { + uint32_t actionId = 0; + std::string name; + std::string description; + uint32_t spellId = 0; + uint32_t classFilter = 0; + uint8_t actionKind = Revive; + int8_t happinessRestore = 0; + uint8_t requiresPet = 1; + uint8_t requiresStableNPC = 0; + uint32_t costCopper = 0; + uint32_t reagentItemId = 0; + uint32_t castTimeMs = 0; + uint32_t cooldownSec = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t actionId) const; + + // Returns all actions available to a specific class + // bit (4=Hunter, 256=Warlock). Used by the action-bar + // UI to populate the pet-actions tab per character. + std::vector findByClass(uint32_t classBit) const; + + // Returns all actions of one kind — used by the + // stable-master NPC interaction handler to find which + // actions become available when the player talks to + // a stable master. + std::vector findByKind(uint8_t actionKind) const; +}; + +class WoweePetCareLoader { +public: + static bool save(const WoweePetCare& cat, + const std::string& basePath); + static WoweePetCare load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-pcr* variants. + // + // makeHunterCare — 5 Hunter pet care actions + // (Revive / Mend / Feed / + // Dismiss / Tame). + // makeStableActions — 4 stable-master actions + // (Stable Slots / Untrain / + // Rename / Abandon). + // makeWarlockMinions — 4 Warlock minion summons + // (Summon Imp / Voidwalker / + // Succubus / Felhunter). + static WoweePetCare makeHunterCare(const std::string& catalogName); + static WoweePetCare makeStableActions(const std::string& catalogName); + static WoweePetCare makeWarlockMinions(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_pet_care.cpp b/src/pipeline/wowee_pet_care.cpp new file mode 100644 index 00000000..e4253d6e --- /dev/null +++ b/src/pipeline/wowee_pet_care.cpp @@ -0,0 +1,308 @@ +#include "pipeline/wowee_pet_care.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'P', 'C', 'R'}; +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) != ".wpcr") { + base += ".wpcr"; + } + 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 WoweePetCare::Entry* +WoweePetCare::findById(uint32_t actionId) const { + for (const auto& e : entries) + if (e.actionId == actionId) return &e; + return nullptr; +} + +std::vector +WoweePetCare::findByClass(uint32_t classBit) const { + std::vector out; + for (const auto& e : entries) + if (e.classFilter & classBit) out.push_back(&e); + return out; +} + +std::vector +WoweePetCare::findByKind(uint8_t actionKind) const { + std::vector out; + for (const auto& e : entries) + if (e.actionKind == actionKind) out.push_back(&e); + return out; +} + +bool WoweePetCareLoader::save(const WoweePetCare& 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.actionId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.spellId); + writePOD(os, e.classFilter); + writePOD(os, e.actionKind); + writePOD(os, e.happinessRestore); + writePOD(os, e.requiresPet); + writePOD(os, e.requiresStableNPC); + writePOD(os, e.costCopper); + writePOD(os, e.reagentItemId); + writePOD(os, e.castTimeMs); + writePOD(os, e.cooldownSec); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweePetCare WoweePetCareLoader::load(const std::string& basePath) { + WoweePetCare 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.actionId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.spellId) || + !readPOD(is, e.classFilter) || + !readPOD(is, e.actionKind) || + !readPOD(is, e.happinessRestore) || + !readPOD(is, e.requiresPet) || + !readPOD(is, e.requiresStableNPC) || + !readPOD(is, e.costCopper) || + !readPOD(is, e.reagentItemId) || + !readPOD(is, e.castTimeMs) || + !readPOD(is, e.cooldownSec) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweePetCareLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweePetCare WoweePetCareLoader::makeHunterCare( + const std::string& catalogName) { + using P = WoweePetCare; + WoweePetCare c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t spellId, uint8_t kind, + int8_t happiness, uint8_t needsPet, + uint32_t cost, uint32_t reagent, + uint32_t castMs, const char* desc) { + P::Entry e; + e.actionId = id; e.name = name; e.description = desc; + e.spellId = spellId; + e.classFilter = 4; // Hunter + e.actionKind = kind; + e.happinessRestore = happiness; + e.requiresPet = needsPet; + e.requiresStableNPC = 0; + e.costCopper = cost; + e.reagentItemId = reagent; + e.castTimeMs = castMs; + // Tame Beast canonically has a 15s internal + // cooldown to prevent macro-spam; other actions + // are off-GCD or no cooldown. + e.cooldownSec = (kind == P::Tame) ? 15 : 0; + e.iconColorRGBA = packRgba(170, 210, 100); // hunter green + c.entries.push_back(e); + }; + add(1, "RevivePet", 982, P::Revive, + 0, 0, 0, 0, 1500, + "Revive a dead pet. 1.5s cast, no cost. Pet " + "must have died within the last 60 seconds."); + add(2, "MendPet", 136, P::Mend, + +10, 1, 0, 0, 0, + "Channel a healing-over-time on the pet. 25s " + "duration, instant cast, +10 happiness over " + "duration. Cannot move while channeling."); + add(3, "FeedPet", 6991, P::Feed, + +10, 1, 0, 4536, 1500, + "Feed the pet a food item to restore happiness. " + "Reagent (Spongy Morel itemId 4536 default; the " + "client picks an appropriate food per-pet " + "diet at cast time)."); + add(4, "DismissPet", 2641, P::Dismiss, + 0, 1, 0, 0, 0, + "Dismiss the active pet (instant). Pet returns " + "to the world but is invisible until re-summoned. " + "No cost, no cooldown."); + add(5, "TameBeast", 1515, P::Tame, + 0, 0, 0, 0, 20000, + "Tame a wild beast. 20s channel — beast attacks " + "during channel; succeed only if hunter survives. " + "No reagent, no cost."); + return c; +} + +WoweePetCare WoweePetCareLoader::makeStableActions( + const std::string& catalogName) { + using P = WoweePetCare; + WoweePetCare c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t spellId, uint8_t kind, + uint32_t cost, const char* desc) { + P::Entry e; + e.actionId = id; e.name = name; e.description = desc; + e.spellId = spellId; + e.classFilter = 4; // Hunter only + e.actionKind = kind; + e.happinessRestore = 0; + e.requiresPet = (kind == P::Stable || + kind == P::Untrain || + kind == P::Rename || + kind == P::Abandon) ? 1 : 0; + e.requiresStableNPC = 1; + e.costCopper = cost; + e.reagentItemId = 0; + e.castTimeMs = 0; + e.cooldownSec = 0; + e.iconColorRGBA = packRgba(140, 100, 60); // stable brown + c.entries.push_back(e); + }; + // costCopper: 50 = 50 copper = 50s + add(100, "StableSlotPurchase", 0, P::Stable, + 500000, + "Purchase a stable slot from the stable master. " + "500g (500000 copper). Up to 5 slots available " + "as the hunter's stable grows."); + add(101, "UntrainPet", 0, P::Untrain, + 10000, + "Reset all pet talent points. Cost ramps with " + "each untrain (1g first, +1g each subsequent — " + "10000 copper = 1g shown as the base entry; " + "client computes ramp at runtime)."); + add(102, "RenamePet", 0, P::Rename, + 0, + "Rename the active pet. Free, instant. Available " + "from any stable master."); + add(103, "AbandonPet", 2641, P::Abandon, + 0, + "Permanently release the active pet (back to " + "the wild). Free, instant — but PERMANENT. The " + "pet is gone forever; cannot be re-tamed without " + "finding the same beast in the world. UI " + "confirmation prompt highly recommended."); + return c; +} + +WoweePetCare WoweePetCareLoader::makeWarlockMinions( + const std::string& catalogName) { + using P = WoweePetCare; + WoweePetCare c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t spellId, uint32_t reagent, + uint32_t castMs, const char* desc) { + P::Entry e; + e.actionId = id; e.name = name; e.description = desc; + e.spellId = spellId; + e.classFilter = 256; // Warlock + e.actionKind = P::Summon; + e.happinessRestore = 0; + e.requiresPet = 0; + e.requiresStableNPC = 0; + e.costCopper = 0; + e.reagentItemId = reagent; + e.castTimeMs = castMs; + e.cooldownSec = 10; // shared 10s warlock + // summon cooldown + e.iconColorRGBA = packRgba(140, 30, 140); // warlock purple + c.entries.push_back(e); + }; + // Reagent: Soul Shard (itemId 6265). + add(200, "SummonImp", 688, 6265, 6500, + "Summon Imp — 6.5s cast, 1 Soul Shard. Imp is " + "the leveling-default minion (ranged caster, " + "Firebolt + Phase Shift)."); + add(201, "SummonVoidwalker", 697, 6265, 10000, + "Summon Voidwalker — 10s cast, 1 Soul Shard. " + "Tank minion (Sacrifice + Suffering taunt)."); + add(202, "SummonSuccubus", 712, 6265, 10000, + "Summon Succubus — 10s cast, 1 Soul Shard. " + "DPS minion (Lash of Pain + Seduce CC)."); + add(203, "SummonFelhunter", 691, 6265, 10000, + "Summon Felhunter — 10s cast, 1 Soul Shard. " + "Anti-magic minion (Spell Lock interrupt + " + "Devour Magic dispel)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 97b8c96a..48af636a 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -341,6 +341,8 @@ const char* const kArgRequired[] = { "--gen-mnl", "--gen-mnl-dalaran", "--gen-mnl-undercity", "--info-wmnl", "--validate-wmnl", "--export-wmnl-json", "--import-wmnl-json", + "--gen-pcr", "--gen-pcr-stable", "--gen-pcr-warlock", + "--info-wpcr", "--validate-wpcr", "--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 61c7f5f4..03461299 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -155,6 +155,7 @@ #include "cli_heroic_scaling_catalog.hpp" #include "cli_reputation_rewards_catalog.hpp" #include "cli_minimap_levels_catalog.hpp" +#include "cli_pet_care_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -355,6 +356,7 @@ constexpr DispatchFn kDispatchTable[] = { handleHeroicScalingCatalog, handleReputationRewardsCatalog, handleMinimapLevelsCatalog, + handlePetCareCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index f80064eb..1163a15a 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -113,6 +113,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','H','R','D'}, ".whrd", "raid", "--info-whrd", "Heroic loot scaling catalog"}, {{'W','R','P','R'}, ".wrpr", "factions", "--info-wrpr", "Reputation reward tier catalog"}, {{'W','M','N','L'}, ".wmnl", "worldmap", "--info-wmnl", "Minimap multi-level catalog"}, + {{'W','P','C','R'}, ".wpcr", "pets", "--info-wpcr", "Pet care + action 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 b5ee5c23..47bde5f4 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2307,6 +2307,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wmnl to a human-editable JSON sidecar (defaults to .wmnl.json; minZ/maxZ as floats, all string fields as plain strings)\n"); std::printf(" --import-wmnl-json [out-base]\n"); std::printf(" Import a .wmnl.json sidecar back into binary .wmnl (no enums to coerce — pure positional/textual data; minZ/maxZ float-precise round-trip)\n"); + std::printf(" --gen-pcr [name]\n"); + std::printf(" Emit .wpcr 5 Hunter pet care actions (Revive Pet / Mend Pet / Feed Pet / Dismiss Pet / Tame Beast)\n"); + std::printf(" --gen-pcr-stable [name]\n"); + std::printf(" Emit .wpcr 4 stable-master actions (Stable Slot Purchase 500g / Untrain Pet 1g+ramp / Rename Pet / Abandon Pet permanent)\n"); + std::printf(" --gen-pcr-warlock [name]\n"); + std::printf(" Emit .wpcr 4 Warlock minion summons (Imp 6.5s / Voidwalker 10s / Succubus 10s / Felhunter 10s, all 1 Soul Shard)\n"); + std::printf(" --info-wpcr [--json]\n"); + std::printf(" Print WPCR entries (id / spellId / classFilter / actionKind / happiness / pet-required / stable-NPC required / cost copper / reagent / cast ms / name)\n"); + std::printf(" --validate-wpcr [--json]\n"); + std::printf(" Static checks: id+name+classFilter required, actionKind 0..10, no duplicate actionIds, per-kind constraints (Tame and Summon REQUIRE no active pet, requiresPet must be 0); warns on happinessRestore outside +/-25, Stable kind without requiresStableNPC, Tame kind without cooldown (canonically 15 sec)\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 9849a226..b5feb75f 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -135,6 +135,7 @@ constexpr FormatRow kFormats[] = { {"WHRD", ".whrd", "raid", "implicit Heroic-mode loot scaling", "Heroic loot scaling catalog (per instance+difficulty)"}, {"WRPR", ".wrpr", "factions", "npc_vendor reqstanding + rep gates", "Reputation reward tier catalog (per faction)"}, {"WMNL", ".wmnl", "worldmap", "WorldMapTransforms.dbc + Overlay", "Minimap multi-level catalog (vertical zones)"}, + {"WPCR", ".wpcr", "pets", "Spell.dbc pet ops + npc_text stable","Pet care + action catalog (Hunter / Warlock / stable mgmt)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_pet_care_catalog.cpp b/tools/editor/cli_pet_care_catalog.cpp new file mode 100644 index 00000000..65d8c047 --- /dev/null +++ b/tools/editor/cli_pet_care_catalog.cpp @@ -0,0 +1,289 @@ +#include "cli_pet_care_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_pet_care.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWpcrExt(std::string base) { + stripExt(base, ".wpcr"); + return base; +} + +const char* actionKindName(uint8_t k) { + using P = wowee::pipeline::WoweePetCare; + switch (k) { + case P::Revive: return "revive"; + case P::Mend: return "mend"; + case P::Feed: return "feed"; + case P::Dismiss: return "dismiss"; + case P::Tame: return "tame"; + case P::BeastLore: return "beastlore"; + case P::Stable: return "stable"; + case P::Untrain: return "untrain"; + case P::Rename: return "rename"; + case P::Abandon: return "abandon"; + case P::Summon: return "summon"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweePetCare& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweePetCareLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wpcr\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweePetCare& c, + const std::string& base) { + std::printf("Wrote %s.wpcr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" actions : %zu\n", c.entries.size()); +} + +int handleGenHunter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "HunterPetCare"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpcrExt(base); + auto c = wowee::pipeline::WoweePetCareLoader::makeHunterCare(name); + if (!saveOrError(c, base, "gen-pcr")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenStable(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StableMasterActions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpcrExt(base); + auto c = wowee::pipeline::WoweePetCareLoader::makeStableActions(name); + if (!saveOrError(c, base, "gen-pcr-stable")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWarlock(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WarlockMinionSummons"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWpcrExt(base); + auto c = wowee::pipeline::WoweePetCareLoader::makeWarlockMinions(name); + if (!saveOrError(c, base, "gen-pcr-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 = stripWpcrExt(base); + if (!wowee::pipeline::WoweePetCareLoader::exists(base)) { + std::fprintf(stderr, "WPCR not found: %s.wpcr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePetCareLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wpcr"] = base + ".wpcr"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"actionId", e.actionId}, + {"name", e.name}, + {"description", e.description}, + {"spellId", e.spellId}, + {"classFilter", e.classFilter}, + {"actionKind", e.actionKind}, + {"actionKindName", actionKindName(e.actionKind)}, + {"happinessRestore", e.happinessRestore}, + {"requiresPet", e.requiresPet != 0}, + {"requiresStableNPC", e.requiresStableNPC != 0}, + {"costCopper", e.costCopper}, + {"reagentItemId", e.reagentItemId}, + {"castTimeMs", e.castTimeMs}, + {"cooldownSec", e.cooldownSec}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WPCR: %s.wpcr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" actions : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id spell class kind happy pet stb cost(c) reagent cast(ms) name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %5u %-9s %+3d %s %s %6u %5u %5u %s\n", + e.actionId, e.spellId, e.classFilter, + actionKindName(e.actionKind), + e.happinessRestore, + e.requiresPet ? "Y" : "n", + e.requiresStableNPC ? "Y" : "n", + e.costCopper, e.reagentItemId, + e.castTimeMs, e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWpcrExt(base); + if (!wowee::pipeline::WoweePetCareLoader::exists(base)) { + std::fprintf(stderr, + "validate-wpcr: WPCR not found: %s.wpcr\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweePetCareLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set 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.actionId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.actionId == 0) + errors.push_back(ctx + ": actionId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.classFilter == 0) { + errors.push_back(ctx + + ": classFilter is 0 — no class can use " + "this action"); + } + if (e.actionKind > 10) { + errors.push_back(ctx + ": actionKind " + + std::to_string(e.actionKind) + + " out of range (must be 0..10)"); + } + if (e.happinessRestore < -25 || e.happinessRestore > 25) { + warnings.push_back(ctx + + ": happinessRestore " + + std::to_string(e.happinessRestore) + + " outside +/-25 — pet happiness range " + "is normally [-100, +100], single-action " + "swing >25 is unusual"); + } + // Per-kind validity rules: + using P = wowee::pipeline::WoweePetCare; + if (e.actionKind == P::Tame && e.requiresPet != 0) { + errors.push_back(ctx + + ": Tame action requires NO pet active " + "(requiresPet must be 0) — you can't tame " + "while another pet is out"); + } + if (e.actionKind == P::Summon && e.requiresPet != 0) { + errors.push_back(ctx + + ": Summon action requires NO pet active " + "(requiresPet must be 0) — Warlock can't " + "summon while another minion is out"); + } + if (e.actionKind == P::Revive && e.requiresPet != 0) { + warnings.push_back(ctx + + ": Revive action with requiresPet=1 — " + "revive should target a DEAD pet, not " + "require an active one. Verify intent."); + } + if (e.actionKind == P::Stable && + e.requiresStableNPC == 0) { + warnings.push_back(ctx + + ": Stable action with requiresStableNPC=0 " + "— stable slot purchases are normally " + "gated to stable-master conversation"); + } + // Tame with cooldown — Tame Beast has a fixed + // 15-second internal cooldown in 3.3.5; warn if + // unset. + if (e.actionKind == P::Tame && e.cooldownSec == 0) { + warnings.push_back(ctx + + ": Tame action with cooldownSec=0 — Tame " + "Beast canonically has a 15-sec internal " + "cooldown to prevent macro-spam"); + } + if (!idsSeen.insert(e.actionId).second) { + errors.push_back(ctx + ": duplicate actionId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wpcr"] = base + ".wpcr"; + 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-wpcr: %s.wpcr\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu actions, all actionIds " + "unique, per-kind constraints " + "satisfied\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 handlePetCareCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-pcr") == 0 && i + 1 < argc) { + outRc = handleGenHunter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-pcr-stable") == 0 && i + 1 < argc) { + outRc = handleGenStable(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-pcr-warlock") == 0 && i + 1 < argc) { + outRc = handleGenWarlock(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wpcr") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wpcr") == 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_care_catalog.hpp b/tools/editor/cli_pet_care_catalog.hpp new file mode 100644 index 00000000..4d85120a --- /dev/null +++ b/tools/editor/cli_pet_care_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handlePetCareCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee