feat(spell): fill notInterruptible in UnitCastingInfo / UnitChannelInfo

Mirror the 3.3.5 client's own predicate on 1.12 data instead of pushing a
placeholder false. The value is relative to the local player: the spellbook
is folded into school masks of INTERRUPT_CAST spells and MOD_SILENCE spells
(cached on Player::StatSignal), a cast is not interruptible when its
PreventionType is not SILENCE or when the caster's immunity auras (effect /
state / school / mechanic immunity) strip every mask, and the flag is never
reported while the player knows no interrupt or silence at all.

1.12 differences, each documented in Spell::Interruptible: the vanilla
server's interrupt effect also needs InterruptFlags 0x02 (channels:
ChannelInterruptFlags 0x04), which silence ignores; SMSG_SPELL_START carries
no immunity block, so creature_template interrupt immunity stays invisible;
Feral Charge's interrupt lives on its triggered spell, so the capability walk
follows one TRIGGER_SPELL hop.

Verified in-game: a paladin's Holy Light reads false, flips to true while
Divine Shield is up, and back to false when it drops.

Also centralize SPELL_EFFECT_APPLY_AURA in Offsets.h (was local to Level.cpp).
This commit is contained in:
Brues
2026-09-12 22:21:16 -05:00
parent 390eec4749
commit b4e5f4f2bb
6 changed files with 331 additions and 25 deletions
+14 -5
View File
@@ -15286,9 +15286,18 @@ so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
> the cast's **castGUID** — the exact string the
> [`UNIT_SPELLCAST_*`](#unit_spellcast_-events) events carry for this cast,
> so you can correlate the polled info with the events (works for the player
> and other units). Fields that can't be filled are structurally-correct
> placeholders: `castBarID` = `nil`, `notInterruptible` = `false`,
> `delayTimeMs` = `0`.
> and other units). `delayTimeMs` is the pushback the cast has taken so far,
> in milliseconds. `castBarID` has no source here and is always `nil`.
>
> `notInterruptible` is `true` when no interrupt or silence that **you**
> know can stop the cast. The value is relative to your own abilities, so a
> rogue and a mage can read different values for the same cast. It is `true`
> in two cases: the spell can never be interrupted, or the caster has an aura
> that blocks every interrupt and silence you know (for example Divine Shield,
> Ice Block, or Banish). If you know no interrupt and no silence, the value is
> always `false`. Limits: a creature's built-in interrupt immunity is
> server-side data that the client never receives, so a boss with that
> immunity reads `false` while your interrupt still fails on it.
### `C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`
@@ -15316,8 +15325,8 @@ as casts: when we observed the channel begin, the remote unit returns full
**start/end times** (validated against the live `UNIT_FIELD_CHANNEL_SPELL`
so a stale cache entry never applies to a different/ended channel);
otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
**`nil` times**. The player path is unchanged (full timing). Same
placeholder fields as `C_Spell.UnitCastingInfo`.
**`nil` times**. The player path is unchanged (full timing).
`notInterruptible` follows the same rules as in `C_Spell.UnitCastingInfo`.
### `C_Spell.GetSpellLevelInfo(spellID)`
+41
View File
@@ -7817,6 +7817,15 @@ enum Offsets {
// recipes, e.g. "Steel Plate Boots" = effect 36).
OFF_SPELL_RECORD_EFFECT = 0xF4, // int32[3]
SPELL_EFFECT_SCHOOL_DAMAGE = 2, // direct damage (verified vs server SpellDefines.h)
SPELL_EFFECT_APPLY_AURA = 6, // EffectApplyAuraName[i] then names the aura type
SPELL_EFFECT_TRIGGER_SPELL = 64, // casts EffectTriggerSpell[i] (Feral Charge 16979 → 19675)
// Kick / Pummel / Shield Bash / Counterspell / Earth Shock — 57 Spell.dbc
// rows carry it. The server's Spell::EffectInterruptCast is its handler.
SPELL_EFFECT_INTERRUPT_CAST = 68,
// EffectTriggerSpell[3] (columns 109-111): the spell an effect 64 casts.
// Verified in Spell.dbc: Feral Charge 16979 → [1] = 19675 "Feral Charge
// Effect" (the row that carries the druid's INTERRUPT_CAST).
OFF_SPELL_RECORD_EFFECT_TRIGGER_SPELL = 0x1B4, // int32[3]
SPELL_EFFECT_CREATE_ITEM = 24,
SPELL_EFFECT_LEARN_SPELL = 36,
SPELL_EFFECT_ENCHANT_ITEM = 53,
@@ -7918,10 +7927,27 @@ enum Offsets {
// failure on this bit) use it; thrown items (grenades — e.g. spell 4068)
// lack it, so movement must not end their cast. See Spell::Cast (issue #23).
SPELL_INTERRUPT_FLAG_MOVEMENT = 0x1,
// Bit 1: the bit the server's interrupt EFFECT tests on a cast before Kick /
// Pummel / Counterspell may stop it — tortoise `Spell::EffectInterruptCast`
// (vmangos names the same bit DAMAGE_PUSHBACK). Player casts carry 0x0F;
// creature casts with 0x00 / 0x08 / 0x09 / 0x0D (Shadow Flame, Frost
// Breath, …) cannot be interrupted by those abilities. Silence auras ignore
// the flags (PreventionType only). Read by Spell::Interruptible.
SPELL_INTERRUPT_FLAG_DAMAGE = 0x2,
// ChannelInterruptFlags (column 23). Bit 2 is the channel analog of the
// gate above (CHANNEL_FLAG_INTERRUPT in both cores). Verified in Spell.dbc:
// Blizzard / Arcane Missiles 0x7C0C, non-channels 0.
OFF_SPELL_RECORD_CHANNEL_INTERRUPT_FLAGS = 0x5C, // u32 (column 23)
CHANNEL_FLAG_INTERRUPT = 0x4,
// Remaining Spell.dbc record fields — the single source of truth for the
// record layout (byte = column * 4). Modules must use these rather than
// redefining local copies.
// Single school id (column 1) — 0 physical … 6 arcane; a spell's school
// MASK is `1 << School` (the server's GetSpellSchoolMask). Verified in
// Spell.dbc: Kick 0, Fireball 2, Earth Shock 3, Shadow Bolt 5,
// Counterspell 6. Read by Spell::Interruptible.
OFF_SPELL_RECORD_SCHOOL = 0x04, // u32 (column 1)
OFF_SPELL_RECORD_CASTING_TIME_INDEX = 0x48, // u32 → SpellCastTimes.dbc
OFF_SPELL_RECORD_MAX_LEVEL = 0x6C, // u32 (scaling cap level)
OFF_SPELL_RECORD_SPELL_LEVEL = 0x74, // u32 (this rank's effective level)
@@ -7946,6 +7972,12 @@ enum Offsets {
// reads `spellRec[+0x14]` as the spell's mechanic when the per-
// effect EffectMiscValue is 0. Read by C_Spell.GetSpellMechanicByID.
OFF_SPELL_RECORD_MECHANIC = 0x14,
// SpellMechanic.dbc ids (server SpellDefines.h `Mechanics`). Every
// interrupt ability carries 26 — per effect (Kick / Pummel / Shield Bash /
// Earth Shock EffectMechanic[1]) or spell-level (Counterspell Mechanic) —
// so MECHANIC_IMMUNITY of 26 is what makes a caster unkickable.
MECHANIC_SILENCE = 9,
MECHANIC_INTERRUPT = 26,
SPELL_AURA_MOD_STEALTH = 16,
SPELL_AURA_MOD_SHAPESHIFT = 36,
SPELL_AURA_MOUNTED = 78,
@@ -7967,6 +7999,15 @@ enum Offsets {
SPELL_AURA_MOD_SILENCE = 27,
SPELL_AURA_MOD_PACIFY_SILENCE = 60,
SPELL_AURA_MOD_DISARM = 67,
// Immunity aura types (server SpellAuraDefines.h). EffectMiscValue: 37 = a
// SPELL_EFFECT id, 38 = a SPELL_AURA id, 39 = a school MASK (Divine Shield
// rows carry 39 with 1 and 126; verified in Spell.dbc), 77 = a SpellMechanic
// id. The 3.3.5 client's not-interruptible walk tests exactly these four;
// read by Spell::Interruptible.
SPELL_AURA_EFFECT_IMMUNITY = 37,
SPELL_AURA_STATE_IMMUNITY = 38,
SPELL_AURA_SCHOOL_IMMUNITY = 39,
SPELL_AURA_MECHANIC_IMMUNITY = 77,
// SMSG_SPELL_COOLDOWN (0x134) handler, registered by the spell-opcode
// boot registrar FUN_006e7150 exactly like the failure/channel handlers
+33 -18
View File
@@ -64,9 +64,11 @@
// test only the victim's client received it), so a remote bar can't
// stretch. Inherent 1.12 protocol gap, not recoverable client-side.
//
// Modern-signature fields vanilla can't fill are structurally-correct
// placeholders: castID / castBarID = nil, notInterruptible = false,
// isTradeskill = false (no readable flag in 1.12), delayTimeMs = 0.
// The other modern-signature fields: `castID` is the cast's castGUID
// (Spell::CastEvents), `isTradeskill` is SPELL_ATTR_TRADESPELL, `delayTimeMs`
// the accumulated pushback, `notInterruptible` the 3.3.5 client's own
// player-relative predicate on 1.12 data (Spell::Interruptible). `castBarID`
// has no 1.12 source and stays nil.
#include "Game.h"
#include "Offsets.h"
@@ -75,6 +77,7 @@
#include "net/PacketDispatch.h"
#include "net/PacketReader.h"
#include "player/Info.h"
#include "spell/Interruptible.h"
#include "spell/Lookup.h"
#include "spell/CastEvents.h"
#include "tick/WorldTick.h"
@@ -293,8 +296,11 @@ const char *SpellIconPath(const uint8_t *rec) {
// Pushes UnitCastingInfo's 11-tuple from a tracked cast, or nothing (nil)
// if there's no active cast. `casterGuid` identifies whose cast this is (the
// player's or a remote unit's) so `castID` can be pulled from Spell::CastEvents
// — the same castGUID the cast's UNIT_SPELLCAST_* events carry.
int PushCastInfo(void *L, const TrackedSpell &c, uint64_t casterGuid) {
// — the same castGUID the cast's UNIT_SPELLCAST_* events carry. `casterUnit`
// is the caster's CGUnit (null when unresolvable) for the immunity-aura half
// of `notInterruptible`.
int PushCastInfo(void *L, const TrackedSpell &c, uint64_t casterGuid,
const uint8_t *casterUnit) {
if (c.spellID == 0)
return 0;
// Self-expire: once the cast window has elapsed, report nothing even if
@@ -319,7 +325,8 @@ int PushCastInfo(void *L, const TrackedSpell &c, uint64_t casterGuid) {
Game::Lua::PushString(L, castGuid);
else
Game::Lua::PushNil(L);
Game::Lua::PushBool(L, false); // 8 notInterruptible
Game::Lua::PushBool(L, Spell::Interruptible::NotInterruptible(
casterUnit, rec, /*isChannel*/ false)); // 8 notInterruptible
Game::Lua::PushNumber(L, static_cast<double>(c.spellID)); // 9 castingSpellID
Game::Lua::PushNil(L); // 10 castBarID
Game::Lua::PushNumber(L, static_cast<double>(c.delayMs)); // 11 delayTimeMs
@@ -328,7 +335,10 @@ int PushCastInfo(void *L, const TrackedSpell &c, uint64_t casterGuid) {
// Pushes UnitChannelInfo's 8-tuple. `haveTimes` is false for remote units
// (we only track the local player's channel start), pushing nil times.
int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes) {
// `casterUnit` is the channeling CGUnit (null when unresolvable), for the
// immunity-aura half of `notInterruptible`.
int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes,
const uint8_t *casterUnit) {
if (spellID == 0)
return 0;
// Self-expire a timed channel once its window elapses (mirrors
@@ -352,7 +362,8 @@ int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes
Game::Lua::PushNil(L);
}
Game::Lua::PushBool(L, IsTradeskill(rec)); // 6 isTradeskill
Game::Lua::PushBool(L, false); // 7 notInterruptible
Game::Lua::PushBool(L, Spell::Interruptible::NotInterruptible(
casterUnit, rec, /*isChannel*/ true)); // 7 notInterruptible
Game::Lua::PushNumber(L, static_cast<double>(spellID)); // 8 spellID
return 8;
}
@@ -857,7 +868,8 @@ static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
// `CastingInfo()` — local player's cast, no token lookup.
static int __fastcall Script_CastingInfo(void *L) {
return PushCastInfo(L, g_cast, Unit::Identity::PlayerGuid());
return PushCastInfo(L, g_cast, Unit::Identity::PlayerGuid(),
Unit::Identity::PlayerObject());
}
// `UnitCastingInfo(unit)` — local player from self-tracking; other units
@@ -874,19 +886,21 @@ static int __fastcall Script_UnitCastingInfo(void *L) {
if (u == nullptr)
return 0;
if (u == Resolve("player"))
return PushCastInfo(L, g_cast, Unit::Identity::PlayerGuid());
return PushCastInfo(L, g_cast, Unit::Identity::PlayerGuid(),
Unit::Identity::PlayerObject());
const uint64_t guid = Unit::Identity::GuidForObject(u);
const RemoteCast *rc = FindRemoteCast(guid);
if (rc != nullptr && !rc->isChannel && NowMs() < rc->endMs)
return PushCastInfo(L, TrackedSpell{rc->spellID, rc->startMs, rc->endMs},
guid);
guid, static_cast<const uint8_t *>(u));
return 0;
}
// `ChannelInfo()` — local player's channel, no token lookup.
static int __fastcall Script_ChannelInfo(void *L) {
return PushChannelInfo(L, g_channel.spellID, g_channel.startMs, g_channel.endMs, true);
return PushChannelInfo(L, g_channel.spellID, g_channel.startMs, g_channel.endMs, true,
Unit::Identity::PlayerObject());
}
// `UnitChannelInfo(unit)` — full timing for the player; spellID/name/
@@ -901,10 +915,11 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
if (u == nullptr)
return 0;
if (u == Resolve("player"))
return PushChannelInfo(L, g_channel.spellID, g_channel.startMs, g_channel.endMs, true);
return PushChannelInfo(L, g_channel.spellID, g_channel.startMs, g_channel.endMs, true,
Unit::Identity::PlayerObject());
auto *desc = Game::Read<const uint8_t *>(
u, Offsets::OFF_UNIT_DESCRIPTOR);
auto *unit = static_cast<const uint8_t *>(u);
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
if (desc == nullptr)
return 0;
// The live +0x228 field is authoritative for "is this unit channeling
@@ -913,10 +928,10 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
const int spellID = Game::Read<int>(desc, Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
if (spellID == 0)
return 0;
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(u));
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(unit));
if (rc != nullptr && rc->isChannel && rc->spellID == spellID && NowMs() < rc->endMs)
return PushChannelInfo(L, spellID, rc->startMs, rc->endMs, /*haveTimes*/ true);
return PushChannelInfo(L, spellID, 0, 0, /*haveTimes*/ false);
return PushChannelInfo(L, spellID, rc->startMs, rc->endMs, /*haveTimes*/ true, unit);
return PushChannelInfo(L, spellID, 0, 0, /*haveTimes*/ false, unit);
}
// The unit GUID `caster` is currently casting / channeling AT, or 0 when it
+210
View File
@@ -0,0 +1,210 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `Spell::Interruptible` — `notInterruptible` for UnitCastingInfo /
// UnitChannelInfo.
//
// The value is RELATIVE TO THE LOCAL PLAYER: "can none of MY interrupts or
// silences stop this cast?" — a rogue and a mage read different answers for
// the same boss cast. That is the 3.3.5 client's design, decompiled on the
// Frostmourne binary and mirrored here piece by piece:
//
// 1. Capability (CGSpellBook rebuild `FUN_0053ca70`, the `param_3` block):
// walk the player's spellbook and OR each spell's school mask into two
// globals — spells with an INTERRUPT_CAST effect, and spells applying a
// MOD_SILENCE aura. Both zero ⇒ the reader below never returns true.
// (3.3.5 also keeps NPC-only variants from AttributesEx7 bits 0x800 /
// 0x1000; 1.12 has no AttributesEx7, so those masks are simply 0.)
// 2. Per-cast verdict (`FUN_007262e0`, run from the cast / channel setters
// for the target, focus, player and pet, and again on their aura updates):
// bail "interruptible" on AttributesEx7 CAN_ALWAYS_BE_INTERRUPTED (absent
// in 1.12); flag "not interruptible" when PreventionType != SILENCE; else
// strip from the two masks everything the caster is immune to — the
// SMSG_SPELL_START immunity block (CAST_FLAG_IMMUNITY 0x04000000: a school
// mask and a mechanic mask, stored at CGUnit +0xa64 / +0xa68 by the 0x131
// body `FUN_00806700`) and the caster's immunity AURAS: EFFECT_IMMUNITY of
// INTERRUPT_CAST, STATE_IMMUNITY of MOD_SILENCE, SCHOOL_IMMUNITY (its misc
// is a school mask), MECHANIC_IMMUNITY of interrupt / silence. Not
// interruptible when nothing survives.
// 3. Reader (`FUN_0071ab20`, what Script_UnitCastingInfo / UnitChannelInfo
// push): the per-cast flag AND the player has any capability.
//
// What 1.12 changes:
// - No immunity block in SMSG_SPELL_START (tortoise SendSpellStart writes
// only 0x02 | AMMO), so a creature's `creature_template` interrupt immunity
// is invisible — 295 of 12,701 Turtle templates, 64 of 229 rank-3 bosses
// carry the MECHANIC_INTERRUPT bit. Documented gap in API.md; the aura part
// of step 2 is fully available.
// - Spell.dbc carries a single `School` column, so a spell's mask is
// `1 << School` (the server's GetSpellSchoolMask does the same).
// - The vanilla server's interrupt EFFECT has an extra static gate the 3.3.5
// client never checks: tortoise and vmangos `Spell::EffectInterruptCast`
// only stop a cast with InterruptFlags 0x02 (a channel: ChannelInterrupt-
// Flags 0x04). Blizzard data keeps those consistent with PreventionType;
// vanilla creature spells do not (225 timed casts have PreventionType
// SILENCE and no 0x02 — Nefarian's Shadow Flame 0x08, Frost Breath 0). A
// SILENCE aura ignores the flags (`Aura::HandleAuraModSilence` tests
// PreventionType only), so the gate applies to the interrupt mask alone.
// - A druid's interrupt lives on the TRIGGERED "Feral Charge Effect" 19675,
// not on Feral Charge 16979 (effect TRIGGER_SPELL). 3.3.5 covered that case
// with an AttributesEx7 bit on 16979; here the capability walk follows one
// TRIGGER_SPELL hop instead.
//
// Evaluation is lazy — at query time, from the caster's live aura slots — rather
// than stamped at cast start and re-evaluated on aura updates like 3.3.5. Same
// answer, no extra hooks; the 3.3.5 UNIT_SPELLCAST_INTERRUPTIBLE /
// _NOT_INTERRUPTIBLE change events are not fired (yet).
#include "Game.h"
#include "Offsets.h"
#include "aura/Data.h"
#include "player/StatSignal.h"
#include "spell/Interruptible.h"
#include "spell/Lookup.h"
#include <cstdint>
namespace Spell::Interruptible {
namespace {
// School masks of what the local player can stop a cast with.
struct Capability {
uint32_t interrupt = 0; // schools of the player's INTERRUPT_CAST spells
uint32_t silence = 0; // schools of the player's MOD_SILENCE spells
bool Any() const { return (interrupt | silence) != 0; }
};
uint32_t SchoolMask(const uint8_t *rec) {
const uint32_t school = Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_SCHOOL);
return school < 32 ? (1u << school) : 0;
}
// ORs `rec`'s school into the capability for each stopping effect it carries.
// `followTrigger` takes one TRIGGER_SPELL hop (Feral Charge — see the header);
// the triggered spell contributes ITS school, since that is the spell that lands.
void Fold(const uint8_t *rec, Capability &cap, bool followTrigger) {
const uint32_t mask = SchoolMask(rec);
auto *effect = Game::Ptr<const uint32_t>(rec, Offsets::OFF_SPELL_RECORD_EFFECT);
auto *aura = Game::Ptr<const uint32_t>(rec, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
auto *trigger = Game::Ptr<const int32_t>(rec, Offsets::OFF_SPELL_RECORD_EFFECT_TRIGGER_SPELL);
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
if (effect[i] == Offsets::SPELL_EFFECT_INTERRUPT_CAST) {
cap.interrupt |= mask;
} else if (effect[i] == Offsets::SPELL_EFFECT_APPLY_AURA &&
aura[i] == Offsets::SPELL_AURA_MOD_SILENCE) {
cap.silence |= mask;
} else if (followTrigger && effect[i] == Offsets::SPELL_EFFECT_TRIGGER_SPELL &&
trigger[i] > 0) {
if (const uint8_t *t = Spell::Lookup::RecordForID(trigger[i]))
Fold(t, cap, false);
}
}
}
// The player's capability, recomputed only when the spellbook may have changed:
// keyed on Player::StatSignal, which Spell::Learn bumps from the engine's learn /
// unlearn writers (the same invalidation the spell-knowledge caches use).
uint32_t g_epoch = 0;
bool g_valid = false;
Capability g_cap;
const Capability &PlayerCapability() {
const uint32_t epoch = Player::StatSignal::Epoch();
if (g_valid && epoch == g_epoch)
return g_cap;
Capability cap;
const int count = Game::Read<int>(Offsets::VAR_PLAYER_SPELLBOOK_COUNT);
for (int slot = 1; slot <= count && slot <= Offsets::SPELLBOOK_MAX_SLOTS; ++slot) {
const int id = Spell::Lookup::SpellbookSlotToID(slot, /*player*/ 0);
if (id <= 0)
continue;
if (const uint8_t *rec = Spell::Lookup::RecordForID(id))
Fold(rec, cap, /*followTrigger*/ true);
}
g_cap = cap;
g_epoch = epoch;
g_valid = true;
return g_cap;
}
// Strips from `cap` everything `caster`'s auras make it immune to — the 3.3.5
// aura walk of `FUN_007262e0`, over the raw descriptor slots (hidden passive
// immunities count too, so no visibility gate). Stops once nothing is left.
void StripImmunities(const uint8_t *caster, Capability &cap) {
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL && cap.Any(); ++slot) {
const uint32_t id = Aura::Data::ReadSpellID(caster, slot);
if (id == 0)
continue;
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(id));
if (rec == nullptr)
continue;
auto *aura = Game::Ptr<const uint32_t>(rec, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
auto *misc = Game::Ptr<const int32_t>(rec, Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
switch (aura[i]) {
case Offsets::SPELL_AURA_EFFECT_IMMUNITY:
if (misc[i] == Offsets::SPELL_EFFECT_INTERRUPT_CAST)
cap.interrupt = 0;
break;
case Offsets::SPELL_AURA_STATE_IMMUNITY:
if (misc[i] == Offsets::SPELL_AURA_MOD_SILENCE)
cap.silence = 0;
break;
case Offsets::SPELL_AURA_SCHOOL_IMMUNITY:
cap.interrupt &= ~static_cast<uint32_t>(misc[i]);
cap.silence &= ~static_cast<uint32_t>(misc[i]);
break;
case Offsets::SPELL_AURA_MECHANIC_IMMUNITY:
if (misc[i] == Offsets::MECHANIC_INTERRUPT)
cap.interrupt = 0;
else if (misc[i] == Offsets::MECHANIC_SILENCE)
cap.silence = 0;
break;
default:
break;
}
}
}
}
} // namespace
bool NotInterruptible(const uint8_t *caster, const uint8_t *spellRecord, bool isChannel) {
if (spellRecord == nullptr)
return false;
// Reader rule first: with no interrupt or silence known, the flag is never
// reported — whatever the cast is.
Capability cap = PlayerCapability();
if (!cap.Any())
return false;
// Static: PreventionType is the one gate shared by the server's interrupt
// effect, the server's silence aura and the 3.3.5 client.
if (Game::Read<int>(spellRecord, Offsets::OFF_SPELL_RECORD_PREVENTION_TYPE) !=
Offsets::SPELL_PREVENTION_TYPE_SILENCE)
return true;
if (caster != nullptr)
StripImmunities(caster, cap);
// The interrupt EFFECT additionally needs the cast's flag bit (server gate);
// a silence does not.
const uint32_t flags = Game::Read<uint32_t>(
spellRecord, isChannel ? Offsets::OFF_SPELL_RECORD_CHANNEL_INTERRUPT_FLAGS
: Offsets::OFF_SPELL_RECORD_INTERRUPT_FLAGS);
const uint32_t needed = isChannel ? Offsets::CHANNEL_FLAG_INTERRUPT
: Offsets::SPELL_INTERRUPT_FLAG_DAMAGE;
const bool interruptWorks = cap.interrupt != 0 && (flags & needed) != 0;
const bool silenceWorks = cap.silence != 0;
return !(interruptWorks || silenceWorks);
}
} // namespace Spell::Interruptible
+32
View File
@@ -0,0 +1,32 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// `Spell::Interruptible` — the `notInterruptible` field of `UnitCastingInfo` /
// `UnitChannelInfo`: can nothing the LOCAL PLAYER knows stop this cast? The
// 3.3.5 client's own predicate, run on 1.12 data — see Interruptible.cpp.
namespace Spell::Interruptible {
// True when no interrupt or silence the local player knows can stop
// `spellRecord`, cast (or channeled, per `isChannel`) by `caster` — a CGUnit
// pointer, or null when the caster's auras aren't readable (the static and
// capability parts still apply). False for a null record, and ALWAYS false
// while the player knows no interrupt or silence at all (the 3.3.5 reader's
// rule: the flag only exists for someone who could act on it).
bool NotInterruptible(const uint8_t *caster, const uint8_t *spellRecord,
bool isChannel);
} // namespace Spell::Interruptible
+1 -2
View File
@@ -91,7 +91,6 @@ bool SpellHiddenFromSpellbook(int spellID) {
// checks are NOT in the 1.12 client's Spell.dbc — server-only columns — so
// the spellLevel rule is the only client-readable target-level mechanism.)
constexpr uint32_t SPELL_EFFECT_APPLY_AURA = 6;
constexpr uint32_t SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 35;
int LocaleIndex() {
@@ -154,7 +153,7 @@ int RequiredTargetLevel(const uint8_t *record) {
Game::Ptr<const uint32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
bool positiveAura = false;
for (int i = 0; i < 3; ++i) {
if ((effect[i] == SPELL_EFFECT_APPLY_AURA &&
if ((effect[i] == Offsets::SPELL_EFFECT_APPLY_AURA &&
IsPositiveRankTarget(targetA[i])) ||
effect[i] == SPELL_EFFECT_APPLY_AREA_AURA_PARTY) {
positiveAura = true;