From 4b2514af7958eb92375af6284de41d8015e1b213 Mon Sep 17 00:00:00 2001 From: Glyalith Date: Sun, 6 Sep 2026 19:32:06 -0600 Subject: [PATCH 1/2] fix(cdm): let a tracked bar find its debuff on the target Reported for a rogue Blind macro that sets focus to the target, clears the target, targets and blinds someone else, then restores the original target from focus, all inside one macro. Afterwards the player's bleeds on that target stop showing on the tracked bar until they switch target and back. The stall is Blizzard's. CooldownViewerMixin:OnPlayerTargetChanged only refreshes when UnitGUID("target") differs from the one it stored, and this macro ends on the GUID it started with, so the refresh never runs and the item stays inactive. Its frame-scoped aura cache is keyed on unit token and stamped with GetTime(), and nothing on the target-change path marks it dirty, so a second target change inside one frame reads the stale list. Our display is a faithful mirror of that, so it goes quiet with it. But the bind-miss fallback that already exists for "the viewer has not bound this aura yet" only ever asked GetPlayerAuraBySpellID, which cannot see a debuff on somebody else. It now asks the target too, so the bar rides out the stall. Only a readable sourceUnit mismatch rejects the aura, so another player's copy of the same debuff cannot drive the bar while an unreadable one still shows. Field reads need no new guarding: the consumer already classifies duration, expirationTime and applications before comparing them. PLAYER_TARGET_CHANGED joins both tick wake sets as well. Target-applied auras bind and release on that edge and on no player-scoped one, so a parked ticker had no way to learn a new target already carried a tracked debuff. --- .../EllesmereUICdmBuffBars.lua | 30 +++++++++++++++++++ .../EllesmereUICdmHooks.lua | 4 +++ 2 files changed, 34 insertions(+) diff --git a/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua b/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua index 75648db64..9e6829ca8 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua @@ -874,12 +874,19 @@ function _tbbWake.Sleep() _tbbWake:RegisterUnitEvent("UNIT_AURA", "player") _tbbWake:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player") _tbbWake:RegisterEvent("PLAYER_REGEN_DISABLED") + -- A debuff the player left on a new target is live the moment it is selected, and + -- none of the player-scoped edges above can see that. Rare enough to wake on + -- without a probe. + _tbbWake:RegisterEvent("PLAYER_TARGET_CHANGED") end function _tbbWake.Wake() _tbbWake:UnregisterAllEvents() -- Stay subscribed to the aura edge while AWAKE: pool composition changes only on a -- player aura event or a pool Acquire (hooked separately), retiring the assignment memo. _tbbWake:RegisterUnitEvent("UNIT_AURA", "player") + -- Target changes rebind which auras the viewer's frames carry, so the awake branch + -- of OnEvent retires the assignment memo on this the same as any other non-aura edge. + _tbbWake:RegisterEvent("PLAYER_TARGET_CHANGED") _tbbWake._idleTicks = 0 _tbbAssignDirty = true _tbbReflowDirty = true @@ -5204,6 +5211,10 @@ function ns.UpdateTrackedBuffBarTimers() -- Liveness for the idle sleeper: set by any branch below that is actually animating or tracking something this tick. local tickLive = false + -- Resolved once per tick, not per bar: the bind-miss fallback below asks the target + -- for a debuff the player applied, and there is no point asking with nothing targeted. + local hasTarget = UnitExists and UnitExists("target") and true or false + -- Profile-wide smooth-fill switches, resolved once per tick for every fill site (absent buffs key = enabled; absent cooldowns key = OFF). local sm do @@ -5314,6 +5325,25 @@ function ns.UpdateTrackedBuffBarTimers() if not fbAura and cfg.baseSpellID and cfg.baseSpellID > 0 then fbAura = C_UnitAuras.GetPlayerAuraBySpellID(cfg.baseSpellID) end + -- Same net for a debuff the player put on the TARGET, which the queries + -- above can never see. Blizzard's viewer stalls exactly there after a + -- macro that clears and restores the target inside one frame: its + -- OnPlayerTargetChanged compares GUIDs, sees the same one it stored, and + -- never refreshes, so the item stays inactive until a real target switch. + if not fbAura and hasTarget and C_UnitAuras.GetUnitAuraBySpellID then + fbAura = C_UnitAuras.GetUnitAuraBySpellID("target", cfg.spellID) + if not fbAura and cfg.baseSpellID and cfg.baseSpellID > 0 then + fbAura = C_UnitAuras.GetUnitAuraBySpellID("target", cfg.baseSpellID) + end + -- Somebody else's copy of the same debuff must not drive our bar. + -- sourceUnit reads secret on an enemy in restricted content, so only + -- a READABLE mismatch rejects; an unreadable one is left to show. + local src = fbAura and fbAura.sourceUnit + if src and not (issecretvalue and issecretvalue(src)) + and src ~= "player" then + fbAura = nil + end + end -- Fallback driving means the viewer has not bound this aura yet, and -- Blizzard's late-bind can land WITHOUT a fresh player aura event. Keep -- the memo dirty while any fallback is live so the next tick re-pairs diff --git a/EllesmereUICooldownManager/EllesmereUICdmHooks.lua b/EllesmereUICooldownManager/EllesmereUICdmHooks.lua index ae20d58c9..33f2053b2 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmHooks.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmHooks.lua @@ -10222,6 +10222,10 @@ function ns.SetupViewerHooks() cdmBuffTickFrame:RegisterUnitEvent("UNIT_AURA", "player") cdmBuffTickFrame:RegisterEvent("PLAYER_TOTEM_UPDATE") cdmBuffTickFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + -- Target-applied auras bind and release on this edge and on no player-scoped + -- one, so without it a tracked debuff's glow could only be picked up by the + -- 1s staleness net below, or not at all once the ticker had settled. + cdmBuffTickFrame:RegisterEvent("PLAYER_TARGET_CHANGED") cdmBuffTickFrame:SetScript("OnEvent", function(_, event, _, updateInfo) ns._btDirty = true -- Gen bump on anything that can CHANGE which auras are active: additions From 9a1182027a5e2a09b3a5ecc01d80e7f10c4e70e7 Mon Sep 17 00:00:00 2001 From: Glyalith Date: Tue, 8 Sep 2026 07:03:29 -0600 Subject: [PATCH 2/2] Filter target fallback by player and probe before waking --- .../EllesmereUICdmBuffBars.lua | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua b/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua index 9e6829ca8..ca6138bdb 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmBuffBars.lua @@ -875,8 +875,7 @@ function _tbbWake.Sleep() _tbbWake:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player") _tbbWake:RegisterEvent("PLAYER_REGEN_DISABLED") -- A debuff the player left on a new target is live the moment it is selected, and - -- none of the player-scoped edges above can see that. Rare enough to wake on - -- without a probe. + -- none of the player-scoped edges above can see that. Probe before waking. _tbbWake:RegisterEvent("PLAYER_TARGET_CHANGED") end function _tbbWake.Wake() @@ -896,6 +895,33 @@ end -- ticking, so this probe answers "could any bar be live?" WITHOUT waking: an active -- viewer frame, or a live player aura for a fallback-class config. Casts and combat -- entry skip the probe (rare at idle; the legitimate start edges the probe can't see). +-- Cache names by configuration, retiring entries when their configuration is dropped. +_tbbWake._targetNames = setmetatable({}, { __mode = "k" }) +function _tbbWake.GetTargetAura(cfg) + if not UnitExists("target") then return nil end + local names = _tbbWake._targetNames[cfg] + if not names then + names = {} + _tbbWake._targetNames[cfg] = names + end + if names.spellID ~= cfg.spellID then + names.spellID, names.name = cfg.spellID, nil + end + if names.baseSpellID ~= cfg.baseSpellID then + names.baseSpellID, names.baseName = cfg.baseSpellID, nil + end + if not names.name then names.name = C_Spell.GetSpellName(cfg.spellID) end + if not names.baseName and cfg.baseSpellID and cfg.baseSpellID > 0 then + names.baseName = C_Spell.GetSpellName(cfg.baseSpellID) + end + -- Ownership is filtered by the engine; never inspect a secret sourceUnit. + local filter = UnitIsFriend("player", "target") and "HELPFUL|PLAYER" or "HARMFUL|PLAYER" + local aura = names.name and C_UnitAuras.GetAuraDataBySpellName("target", names.name, filter) + if not aura and names.baseName and names.baseName ~= names.name then + aura = C_UnitAuras.GetAuraDataBySpellName("target", names.baseName, filter) + end + return aura +end function _tbbWake.Probe() if ns._tbbPlaceholderMode then return true end local viewer = _G["BuffBarCooldownViewer"] @@ -914,7 +940,8 @@ function _tbbWake.Probe() and cfg.spellID and cfg.spellID > 0 then if C_UnitAuras.GetPlayerAuraBySpellID(cfg.spellID) or (cfg.baseSpellID and cfg.baseSpellID > 0 - and C_UnitAuras.GetPlayerAuraBySpellID(cfg.baseSpellID)) then + and C_UnitAuras.GetPlayerAuraBySpellID(cfg.baseSpellID)) + or _tbbWake.GetTargetAura(cfg) then return true end end @@ -945,7 +972,8 @@ function _tbbWake.OnEvent(_, event, _, updateInfo) end return end - if event == "UNIT_AURA" and not _tbbWake.Probe() then return end + if (event == "UNIT_AURA" or event == "PLAYER_TARGET_CHANGED") + and not _tbbWake.Probe() then return end _tbbWake.Wake() end _tbbWake:SetScript("OnEvent", _tbbWake.OnEvent) @@ -5330,19 +5358,8 @@ function ns.UpdateTrackedBuffBarTimers() -- macro that clears and restores the target inside one frame: its -- OnPlayerTargetChanged compares GUIDs, sees the same one it stored, and -- never refreshes, so the item stays inactive until a real target switch. - if not fbAura and hasTarget and C_UnitAuras.GetUnitAuraBySpellID then - fbAura = C_UnitAuras.GetUnitAuraBySpellID("target", cfg.spellID) - if not fbAura and cfg.baseSpellID and cfg.baseSpellID > 0 then - fbAura = C_UnitAuras.GetUnitAuraBySpellID("target", cfg.baseSpellID) - end - -- Somebody else's copy of the same debuff must not drive our bar. - -- sourceUnit reads secret on an enemy in restricted content, so only - -- a READABLE mismatch rejects; an unreadable one is left to show. - local src = fbAura and fbAura.sourceUnit - if src and not (issecretvalue and issecretvalue(src)) - and src ~= "player" then - fbAura = nil - end + if not fbAura and hasTarget then + fbAura = _tbbWake.GetTargetAura(cfg) end -- Fallback driving means the viewer has not bound this aura yet, and -- Blizzard's late-bind can land WITHOUT a fresh player aura event. Keep