Skip to content

CDM: don't trust a FocusKick interrupt this character can't cast - #1194

Merged
EllesmereGaming merged 5 commits into
EllesmereGaming:mainfrom
dfrisone:cdm-focuskick-interrupt-spec-validate
Aug 5, 2026
Merged

CDM: don't trust a FocusKick interrupt this character can't cast#1194
EllesmereGaming merged 5 commits into
EllesmereGaming:mainfrom
dfrisone:cdm-focuskick-interrupt-spec-validate

Conversation

@dfrisone

@dfrisone dfrisone commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

The bug

Reported on a Holy Paladin, which has no interrupt at all: the FocusKick bar showed Rebuke as its Interrupt Spell, and the focus cast sound fired for a spell the character cannot cast.

Root cause

focusKickInterruptSpellID lives on the bar definition, which is profile-level and therefore shared by every spec and every character on that profile. The spell it names is per-spec spellbook content. Nothing reconciled the two.

A Ret Paladin sets Rebuke, the profile carries it, and Holy reads it straight back. An earlier field report of a bare 47528 in the same dropdown was a Death Knight's Mind Freeze doing exactly the same thing.

The sound path made it worse rather than catching it. Its only guard was C_Spell.GetSpellCooldown(spellID).isActive — and a spell you do not know is never on cooldown, so an unknown id looks permanently ready and the gate passes every time. An unknown spell is the worst possible input for a readiness check.

Two changes in #1178 are why this surfaced now, though neither created it:

  • Giving a stale id a name in the dropdown turned it from a bare number that read as noise into "Rebuke", which reads as a deliberate setting. That made a silent leak legible, which is how it finally got reported.
  • Letting an explicit focusKickInterruptSpellID arm the cast-sound proxy on its own, without the spell being on the bar, meant a spec that cannot kick now gets pinged.

The fix

ns.ResolveCastableInterrupt(sid) returns the id this character can actually cast, or nil:

  1. IsPlayerSpell / C_SpellBook.IsSpellKnownOrInSpellBook
  2. walk to the override (FindSpellOverrideByID) — talented into a replacement, stored id is the base
  3. walk to the base (C_Spell.GetBaseSpell) — talented back out, stored id is the replacement
  4. the pet bank last, since pet interrupts (a Warlock's Axe Toss, a Hunter's pet kick) are legitimate picks IsPlayerSpell cannot see

Both readers of that id are now validated: the cast-sound handler resolves the explicit pick and otherwise falls through to the first castable spell on the bar, and the options dropdown only labels a stale id when it is castable, treating "no label" as "not a selection".

Three decisions worth stating

It returns the resolved id, not a boolean. The caller feeds it to a cooldown check, and a talent swap moves an interrupt between its base and override forms while the stored id stays put. Answering "yes, known" off the base form while leaving the caller holding the un-castable form would feed the readiness gate a spell that is never on cooldown — the same always-ready failure, reached from the other side. The base/override walk is also what stops the guard silencing a legitimate kick stored under the form the player is not currently talented into, which would regress the fix in #1178.

It validates on read and never writes back. The stored id is still correct for the spec that set it. Clearing it during a Holy login would silently destroy the Ret setting the first time the player switched characters. That applies to any "clean up the stale value" instinct on profile-level data shared across specs.

The check runs at fire time, not arming time. Arming happens during loading screens, when the spellbook reads empty for reasons unrelated to spec. Gating arming on IsPlayerSpell would fold "not loaded yet" into "cannot cast", reintroducing the teardown #1178 fixed. The cast handler only runs on a live UNIT_SPELLCAST_START, by which point the spellbook has settled.

It is also deliberately spellbook-driven rather than a class/spec table: which specs carry an interrupt is Blizzard's to change, and a hardcoded list goes stale on a patch. (Worth noting the common assumption that Resto Shaman is the only healer with a kick is not right either — Preservation has Quell, Mistweaver has Spear Hand Strike.)

