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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
103
hook/src/hook_registry.cpp
Normal 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
|
||||
32
hook/src/hook_registry.hpp
Normal file
32
hook/src/hook_registry.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user