mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
feat(pipeline): add WSCH (Wowee Spell School) catalog
56th open format — replaces SpellSchools.dbc plus the Resistances.dbc resistance-cap tables. Defines damage schools spells use: Physical, Holy, Fire, Nature, Frost, Shadow, Arcane, plus combined / hybrid schools that count as multiple types simultaneously (Spellfire, Spellshadow, Spellfrost — relevant for resistance-bypass mechanics). 7 canonical schools with single-bit IDs (1, 2, 4, 8, 16, 32, 64) so combinedSchoolMask values line up directly with the spell engine's school-bit enum. Hybrid schools use high-bit IDs (0x80000001+) and their combinedSchoolMask references the canonical bits they qualify as. Each school carries visual identity (color tint, icon), gameplay rules (canBeImmune / canBeAbsorbed / canBeReflected / canCrit), resistance cap at max level, and cast / impact sound IDs. Cross-references with prior formats — castSoundId and impactSoundId point at WSND.soundId; combinedSchoolMask is a bitmask of OTHER WSCH.schoolId values within the same catalog. CLI: --gen-sch (3 base — Physical / Fire / Holy showing non-resistable Holy + non-reflectable Physical), --gen-sch- magical (6 canonical magical schools with proper colors + 365 max-level resistance caps), --gen-sch-combined (3 hybrids — Spellfire / Spellshadow / Spellfrost with multi-bit combinedSchoolMask), --info-wsch, --validate-wsch with --json variants. Validator catches id+name required, reflected-without-absorbed warning (reflected damage should be absorbable), self-referential combinedSchoolMask (school qualifying as itself), and combined-mask references to bits not defined in the same catalog (resolved at runtime across catalogs). Format graph: 55 → 56 binary formats. CLI flag count: 798 → 804.
This commit is contained in:
parent
23bb97651c
commit
a664027f5f
10 changed files with 678 additions and 0 deletions
|
|
@ -644,6 +644,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_companions.cpp
|
||||
src/pipeline/wowee_spell_mechanics.cpp
|
||||
src/pipeline/wowee_keybindings.cpp
|
||||
src/pipeline/wowee_spell_schools.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1441,6 +1442,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_spell_mechanics_catalog.cpp
|
||||
tools/editor/cli_keybindings_catalog.cpp
|
||||
tools/editor/cli_tree_summary_md.cpp
|
||||
tools/editor/cli_spell_schools_catalog.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
@ -1563,6 +1565,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_companions.cpp
|
||||
src/pipeline/wowee_spell_mechanics.cpp
|
||||
src/pipeline/wowee_keybindings.cpp
|
||||
src/pipeline/wowee_spell_schools.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
109
include/pipeline/wowee_spell_schools.hpp
Normal file
109
include/pipeline/wowee_spell_schools.hpp
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Spell School catalog (.wsch) — novel
|
||||
// replacement for Blizzard's SpellSchools.dbc plus the
|
||||
// Resistances.dbc resistance-cap tables. Defines the damage
|
||||
// schools spells use: Physical, Holy, Fire, Nature, Frost,
|
||||
// Shadow, Arcane, plus combined / hybrid schools that count
|
||||
// as multiple types simultaneously (Spellfire, Spellfrost,
|
||||
// Spellshadow — relevant for resistance bypass mechanics).
|
||||
//
|
||||
// Each school carries the visual identity (color tint,
|
||||
// icon), the gameplay rules (can be absorbed by shields,
|
||||
// can crit, can creatures be immune), the resistance cap
|
||||
// at max level, and audio (cast / impact sound IDs).
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WSCH.entry.castSoundId → WSND.soundId
|
||||
// WSCH.entry.impactSoundId → WSND.soundId
|
||||
// WSCH.entry.combinedSchoolMask is a bitmask of OTHER
|
||||
// WSCH.schoolId values —
|
||||
// hybrid schools set this
|
||||
// to expose multi-type
|
||||
// damage profiles.
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WSCH"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// schoolId (uint32)
|
||||
// nameLen + name
|
||||
// descLen + description
|
||||
// iconLen + iconPath
|
||||
// canBeImmune (uint8) / canBeAbsorbed (uint8) /
|
||||
// canBeReflected (uint8) / canCrit (uint8)
|
||||
// colorRGBA (uint32)
|
||||
// baseResistanceCap (uint32)
|
||||
// castSoundId (uint32)
|
||||
// impactSoundId (uint32)
|
||||
// combinedSchoolMask (uint32)
|
||||
struct WoweeSpellSchool {
|
||||
// Canonical school IDs match WoW's spell school enum so
|
||||
// hybrid masks line up with what the spell engine expects.
|
||||
static constexpr uint32_t kSchoolPhysical = 1u << 0;
|
||||
static constexpr uint32_t kSchoolHoly = 1u << 1;
|
||||
static constexpr uint32_t kSchoolFire = 1u << 2;
|
||||
static constexpr uint32_t kSchoolNature = 1u << 3;
|
||||
static constexpr uint32_t kSchoolFrost = 1u << 4;
|
||||
static constexpr uint32_t kSchoolShadow = 1u << 5;
|
||||
static constexpr uint32_t kSchoolArcane = 1u << 6;
|
||||
|
||||
struct Entry {
|
||||
uint32_t schoolId = 0;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string iconPath;
|
||||
uint8_t canBeImmune = 1; // 1 = creatures can be immune
|
||||
uint8_t canBeAbsorbed = 1; // 1 = shields can soak this
|
||||
uint8_t canBeReflected = 0;
|
||||
uint8_t canCrit = 1;
|
||||
uint32_t colorRGBA = 0xFFFFFFFFu;
|
||||
uint32_t baseResistanceCap = 0; // max useful resistance at 80
|
||||
uint32_t castSoundId = 0; // WSND cross-ref
|
||||
uint32_t impactSoundId = 0; // WSND cross-ref
|
||||
uint32_t combinedSchoolMask = 0; // hybrid: bitmask of others
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
const Entry* findById(uint32_t schoolId) const;
|
||||
};
|
||||
|
||||
class WoweeSpellSchoolLoader {
|
||||
public:
|
||||
static bool save(const WoweeSpellSchool& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeSpellSchool load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-sch* variants.
|
||||
//
|
||||
// makeStarter — 3 base schools (Physical / Fire /
|
||||
// Holy) — Physical bypasses absorbs,
|
||||
// Holy can't be reflected.
|
||||
// makeMagical — 6 magical schools (Holy / Fire /
|
||||
// Nature / Frost / Shadow / Arcane)
|
||||
// covering the full canonical set
|
||||
// with proper colors + resistance caps.
|
||||
// makeCombined — 3 hybrid schools (Spellfire /
|
||||
// Spellshadow / Spellfrost) showing
|
||||
// combinedSchoolMask wiring.
|
||||
static WoweeSpellSchool makeStarter(const std::string& catalogName);
|
||||
static WoweeSpellSchool makeMagical(const std::string& catalogName);
|
||||
static WoweeSpellSchool makeCombined(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
264
src/pipeline/wowee_spell_schools.cpp
Normal file
264
src/pipeline/wowee_spell_schools.cpp
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
#include "pipeline/wowee_spell_schools.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'S', 'C', 'H'};
|
||||
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) != ".wsch") {
|
||||
base += ".wsch";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) {
|
||||
return (static_cast<uint32_t>(a) << 24) |
|
||||
(static_cast<uint32_t>(b) << 16) |
|
||||
(static_cast<uint32_t>(g) << 8) |
|
||||
static_cast<uint32_t>(r);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeSpellSchool::Entry*
|
||||
WoweeSpellSchool::findById(uint32_t schoolId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.schoolId == schoolId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WoweeSpellSchoolLoader::save(const WoweeSpellSchool& 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.schoolId);
|
||||
writeStr(os, e.name);
|
||||
writeStr(os, e.description);
|
||||
writeStr(os, e.iconPath);
|
||||
writePOD(os, e.canBeImmune);
|
||||
writePOD(os, e.canBeAbsorbed);
|
||||
writePOD(os, e.canBeReflected);
|
||||
writePOD(os, e.canCrit);
|
||||
writePOD(os, e.colorRGBA);
|
||||
writePOD(os, e.baseResistanceCap);
|
||||
writePOD(os, e.castSoundId);
|
||||
writePOD(os, e.impactSoundId);
|
||||
writePOD(os, e.combinedSchoolMask);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeSpellSchool WoweeSpellSchoolLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweeSpellSchool 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.schoolId)) {
|
||||
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.canBeImmune) ||
|
||||
!readPOD(is, e.canBeAbsorbed) ||
|
||||
!readPOD(is, e.canBeReflected) ||
|
||||
!readPOD(is, e.canCrit) ||
|
||||
!readPOD(is, e.colorRGBA) ||
|
||||
!readPOD(is, e.baseResistanceCap) ||
|
||||
!readPOD(is, e.castSoundId) ||
|
||||
!readPOD(is, e.impactSoundId) ||
|
||||
!readPOD(is, e.combinedSchoolMask)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeSpellSchoolLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeSpellSchool WoweeSpellSchoolLoader::makeStarter(
|
||||
const std::string& catalogName) {
|
||||
WoweeSpellSchool c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeSpellSchool::Entry e;
|
||||
e.schoolId = WoweeSpellSchool::kSchoolPhysical;
|
||||
e.name = "Physical";
|
||||
e.description = "Melee + ranged weapon damage. Mitigated by "
|
||||
"armor instead of resistance.";
|
||||
e.iconPath = "Interface/Icons/INV_Sword_04.blp";
|
||||
e.canBeAbsorbed = 1;
|
||||
e.canBeReflected = 0; // physical hits aren't reflected
|
||||
e.canCrit = 1;
|
||||
e.colorRGBA = packRgba(220, 220, 220); // light gray
|
||||
e.baseResistanceCap = 0; // armor instead
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeSpellSchool::Entry e;
|
||||
e.schoolId = WoweeSpellSchool::kSchoolFire;
|
||||
e.name = "Fire";
|
||||
e.description = "Pyromancy / dragon breath / molten attacks.";
|
||||
e.iconPath = "Interface/Icons/Spell_Fire_FlameBolt.blp";
|
||||
e.canBeAbsorbed = 1;
|
||||
e.canBeReflected = 1;
|
||||
e.canCrit = 1;
|
||||
e.colorRGBA = packRgba(220, 70, 0); // orange-red
|
||||
e.baseResistanceCap = 365;
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeSpellSchool::Entry e;
|
||||
e.schoolId = WoweeSpellSchool::kSchoolHoly;
|
||||
e.name = "Holy";
|
||||
e.description = "Light-aspected damage and healing.";
|
||||
e.iconPath = "Interface/Icons/Spell_Holy_HolyBolt.blp";
|
||||
e.canBeImmune = 0; // holy can't be resisted
|
||||
e.canBeAbsorbed = 1;
|
||||
e.canBeReflected = 0;
|
||||
e.canCrit = 1;
|
||||
e.colorRGBA = packRgba(255, 230, 130); // pale gold
|
||||
e.baseResistanceCap = 0;
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeSpellSchool WoweeSpellSchoolLoader::makeMagical(
|
||||
const std::string& catalogName) {
|
||||
WoweeSpellSchool c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name, uint8_t r,
|
||||
uint8_t g, uint8_t b, uint32_t resCap,
|
||||
const char* desc) {
|
||||
WoweeSpellSchool::Entry e;
|
||||
e.schoolId = id; e.name = name; e.description = desc;
|
||||
e.iconPath = std::string("Interface/Icons/Spell_") +
|
||||
name + ".blp";
|
||||
e.colorRGBA = packRgba(r, g, b);
|
||||
e.baseResistanceCap = resCap;
|
||||
e.canBeAbsorbed = 1;
|
||||
e.canBeReflected = 1;
|
||||
e.canCrit = 1;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
add(WoweeSpellSchool::kSchoolHoly, "Holy",
|
||||
255, 230, 130, 0, // no holy resist gear
|
||||
"Light-aspected damage / heal.");
|
||||
add(WoweeSpellSchool::kSchoolFire, "Fire",
|
||||
220, 70, 0, 365,
|
||||
"Pyromancy / dragon breath.");
|
||||
add(WoweeSpellSchool::kSchoolNature, "Nature",
|
||||
50, 200, 50, 365,
|
||||
"Lightning / poison / wild growth.");
|
||||
add(WoweeSpellSchool::kSchoolFrost, "Frost",
|
||||
150, 200, 255, 365,
|
||||
"Ice / chill / glacial.");
|
||||
add(WoweeSpellSchool::kSchoolShadow, "Shadow",
|
||||
90, 30, 130, 365,
|
||||
"Necromancy / void / corruption.");
|
||||
add(WoweeSpellSchool::kSchoolArcane, "Arcane",
|
||||
180, 100, 220, 365,
|
||||
"Pure mana / arcane missiles / time.");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeSpellSchool WoweeSpellSchoolLoader::makeCombined(
|
||||
const std::string& catalogName) {
|
||||
WoweeSpellSchool c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name, uint32_t maskBits,
|
||||
uint8_t r, uint8_t g, uint8_t b,
|
||||
const char* desc) {
|
||||
WoweeSpellSchool::Entry e;
|
||||
e.schoolId = id; e.name = name; e.description = desc;
|
||||
e.iconPath = std::string("Interface/Icons/Spell_") +
|
||||
name + ".blp";
|
||||
e.combinedSchoolMask = maskBits;
|
||||
e.colorRGBA = packRgba(r, g, b);
|
||||
e.canBeAbsorbed = 1;
|
||||
e.baseResistanceCap = 365;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
// Hybrid schools — combinedSchoolMask is the bitmask of
|
||||
// canonical schools they qualify as. Spell engine uses the
|
||||
// LOWER resistance of the combined set, so hybrids bypass
|
||||
// single-school resist gear.
|
||||
add(0x80000001, "Spellfire",
|
||||
WoweeSpellSchool::kSchoolFire | WoweeSpellSchool::kSchoolArcane,
|
||||
230, 100, 200,
|
||||
"Combined Fire+Arcane — bypasses single-school resist.");
|
||||
add(0x80000002, "Spellshadow",
|
||||
WoweeSpellSchool::kSchoolShadow | WoweeSpellSchool::kSchoolArcane,
|
||||
140, 50, 200,
|
||||
"Combined Shadow+Arcane — Shadow priest specialty.");
|
||||
add(0x80000003, "Spellfrost",
|
||||
WoweeSpellSchool::kSchoolFrost | WoweeSpellSchool::kSchoolArcane,
|
||||
130, 180, 240,
|
||||
"Combined Frost+Arcane — Frostfire bolt class.");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -169,6 +169,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-kbd", "--gen-kbd-movement", "--gen-kbd-ui",
|
||||
"--info-wkbd", "--validate-wkbd",
|
||||
"--export-wkbd-json", "--import-wkbd-json",
|
||||
"--gen-sch", "--gen-sch-magical", "--gen-sch-combined",
|
||||
"--info-wsch", "--validate-wsch",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
#include "cli_spell_mechanics_catalog.hpp"
|
||||
#include "cli_keybindings_catalog.hpp"
|
||||
#include "cli_tree_summary_md.hpp"
|
||||
#include "cli_spell_schools_catalog.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -221,6 +222,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleSpellMechanicsCatalog,
|
||||
handleKeybindingsCatalog,
|
||||
handleTreeSummaryMd,
|
||||
handleSpellSchoolsCatalog,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ constexpr FormatMagicEntry kFormats[] = {
|
|||
{{'W','C','M','P'}, ".wcmp", "pets", "--info-wcmp", "Companion / vanity pet catalog"},
|
||||
{{'W','S','M','C'}, ".wsmc", "spells", "--info-wsmc", "Spell mechanic catalog"},
|
||||
{{'W','K','B','D'}, ".wkbd", "input", "--info-wkbd", "Keybinding catalog"},
|
||||
{{'W','S','C','H'}, ".wsch", "spells", "--info-wsch", "Spell school / damage 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"},
|
||||
|
|
|
|||
|
|
@ -1515,6 +1515,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wkbd to a human-editable JSON sidecar (defaults to <base>.wkbd.json)\n");
|
||||
std::printf(" --import-wkbd-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wkbd.json sidecar back into binary .wkbd (accepts category int OR name string; isUserOverridable defaults to 1)\n");
|
||||
std::printf(" --gen-sch <wsch-base> [name]\n");
|
||||
std::printf(" Emit .wsch starter: 3 base schools (Physical / Fire / Holy) with proper colors+resistance caps\n");
|
||||
std::printf(" --gen-sch-magical <wsch-base> [name]\n");
|
||||
std::printf(" Emit .wsch 6 magical schools (Holy / Fire / Nature / Frost / Shadow / Arcane) with canonical schoolId bits\n");
|
||||
std::printf(" --gen-sch-combined <wsch-base> [name]\n");
|
||||
std::printf(" Emit .wsch 3 hybrid schools (Spellfire / Spellshadow / Spellfrost) with combinedSchoolMask wiring\n");
|
||||
std::printf(" --info-wsch <wsch-base> [--json]\n");
|
||||
std::printf(" Print WSCH entries (schoolId / immune+absorb+reflect+crit flags / resistance cap / combined-mask / name)\n");
|
||||
std::printf(" --validate-wsch <wsch-base> [--json]\n");
|
||||
std::printf(" Static checks: id+name required, reflected-without-absorbed warning, combined-mask references defined schools, no self-reference\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");
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ constexpr FormatRow kFormats[] = {
|
|||
{"WCMP", ".wcmp", "pets", "CreatureFamily + companion SQL", "Companion / vanity pet catalog"},
|
||||
{"WSMC", ".wsmc", "spells", "SpellMechanic.dbc + DR tables", "Spell mechanic / CC category catalog"},
|
||||
{"WKBD", ".wkbd", "input", "KeyBinding.dbc + default binds", "Default keybinding catalog"},
|
||||
{"WSCH", ".wsch", "spells", "SpellSchools.dbc + Resistances", "Spell school / damage type catalog"},
|
||||
|
||||
// Additional pipeline catalogs without the alternating
|
||||
// gen/info/validate CLI surface (loaded by the engine
|
||||
|
|
|
|||
274
tools/editor/cli_spell_schools_catalog.cpp
Normal file
274
tools/editor/cli_spell_schools_catalog.cpp
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
#include "cli_spell_schools_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_spell_schools.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 stripWschExt(std::string base) {
|
||||
stripExt(base, ".wsch");
|
||||
return base;
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeSpellSchool& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeSpellSchoolLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wsch\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeSpellSchool& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wsch\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" schools : %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenStarter(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StarterSchools";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWschExt(base);
|
||||
auto c = wowee::pipeline::WoweeSpellSchoolLoader::makeStarter(name);
|
||||
if (!saveOrError(c, base, "gen-sch")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenMagical(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "MagicalSchools";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWschExt(base);
|
||||
auto c = wowee::pipeline::WoweeSpellSchoolLoader::makeMagical(name);
|
||||
if (!saveOrError(c, base, "gen-sch-magical")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenCombined(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "CombinedSchools";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWschExt(base);
|
||||
auto c = wowee::pipeline::WoweeSpellSchoolLoader::makeCombined(name);
|
||||
if (!saveOrError(c, base, "gen-sch-combined")) 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 = stripWschExt(base);
|
||||
if (!wowee::pipeline::WoweeSpellSchoolLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WSCH not found: %s.wsch\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeSpellSchoolLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wsch"] = base + ".wsch";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
arr.push_back({
|
||||
{"schoolId", e.schoolId},
|
||||
{"name", e.name},
|
||||
{"description", e.description},
|
||||
{"iconPath", e.iconPath},
|
||||
{"canBeImmune", e.canBeImmune},
|
||||
{"canBeAbsorbed", e.canBeAbsorbed},
|
||||
{"canBeReflected", e.canBeReflected},
|
||||
{"canCrit", e.canCrit},
|
||||
{"colorRGBA", e.colorRGBA},
|
||||
{"baseResistanceCap", e.baseResistanceCap},
|
||||
{"castSoundId", e.castSoundId},
|
||||
{"impactSoundId", e.impactSoundId},
|
||||
{"combinedSchoolMask", e.combinedSchoolMask},
|
||||
});
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WSCH: %s.wsch\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" schools : %zu\n", c.entries.size());
|
||||
if (c.entries.empty()) return 0;
|
||||
std::printf(" schoolId immune absorb reflect crit resCap combined-mask name\n");
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf(" 0x%08x %u %u %u %u %5u 0x%08x %s\n",
|
||||
e.schoolId, e.canBeImmune, e.canBeAbsorbed,
|
||||
e.canBeReflected, e.canCrit,
|
||||
e.baseResistanceCap, e.combinedSchoolMask,
|
||||
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 = stripWschExt(base);
|
||||
if (!wowee::pipeline::WoweeSpellSchoolLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wsch: WSCH not found: %s.wsch\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeSpellSchoolLoader::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;
|
||||
// Build the set of canonical (single-bit) school IDs so we
|
||||
// can check that combinedSchoolMask only references real
|
||||
// schools defined in this catalog.
|
||||
std::vector<uint32_t> canonicalIds;
|
||||
for (const auto& e : c.entries) {
|
||||
// A canonical school is one whose schoolId is a single
|
||||
// power of 2 (only one bit set). Hybrid schools have
|
||||
// the high bit set or multiple bits.
|
||||
if (e.schoolId != 0 &&
|
||||
(e.schoolId & (e.schoolId - 1)) == 0 &&
|
||||
e.schoolId < (1u << 16)) {
|
||||
canonicalIds.push_back(e.schoolId);
|
||||
}
|
||||
}
|
||||
auto schoolBitDefined = [&](uint32_t bit) {
|
||||
for (uint32_t s : canonicalIds) if (s == bit) return true;
|
||||
return false;
|
||||
};
|
||||
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=0x" +
|
||||
[&]() {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"%08x", e.schoolId);
|
||||
return std::string(buf);
|
||||
}();
|
||||
if (!e.name.empty()) ctx += " " + e.name;
|
||||
ctx += ")";
|
||||
if (e.schoolId == 0)
|
||||
errors.push_back(ctx + ": schoolId is 0");
|
||||
if (e.name.empty())
|
||||
errors.push_back(ctx + ": name is empty");
|
||||
// canBeReflected without canBeAbsorbed is unusual —
|
||||
// reflected damage typically also goes through absorb.
|
||||
if (e.canBeReflected && !e.canBeAbsorbed) {
|
||||
warnings.push_back(ctx +
|
||||
": canBeReflected=1 but canBeAbsorbed=0 "
|
||||
"(reflected damage usually absorbable too)");
|
||||
}
|
||||
// combinedSchoolMask must only reference canonical
|
||||
// school bits that exist in this catalog.
|
||||
if (e.combinedSchoolMask != 0) {
|
||||
for (int b = 0; b < 16; ++b) {
|
||||
uint32_t bit = 1u << b;
|
||||
if ((e.combinedSchoolMask & bit) &&
|
||||
!schoolBitDefined(bit)) {
|
||||
warnings.push_back(ctx +
|
||||
": combinedSchoolMask references bit 0x" +
|
||||
[&]() {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"%x", bit);
|
||||
return std::string(buf);
|
||||
}() +
|
||||
" which isn't defined in this catalog");
|
||||
}
|
||||
}
|
||||
// A hybrid school's combinedSchoolMask should not
|
||||
// include itself (would be self-referential and
|
||||
// confuses the resistance lookup).
|
||||
if (e.combinedSchoolMask & e.schoolId) {
|
||||
errors.push_back(ctx +
|
||||
": combinedSchoolMask includes own schoolId "
|
||||
"(self-referential)");
|
||||
}
|
||||
}
|
||||
for (uint32_t prev : idsSeen) {
|
||||
if (prev == e.schoolId) {
|
||||
errors.push_back(ctx + ": duplicate schoolId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
idsSeen.push_back(e.schoolId);
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wsch"] = base + ".wsch";
|
||||
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-wsch: %s.wsch\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu schools, all schoolIds unique, "
|
||||
"all combined masks resolve\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 handleSpellSchoolsCatalog(int& i, int argc, char** argv,
|
||||
int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-sch") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenStarter(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-sch-magical") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenMagical(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-sch-combined") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenCombined(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wsch") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wsch") == 0 && i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
12
tools/editor/cli_spell_schools_catalog.hpp
Normal file
12
tools/editor/cli_spell_schools_catalog.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleSpellSchoolsCatalog(int& i, int argc, char** argv,
|
||||
int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue