feat(editor): add WCRE (Creature Resistance & Immunity) — 106th open format

Novel replacement for the per-creature resistance columns
that vanilla WoW buried inside creature_template
(resistance1..6 fields) plus the SpellSchoolMask immunity
and mechanic_immune_mask columns. Each entry is one
creature's full defensive profile: 6 magic-school resist
values (int16, with 32767 as the full-immunity sentinel),
a physical-resistance percentage (0..75 game-engine cap),
plus three immunity bitmasks (CC kinds, spell mechanics,
magic schools).

The CC-immunity mask uses 14 named bits: ImmuneRoot /
Snare / Stun / Fear / Sleep / Silence / Charm / Disarm /
Polymorph / Banish / Knockback / Interrupt / Taunt /
Bleed. The info display renders the mask as a "+"-joined
token list ("root+stun+fear") for readability; "all" for
0xFFFF (typical raid-boss CC profile) and "none" for 0.

Three preset emitters: makeRaidBosses (5 canonical raid
bosses with iconic single-school immunities — Ragnaros
fire / Vael 50%-all / Hakkar arcane / Kel'Thuzad shadow
/ Onyxia fire+frost partial), makeElites (5 mid-tier
elites with single-school resists), makeImmunities (4
selective CC-immunity test cases — root-immune treant,
stun-immune worg, silence-immune acolyte, fear+charm+
poly-immune undead).

Validator's most novel check is creatureEntry uniqueness
— multiple WCRE entries binding the same creature would
make the damage-calc lookup ambiguous (which profile
applies?). Also catches negative resists < -100 (extreme
>2x damage taken), physicalResistPct > 75 (clamped at
runtime to game-engine armor cap), and reserved bits in
schoolImmunityMask (only bits 0-5 are meaningful).

Format count 105 -> 106. CLI flag count 1162 -> 1167.
This commit is contained in:
Kelsi 2026-05-10 01:40:39 -07:00
parent e4e15b3ffa
commit f9cad45154
10 changed files with 781 additions and 0 deletions

View file

