Hook registry: list installed hooks + call counts in Injection panel

Add a process-wide hook registry (hook/src/hook_registry) that every hook
module registers its hooks with and bumps a counter from each detour. The
XInput, focus-spoof, and audio render-hooks now register their individual
hooks (XInputGetState/Ex/Caps/SetState; GetForegroundWindow/GetActiveWindow/
GetFocus/WndProc guard; IMMDevice::Activate, IAudioClient::Initialize/
GetService, IAudioRenderClient::GetBuffer/ReleaseBuffer) and count calls.

The worker publishes the table to the host each tick over a new HookStatus
field (protocol v4 -> v5: HookEntry[] + count). The Injection panel shows it
as a collapsible table grouped by subsystem with an installed flag and call
count per hook; coop_audio_probe prints the same table headless.

Verified against Phantom Brave: 13 hooks listed with live counts (focus APIs
polled heavily, GetBuffer/ReleaseBuffer ticking with the audio render loop).
All four tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 20:09:39 +02:00
parent 565934e8cf
commit 1f905940ef
16 changed files with 371 additions and 14 deletions

View File

@@ -9,6 +9,8 @@
#include <safetyhook.hpp>
#include "hook_registry.hpp"
namespace coop::hook
{
@@ -22,6 +24,13 @@ constexpr std::uint16_t kGuideButton = 0x0400;
IpcClient* g_ipc = nullptr;
std::vector<safetyhook::InlineHook> g_hooks;
// Registry ids for the hook list (one per logical export; shared across the
// xinput*.dll variants that may each export it).
int g_id_getstate = -1;
int g_id_getstateex = -1;
int g_id_getcaps = -1;
int g_id_setstate = -1;
// Last good snapshot, so a momentary failed IPC read (host mid-write) doesn't
// flicker the controller as disconnected inside the game.
std::array<CoopPadState, kMaxPads> g_cache;
@@ -86,16 +95,19 @@ DWORD query_state(DWORD user_index, XINPUT_STATE* state, bool keep_guide)
DWORD WINAPI hk_XInputGetState(DWORD user_index, XINPUT_STATE* state)
{
hook_note_call(g_id_getstate);
return query_state(user_index, state, /*keep_guide=*/false);
}
DWORD WINAPI hk_XInputGetStateEx(DWORD user_index, XINPUT_STATE* state)
{
hook_note_call(g_id_getstateex);
return query_state(user_index, state, /*keep_guide=*/true);
}
DWORD WINAPI hk_XInputGetCapabilities(DWORD user_index, DWORD /*flags*/, XINPUT_CAPABILITIES* caps)
{
hook_note_call(g_id_getcaps);
if (caps == nullptr || user_index >= kMaxPads)
{
return ERROR_DEVICE_NOT_CONNECTED;
@@ -131,6 +143,7 @@ DWORD WINAPI hk_XInputGetCapabilities(DWORD user_index, DWORD /*flags*/, XINPUT_
// phase; for now report success so the game's logic is happy.
DWORD WINAPI hk_XInputSetState(DWORD user_index, XINPUT_VIBRATION* /*vibration*/)
{
hook_note_call(g_id_setstate);
if (user_index >= kMaxPads || !g_cache[user_index].connected)
{
return ERROR_DEVICE_NOT_CONNECTED;
@@ -138,7 +151,7 @@ DWORD WINAPI hk_XInputSetState(DWORD user_index, XINPUT_VIBRATION* /*vibration*/
return ERROR_SUCCESS;
}
void hook_export(HMODULE module, const char* name, void* detour)
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
{
if (module == nullptr)
{
@@ -147,10 +160,11 @@ void hook_export(HMODULE module, const char* name, void* detour)
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
{
g_hooks.emplace_back(safetyhook::create_inline(target, detour));
hook_set_installed(registry_id, true);
}
}
void hook_ordinal(HMODULE module, WORD ordinal, void* detour)
void hook_ordinal(HMODULE module, WORD ordinal, void* detour, int registry_id)
{
if (module == nullptr)
{
@@ -159,6 +173,7 @@ void hook_ordinal(HMODULE module, WORD ordinal, void* detour)
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, MAKEINTRESOURCEA(ordinal))))
{
g_hooks.emplace_back(safetyhook::create_inline(target, detour));
hook_set_installed(registry_id, true);
}
}
@@ -173,6 +188,11 @@ bool install_xinput_hooks(IpcClient& ipc)
g_ipc = &ipc;
refresh_cache();
g_id_getstate = hook_register("XInputGetState", HookSubsys_Input);
g_id_getstateex = hook_register("XInputGetStateEx (ord 100)", HookSubsys_Input);
g_id_getcaps = hook_register("XInputGetCapabilities", HookSubsys_Input);
g_id_setstate = hook_register("XInputSetState", HookSubsys_Input);
// A process generally loads exactly one of these, but hook every one that is
// present so we don't miss the one the game actually calls.
const wchar_t* modules[] = {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"xinputuap.dll"};
@@ -183,10 +203,11 @@ bool install_xinput_hooks(IpcClient& ipc)
{
continue;
}
hook_export(module, "XInputGetState", reinterpret_cast<void*>(&hk_XInputGetState));
hook_ordinal(module, 100, reinterpret_cast<void*>(&hk_XInputGetStateEx));
hook_export(module, "XInputGetCapabilities", reinterpret_cast<void*>(&hk_XInputGetCapabilities));
hook_export(module, "XInputSetState", reinterpret_cast<void*>(&hk_XInputSetState));
hook_export(module, "XInputGetState", reinterpret_cast<void*>(&hk_XInputGetState), g_id_getstate);
hook_ordinal(module, 100, reinterpret_cast<void*>(&hk_XInputGetStateEx), g_id_getstateex);
hook_export(module, "XInputGetCapabilities", reinterpret_cast<void*>(&hk_XInputGetCapabilities),
g_id_getcaps);
hook_export(module, "XInputSetState", reinterpret_cast<void*>(&hk_XInputSetState), g_id_setstate);
}
if (!g_hooks.empty())
{
@@ -199,6 +220,10 @@ bool install_xinput_hooks(IpcClient& ipc)
void remove_xinput_hooks()
{
g_hooks.clear(); // InlineHook destructor restores the original bytes
hook_set_installed(g_id_getstate, false);
hook_set_installed(g_id_getstateex, false);
hook_set_installed(g_id_getcaps, false);
hook_set_installed(g_id_setstate, false);
g_ipc = nullptr;
}