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:
68
hook/src/vtable_hook.hpp
Normal file
68
hook/src/vtable_hook.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// Hook a single COM vtable slot by overwriting its function pointer; the original is called
|
||||
// through the saved pointer. We use this instead of SafetyHook's inline hooks for COM methods
|
||||
// because, on x86, MMDevApi/AudioSes/DirectInput prologues use dynamic stack alignment
|
||||
// (`and esp,-8`) with EBP-relative argument access, which SafetyHook's trampoline relocation
|
||||
// mishandles -> the original runs with garbage args and faults. Swapping the vtable entry leaves
|
||||
// the original code untouched, so it runs with a pristine stack regardless of prologue shape.
|
||||
// Every instance of a COM coclass shares one vtable, so a single swap intercepts all of them.
|
||||
// See the project's stdcall-x86 note. (audio_hook.cpp has its own equivalent; this is the shared
|
||||
// copy for the input-side COM hooks.)
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace coop::hook
|
||||
{
|
||||
|
||||
class VtableHook
|
||||
{
|
||||
public:
|
||||
bool install(void* com_object, unsigned index, void* detour)
|
||||
{
|
||||
if (m_vtable != nullptr)
|
||||
{
|
||||
return true; // already installed (shared vtable covers every instance)
|
||||
}
|
||||
auto** vtable = *reinterpret_cast<void***>(com_object);
|
||||
DWORD old_protect = 0;
|
||||
if (!VirtualProtect(&vtable[index], sizeof(void*), PAGE_READWRITE, &old_protect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_original = vtable[index];
|
||||
vtable[index] = detour; // aligned pointer store -> atomic vs. a concurrent caller
|
||||
VirtualProtect(&vtable[index], sizeof(void*), old_protect, &old_protect);
|
||||
m_vtable = vtable;
|
||||
m_index = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove()
|
||||
{
|
||||
if (m_vtable == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DWORD old_protect = 0;
|
||||
if (VirtualProtect(&m_vtable[m_index], sizeof(void*), PAGE_READWRITE, &old_protect))
|
||||
{
|
||||
m_vtable[m_index] = m_original;
|
||||
VirtualProtect(&m_vtable[m_index], sizeof(void*), old_protect, &old_protect);
|
||||
}
|
||||
m_vtable = nullptr;
|
||||
// Keep m_original valid: a detour already running on the game's thread may still call
|
||||
// original() after we restore the slot. The original lives in the loaded module, so the
|
||||
// pointer stays valid; a re-install re-reads it.
|
||||
m_index = 0;
|
||||
}
|
||||
|
||||
template <typename Fn> Fn original() const { return reinterpret_cast<Fn>(m_original); }
|
||||
explicit operator bool() const { return m_vtable != nullptr; }
|
||||
|
||||
private:
|
||||
void** m_vtable = nullptr;
|
||||
unsigned m_index = 0;
|
||||
void* m_original = nullptr;
|
||||
};
|
||||
|
||||
} // namespace coop::hook
|
||||
Reference in New Issue
Block a user