Phase 1a: per-slot poll viz + input-path diagnostics

Surfaced by a game (Life is Strange: Before the Storm) that ignores
controller input when it lacks true OS focus even though it still polls
XInput. To find the focus-gated detection path, instrument the hook.

Protocol v3 status back-channel now reports:
- per-slot XInputGetState and XInputGetCapabilities counters (replacing the
  single aggregate), so the overlay shows exactly which slots the game polls
  and how fast;
- focus-API call counts (GetForegroundWindow/GetActiveWindow/GetFocus) to see
  whether the game consults the APIs we spoof;
- input-path diagnostics: whether the process registered Raw Input for a
  gamepad usage and whether it set RIDEV_INPUTSINK (background delivery), and
  whether a DirectInput dll is loaded.

Host overlay gains a per-slot poll table and an "Input path" section. The DLL
refreshes input diagnostics each worker tick via GetRegisteredRawInputDevices.
hook_selftest updated for per-slot counters; passes.

This is diagnostic-only: once a real run shows which path LiS uses, the
targeted focus fix (e.g. forcing RIDEV_INPUTSINK or DI background coop) follows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:38:07 +02:00
parent df4325d21b
commit a0a12d69fe
11 changed files with 210 additions and 36 deletions

View File

@@ -16,6 +16,7 @@ HWND g_game_hwnd = nullptr;
WNDPROC g_orig_proc = nullptr;
bool g_unicode = true;
std::vector<safetyhook::InlineHook> g_focus_hooks;
IpcClient* g_focus_ipc = nullptr;
struct EnumContext
{
@@ -85,16 +86,28 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
HWND WINAPI hk_GetForegroundWindow()
{
if (g_focus_ipc != nullptr)
{
g_focus_ipc->note_focus_query(FocusApi_Foreground);
}
return g_game_hwnd;
}
HWND WINAPI hk_GetActiveWindow()
{
if (g_focus_ipc != nullptr)
{
g_focus_ipc->note_focus_query(FocusApi_Active);
}
return g_game_hwnd;
}
HWND WINAPI hk_GetFocus()
{
if (g_focus_ipc != nullptr)
{
g_focus_ipc->note_focus_query(FocusApi_Focus);
}
return g_game_hwnd;
}
@@ -110,6 +123,7 @@ void hook_export(HMODULE module, const char* name, void* detour)
bool install_focus_spoof(IpcClient& ipc)
{
g_focus_ipc = &ipc;
if (g_game_hwnd != nullptr)
{
return true; // already active
@@ -142,6 +156,40 @@ bool install_focus_spoof(IpcClient& ipc)
return true;
}
void update_input_diagnostics(IpcClient& ipc)
{
// Inspect how the game currently reads input, to find a focus-gated path that
// would explain a controller working only when the game has true focus.
bool raw_registered = false;
bool raw_gamepad = false;
bool raw_gamepad_sink = false;
UINT count = 0;
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0)
{
std::vector<RAWINPUTDEVICE> devices(count);
const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE));
if (got != static_cast<UINT>(-1))
{
raw_registered = got > 0;
for (UINT i = 0; i < got; ++i)
{
// Generic Desktop (0x01) joystick (0x04) / gamepad (0x05).
const bool is_pad =
devices[i].usUsagePage == 0x01 && (devices[i].usUsage == 0x04 || devices[i].usUsage == 0x05);
if (is_pad)
{
raw_gamepad = true;
raw_gamepad_sink = (devices[i].dwFlags & RIDEV_INPUTSINK) != 0;
}
}
}
}
const bool dinput = GetModuleHandleW(L"dinput8.dll") != nullptr || GetModuleHandleW(L"dinput.dll") != nullptr;
ipc.set_input_diagnostics(raw_registered, raw_gamepad, raw_gamepad_sink, dinput);
}
void remove_focus_spoof()
{
if (g_game_hwnd != nullptr && g_orig_proc != nullptr)
@@ -158,6 +206,7 @@ void remove_focus_spoof()
g_focus_hooks.clear();
g_game_hwnd = nullptr;
g_orig_proc = nullptr;
g_focus_ipc = nullptr;
}
} // namespace coop::hook