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>
177 lines
4.4 KiB
C++
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
|