From c0920a20471cc9c7ef307e774d39c6cda60c6092 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 20:09:44 -0700 Subject: [PATCH] feat(pipeline): add WSMC (Wowee Spell Mechanic) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 54th open format — replaces SpellMechanic.dbc plus the AzerothCore-style diminishing-returns (DR) tables. Defines crowd-control mechanic categories that spells reference: Stun, Silence, Polymorph, Sleep, Fear, Root, Snare, Slow, Knockback, etc. Each mechanic carries gameplay metadata (breaks-on-damage, can-be-dispelled, default duration, max stacks) plus DR category and dispel type. 8 DR categories (DRNone / DRStun / DRDisorient / DRSilence / DRRoot / DRPolymorph / DRControlled / DRMisc) — the runtime uses these to gate repeated CC on the same target. 7 dispel types (DispelNone / Magic / Curse / Disease / Poison / Enrage / Stealth) bind which dispel spells can remove the mechanic. conflictsMask is a bitmask of OTHER mechanic IDs — only one mechanic from a conflict-group can apply to a target simultaneously. Cross-references with prior formats — mechanicId is referenced by WSPL.spellId entries that apply this CC; this catalog is referenced from spell tags rather than referencing out. CLI: --gen-smc (3 baseline Stun/Silence/Snare), --gen-smc- hard (5 hard-CC: Stun/Polymorph/Sleep/Fear/Knockback with conflictsMask wiring), --gen-smc-roots (4 movement-impair: Root/Snare/Slow stacking 5x/GroundPin breaks-on-damage), --info-wsmc, --validate-wsmc with --json variants. Validator catches id+name required, DR category 0..7, dispel type 0..6, maxStacks=0 (mechanic could never apply), canBeDispelled+DispelNone inconsistency, and self-conflict bit set in conflictsMask (mechanic blocking itself). Format graph: 53 → 54 binary formats. CLI flag count: 784 → 789. --- CMakeLists.txt | 3 + include/pipeline/wowee_spell_mechanics.hpp | 120 ++++++++ src/pipeline/wowee_spell_mechanics.cpp | 283 +++++++++++++++++++ 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_mechanics_catalog.cpp | 246 ++++++++++++++++ tools/editor/cli_spell_mechanics_catalog.hpp | 12 + 10 files changed, 680 insertions(+) create mode 100644 include/pipeline/wowee_spell_mechanics.hpp create mode 100644 src/pipeline/wowee_spell_mechanics.cpp create mode 100644 tools/editor/cli_spell_mechanics_catalog.cpp create mode 100644 tools/editor/cli_spell_mechanics_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 623fc6bb..6463c915 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -642,6 +642,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_item_sets.cpp src/pipeline/wowee_game_tips.cpp src/pipeline/wowee_companions.cpp + src/pipeline/wowee_spell_mechanics.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1436,6 +1437,7 @@ add_executable(wowee_editor tools/editor/cli_touch_tree.cpp tools/editor/cli_game_tips_catalog.cpp tools/editor/cli_companions_catalog.cpp + tools/editor/cli_spell_mechanics_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1556,6 +1558,7 @@ add_executable(wowee_editor src/pipeline/wowee_item_sets.cpp src/pipeline/wowee_game_tips.cpp src/pipeline/wowee_companions.cpp + src/pipeline/wowee_spell_mechanics.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_mechanics.hpp b/include/pipeline/wowee_spell_mechanics.hpp new file mode 100644 index 00000000..dd99cb0a --- /dev/null +++ b/include/pipeline/wowee_spell_mechanics.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Mechanic catalog (.wsmc) — novel +// replacement for Blizzard's SpellMechanic.dbc plus the +// AzerothCore-style diminishing-returns (DR) tables. Defines +// crowd-control mechanic categories that spells reference: +// Stun, Silence, Polymorph, Root, Snare, Disorient, Disarm, +// Knockback, Sleep, Fear, Charm, Frenzy. Each mechanic +// carries gameplay metadata (breaks-on-damage, can-be- +// dispelled, default duration) plus a diminishing-returns +// category that the runtime uses to gate repeated CC on +// the same target. +// +// Cross-references with previously-added formats: +// WSMC.entry.mechanicId is referenced by WSPL.spellId +// (each spell tags itself with the +// CC category it applies) +// WSMC.entry.conflictsMask is a bitmask of OTHER WSMC +// mechanicIds — only one mechanic +// from a conflict-group can apply +// to a target simultaneously. +// +// Binary layout (little-endian): +// magic[4] = "WSMC" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// mechanicId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// breaksOnDamage (uint8) / canBeDispelled (uint8) / +// drCategory (uint8) / dispelType (uint8) +// defaultDurationMs (uint32) +// maxStacks (uint8) / pad[3] +// conflictsMask (uint32) +struct WoweeSpellMechanic { + enum DiminishingReturnsCategory : uint8_t { + DRNone = 0, // no DR grouping + DRStun = 1, // stuns share a DR bucket + DRDisorient = 2, // fears + horrors + scares + DRSilence = 3, // silences + interrupts + DRRoot = 4, // roots (entangling, frost nova) + DRPolymorph = 5, // CC polymorph effects + DRControlled = 6, // mind controls + charms + DRMisc = 7, // catch-all bucket + }; + + enum DispelType : uint8_t { + DispelNone = 0, // not dispellable + DispelMagic = 1, // dispel magic / mass dispel + DispelCurse = 2, // remove curse / curse of weakness + DispelDisease = 3, // cure disease / abolish + DispelPoison = 4, // cure poison / cleanse + DispelEnrage = 5, // tranquilizing shot / soothe + DispelStealth = 6, // not really dispellable, removed + // by detection + }; + + struct Entry { + uint32_t mechanicId = 0; + std::string name; + std::string description; + std::string iconPath; + uint8_t breaksOnDamage = 0; // 1 = ends on damage + uint8_t canBeDispelled = 0; + uint8_t drCategory = DRNone; + uint8_t dispelType = DispelNone; + uint32_t defaultDurationMs = 0; // 0 = caster-defined + uint8_t maxStacks = 1; + uint32_t conflictsMask = 0; // bitmask of other ids + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t mechanicId) const; + + static const char* drCategoryName(uint8_t c); + static const char* dispelTypeName(uint8_t d); +}; + +class WoweeSpellMechanicLoader { +public: + static bool save(const WoweeSpellMechanic& cat, + const std::string& basePath); + static WoweeSpellMechanic load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-smc* variants. + // + // makeStarter — 3 baseline mechanics (Stun / Silence / + // Snare) — covers the most common CC + // cases one each in 3 different DR + // categories. + // makeHardCC — 5 hard-CC mechanics (Stun, Polymorph, + // Sleep, Fear, Knockback) with + // conflictsMask wiring so they DR each + // other in the right buckets. + // makeRoots — 4 movement-impair mechanics (Root, + // Snare, Slow, GroundPin) with the + // breaks-on-damage and stacking flags + // set per real WoW behavior. + static WoweeSpellMechanic makeStarter(const std::string& catalogName); + static WoweeSpellMechanic makeHardCC(const std::string& catalogName); + static WoweeSpellMechanic makeRoots(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_mechanics.cpp b/src/pipeline/wowee_spell_mechanics.cpp new file mode 100644 index 00000000..f624c5b0 --- /dev/null +++ b/src/pipeline/wowee_spell_mechanics.cpp @@ -0,0 +1,283 @@ +#include "pipeline/wowee_spell_mechanics.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'M', 'C'}; +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) != ".wsmc") { + base += ".wsmc"; + } + return base; +} + +} // namespace + +const WoweeSpellMechanic::Entry* +WoweeSpellMechanic::findById(uint32_t mechanicId) const { + for (const auto& e : entries) + if (e.mechanicId == mechanicId) return &e; + return nullptr; +} + +const char* WoweeSpellMechanic::drCategoryName(uint8_t c) { + switch (c) { + case DRNone: return "none"; + case DRStun: return "stun"; + case DRDisorient: return "disorient"; + case DRSilence: return "silence"; + case DRRoot: return "root"; + case DRPolymorph: return "polymorph"; + case DRControlled: return "controlled"; + case DRMisc: return "misc"; + default: return "unknown"; + } +} + +const char* WoweeSpellMechanic::dispelTypeName(uint8_t d) { + switch (d) { + case DispelNone: return "none"; + case DispelMagic: return "magic"; + case DispelCurse: return "curse"; + case DispelDisease: return "disease"; + case DispelPoison: return "poison"; + case DispelEnrage: return "enrage"; + case DispelStealth: return "stealth"; + default: return "unknown"; + } +} + +bool WoweeSpellMechanicLoader::save(const WoweeSpellMechanic& 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.mechanicId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.iconPath); + writePOD(os, e.breaksOnDamage); + writePOD(os, e.canBeDispelled); + writePOD(os, e.drCategory); + writePOD(os, e.dispelType); + writePOD(os, e.defaultDurationMs); + writePOD(os, e.maxStacks); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, e.conflictsMask); + } + return os.good(); +} + +WoweeSpellMechanic WoweeSpellMechanicLoader::load( + const std::string& basePath) { + WoweeSpellMechanic 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.mechanicId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.iconPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.breaksOnDamage) || + !readPOD(is, e.canBeDispelled) || + !readPOD(is, e.drCategory) || + !readPOD(is, e.dispelType) || + !readPOD(is, e.defaultDurationMs) || + !readPOD(is, e.maxStacks)) { + out.entries.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, e.conflictsMask)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellMechanicLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpellMechanic WoweeSpellMechanicLoader::makeStarter( + const std::string& catalogName) { + WoweeSpellMechanic c; + c.name = catalogName; + { + WoweeSpellMechanic::Entry e; + e.mechanicId = 1; e.name = "Stun"; + e.description = "Target is stunned and cannot move, attack, " + "or cast."; + e.iconPath = "Interface/Icons/Spell_Magic_PolymorphRabbit.blp"; + e.breaksOnDamage = 0; + e.canBeDispelled = 0; + e.drCategory = WoweeSpellMechanic::DRStun; + e.dispelType = WoweeSpellMechanic::DispelNone; + e.defaultDurationMs = 4000; + e.maxStacks = 1; + c.entries.push_back(e); + } + { + WoweeSpellMechanic::Entry e; + e.mechanicId = 2; e.name = "Silence"; + e.description = "Target cannot cast spells."; + e.iconPath = "Interface/Icons/Spell_Shadow_ImpPhaseShift.blp"; + e.breaksOnDamage = 0; + e.canBeDispelled = 1; + e.drCategory = WoweeSpellMechanic::DRSilence; + e.dispelType = WoweeSpellMechanic::DispelMagic; + e.defaultDurationMs = 3000; + e.maxStacks = 1; + c.entries.push_back(e); + } + { + WoweeSpellMechanic::Entry e; + e.mechanicId = 3; e.name = "Snare"; + e.description = "Target's movement speed reduced."; + e.iconPath = "Interface/Icons/Spell_Frost_FrostShock.blp"; + e.breaksOnDamage = 0; + e.canBeDispelled = 1; + e.drCategory = WoweeSpellMechanic::DRRoot; + e.dispelType = WoweeSpellMechanic::DispelMagic; + e.defaultDurationMs = 8000; + e.maxStacks = 1; + c.entries.push_back(e); + } + return c; +} + +WoweeSpellMechanic WoweeSpellMechanicLoader::makeHardCC( + const std::string& catalogName) { + WoweeSpellMechanic c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t breaks, + uint8_t dispellable, uint8_t dr, + uint8_t dispelKind, uint32_t durMs, + uint32_t conflicts, const char* desc) { + WoweeSpellMechanic::Entry e; + e.mechanicId = id; e.name = name; e.description = desc; + e.iconPath = std::string("Interface/Icons/Spell_") + + name + ".blp"; + e.breaksOnDamage = breaks; + e.canBeDispelled = dispellable; + e.drCategory = dr; + e.dispelType = dispelKind; + e.defaultDurationMs = durMs; + e.maxStacks = 1; + e.conflictsMask = conflicts; + c.entries.push_back(e); + }; + // Hard-CC mechanics. conflictsMask uses bits = mechanicId + // shifted left — so id=10 (Stun) has bit 0x400 (1<<10) + // referenced by anything that conflicts with Stun. + add(10, "Stun", 0, 0, WoweeSpellMechanic::DRStun, + WoweeSpellMechanic::DispelNone, 4000, 0, + "Target stunned — no actions for 4 seconds."); + add(11, "Polymorph", 1, 1, WoweeSpellMechanic::DRPolymorph, + WoweeSpellMechanic::DispelMagic, 8000, (1u << 10), + "Transformed into a sheep — breaks on damage."); + add(12, "Sleep", 1, 1, WoweeSpellMechanic::DRPolymorph, + WoweeSpellMechanic::DispelMagic, 6000, (1u << 11), + "Target sleeping — breaks on damage."); + add(13, "Fear", 1, 1, WoweeSpellMechanic::DRDisorient, + WoweeSpellMechanic::DispelMagic, 8000, 0, + "Target flees in random direction."); + add(14, "Knockback", 0, 0, WoweeSpellMechanic::DRStun, + WoweeSpellMechanic::DispelNone, 1500, (1u << 10), + "Target launched backward — brief knockdown."); + return c; +} + +WoweeSpellMechanic WoweeSpellMechanicLoader::makeRoots( + const std::string& catalogName) { + WoweeSpellMechanic c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t breaks, + uint8_t maxStacks, uint32_t durMs, + const char* desc) { + WoweeSpellMechanic::Entry e; + e.mechanicId = id; e.name = name; e.description = desc; + e.iconPath = std::string("Interface/Icons/Spell_Nature_") + + name + ".blp"; + e.breaksOnDamage = breaks; + e.canBeDispelled = 1; + e.drCategory = WoweeSpellMechanic::DRRoot; + e.dispelType = WoweeSpellMechanic::DispelMagic; + e.defaultDurationMs = durMs; + e.maxStacks = maxStacks; + c.entries.push_back(e); + }; + add(20, "Root", 0, 1, 6000, + "Target rooted in place — cannot move."); + add(21, "Snare", 0, 1, 8000, + "Movement speed reduced by 50%."); + add(22, "Slow", 0, 5, 10000, + "Stacking slow — each stack adds 10% slow up to 50%."); + add(23, "GroundPin", 1, 1, 3000, + "Pinned to the ground — breaks on damage."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 60092471..a759abb5 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -163,6 +163,8 @@ const char* const kArgRequired[] = { "--gen-cmp", "--gen-cmp-rare", "--gen-cmp-faction", "--info-wcmp", "--validate-wcmp", "--export-wcmp-json", "--import-wcmp-json", + "--gen-smc", "--gen-smc-hard", "--gen-smc-roots", + "--info-wsmc", "--validate-wsmc", "--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 efa5ea93..7031abd8 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -87,6 +87,7 @@ #include "cli_touch_tree.hpp" #include "cli_game_tips_catalog.hpp" #include "cli_companions_catalog.hpp" +#include "cli_spell_mechanics_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -215,6 +216,7 @@ constexpr DispatchFn kDispatchTable[] = { handleTouchTree, handleGameTipsCatalog, handleCompanionsCatalog, + handleSpellMechanicsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 65dd9a7f..842d5929 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -56,6 +56,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','E','T'}, ".wset", "items", "--info-wset", "Item set / tier-bonus catalog"}, {{'W','G','T','P'}, ".wgtp", "ui", "--info-wgtp", "Game tips / tutorial catalog"}, {{'W','C','M','P'}, ".wcmp", "pets", "--info-wcmp", "Companion / vanity pet catalog"}, + {{'W','S','M','C'}, ".wsmc", "spells", "--info-wsmc", "Spell mechanic 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 11d57e9a..0f86cfde 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1485,6 +1485,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcmp to a human-editable JSON sidecar (defaults to .wcmp.json)\n"); std::printf(" --import-wcmp-json [out-base]\n"); std::printf(" Import a .wcmp.json sidecar back into binary .wcmp (accepts companionKind/rarity/factionRestriction int OR name string)\n"); + std::printf(" --gen-smc [name]\n"); + std::printf(" Emit .wsmc starter: 3 baseline mechanics (Stun / Silence / Snare) covering the most common DR categories\n"); + std::printf(" --gen-smc-hard [name]\n"); + std::printf(" Emit .wsmc 5 hard-CC mechanics (Stun / Polymorph / Sleep / Fear / Knockback) with conflictsMask DR wiring\n"); + std::printf(" --gen-smc-roots [name]\n"); + std::printf(" Emit .wsmc 4 movement-impair mechanics (Root / Snare / Slow stacking / GroundPin breaks-on-damage)\n"); + std::printf(" --info-wsmc [--json]\n"); + std::printf(" Print WSMC entries (id / DR category / dispel type / breaks / dispellable / duration / max stacks / conflicts mask / name)\n"); + std::printf(" --validate-wsmc [--json]\n"); + std::printf(" Static checks: id+name required, DR 0..7 / dispel 0..6, maxStacks>0, dispellable+none-dispel inconsistency, self-conflict bit\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 b7dd8199..14c9ccab 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -78,6 +78,7 @@ constexpr FormatRow kFormats[] = { {"WSET", ".wset", "items", "ItemSet.dbc + ItemSetSpell.dbc", "Item set + tier-bonus catalog"}, {"WGTP", ".wgtp", "ui", "GameTips.dbc + tutorial hints", "Game tips / tutorial / loading-screen catalog"}, {"WCMP", ".wcmp", "pets", "CreatureFamily + companion SQL", "Companion / vanity pet catalog"}, + {"WSMC", ".wsmc", "spells", "SpellMechanic.dbc + DR tables", "Spell mechanic / CC category catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_mechanics_catalog.cpp b/tools/editor/cli_spell_mechanics_catalog.cpp new file mode 100644 index 00000000..0880d0a7 --- /dev/null +++ b/tools/editor/cli_spell_mechanics_catalog.cpp @@ -0,0 +1,246 @@ +#include "cli_spell_mechanics_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_mechanics.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWsmcExt(std::string base) { + stripExt(base, ".wsmc"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpellMechanic& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellMechanicLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wsmc\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellMechanic& c, + const std::string& base) { + std::printf("Wrote %s.wsmc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" mechanics : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterMechanics"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsmcExt(base); + auto c = wowee::pipeline::WoweeSpellMechanicLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-smc")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenHardCC(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "HardCCMechanics"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsmcExt(base); + auto c = wowee::pipeline::WoweeSpellMechanicLoader::makeHardCC(name); + if (!saveOrError(c, base, "gen-smc-hard")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRoots(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RootMechanics"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsmcExt(base); + auto c = wowee::pipeline::WoweeSpellMechanicLoader::makeRoots(name); + if (!saveOrError(c, base, "gen-smc-roots")) 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 = stripWsmcExt(base); + if (!wowee::pipeline::WoweeSpellMechanicLoader::exists(base)) { + std::fprintf(stderr, "WSMC not found: %s.wsmc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellMechanicLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wsmc"] = base + ".wsmc"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"mechanicId", e.mechanicId}, + {"name", e.name}, + {"description", e.description}, + {"iconPath", e.iconPath}, + {"breaksOnDamage", e.breaksOnDamage}, + {"canBeDispelled", e.canBeDispelled}, + {"drCategory", e.drCategory}, + {"drCategoryName", wowee::pipeline::WoweeSpellMechanic::drCategoryName(e.drCategory)}, + {"dispelType", e.dispelType}, + {"dispelTypeName", wowee::pipeline::WoweeSpellMechanic::dispelTypeName(e.dispelType)}, + {"defaultDurationMs", e.defaultDurationMs}, + {"maxStacks", e.maxStacks}, + {"conflictsMask", e.conflictsMask}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSMC: %s.wsmc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" mechanics : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id DR-cat dispel breaks disp dur(ms) stack conflicts name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-10s %-8s %u %u %5u %3u 0x%08x %s\n", + e.mechanicId, + wowee::pipeline::WoweeSpellMechanic::drCategoryName(e.drCategory), + wowee::pipeline::WoweeSpellMechanic::dispelTypeName(e.dispelType), + e.breaksOnDamage, e.canBeDispelled, + e.defaultDurationMs, e.maxStacks, + e.conflictsMask, 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 = stripWsmcExt(base); + if (!wowee::pipeline::WoweeSpellMechanicLoader::exists(base)) { + std::fprintf(stderr, + "validate-wsmc: WSMC not found: %s.wsmc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellMechanicLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector 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.mechanicId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.mechanicId == 0) + errors.push_back(ctx + ": mechanicId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.drCategory > wowee::pipeline::WoweeSpellMechanic::DRMisc) { + errors.push_back(ctx + ": drCategory " + + std::to_string(e.drCategory) + " not in 0..7"); + } + if (e.dispelType > wowee::pipeline::WoweeSpellMechanic::DispelStealth) { + errors.push_back(ctx + ": dispelType " + + std::to_string(e.dispelType) + " not in 0..6"); + } + if (e.maxStacks == 0) { + errors.push_back(ctx + + ": maxStacks=0 (mechanic could never apply)"); + } + // canBeDispelled=1 with dispelType=None is contradictory + // — without a dispel category, no spell can target this + // mechanic for removal. + if (e.canBeDispelled && + e.dispelType == wowee::pipeline::WoweeSpellMechanic::DispelNone) { + errors.push_back(ctx + + ": canBeDispelled=1 but dispelType=none " + "(no dispel spell can target this)"); + } + // A mechanic that conflicts with itself is wrong — + // `conflictsMask & (1 << mechanicId)` would mean the + // mechanic blocks itself. + if (e.mechanicId < 32 && + (e.conflictsMask & (1u << e.mechanicId))) { + errors.push_back(ctx + + ": conflictsMask includes own mechanicId bit " + "(mechanic conflicts with itself)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.mechanicId) { + errors.push_back(ctx + ": duplicate mechanicId"); + break; + } + } + idsSeen.push_back(e.mechanicId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wsmc"] = base + ".wsmc"; + 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-wsmc: %s.wsmc\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu mechanics, all mechanicIds 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 handleSpellMechanicsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-smc") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-smc-hard") == 0 && i + 1 < argc) { + outRc = handleGenHardCC(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-smc-roots") == 0 && i + 1 < argc) { + outRc = handleGenRoots(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wsmc") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wsmc") == 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_mechanics_catalog.hpp b/tools/editor/cli_spell_mechanics_catalog.hpp new file mode 100644 index 00000000..9fa67151 --- /dev/null +++ b/tools/editor/cli_spell_mechanics_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellMechanicsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee