feat(pipeline): add WSVK (Wowee Spell Visual Kit) catalog

47th open format — replaces SpellVisualKit.dbc +
SpellVisualEffectName.dbc plus the AzerothCore-style spell
visual SQL data. Defines per-spell visual presentations:
cast-bar effect model, projectile model + travel speed +
arc gravity, impact effect model, hand effect on the caster,
and the animations + sounds that fire at cast / channel /
impact time.

Cross-references with prior formats — castAnimId / impactAnimId
/ precastAnimId point at WANI.animationId, castSoundId /
impactSoundId point at WSND.soundId. Spell catalogs (WSPL)
will reference visualKitId here to bind "what mechanically
happens" to "what plays visually."

CLI: --gen-svk (3-kit Frostbolt/Fireball/HealingTouch starter
showing projectile + AoE + heal patterns), --gen-svk-combat
(5 melee/ranged with WANI animation refs), --gen-svk-utility
(4 portal/hearth/mount/resurrect with no projectile),
--info-wsvk, --validate-wsvk with --json variants. Validator
catches id=0/duplicates, missing name, negative speeds/radii,
projectile-model + speed coherence (model without speed =
never travels; speed without model = invisible), and a
no-effect catch-all (no models + no anims + no sounds).
This commit is contained in:
Kelsi 2026-05-09 19:23:36 -07:00
parent 441b962c3f
commit beca69352a
10 changed files with 658 additions and 0 deletions

View file

