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>
This commit is contained in:
14
tests/CMakeLists.txt
Normal file
14
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
# Self-contained verification of the forwarding core (IPC + XInput hook).
|
||||
# Reuses the hook's xinput_hook.cpp directly so it exercises the shipping code.
|
||||
add_executable(hook_selftest
|
||||
hook_selftest.cpp
|
||||
${CMAKE_SOURCE_DIR}/hook/src/xinput_hook.cpp)
|
||||
|
||||
target_include_directories(hook_selftest PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
|
||||
|
||||
target_link_libraries(hook_selftest PRIVATE
|
||||
coop_common
|
||||
safetyhook::safetyhook
|
||||
xinput)
|
||||
|
||||
add_test(NAME hook_selftest COMMAND hook_selftest)
|
||||
84
tests/hook_selftest.cpp
Normal file
84
tests/hook_selftest.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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");
|
||||
|
||||
hook::remove_xinput_hooks();
|
||||
|
||||
std::printf(g_failures == 0 ? "SELFTEST PASS\n" : "SELFTEST FAILED (%d)\n", g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user