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

@@ -112,6 +112,11 @@ Done:
forwarding, controller polling rate, mirror resolution, audio source + buffered
ms) and reveal the verbose diagnostics (per-slot poll table, focus-API counts,
input-path detection, per-render-stream table) only when Debug details is on.
- **Installed-hooks list. ✅** The DLL keeps a registry of every individual hook
it installs (XInput, focus, audio), with a running call counter per hook,
reported over IPC. The Injection panel shows it grouped by subsystem
(input / focus / audio) so you can see exactly what's hooked and how busy each
hook is. `coop_audio_probe` prints the same table headless.
Future work, roughly in priority order:

View File

@@ -11,7 +11,7 @@ namespace coop
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
// refuses to attach to a host with a mismatched version.
inline constexpr std::uint32_t kProtocolVersion = 4;
inline constexpr std::uint32_t kProtocolVersion = 5;
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
@@ -60,6 +60,28 @@ struct AudioStreamInfo
std::uint64_t frames_rendered;
};
// Orthogonal hook subsystems the host can install/remove independently.
enum HookSubsystem : std::uint32_t
{
HookSubsys_Input = 0, // XInput hooks (forward the guest pad)
HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused)
HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo)
HookSubsys_Count = 3,
};
// Maximum individual hooks reported in the registry (a few per subsystem).
inline constexpr std::uint32_t kMaxHookEntries = 24;
// One installed hook, for the Injection panel's hook list. POD diagnostics, like
// AudioStreamInfo: the hook is the sole writer; benign cross-process races are ok.
struct HookEntry
{
char name[40]; // e.g. "XInputGetState"
std::uint32_t subsystem; // HookSubsystem
std::uint32_t installed; // 1 if currently hooked
std::uint64_t calls; // cumulative times the detour ran
};
// Indices into HookStatus::focus_query_calls.
enum FocusApi : std::uint32_t
{
@@ -97,6 +119,11 @@ struct HookStatus
// multi-stream game is visible before/without turning the mirror on.
std::uint32_t audio_streams_seen; // distinct render clients ever created
AudioStreamInfo audio_streams[kMaxAudioStreams]; // per-slot detail, [0] is primary
// Hook registry: every individual hook the DLL has installed, with a running
// call count. Lets the Injection panel list exactly what's hooked and how busy.
std::uint32_t hook_entry_count;
HookEntry hook_entries[kMaxHookEntries];
};
// Top-level shared block. The host is the sole writer of pad state; the hook is

View File

@@ -3,7 +3,8 @@ add_library(coop_hook SHARED
src/xinput_hook.cpp
src/focus_spoof.cpp
src/audio_hook.cpp
src/debug_log.cpp)
src/debug_log.cpp
src/hook_registry.cpp)
target_include_directories(coop_hook PRIVATE src)

View File

@@ -14,6 +14,7 @@
#include <safetyhook.hpp>
#include "debug_log.hpp"
#include "hook_registry.hpp"
namespace coop::hook
{
@@ -55,6 +56,13 @@ safetyhook::InlineHook g_hk_getbuffer;
safetyhook::InlineHook g_hk_releasebuffer;
bool g_audioclient_hooked = false;
// Registry ids for the hook list.
int g_id_activate = -1;
int g_id_initialize = -1;
int g_id_getservice = -1;
int g_id_getbuffer = -1;
int g_id_releasebuffer = -1;
// Our own probe COM objects, created at anchor time purely to read the shared
// IAudioClient / IAudioRenderClient vtables and hook GetBuffer/ReleaseBuffer
// *proactively* — so render clients the game created before we injected (the
@@ -142,6 +150,7 @@ void try_register_lazy(IAudioRenderClient* rc);
HRESULT STDMETHODCALLTYPE hk_GetBuffer(IAudioRenderClient* self, UINT32 num_frames, BYTE** data)
{
hook_note_call(g_id_getbuffer);
const HRESULT hr = g_hk_getbuffer.call<HRESULT>(self, num_frames, data);
if (SUCCEEDED(hr) && data != nullptr)
{
@@ -154,6 +163,7 @@ HRESULT STDMETHODCALLTYPE hk_GetBuffer(IAudioRenderClient* self, UINT32 num_fram
HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_frames, DWORD flags)
{
hook_note_call(g_id_releasebuffer);
// A render client we've never seen actively rendering is almost certainly one
// the game created before we injected; adopt it now (the first becomes the
// primary we capture). Skip our own silent probe client.
@@ -300,6 +310,7 @@ HRESULT STDMETHODCALLTYPE hk_Initialize(IAudioClient* self, AUDCLNT_SHAREMODE mo
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX* format, LPCGUID session)
{
hook_note_call(g_id_initialize);
const HRESULT hr =
g_hk_initialize.call<HRESULT>(self, mode, flags, buffer_duration, periodicity, format, session);
logf("hk_Initialize: client=%p mode=%d flags=0x%lX hr=0x%08lX fmt=%s", self, mode,
@@ -314,6 +325,7 @@ HRESULT STDMETHODCALLTYPE hk_Initialize(IAudioClient* self, AUDCLNT_SHAREMODE mo
HRESULT STDMETHODCALLTYPE hk_GetService(IAudioClient* self, REFIID riid, void** ppv)
{
hook_note_call(g_id_getservice);
const HRESULT hr = g_hk_getservice.call<HRESULT>(self, riid, ppv);
const bool is_render = (riid == __uuidof(IAudioRenderClient));
logf("hk_GetService: client=%p hr=0x%08lX render_client=%d", self, static_cast<unsigned long>(hr),
@@ -371,6 +383,7 @@ void install_audioclient_hooks(IAudioClient* ac)
HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ctx, PROPVARIANT* params,
void** ppv)
{
hook_note_call(g_id_activate);
const HRESULT hr = g_hk_activate.call<HRESULT>(self, riid, cls_ctx, params, ppv);
const bool is_audioclient = (riid == __uuidof(IAudioClient) || riid == __uuidof(IAudioClient2) ||
riid == __uuidof(IAudioClient3));
@@ -395,6 +408,12 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
return true; // anchor already installed
}
g_id_activate = hook_register("IMMDevice::Activate", HookSubsys_Audio);
g_id_initialize = hook_register("IAudioClient::Initialize", HookSubsys_Audio);
g_id_getservice = hook_register("IAudioClient::GetService", HookSubsys_Audio);
g_id_getbuffer = hook_register("IAudioRenderClient::GetBuffer", HookSubsys_Audio);
g_id_releasebuffer = hook_register("IAudioRenderClient::ReleaseBuffer", HookSubsys_Audio);
// Anchor: instantiate our own enumerator + default render device purely to
// read the shared IMMDevice vtable and hook Activate. Every IMMDevice in the
// process shares this vtable, so the game's Activate calls are intercepted.
@@ -471,6 +490,12 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
// stay valid; they're released in remove_audio_hooks.
}
hook_set_installed(g_id_activate, static_cast<bool>(g_hk_activate));
hook_set_installed(g_id_initialize, static_cast<bool>(g_hk_initialize));
hook_set_installed(g_id_getservice, static_cast<bool>(g_hk_getservice));
hook_set_installed(g_id_getbuffer, static_cast<bool>(g_hk_getbuffer));
hook_set_installed(g_id_releasebuffer, static_cast<bool>(g_hk_releasebuffer));
logf("install_audio_hooks: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d (device=%p)",
static_cast<bool>(g_hk_activate) ? 1 : 0, static_cast<bool>(g_hk_initialize) ? 1 : 0,
static_cast<bool>(g_hk_getservice) ? 1 : 0, static_cast<bool>(g_hk_getbuffer) ? 1 : 0,
@@ -522,6 +547,11 @@ void remove_audio_hooks()
g_hk_initialize = {};
g_hk_activate = {};
g_audioclient_hooked = false;
hook_set_installed(g_id_activate, false);
hook_set_installed(g_id_initialize, false);
hook_set_installed(g_id_getservice, false);
hook_set_installed(g_id_getbuffer, false);
hook_set_installed(g_id_releasebuffer, false);
// Hooks are gone; safe to drop the probe objects that held the vtables.
if (IAudioRenderClient* sr = g_self_render.exchange(nullptr, std::memory_order_acq_rel))

View File

@@ -17,6 +17,7 @@
#include "coop/shared_memory.hpp"
#include "debug_log.hpp"
#include "focus_spoof.hpp"
#include "hook_registry.hpp"
#include "ipc_client.hpp"
#include "xinput_hook.hpp"
@@ -97,6 +98,7 @@ DWORD WINAPI worker_thread(LPVOID)
coop::hook::republish_audio_format();
}
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
coop::hook::hook_publish(g_ipc); // installed-hooks list + call counts
g_ipc.heartbeat();
Sleep(250);
}
@@ -130,6 +132,7 @@ BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
coop::hook::remove_focus_spoof();
coop::hook::remove_xinput_hooks();
coop::hook::remove_audio_hooks();
coop::hook::hook_registry_reset();
}
break;
default:

View File

@@ -6,6 +6,8 @@
#include <safetyhook.hpp>
#include "hook_registry.hpp"
namespace coop::hook
{
@@ -18,6 +20,11 @@ bool g_unicode = true;
std::vector<safetyhook::InlineHook> g_focus_hooks;
IpcClient* g_focus_ipc = nullptr;
int g_id_foreground = -1;
int g_id_active = -1;
int g_id_focus = -1;
int g_id_wndproc = -1;
struct EnumContext
{
DWORD pid;
@@ -67,15 +74,19 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
if (LOWORD(wparam) == WA_INACTIVE)
{
wparam = MAKEWPARAM(WA_ACTIVE, HIWORD(wparam));
hook_note_call(g_id_wndproc);
}
break;
case WM_ACTIVATEAPP:
wparam = TRUE; // app is "still active"
hook_note_call(g_id_wndproc);
break;
case WM_NCACTIVATE:
wparam = TRUE; // keep the active (non-greyed) appearance
hook_note_call(g_id_wndproc);
break;
case WM_KILLFOCUS:
hook_note_call(g_id_wndproc);
return 0; // swallow: never tell the game it lost keyboard focus
default:
break;
@@ -86,6 +97,7 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
HWND WINAPI hk_GetForegroundWindow()
{
hook_note_call(g_id_foreground);
if (g_focus_ipc != nullptr)
{
g_focus_ipc->note_focus_query(FocusApi_Foreground);
@@ -95,6 +107,7 @@ HWND WINAPI hk_GetForegroundWindow()
HWND WINAPI hk_GetActiveWindow()
{
hook_note_call(g_id_active);
if (g_focus_ipc != nullptr)
{
g_focus_ipc->note_focus_query(FocusApi_Active);
@@ -104,6 +117,7 @@ HWND WINAPI hk_GetActiveWindow()
HWND WINAPI hk_GetFocus()
{
hook_note_call(g_id_focus);
if (g_focus_ipc != nullptr)
{
g_focus_ipc->note_focus_query(FocusApi_Focus);
@@ -111,11 +125,12 @@ HWND WINAPI hk_GetFocus()
return g_game_hwnd;
}
void hook_export(HMODULE module, const char* name, void* detour)
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
{
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
{
g_focus_hooks.emplace_back(safetyhook::create_inline(target, detour));
hook_set_installed(registry_id, true);
}
}
@@ -129,6 +144,11 @@ bool install_focus_spoof(IpcClient& ipc)
return true; // already active
}
g_id_foreground = hook_register("GetForegroundWindow", HookSubsys_Focus);
g_id_active = hook_register("GetActiveWindow", HookSubsys_Focus);
g_id_focus = hook_register("GetFocus", HookSubsys_Focus);
g_id_wndproc = hook_register("WndProc (deactivation guard)", HookSubsys_Focus);
HWND hwnd = find_main_window(GetCurrentProcessId());
if (hwnd == nullptr)
{
@@ -144,12 +164,14 @@ bool install_focus_spoof(IpcClient& ipc)
? SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc))
: SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
g_orig_proc = reinterpret_cast<WNDPROC>(replaced);
hook_set_installed(g_id_wndproc, true);
if (HMODULE user32 = GetModuleHandleW(L"user32.dll"))
{
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow));
hook_export(user32, "GetActiveWindow", reinterpret_cast<void*>(&hk_GetActiveWindow));
hook_export(user32, "GetFocus", reinterpret_cast<void*>(&hk_GetFocus));
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow),
g_id_foreground);
hook_export(user32, "GetActiveWindow", reinterpret_cast<void*>(&hk_GetActiveWindow), g_id_active);
hook_export(user32, "GetFocus", reinterpret_cast<void*>(&hk_GetFocus), g_id_focus);
}
ipc.mark_focus_spoof(true, reinterpret_cast<std::uint64_t>(hwnd));
@@ -204,6 +226,10 @@ void remove_focus_spoof()
}
}
g_focus_hooks.clear();
hook_set_installed(g_id_foreground, false);
hook_set_installed(g_id_active, false);
hook_set_installed(g_id_focus, false);
hook_set_installed(g_id_wndproc, false);
g_game_hwnd = nullptr;
g_orig_proc = nullptr;
g_focus_ipc = nullptr;

103
hook/src/hook_registry.cpp Normal file
View File

@@ -0,0 +1,103 @@
#include "hook_registry.hpp"
#include <atomic>
#include <cstring>
#include <mutex>
namespace coop::hook
{
namespace
{
struct Slot
{
char name[40] = {};
std::atomic<std::uint32_t> subsystem{0};
std::atomic<std::uint32_t> installed{0};
std::atomic<std::uint64_t> calls{0};
std::atomic<std::uint32_t> used{0};
};
Slot g_slots[kMaxHookEntries];
std::atomic<std::uint32_t> g_count{0}; // high-water mark of allocated slots
std::mutex g_register_mutex; // registration only (rare); calls are lock-free
} // namespace
int hook_register(const char* name, std::uint32_t subsystem)
{
std::scoped_lock lock(g_register_mutex);
const std::uint32_t count = g_count.load(std::memory_order_relaxed);
for (std::uint32_t i = 0; i < count; ++i)
{
if (g_slots[i].used.load(std::memory_order_relaxed) && std::strcmp(g_slots[i].name, name) == 0)
{
return static_cast<int>(i); // already registered
}
}
if (count >= kMaxHookEntries)
{
return -1; // table full
}
Slot& s = g_slots[count];
std::strncpy(s.name, name, sizeof(s.name) - 1);
s.name[sizeof(s.name) - 1] = '\0';
s.subsystem.store(subsystem, std::memory_order_relaxed);
s.installed.store(0, std::memory_order_relaxed);
s.calls.store(0, std::memory_order_relaxed);
s.used.store(1, std::memory_order_release);
g_count.store(count + 1, std::memory_order_release);
return static_cast<int>(count);
}
void hook_set_installed(int id, bool installed)
{
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
{
g_slots[id].installed.store(installed ? 1u : 0u, std::memory_order_relaxed);
}
}
void hook_note_call(int id)
{
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
{
g_slots[id].calls.fetch_add(1, std::memory_order_relaxed);
}
}
void hook_publish(IpcClient& ipc)
{
const std::uint32_t count = g_count.load(std::memory_order_acquire);
HookEntry entries[kMaxHookEntries];
std::uint32_t n = 0;
for (std::uint32_t i = 0; i < count && i < kMaxHookEntries; ++i)
{
if (!g_slots[i].used.load(std::memory_order_acquire))
{
continue;
}
HookEntry& e = entries[n];
std::memcpy(e.name, g_slots[i].name, sizeof(e.name));
e.subsystem = g_slots[i].subsystem.load(std::memory_order_relaxed);
e.installed = g_slots[i].installed.load(std::memory_order_relaxed);
e.calls = g_slots[i].calls.load(std::memory_order_relaxed);
++n;
}
ipc.publish_hook_entries(entries, n);
}
void hook_registry_reset()
{
std::scoped_lock lock(g_register_mutex);
for (auto& s : g_slots)
{
s.used.store(0, std::memory_order_relaxed);
s.installed.store(0, std::memory_order_relaxed);
s.calls.store(0, std::memory_order_relaxed);
}
g_count.store(0, std::memory_order_release);
}
} // namespace coop::hook

View File

@@ -0,0 +1,32 @@
// Process-wide registry of the individual hooks the DLL installs, with a call
// counter per hook. Each hook module (XInput, focus, audio) registers its hooks
// once and bumps the counter from its detour; the worker thread publishes the
// table to the host over IPC for the Injection panel's hook list.
#pragma once
#include <cstdint>
#include "coop/protocol.hpp"
#include "ipc_client.hpp"
namespace coop::hook
{
// Find-or-create a registry slot for `name` in `subsystem`; returns a stable id
// (>= 0) used with the calls below, or -1 if the table is full. Idempotent: the
// same name returns the same id, so install/remove cycles keep one slot.
int hook_register(const char* name, std::uint32_t subsystem);
// Mark a hook installed / removed (drives the "installed" column).
void hook_set_installed(int id, bool installed);
// Bump a hook's call counter. Cheap (relaxed atomic); safe on any thread.
void hook_note_call(int id);
// Snapshot the registry into the host's HookStatus back-channel.
void hook_publish(IpcClient& ipc);
// Forget everything (DLL detach).
void hook_registry_reset();
} // namespace coop::hook

View File

@@ -148,6 +148,26 @@ public:
}
}
// --- Hook registry -----------------------------------------------------
// Publish the installed-hooks table (name / subsystem / installed / calls).
void publish_hook_entries(const HookEntry* entries, std::uint32_t count)
{
if (block_ == nullptr)
{
return;
}
if (count > kMaxHookEntries)
{
count = kMaxHookEntries;
}
for (std::uint32_t i = 0; i < count; ++i)
{
block_->status.hook_entries[i] = entries[i];
}
block_->status.hook_entry_count = count;
}
private:
SharedMemory shm_;
SharedBlock* block_ = nullptr;

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;
}

View File

@@ -139,6 +139,67 @@ void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
server_.publish(synthetic);
}
void InjectionPanel::draw_hook_list(const HookStatusView& status)
{
static const char* kSubsysName[] = {"Input", "Focus", "Audio"};
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
if (n == 0)
{
return;
}
if (!ImGui::CollapsingHeader("Installed hooks", ImGuiTreeNodeFlags_DefaultOpen))
{
return;
}
if (ImGui::BeginTable("hooks", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Hook");
ImGui::TableSetupColumn("On", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Calls", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableHeadersRow();
// Group rows by subsystem so related hooks sit together.
for (std::uint32_t sub = 0; sub < HookSubsys_Count; ++sub)
{
bool header_done = false;
for (std::uint32_t i = 0; i < n; ++i)
{
const HookEntry& e = status.hook_entries[i];
if (e.subsystem != sub)
{
continue;
}
if (!header_done)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextDisabled("%s", kSubsysName[sub < 3 ? sub : 0]);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
header_done = true;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(e.name);
ImGui::TableNextColumn();
if (e.installed)
{
ImGui::TextColored(kGreen, "yes");
}
else
{
ImGui::TextDisabled("no");
}
ImGui::TableNextColumn();
ImGui::Text("%llu", static_cast<unsigned long long>(e.calls));
}
}
ImGui::EndTable();
}
}
void InjectionPanel::draw_hook_status(bool debug_details)
{
if (!server_.running())
@@ -160,6 +221,8 @@ void InjectionPanel::draw_hook_status(bool debug_details)
status.focus_spoof ? "active" : "inactive");
ImGui::TextDisabled("Controller poll rates are in the Controllers panel.");
draw_hook_list(status);
if (!debug_details)
{
return; // everything below is diagnostic detail

View File

@@ -45,6 +45,7 @@ public:
private:
void refresh_processes();
void inject_selected();
void draw_hook_list(const HookStatusView& status);
void draw_hook_status(bool debug_details);
std::vector<ProcessEntry> processes_;

View File

@@ -71,6 +71,11 @@ HookStatusView IpcServer::hook_status() const
{
view.audio_streams[i] = s.audio_streams[i];
}
view.hook_entry_count = s.hook_entry_count;
for (std::uint32_t i = 0; i < kMaxHookEntries; ++i)
{
view.hook_entries[i] = s.hook_entries[i];
}
return view;
}

View File

@@ -31,6 +31,10 @@ struct HookStatusView
// Audio render-hook diagnostics (for the Audio panel's stream-count view).
std::uint32_t audio_streams_seen = 0;
AudioStreamInfo audio_streams[kMaxAudioStreams] = {};
// Installed-hooks registry (for the Injection panel's hook list).
std::uint32_t hook_entry_count = 0;
HookEntry hook_entries[kMaxHookEntries] = {};
};
class IpcServer

View File

@@ -2,7 +2,8 @@
# Reuses the hook's xinput_hook.cpp directly so it exercises the shipping code.
add_executable(hook_selftest
hook_selftest.cpp
${CMAKE_SOURCE_DIR}/hook/src/xinput_hook.cpp)
${CMAKE_SOURCE_DIR}/hook/src/xinput_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(hook_selftest PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
@@ -42,7 +43,8 @@ add_test(NAME audio_loopback_test COMMAND audio_loopback_test)
add_executable(audio_hook_test
audio_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/audio_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp)
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(audio_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)

View File

@@ -233,6 +233,16 @@ int wmain(int argc, wchar_t** argv)
}
}
// Dump the hook registry so the installed-hooks list can be verified headless.
static const char* kSubsys[] = {"Input", "Focus", "Audio"};
std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count);
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
{
const coop::HookEntry& e = status.hook_entries[i];
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 3 ? kSubsys[e.subsystem] : "?", e.name,
e.installed ? "ON " : "off", static_cast<unsigned long long>(e.calls));
}
std::printf("\nDone. Leaving the hook loaded in the game.\n");
block->magic = 0; // invalidate so a late hook read won't trust stale data
return 0;