// 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 #include #include #include "audio_hook.hpp" #include "coop/audio_ring.hpp" #include "coop/shared_memory.hpp" #include "debug_log.hpp" #include "focus_spoof.hpp" #include "ipc_client.hpp" #include "xinput_hook.hpp" namespace { coop::hook::IpcClient g_ipc; std::atomic g_running{true}; coop::SharedMemory g_audio_shm; // the host's audio 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; } 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; // 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. while (g_running.load(std::memory_order_relaxed)) { if (!xinput_installed) { xinput_installed = coop::hook::install_xinput_hooks(g_ipc); } if (!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) { audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr); if (audio_installed) { coop::hook::logf("worker_thread: audio hooks installed"); } } 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))) { auto* ring = g_audio_shm.as(); 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 } } } coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change 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::remove_focus_spoof(); coop::hook::remove_xinput_hooks(); coop::hook::remove_audio_hooks(); } break; default: break; } return TRUE; }