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:
@@ -43,6 +43,7 @@ DWORD WINAPI worker_thread(LPVOID)
|
||||
{
|
||||
focus_installed = coop::hook::install_focus_spoof(g_ipc);
|
||||
}
|
||||
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
||||
g_ipc.heartbeat();
|
||||
Sleep(250);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace coop::hook
|
||||
// status through `ipc`.
|
||||
bool install_focus_spoof(IpcClient& ipc);
|
||||
|
||||
// Reports how the game currently reads input (Raw Input gamepad usage + sink
|
||||
// flag, DirectInput presence) so the host can identify a focus-gated path. Cheap
|
||||
// to call each tick.
|
||||
void update_input_diagnostics(IpcClient& ipc);
|
||||
|
||||
// Restores the original window procedure and removes the focus API hooks.
|
||||
void remove_focus_spoof();
|
||||
|
||||
|
||||
@@ -57,14 +57,28 @@ public:
|
||||
|
||||
// --- Status back-channel (hook -> host diagnostics) --------------------
|
||||
|
||||
// Record that the game queried a controller slot; the host turns the
|
||||
// cumulative count into a poll rate to prove the hook is live.
|
||||
void note_query(std::uint32_t user_index)
|
||||
// Record that the game queried a controller slot via XInputGetState/Ex.
|
||||
void note_state_query(std::uint32_t user_index)
|
||||
{
|
||||
if (block_ != nullptr)
|
||||
if (block_ != nullptr && user_index < kMaxPads)
|
||||
{
|
||||
block_->status.xinput_queries.fetch_add(1, std::memory_order_relaxed);
|
||||
block_->status.last_user_index = user_index;
|
||||
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
void note_caps_query(std::uint32_t user_index)
|
||||
{
|
||||
if (block_ != nullptr && user_index < kMaxPads)
|
||||
{
|
||||
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
void note_focus_query(FocusApi which)
|
||||
{
|
||||
if (block_ != nullptr && which < FocusApi_Count)
|
||||
{
|
||||
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +100,17 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
|
||||
{
|
||||
if (block_ != nullptr)
|
||||
{
|
||||
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
|
||||
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
|
||||
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
|
||||
block_->status.dinput_loaded = dinput ? 1u : 0u;
|
||||
}
|
||||
}
|
||||
|
||||
void heartbeat()
|
||||
{
|
||||
if (block_ != nullptr)
|
||||
|
||||
@@ -64,7 +64,7 @@ DWORD query_state(DWORD user_index, XINPUT_STATE* state, bool keep_guide)
|
||||
}
|
||||
if (g_ipc != nullptr)
|
||||
{
|
||||
g_ipc->note_query(user_index); // proves to the host the game is polling us
|
||||
g_ipc->note_state_query(user_index); // proves to the host the game is polling us
|
||||
}
|
||||
refresh_cache();
|
||||
const CoopPadState& pad = g_cache[user_index];
|
||||
@@ -100,6 +100,10 @@ DWORD WINAPI hk_XInputGetCapabilities(DWORD user_index, DWORD /*flags*/, XINPUT_
|
||||
{
|
||||
return ERROR_DEVICE_NOT_CONNECTED;
|
||||
}
|
||||
if (g_ipc != nullptr)
|
||||
{
|
||||
g_ipc->note_caps_query(user_index);
|
||||
}
|
||||
refresh_cache();
|
||||
if (!g_cache[user_index].connected)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user