@ -634,6 +634,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_holidays.cpp
src/pipeline/wowee_liquids.cpp
src/pipeline/wowee_animations.cpp
src/pipeline/wowee_spell_visuals.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1416,6 +1417,7 @@ add_executable(wowee_editor
tools/editor/cli_list_formats.cpp
tools/editor/cli_info_magic.cpp
tools/editor/cli_animations_catalog.cpp
tools/editor/cli_spell_visuals_catalog.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1528,6 +1530,7 @@ add_executable(wowee_editor
src/pipeline/wowee_holidays.cpp
src/pipeline/wowee_liquids.cpp
src/pipeline/wowee_animations.cpp
src/pipeline/wowee_spell_visuals.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,105 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Spell Visual Kit catalog (.wsvk) — novel
// replacement for Blizzard's SpellVisualKit.dbc +
// SpellVisualEffectName.dbc + the AzerothCore-style spell
// visual SQL data. The 47th open format added to the editor.
//
// Defines per-spell visual presentations: cast-bar effect
// model, projectile model + travel speed + arc gravity,
// impact effect model, hand effect on the caster, and the
// animations + sounds that fire at cast / channel / impact
// time. Spells reference a visualKitId here from Spell.dbc
// (or WSPL.spellId.visualKitId), so this catalog binds the
// "what happens visually" to the "what mechanically happens"
// in the spell catalog.
//
// Cross-references with previously-added formats:
// WSVK.entry.castAnimId → WANI.entry.animationId
// WSVK.entry.impactAnimId → WANI.entry.animationId
// WSVK.entry.precastAnimId → WANI.entry.animationId
// WSVK.entry.castSoundId → WSND.entry.soundId
// WSVK.entry.impactSoundId → WSND.entry.soundId
//
// Binary layout (little-endian):
// magic[4] = "WSVK"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// visualKitId (uint32)
// nameLen + name
// descLen + description
// castModelLen + castEffectModelPath
// projModelLen + projectileModelPath
// impactModelLen + impactEffectModelPath
// handModelLen + handEffectModelPath
// precastAnimId (uint32)
// castAnimId (uint32)
// impactAnimId (uint32)
// castSoundId (uint32)
// impactSoundId (uint32)
// projectileSpeed (float) — units/sec, 0=instant
// projectileGravity (float) — 0=straight line
// castDurationMs (uint32)
// impactRadius (float) — splash AoE in units
struct WoweeSpellVisualKit {
struct Entry {
uint32_t visualKitId = 0;
std::string name;
std::string description;
std::string castEffectModelPath; // M2 played during cast
std::string projectileModelPath; // M2 that flies to target
std::string impactEffectModelPath; // M2 played on impact
std::string handEffectModelPath; // M2 attached to caster hand
uint32_t precastAnimId = 0; // WANI cross-ref
uint32_t castAnimId = 0; // WANI cross-ref
uint32_t impactAnimId = 0; // WANI cross-ref
uint32_t castSoundId = 0; // WSND cross-ref
uint32_t impactSoundId = 0; // WSND cross-ref
float projectileSpeed = 0.0f; // 0 = instant hit
float projectileGravity = 0.0f; // 0 = straight line
uint32_t castDurationMs = 0;
float impactRadius = 0.0f; // 0 = single-target
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t visualKitId) const;
};
class WoweeSpellVisualKitLoader {
public:
static bool save(const WoweeSpellVisualKit& cat,
const std::string& basePath);
static WoweeSpellVisualKit load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-svk* variants.
//
// makeStarter — 3 visual kits (Frostbolt / Fireball /
// Healing) covering the canonical
// projectile + heal triad.
// makeCombat — 5 combat visuals (sword swing impact,
// arrow shot, ground pound, parry,
// deflect) with WANI animation refs.
// makeUtility — 4 utility visuals (portal/teleport,
// hearthstone return, mount summon,
// resurrection) with no projectile.
static WoweeSpellVisualKit makeStarter(const std::string& catalogName);
static WoweeSpellVisualKit makeCombat(const std::string& catalogName);
static WoweeSpellVisualKit makeUtility(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,270 @@
#include "pipeline/wowee_spell_visuals.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'S', 'V', 'K'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
void writeStr(std::ofstream& os, const std::string& s) {
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(n)) {
s.clear();
return false;
}
}
return true;
}
std::string normalizePath(std::string base) {
if (base.size() < 5 || base.substr(base.size() - 5) != ".wsvk") {
base += ".wsvk";
}
return base;
}
} // namespace
const WoweeSpellVisualKit::Entry*
WoweeSpellVisualKit::findById(uint32_t visualKitId) const {
for (const auto& e : entries)
if (e.visualKitId == visualKitId) return &e;
return nullptr;
}
bool WoweeSpellVisualKitLoader::save(const WoweeSpellVisualKit& 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<uint32_t>(cat.entries.size());
writePOD(os, entryCount);
for (const auto& e : cat.entries) {
writePOD(os, e.visualKitId);
writeStr(os, e.name);
writeStr(os, e.description);
writeStr(os, e.castEffectModelPath);
writeStr(os, e.projectileModelPath);
writeStr(os, e.impactEffectModelPath);
writeStr(os, e.handEffectModelPath);
writePOD(os, e.precastAnimId);
writePOD(os, e.castAnimId);
writePOD(os, e.impactAnimId);
writePOD(os, e.castSoundId);
writePOD(os, e.impactSoundId);
writePOD(os, e.projectileSpeed);
writePOD(os, e.projectileGravity);
writePOD(os, e.castDurationMs);
writePOD(os, e.impactRadius);
}
return os.good();
}
WoweeSpellVisualKit WoweeSpellVisualKitLoader::load(
const std::string& basePath) {
WoweeSpellVisualKit 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.visualKitId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description) ||
!readStr(is, e.castEffectModelPath) ||
!readStr(is, e.projectileModelPath) ||
!readStr(is, e.impactEffectModelPath) ||
!readStr(is, e.handEffectModelPath)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.precastAnimId) ||
!readPOD(is, e.castAnimId) ||
!readPOD(is, e.impactAnimId) ||
!readPOD(is, e.castSoundId) ||
!readPOD(is, e.impactSoundId) ||
!readPOD(is, e.projectileSpeed) ||
!readPOD(is, e.projectileGravity) ||
!readPOD(is, e.castDurationMs) ||
!readPOD(is, e.impactRadius)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeSpellVisualKitLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeSpellVisualKit WoweeSpellVisualKitLoader::makeStarter(
const std::string& catalogName) {
WoweeSpellVisualKit c;
c.name = catalogName;
{
WoweeSpellVisualKit::Entry e;
e.visualKitId = 1; e.name = "Frostbolt";
e.description = "Mage frostbolt — slow icy projectile.";
e.castEffectModelPath = "Spells/Cast/Frost/cast_frost.m2";
e.projectileModelPath = "Spells/Missiles/frostbolt.m2";
e.impactEffectModelPath = "Spells/Impact/Frost/impact_frost.m2";
e.handEffectModelPath = "Spells/Hand/frost_hand_glow.m2";
e.castAnimId = 54; // ChannelCast from WANI combat
e.castSoundId = 100; // WSND cross-ref
e.impactSoundId = 101;
e.projectileSpeed = 25.0f;
e.castDurationMs = 2500;
e.impactRadius = 0.0f; // single-target
c.entries.push_back(e);
}
{
WoweeSpellVisualKit::Entry e;
e.visualKitId = 2; e.name = "Fireball";
e.description = "Mage fireball — fast fiery projectile + AoE.";
e.castEffectModelPath = "Spells/Cast/Fire/cast_fire.m2";
e.projectileModelPath = "Spells/Missiles/fireball.m2";
e.impactEffectModelPath = "Spells/Impact/Fire/impact_fire.m2";
e.handEffectModelPath = "Spells/Hand/fire_hand_glow.m2";
e.castAnimId = 54;
e.castSoundId = 102;
e.impactSoundId = 103;
e.projectileSpeed = 35.0f;
e.castDurationMs = 3500;
e.impactRadius = 5.0f; // splash
c.entries.push_back(e);
}
{
WoweeSpellVisualKit::Entry e;
e.visualKitId = 3; e.name = "HealingTouch";
e.description = "Druid healing — golden glow on caster + target.";
e.castEffectModelPath = "Spells/Cast/Holy/cast_holy.m2";
e.impactEffectModelPath = "Spells/Impact/Holy/heal_glow.m2";
e.handEffectModelPath = "Spells/Hand/holy_hand_glow.m2";
e.castAnimId = 54;
e.castSoundId = 104;
e.impactSoundId = 105;
e.projectileSpeed = 0.0f; // instant — no projectile
e.castDurationMs = 3000;
c.entries.push_back(e);
}
return c;
}
WoweeSpellVisualKit WoweeSpellVisualKitLoader::makeCombat(
const std::string& catalogName) {
WoweeSpellVisualKit c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint32_t castAnim, uint32_t impactAnim,
float projSpeed, float gravity,
uint32_t durMs, float radius,
const char* castModel, const char* projModel,
const char* impactModel, const char* desc) {
WoweeSpellVisualKit::Entry e;
e.visualKitId = id; e.name = name; e.description = desc;
e.castEffectModelPath = castModel;
e.projectileModelPath = projModel;
e.impactEffectModelPath = impactModel;
e.castAnimId = castAnim;
e.impactAnimId = impactAnim;
e.projectileSpeed = projSpeed;
e.projectileGravity = gravity;
e.castDurationMs = durMs;
e.impactRadius = radius;
c.entries.push_back(e);
};
add(100, "SwordImpact", 17, 0, 0.0f, 0.0f, 0, 0.0f,
"", "", "Spells/Impact/Physical/sword_hit.m2",
"Sword strike sparks on impact — no cast effect.");
add(101, "ArrowShot", 40, 0, 60.0f, 0.05f, 800, 0.0f,
"Spells/Cast/Physical/draw_bow.m2",
"Spells/Missiles/arrow.m2",
"Spells/Impact/Physical/arrow_hit.m2",
"Arrow with slight gravity drop.");
add(102, "GroundPound", 18, 0, 0.0f, 0.0f, 1200, 8.0f,
"Spells/Cast/Physical/heave.m2", "",
"Spells/Impact/Physical/ground_shockwave.m2",
"AoE ground pound — large impact radius.");
add(103, "ParryFlash", 53, 0, 0.0f, 0.0f, 500, 0.0f,
"Spells/Cast/Physical/parry_flash.m2", "", "",
"Quick parry sparks — instant cast.");
add(104, "DeflectShield", 0, 0, 0.0f, 0.0f, 500, 0.0f,
"Spells/Cast/Physical/shield_deflect.m2", "", "",
"Shield reflection visual — no projectile.");
return c;
}
WoweeSpellVisualKit WoweeSpellVisualKitLoader::makeUtility(
const std::string& catalogName) {
WoweeSpellVisualKit c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint32_t durMs,
const char* castModel, const char* impactModel,
const char* desc) {
WoweeSpellVisualKit::Entry e;
e.visualKitId = id; e.name = name; e.description = desc;
e.castEffectModelPath = castModel;
e.impactEffectModelPath = impactModel;
e.castAnimId = 54; // ChannelCast
e.castDurationMs = durMs;
c.entries.push_back(e);
};
add(200, "PortalCast", 10000,
"Spells/Cast/Arcane/portal_cast.m2",
"Spells/Impact/Arcane/portal_open.m2",
"Mage portal — long channel + persistent doorway.");
add(201, "HearthstoneReturn", 10000,
"Spells/Cast/Arcane/hearth_glow.m2",
"Spells/Impact/Arcane/hearth_arrive.m2",
"Hearthstone teleport — channel + arrival flash.");
add(202, "MountSummon", 1500,
"Spells/Cast/Nature/mount_summon.m2", "",
"Quick mount spawn animation.");
add(203, "Resurrect", 5000,
"Spells/Cast/Holy/resurrect_cast.m2",
"Spells/Impact/Holy/resurrect_target.m2",
"Resurrect — golden beam + corpse glow.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -138,6 +138,8 @@ const char* const kArgRequired[] = {
"--gen-animations", "--gen-animations-combat", "--gen-animations-movement",
"--info-wani", "--validate-wani",
"--export-wani-json", "--import-wani-json",
"--gen-svk", "--gen-svk-combat", "--gen-svk-utility",
"--info-wsvk", "--validate-wsvk",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -76,6 +76,7 @@
#include "cli_list_formats.hpp"
#include "cli_info_magic.hpp"
#include "cli_animations_catalog.hpp"
#include "cli_spell_visuals_catalog.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -193,6 +194,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleListFormats,
handleInfoMagic,
handleAnimationsCatalog,
handleSpellVisualsCatalog,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -1365,6 +1365,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wani to a human-editable JSON sidecar (defaults to <base>.wani.json)\n");
std::printf(" --import-wani-json <json-path> [out-base]\n");
std::printf(" Import a .wani.json sidecar back into binary .wani (accepts behaviorTier int OR name string)\n");
std::printf(" --gen-svk <wsvk-base> [name]\n");
std::printf(" Emit .wsvk starter: 3 visual kits (Frostbolt / Fireball / HealingTouch) with projectile + impact + hand effects\n");
std::printf(" --gen-svk-combat <wsvk-base> [name]\n");
std::printf(" Emit .wsvk 5 combat visuals (sword/arrow/groundpound/parry/deflect) with WANI animation cross-refs\n");
std::printf(" --gen-svk-utility <wsvk-base> [name]\n");
std::printf(" Emit .wsvk 4 utility visuals (portal/hearthstone/mount-summon/resurrect) with no projectile\n");
std::printf(" --info-wsvk <wsvk-base> [--json]\n");
std::printf(" Print WSVK entries (id / cast+impact anim IDs / projectile speed+gravity / cast duration / AoE radius / sounds / name)\n");
std::printf(" --validate-wsvk <wsvk-base> [--json]\n");
std::printf(" Static checks: id>0+unique, name not empty, no negative speeds/radii, projectile-model + speed coherence, no-effect warning\n");
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -67,6 +67,7 @@ constexpr MagicEntry kMagicTable[] = {
{{'W','H','O','L'}, ".whol", "holiday", "--info-whol", "Holiday catalog"},
{{'W','L','I','Q'}, ".wliq", "liquids", "--info-wliq", "Liquid catalog"},
{{'W','A','N','I'}, ".wani", "anim", "--info-wani", "Animation catalog"},
{{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit 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"},

View file

@ -70,6 +70,7 @@ constexpr FormatRow kFormats[] = {
{"WHOL", ".whol", "holiday", "Holidays.dbc + game_event", "Calendar holiday + event catalog"},
{"WLIQ", ".wliq", "liquids", "LiquidType.dbc", "Liquid material catalog (water/lava/slime)"},
{"WANI", ".wani", "anim", "AnimationData.dbc", "Animation ID + fallback + weapon-flag catalog"},
{"WSVK", ".wsvk", "spellfx", "SpellVisualKit.dbc + SpellVisFx", "Spell visual kit (cast/proj/impact effects)"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,253 @@
#include "cli_spell_visuals_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_spell_visuals.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
std::string stripWsvkExt(std::string base) {
stripExt(base, ".wsvk");
return base;
}
bool saveOrError(const wowee::pipeline::WoweeSpellVisualKit& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeSpellVisualKitLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wsvk\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeSpellVisualKit& c,
const std::string& base) {
std::printf("Wrote %s.wsvk\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" visuals : %zu\n", c.entries.size());
}
int handleGenStarter(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "StarterVisualKits";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWsvkExt(base);
auto c = wowee::pipeline::WoweeSpellVisualKitLoader::makeStarter(name);
if (!saveOrError(c, base, "gen-svk")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenCombat(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "CombatVisualKits";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWsvkExt(base);
auto c = wowee::pipeline::WoweeSpellVisualKitLoader::makeCombat(name);
if (!saveOrError(c, base, "gen-svk-combat")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenUtility(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "UtilityVisualKits";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWsvkExt(base);
auto c = wowee::pipeline::WoweeSpellVisualKitLoader::makeUtility(name);
if (!saveOrError(c, base, "gen-svk-utility")) 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 = stripWsvkExt(base);
if (!wowee::pipeline::WoweeSpellVisualKitLoader::exists(base)) {
std::fprintf(stderr, "WSVK not found: %s.wsvk\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeSpellVisualKitLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wsvk"] = base + ".wsvk";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"visualKitId", e.visualKitId},
{"name", e.name},
{"description", e.description},
{"castEffectModelPath", e.castEffectModelPath},
{"projectileModelPath", e.projectileModelPath},
{"impactEffectModelPath", e.impactEffectModelPath},
{"handEffectModelPath", e.handEffectModelPath},
{"precastAnimId", e.precastAnimId},
{"castAnimId", e.castAnimId},
{"impactAnimId", e.impactAnimId},
{"castSoundId", e.castSoundId},
{"impactSoundId", e.impactSoundId},
{"projectileSpeed", e.projectileSpeed},
{"projectileGravity", e.projectileGravity},
{"castDurationMs", e.castDurationMs},
{"impactRadius", e.impactRadius},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WSVK: %s.wsvk\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" visuals : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id castAnim impAnim speed grav dur(ms) AoE castSnd impSnd name\n");
for (const auto& e : c.entries) {
std::printf(" %4u %5u %5u %5.1f %4.2f %5u %4.1f %5u %5u %s\n",
e.visualKitId, e.castAnimId, e.impactAnimId,
e.projectileSpeed, e.projectileGravity,
e.castDurationMs, e.impactRadius,
e.castSoundId, e.impactSoundId, 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 = stripWsvkExt(base);
if (!wowee::pipeline::WoweeSpellVisualKitLoader::exists(base)) {
std::fprintf(stderr,
"validate-wsvk: WSVK not found: %s.wsvk\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeSpellVisualKitLoader::load(base);
std::vector<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::vector<uint32_t> 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.visualKitId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.visualKitId == 0)
errors.push_back(ctx + ": visualKitId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.projectileSpeed < 0.0f)
errors.push_back(ctx + ": projectileSpeed " +
std::to_string(e.projectileSpeed) +
" negative (use 0 for instant)");
if (e.projectileGravity < 0.0f)
errors.push_back(ctx + ": projectileGravity " +
std::to_string(e.projectileGravity) +
" negative (use 0 for straight line)");
if (e.impactRadius < 0.0f)
errors.push_back(ctx + ": impactRadius " +
std::to_string(e.impactRadius) +
" negative (use 0 for single-target)");
// Projectile model + zero speed = projectile defined
// but never travels. Inverse: speed > 0 + no model =
// invisible projectile. Both are usually mistakes.
if (!e.projectileModelPath.empty() &&
e.projectileSpeed == 0.0f) {
warnings.push_back(ctx +
": projectileModelPath set but projectileSpeed=0 "
"(model never travels)");
}
if (e.projectileModelPath.empty() &&
e.projectileSpeed > 0.0f) {
warnings.push_back(ctx +
": projectileSpeed > 0 but no projectileModelPath "
"(invisible projectile)");
}
// No effect model AND no animation AND no sound = the
// visual kit has no observable effect at all.
if (e.castEffectModelPath.empty() &&
e.impactEffectModelPath.empty() &&
e.handEffectModelPath.empty() &&
e.castAnimId == 0 && e.impactAnimId == 0 &&
e.castSoundId == 0 && e.impactSoundId == 0) {
warnings.push_back(ctx +
": no models, animations, or sounds — visual kit has no observable effect");
}
for (uint32_t prev : idsSeen) {
if (prev == e.visualKitId) {
errors.push_back(ctx + ": duplicate visualKitId");
break;
}
}
idsSeen.push_back(e.visualKitId);
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wsvk"] = base + ".wsvk";
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-wsvk: %s.wsvk\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu visual kits, all visualKitIds 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 handleSpellVisualsCatalog(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-svk") == 0 && i + 1 < argc) {
outRc = handleGenStarter(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-svk-combat") == 0 && i + 1 < argc) {
outRc = handleGenCombat(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-svk-utility") == 0 && i + 1 < argc) {
outRc = handleGenUtility(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wsvk") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wsvk") == 0 && i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,11 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
bool handleSpellVisualsCatalog(int& i, int argc, char** argv, int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee