Files
CoopAllTheThings/hook/src/dllmain.cpp
BlackMark 9557b9ca69 Log window: stream the hook's logs over IPC into an in-app Log panel
Add a shared log ring (common/coop/log_ring.hpp): a lossy multi-producer /
single-consumer ring named coop_log_<pid>. The hook logs from several threads,
so producers claim a slot with fetch_add and publish each record with a
release store of its sequence; the consumer reads in order and tolerates
losing the oldest lines if it falls a whole ring behind.

The DLL's logf() now formats once and pushes every line to the ring (the file
trace stays as an opt-in mirror); the worker attaches the ring right after IPC
connect so bring-up is captured. The host (IpcServer) creates the ring at
injection time and exposes drain_logs(); a new LogPanel pulls new lines each
frame into a bounded rolling buffer and renders them with auto-scroll, a
filter, and clear. Added to the View menu (and UiState.show_log).

Verified against Phantom Brave via coop_audio_probe, which now also creates the
ring and drains it: the full hook bring-up trace streamed over IPC. All four
tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:25:04 +02:00

196 lines
5.8 KiB
C++

// coop_hook.dll -- injected into the target game by the host.
//
// On load it opens the host's shared-memory channel (named by this process's
// pid), hooks XInput so the game reads the forwarded controller state, and
// spoofs focus so the game keeps running while the tool holds the real OS focus.
// All real work happens on a worker thread; DllMain only kicks it off to stay
// clear of the loader lock.
#include <atomic>
#include <windows.h>
#include <objbase.h>
#include "audio_hook.hpp"
#include "coop/audio_ring.hpp"
#include "coop/log_ring.hpp"
#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"
namespace
{
coop::hook::IpcClient g_ipc;
std::atomic<bool> g_running{true};
coop::SharedMemory g_audio_shm; // the host's audio ring, opened when present
coop::SharedMemory g_log_shm; // the host's log ring, opened when present
DWORD WINAPI worker_thread(LPVOID)
{
coop::hook::logf("worker_thread: started");
// The host creates the mapping around injection time; give it a few seconds.
if (!g_ipc.connect(/*attempts=*/200, /*delay_ms=*/25))
{
coop::hook::logf("worker_thread: IPC connect FAILED (no host mapping); exiting");
return 0;
}
// Attach the host's log ring first so the rest of bring-up streams to the Log
// window. The host creates it at injection time; it's normally already there.
{
const std::wstring log_name = coop::log_ring_name(GetCurrentProcessId());
if (g_log_shm.open(log_name, coop::log_ring_total_size(coop::kLogCapacity)))
{
auto* lr = g_log_shm.as<coop::LogRing>();
if (coop::log_ring_valid(*lr))
{
coop::hook::set_log_ring(lr);
}
else
{
g_log_shm.reset();
}
}
}
coop::hook::logf("worker_thread: IPC connected");
// The audio render-hook instantiates a COM enumerator on this thread.
const bool com_ok = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
coop::hook::logf("worker_thread: CoInitializeEx com_ok=%d", com_ok ? 1 : 0);
bool xinput_installed = false;
bool focus_installed = false;
bool audio_installed = false;
bool audio_ring_open = false;
// Each tick, reconcile each subsystem with the host's requested state: install
// what's wanted but missing (modules / the game window may appear lazily) and
// remove what's no longer wanted (the host toggled it off). Beat a heartbeat so
// the host can see the hook is alive.
while (g_running.load(std::memory_order_relaxed))
{
// --- Input (XInput) ---
const bool want_input = g_ipc.subsystem_install_requested(coop::HookSubsys_Input);
if (want_input && !xinput_installed)
{
xinput_installed = coop::hook::install_xinput_hooks(g_ipc);
}
else if (!want_input && xinput_installed)
{
coop::hook::remove_xinput_hooks();
xinput_installed = false;
}
// --- Focus spoof ---
const bool want_focus = g_ipc.subsystem_install_requested(coop::HookSubsys_Focus);
if (want_focus && !focus_installed)
{
focus_installed = coop::hook::install_focus_spoof(g_ipc);
}
else if (!want_focus && focus_installed)
{
coop::hook::remove_focus_spoof();
focus_installed = false;
}
// --- Audio render-hook ---
// Install even before the host's ring exists so render streams are counted
// regardless; attach the ring (enabling capture+silence) once it appears.
const bool want_audio = com_ok && g_ipc.subsystem_install_requested(coop::HookSubsys_Audio);
if (want_audio && !audio_installed)
{
audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr);
if (audio_installed)
{
coop::hook::logf("worker_thread: audio hooks installed");
audio_ring_open = false; // re-attach the ring below after a reinstall
}
}
else if (!want_audio && audio_installed)
{
coop::hook::remove_audio_hooks();
audio_installed = false;
audio_ring_open = false;
coop::hook::logf("worker_thread: audio hooks removed (host request)");
}
if (audio_installed && !audio_ring_open)
{
const std::wstring name = coop::audio_ring_name(GetCurrentProcessId());
if (!g_audio_shm.valid())
{
g_audio_shm.open(name, coop::audio_ring_total_size(coop::kAudioRingCapacity));
}
if (g_audio_shm.valid())
{
auto* ring = g_audio_shm.as<coop::AudioRingHeader>();
if (coop::audio_ring_valid(*ring))
{
coop::hook::set_audio_ring(ring);
audio_ring_open = true;
coop::hook::logf("worker_thread: audio ring opened");
}
else
{
g_audio_shm.reset(); // present but not our contract; retry
}
}
}
// The primary stream is often registered before the ring is attached (or the
// host re-inits the ring on a mirror re-toggle, clearing its format); keep
// the format published so the host consumes the ring instead of falling back.
if (audio_ring_open)
{
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);
}
if (com_ok)
{
CoUninitialize();
}
return 0;
}
} // namespace
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(module);
if (HANDLE thread = CreateThread(nullptr, 0, &worker_thread, nullptr, 0, nullptr))
{
CloseHandle(thread);
}
break;
case DLL_PROCESS_DETACH:
// Skip cleanup when the process is tearing down (reserved != null): the
// loader is already unwinding and touching other modules is unsafe.
if (reserved == nullptr)
{
g_running.store(false, std::memory_order_relaxed);
coop::hook::set_log_ring(nullptr);
coop::hook::remove_focus_spoof();
coop::hook::remove_xinput_hooks();
coop::hook::remove_audio_hooks();
coop::hook::hook_registry_reset();
}
break;
default:
break;
}
return TRUE;
}