From df1b225767546484992290ba0044511cbc07579a Mon Sep 17 00:00:00 2001 From: Glyalith Date: Mon, 7 Sep 2026 11:56:37 -0600 Subject: [PATCH] fix(actionbars): keep press-and-hold working with Single Button Assist Reported: with Single Button Assist bound to a press-and-hold key, casts are delayed from the first press. Disabling EllesmereUI Action Bars fixes it, on a clean install with no other addons, across bars and classes. Press-and-hold casts on key DOWN, and that is a secure attribute Blizzard sets in UpdatePressAndHoldAction. The only event reaching it is ACTIONBAR_SLOT_CHANGED. We were unregistering Blizzard's whole ActionBarButtonEventsFrame and re-registering a subset, which made US the registrant, which made that SetAttribute a tainted write the client blocks in combat. The workaround was to withhold the event whenever InCombatLockdown, so for the whole pull the attribute went stale against an assist slot that re-picks ~11x/sec, and the button fell back to casting on release. The wholesale wipe is now a targeted one: every event Blizzard registers on that frame is unregistered by name EXCEPT ACTIONBAR_SLOT_CHANGED, which is left on Blizzard's own registration and never taken over. Untainted dispatch, so the attribute keeps updating under lockdown. All three kill sites go through the same helper, including the redundant-kill safety net, which would otherwise strand it again -- nothing re-registers that event any more. The performance reason for the original wipe is untouched: ACTIONBAR_UPDATE_COOLDOWN and the rest are still suppressed, and that was the ~1500/sec path. ACTIONBAR_SLOT_CHANGED is gated per button by Blizzard on "arg1 == 0 or arg1 == self.action", so it wakes only the button whose slot moved. Helper is ns-scoped rather than a file local: this file is at Lua's 200-local main-chunk cap and a local pushed it over. --- .../EllesmereUIActionBars.lua | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/EllesmereUIActionBars/EllesmereUIActionBars.lua b/EllesmereUIActionBars/EllesmereUIActionBars.lua index d68179238..79da0f671 100644 --- a/EllesmereUIActionBars/EllesmereUIActionBars.lua +++ b/EllesmereUIActionBars/EllesmereUIActionBars.lua @@ -964,12 +964,34 @@ end -- dispatcher handles the needed events with HasAction() filtering (GCD swipes -- ride its ACTIONBAR_UPDATE_COOLDOWN); re-registered during vehicle/override -- so Blizzard's OverrideActionBar buttons (not replaced by us) still get cooldowns. -if ActionBarButtonEventsFrame then ActionBarButtonEventsFrame:UnregisterAllEvents() end +-- +-- Named one by one rather than UnregisterAllEvents, to leave exactly one behind: +-- ACTIONBAR_SLOT_CHANGED stays on BLIZZARD's registration. It is the only event +-- reaching Update() -> UpdatePressAndHoldAction -> SetAttribute, and a dispatch we +-- own makes that a tainted write the client blocks in combat -- which forced the +-- in-combat gate below and left press-and-hold stale all pull against an assist +-- slot re-picking ~11x/sec. List mirrors ActionBarButtonEventsFrameMixin:OnLoad. +-- do-end scoped, ns-exposed: this file is at Lua's 200-local main-chunk cap. +do + local suppress = { + "PLAYER_ENTERING_WORLD", "UPDATE_BINDINGS", "GAME_PAD_ACTIVE_CHANGED", + "UPDATE_SHAPESHIFT_FORM", "ACTIONBAR_UPDATE_COOLDOWN", "PET_BAR_UPDATE", + "UNIT_FLAGS", "UNIT_AURA", "PLAYER_MOUNT_DISPLAY_CHANGED", + } + ns.SuppressBlizzButtonEvents = function() + if not ActionBarButtonEventsFrame then return end + for i = 1, #suppress do + ActionBarButtonEventsFrame:UnregisterEvent(suppress[i]) + end + end +end +ns.SuppressBlizzButtonEvents() if ActionBarActionEventsFrame then ActionBarActionEventsFrame:UnregisterAllEvents() end do local _abefEvents = { + -- No ACTIONBAR_SLOT_CHANGED: Blizzard's registration is never taken over. "ACTIONBAR_UPDATE_COOLDOWN", "ACTIONBAR_UPDATE_STATE", - "ACTIONBAR_UPDATE_USABLE", "ACTIONBAR_SLOT_CHANGED", + "ACTIONBAR_UPDATE_USABLE", -- Spell-typed extra-action buttons (delve abilities) carry no action -- slot, so their cooldown fires SPELL_UPDATE_COOLDOWN not this event. "SPELL_UPDATE_COOLDOWN", @@ -1030,13 +1052,13 @@ do return _classPH end -- ACTIONBAR_SLOT_CHANGED is the only event in either set that reaches - -- Blizzard's Update() -> UpdatePressAndHoldAction -> SetAttribute. The - -- registration is ours, so the dispatch runs under our taint and that write - -- is BLOCKED in combat, on Blizzard's own ActionButtonN and reported as - -- EllesmereUI. An assisted-combat action dirties its slot ~11x/sec, so a - -- raid pull spams it (Jera, 9.0.1). Registered out of combat only, both - -- edges driven by the REGEN events; PLAYER_ENTERING_WORLD, the other - -- Update() path, cannot fire under lockdown. + -- Blizzard's Update() -> UpdatePressAndHoldAction -> SetAttribute, so a + -- dispatch WE own makes that a tainted write the client blocks in combat, on + -- Blizzard's own ActionButtonN and reported as EllesmereUI (Jera, 9.0.1). + -- We no longer own it: SuppressBlizzButtonEvents leaves Blizzard's own + -- registration alone, so it keeps arriving under lockdown and press-and-hold + -- stays current against a Single Button Assist slot re-picking ~11x/sec. + -- slotOK stays in the mode key so the REGEN edges still re-derive the rest. local function ApplyBroadcaster() local want = (_vehNeed or _extraNeed) and "full" or ((_phNeed or ClassMayPressHold()) and "ph" or "off") @@ -1046,14 +1068,14 @@ do -- Always drop to a known state first: "full" and "ph" are different -- registration sets, so switching between them directly would leave the -- wider set's events behind. - if ActionBarButtonEventsFrame then ActionBarButtonEventsFrame:UnregisterAllEvents() end + -- Targeted, so Blizzard's own ACTIONBAR_SLOT_CHANGED registration survives + -- every mode switch (see SuppressBlizzButtonEvents). + SuppressBlizzButtonEvents() if ActionBarActionEventsFrame then ActionBarActionEventsFrame:UnregisterAllEvents() end if want == "full" then if ActionBarButtonEventsFrame then for _, ev in ipairs(_abefEvents) do - if slotOK or ev ~= "ACTIONBAR_SLOT_CHANGED" then - ActionBarButtonEventsFrame:RegisterEvent(ev) - end + ActionBarButtonEventsFrame:RegisterEvent(ev) end end if ActionBarActionEventsFrame then @@ -1063,9 +1085,8 @@ do end elseif want == "ph" then if ActionBarButtonEventsFrame then - if slotOK then - ActionBarButtonEventsFrame:RegisterEvent("ACTIONBAR_SLOT_CHANGED") - end + -- No ACTIONBAR_SLOT_CHANGED here any more: Blizzard's own registration + -- is left in place and never taken over, so it keeps arriving in combat. -- PLAYER_ENTERING_WORLD as well, because SLOT_CHANGED alone -- cannot seed a login. Blizzard gates that one on -- "arg1 == 0 or arg1 == tonumber(self.action)", so a button only @@ -14602,7 +14623,10 @@ function EAB:FinishSetup() end -- Both event broadcasters are killed at file-load time (top of file). -- Redundant kill here as safety net in case Blizzard re-creates them. - if _G.ActionBarButtonEventsFrame then _G.ActionBarButtonEventsFrame:UnregisterAllEvents() end + -- Targeted, so Blizzard's ACTIONBAR_SLOT_CHANGED survives this kill too: + -- a wholesale wipe here would strand press-and-hold, and the resync below + -- never re-registers that event any more. + if ns.SuppressBlizzButtonEvents then ns.SuppressBlizzButtonEvents() end if _G.ActionBarActionEventsFrame then _G.ActionBarActionEventsFrame:UnregisterAllEvents() end -- ...then hand control back to the mode machine. This safety net runs -- after the press-and-hold mode may already have registered, so without