Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .context/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Suggested buckets (map to existing rules):
## CombatMode-specific reminders

- **Mouselook / free-look / CVars:** free-look state machine lives in `Core/FreeLookController.lua`; **all addon-owned `SetCVar` writes route through** `Core/RuntimeCVarManager.lua` (`CM.SetCVar*` helpers). Modules should compute/decide values locally, then call the manager to perform the write. Keep enable/disable paths symmetric.
- **macOS stuck-cursor freeze:** a `MouselookStop()` issued while the window is backgrounded (auto-unlock from a watched frame during alt-tab) leaves the OS mouse capture stuck — frozen cursor/camera on return while `IsMouselooking()` lies. No focus event exists. `CM.ResyncMouselookOnFocusRegain` recovers the case where frames halt while backgrounded (fullscreen); when the window keeps rendering (windowed background FPS), `OnUpdate` can't detect it, so the `Combat Mode - Reset Mouse Look` keybind (`CombatMode_ResetMouseLook`, FreeLookController) re-grabs+releases as manual recovery.
- **Override bindings / secure buttons:** follow existing `SecureActionButtonTemplate` and `Core/BindingOverrides.lua` patterns.
- **Vendored libs:** do not rewrite `CombatMode/Libs/**`; see `.cursor/rules/combatmode-vendored-libs.mdc`.

