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>
This commit is contained in:
@@ -14,6 +14,24 @@ target_link_libraries(hook_selftest PRIVATE
|
||||
|
||||
add_test(NAME hook_selftest COMMAND hook_selftest)
|
||||
|
||||
# In-process self-test for the DirectInput forwarding path: reuses mkb_hook.cpp, installs the MKB
|
||||
# hooks (which vtable-swap IDirectInputDevice8::GetDeviceState), forwards a synthetic key + mouse
|
||||
# button through the ring, then creates a real DI keyboard + mouse device and asserts GetDeviceState
|
||||
# returns the forwarded input. Skips cleanly if DirectInput can't acquire a device.
|
||||
add_executable(dinput_hook_test
|
||||
dinput_hook_test.cpp
|
||||
${CMAKE_SOURCE_DIR}/hook/src/mkb_hook.cpp
|
||||
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
|
||||
target_include_directories(dinput_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
|
||||
target_link_libraries(dinput_hook_test PRIVATE
|
||||
coop_common
|
||||
safetyhook::safetyhook
|
||||
user32
|
||||
dinput8
|
||||
dxguid
|
||||
ole32)
|
||||
add_test(NAME dinput_hook_test COMMAND dinput_hook_test)
|
||||
|
||||
# Unit test for the shared audio ring (lock-free SPSC push/pop, wrap-around,
|
||||
# format handshake, overrun policy). Header-only, no hook or audio device.
|
||||
add_executable(audio_ring_test audio_ring_test.cpp)
|
||||
@@ -242,6 +260,7 @@ add_test(NAME ui_fit_test COMMAND ui_fit_test)
|
||||
# test's "coop_tone.exe alongside me" lookup keeps working.
|
||||
coop_output_subdir(tests
|
||||
hook_selftest
|
||||
dinput_hook_test
|
||||
audio_ring_test
|
||||
mkb_ring_test
|
||||
mkb_map_test
|
||||
|
||||
158
tests/dinput_hook_test.cpp
Normal file
158
tests/dinput_hook_test.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user