From 16ebcdda44b96cc3a1999926acb50eecdf449b4f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 23:32:25 -0700 Subject: [PATCH] feat(editor): add WSPS (Spell Proc Trigger) open catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for AzerothCore's spell_proc_event SQL table plus the per-spell proc fields embedded in Spell.dbc. Defines when a "trigger" spell fires in response to other spell/combat events: Windfury Weapon procs on melee attack, Clearcasting on damaging cast, Judgement of Wisdom on melee hit, etc. Each entry says "when an event matching procFlags fires from a spell matching procFromSpellId (0 = any), at procChance probability with at most one trigger per internalCooldownMs window, fire triggerSpellId". The procPpm field provides an alternative procs-per-minute formula (when non-zero, supersedes procChance and scales with weapon speed for melee procs — the canonical WoW behavior for weapon imbues). 13-bit procFlags bitfield covers the standard event taxonomy: DealtMeleeAutoAttack / DealtMeleeSpell / TakenMeleeAutoAttack / TakenMeleeSpell / DealtRangedAutoAttack / DealtRangedSpell / DealtSpell / DealtSpellHeal / TakenSpell / OnKill / OnDeath / OnCastFinished / Critical (the last is a modifier — fires only on crit-tagged events). Cross-references back to WSPL (triggerSpellId references the spell that fires; procFromSpellId optionally restricts to a specific source spell). Three preset emitters: --gen-sps (4 weapon-imbue procs with canonical PPM rates and ICDs), --gen-sps-aura (4 aura-tied procs across multiple proc-flag combinations), --gen-sps-talent (4 talent procs including charge-consuming Clearcasting and Nightfall variants). Validation enforces id+name+triggerSpellId+procFlags presence, no duplicate ids; warns on: - procChance outside [0..1] (engine clamps) - procPpm < 0 (invalid PPM rate) - both procChance > 0 AND procPpm > 0 set (engine prefers PPM so procChance is silently ignored) - both procChance=0 AND procPpm=0 (proc never fires) Wired through the cross-format table; WSPS appears automatically in all 18 cross-format utilities. Format count 88 -> 89; CLI flag count 1041 -> 1046. --- CMakeLists.txt | 3 + include/pipeline/wowee_spell_procs.hpp | 120 +++++++++ src/pipeline/wowee_spell_procs.cpp | 269 +++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + tools/editor/cli_spell_procs_catalog.cpp | 294 +++++++++++++++++++++++ tools/editor/cli_spell_procs_catalog.hpp | 12 + 10 files changed, 714 insertions(+) create mode 100644 include/pipeline/wowee_spell_procs.hpp create mode 100644 src/pipeline/wowee_spell_procs.cpp create mode 100644 tools/editor/cli_spell_procs_catalog.cpp create mode 100644 tools/editor/cli_spell_procs_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bbd76af..34492f3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -677,6 +677,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_item_flags.cpp src/pipeline/wowee_npc_services.cpp src/pipeline/wowee_token_rewards.cpp + src/pipeline/wowee_spell_procs.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1517,6 +1518,7 @@ add_executable(wowee_editor tools/editor/cli_item_flags_catalog.cpp tools/editor/cli_npc_services_catalog.cpp tools/editor/cli_token_rewards_catalog.cpp + tools/editor/cli_spell_procs_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1672,6 +1674,7 @@ add_executable(wowee_editor src/pipeline/wowee_item_flags.cpp src/pipeline/wowee_npc_services.cpp src/pipeline/wowee_token_rewards.cpp + src/pipeline/wowee_spell_procs.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_procs.hpp b/include/pipeline/wowee_spell_procs.hpp new file mode 100644 index 00000000..f8080258 --- /dev/null +++ b/include/pipeline/wowee_spell_procs.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Proc Trigger catalog (.wsps) — novel +// replacement for AzerothCore's spell_proc_event SQL +// table plus the per-spell proc fields embedded in +// Spell.dbc. Defines when a "trigger" spell fires in +// response to other spell/combat events: Windfury Weapon +// procs on melee attack, Clearcasting on damaging cast, +// Judgement of Wisdom on melee hit, etc. +// +// Each entry says "when an event matching procFlags fires +// from a spell matching procFromSpellId (0 = any), at +// procChance probability with at most one trigger per +// internalCooldownMs window, fire triggerSpellId". The +// procPpm field provides an alternative procs-per-minute +// formula (when non-zero, supersedes procChance and +// scales with weapon speed for melee procs). +// +// Cross-references with previously-added formats: +// WSPL: triggerSpellId references the spell that fires +// on proc; procFromSpellId references the source +// spell whose events qualify (0 = any spell). +// +// Binary layout (little-endian): +// magic[4] = "WSPS" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// procId (uint32) +// nameLen + name +// descLen + description +// triggerSpellId (uint32) +// procFromSpellId (uint32) +// procChance (float) +// procPpm (float) +// procFlags (uint32) +// internalCooldownMs (uint32) +// charges (uint8) / pad[3] +// iconColorRGBA (uint32) +struct WoweeSpellProc { + enum ProcFlag : uint32_t { + DealtMeleeAutoAttack = 1u << 0, // white melee swing landed + DealtMeleeSpell = 1u << 1, // melee ability hit + TakenMeleeAutoAttack = 1u << 2, // received white melee + TakenMeleeSpell = 1u << 3, // received melee ability + DealtRangedAutoAttack = 1u << 4, // ranged auto-shot landed + DealtRangedSpell = 1u << 5, // ranged spell landed + DealtSpell = 1u << 6, // any harmful spell landed + DealtSpellHeal = 1u << 7, // healing spell landed + TakenSpell = 1u << 8, // received any harmful spell + OnKill = 1u << 9, // fired a killing blow + OnDeath = 1u << 10, // wearer died + OnCastFinished = 1u << 11, // any spell cast completed + Critical = 1u << 12, // restricted to crit events + }; + + struct Entry { + uint32_t procId = 0; + std::string name; + std::string description; + uint32_t triggerSpellId = 0; + uint32_t procFromSpellId = 0; // 0 = any source + float procChance = 0.0f; // 0..1 + float procPpm = 0.0f; // 0 = use procChance + uint32_t procFlags = 0; + uint32_t internalCooldownMs = 0; + uint8_t charges = 0; // 0 = unlimited + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint8_t pad2 = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t procId) const; + + static const char* procFlagName(uint32_t bit); +}; + +class WoweeSpellProcLoader { +public: + static bool save(const WoweeSpellProc& cat, + const std::string& basePath); + static WoweeSpellProc load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-sps* variants. + // + // makeWeapon — 4 weapon-imbue procs (Windfury / + // Frostbrand / Flametongue / Mana Oil) + // triggered on DealtMeleeAutoAttack + // with PPM-style chance. + // makeAura — 4 aura-tied procs (Blessing of + // Wisdom mana return, Molten Armor + // crit-reflect, Earth Shield heal, + // Judgement of Wisdom). + // makeTalent — 4 talent procs (Clearcasting, + // Omen of Clarity, Seal of + // Righteousness, Nightfall) with + // internal cooldowns to match canonical + // WoW behavior. + static WoweeSpellProc makeWeapon(const std::string& catalogName); + static WoweeSpellProc makeAura(const std::string& catalogName); + static WoweeSpellProc makeTalent(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_procs.cpp b/src/pipeline/wowee_spell_procs.cpp new file mode 100644 index 00000000..6dcf7544 --- /dev/null +++ b/src/pipeline/wowee_spell_procs.cpp @@ -0,0 +1,269 @@ +#include "pipeline/wowee_spell_procs.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'P', 'S'}; +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) != ".wsps") { + base += ".wsps"; + } + 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 WoweeSpellProc::Entry* +WoweeSpellProc::findById(uint32_t procId) const { + for (const auto& e : entries) + if (e.procId == procId) return &e; + return nullptr; +} + +const char* WoweeSpellProc::procFlagName(uint32_t bit) { + switch (bit) { + case DealtMeleeAutoAttack: return "DealtMeleeAutoAttack"; + case DealtMeleeSpell: return "DealtMeleeSpell"; + case TakenMeleeAutoAttack: return "TakenMeleeAutoAttack"; + case TakenMeleeSpell: return "TakenMeleeSpell"; + case DealtRangedAutoAttack: return "DealtRangedAutoAttack"; + case DealtRangedSpell: return "DealtRangedSpell"; + case DealtSpell: return "DealtSpell"; + case DealtSpellHeal: return "DealtSpellHeal"; + case TakenSpell: return "TakenSpell"; + case OnKill: return "OnKill"; + case OnDeath: return "OnDeath"; + case OnCastFinished: return "OnCastFinished"; + case Critical: return "Critical"; + default: return "Unknown"; + } +} + +bool WoweeSpellProcLoader::save(const WoweeSpellProc& 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.procId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.triggerSpellId); + writePOD(os, e.procFromSpellId); + writePOD(os, e.procChance); + writePOD(os, e.procPpm); + writePOD(os, e.procFlags); + writePOD(os, e.internalCooldownMs); + writePOD(os, e.charges); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.pad2); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeSpellProc WoweeSpellProcLoader::load(const std::string& basePath) { + WoweeSpellProc 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.procId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.triggerSpellId) || + !readPOD(is, e.procFromSpellId) || + !readPOD(is, e.procChance) || + !readPOD(is, e.procPpm) || + !readPOD(is, e.procFlags) || + !readPOD(is, e.internalCooldownMs) || + !readPOD(is, e.charges) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.pad2) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellProcLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpellProc WoweeSpellProcLoader::makeWeapon( + const std::string& catalogName) { + using P = WoweeSpellProc; + WoweeSpellProc c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t triggerSpell, + float ppm, uint32_t cd, const char* desc) { + P::Entry e; + e.procId = id; e.name = name; e.description = desc; + e.triggerSpellId = triggerSpell; + // Weapon imbue procs all key off DealtMeleeAutoAttack + // (white swings only) with PPM-style chance. + e.procFlags = P::DealtMeleeAutoAttack; + e.procPpm = ppm; + e.internalCooldownMs = cd; + e.iconColorRGBA = packRgba(220, 180, 100); // weapon yellow + c.entries.push_back(e); + }; + // Spell ids match canonical 3.3.5a weapon-imbue triggers. + add(1, "WindfuryWeapon", 25504, 20.0f, 3000, + "Windfury Weapon — 20 PPM extra attack with 3s ICD."); + add(2, "FrostbrandWeapon", 8034, 9.0f, 0, + "Frostbrand Weapon — 9 PPM frost damage + slow."); + add(3, "FlametongueWeapon",10444, 15.0f, 0, + "Flametongue Weapon — 15 PPM fire damage on hit."); + add(4, "ManaOilTorch", 28568, 4.0f, 5000, + "Brilliant Mana Oil — 4 PPM mana restore on hit, " + "5s ICD."); + return c; +} + +WoweeSpellProc WoweeSpellProcLoader::makeAura( + const std::string& catalogName) { + using P = WoweeSpellProc; + WoweeSpellProc c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t triggerSpell, + uint32_t flags, float chance, uint32_t cd, + const char* desc) { + P::Entry e; + e.procId = id; e.name = name; e.description = desc; + e.triggerSpellId = triggerSpell; + e.procFlags = flags; + e.procChance = chance; + e.internalCooldownMs = cd; + e.iconColorRGBA = packRgba(220, 200, 100); // holy gold + c.entries.push_back(e); + }; + // Aura-tied procs — buff is on the player, fires when + // they take/deal qualifying actions. + add(100, "BlessingOfWisdomMana", 27144, + P::DealtMeleeAutoAttack | P::DealtMeleeSpell, + 1.0f, 0, + "Blessing of Wisdom — 100%% mana return on melee/spell."); + add(101, "MoltenArmorCrit", 30482, + P::TakenMeleeAutoAttack | P::TakenMeleeSpell, + 0.05f, 0, + "Molten Armor — 5%% damage reflect on incoming melee."); + add(102, "EarthShieldHeal", 974, + P::TakenSpell | P::TakenMeleeSpell, + 1.0f, 1500, + "Earth Shield — 100%% heal on damage taken, 1.5s ICD."); + add(103, "JudgementOfWisdom", 20186, + P::DealtMeleeAutoAttack | P::DealtMeleeSpell, + 0.5f, 0, + "Judgement of Wisdom — 50%% mana return per hit on " + "judged target."); + return c; +} + +WoweeSpellProc WoweeSpellProcLoader::makeTalent( + const std::string& catalogName) { + using P = WoweeSpellProc; + WoweeSpellProc c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t triggerSpell, + uint32_t fromSpell, uint32_t flags, float chance, + uint32_t cd, uint8_t charges, const char* desc) { + P::Entry e; + e.procId = id; e.name = name; e.description = desc; + e.triggerSpellId = triggerSpell; + e.procFromSpellId = fromSpell; + e.procFlags = flags; + e.procChance = chance; + e.internalCooldownMs = cd; + e.charges = charges; + e.iconColorRGBA = packRgba(180, 100, 240); // talent purple + c.entries.push_back(e); + }; + add(200, "Clearcasting", 12536, 0, + P::DealtSpell, 0.10f, 0, 1, + "Mage Arcane Concentration — 10%% chance per cast for " + "next-spell free, 1 charge."); + add(201, "OmenOfClarity", 16870, 0, + P::DealtMeleeAutoAttack | P::DealtSpell, 0.06f, 0, 1, + "Druid Omen of Clarity — 6%% per swing/cast for " + "next-ability free, 1 charge."); + add(202, "SealOfRighteousness",25742, 21084, + P::DealtMeleeAutoAttack, 1.0f, 0, 0, + "Paladin Seal of Righteousness — 100%% on melee swing " + "while seal active."); + add(203, "Nightfall", 17941, 0, + P::DealtSpell, 0.04f, 0, 1, + "Warlock Nightfall — 4%% per Drain Soul/Corruption tick " + "for instant Shadow Bolt, 1 charge."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 71e3122c..c88c630a 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -273,6 +273,8 @@ const char* const kArgRequired[] = { "--gen-tbr", "--gen-tbr-pvp", "--gen-tbr-faction", "--info-wtbr", "--validate-wtbr", "--export-wtbr-json", "--import-wtbr-json", + "--gen-sps", "--gen-sps-aura", "--gen-sps-talent", + "--info-wsps", "--validate-wsps", "--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 816c4bfb..aae24e11 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -133,6 +133,7 @@ #include "cli_item_flags_catalog.hpp" #include "cli_npc_services_catalog.hpp" #include "cli_token_rewards_catalog.hpp" +#include "cli_spell_procs_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -307,6 +308,7 @@ constexpr DispatchFn kDispatchTable[] = { handleItemFlagsCatalog, handleNPCServicesCatalog, handleTokenRewardsCatalog, + handleSpellProcsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 41714389..2f5a637f 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -91,6 +91,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','I','F','S'}, ".wifs", "items", "--info-wifs", "Item flag bit catalog"}, {{'W','B','K','D'}, ".wbkd", "npcs", "--info-wbkd", "NPC service definition catalog"}, {{'W','T','B','R'}, ".wtbr", "tokens", "--info-wtbr", "Token reward redemption catalog"}, + {{'W','S','P','S'}, ".wsps", "spells", "--info-wsps", "Spell proc trigger 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 23e4f15f..e8d47795 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1999,6 +1999,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wtbr to a human-editable JSON sidecar (defaults to .wtbr.json)\n"); std::printf(" --import-wtbr-json [out-base]\n"); std::printf(" Import a .wtbr.json sidecar back into binary .wtbr (accepts rewardKind/requiredFactionStanding int OR name)\n"); + std::printf(" --gen-sps [name]\n"); + std::printf(" Emit .wsps 4 weapon-imbue procs (Windfury 20PPM+3sICD / Frostbrand 9PPM / Flametongue 15PPM / Mana Oil 4PPM+5sICD) on DealtMeleeAutoAttack\n"); + std::printf(" --gen-sps-aura [name]\n"); + std::printf(" Emit .wsps 4 aura-tied procs (Blessing of Wisdom mana / Molten Armor reflect / Earth Shield heal / Judgement of Wisdom)\n"); + std::printf(" --gen-sps-talent [name]\n"); + std::printf(" Emit .wsps 4 talent procs (Clearcasting / Omen of Clarity / Seal of Righteousness / Nightfall) with charge-consuming variants\n"); + std::printf(" --info-wsps [--json]\n"); + std::printf(" Print WSPS entries (id / triggerSpellId / fromSpellId / chance%% / PPM / ICD / charges / proc flags)\n"); + std::printf(" --validate-wsps [--json]\n"); + std::printf(" Static checks: id+name+triggerSpellId+procFlags required, no duplicate ids; warns on chance outside [0,1], procPpm<0, both chance+ppm set (PPM wins), neither set (never fires)\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 65042cf4..dd77027e 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -113,6 +113,7 @@ constexpr FormatRow kFormats[] = { {"WIFS", ".wifs", "items", "Item.dbc Flags bit meanings", "Item flag bit catalog"}, {"WBKD", ".wbkd", "npcs", "npc_vendor + npc_trainer SQL", "NPC service definition catalog"}, {"WTBR", ".wtbr", "tokens", "currency_token_reward SQL", "Token reward redemption catalog"}, + {"WSPS", ".wsps", "spells", "spell_proc_event SQL + Spell.dbc", "Spell proc trigger catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_procs_catalog.cpp b/tools/editor/cli_spell_procs_catalog.cpp new file mode 100644 index 00000000..d1d5a048 --- /dev/null +++ b/tools/editor/cli_spell_procs_catalog.cpp @@ -0,0 +1,294 @@ +#include "cli_spell_procs_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_procs.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWspsExt(std::string base) { + stripExt(base, ".wsps"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpellProc& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellProcLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wsps\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellProc& c, + const std::string& base) { + std::printf("Wrote %s.wsps\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" procs : %zu\n", c.entries.size()); +} + +int handleGenWeapon(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WeaponProcs"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspsExt(base); + auto c = wowee::pipeline::WoweeSpellProcLoader::makeWeapon(name); + if (!saveOrError(c, base, "gen-sps")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAura(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AuraProcs"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspsExt(base); + auto c = wowee::pipeline::WoweeSpellProcLoader::makeAura(name); + if (!saveOrError(c, base, "gen-sps-aura")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenTalent(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "TalentProcs"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspsExt(base); + auto c = wowee::pipeline::WoweeSpellProcLoader::makeTalent(name); + if (!saveOrError(c, base, "gen-sps-talent")) return 1; + printGenSummary(c, base); + return 0; +} + +void appendProcFlagNames(uint32_t flags, std::string& out) { + using F = wowee::pipeline::WoweeSpellProc; + auto add = [&](const char* n) { + if (!out.empty()) out += "|"; + out += n; + }; + if (flags & F::DealtMeleeAutoAttack) add("DealtMeleeAutoAttack"); + if (flags & F::DealtMeleeSpell) add("DealtMeleeSpell"); + if (flags & F::TakenMeleeAutoAttack) add("TakenMeleeAutoAttack"); + if (flags & F::TakenMeleeSpell) add("TakenMeleeSpell"); + if (flags & F::DealtRangedAutoAttack) add("DealtRangedAutoAttack"); + if (flags & F::DealtRangedSpell) add("DealtRangedSpell"); + if (flags & F::DealtSpell) add("DealtSpell"); + if (flags & F::DealtSpellHeal) add("DealtSpellHeal"); + if (flags & F::TakenSpell) add("TakenSpell"); + if (flags & F::OnKill) add("OnKill"); + if (flags & F::OnDeath) add("OnDeath"); + if (flags & F::OnCastFinished) add("OnCastFinished"); + if (flags & F::Critical) add("Critical"); + if (out.empty()) out = "-"; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWspsExt(base); + if (!wowee::pipeline::WoweeSpellProcLoader::exists(base)) { + std::fprintf(stderr, "WSPS not found: %s.wsps\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellProcLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wsps"] = base + ".wsps"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string flagNames; + appendProcFlagNames(e.procFlags, flagNames); + arr.push_back({ + {"procId", e.procId}, + {"name", e.name}, + {"description", e.description}, + {"triggerSpellId", e.triggerSpellId}, + {"procFromSpellId", e.procFromSpellId}, + {"procChance", e.procChance}, + {"procPpm", e.procPpm}, + {"procFlags", e.procFlags}, + {"procFlagsLabels", flagNames}, + {"internalCooldownMs", e.internalCooldownMs}, + {"charges", e.charges}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSPS: %s.wsps\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" procs : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id trigger fromSpell chance%% PPM ICDms chg flags\n"); + for (const auto& e : c.entries) { + std::string flagNames; + appendProcFlagNames(e.procFlags, flagNames); + std::printf(" %4u %5u %5u %5.1f %5.1f %5u %3u %s\n", + e.procId, e.triggerSpellId, e.procFromSpellId, + e.procChance * 100.0f, e.procPpm, + e.internalCooldownMs, e.charges, + flagNames.c_str()); + std::printf(" %s\n", + 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 = stripWspsExt(base); + if (!wowee::pipeline::WoweeSpellProcLoader::exists(base)) { + std::fprintf(stderr, + "validate-wsps: WSPS not found: %s.wsps\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellProcLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + constexpr uint32_t kKnownFlagMask = + wowee::pipeline::WoweeSpellProc::DealtMeleeAutoAttack | + wowee::pipeline::WoweeSpellProc::DealtMeleeSpell | + wowee::pipeline::WoweeSpellProc::TakenMeleeAutoAttack | + wowee::pipeline::WoweeSpellProc::TakenMeleeSpell | + wowee::pipeline::WoweeSpellProc::DealtRangedAutoAttack | + wowee::pipeline::WoweeSpellProc::DealtRangedSpell | + wowee::pipeline::WoweeSpellProc::DealtSpell | + wowee::pipeline::WoweeSpellProc::DealtSpellHeal | + wowee::pipeline::WoweeSpellProc::TakenSpell | + wowee::pipeline::WoweeSpellProc::OnKill | + wowee::pipeline::WoweeSpellProc::OnDeath | + wowee::pipeline::WoweeSpellProc::OnCastFinished | + wowee::pipeline::WoweeSpellProc::Critical; + 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.procId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.procId == 0) + errors.push_back(ctx + ": procId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.triggerSpellId == 0) + errors.push_back(ctx + + ": triggerSpellId is 0 — proc will fire nothing"); + if (e.procFlags == 0) + errors.push_back(ctx + + ": procFlags is 0 — proc will never trigger " + "(no qualifying event configured)"); + if (e.procFlags & ~kKnownFlagMask) { + warnings.push_back(ctx + + ": procFlags has bits outside known mask " + + "(0x" + std::to_string(e.procFlags & ~kKnownFlagMask) + + ") — engine will ignore unknown flags"); + } + if (e.procChance < 0.0f || e.procChance > 1.0f) { + warnings.push_back(ctx + + ": procChance " + std::to_string(e.procChance) + + " is outside [0..1]; engine clamps but author " + "should double-check"); + } + if (e.procPpm < 0.0f) { + errors.push_back(ctx + + ": procPpm < 0 — invalid procs-per-minute rate"); + } + // Both procChance and procPpm set is contradictory — + // engine prefers procPpm when non-zero so procChance + // is ignored. + if (e.procChance > 0.0f && e.procPpm > 0.0f) { + warnings.push_back(ctx + + ": both procChance (" + std::to_string(e.procChance) + + ") and procPpm (" + std::to_string(e.procPpm) + + ") set — engine uses procPpm and ignores " + "procChance"); + } + // No chance configured at all = proc never fires. + if (e.procChance == 0.0f && e.procPpm == 0.0f) { + warnings.push_back(ctx + + ": both procChance=0 and procPpm=0 — proc " + + "will never trigger"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.procId) { + errors.push_back(ctx + ": duplicate procId"); + break; + } + } + idsSeen.push_back(e.procId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wsps"] = base + ".wsps"; + 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-wsps: %s.wsps\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu procs, all procIds unique\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 handleSpellProcsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-sps") == 0 && i + 1 < argc) { + outRc = handleGenWeapon(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-sps-aura") == 0 && i + 1 < argc) { + outRc = handleGenAura(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-sps-talent") == 0 && i + 1 < argc) { + outRc = handleGenTalent(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wsps") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wsps") == 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_spell_procs_catalog.hpp b/tools/editor/cli_spell_procs_catalog.hpp new file mode 100644 index 00000000..3b6d3e81 --- /dev/null +++ b/tools/editor/cli_spell_procs_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellProcsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee