mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
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:
+14
-5
@@ -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)`
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user