diff --git a/CMakeLists.txt b/CMakeLists.txt index de90952a..d6580315 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -670,6 +670,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_currency_types.cpp src/pipeline/wowee_spell_reagents.cpp src/pipeline/wowee_achievement_criteria.cpp + src/pipeline/wowee_spell_effect_types.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1500,6 +1501,7 @@ add_executable(wowee_editor tools/editor/cli_currency_types_catalog.cpp tools/editor/cli_spell_reagents_catalog.cpp tools/editor/cli_achievement_criteria_catalog.cpp + tools/editor/cli_spell_effect_types_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1648,6 +1650,7 @@ add_executable(wowee_editor src/pipeline/wowee_currency_types.cpp src/pipeline/wowee_spell_reagents.cpp src/pipeline/wowee_achievement_criteria.cpp + src/pipeline/wowee_spell_effect_types.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_effect_types.hpp b/include/pipeline/wowee_spell_effect_types.hpp new file mode 100644 index 00000000..06683a7f --- /dev/null +++ b/include/pipeline/wowee_spell_effect_types.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Effect Type catalog (.wsef) — novel +// replacement for the SpellEffect.Effect field meanings +// in Spell.dbc plus the engine's hard-coded effect +// dispatch table. Defines what each spell-effect integer +// value actually does — SCHOOL_DAMAGE=2 deals magical +// damage, DUMMY=3 is a script hook, HEAL=10 restores +// health, ENERGIZE=30 restores power, APPLY_AURA=6 +// attaches a buff/debuff, etc. +// +// WotLK's Spell.dbc has 192+ effect type integers, each +// with its own resolver in the spell engine. This catalog +// lets the engine look up "given effect=10, what +// resolution behavior do I run?" via a single table lookup +// instead of a hard-coded switch statement, and lets +// server-custom spells reference new effect IDs without +// touching engine code. +// +// Distinct from WAUR (Spell Aura Type) which is the +// secondary classification used when effectType is +// APPLY_AURA — that's a separate enum entirely with its +// own ~300 values. +// +// Cross-references with previously-added formats: +// WSPL: spell.effect[1..3] each reference an effectId +// here. +// WSPC: spells with cost-modifying effects can reference +// WSPC powerCostId via the targetType field +// interpretation. +// +// Binary layout (little-endian): +// magic[4] = "WSEF" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// effectId (uint32) +// nameLen + name +// descLen + description +// effectKind (uint8) / behaviorFlags (uint8) / pad[2] +// baseAmount (int32) +// iconColorRGBA (uint32) +struct WoweeSpellEffectType { + enum EffectKind : uint8_t { + Damage = 0, // deals damage to target + Heal = 1, // restores health + Aura = 2, // applies buff/debuff + Energize = 3, // restores power resource + Trigger = 4, // fires another spell + Movement = 5, // teleport / charge / pull + Summon = 6, // summon pet/totem/object + Dispel = 7, // remove auras + Dummy = 8, // script hook, no built-in behavior + Misc = 9, // catch-all + }; + + enum BehaviorFlag : uint8_t { + RequiresTarget = 1u << 0, // must have a target + RequiresLineOfSight = 1u << 1, // LoS check on target + IsHostileEffect = 1u << 2, // hostile only (PvP gating) + IsBeneficialEffect = 1u << 3, // friendly only + IgnoresImmunities = 1u << 4, // bypasses Bubble / IBF / etc + TriggersGCD = 1u << 5, // counts toward GCD + }; + + struct Entry { + uint32_t effectId = 0; + std::string name; + std::string description; + uint8_t effectKind = Damage; + uint8_t behaviorFlags = 0; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + int32_t baseAmount = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t effectId) const; + + static const char* effectKindName(uint8_t k); +}; + +class WoweeSpellEffectTypeLoader { +public: + static bool save(const WoweeSpellEffectType& cat, + const std::string& basePath); + static WoweeSpellEffectType load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-sef* variants. + // + // makeDamage — 5 damage effect entries (SchoolDamage, + // EnvironmentalDamage, WeaponDamageNoSchool, + // NormalizedWeaponDmg, PowerBurn) covering + // the standard damage-effect IDs from + // Spell.dbc. + // makeHealing — 4 healing effects (Heal, HealMaxHealth, + // HealPct, ScriptedHeal) — all flagged + // IsBeneficialEffect. + // makeAura — 5 aura-application effects + // (ApplyAura, ApplyAuraOnPet, + // AreaAuraParty, AreaAuraOwner, + // PersistentAreaAura). + static WoweeSpellEffectType makeDamage(const std::string& catalogName); + static WoweeSpellEffectType makeHealing(const std::string& catalogName); + static WoweeSpellEffectType makeAura(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_effect_types.cpp b/src/pipeline/wowee_spell_effect_types.cpp new file mode 100644 index 00000000..360c3ee7 --- /dev/null +++ b/src/pipeline/wowee_spell_effect_types.cpp @@ -0,0 +1,246 @@ +#include "pipeline/wowee_spell_effect_types.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'E', 'F'}; +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) != ".wsef") { + base += ".wsef"; + } + 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 WoweeSpellEffectType::Entry* +WoweeSpellEffectType::findById(uint32_t effectId) const { + for (const auto& e : entries) + if (e.effectId == effectId) return &e; + return nullptr; +} + +const char* WoweeSpellEffectType::effectKindName(uint8_t k) { + switch (k) { + case Damage: return "damage"; + case Heal: return "heal"; + case Aura: return "aura"; + case Energize: return "energize"; + case Trigger: return "trigger"; + case Movement: return "movement"; + case Summon: return "summon"; + case Dispel: return "dispel"; + case Dummy: return "dummy"; + case Misc: return "misc"; + default: return "unknown"; + } +} + +bool WoweeSpellEffectTypeLoader::save(const WoweeSpellEffectType& 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.effectId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.effectKind); + writePOD(os, e.behaviorFlags); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.baseAmount); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeSpellEffectType WoweeSpellEffectTypeLoader::load( + const std::string& basePath) { + WoweeSpellEffectType 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.effectId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.effectKind) || + !readPOD(is, e.behaviorFlags) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.baseAmount) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellEffectTypeLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpellEffectType WoweeSpellEffectTypeLoader::makeDamage( + const std::string& catalogName) { + using S = WoweeSpellEffectType; + WoweeSpellEffectType c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t flags, int32_t base, const char* desc) { + S::Entry e; + e.effectId = id; e.name = name; e.description = desc; + e.effectKind = kind; + e.behaviorFlags = flags; + e.baseAmount = base; + e.iconColorRGBA = packRgba(220, 80, 80); // damage red + c.entries.push_back(e); + }; + // Standard WotLK damage effect IDs from Spell.dbc. + add(2, "SchoolDamage", S::Damage, + S::RequiresTarget | S::RequiresLineOfSight | + S::IsHostileEffect | S::TriggersGCD, + 0, "School-typed magical damage (Fire / Frost / etc)."); + add(13, "EnvironmentalDamage", S::Damage, + S::IsHostileEffect, 0, + "Environmental damage (lava / falling / drowning)."); + add(58, "WeaponDamageNoSchool", S::Damage, + S::RequiresTarget | S::IsHostileEffect | S::TriggersGCD, + 0, "Weapon damage with no spell school override."); + add(121, "NormalizedWeaponDmg", S::Damage, + S::RequiresTarget | S::IsHostileEffect | S::TriggersGCD, + 0, "Weapon damage normalized to weapon speed."); + add(67, "PowerBurn", S::Damage, + S::RequiresTarget | S::IsHostileEffect | S::TriggersGCD, + 0, "Burns power resource and deals damage equal to " + "the burned amount (Mana Burn)."); + return c; +} + +WoweeSpellEffectType WoweeSpellEffectTypeLoader::makeHealing( + const std::string& catalogName) { + using S = WoweeSpellEffectType; + WoweeSpellEffectType c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t flags, + int32_t base, const char* desc) { + S::Entry e; + e.effectId = id; e.name = name; e.description = desc; + e.effectKind = S::Heal; + e.behaviorFlags = flags; + e.baseAmount = base; + e.iconColorRGBA = packRgba(80, 240, 80); // healing green + c.entries.push_back(e); + }; + add(10, "Heal", S::RequiresTarget | S::IsBeneficialEffect | + S::TriggersGCD, 0, + "Restore health to target by baseAmount."); + add(46, "HealMaxHealth", S::RequiresTarget | S::IsBeneficialEffect, + 0, "Restore target to full health (Lay on Hands)."); + add(116, "HealPct", S::RequiresTarget | S::IsBeneficialEffect | + S::TriggersGCD, 25, + "Restore health as a percentage of target's max HP."); + add(118, "ScriptedHeal", S::RequiresTarget | S::IsBeneficialEffect, + 0, "Custom heal effect, server-script implements the " + "actual restoration formula."); + return c; +} + +WoweeSpellEffectType WoweeSpellEffectTypeLoader::makeAura( + const std::string& catalogName) { + using S = WoweeSpellEffectType; + WoweeSpellEffectType c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t flags, + const char* desc) { + S::Entry e; + e.effectId = id; e.name = name; e.description = desc; + e.effectKind = S::Aura; + e.behaviorFlags = flags; + e.iconColorRGBA = packRgba(180, 100, 240); // aura purple + c.entries.push_back(e); + }; + add(6, "ApplyAura", S::RequiresTarget | S::TriggersGCD, + "Apply a buff/debuff to target — auraType field " + "selects the aura behavior (see WAUR catalog)."); + add(35, "ApplyAuraOnPet", S::RequiresTarget, + "Apply aura to caster's pet (Hunter Mend Pet, " + "Warlock Demonic Empowerment)."); + add(65, "AreaAuraParty", S::IsBeneficialEffect, + "Area aura that affects all party members within " + "range (Power Word: Fortitude raid buffs)."); + add(82, "AreaAuraOwner", S::IsBeneficialEffect, + "Area aura tied to a totem/object that affects its " + "owner (Mana Spring totem)."); + add(27, "PersistentAreaAura", 0, + "Ground-targeted persistent area aura (Consecration, " + "Blizzard, Death and Decay)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 4da5cb6f..e48327a7 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -251,6 +251,8 @@ const char* const kArgRequired[] = { "--gen-acr", "--gen-acr-quest", "--gen-acr-mixed", "--info-wacr", "--validate-wacr", "--export-wacr-json", "--import-wacr-json", + "--gen-sef", "--gen-sef-healing", "--gen-sef-aura", + "--info-wsef", "--validate-wsef", "--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 62a1164b..cf9b0769 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -123,6 +123,7 @@ #include "cli_currency_types_catalog.hpp" #include "cli_spell_reagents_catalog.hpp" #include "cli_achievement_criteria_catalog.hpp" +#include "cli_spell_effect_types_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -287,6 +288,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCurrencyTypesCatalog, handleSpellReagentsCatalog, handleAchievementCriteriaCatalog, + handleSpellEffectTypesCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index a51c9d9e..92a40a46 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -84,6 +84,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','C','T','R'}, ".wctr", "currency", "--info-wctr", "Currency type catalog"}, {{'W','S','P','R'}, ".wspr", "spells", "--info-wspr", "Spell reagent set catalog"}, {{'W','A','C','R'}, ".wacr", "achieve", "--info-wacr", "Achievement criteria catalog"}, + {{'W','S','E','F'}, ".wsef", "spells", "--info-wsef", "Spell effect type 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 5b8aa69d..3649ba40 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1895,6 +1895,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wacr to a human-editable JSON sidecar (defaults to .wacr.json)\n"); std::printf(" --import-wacr-json [out-base]\n"); std::printf(" Import a .wacr.json sidecar back into binary .wacr (accepts criteriaType int 0..12 OR criteriaTypeName string)\n"); + std::printf(" --gen-sef [name]\n"); + std::printf(" Emit .wsef 5 damage effect entries (SchoolDamage / EnvironmentalDamage / WeaponDamageNoSchool / NormalizedWeaponDmg / PowerBurn)\n"); + std::printf(" --gen-sef-healing [name]\n"); + std::printf(" Emit .wsef 4 healing effects (Heal / HealMaxHealth / HealPct / ScriptedHeal) all flagged IsBeneficialEffect\n"); + std::printf(" --gen-sef-aura [name]\n"); + std::printf(" Emit .wsef 5 aura-application effects (ApplyAura / ApplyAuraOnPet / AreaAuraParty / AreaAuraOwner / PersistentAreaAura)\n"); + std::printf(" --info-wsef [--json]\n"); + std::printf(" Print WSEF entries (id / kind / baseAmount / behavior flags / name) — flags decoded as label list\n"); + std::printf(" --validate-wsef [--json]\n"); + std::printf(" Static checks: name required, effectKind 0..9, no duplicate ids; warns on Hostile+Beneficial conflict, Damage without TriggersGCD, Heal without IsBeneficialEffect\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 36de9a27..4867a032 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -106,6 +106,7 @@ constexpr FormatRow kFormats[] = { {"WCTR", ".wctr", "currency", "CurrencyTypes.dbc + caps", "Currency type catalog"}, {"WSPR", ".wspr", "spells", "Spell.dbc Reagent[8]+Count[8]", "Spell reagent set catalog"}, {"WACR", ".wacr", "achieve", "Achievement_Criteria.dbc", "Achievement criteria catalog"}, + {"WSEF", ".wsef", "spells", "SpellEffect.Effect dispatch", "Spell effect type catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_effect_types_catalog.cpp b/tools/editor/cli_spell_effect_types_catalog.cpp new file mode 100644 index 00000000..c8dfd6b9 --- /dev/null +++ b/tools/editor/cli_spell_effect_types_catalog.cpp @@ -0,0 +1,269 @@ +#include "cli_spell_effect_types_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_effect_types.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWsefExt(std::string base) { + stripExt(base, ".wsef"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpellEffectType& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellEffectTypeLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wsef\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellEffectType& c, + const std::string& base) { + std::printf("Wrote %s.wsef\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" effects : %zu\n", c.entries.size()); +} + +int handleGenDamage(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "DamageEffects"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsefExt(base); + auto c = wowee::pipeline::WoweeSpellEffectTypeLoader::makeDamage(name); + if (!saveOrError(c, base, "gen-sef")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenHealing(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "HealingEffects"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsefExt(base); + auto c = wowee::pipeline::WoweeSpellEffectTypeLoader::makeHealing(name); + if (!saveOrError(c, base, "gen-sef-healing")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAura(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AuraEffects"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsefExt(base); + auto c = wowee::pipeline::WoweeSpellEffectTypeLoader::makeAura(name); + if (!saveOrError(c, base, "gen-sef-aura")) return 1; + printGenSummary(c, base); + return 0; +} + +void appendBehaviorFlagNames(uint8_t flags, std::string& out) { + using F = wowee::pipeline::WoweeSpellEffectType; + auto add = [&](const char* n) { + if (!out.empty()) out += "|"; + out += n; + }; + if (flags & F::RequiresTarget) add("RequiresTarget"); + if (flags & F::RequiresLineOfSight) add("RequiresLineOfSight"); + if (flags & F::IsHostileEffect) add("IsHostileEffect"); + if (flags & F::IsBeneficialEffect) add("IsBeneficialEffect"); + if (flags & F::IgnoresImmunities) add("IgnoresImmunities"); + if (flags & F::TriggersGCD) add("TriggersGCD"); + if (out.empty()) out = "-"; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWsefExt(base); + if (!wowee::pipeline::WoweeSpellEffectTypeLoader::exists(base)) { + std::fprintf(stderr, "WSEF not found: %s.wsef\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellEffectTypeLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wsef"] = base + ".wsef"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string flagNames; + appendBehaviorFlagNames(e.behaviorFlags, flagNames); + arr.push_back({ + {"effectId", e.effectId}, + {"name", e.name}, + {"description", e.description}, + {"effectKind", e.effectKind}, + {"effectKindName", wowee::pipeline::WoweeSpellEffectType::effectKindName(e.effectKind)}, + {"behaviorFlags", e.behaviorFlags}, + {"behaviorFlagsLabels", flagNames}, + {"baseAmount", e.baseAmount}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSEF: %s.wsef\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" effects : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind baseAmt flags name\n"); + for (const auto& e : c.entries) { + std::string flagNames; + appendBehaviorFlagNames(e.behaviorFlags, flagNames); + std::printf(" %4u %-9s %7d %-50s %s\n", + e.effectId, + wowee::pipeline::WoweeSpellEffectType::effectKindName(e.effectKind), + e.baseAmount, + flagNames.c_str(), + 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 = stripWsefExt(base); + if (!wowee::pipeline::WoweeSpellEffectTypeLoader::exists(base)) { + std::fprintf(stderr, + "validate-wsef: WSEF not found: %s.wsef\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellEffectTypeLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + constexpr uint8_t kKnownFlagMask = + wowee::pipeline::WoweeSpellEffectType::RequiresTarget | + wowee::pipeline::WoweeSpellEffectType::RequiresLineOfSight | + wowee::pipeline::WoweeSpellEffectType::IsHostileEffect | + wowee::pipeline::WoweeSpellEffectType::IsBeneficialEffect | + wowee::pipeline::WoweeSpellEffectType::IgnoresImmunities | + wowee::pipeline::WoweeSpellEffectType::TriggersGCD; + 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.effectId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.effectKind > wowee::pipeline::WoweeSpellEffectType::Misc) { + errors.push_back(ctx + ": effectKind " + + std::to_string(e.effectKind) + " not in 0..9"); + } + if (e.behaviorFlags & ~kKnownFlagMask) { + warnings.push_back(ctx + + ": behaviorFlags has bits outside known mask " + + "(0x" + std::to_string(e.behaviorFlags & ~kKnownFlagMask) + + ") — engine will ignore unknown flags"); + } + // Both Hostile and Beneficial set is contradictory. + if ((e.behaviorFlags & wowee::pipeline::WoweeSpellEffectType::IsHostileEffect) && + (e.behaviorFlags & wowee::pipeline::WoweeSpellEffectType::IsBeneficialEffect)) { + warnings.push_back(ctx + + ": both IsHostileEffect and IsBeneficialEffect " + "flags set — engine treats this as Hostile (flag " + "wins) but the contradiction suggests a config bug"); + } + // Damage kind without TriggersGCD is unusual. + if (e.effectKind == wowee::pipeline::WoweeSpellEffectType::Damage && + !(e.behaviorFlags & wowee::pipeline::WoweeSpellEffectType::TriggersGCD) && + e.effectId != 13) { // EnvironmentalDamage doesn't trigger GCD + warnings.push_back(ctx + + ": Damage kind without TriggersGCD — most damage " + "effects should be on the GCD; double-check this " + "is intentional"); + } + // Heal kind without IsBeneficialEffect is suspicious. + if (e.effectKind == wowee::pipeline::WoweeSpellEffectType::Heal && + !(e.behaviorFlags & wowee::pipeline::WoweeSpellEffectType::IsBeneficialEffect)) { + warnings.push_back(ctx + + ": Heal kind without IsBeneficialEffect — " + "engine treats heals as ungated, may damage enemies"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.effectId) { + errors.push_back(ctx + ": duplicate effectId"); + break; + } + } + idsSeen.push_back(e.effectId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wsef"] = base + ".wsef"; + 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-wsef: %s.wsef\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu effects, all effectIds 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 handleSpellEffectTypesCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-sef") == 0 && i + 1 < argc) { + outRc = handleGenDamage(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-sef-healing") == 0 && i + 1 < argc) { + outRc = handleGenHealing(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-sef-aura") == 0 && i + 1 < argc) { + outRc = handleGenAura(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wsef") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wsef") == 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_effect_types_catalog.hpp b/tools/editor/cli_spell_effect_types_catalog.hpp new file mode 100644 index 00000000..d327277e --- /dev/null +++ b/tools/editor/cli_spell_effect_types_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellEffectTypesCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee