diff --git a/CMakeLists.txt b/CMakeLists.txt index c558cf7f..d2f57e8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -692,6 +692,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_emotes.cpp src/pipeline/wowee_buff_book.cpp src/pipeline/wowee_tabards.cpp + src/pipeline/wowee_spell_markers.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1547,6 +1548,7 @@ add_executable(wowee_editor tools/editor/cli_emotes_catalog.cpp tools/editor/cli_buff_book_catalog.cpp tools/editor/cli_tabards_catalog.cpp + tools/editor/cli_spell_markers_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_quest_objective.cpp @@ -1719,6 +1721,7 @@ add_executable(wowee_editor src/pipeline/wowee_emotes.cpp src/pipeline/wowee_buff_book.cpp src/pipeline/wowee_tabards.cpp + src/pipeline/wowee_spell_markers.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_markers.hpp b/include/pipeline/wowee_spell_markers.hpp new file mode 100644 index 00000000..ed7d9a42 --- /dev/null +++ b/include/pipeline/wowee_spell_markers.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Persistent Marker catalog (.wspm) — +// novel replacement for the SpellAreaTrigger.dbc + +// AreaTriggerCreateProperties combination that vanilla +// WoW used for AoE ground decals (Blizzard, Hurricane, +// Consecration, Death and Decay, Rain of Fire, Flame +// Strike), boss-arena hazard zones (Putricide poison +// pools, Sindragosa frost tombs), and environmental +// effects (Wintergrasp lightning storm strike radius, +// Silithus sandstorm cones). +// +// Each entry binds one spellId to a ground decal: a +// texture path, radius (in world units), duration, +// damage tick interval, edge-fade rendering mode, and +// stack/destroy semantics. The visual effects pipeline +// reads this catalog at spell cast time to spawn the +// right ground-tracked decal. +// +// Cross-references with previously-added formats: +// WSPL: spellId references the WSPL spell catalog +// (the spell whose cast creates this marker). +// WSND: tickSoundId references the WSND sound catalog +// (the per-tick audio cue, e.g. crackling fire). +// +// Binary layout (little-endian): +// magic[4] = "WSPM" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// markerId (uint32) +// nameLen + name +// descLen + description +// spellId (uint32) +// pathLen + groundTexturePath — BLP/WOT path +// radius (float) — world units +// duration (float) — seconds (0 = until +// caster cancels / +// mana drains) +// tickIntervalMs (uint32) — ms between damage +// ticks +// decalColor (uint32) — RGBA tint applied to +// the texture +// edgeFadeMode (uint8) — Hard / SoftEdge / +// Pulse +// stackable (uint8) — 0/1 bool +// destroyOnCancel (uint8) — 0/1: vanish when the +// caster cancels the +// channel +// pad0 (uint8) +// tickSoundId (uint32) — 0 if silent +// iconColorRGBA (uint32) +struct WoweeSpellMarkers { + enum EdgeFadeMode : uint8_t { + Hard = 0, // sharp circle edge — no fade + SoftEdge = 1, // alpha-fade outer 20% of radius + Pulse = 2, // sinusoidal alpha pulse, full + // radius + }; + + struct Entry { + uint32_t markerId = 0; + std::string name; + std::string description; + uint32_t spellId = 0; + std::string groundTexturePath; + float radius = 8.0f; + float duration = 8.0f; + uint32_t tickIntervalMs = 1000; + uint32_t decalColor = 0xFFFFFFFFu; + uint8_t edgeFadeMode = SoftEdge; + uint8_t stackable = 0; + uint8_t destroyOnCancel = 1; + uint8_t pad0 = 0; + uint32_t tickSoundId = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t markerId) const; + + // Returns the marker (if any) bound to the given + // spellId. Used by the spell-cast pipeline to look + // up "which decal does this spell spawn?" + const Entry* findBySpell(uint32_t spellId) const; +}; + +class WoweeSpellMarkersLoader { +public: + static bool save(const WoweeSpellMarkers& cat, + const std::string& basePath); + static WoweeSpellMarkers load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-spm* variants. + // + // makeMageAoE — 4 mage AoE ground spells + // (Blizzard / Flamestrike / + // BlastWave / Frost Nova + // visual ring). + // makeRaidHazards — 5 boss-arena hazard zones + // (Putricide poison pool / + // Sindragosa frost tomb / + // Saurfang blood-frenzy bonus / + // DBS shadow puddle / Marrowgar + // Bone Storm radius). + // makeEnvironment — 3 environmental effects + // (Wintergrasp lightning strike + // radius / Silithus sandstorm + // cone / open-world blizzard + // zone). + static WoweeSpellMarkers makeMageAoE(const std::string& catalogName); + static WoweeSpellMarkers makeRaidHazards(const std::string& catalogName); + static WoweeSpellMarkers makeEnvironment(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_markers.cpp b/src/pipeline/wowee_spell_markers.cpp new file mode 100644 index 00000000..dd63f581 --- /dev/null +++ b/src/pipeline/wowee_spell_markers.cpp @@ -0,0 +1,324 @@ +#include "pipeline/wowee_spell_markers.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'P', 'M'}; +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) != ".wspm") { + base += ".wspm"; + } + 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 WoweeSpellMarkers::Entry* +WoweeSpellMarkers::findById(uint32_t markerId) const { + for (const auto& e : entries) + if (e.markerId == markerId) return &e; + return nullptr; +} + +const WoweeSpellMarkers::Entry* +WoweeSpellMarkers::findBySpell(uint32_t spellId) const { + for (const auto& e : entries) + if (e.spellId == spellId) return &e; + return nullptr; +} + +bool WoweeSpellMarkersLoader::save(const WoweeSpellMarkers& 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.markerId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.spellId); + writeStr(os, e.groundTexturePath); + writePOD(os, e.radius); + writePOD(os, e.duration); + writePOD(os, e.tickIntervalMs); + writePOD(os, e.decalColor); + writePOD(os, e.edgeFadeMode); + writePOD(os, e.stackable); + writePOD(os, e.destroyOnCancel); + writePOD(os, e.pad0); + writePOD(os, e.tickSoundId); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeSpellMarkers WoweeSpellMarkersLoader::load( + const std::string& basePath) { + WoweeSpellMarkers 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.markerId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.spellId) || + !readStr(is, e.groundTexturePath) || + !readPOD(is, e.radius) || + !readPOD(is, e.duration) || + !readPOD(is, e.tickIntervalMs) || + !readPOD(is, e.decalColor) || + !readPOD(is, e.edgeFadeMode) || + !readPOD(is, e.stackable) || + !readPOD(is, e.destroyOnCancel) || + !readPOD(is, e.pad0) || + !readPOD(is, e.tickSoundId) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellMarkersLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpellMarkers WoweeSpellMarkersLoader::makeMageAoE( + const std::string& catalogName) { + using S = WoweeSpellMarkers; + WoweeSpellMarkers c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t spellId, const char* texPath, + float radius, float duration, + uint32_t tickMs, uint32_t color, + uint8_t fade, uint32_t soundId, + const char* desc) { + S::Entry e; + e.markerId = id; e.name = name; e.description = desc; + e.spellId = spellId; + e.groundTexturePath = texPath; + e.radius = radius; + e.duration = duration; + e.tickIntervalMs = tickMs; + e.decalColor = color; + e.edgeFadeMode = fade; + e.stackable = 0; + e.destroyOnCancel = 1; + e.tickSoundId = soundId; + e.iconColorRGBA = packRgba(140, 200, 255); // mage blue + c.entries.push_back(e); + }; + add(1, "Blizzard", 10, "Spell\\Blizzard\\BlizzardGround.blp", + 12.0f, 8.0f, 1000, packRgba(180, 220, 255, 200), + S::SoftEdge, 11075, + "Mage Blizzard ground decal — 12yd radius, 8s " + "channel, 1s ticks. Soft-edge fade, blue tint, " + "wind-howl tick sound."); + add(2, "Flamestrike", 11, "Spell\\Flamestrike\\FlamestrikeRing.blp", + 8.0f, 8.0f, 1000, packRgba(220, 100, 30, 220), + S::Pulse, 6580, + "Mage Flamestrike ground decal — 8yd radius, 8s " + "lingering DoT after initial impact. Pulse fade " + "to suggest ongoing flames."); + add(3, "BlastWaveRing", 13, "Spell\\BlastWave\\ImpactRing.blp", + 10.0f, 0.5f, 0, packRgba(255, 180, 80, 255), + S::Hard, 0, + "Mage Blast Wave impact ring — 10yd radius, 0.5s " + "burst, no ticks (single-instance damage). Hard " + "edge for sharp shockwave."); + add(4, "FrostNovaRing", 14, "Spell\\FrostNova\\FreezeRing.blp", + 10.0f, 1.0f, 0, packRgba(180, 220, 255, 180), + S::SoftEdge, 0, + "Mage Frost Nova freeze indicator — 10yd radius, " + "1s visible after cast (the actual root lasts 8s " + "via the spell aura, the marker is just the visual " + "cue)."); + return c; +} + +WoweeSpellMarkers WoweeSpellMarkersLoader::makeRaidHazards( + const std::string& catalogName) { + using S = WoweeSpellMarkers; + WoweeSpellMarkers c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t spellId, const char* texPath, + float radius, float duration, + uint32_t tickMs, uint32_t color, + uint8_t fade, const char* desc) { + S::Entry e; + e.markerId = id; e.name = name; e.description = desc; + e.spellId = spellId; + e.groundTexturePath = texPath; + e.radius = radius; + e.duration = duration; + e.tickIntervalMs = tickMs; + e.decalColor = color; + e.edgeFadeMode = fade; + e.stackable = 0; + e.destroyOnCancel = 0; // hazards persist + // until duration expires + e.tickSoundId = 0; + e.iconColorRGBA = packRgba(220, 80, 80); // raid red + c.entries.push_back(e); + }; + add(100, "PutricidePoisonPool", 70341, + "Spell\\Poison\\PoisonPool.blp", + 4.0f, 30.0f, 500, packRgba(80, 220, 60, 200), + S::Pulse, + "Putricide poison-pool ground decal — 4yd " + "radius, 30s, 500ms damage ticks. Pulse fade. " + "Standing in this is a wipe risk."); + add(101, "SindragosaFrostTomb", 70106, + "Spell\\Frost\\IceBlock.blp", + 2.5f, 12.0f, 0, packRgba(180, 220, 255, 220), + S::Hard, + "Sindragosa frost-tomb circle — 2.5yd radius, " + "12s, no ticks (damage is from the ice block aura, " + "not the marker). Hard edge for clear stay-out " + "boundary."); + add(102, "SaurfangBloodFrenzy", 72444, + "Spell\\Blood\\BloodFrenzy.blp", + 15.0f, 60.0f, 1000, packRgba(220, 60, 60, 180), + S::SoftEdge, + "Deathbringer Saurfang Mark of the Fallen " + "Champion zone — 15yd radius, 60s, 1s healing " + "ticks for Saurfang. DPS this player or wipe."); + add(103, "ProfessorPutricideShadow", 70447, + "Spell\\Shadow\\ShadowPuddle.blp", + 5.0f, 25.0f, 500, packRgba(60, 30, 100, 200), + S::Pulse, + "DBS / Putricide phase 2 shadow puddle — 5yd " + "radius, 25s, 500ms shadow ticks."); + add(104, "MarrowgarBoneStorm", 70233, + "Spell\\Bone\\BoneStorm.blp", + 18.0f, 20.0f, 1000, packRgba(220, 220, 200, 180), + S::Pulse, + "Lord Marrowgar Bone Storm radius — 18yd, 20s, " + "1s ticks. Marker tracks Marrowgar himself; " + "stays during the channel, vanishes when he " + "stops."); + return c; +} + +WoweeSpellMarkers WoweeSpellMarkersLoader::makeEnvironment( + const std::string& catalogName) { + using S = WoweeSpellMarkers; + WoweeSpellMarkers c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t spellId, const char* texPath, + float radius, float duration, + uint32_t tickMs, uint32_t color, + uint8_t fade, const char* desc) { + S::Entry e; + e.markerId = id; e.name = name; e.description = desc; + e.spellId = spellId; + e.groundTexturePath = texPath; + e.radius = radius; + e.duration = duration; + e.tickIntervalMs = tickMs; + e.decalColor = color; + e.edgeFadeMode = fade; + e.stackable = 1; // environmental effects + // can stack (multiple + // lightning strikes) + e.destroyOnCancel = 0; // environment, no caster + // to cancel + e.tickSoundId = 0; + e.iconColorRGBA = packRgba(180, 180, 200); // env neutral + c.entries.push_back(e); + }; + add(200, "WintergraspLightning", 60014, + "Spell\\Weather\\LightningStrike.blp", + 6.0f, 1.0f, 0, packRgba(220, 220, 100, 240), + S::Hard, + "Wintergrasp open-world lightning strike — 6yd " + "blast radius, 1s flash. Hard edge for clear " + "danger zone."); + add(201, "SilithusSandstormCone", 38567, + "Spell\\Weather\\SandstormCone.blp", + 25.0f, 0.0f, 2000, packRgba(220, 200, 140, 100), + S::SoftEdge, + "Silithus environmental sandstorm cone — 25yd " + "radius, no time limit (until weather changes), " + "2s movement-slow ticks."); + add(202, "OpenWorldBlizzardZone", 30000, + "Spell\\Weather\\BlizzardZone.blp", + 40.0f, 0.0f, 5000, packRgba(220, 220, 240, 80), + S::SoftEdge, + "Northrend open-world blizzard zone — 40yd " + "radius, no time limit, 5s minor frost ticks. " + "Pure visual + ambient gameplay (no real damage " + "in Wrath; placeholder for storm system)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index a3fcf86a..d65129b5 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -319,6 +319,8 @@ const char* const kArgRequired[] = { "--gen-tbd", "--gen-tbd-horde", "--gen-tbd-faction", "--info-wtbd", "--validate-wtbd", "--export-wtbd-json", "--import-wtbd-json", + "--gen-spm", "--gen-spm-raid", "--gen-spm-env", + "--info-wspm", "--validate-wspm", "--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 b4178e3d..a02e7c46 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -148,6 +148,7 @@ #include "cli_emotes_catalog.hpp" #include "cli_buff_book_catalog.hpp" #include "cli_tabards_catalog.hpp" +#include "cli_spell_markers_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_quest_objective.hpp" @@ -339,6 +340,7 @@ constexpr DispatchFn kDispatchTable[] = { handleEmotesCatalog, handleBuffBookCatalog, handleTabardsCatalog, + handleSpellMarkersCatalog, handleCatalogPluck, handleCatalogFind, handleQuestObjective, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index ec471930..199fb147 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -106,6 +106,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','E','M','O'}, ".wemo", "social", "--info-wemo", "Emote definition catalog"}, {{'W','B','A','B'}, ".wbab", "spells", "--info-wbab", "Buff & Aura book (rank chains)"}, {{'W','T','B','D'}, ".wtbd", "guilds", "--info-wtbd", "Tabard design / heraldry catalog"}, + {{'W','S','P','M'}, ".wspm", "spellfx", "--info-wspm", "Spell persistent marker 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 0c676d7b..4eafdfbf 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2209,6 +2209,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wtbd to a human-editable JSON sidecar (defaults to .wtbd.json; emits both backgroundPattern/borderPattern ints AND name strings; all 3 colors as 0xAARRGGBB uint32)\n"); std::printf(" --import-wtbd-json [out-base]\n"); std::printf(" Import a .wtbd.json sidecar back into binary .wtbd (backgroundPattern int OR \"solid\"/\"gradient\"/\"chevron\"/\"quartered\"/\"starburst\"; borderPattern int OR \"none\"/\"thin\"/\"thick\"/\"decorative\"; isApproved bool OR int)\n"); + std::printf(" --gen-spm [name]\n"); + std::printf(" Emit .wspm 4 mage AoE ground markers (Blizzard / Flamestrike / BlastWave ring / Frost Nova ring)\n"); + std::printf(" --gen-spm-raid [name]\n"); + std::printf(" Emit .wspm 5 boss-arena hazard zones (Putricide poison pool / Sindragosa frost tomb / Saurfang Mark zone / Putricide shadow puddle / Marrowgar Bone Storm)\n"); + std::printf(" --gen-spm-env [name]\n"); + std::printf(" Emit .wspm 3 environmental ground effects (Wintergrasp lightning / Silithus sandstorm cone / open-world blizzard zone)\n"); + std::printf(" --info-wspm [--json]\n"); + std::printf(" Print WSPM entries (id / spell / radius / duration / tick interval / fade mode / stack-flag / destroy-on-cancel / name)\n"); + std::printf(" --validate-wspm [--json]\n"); + std::printf(" Static checks: id+name+spellId+texturePath required, radius>0, edgeFadeMode 0..2, no duplicate markerIds, no duplicate spellIds (spell-cast lookup ambiguity); warns on radius>100yd, tickIntervalMs<100ms (perf risk for stackable), decalColor alpha=0 (invisible)\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 b6c42587..a7b1a034 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -128,6 +128,7 @@ constexpr FormatRow kFormats[] = { {"WEMO", ".wemo", "social", "EmotesText.dbc + EmotesTextSound", "Emote definition catalog (/dance, /wave, etc.)"}, {"WBAB", ".wbab", "spells", "Spell.dbc nextRank/prevRank ptrs", "Buff & Aura book — long-duration class buffs with rank chains"}, {"WTBD", ".wtbd", "guilds", "guild_member tabard config blob", "Tabard design / heraldry catalog (3-color)"}, + {"WSPM", ".wspm", "spellfx", "AreaTrigger.dbc + decal blob", "Spell persistent marker catalog (AoE ground decals)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_markers_catalog.cpp b/tools/editor/cli_spell_markers_catalog.cpp new file mode 100644 index 00000000..2e231b3d --- /dev/null +++ b/tools/editor/cli_spell_markers_catalog.cpp @@ -0,0 +1,280 @@ +#include "cli_spell_markers_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_markers.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWspmExt(std::string base) { + stripExt(base, ".wspm"); + return base; +} + +const char* edgeFadeModeName(uint8_t m) { + using S = wowee::pipeline::WoweeSpellMarkers; + switch (m) { + case S::Hard: return "hard"; + case S::SoftEdge: return "softedge"; + case S::Pulse: return "pulse"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweeSpellMarkers& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellMarkersLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wspm\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellMarkers& c, + const std::string& base) { + std::printf("Wrote %s.wspm\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" markers : %zu\n", c.entries.size()); +} + +int handleGenMage(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MageAoEMarkers"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspmExt(base); + auto c = wowee::pipeline::WoweeSpellMarkersLoader::makeMageAoE(name); + if (!saveOrError(c, base, "gen-spm")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRaid(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RaidHazardMarkers"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspmExt(base); + auto c = wowee::pipeline::WoweeSpellMarkersLoader::makeRaidHazards(name); + if (!saveOrError(c, base, "gen-spm-raid")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenEnvironment(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "EnvironmentMarkers"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspmExt(base); + auto c = wowee::pipeline::WoweeSpellMarkersLoader::makeEnvironment(name); + if (!saveOrError(c, base, "gen-spm-env")) 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 = stripWspmExt(base); + if (!wowee::pipeline::WoweeSpellMarkersLoader::exists(base)) { + std::fprintf(stderr, "WSPM not found: %s.wspm\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellMarkersLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wspm"] = base + ".wspm"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"markerId", e.markerId}, + {"name", e.name}, + {"description", e.description}, + {"spellId", e.spellId}, + {"groundTexturePath", e.groundTexturePath}, + {"radius", e.radius}, + {"duration", e.duration}, + {"tickIntervalMs", e.tickIntervalMs}, + {"decalColor", e.decalColor}, + {"edgeFadeMode", e.edgeFadeMode}, + {"edgeFadeModeName", + edgeFadeModeName(e.edgeFadeMode)}, + {"stackable", e.stackable != 0}, + {"destroyOnCancel", e.destroyOnCancel != 0}, + {"tickSoundId", e.tickSoundId}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSPM: %s.wspm\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" markers : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id spell radius dur(s) tick(ms) fade stack dest name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %5.1f %5.1f %5u %-9s %s %s %s\n", + e.markerId, e.spellId, e.radius, + e.duration, e.tickIntervalMs, + edgeFadeModeName(e.edgeFadeMode), + e.stackable ? "yes" : "no ", + e.destroyOnCancel ? "yes" : "no ", + 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 = stripWspmExt(base); + if (!wowee::pipeline::WoweeSpellMarkersLoader::exists(base)) { + std::fprintf(stderr, + "validate-wspm: WSPM not found: %s.wspm\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellMarkersLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + std::set spellIdsSeen; + 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.markerId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.markerId == 0) + errors.push_back(ctx + ": markerId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.spellId == 0) { + errors.push_back(ctx + + ": spellId is 0 — marker is not bound to " + "any spell"); + } + if (e.groundTexturePath.empty()) { + errors.push_back(ctx + + ": groundTexturePath is empty — decal " + "would render as untextured solid color"); + } + if (e.radius <= 0.0f) { + errors.push_back(ctx + ": radius " + + std::to_string(e.radius) + + " <= 0 — decal would have zero area"); + } + if (e.radius > 100.0f) { + warnings.push_back(ctx + ": radius " + + std::to_string(e.radius) + + " > 100 yards — covers more than the " + "average raid arena, verify if intentional"); + } + if (e.edgeFadeMode > 2) { + errors.push_back(ctx + ": edgeFadeMode " + + std::to_string(e.edgeFadeMode) + + " out of range (must be 0..2)"); + } + // tickIntervalMs sanity: zero is legal (one-shot + // burst), but very short intervals would tank + // performance with many simultaneous markers. + if (e.tickIntervalMs > 0 && e.tickIntervalMs < 100) { + warnings.push_back(ctx + ": tickIntervalMs " + + std::to_string(e.tickIntervalMs) + + " < 100ms — fires more than 10× per second; " + "verify performance impact for stackable " + "markers"); + } + // Decal alpha=0 = invisible — likely an error. + uint8_t alpha = (e.decalColor >> 24) & 0xFF; + if (alpha == 0) { + warnings.push_back(ctx + + ": decalColor has alpha=0 — marker would " + "render fully transparent / invisible"); + } + // Multiple markers binding the same spellId is + // ambiguous (which decal does the spell spawn?). + if (e.spellId != 0 && + !spellIdsSeen.insert(e.spellId).second) { + errors.push_back(ctx + + ": spellId " + std::to_string(e.spellId) + + " is already bound by another marker — " + "spell-cast lookup would be ambiguous"); + } + if (!idsSeen.insert(e.markerId).second) { + errors.push_back(ctx + ": duplicate markerId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wspm"] = base + ".wspm"; + 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-wspm: %s.wspm\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu markers, all markerIds + " + "spellIds 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 handleSpellMarkersCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-spm") == 0 && i + 1 < argc) { + outRc = handleGenMage(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spm-raid") == 0 && i + 1 < argc) { + outRc = handleGenRaid(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spm-env") == 0 && i + 1 < argc) { + outRc = handleGenEnvironment(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wspm") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wspm") == 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_markers_catalog.hpp b/tools/editor/cli_spell_markers_catalog.hpp new file mode 100644 index 00000000..8da3d72e --- /dev/null +++ b/tools/editor/cli_spell_markers_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellMarkersCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee