diff --git a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua index 6c5a90444..ad4543c09 100644 --- a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua +++ b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua @@ -17769,8 +17769,16 @@ initFrame:SetScript("OnEvent", function(self) -- "47528"). Give it a NAME but deliberately do NOT add it to -- spellOrder -- it must not appear as a selectable option on a -- bar that no longer holds it. + -- + -- Label it ONLY when this character can actually cast it. The + -- id is profile-level and the spellbook behind it is per-spec, + -- so a spec that shares the profile but not the spell inherits + -- a pick it can never use -- field-reported as a Holy Paladin + -- being shown "Rebuke". Leaving it unlabelled is what makes + -- getValue below fall back to the bar's own contents. local selSid = BD and BD() and BD().focusKickInterruptSpellID - if selSid then + if selSid and (not ns.ResolveCastableInterrupt + or ns.ResolveCastableInterrupt(selSid)) then local selKey = tostring(selSid) if not spellValues[selKey] then local selInfo = C_Spell and C_Spell.GetSpellInfo @@ -17793,7 +17801,13 @@ initFrame:SetScript("OnEvent", function(self) getValue = function() local sid = BD().focusKickInterruptSpellID if not sid then return spellOrder[1] end - return tostring(sid) + -- No label means RebuildSpellOptions rejected it: either + -- this character cannot cast it, or it is not on the bar + -- and not castable. Show what the bar actually holds + -- rather than a selection the player never made. + local key = tostring(sid) + if not spellValues[key] then return spellOrder[1] end + return key end, setValue = function(v) if v == "__none" then diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua index aacf97f97..2623d3e24 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua @@ -6565,6 +6565,57 @@ local function RefreshFocusCastProxyUnit() _focusCastProxy:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", unit) end ns.RefreshFocusCastProxyUnit = RefreshFocusCastProxyUnit + +function ns.IsSpellInPlayerBook(id) + if IsPlayerSpell and IsPlayerSpell(id) then return true end + if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook + and C_SpellBook.IsSpellKnownOrInSpellBook(id) then + return true + end + return false +end + +-- Returns the id of the stored interrupt in the form this character can +-- actually cast, or nil. +-- +-- Validate on READ and never write: focusKickInterruptSpellID is profile-level +-- while the spellbook behind it is per-spec, so the id stays correct for the +-- spec that set it. Clearing it here would destroy that spec's setting the +-- first time the player logged in on another one. +-- +-- Asks the SPELLBOOK, never a class/spec table. Which specs carry an interrupt +-- is Blizzard's to change, and a hardcoded list of that goes stale on a patch. +-- Pet-bank interrupts (a Warlock's Axe Toss, a Hunter's pet kick) are +-- legitimate picks that IsPlayerSpell cannot see, so check both banks. +-- +-- Returning the resolved id rather than a boolean is the point: 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" but leaving the caller holding the un-castable form would put an id +-- that is never on cooldown into the readiness gate. +function ns.ResolveCastableInterrupt(sid) + if type(sid) ~= "number" or sid <= 0 then return nil end + local knownInBook = ns.IsSpellInPlayerBook + if knownInBook(sid) then return sid end + -- Talented into a replacement, stored id is the base form. + if C_SpellBook and C_SpellBook.FindSpellOverrideByID then + local ovr = C_SpellBook.FindSpellOverrideByID(sid) + if ovr and ovr > 0 and ovr ~= sid and knownInBook(ovr) then return ovr end + end + -- Talented back out, stored id is the replacement form. + if C_Spell and C_Spell.GetBaseSpell then + local base = C_Spell.GetBaseSpell(sid) + if base and base > 0 and base ~= sid and knownInBook(base) then return base end + end + -- Pet bank last: it has no override/base indirection to walk. + if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook + and Enum and Enum.SpellBookSpellBank + and C_SpellBook.IsSpellKnownOrInSpellBook(sid, Enum.SpellBookSpellBank.Pet) then + return sid + end + return nil +end + local function EnsureFocusCastProxy() if _focusCastProxy then -- Demand-gate re-activation: re-register (idempotent; re-applying @@ -6584,16 +6635,40 @@ local function EnsureFocusCastProxy() local soundKey = bd.focusCastSoundKey or "none" if soundKey == "none" then return end local spellID = bd.focusKickInterruptSpellID + -- An explicit pick is only trusted while this character can cast it. + -- A stale profile-level id sails through the cooldown gate below + -- forever -- a spell you do not know is never on cooldown -- so a Holy + -- Paladin inheriting a Ret Paladin's Rebuke would be pinged for every + -- focus cast to interrupt something they have no interrupt for. + -- + -- Deliberately checked HERE and not at arming time: arming runs during + -- loading screens, when the spellbook reads empty for reasons that have + -- nothing to do with the player's spec, and folding "not loaded yet" + -- into "cannot cast it" is what silently unregistered a working proxy + -- in the first place. This handler only runs on a live cast, by which + -- point the spellbook is settled. + if spellID then spellID = ns.ResolveCastableInterrupt(spellID) end -- Auto-fallback: if user hasn't explicitly picked a spell, use the - -- first positive spell on the bar. The picker exists for users who - -- want a specific spell when multiple are on the bar. + -- first CASTABLE positive spell on the bar. The picker exists for users + -- who want a specific spell when multiple are on the bar. + -- + -- The bar's own list needs the same check as the explicit pick, not + -- because it crosses specs (assignedSpells is per-spec) but because it + -- can hold spells this spec no longer has: 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. Whichever list supplies the id, an interrupt this character + -- cannot cast reads as permanently ready to the cooldown gate below. if not spellID or spellID <= 0 then local sd = ns.GetBarSpellData and ns.GetBarSpellData(FOCUSKICK_BAR_KEY) if sd and sd.assignedSpells then for _, sid in ipairs(sd.assignedSpells) do if type(sid) == "number" and sid > 0 then - spellID = sid - break + local castable = ns.ResolveCastableInterrupt(sid) + if castable then + spellID = castable + break + end end end end diff --git a/Locales/_keys.txt b/Locales/_keys.txt index 1a5ad8122..12d53419f 100644 --- a/Locales/_keys.txt +++ b/Locales/_keys.txt @@ -1,6 +1,6 @@ # Auto-generated by .tools/extract-locale-keys.sh -- do not edit by hand. # Canonical list of translatable English keys passed as string literals -# (666 unique). Regenerate after wrapping new strings. Keys passed as +# (658 unique). Regenerate after wrapping new strings. Keys passed as # variables are not listed here -- use the in-game /euiloc harvester for # the complete runtime set. (Raid) @@ -98,7 +98,6 @@ Anchor Anchored Apply Apply All Settings To -Apply the sharer's Blizz UI Enhanced Window Skins and Tooltips, Menus & Popups settings. These are account-wide and will overwrite yours across ALL profiles. Off = keep your own. Apply to Bar Apply to Bar (All Specs) Apply to This Spell @@ -318,17 +317,12 @@ Import Full Account Data Import Profile Import Selected Addons Import a profile from string. -Import the anchor & size-match relationships from this profile. Off = keep your own layout; only the selected modules' own positions/settings come in. -Import the sharer's complete override setup: spec and conditional override values, groups, their custom Unlock Mode layouts, and Buff Manager overrides. This replaces ALL of your own overrides. Off = keep yours untouched. Import will include %1$s of %2$s addons. Importing %1$s In-game countdown unavailable in combat; the boss mod pull timer still started. Include Include CDM Spell Layout? -Include Overrides Include This Spell -Include Window Skins -Include layout Include your Cooldown Manager spell layout (which spells sit on which bars) plus all per-spell settings for any specs you choose. Include: Independent @@ -585,8 +579,6 @@ This is where you can control the settings of Unlock Mode.\n\nElements can be re This option requires %1$s to be %2$s This option requires Subtitle Text to include the Guild Name This preset -This profile string does not carry any Window & Tooltip Skins settings. -This profile string does not carry any override data. This profile was made at %1$d%% UI scale; yours is %2$d%%. Change your UI scale to match the imported profile? This will show all profiles at this scale as UI Scale is not a per-profile setting, but can be changed at any time back to your original value. This setting's active Apply to Bar (All Specs) value will be replaced. This setting's active Apply to Bar value will be replaced.