diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a2e9ca..b6905b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ add_executable(SteamlessController WIN32 src/app/VirtualController.cpp src/app/TrackpadMouse.cpp src/app/KeyInput.cpp + src/app/InputInjection.cpp src/app/SteamWatcher.cpp src/app/DeviceRestart.cpp src/app/EventLog.cpp diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp index 83493c7..200a8bb 100644 --- a/src/app/ControllerManager.cpp +++ b/src/app/ControllerManager.cpp @@ -1,5 +1,6 @@ #include "ControllerManager.h" #include "EventLog.h" +#include "InputInjection.h" #include "VirtualController.h" #include "TrackpadMouse.h" #include "KeyInput.h" @@ -300,7 +301,7 @@ static bool SendPaddleInput(const BackButtonBinding& binding, bool down) { INPUT inp{}; inp.type = INPUT_MOUSE; inp.mi.dwFlags = flags; - SendInput(1, &inp, sizeof(INPUT)); + InputInjection::Send(inp, "paddle-mouse"); }; switch (binding.kind) { @@ -327,7 +328,7 @@ static bool SendPaddleInput(const BackButtonBinding& binding, bool down) { default: return false; } - SendInput(1, &inp, sizeof(INPUT)); + InputInjection::Send(inp, "paddle-mouse"); return true; } @@ -514,6 +515,11 @@ ControllerManager::EnableGameModeSlot(Slot& slot, bool& vigemMissingOut, slot.vc->SetTrackpadMouseClaim(m_trackpadMouseEnabled, m_useLeftTrackpad); EventLog::Write("GAMEMODE: enabled %ls", slot.path.c_str()); + // Takeover is the moment users report the trackpad arriving dead, and what + // decides whether injection can land is whatever window happens to be in + // front right then — so record it here rather than reconstructing it later. + if (m_trackpadMouseEnabled) + InputInjection::LogEnvironment("game mode taken"); slot.gameModeActive = true; slot.trackpad.Reset(); ReleaseHeldPaddleInputs(slot); @@ -557,6 +563,7 @@ void ControllerManager::ReadLoop(Slot* slot) { uint8_t buf[64]; uint8_t prevBuf[64] = {}; bool hasPrev = false; + bool loggedShape = false; auto lastKeepalive = std::chrono::steady_clock::now(); auto lastReport = std::chrono::steady_clock::now(); bool stalled = false; @@ -637,6 +644,22 @@ void ControllerManager::ReadLoop(Slot* slot) { if (!SteamController::IsStateReportId(buf[0])) continue; + // Report shape, once per read loop. The trackpad mouse needs a longer + // report than the haptics do, so a transport that reports short loses + // cursor movement while every buzz still fires — the same thing a + // blocked SendInput looks like from the user's side, and only this + // line tells them apart. + if (!loggedShape) { + loggedShape = true; + EventLog::Write("REPORT: state id=0x%02X len=%zu (transport=%s)", + buf[0], n, + SteamController::TransportName(slot->transport)); + if (n < 30) + EventLog::Write("REPORT: %zu bytes is under the 30 the trackpad " + "mouse requires — trackpad movement is being " + "dropped before it reaches SendInput", n); + } + if (slot->vc) slot->vc->Update(buf, n, m_backConfig); slot->trackpad.Update(buf, n); diff --git a/src/app/EventLog.h b/src/app/EventLog.h index a5cee8f..61f0487 100644 --- a/src/app/EventLog.h +++ b/src/app/EventLog.h @@ -1,4 +1,5 @@ #pragma once +#include #include // Persistent diagnostic event log for support and field debugging: device @@ -9,7 +10,12 @@ // Thread-safe. printf-style formatting; use %ls for wide strings (paths). namespace EventLog { -void Write(const char* fmt, ...); +// Annotated so the compiler type-checks every format string against its +// arguments. These lines are read long after the fact, by whoever is trying to +// explain a fault they cannot reproduce; a specifier that does not match its +// argument corrupts the one record of what happened, and the paths that log +// least often — failures — are exactly the ones no test run exercises. +void Write(_Printf_format_string_ const char* fmt, ...); // Full path of the current log file (for "Open Event Log" in the tray menu). std::wstring FilePath(); diff --git a/src/app/InputInjection.cpp b/src/app/InputInjection.cpp new file mode 100644 index 0000000..cd72127 --- /dev/null +++ b/src/app/InputInjection.cpp @@ -0,0 +1,362 @@ +#include "InputInjection.h" +#include "EventLog.h" +#include +#include +#include +#include + +namespace { + +// A wedged foreground window refuses every event at report rate, so the log +// gets one line per burst rather than one per event. +constexpr uint64_t kLogGapMs = 5000; + +// The balloon is a warning, not a running commentary: alt-tabbing in and out of +// an elevated window must not produce one notification per trip. +constexpr uint64_t kAlertGapMs = 60000; + +constexpr DWORD kIntegrityUnknown = 0xFFFFFFFFu; + +std::atomic g_refused{0}; // refusals since the last summary +std::atomic g_refusedTotal{0}; // refusals since the last recovery +std::atomic g_lastRefusalLogMs{0}; +std::atomic g_lastStuckLogMs{0}; +std::atomic g_refusing{false}; + +// True at most once per kLogGapMs, tracked per call site. +bool DueForLog(std::atomic& lastMs) { + const uint64_t now = GetTickCount64(); + uint64_t last = lastMs.load(std::memory_order_relaxed); + if (last != 0 && now - last < kLogGapMs) return false; + // A racing thread that wins here logs instead of this one; either way the + // line is written once. + return lastMs.compare_exchange_strong(last, now, std::memory_order_relaxed); +} + +const wchar_t* IntegrityName(DWORD rid) { + if (rid == kIntegrityUnknown) return L"unreadable"; + if (rid >= SECURITY_MANDATORY_SYSTEM_RID) return L"System"; + if (rid >= SECURITY_MANDATORY_HIGH_RID) return L"High/elevated"; + if (rid >= SECURITY_MANDATORY_MEDIUM_RID) return L"Medium"; + if (rid >= SECURITY_MANDATORY_LOW_RID) return L"Low"; + return L"Untrusted"; +} + +DWORD ProcessIntegrity(HANDLE process) { + HANDLE token = nullptr; + if (!OpenProcessToken(process, TOKEN_QUERY, &token)) return kIntegrityUnknown; + + DWORD size = 0; + GetTokenInformation(token, TokenIntegrityLevel, nullptr, 0, &size); + + DWORD rid = kIntegrityUnknown; + if (size > 0) { + std::vector buf(size); + auto* label = reinterpret_cast(buf.data()); + if (GetTokenInformation(token, TokenIntegrityLevel, label, size, &size)) { + const UCHAR* count = GetSidSubAuthorityCount(label->Label.Sid); + if (count && *count > 0) { + rid = *GetSidSubAuthority(label->Label.Sid, + static_cast(*count - 1)); + } + } + } + CloseHandle(token); + return rid; +} + +// This process's own level never changes, so it is worth asking once. +DWORD OwnIntegrity() { + static const DWORD own = ProcessIntegrity(GetCurrentProcess()); + return own; +} + +// --------------------------------------------------------------------------- +// Blocked-state tracking +// --------------------------------------------------------------------------- + +std::mutex g_stateMutex; +InputInjection::AlertFn g_alertFn; // guarded by g_stateMutex +std::wstring g_blockerLogged; // blocker named in the log right now +std::wstring g_alertBlocker; // blocker the last balloon named +uint64_t g_lastAlertMs = 0; + +// Foreground lookups are cached against the window they describe: injection +// happens at report rate, foreground changes do not, and a process cannot +// change integrity level while it lives. +HWND g_cachedHwnd = nullptr; // guarded by g_stateMutex +DWORD g_cachedPid = 0; +bool g_cachedBlocked = false; +std::wstring g_cachedName; + +// Exe name of the foreground window's process when it outranks this one, so +// anything injected now is discarded. Empty when injection can land. +// +// A process that cannot be opened at all — protected processes, in practice +// anti-cheat — is deliberately NOT called blocked: its level is unknown, and +// guessing would put a wrong name in front of the user. The stuck-cursor check +// still catches it, and LogEnvironment records that the process was unreadable. +std::wstring ForegroundBlocker(DWORD& pidOut) { + pidOut = 0; + + const HWND fg = GetForegroundWindow(); + if (!fg) return L""; // secure desktop or lock screen; nothing to name + + DWORD pid = 0; + GetWindowThreadProcessId(fg, &pid); + if (pid == 0 || pid == GetCurrentProcessId()) return L""; + pidOut = pid; + + std::lock_guard lk(g_stateMutex); + if (fg == g_cachedHwnd && pid == g_cachedPid) + return g_cachedBlocked ? g_cachedName : std::wstring(); + + g_cachedHwnd = fg; + g_cachedPid = pid; + g_cachedBlocked = false; + g_cachedName.clear(); + + HANDLE proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!proc) return L""; + + const DWORD integrity = ProcessIntegrity(proc); + + wchar_t image[MAX_PATH] = L""; + DWORD imageLen = MAX_PATH; + if (!QueryFullProcessImageNameW(proc, 0, image, &imageLen)) + wcscpy_s(image, L"(unknown)"); + CloseHandle(proc); + + const DWORD own = OwnIntegrity(); + if (integrity == kIntegrityUnknown || own == kIntegrityUnknown + || integrity <= own) + return L""; + + const wchar_t* leaf = wcsrchr(image, L'\\'); + g_cachedName = leaf ? leaf + 1 : image; + g_cachedBlocked = true; + return g_cachedName; +} + +// Records the blocked state changing: one log line per blocker rather than per +// event, and a balloon the first time each blocker gets in the way. +void NoteBlockState(const std::wstring& blocker, DWORD pid, const char* what) { + std::wstring startedBlocking, stoppedBlocking; + InputInjection::AlertFn alert; + + { + std::lock_guard lk(g_stateMutex); + if (!blocker.empty()) { + if (blocker != g_blockerLogged) { + startedBlocking = blocker; + g_blockerLogged = blocker; + + const uint64_t now = GetTickCount64(); + if (blocker != g_alertBlocker || now - g_lastAlertMs >= kAlertGapMs) { + g_alertBlocker = blocker; + g_lastAlertMs = now; + alert = g_alertFn; + } + } + } else if (!g_blockerLogged.empty()) { + stoppedBlocking = g_blockerLogged; + g_blockerLogged.clear(); + } + } + + if (!startedBlocking.empty()) { + EventLog::Write("INJECT: BLOCKED (%s) — the foreground window belongs to " + "%ls (pid=%lu), which runs at a higher integrity level; " + "Windows accepts each event and then discards it", + what, startedBlocking.c_str(), pid); + InputInjection::LogEnvironment("state while blocked"); + } + if (!stoppedBlocking.empty()) + EventLog::Write("INJECT: unblocked — %ls is no longer the foreground window", + stoppedBlocking.c_str()); + + // Outside the lock: the callback is the tray's, and it is not this + // module's business what it does before returning. + if (alert) { + std::wstring text = startedBlocking; + text += L" is running as administrator, and Windows does not let " + L"SteamlessController send input while it is the active window. " + L"The trackpad and buttons work again as soon as you switch to " + L"another window or close it."; + alert(L"Controller input is being blocked", text); + } +} + +// "explorer.exe pid=1234 integrity=Medium", or why that could not be answered. +// A process this app cannot open at all is a finding in itself: protected +// processes (anti-cheat, in practice) refuse even limited query access, and +// those are also the processes most likely to be filtering injected input. +std::wstring DescribeForeground(DWORD& integrityOut) { + integrityOut = kIntegrityUnknown; + + const HWND fg = GetForegroundWindow(); + if (!fg) { + // No foreground window at all: a UAC prompt or the lock screen has + // taken the input desktop, and nothing injected reaches the user's + // session until it goes away. + return L"(none — no foreground window; a UAC prompt, the lock screen " + L"or a desktop switch owns input)"; + } + + DWORD pid = 0; + GetWindowThreadProcessId(fg, &pid); + + HANDLE proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!proc) { + wchar_t out[128]; + swprintf_s(out, L"pid=%lu (cannot open, err=%lu — protected process?)", + pid, GetLastError()); + return out; + } + + wchar_t image[MAX_PATH] = L""; + DWORD imageLen = MAX_PATH; + if (!QueryFullProcessImageNameW(proc, 0, image, &imageLen)) + wcscpy_s(image, L"(unknown)"); + + // Leaf name only — the full path adds nothing here and can carry the + // user's name in it. + const wchar_t* leaf = wcsrchr(image, L'\\'); + leaf = leaf ? leaf + 1 : image; + + integrityOut = ProcessIntegrity(proc); + CloseHandle(proc); + + wchar_t out[256]; + swprintf_s(out, L"%s pid=%lu integrity=%s", + leaf, pid, IntegrityName(integrityOut)); + return out; +} + +// The cursor clip, when a window has narrowed it. Games set this and do not +// always restore it, which pins the cursor while injection still succeeds. +std::wstring DescribeClip() { + RECT clip{}; + if (!GetClipCursor(&clip)) { + wchar_t out[64]; + swprintf_s(out, L"unreadable (err=%lu)", GetLastError()); + return out; + } + + RECT screen{}; + screen.left = GetSystemMetrics(SM_XVIRTUALSCREEN); + screen.top = GetSystemMetrics(SM_YVIRTUALSCREEN); + screen.right = screen.left + GetSystemMetrics(SM_CXVIRTUALSCREEN); + screen.bottom = screen.top + GetSystemMetrics(SM_CYVIRTUALSCREEN); + // The bounds go in either way: a cursor sitting on an edge of the desktop + // cannot move further that way, which reads as "stuck" without them. + wchar_t out[160]; + if (EqualRect(&clip, &screen)) { + swprintf_s(out, L"none (screen is (%ld,%ld)-(%ld,%ld))", + screen.left, screen.top, screen.right, screen.bottom); + } else { + swprintf_s(out, L"CONFINED to (%ld,%ld)-(%ld,%ld) of screen " + L"(%ld,%ld)-(%ld,%ld)", + clip.left, clip.top, clip.right, clip.bottom, + screen.left, screen.top, screen.right, screen.bottom); + } + return out; +} + +std::wstring DescribeCursor() { + POINT pt{}; + if (!GetCursorPos(&pt)) { + // Fails when this thread's desktop is not the input desktop — the + // session is locked, or a secure desktop is up. + wchar_t out[96]; + swprintf_s(out, L"unreadable (err=%lu — not on the input desktop)", + GetLastError()); + return out; + } + wchar_t out[64]; + swprintf_s(out, L"(%ld,%ld)", pt.x, pt.y); + return out; +} + +} // namespace + +namespace InputInjection { + +void LogEnvironment(const char* reason) { + DWORD fgIntegrity = kIntegrityUnknown; + const std::wstring fg = DescribeForeground(fgIntegrity); + const DWORD own = OwnIntegrity(); + const std::wstring clip = DescribeClip(); + const std::wstring cur = DescribeCursor(); + + const bool outranked = fgIntegrity != kIntegrityUnknown + && own != kIntegrityUnknown + && fgIntegrity > own; + + EventLog::Write("INJECT: %s — foreground=%ls, us=integrity=%ls%s, " + "cursorClip=%ls, cursor=%ls", + reason, fg.c_str(), IntegrityName(own), + outranked ? " — FOREGROUND OUTRANKS US, injected input is " + "blocked (UIPI) until focus moves or it closes" + : "", + clip.c_str(), cur.c_str()); +} + +void SetAlertCallback(AlertFn fn) { + std::lock_guard lk(g_stateMutex); + g_alertFn = std::move(fn); +} + +bool Send(const INPUT& input, const char* what) { + // Asked before sending, and sent either way: the check is what reports the + // fault, not a gate on the event. If it is ever wrong about a system, the + // input still goes out exactly as it did before. + DWORD blockerPid = 0; + const std::wstring blocker = ForegroundBlocker(blockerPid); + NoteBlockState(blocker, blockerPid, what); + + INPUT copy = input; // SendInput takes a non-const array + if (SendInput(1, ©, sizeof(INPUT)) == 1) { + if (g_refusing.exchange(false)) { + EventLog::Write("INJECT: accepted again after %llu refused event(s)", + g_refusedTotal.exchange(0)); + g_refused.store(0); + g_lastRefusalLogMs.store(0); + } + return true; + } + + const DWORD err = GetLastError(); + g_refusing.store(true); + g_refusedTotal.fetch_add(1); + const uint64_t burst = g_refused.fetch_add(1) + 1; + + if (DueForLog(g_lastRefusalLogMs)) { + g_refused.store(0); + EventLog::Write("INJECT: SendInput REFUSED (%s, err=%lu%s) — %llu event(s) " + "dropped since the last line", + what, err, + err == ERROR_ACCESS_DENIED ? " ACCESS_DENIED" : "", + burst); + LogEnvironment("state at refusal"); + } + return false; +} + +void LogCursorNotMoving(long travelPx, POINT at) { + { + // A known blocker already accounts for a still cursor, and has said so + // in better terms. What is left for this check is the causes nothing + // else can see: a cursor clip, or a hook eating injected input. + std::lock_guard lk(g_stateMutex); + if (!g_blockerLogged.empty()) return; + } + if (!DueForLog(g_lastStuckLogMs)) return; + EventLog::Write("INJECT: %ld px of cursor movement accepted but the cursor " + "has not moved from (%ld,%ld) — something is discarding " + "injected motion (cursor clip, or a low-level hook)", + travelPx, at.x, at.y); + LogEnvironment("state while cursor is stuck"); +} + +} // namespace InputInjection diff --git a/src/app/InputInjection.h b/src/app/InputInjection.h new file mode 100644 index 0000000..db2f34c --- /dev/null +++ b/src/app/InputInjection.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include + +// Synthetic input, with the reasons it can fail recorded. +// +// Every key, mouse button and cursor move this app produces leaves through +// SendInput, and SendInput is the one stage of the pipeline Windows can refuse +// without any other symptom: haptics and the virtual pad are HID writes and +// keep working regardless. A refused injection therefore looks exactly like +// "the trackpad stopped moving the mouse, but the pad still buzzes", with +// nothing in the log to say why. +// +// The refusal that matters is UIPI: a process may not inject input while the +// foreground window belongs to a process at a higher integrity level, so an +// elevated game, launcher or tool in the foreground silently swallows +// everything this app sends until focus moves elsewhere or that window closes. +// This app never runs elevated (see SteamlessDeviceCycle) precisely so that it +// is not the one doing the blocking, which leaves it on the receiving end. +// +// The refusal is not visible in SendInput's result: on Windows 11 the call +// reports success and the event is discarded afterwards, so the only reliable +// way to know is to ask, before injecting, whether the foreground window +// outranks this process. Send does that on every event and reports the state +// changing — which covers cursor movement, trackpad clicks, paddle buttons and +// keys alike, none of which can be checked from their own return value. +// +// Injection can also be accepted and still do nothing for reasons that are not +// UIPI — a game that left the cursor clipped, or a low-level hook dropping +// injected events — so movement is checked against the cursor actually moving +// as well. +namespace InputInjection { + +// How the app tells the user something is wrong (the tray balloon). Called from +// a controller read thread, so the callback must marshal to the UI thread — the +// tray's existing alert path already does. +using AlertFn = std::function; +void SetAlertCallback(AlertFn fn); + +// SendInput for a single event. 'what' is a short tag naming the caller +// ("trackpad-move", "trackpad-click", "paddle-mouse", "key") so a log line can +// say what was lost. Returns whether Windows accepted the event — which is not +// the same as the event arriving; see above. +bool Send(const INPUT& input, const char* what); + +// Everything that decides whether injection can land: the foreground window's +// process and integrity level, ours, any cursor clip, and whether the cursor +// can be read at all. Logged when game mode is taken, and whenever injection +// is refused. 'reason' leads the line. +void LogEnvironment(const char* reason); + +// Injection was accepted but the cursor did not move. 'travelPx' is how much +// movement was sent while the cursor sat at 'at'. Rate-limited like refusals. +void LogCursorNotMoving(long travelPx, POINT at); + +} // namespace InputInjection diff --git a/src/app/KeyInput.cpp b/src/app/KeyInput.cpp index 80fd15f..8765da9 100644 --- a/src/app/KeyInput.cpp +++ b/src/app/KeyInput.cpp @@ -1,4 +1,5 @@ #include "KeyInput.h" +#include "InputInjection.h" #include #include @@ -149,7 +150,7 @@ void SendKeyInput(uint16_t vk, bool down) { input.ki.dwFlags = KEYEVENTF_SCANCODE; if (sc.extended) input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; if (!down) input.ki.dwFlags |= KEYEVENTF_KEYUP; - SendInput(1, &input, sizeof(INPUT)); + InputInjection::Send(input, "key"); } std::chrono::milliseconds KeyRepeatDelay() { diff --git a/src/app/TrackpadMouse.cpp b/src/app/TrackpadMouse.cpp index 5a62d01..9c358b0 100644 --- a/src/app/TrackpadMouse.cpp +++ b/src/app/TrackpadMouse.cpp @@ -1,6 +1,8 @@ #include "TrackpadMouse.h" +#include "InputInjection.h" #include "steam/SteamController.h" #include +#include #include #include @@ -8,7 +10,7 @@ static void SendMouseButton(DWORD flags) { INPUT input{}; input.type = INPUT_MOUSE; input.mi.dwFlags = flags; - SendInput(1, &input, sizeof(INPUT)); + InputInjection::Send(input, "trackpad-click"); } void TrackpadMouse::Reset() { @@ -19,6 +21,35 @@ void TrackpadMouse::Reset() { m_prevY = 0; m_remX = 0.0f; m_remY = 0.0f; + m_haveRef = false; + m_travelSent = 0; +} + +// Tracks whether accepted movement is reaching the cursor. Re-arms whenever the +// cursor does move, so only a genuinely pinned cursor accumulates travel. +void TrackpadMouse::NoteMovementSent(long px, bool haveCursor, + long cursorX, long cursorY) { + if (!haveCursor) { + // No cursor position to compare against — GetCursorPos itself fails + // off the input desktop, and InputInjection reports that separately. + m_haveRef = false; + m_travelSent = 0; + return; + } + + if (!m_haveRef || cursorX != m_refCursorX || cursorY != m_refCursorY) { + m_refCursorX = cursorX; + m_refCursorY = cursorY; + m_haveRef = true; + m_travelSent = 0; + } + + m_travelSent += px; + if (m_travelSent >= STUCK_TRAVEL_PX) { + POINT at{ m_refCursorX, m_refCursorY }; + InputInjection::LogCursorNotMoving(m_travelSent, at); + m_travelSent = 0; // re-arm; the log call rate-limits itself + } } void TrackpadMouse::Update(const uint8_t* buf, size_t n) { @@ -64,7 +95,15 @@ void TrackpadMouse::Update(const uint8_t* buf, size_t n) { input.mi.dwFlags = MOUSEEVENTF_MOVE; input.mi.dx = ix; input.mi.dy = iy; - SendInput(1, &input, sizeof(INPUT)); + // Read the cursor before sending: SendInput queues the event + // rather than applying it, so a position read straight after + // would still be the old one. + POINT before{}; + const bool haveBefore = GetCursorPos(&before) != FALSE; + if (InputInjection::Send(input, "trackpad-move")) { + NoteMovementSent(std::labs(ix) + std::labs(iy), + haveBefore, before.x, before.y); + } } } } diff --git a/src/app/TrackpadMouse.h b/src/app/TrackpadMouse.h index 4d36809..d38cf84 100644 --- a/src/app/TrackpadMouse.h +++ b/src/app/TrackpadMouse.h @@ -21,5 +21,22 @@ private: float m_remX = 0.0f; // sub-pixel movement carry float m_remY = 0.0f; + // Movement Windows accepted, checked against the cursor actually moving. + // A refused SendInput reports itself; motion that is accepted and then + // discarded — by a cursor clip a game left behind, or a low-level hook + // filtering injected input — looks identical to the user and otherwise + // leaves no trace at all. Records where the cursor was and how much travel + // has been sent since; when the travel adds up and the position has not + // changed, that is worth a log line. + long m_refCursorX = 0; + long m_refCursorY = 0; + bool m_haveRef = false; + long m_travelSent = 0; + + // Enough sent movement that a still cursor cannot be a coincidence. + static constexpr long STUCK_TRAVEL_PX = 120; + static constexpr float SENSITIVITY = 0.01125f; + + void NoteMovementSent(long px, bool haveCursor, long cursorX, long cursorY); }; diff --git a/src/app/TrayApp.cpp b/src/app/TrayApp.cpp index 41f35d6..16c00ab 100644 --- a/src/app/TrayApp.cpp +++ b/src/app/TrayApp.cpp @@ -3,6 +3,7 @@ #include "ControllerPlatform.h" #include "DeviceRestart.h" #include "EventLog.h" +#include "InputInjection.h" #include "steam/SteamController.h" #include "resource.h" #include @@ -93,15 +94,19 @@ bool TrayApp::Init(HINSTANCE hInstance) { [this](bool connected, bool gameModeActive, bool vigemMissing) { UpdateTrayIcon(connected, gameModeActive, vigemMissing); }); - m_controller->SetAlertCallback( - [this](const std::wstring& title, const std::wstring& text) { - { - std::lock_guard lk(m_alertMutex); - m_alertTitle = title; - m_alertText = text; - } - PostMessageW(m_hwnd, WM_ALERT, 0, 0); - }); + auto raiseAlert = [this](const std::wstring& title, const std::wstring& text) { + { + std::lock_guard lk(m_alertMutex); + m_alertTitle = title; + m_alertText = text; + } + PostMessageW(m_hwnd, WM_ALERT, 0, 0); + }; + m_controller->SetAlertCallback(raiseAlert); + // Blocked input reaches the user the same way a dead controller does: it + // presents identically — nothing happens — and the cause is something only + // this app is in a position to name. + InputInjection::SetAlertCallback(raiseAlert); LoadSettings(); // Converge the startup mechanism with the loaded mode — e.g. the first