@ -0,0 +1,317 @@
#include "pipeline/wowee_creature_resists.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'C', 'R', 'E'};
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) != ".wcre") {
base += ".wcre";
}
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 WoweeCreatureResists::Entry*
WoweeCreatureResists::findById(uint32_t resistId) const {
for (const auto& e : entries)
if (e.resistId == resistId) return &e;
return nullptr;
}
const WoweeCreatureResists::Entry*
WoweeCreatureResists::findByCreature(uint32_t creatureEntry) const {
for (const auto& e : entries)
if (e.creatureEntry == creatureEntry) return &e;
return nullptr;
}
bool WoweeCreatureResistsLoader::save(const WoweeCreatureResists& 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.resistId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.creatureEntry);
writePOD(os, e.holyResist);
writePOD(os, e.fireResist);
writePOD(os, e.natureResist);
writePOD(os, e.frostResist);
writePOD(os, e.shadowResist);
writePOD(os, e.arcaneResist);
writePOD(os, e.physicalResistPct);
writePOD(os, e.pad0);
writePOD(os, e.ccImmunityMask);
writePOD(os, e.mechanicImmunityMask);
writePOD(os, e.schoolImmunityMask);
writePOD(os, e.pad1);
writePOD(os, e.pad2);
writePOD(os, e.pad3);
writePOD(os, e.iconColorRGBA);
}
return os.good();
}
WoweeCreatureResists WoweeCreatureResistsLoader::load(
const std::string& basePath) {
WoweeCreatureResists 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.resistId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.creatureEntry) ||
!readPOD(is, e.holyResist) ||
!readPOD(is, e.fireResist) ||
!readPOD(is, e.natureResist) ||
!readPOD(is, e.frostResist) ||
!readPOD(is, e.shadowResist) ||
!readPOD(is, e.arcaneResist) ||
!readPOD(is, e.physicalResistPct) ||
!readPOD(is, e.pad0) ||
!readPOD(is, e.ccImmunityMask) ||
!readPOD(is, e.mechanicImmunityMask) ||
!readPOD(is, e.schoolImmunityMask) ||
!readPOD(is, e.pad1) ||
!readPOD(is, e.pad2) ||
!readPOD(is, e.pad3) ||
!readPOD(is, e.iconColorRGBA)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeCreatureResistsLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeCreatureResists WoweeCreatureResistsLoader::makeRaidBosses(
const std::string& catalogName) {
using R = WoweeCreatureResists;
WoweeCreatureResists c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint32_t entry,
int16_t holy, int16_t fire,
int16_t nature, int16_t frost,
int16_t shadow, int16_t arcane,
uint8_t physPct,
uint16_t ccImm, uint8_t schoolImm,
const char* desc) {
R::Entry e;
e.resistId = id; e.name = name; e.description = desc;
e.creatureEntry = entry;
e.holyResist = holy; e.fireResist = fire;
e.natureResist = nature; e.frostResist = frost;
e.shadowResist = shadow; e.arcaneResist = arcane;
e.physicalResistPct = physPct;
e.ccImmunityMask = ccImm;
e.schoolImmunityMask = schoolImm;
// Bosses immune to most CC by convention (server-
// wide behavior — bosses can't be CC'd at all).
e.mechanicImmunityMask = 0xFFFFFFFFu;
e.iconColorRGBA = packRgba(220, 60, 60); // boss red
c.entries.push_back(e);
};
// Ragnaros — fire-school immune (school bit 0x04 in
// typical school-bit numbering: holy=1, fire=2,
// nature=4, frost=8, shadow=16, arcane=32). Using
// 0x02 here for fire.
add(1, "RagnarosFireImmune", 11502,
0, 32767, 0, 0, 0, 0, 0,
0xFFFF, 0x02,
"Ragnaros (Molten Core boss) — 100% fire-school "
"immunity (fireResist=32767 = full block) plus "
"schoolImmunityMask bit for fire. All CC immune.");
add(2, "VaelHalfResist", 13020,
100, 100, 100, 100, 100, 100, 0,
0xFFFF, 0,
"Vaelastrasz (BWL) — 100 resist to all 6 magic "
"schools (~50% mitigation against caster spells). "
"All CC immune.");
add(3, "HakkarArcaneImmune", 14834,
0, 0, 0, 0, 0, 32767, 0,
0xFFFF, 0x20,
"Hakkar (ZG) — full arcane immunity. All CC "
"immune. Other schools at default zero resist.");
add(4, "KelthuzadShadowImmune", 15990,
0, 0, 0, 200, 32767, 0, 0,
0xFFFF, 0x10,
"Kel'Thuzad (Naxx) — full shadow-school "
"immunity, plus 200 frost resist (~50% frost "
"mitigation). Iconic anti-warlock fight.");
add(5, "OnyxiaPartialImmune", 10184,
0, 100, 0, 100, 0, 0, 0,
0xFFFF, 0,
"Onyxia — 100 fire + 100 frost resist (50% "
"mitigation against both schools). All CC "
"immune.");
return c;
}
WoweeCreatureResists WoweeCreatureResistsLoader::makeElites(
const std::string& catalogName) {
using R = WoweeCreatureResists;
WoweeCreatureResists c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint32_t entry,
int16_t holy, int16_t fire,
int16_t nature, int16_t frost,
int16_t shadow, int16_t arcane,
const char* desc) {
R::Entry e;
e.resistId = id; e.name = name; e.description = desc;
e.creatureEntry = entry;
e.holyResist = holy; e.fireResist = fire;
e.natureResist = nature; e.frostResist = frost;
e.shadowResist = shadow; e.arcaneResist = arcane;
e.physicalResistPct = 0;
// Elites can be CC'd but not all — 80%
// mechanicImmune to common debuffs.
e.ccImmunityMask = R::ImmuneFear | R::ImmuneSleep;
e.mechanicImmunityMask = 0;
e.schoolImmunityMask = 0;
e.iconColorRGBA = packRgba(200, 140, 60); // elite gold
c.entries.push_back(e);
};
add(100, "WaterElementalFireResist", 12471,
0, 60, 0, 0, 0, 0,
"Water Elemental (Bog of Sorrows) — 60 fire "
"resist (~30% mitigation). Frost-themed mob "
"resists fire by lore.");
add(101, "StoneGiantNatureResist", 12476,
0, 0, 100, 0, 0, 0,
"Stone Giant (Stonetalon) — 100 nature resist "
"(50% mitigation). Earth-element mob resists "
"druid spells.");
add(102, "ScarletPriestHolyResist", 11030,
80, 0, 0, 0, 60, 0,
"Scarlet Crusade Priest (Scarlet Monastery) — "
"80 holy + 60 shadow resist. Light/dark-school "
"trained.");
add(103, "DustwindStormcasterArcane", 8519,
0, 0, 0, 0, 0, 80,
"Dustwind Stormcaster (Silithus) — 80 arcane "
"resist (40% mitigation). Caster mob favors "
"arcane defense.");
add(104, "FrostwolfEntinelFrost", 10920,
0, 0, 0, 60, 0, 0,
"Frostwolf Sentinel (Alterac Valley) — 60 frost "
"resist (30% mitigation). Northern enemy "
"acclimated to cold.");
return c;
}
WoweeCreatureResists WoweeCreatureResistsLoader::makeImmunities(
const std::string& catalogName) {
using R = WoweeCreatureResists;
WoweeCreatureResists c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint32_t entry, uint16_t ccMask,
uint32_t mechMask, const char* desc) {
R::Entry e;
e.resistId = id; e.name = name; e.description = desc;
e.creatureEntry = entry;
e.ccImmunityMask = ccMask;
e.mechanicImmunityMask = mechMask;
e.schoolImmunityMask = 0;
e.iconColorRGBA = packRgba(140, 100, 240); // immune purple
c.entries.push_back(e);
};
add(200, "RootImmuneTreant", 12477,
R::ImmuneRoot | R::ImmuneSnare, 0,
"Treant (Felwood) — root + snare immune. "
"Cannot be Entangling Roots or Frost Trap'd.");
add(201, "StunImmuneAlphaWolf", 14283,
R::ImmuneStun, 0,
"Alpha Worg (Silverpine) — stun immune. "
"Cannot be Cheap Shot'd or Concussion Blow'd.");
add(202, "SilenceImmuneCaster", 11839,
R::ImmuneSilence | R::ImmuneInterrupt, 0,
"Cult of the Damned Acolyte — silence + "
"interrupt immune. Spell pushback works but "
"the cast itself can't be interrupted.");
add(203, "FearImmuneUndead", 18525,
R::ImmuneFear | R::ImmuneCharm | R::ImmunePolymorph,
0,
"Risen Construct (Naxxramas) — fear + charm + "
"polymorph immune. Mind-affecting CC has no "
"effect; physical CC like stun/root still works.");
return c;
}
} // namespace pipeline
} // namespace wowee