Skip to content

Commit d877225

Browse files
author
mergetest
committed
Click casting: self-heal keyboard hover binds when they break mid-session
Keyboard hover binds could die mid-combat and stay dead until /reload (bug #976). The existing OnEnter/ticker diagnostics detected the failure modes but only logged them; they now trigger a repair that resets the restricted-env mouseover tracking, clears stray override bindings, and rebuilds the macro map + per-frame binding snippets. Repairs run out of combat only (queued to combat end otherwise), capped at one per 5s, and run unconditionally after every loading screen.
1 parent c3678d5 commit d877225

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Bug Fixes
66

77
* (Click Casting) Fixed a Lua error walking Blizzard unit frames after combat that could stop click-casting bindings from working on the default frames until reload — the frame scan now skips protected (secret) values introduced by recent client versions. (by Krathe)
8+
* (Click Casting) Keyboard binds that stop working mid-session now recover automatically after combat ends or a loading screen, instead of staying broken until a reload.
89

910
## [4.7.1]
1011

ClickCasting/Events.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ function CC:RegisterEvents()
9090
CC:RegisterAllNameplates()
9191
-- Apply hovercast bindings
9292
CC:ApplyGlobalBindings()
93-
93+
94+
-- Self-heal (bug #976): reset restricted-env hover tracking and
95+
-- rebuild keyboard binding snippets after every loading screen,
96+
-- so a broken hover-bind state never survives a zone change
97+
CC:RunBindingRepair("zone-in", true)
98+
9499
-- Check for loadout-based profile on initial load
95100
C_Timer.After(1, function()
96101
if not InCombatLockdown() then
@@ -158,6 +163,13 @@ function CC:OnCombatEnd()
158163
self.needsFullRegistration = nil
159164
end
160165

166+
-- Self-heal repair queued during combat (bug #976)
167+
if self.pendingBindingRepair then
168+
local reason = self.pendingBindingRepair
169+
self.pendingBindingRepair = nil
170+
self:RunBindingRepair(reason)
171+
end
172+
161173
-- Refresh bindings if needed
162174
if self.needsBindingRefresh then
163175
self:ApplyBindings()

ClickCasting/Frames.lua

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,7 @@ function CC:StartDiagnosticTicker(frame)
13331333
DF:DebugError("CLICK", "BINDINGS VANISHED on %s at tick %d! wrapEnter=%d wrapLeave=%d isSecureMO=%s visible=%s mouseOver=%s combat=%s",
13341334
frameName, CC.diagTickCount, wrapEnterCount, wrapLeaveCount, tostring(isSecureMouseover),
13351335
tostring(frame:IsVisible()), tostring(frame:IsMouseOver()), tostring(InCombatLockdown()))
1336+
CC:RequestBindingRepair("bindings-vanished")
13361337
end
13371338

13381339
-- Detect mouseoverbutton desync: we think we're hovering this frame,
@@ -1343,6 +1344,7 @@ function CC:StartDiagnosticTicker(frame)
13431344
CC.diagDesyncReported = true
13441345
DF:DebugError("CLICK", "MOUSEOVERBUTTON DESYNC on %s at tick %d! dfIsSecureMouseover=nil wrapEnter=%d wrapLeave=%d kbActive=%s",
13451346
frameName, CC.diagTickCount, wrapEnterCount, wrapLeaveCount, tostring(bindingsActive))
1347+
CC:RequestBindingRepair("mouseover-desync")
13461348
end
13471349
else
13481350
CC.diagDesyncReported = nil
@@ -1560,19 +1562,22 @@ function CC:SetupSecureHandlers(frame)
15601562
if wrapEnterFired and postCheck ~= "ok" then
15611563
DF:DebugError("CLICK", "POST-CHECK FAILED on %s! mouseoverbutton=%s after phase 7 — self reference lost during OnEnter",
15621564
frameName, postCheck)
1565+
CC:RequestBindingRepair("post-check-" .. postCheck)
15631566
end
15641567

15651568
-- Key diagnostic: WrapScript didn't complete all phases
15661569
if wrapEnterFired and enterPhase < 7 and hasKeyboardBindings then
15671570
DF:DebugError("CLICK", "WRAPSCRIPT INCOMPLETE on %s! phase=%d (expected 7) prev=%s",
15681571
frameName, enterPhase, prevMouseover)
1572+
CC:RequestBindingRepair("wrap-incomplete-phase" .. enterPhase)
15691573
end
15701574

15711575
-- Key diagnostic: hover is on but WrapScript didn't activate keyboard bindings
15721576
if hasKeyboardBindings and not bindingsActive then
15731577
DF:DebugWarn("CLICK", "HOVER BUT NO KB BINDINGS on %s! Key presses will go to action bar (phase=%d)", frameName, enterPhase)
15741578
if not wrapEnterFired then
15751579
DF:DebugWarn("CLICK", " WrapScript OnEnter DID NOT FIRE (enterCount=%d leaveCount=%d)", wrapEnterCount, wrapLeaveCount)
1580+
CC:RequestBindingRepair("wrap-not-firing")
15761581
DF:DebugWarn("CLICK", " frame visible=%s shown=%s mouseOver=%s combat=%s",
15771582
tostring(self:IsVisible()), tostring(self:IsShown()),
15781583
tostring(self:IsMouseOver()), tostring(InCombatLockdown()))
@@ -1590,6 +1595,17 @@ function CC:SetupSecureHandlers(frame)
15901595
tostring(self.dfKeyboardHandlersSetup), tostring(self.dfClickCastRegistered))
15911596
end
15921597

1598+
-- Self-heal detection: this frame's snippet is empty but the profile
1599+
-- has keyboard binds — likely wiped by a transient refresh (bug #976).
1600+
-- One-shot per frame: the flag is reset when a rebuild gives the frame
1601+
-- a real snippet again, so legitimately-empty frames don't loop.
1602+
if not hasKeyboardBindings and self.dfIsDandersFrame
1603+
and not self.dfSnippetRepairTried and CC:ProfileHasKeyboardBindings() then
1604+
self.dfSnippetRepairTried = true
1605+
DF:DebugWarn("CLICK", "EMPTY SNIPPET on %s but profile has keyboard binds", frameName)
1606+
CC:RequestBindingRepair("empty-snippet")
1607+
end
1608+
15931609
-- Warn if mouse click-cast attributes are missing
15941610
if not type1 or type1 == "" then
15951611
DF:DebugWarn("CLICK", "NO TYPE1 on %s - left-click won't cast!", frameName)
@@ -1758,6 +1774,39 @@ function CC:UpdateFrameBindingAttributes(frame)
17581774
-- Store the snippet - _onenter will run this on every hover
17591775
local snippet = table.concat(snippetLines, "\n")
17601776
frame:SetAttribute("dfBindingSnippet", snippet)
1777+
1778+
-- Frame has a real snippet again — allow the empty-snippet self-heal
1779+
-- detection to fire once more if it ever gets wiped (bug #976)
1780+
if snippet ~= "" then
1781+
frame.dfSnippetRepairTried = nil
1782+
end
1783+
end
1784+
1785+
-- Returns true if any enabled binding in the unified macro map needs the
1786+
-- hover snippet path (keyboard, scroll, or meta-mouse). Used by the
1787+
-- self-heal detection to tell "snippet legitimately empty" apart from
1788+
-- "snippet wiped". Cached per macro-map instance so hover hooks stay cheap.
1789+
function CC:ProfileHasKeyboardBindings()
1790+
if not self.unifiedMacroMap then return false end
1791+
if self.kbCheckMap == self.unifiedMacroMap then
1792+
return self.kbCheckResult
1793+
end
1794+
1795+
local result = false
1796+
for _, data in pairs(self.unifiedMacroMap) do
1797+
local binding = data.templateBinding
1798+
local bindType = binding.bindType or "mouse"
1799+
local isMetaMouse = (bindType == "mouse") and binding.modifiers
1800+
and binding.modifiers:lower():find("meta")
1801+
if bindType == "key" or bindType == "scroll" or isMetaMouse then
1802+
result = true
1803+
break
1804+
end
1805+
end
1806+
1807+
self.kbCheckMap = self.unifiedMacroMap
1808+
self.kbCheckResult = result
1809+
return result
17611810
end
17621811

17631812
-- Legacy alias for compatibility
@@ -1790,6 +1839,87 @@ function CC:RefreshKeyboardBindings()
17901839
self.pendingKeyboardRefresh = false
17911840
end
17921841

1842+
-- ============================================================
1843+
-- SELF-HEALING BINDING REPAIR (bug #976)
1844+
-- Keyboard hover-binds can die mid-session (stale mouseoverbutton in the
1845+
-- restricted environment, spurious state-driver clears, wiped snippets)
1846+
-- and previously only /reload recovered. The OnEnter/ticker diagnostics
1847+
-- already DETECT all of these; this repair path fixes them out of combat:
1848+
-- 1. reset the restricted env's mouseoverbutton tracking
1849+
-- 2. clear stray override bindings so keys can't stay stolen
1850+
-- 3. rebuild the macro map + every frame's binding snippet
1851+
-- All repair actions are insecure out-of-combat calls the addon already
1852+
-- uses elsewhere — no new taint surface. In combat the repair is queued
1853+
-- and OnCombatEnd runs it.
1854+
-- ============================================================
1855+
1856+
local REPAIR_COOLDOWN = 5 -- seconds between repair attempts
1857+
1858+
-- Safe to call from anywhere, including combat and secure-hook callbacks
1859+
function CC:RequestBindingRepair(reason)
1860+
if InCombatLockdown() then
1861+
if not self.pendingBindingRepair then
1862+
DF:DebugWarn("CLICK", "Binding repair queued for combat end (%s)", tostring(reason))
1863+
end
1864+
self.pendingBindingRepair = reason
1865+
return
1866+
end
1867+
self:RunBindingRepair(reason)
1868+
end
1869+
1870+
function CC:RunBindingRepair(reason, force)
1871+
if InCombatLockdown() then
1872+
self.pendingBindingRepair = reason
1873+
return
1874+
end
1875+
if not self.db or not self.db.enabled then return end
1876+
1877+
if not force then
1878+
if self.lastBindingRepair and (GetTime() - self.lastBindingRepair) < REPAIR_COOLDOWN then
1879+
return
1880+
end
1881+
end
1882+
self.lastBindingRepair = GetTime()
1883+
1884+
DF:DebugWarn("CLICK", "Running binding repair (%s)", tostring(reason))
1885+
1886+
-- 1. Reset restricted-env hover tracking. A stale mouseoverbutton
1887+
-- reference aborts every OnEnter WrapScript at the cross-frame
1888+
-- cleanup step, killing keyboard binds addon-wide until /reload.
1889+
if self.header and self.header.Execute then
1890+
pcall(function()
1891+
self.header:Execute([[ mouseoverbutton = nil ]])
1892+
end)
1893+
end
1894+
1895+
-- 2. Clear stray override bindings + hover state. After the env reset
1896+
-- the OnLeave wrap can no longer clear these (mouseoverbutton ~= self),
1897+
-- so clear them here or keys would stay stolen from the action bars.
1898+
local function scrub(frame)
1899+
if frame.GetAttribute and frame:GetAttribute("dfBindingsActive") then
1900+
pcall(ClearOverrideBindings, frame)
1901+
frame:SetAttribute("dfBindingsActive", nil)
1902+
frame:SetAttribute("dfIsSecureMouseover", nil)
1903+
end
1904+
end
1905+
if self.registeredFrames then
1906+
for frame in pairs(self.registeredFrames) do
1907+
scrub(frame)
1908+
end
1909+
end
1910+
if DF.unitFrames then
1911+
for _, frame in pairs(DF.unitFrames) do
1912+
scrub(frame)
1913+
end
1914+
end
1915+
1916+
-- 3. Rebuild the macro map and every frame's snippet, in case snippets
1917+
-- were wiped by a transient refresh. The next OnEnter re-applies
1918+
-- bindings from the fresh snippet.
1919+
self.unifiedMacroMap = self:BuildUnifiedMacroMap()
1920+
self:RefreshKeyboardBindings()
1921+
end
1922+
17931923
-- Legacy function - now calls RefreshKeyboardBindings
17941924
function CC:BuildKeyboardBindingSnippets()
17951925
self:RefreshKeyboardBindings()

0 commit comments

Comments
 (0)