Per-subsystem hook control: install/remove input, focus, audio at runtime
Add a host->hook control channel (protocol v5 -> v6: HookControl in SharedBlock, per-subsystem "disabled" flags, 0 = install so the zero-filled default is unchanged). The worker now reconciles each subsystem every tick: install what's requested-and-missing, remove what's no longer wanted -- so the audio hooks re-attach the ring and republish format on a reinstall, and XInput/focus clear their stale status flags on removal. Injection panel: a checkbox per subsystem (input forwarding / focus spoof / audio render-hook) toggles it at runtime, showing the requested vs actual installed state from the registry, plus DLL heartbeat liveness. The hook-status section now keys off whether a DLL was injected (host-side) rather than the input-hook "attached" flag, so it stays visible with input unhooked. Guards for dependent features: the synthetic-input control is disabled when input forwarding is off, and the Audio panel explains that mirroring uses loopback (echo) when the render-hook is off. Verified against Phantom Brave via coop_audio_probe: starting with audio requested off installs only input+focus (8 hooks, no capture); re-enabling at runtime installs the audio hooks (13) and capture starts immediately. All four tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,33 +49,65 @@ DWORD WINAPI worker_thread(LPVOID)
|
||||
bool audio_installed = false;
|
||||
bool audio_ring_open = false;
|
||||
|
||||
// Keep retrying the installs (XInput and the game window may both appear
|
||||
// lazily) and beat a heartbeat so the host can show the hook is alive.
|
||||
// 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))
|
||||
{
|
||||
if (!xinput_installed)
|
||||
// --- 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);
|
||||
}
|
||||
if (!focus_installed)
|
||||
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);
|
||||
}
|
||||
// Install the audio render-hook even before the host's ring exists, so
|
||||
// render streams are counted for the debug view regardless; attach the
|
||||
// ring (enabling capture+silence) once the host creates it.
|
||||
if (com_ok && !audio_installed)
|
||||
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.open(name, coop::audio_ring_total_size(coop::kAudioRingCapacity)))
|
||||
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))
|
||||
|
||||
@@ -230,6 +230,10 @@ void remove_focus_spoof()
|
||||
hook_set_installed(g_id_active, false);
|
||||
hook_set_installed(g_id_focus, false);
|
||||
hook_set_installed(g_id_wndproc, false);
|
||||
if (g_focus_ipc != nullptr)
|
||||
{
|
||||
g_focus_ipc->mark_focus_spoof(false, 0);
|
||||
}
|
||||
g_game_hwnd = nullptr;
|
||||
g_orig_proc = nullptr;
|
||||
g_focus_ipc = nullptr;
|
||||
|
||||
@@ -44,6 +44,17 @@ public:
|
||||
return block_ != nullptr;
|
||||
}
|
||||
|
||||
// Host-requested install state for a subsystem (default = install, since the
|
||||
// mapping is zero-filled and 0 means "disabled flag clear" = install).
|
||||
[[nodiscard]] bool subsystem_install_requested(std::uint32_t subsystem) const
|
||||
{
|
||||
if (block_ == nullptr || subsystem >= HookSubsys_Count)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return block_->control.subsystem_disabled[subsystem].load(std::memory_order_acquire) == 0;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -91,6 +102,16 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// XInput hooks were removed (host unhooked input): clear the attached flag so
|
||||
// the Controllers panel stops showing stale poll rates.
|
||||
void mark_detached()
|
||||
{
|
||||
if (block_ != nullptr)
|
||||
{
|
||||
block_->status.attached = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
|
||||
{
|
||||
if (block_ != nullptr)
|
||||
|
||||
@@ -224,6 +224,10 @@ void remove_xinput_hooks()
|
||||
hook_set_installed(g_id_getstateex, false);
|
||||
hook_set_installed(g_id_getcaps, false);
|
||||
hook_set_installed(g_id_setstate, false);
|
||||
if (g_ipc != nullptr)
|
||||
{
|
||||
g_ipc->mark_detached();
|
||||
}
|
||||
g_ipc = nullptr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user