Phase 1a: focus spoofing + hook observability

Two problems surfaced in testing: (1) no way to tell whether the injected
hook was actually the input source, and (2) the final design needs the tool
window focused for Steam RPT capture, which would pause/silence games that
react to focus loss. Both are addressed here.

- Focus spoofing (hook/focus_spoof): find the game's main window, subclass it
  to rewrite/swallow WM_ACTIVATE/ACTIVATEAPP/NCACTIVATE/KILLFOCUS, and inline-
  hook GetForegroundWindow/GetActiveWindow/GetFocus to always report the game
  as active. The game keeps running and polling while unfocused.
- Status back-channel (protocol v2): the DLL reports attached/focus-spoof
  flags, game pid/hwnd, a heartbeat, and a cumulative XInputGetState counter.
  The host overlay turns the counter into a live poll rate, so "is the hook
  working" is directly observable.
- Synthetic test-input toggle in the host: forwards a known automated pattern
  (stick circle + periodic A) to prove forwarding independent of the physical
  pad.
- hook_selftest extended to assert the status channel; passes.

Documented the windowed/borderless requirement and the new observable test
flow in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:13:06 +02:00
parent e370c8dcc5
commit df4325d21b
14 changed files with 447 additions and 34 deletions

View File

@@ -47,11 +47,15 @@ window with an ImGui overlay listing every controller it sees. Confirmed
end-to-end: Steam RPT streams the window under a donor appid, and guest gamepads
arrive (with correct slot assignment) as XInput.
**Phase 1a — input forwarding (current).** The host can inject `coop_hook.dll`
into a running game; the DLL hooks XInput (via SafetyHook) so the game reads the
controller state the host forwards over shared memory — and *only* that state, so
physical/other controllers are hidden from the game. The in-process
`hook_selftest` validates the IPC + hook core without needing a game.
**Phase 1a — input forwarding + focus spoofing (current).** The host injects
`coop_hook.dll`; the DLL hooks XInput (via SafetyHook) so the game reads the
forwarded controller state and *only* that state. The DLL also **spoofs focus**
(hooks `GetForegroundWindow`/`GetActiveWindow`/`GetFocus` and subclasses the game
window to swallow deactivation messages) so the game keeps running and polling
while the tool holds the real OS focus — required because Steam RPT only captures
the focused window. A hook→host status back-channel shows whether the hook is
attached and how fast the game is polling it. The in-process `hook_selftest`
validates the IPC + hook core without needing a game.
Still ahead (scoped in the plan): video mirror (WGC, then a `Present` hook),
audio (WASAPI process loopback), and x86 support.
@@ -119,15 +123,26 @@ This needs no RPT, donor, or second account — just the host, the hook, a
controller, and a target game. `coop_host.exe` and `coop_hook.dll` must sit in
the same folder (the build places both in `bin/<Config>/`).
1. Start a DRM-free, **non-anti-cheat**, XInput game (e.g. a small controller
sample or a permissive indie title) and get to a screen that reads the pad.
**Requirement:** run the target game **windowed or borderless**, not exclusive
fullscreen. Exclusive fullscreen minimizes on focus loss (defeating the focus
spoof) and can't be window-captured later. Only **controller** input is
forwarded — while the game is unfocused it won't receive OS keyboard/mouse.
1. Start a DRM-free, **non-anti-cheat**, XInput game in windowed/borderless mode
and get to a screen that reads the pad.
2. Run `bin\Debug\coop_host.exe`. In the **Injection** panel, filter for the
game's `.exe`, select it, and click **Inject & Connect**. The status line
should turn green ("Injected … / Forwarding input to pid …").
3. Press buttons on your physical controller. The game should respond — its
XInput now comes from the host's forwarded state, not the device directly.
Unplug-test: other controllers/slots are hidden from the game.
4. Click **Stop forwarding** (or quit the host) to tear down the channel.
game's `.exe`, select it, and click **Inject & Connect**.
3. Watch the **Hook status** section. Once it shows **Attached** and a non-zero
**"XInput polled: N/s"**, the game is provably reading our hook — injection
works. **Focus spoof: active** confirms the window was found and subclassed.
4. **Prove forwarding is the source:** tick **Forward synthetic test input**.
The game should now move on its own — left stick sweeping a circle, A pressed
every other second — independent of your physical controller. Untick it to
return control to your pad.
5. Sanity-check the focus spoof: click into another window so the game loses real
focus. It should keep running/animating (not pause), and the poll rate should
stay non-zero.
6. Click **Stop forwarding** (or quit the host) to tear down the channel.
> If injection fails with an access error, run the host as administrator. If it
> reports "target is 32-bit", that game needs the x86 hook (a later phase).