Files
CoopAllTheThings/tests/dinput_hook_test.cpp
BlackMark 911b543d98 Forward mouse + keyboard to DirectInput and Raw Input games
The MKB subsystem covered message-loop (PostMessage) and polling
(GetAsyncKeyState/GetKeyboardState/GetCursorPos) games. Add the two remaining
read paths, both fed from the same synthesized state:

- DirectInput: vtable-swap IDirectInputDevice8::GetDeviceState (a COM method ->
  vtable swap, not inline, per the x86 COM-prologue trap), reading the shared
  vtable from a kept-alive probe device (the game made its devices before we
  injected). Dispatch on cbData: 256 = keyboard BYTE[256] indexed by DIK
  scan-code (map VK->DIK via MapVirtualKey VK_TO_VSC); DIMOUSESTATE = mouse
  buttons + relative deltas from the forwarded cursor.
- Raw Input: games get no WM_INPUT while unfocused, so mkb_pump synthesizes it
  (PostMessage WM_INPUT with lParam = one of our RAWINPUT slots) and the hooked
  GetRawInputData serves that slot back (RID_HEADER + RID_INPUT). Covers keys +
  buttons; relative raw-mouse movement isn't in the position-based MKB stream.

Both detours use the DetourGate safe-unhook guard, and the mock_game_test storm
exercises their install/remove. dinput_hook_test drives the real path end-to-end:
forward a key + mouse button through the ring, then a real DI keyboard/mouse
device's GetDeviceState returns them. The Raw Input round-trip is operator-
validated against a real game (too brittle to assert in-process). vtable_hook.hpp
factors the COM vtable-swap helper out for reuse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:14:59 +02:00

159 lines
5.1 KiB
C++

// In-process self-test for the DirectInput forwarding path of the MKB subsystem.
//
// Drives the REAL shipping code (mkb_hook.cpp): creates the IPC block + MKB ring, installs the MKB
// hooks (which vtable-swap IDirectInputDevice8::GetDeviceState via a probe device), pushes a
// synthetic key + mouse-button event through the ring, pumps it (so the hook's synthesized state is
// set), then creates its *own* DirectInput keyboard + mouse device and calls GetDeviceState -- and
// asserts the forwarded input shows up in the returned device state. This process plays host + game.
// Skips cleanly if DirectInput can't acquire a device (e.g. no input stack on a CI box).
#include <cstdint>
#include <cstdio>
#include <windows.h>
#include <objbase.h>
#define DIRECTINPUT_VERSION 0x0800
#include <dinput.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
#include "ipc_client.hpp"
#include "mkb_hook.hpp"
using namespace coop;
namespace
{
int g_failures = 0;
void check(bool ok, const char* what)
{
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
if (!ok)
{
++g_failures;
}
}
// A hidden window so DirectInput devices can SetCooperativeLevel/Acquire.
HWND make_window()
{
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = DefWindowProcW;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"coop_dinput_test";
RegisterClassExW(&wc);
return CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, 16, 16, nullptr, nullptr,
wc.hInstance, nullptr);
}
// Create + acquire a DI device of `kind` (GUID_SysKeyboard / GUID_SysMouse) with `fmt`. Returns null
// on any failure (treated as a skip).
IDirectInputDevice8W* make_device(IDirectInput8W* di, const GUID& kind, const DIDATAFORMAT* fmt, HWND hwnd)
{
IDirectInputDevice8W* dev = nullptr;
if (FAILED(di->CreateDevice(kind, &dev, nullptr)) || dev == nullptr)
{
return nullptr;
}
if (FAILED(dev->SetDataFormat(fmt)) ||
FAILED(dev->SetCooperativeLevel(hwnd, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)) || FAILED(dev->Acquire()))
{
dev->Release();
return nullptr;
}
return dev;
}
} // namespace
int main()
{
const bool com = SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED));
// Host side: the IPC block + a couple of forwarded events.
SharedMemory shm;
if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock)))
{
std::printf("Could not create shared memory -- skipping.\n");
return 0;
}
auto* block = shm.as<SharedBlock>();
block->version = kProtocolVersion;
block->sequence.store(0, std::memory_order_relaxed);
block->magic = kProtocolMagic;
hook::IpcClient ipc;
check(ipc.connect(10, 5), "IPC client connect");
check(hook::install_mkb_hooks(ipc), "install MKB hooks");
// Forward 'A' down and a left-mouse-button down, then pump them into the hook's synth state.
const UINT vk = 'A';
MkbEvent key{Mkb_KeyDown, vk, 0, 0};
MkbEvent mouse{Mkb_MouseDown, 0 /*left*/, 100, 80};
push_mkb_event(block->mkb, key);
push_mkb_event(block->mkb, mouse);
hook::mkb_pump(ipc);
HWND hwnd = make_window();
IDirectInput8W* di = nullptr;
if (FAILED(DirectInput8Create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W,
reinterpret_cast<void**>(&di), nullptr)) ||
di == nullptr)
{
std::printf("DirectInput8Create failed -- skipping.\n");
hook::remove_mkb_hooks();
return 0;
}
// Keyboard: GetDeviceState must show our forwarded key (at its DIK = scan code).
if (IDirectInputDevice8W* kbd = make_device(di, GUID_SysKeyboard, &c_dfDIKeyboard, hwnd))
{
BYTE keys[256] = {};
const HRESULT hr = kbd->GetDeviceState(sizeof(keys), keys);
const BYTE dik = static_cast<BYTE>(MapVirtualKeyW(vk, MAPVK_VK_TO_VSC) & 0xFF);
std::printf(" keyboard GetDeviceState hr=0x%08lX dik=0x%02X state=0x%02X\n",
static_cast<unsigned long>(hr), dik, keys[dik]);
check(SUCCEEDED(hr), "keyboard GetDeviceState succeeded");
check((keys[dik] & 0x80) != 0, "forwarded 'A' appears in the DirectInput keyboard state");
kbd->Unacquire();
kbd->Release();
}
else
{
std::printf(" keyboard device unavailable -- skipping keyboard assertion.\n");
}
// Mouse: GetDeviceState must show our forwarded left button.
if (IDirectInputDevice8W* ms = make_device(di, GUID_SysMouse, &c_dfDIMouse, hwnd))
{
DIMOUSESTATE m{};
const HRESULT hr = ms->GetDeviceState(sizeof(m), &m);
std::printf(" mouse GetDeviceState hr=0x%08lX btn0=0x%02X\n", static_cast<unsigned long>(hr),
static_cast<unsigned>(static_cast<BYTE>(m.rgbButtons[0])));
check(SUCCEEDED(hr), "mouse GetDeviceState succeeded");
check((m.rgbButtons[0] & 0x80) != 0, "forwarded left button appears in the DirectInput mouse state");
ms->Unacquire();
ms->Release();
}
else
{
std::printf(" mouse device unavailable -- skipping mouse assertion.\n");
}
di->Release();
hook::remove_mkb_hooks();
if (hwnd != nullptr)
{
DestroyWindow(hwnd);
}
if (com)
{
CoUninitialize();
}
std::printf(g_failures == 0 ? "PASS dinput_hook_test\n" : "FAILED dinput_hook_test (%d)\n", g_failures);
return g_failures == 0 ? 0 : 1;
}