// 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 #include #include #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(); 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.xinput_queries.load(std::memory_order_relaxed) >= 2, "status counts XInput queries"); hook::remove_xinput_hooks(); std::printf(g_failures == 0 ? "SELFTEST PASS\n" : "SELFTEST FAILED (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; }