Scope

Two files in the Cooldown Manager, +96/−15. No change to arming, no change to the nameplate/unit-frame kick tint (EllesmereUI_Kick.lua resolves from the spellbook on every SPELLS_CHANGED and was always correct here); only the CDM FocusKick bar had the profile-level leak.

The branch also carries a separate housekeeping commit regenerating Locales/_keys.txt, which was stale against main.

Testing

Confirmed fixed in game by the reporter on a Holy Paladin: the stale interrupt no longer appears in the dropdown and the focus cast sound no longer fires for a spell the character cannot cast.

focusKickInterruptSpellID lives on the bar definition, which is
profile-level, while the spell it names is per-spec spellbook content.
Nothing reconciled the two, so a Ret Paladin's Rebuke rode the shared
profile onto a Holy Paladin, who has no interrupt at all. The same leak
produced the earlier report of a bare "47528" (Mind Freeze) in the
dropdown.

Two consequences, both fixed by validating on read:

- The cast-sound handler took the stale id straight to its "is my kick
  ready" gate. A spell you do not know is never on cooldown, so the gate
  passed every time and the player was pinged to interrupt casts they
  cannot interrupt.
- The options dropdown rendered the phantom pick as a real selection.

Validated on read and never written back: the id is still correct for
the spec that set it, so clearing it would destroy that spec's setting
the first time the player logged in on another one. Pet-bank interrupts
(Axe Toss, pet kicks) are legitimate picks that IsPlayerSpell cannot
see, so both banks are checked.

The check deliberately sits at fire time rather than at arming time.
Arming runs during loading screens, when the spellbook reads empty for
reasons unrelated to the player's spec, and folding "not loaded yet"
into "cannot cast it" is the exact collapse that unregistered a working
proxy in EllesmereGaming#1178.
Field report: the sound fires on a Holy Paladin. That only happens if
the handler resolved an interrupt id, and it has two sources -- the
explicit profile-level pick and the bar's assignedSpells. The previous
commit only validated the first, so if Rebuke reached this character
through the bar's spell list instead the sound still fired.

assignedSpells is per-spec, but per-spec is not the same as castable:
importing a shared profile with "Include CDM Spell Layout" writes
another character's layout into these spec keys wholesale, and talent
changes strand entries the same way.

The rule is the same whichever list supplies the id -- a spell this
character cannot cast is never on cooldown, so it reads as permanently
ready to the gate that decides whether to play the sound.
…cast

Hardening the same gate from the other side. The check now returns the
castable id rather than a yes/no, and the caller uses what it returns.

A talent swap moves an interrupt between its base and override forms
while the stored id stays put. Answering "yes, known" off the base form
but leaving the caller holding the un-castable stored id would feed the
readiness gate a spell that is never on cooldown -- the same always-ready
failure this branch is about, reached from the other direction. Walking
to the override, then to the base, and handing back whichever one the
spellbook actually has closes both.

It also protects the feature it is guarding: without the base/override
walk, a legitimate kick stored under the form the player is not currently
talented into would read as "cannot cast" and silently kill the sound for
someone who can kick. That is the regression risk of adding a spellbook
check at all, and it is the worse of the two failures.

Resolution stays spellbook-driven throughout. Which specs carry an
interrupt is Blizzard's to change -- Preservation has Quell, Mistweaver
has Spear Hand Strike, Resto Shaman has Wind Shear, most other healers
have nothing -- and a hardcoded table of that goes stale on a patch.
…viour change

The inner helper was a closure rebuilt on every call, and this runs on
every cast start of the tracked unit. Lifted to ns.IsSpellInPlayerBook.

Also trimmed the header comment down to the three things that are not
obvious from the code: validate on read and never write, ask the
spellbook rather than a spec table, and return the resolved id rather
than a boolean. The Rebuke story it repeated is already at the call site.
@EllesmereGaming
EllesmereGaming merged commit 7626675 into EllesmereGaming:main Aug 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants