mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-21 22:22:16 -05:00
Everything about how the controller behaves — paddle bindings, both trackpads, and which kind of pad the game sees — is now configured in one window and saved per game, rather than spread across tray checkboxes that applied everywhere at once. Installed games come from the Start Menu and the packaged-app list. Steam does not shortcut its library with .lnk files, so its games are read from the .url shortcuts it does write; Xbox and Store titles register no shortcut file at all and come from the shell's app list instead. None of these sources can tell a game from any other program, so the picker searches rather than scrolls — a saved profile is pinned, everything else is reached by typing. Trackpads are configured per pad. The previous pair of toggles could only ever describe one pad doing one thing, because a single TrackpadMouse instance was switched between pads by a bool; there are two now. A pad can move the pointer, scroll, feed the DS4 touchpad, or do nothing, and its click is an ordinary binding dispatched down the same path as the paddles — it used to be hardwired to a left click that only fired while that pad drove the mouse. Key bindings can carry Ctrl, Alt, Shift and Win. The modifiers ride in the free top byte of the packed binding, so values written by earlier builds decode unchanged. Windows is chosen from a toggle rather than pressed: the Start menu opens before the page is told anything, which is what made that key unbindable before. Choosing modifiers rather than typing them also keeps combinations like Alt+Tab from being claimed by the system mid-capture. Ctrl+Alt+Delete is refused outright, being the Secure Attention Sequence that no ordinary process can send. Profiles follow the foreground application, and nothing polls to do it. An unelevated process cannot subscribe to process-start events — the WMI class that reports them requires administrator rights, as does an ETW kernel session — but SetWinEventHook reports foreground changes for the cost of one hook and no thread. Focus is also the better question than "is it running": a launcher that starts a game and stays resident answers yes long after the game has exited. Steam profiles are keyed by app id, which a foreground window cannot supply, so Steam's manifests are read to map that id to an install directory; a game's launcher, its anti-cheat service and the game itself all live under it and all resolve to the same profile. Auto Mode still decides whether the controller is ours at all, so a profile only loads while we hold the pad. A balloon names the profile when one loads, under the notification setting, because a profile that quietly fails to match is otherwise invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4.6 KiB
C++
101 lines
4.6 KiB
C++
#pragma once
|
|
#include "BackButtonConfig.h"
|
|
#include "ControllerPlatform.h"
|
|
#include "TrackpadConfig.h"
|
|
#include <atomic>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
class ControllerManager {
|
|
public:
|
|
using StateChangedFn = std::function<void(bool connected, bool gameModeActive, bool vigemMissing)>;
|
|
// 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<void(const std::wstring& title, const std::wstring& text)>;
|
|
|
|
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<void(const BackButtonBinding&)> 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<std::unique_ptr<Slot>> m_slots;
|
|
bool m_steamPresent = false;
|
|
ControllerProfile m_profile;
|
|
|
|
std::atomic<bool> m_capturing{false};
|
|
std::mutex m_captureMutex;
|
|
std::function<void(const BackButtonBinding&)> m_captureCallback;
|
|
};
|