BlackMark df4325d21b 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>
2026-06-19 00:13:06 +02:00

CoopAllTheThings

Steam Remote Play Together (RPT) for any XInput game — without breaking DRM, achievements, or playtime.

Existing "donor game" tools (e.g. RemotePlayWhatever) copy a target game's files into a donor game's folder and rename the executable so Steam streams the target under the donor's appid. That breaks DRM-protected games, breaks achievements, and credits playtime to the donor.

CoopAllTheThings takes a different approach: the real game runs normally under its own appid (so DRM, achievements, and playtime all work), while a lightweight mirror app runs under the donor appid. The mirror presents a borderless window that is a live copy of the game's video + audio, and forwards the guests' input back into the real game. Steam's RPT captures the mirror window — so any XInput game becomes Remote-Play-Together-able.

See docs and the in-repo plan for the full design.

Architecture (target)

Concern Mechanism Where
Receive guest input Steam Input / XInput (RPT delivers guests to the focused window) coop_host.exe
Forward input to game DLL injection + XInput hook (SafetyHook) — game sees only our pad coop_hook.dll
Mirror video Windows Graphics Capture first; IDXGISwapChain::Present hook as the low-latency upgrade host (+ hook)
Mirror audio WASAPI process-loopback capture of the game, re-rendered coop_host.exe
Host ↔ hook IPC Named shared memory (seqlock for input, shared D3D11 texture for video) common/

Limitations

  • Anti-cheat: the input path injects coop_hook.dll into the target game. Games protected by kernel-level anti-cheat (Easy Anti-Cheat, BattlEye, Vanguard, etc.) will detect the injected module and may kick the player or issue a ban. Such games are explicitly out of scope and unsupported — do not use CoopAllTheThings with them. The tool targets single-player and co-op/local-multiplayer titles without active anti-cheat.
  • XInput only: the game must read controllers via XInput (the common case). DirectInput-only / RawInput-only games are not handled.
  • Architecture match: the host and hook DLL must match the game's bitness. x64 is supported first; x86 support is a later phase (see the plan).

Status

Phase 0 — donor spike. ✅ Validated. coop_host.exe is a borderless D3D11 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 + 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.

Building

Requirements: Windows 10/11, Visual Studio 2022 (MSVC + C++ workload), CMake ≥ 3.21.

git clone --recurse-submodules <repo-url>
# or, if already cloned:
git submodule update --init --recursive

cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Debug
# output: bin/Debug/coop_host.exe

Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under third_party/; the Steamworks SDK is vendored manually there when wired. No vcpkg / package manager is used.

Phase 0: validating the donor-launch assumption

This is a manual test — it needs Steam, a second person (or second account), and a donor game that supports Remote Play Together.

  1. Pick a donor game you own that supports Remote Play Together (check the "Remote Play Together" tag on its store page). The donor only needs RPT support; it is never actually played.

  2. Launch the host under the donor's appid. Find the donor's appid (the number in its store URL), then run:

    "C:\Program Files (x86)\Steam\steam.exe" -applaunch <donorAppId> "D:\dev\CoopAllTheThings\bin\Debug\coop_host.exe"
    

    The borderless overlay window should appear and Steam should consider the donor "running" (green status / "Stop" button in the library).

    If the donor ignores the trailing path, set the host as the donor's Launch Options instead ("D:\...\coop_host.exe" %command% variants), or use a launcher such as RemotePlayDetached. Recording which method makes Steam attribute our window to the donor is the main deliverable of Phase 0.

  3. Start Remote Play Together from the Steam friends list / overlay and invite a friend (or a second machine/account).

  4. Verify on the guest side:

    • The guest sees the borderless overlay window streamed (not a black screen).
    • The guest presses buttons on their controller and the corresponding slot in the overlay lights up. This proves RPT routes guest input into our window as XInput — the foundation the whole tool relies on.
  5. Press Esc in the host window to quit.

If steps 2 and 4 both work, the core premise holds and we proceed to Phase 1 (window capture + input injection). If not, we revisit the donor-attribution approach before building further.

Phase 1a: testing input forwarding locally

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>/).

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.
  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).

For a quick sanity check of the forwarding core without a game, run bin\Debug\hook_selftest.exe — it should print SELFTEST PASS.

Description
No description provided
Readme 845 KiB
Languages
C++ 95.7%
CMake 4.2%