BlackMark e370c8dcc5 Phase 1a: input forwarding via DLL injection + XInput hook
The host can now inject coop_hook.dll into a running game and forward
controller state to it over shared memory, so the game reads the host's
(eventually the guest's) input and nothing else.

- hook/: coop_hook.dll. DllMain spawns a worker that opens the shared-memory
  channel (named by the game's pid) and installs SafetyHook inline hooks on
  XInputGetState/GetStateEx/GetCapabilities/SetState. Detours synthesize state
  from shared memory; unmanaged slots report disconnected, hiding physical pads.
- host/: process picker (Toolhelp32), CreateRemoteThread(LoadLibraryW) injector
  with an IsWow64Process2 bitness guard, IPC server publishing pads each frame,
  and an ImGui Injection panel wiring it together.
- tests/: hook_selftest exercises the IPC seqlock + hook detours in-process
  (no game/controller needed); passes.

Build: SafetyHook wired in (COOP_BUILD_HOOK=ON), Zydis via FetchContent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:21:20 +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 (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.

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

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

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%