Files
CoopAllTheThings/tests/hook_selftest.cpp
BlackMark a0a12d69fe 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>
2026-06-19 00:38:07 +02:00

91 lines
3.0 KiB
C++

// In-process self-test for the core forwarding logic: IPC publish/read + the
// SafetyHook XInput interception. No injection or physical controller needed --
// this process plays both host and game. Exits 0 on pass, 1 on failure.
#include <cstdio>
#include <windows.h>
#include <xinput.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
#include "ipc_client.hpp"
#include "xinput_hook.hpp"
using namespace coop;
namespace
{
constexpr std::uint16_t kButtonA = 0x1000;
constexpr std::uint16_t kButtonB = 0x2000;
int g_failures = 0;
void check(bool ok, const char* what)
{
if (!ok)
{
std::printf(" FAIL: %s\n", what);
++g_failures;
}
}
} // namespace
int main()
{
// --- Host side: create the section (named by our pid) and publish a pad. ---
SharedMemory shm;
if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock)))
{
std::printf("FAIL: could not create shared memory\n");
return 1;
}
auto* block = shm.as<SharedBlock>();
block->version = kProtocolVersion;
block->sequence.store(0, std::memory_order_relaxed);
block->magic = kProtocolMagic;
CoopPadState pads[kMaxPads] = {};
pads[0].connected = 1;
pads[0].packet = 7;
pads[0].buttons = kButtonA | kButtonB;
pads[0].left_trigger = 128;
pads[0].thumb_lx = 12345;
pads[0].thumb_ry = -4321;
publish_pads(*block, pads, kMaxPads);
// --- Hook side: connect and install over this process's own xinput. ---
hook::IpcClient ipc;
check(ipc.connect(10, 5), "IPC client connect");
check(hook::install_xinput_hooks(ipc), "install XInput hooks");
// --- Game side: query and verify we get the forwarded synthetic state. ---
XINPUT_STATE state = {};
check(XInputGetState(0, &state) == ERROR_SUCCESS, "slot 0 reports connected");
check(state.dwPacketNumber == 7, "packet number forwarded");
check(state.Gamepad.wButtons == (kButtonA | kButtonB), "buttons forwarded");
check(state.Gamepad.bLeftTrigger == 128, "left trigger forwarded");
check(state.Gamepad.sThumbLX == 12345, "left thumb X forwarded");
check(state.Gamepad.sThumbRY == -4321, "right thumb Y forwarded");
XINPUT_STATE other = {};
check(XInputGetState(1, &other) == ERROR_DEVICE_NOT_CONNECTED, "slot 1 hidden as disconnected");
XINPUT_CAPABILITIES caps = {};
check(XInputGetCapabilities(0, 0, &caps) == ERROR_SUCCESS, "slot 0 capabilities reported");
check(caps.Type == XINPUT_DEVTYPE_GAMEPAD, "capability device type");
// Status back-channel: the host relies on these to prove the hook is live.
check(block->status.attached == 1, "status reports attached");
check(block->status.get_state_calls[0].load(std::memory_order_relaxed) >= 1, "status counts slot 0 GetState");
check(block->status.get_state_calls[1].load(std::memory_order_relaxed) >= 1, "status counts slot 1 GetState");
check(block->status.get_caps_calls[0].load(std::memory_order_relaxed) >= 1, "status counts slot 0 GetCaps");
hook::remove_xinput_hooks();
std::printf(g_failures == 0 ? "SELFTEST PASS\n" : "SELFTEST FAILED (%d)\n", g_failures);
return g_failures == 0 ? 0 : 1;
}