Expand Down
3 changes: 3 additions & 0 deletions CombatMode/Bindings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
CombatMode_CursorModeKey(keystate)
end
</Binding>
<Binding name="Combat Mode - Reset Mouse Look" description="Recover the cursor if it gets stuck after switching windows (re-grabs and releases Mouse Look)" category="|A:::|a|TInterface\Addons\CombatMode\assets\cmtitle:22:95|t">
CombatMode_ResetMouseLook()
</Binding>
<Binding name="Combat Mode - Healing Radial" description="Hold to open Healing Radial for party targeting" header="Healing Radial" category="|A:::|a|TInterface\Addons\CombatMode\assets\cmtitle:22:95|t" runOnUp="true">
if keystate == "down" or keystate == "up" then
CombatMode_HealingRadialKey(keystate)
Expand Down
20 changes: 20 additions & 0 deletions CombatMode/Config/ConfigMouseLook.lua
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,26 @@ CM.Config.MouseLookOptions = {
return (GetBindingKey("INTERACTMOUSEOVER"))
end,
},
resetMouseLook = {
type = "keybinding",
name = "|cffffd700Reset - |cffE52B50Mouse Look|r|r",
desc = "Recovers a stuck cursor/camera that can happen on |cffffd700macOS|r: when a panel (bags, |cffffd700LFG|r ready check, etc.) turns |cffE52B50Mouse Look|r off while |cffffd700WoW|r is in the background, the |cffffd700OS|r mouse capture is left stuck. On returning the camera is frozen and the cursor is gone.\n\nPress this to re-grab and release the camera, bringing the cursor back.\n\n|cff909090This can't be done automatically: the game exposes no window-focus event and keeps rendering while in the background, so the addon has no way to detect the frozen state from the inside. A manual keypress is the only reliable recovery.|r",
width = 1.15,
order = 5.05,
set = function(_, key)
CM.TryApplyBindingChange("reset mouse look keybinding", function()
local oldKey = (GetBindingKey("Combat Mode - Reset Mouse Look"))
if oldKey then
SetBinding(oldKey)
end
SetBinding(key, "Combat Mode - Reset Mouse Look")
SaveBindings(GetCurrentBindingSet())
end)
end,
get = function()
return (GetBindingKey("Combat Mode - Reset Mouse Look"))
end,
},
spacing = Spacing("full", 5.1),
pulseCursor = {
type = "toggle",
Expand Down
34 changes: 24 additions & 10 deletions CombatMode/Core/AutoCursorUnlock.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ local function CursorUnlockFrameVisible(frameArr)
return false
end

for _, frameName in pairs(frameArr) do
for _, frameName in ipairs(frameArr) do
local curFrame = _G[frameName]
if curFrame and curFrame.IsVisible and curFrame:IsVisible() then
CM.DebugPrintThrottled("cursorUnlock", frameName .. " is visible, preventing re-locking.")
return true
end
end

return false
end

local function CursorUnlockFrameGroupVisible(frameNameGroups)
Expand All @@ -57,31 +59,43 @@ local function CursorUnlockFrameGroupVisible(frameNameGroups)
return true
end
end

return false
end

function CM.IsUnlockFrameVisible()
local isGenericPanelOpen = (GetUIPanel("left") or GetUIPanel("right") or GetUIPanel("center"))
and true
or false
if GetUIPanel("left") or GetUIPanel("right") or GetUIPanel("center") then
return true
end

return CursorUnlockFrameVisible(CM.Constants.FramesToCheck)
or CursorUnlockFrameVisible(CM.DB.global.watchlist)
or CursorUnlockFrameGroupVisible(CM.Constants.WildcardFramesToCheck)
or isGenericPanelOpen
end

local compiledCustomConditionSource = nil
local compiledCustomConditionFunc = nil

function CM.IsCustomConditionTrue()
if not CM.DB.global.customCondition then
local source = CM.DB.global.customCondition
if not source or source == "" then
return false
end

local func, err = loadstring(CM.DB.global.customCondition)
if source ~= compiledCustomConditionSource then
compiledCustomConditionSource = source
local func, err = loadstring(source)
compiledCustomConditionFunc = func
if not func then
CM.DebugPrint("Invalid custom condition " .. err)
end
end

if not func then
CM.DebugPrint("Invalid custom condition " .. err)
if not compiledCustomConditionFunc then
return false
end

local success, result = pcall(func)
local success, result = pcall(compiledCustomConditionFunc)

if not success then
CM.DebugPrint("Error executing custom condition: " .. result)
Expand Down
72 changes: 72 additions & 0 deletions CombatMode/Core/FreeLookController.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ local CM = LibStub("AceAddon-3.0"):GetAddon("CombatMode")
-- WoW API
local C_Timer = _G.C_Timer
local GameTooltip = _G.GameTooltip
local GetFramerate = _G.GetFramerate
local GetTime = _G.GetTime
local InCinematic = _G.InCinematic
local IsInCinematicScene = _G.IsInCinematicScene
Expand All @@ -24,6 +25,7 @@ local IsMouselooking = _G.IsMouselooking
local MouselookStart = _G.MouselookStart
local MouselookStop = _G.MouselookStop
local SpellIsTargeting = _G.SpellIsTargeting
local time = _G.time

-- Lua stdlib
local string = _G.string
Expand All @@ -32,6 +34,13 @@ local string = _G.string
local FreeLookOverride = false -- Changes when Free Look state is modified through user input ("Toggle / Hold" keybind and "/cm" cmd)
local CursorModeShowTime = 0 -- GetTime() when cursor was unlocked via keybind (for spurious key-up filter)

local FOCUS_REGAIN_GAP_SECONDS = 0.5
local FOCUS_REGAIN_WALLCLOCK_SECONDS = 2
local FOCUS_REGAIN_MIN_FPS = 6
local lastOnUpdateRealTime = 0
local lastOnUpdateWallTime = 0
local wasBackgrounded = false

-- This prevents the auto running bug.
function CM.IsDefaultMouseActionBeingUsed()
return IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton")
Expand Down Expand Up @@ -218,3 +227,66 @@ function _G.CombatMode_CursorModeKey(keystate)
FreeLookOverride = false
end
end

-- Manual recovery for the macOS stuck-cursor freeze (a MouselookStop issued while the
-- window was backgrounded leaves the OS capture stuck). Re-grab then release to clear it;
-- OnUpdate re-locks afterward if free look should still be active.
function _G.CombatMode_ResetMouseLook()
MouselookStart()
if C_Timer and C_Timer.After then
C_Timer.After(0, MouselookStop)
else
MouselookStop()
end
end

-- macOS: a MouselookStart/Stop issued while the window is backgrounded (e.g. an LFG popup
-- auto-unlocks Mouse Look during alt-tab) leaves the OS mouse capture desynced — frozen
-- camera/input on return. No focus event exists, so a slow/halted frame is treated as
-- "backgrounded"; on the first normal frame after one, force a clean mouselook transition
-- to the intended state (Stop→Start to re-grab, or Start→Stop to clear a stuck capture).
local function FrameLooksBackgrounded(realGap, wallGap)
if realGap >= FOCUS_REGAIN_GAP_SECONDS or wallGap >= FOCUS_REGAIN_WALLCLOCK_SECONDS then
return true
end
return (GetFramerate and GetFramerate() or 60) < FOCUS_REGAIN_MIN_FPS
end

function CM.ResyncMouselookOnFocusRegain()
local now, nowWall = GetTime(), time()
local prev, prevWall = lastOnUpdateRealTime, lastOnUpdateWallTime
lastOnUpdateRealTime, lastOnUpdateWallTime = now, nowWall
if prev == 0 or prevWall == 0 then
return
end

if FrameLooksBackgrounded(now - prev, nowWall - prevWall) then
wasBackgrounded = true
return
end
if not wasBackgrounded then
return
end
wasBackgrounded = false

local shouldLook = not (FreeLookOverride or CM.ShouldFreeLookBeOff())
if shouldLook then
MouselookStop()
else
MouselookStart()
end
local function settle()
if shouldLook then
if not (FreeLookOverride or CM.ShouldFreeLookBeOff()) then
MouselookStart()
end
else
MouselookStop()
end
end
if C_Timer and C_Timer.After then
C_Timer.After(0, settle)
else
settle()
end
end
2 changes: 2 additions & 0 deletions CombatMode/Core/Runtime.lua
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ This is (in most cases) extremely excessive, hence why we're adding a throttle.
local ON_UPDATE_INTERVAL = 0.15
local TIME_SINCE_LAST_UPDATE = 0
function _G.CombatMode_OnUpdate(_, elapsed)
CM.ResyncMouselookOnFocusRegain()

-- Making this thread-safe by keeping track of the last update cycle
TIME_SINCE_LAST_UPDATE = TIME_SINCE_LAST_UPDATE + elapsed

Expand Down