mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-22 06:32:20 -05:00
Manual mode could not take the controller back from a running Steam. The tray toggle called ControllerManager::EnableGameMode() once and discarded the result, so a Blocked outcome -- Steam holding the exclusive write handle -- was simply dropped. The auto modes wrapped the same call in TryAcquireController(), which escalates a blocked claim to a device cycle via the elevated helper and retries on re-arrival. Only closing Steam freed the pad in manual mode. The two paths diverged because acquiring is asynchronous: a blocked claim fires a cycle through Task Scheduler and comes back seconds later as a WM_DEVICECHANGE, so the retry loop lives in timers and message handlers rather than on the stack. Each of those re-entry points has to re-answer "do we still want control?", and the code answered it by recomputing m_autoMode != Manual && WantControl(steamState) -- deriving intent from Steam. That works in the auto modes, where Steam's state is the intent. Manual mode had nothing to derive from: its intent lived in a menu click that was never recorded anywhere. WantControl() said as much, returning false for Manual with the comment "tray toggle decides" -- and the tray toggle's decision went unstored. Store it. m_wantControl replaces the derived guard at all three re-entry points (the IDT_ACQUIRE and IDT_WAKE_POLL timers, and the arrival branch of WM_DEVICECHANGE), leaving WantControl() with a single caller in ApplySteamState. The toggle sets it and calls the shared path, so manual mode gets the retry burst, device cycling, wake polling, and the failure balloon it never had. Release is unified the same way: ReleaseControl() lifts the yield branch out of ApplySteamState, substituting m_steamWatcher.GetState() for the SteamState parameter the manual caller has no access to. Manual toggle-off now also cycles the devnode, so a running Steam sees an arrival event and re-adopts the pad instead of leaving it unclaimed. Three follow-on details: - Manual toggle-on calls EnsureCycleTaskRegistered(), matching what IDM_MODE_GAME already does, so the one-time UAC prompt lands on a menu click rather than ambushing the first cycle. - SetAutoMode(Manual) adopts IsGameModeActive() rather than inheriting a stale m_wantControl from the auto mode being left. - The toggle's menu label tracks m_wantControl in manual mode, not the live handle state. Acquisition can spend several seconds cycling, and a second click during that window should cancel rather than start over. Both AUTO: log lines in the acquire path are now reachable from manual mode, so they are relabelled ACQUIRE: -- these are exactly the lines read when diagnosing a failed handoff, and the old prefix pointed away from it. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>