#pragma once #include "BackButtonConfig.h" #include "ControllerPlatform.h" #include "TrackpadConfig.h" #include #include #include #include #include #include class ControllerManager { public: using StateChangedFn = std::function; // User-facing alert (unexpected disconnect, stall). May fire from a read // thread — the receiver must marshal to its own thread (e.g. PostMessage). using AlertFn = std::function; explicit ControllerManager(StateChangedFn onStateChanged); ~ControllerManager(); ControllerManager(const ControllerManager&) = delete; ControllerManager& operator=(const ControllerManager&) = delete; void OnDeviceChange(); // Set once right after construction, before any game mode is enabled — // read loops copy it without locking. void SetAlertCallback(AlertFn fn) { m_alertFn = std::move(fn); } // Why game mode did not come up, so callers can tell an idle receiver from // a contested one. A puck publishes a slot interface whether or not a // controller is paired into it, and cycling the device because nothing is // switched on is pointless churn. enum class GameModeOutcome { Enabled, NoActiveController, Blocked }; // stateWaitMs is how long each slot is given to prove it is live. Short // values are for polling a receiver whose controller is switched off — // a live slot streams continuously, so it answers almost immediately. GameModeOutcome EnableGameMode(uint32_t stateWaitMs = 250); void DisableGameMode(); // Disables game mode then closes all device handles so another process // (e.g. Steam) can claim the controller. Safe to call when already disabled. void ReleaseDevices(); // Best-effort lizard restore for crash paths — called from the unhandled- // exception filter installed by the constructor. Takes no locks and joins // no threads. The firmware's own revert timeout remains the final // fallback if this fails or the process dies harder than a C++ exception. void EmergencyRestoreAll() noexcept; // Whether steam.exe is currently running. Decides if game mode may settle // for a shared handle: a write handle held while Steam is absent belongs to // some benign system component, but one held while Steam is running is // probably Steam itself, and driving the controller alongside it is worse // than refusing — refusing is what escalates to a device cycle. void SetSteamPresent(bool present) { m_steamPresent = present; } // The whole active profile — platform, paddle bindings and both pads. // Mostly applied live: the read loop reads the profile every frame, so // bindings land on the next report. A changed platform is the exception — // it rebuilds the virtual controller, see the definition. void SetProfile(const ControllerProfile& profile); bool IsConnected() const { return !m_slots.empty(); } bool IsGameModeActive() const; const ControllerProfile& GetProfile() const { return m_profile; } // Called by RemapWindow when a row enters/exits listening state. // Callback fires on the read thread — use PostMessage to marshal to the UI thread. void StartButtonCapture(std::function callback); void StopButtonCapture(); private: struct Slot; void SyncDevices(); void OpenSlot(const std::wstring& path); GameModeOutcome EnableGameModeSlot(Slot& slot, bool& vigemMissingOut, uint32_t stateWaitMs); void DisableGameModeSlot(Slot& slot); void ReleaseHeldPaddleInputs(Slot& slot); void StartReadLoop(Slot& slot); void StopReadLoop(Slot& slot); void ReadLoop(Slot* slot); void NotifyStateChanged(bool vigemMissing = false); // Pushes the current profile's pad settings into one slot's trackpads and // its virtual controller. Shared by SetProfile and slot creation. void ApplyPadSettings(Slot& slot); StateChangedFn m_onStateChanged; AlertFn m_alertFn; std::vector> m_slots; bool m_steamPresent = false; ControllerProfile m_profile; std::atomic m_capturing{false}; std::mutex m_captureMutex; std::function m_captureCallback; };