Files
CoopAllTheThings/hook/src/ipc_client.hpp
BlackMark 1f905940ef 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>
2026-06-19 20:09:39 +02:00

177 lines
4.4 KiB
C++

// Hook-side view of the shared-memory IPC channel. The host creates the section
// (named by the target game's pid); we open it from inside the game and read the
// forwarded pad state the host publishes each frame.
#pragma once
#include <atomic>
#include <cstdint>
#include <windows.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
namespace coop::hook
{
class IpcClient
{
public:
// Tries to open the section a few times: the host may inject us slightly
// before (or after) it creates the mapping. Returns true once connected.
bool connect(int attempts, int delay_ms)
{
const std::wstring name = shared_memory_name(GetCurrentProcessId());
for (int i = 0; i < attempts; ++i)
{
if (shm_.open(name, sizeof(SharedBlock)))
{
auto* block = shm_.as<SharedBlock>();
if (block->magic == kProtocolMagic && block->version == kProtocolVersion)
{
block_ = block;
return true;
}
shm_.reset(); // present but not a contract we understand; retry
}
Sleep(static_cast<DWORD>(delay_ms));
}
return false;
}
[[nodiscard]] bool connected() const
{
return block_ != nullptr;
}
// Copies a torn-free snapshot of all slots. Returns false only if the host
// was mid-write for the whole spin window (caller should reuse its cache).
bool snapshot(CoopPadState (&out)[kMaxPads], std::uint32_t& count) const
{
if (block_ == nullptr)
{
return false;
}
return read_pads(*block_, out, count);
}
// --- Status back-channel (hook -> host diagnostics) --------------------
// Record that the game queried a controller slot via XInputGetState/Ex.
void note_state_query(std::uint32_t user_index)
{
if (block_ != nullptr && user_index < kMaxPads)
{
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
}
}
void note_caps_query(std::uint32_t user_index)
{
if (block_ != nullptr && user_index < kMaxPads)
{
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
}
}
void note_focus_query(FocusApi which)
{
if (block_ != nullptr && which < FocusApi_Count)
{
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
}
}
void mark_attached()
{
if (block_ != nullptr)
{
block_->status.game_pid = GetCurrentProcessId();
block_->status.attached = 1;
}
}
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
{
if (block_ != nullptr)
{
block_->status.focus_spoof = active ? 1u : 0u;
block_->status.game_hwnd = game_hwnd;
}
}
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
{
if (block_ != nullptr)
{
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
block_->status.dinput_loaded = dinput ? 1u : 0u;
}
}
void heartbeat()
{
if (block_ != nullptr)
{
block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed);
}
}
// --- Audio render-hook diagnostics -------------------------------------
// Total distinct render streams the audio hook has observed.
void set_audio_streams_seen(std::uint32_t count)
{
if (block_ != nullptr)
{
block_->status.audio_streams_seen = count;
}
}
// Publish a tracked stream's format/role into its debug slot.
void publish_audio_stream(std::uint32_t slot, const AudioStreamInfo& info)
{
if (block_ != nullptr && slot < kMaxAudioStreams)
{
block_->status.audio_streams[slot] = info;
}
}
// Update a tracked stream's cumulative frame count (host derives live/idle).
void note_audio_frames(std::uint32_t slot, std::uint64_t frames)
{
if (block_ != nullptr && slot < kMaxAudioStreams)
{
block_->status.audio_streams[slot].frames_rendered = frames;
}
}
// --- 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;
};
} // namespace coop::hook