Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs, Allman functions) over every source file so the tree is formatter-clean. Whitespace only -- no behavior change; full x64 + x86 suites pass. Also set SortIncludes: false in .clang-format. Windows include order is load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h / dinput.h; winsock2.h must precede windows.h), and the default alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build break. Leaving order alone keeps the manual, correct grouping.
280 lines
10 KiB
C++
280 lines
10 KiB
C++
#include "focus_spoof.hpp"
|
|
|
|
#include <vector>
|
|
|
|
#include <windows.h>
|
|
|
|
#include <safetyhook.hpp>
|
|
|
|
#include "find_window.hpp"
|
|
#include "hook_guard.hpp"
|
|
#include "hook_install.hpp"
|
|
#include "hook_registry.hpp"
|
|
|
|
namespace coop::hook {
|
|
|
|
namespace {
|
|
|
|
DetourGate g_gate; // drains in-flight focus / WNDPROC detours before remove nulls their state
|
|
|
|
HWND g_game_hwnd = nullptr;
|
|
WNDPROC g_orig_proc = nullptr;
|
|
bool g_unicode = true;
|
|
std::vector<safetyhook::InlineHook> g_focus_hooks;
|
|
IpcClient* g_focus_ipc = nullptr;
|
|
|
|
int g_id_foreground = -1;
|
|
int g_id_active = -1;
|
|
int g_id_focus = -1;
|
|
int g_id_wndproc = -1;
|
|
int g_id_clipcursor = -1;
|
|
int g_id_setcursorpos = -1;
|
|
|
|
// Cursor-release hooks kept separately so their trampolines can be called.
|
|
safetyhook::InlineHook g_hk_clipcursor;
|
|
safetyhook::InlineHook g_hk_setcursorpos;
|
|
|
|
// Replacement window procedure: convince the game it is never deactivated.
|
|
LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
|
{
|
|
DetourGate::Guard guard(g_gate); // keep g_orig_proc / g_unicode valid for this whole dispatch
|
|
switch (msg) {
|
|
case WM_ACTIVATE:
|
|
if (LOWORD(wparam) == WA_INACTIVE) {
|
|
wparam = MAKEWPARAM(WA_ACTIVE, HIWORD(wparam));
|
|
hook_note_call(g_id_wndproc);
|
|
}
|
|
break;
|
|
case WM_ACTIVATEAPP:
|
|
wparam = TRUE; // app is "still active"
|
|
hook_note_call(g_id_wndproc);
|
|
break;
|
|
case WM_NCACTIVATE:
|
|
wparam = TRUE; // keep the active (non-greyed) appearance
|
|
hook_note_call(g_id_wndproc);
|
|
break;
|
|
case WM_KILLFOCUS:
|
|
hook_note_call(g_id_wndproc);
|
|
return 0; // swallow: never tell the game it lost keyboard focus
|
|
default:
|
|
break;
|
|
}
|
|
// Read g_orig_proc once; if the subclass is live but the original isn't published yet (the tiny
|
|
// install/remove window), fall back to DefWindowProc rather than call through a null pointer.
|
|
const WNDPROC orig = g_orig_proc;
|
|
if (orig == nullptr) {
|
|
return g_unicode ? DefWindowProcW(hwnd, msg, wparam, lparam) : DefWindowProcA(hwnd, msg, wparam, lparam);
|
|
}
|
|
return g_unicode ? CallWindowProcW(orig, hwnd, msg, wparam, lparam)
|
|
: CallWindowProcA(orig, hwnd, msg, wparam, lparam);
|
|
}
|
|
|
|
HWND WINAPI hk_GetForegroundWindow()
|
|
{
|
|
DetourGate::Guard guard(g_gate);
|
|
hook_note_call(g_id_foreground);
|
|
if (g_focus_ipc != nullptr) {
|
|
g_focus_ipc->note_focus_query(FocusApi_Foreground);
|
|
}
|
|
return g_game_hwnd;
|
|
}
|
|
|
|
HWND WINAPI hk_GetActiveWindow()
|
|
{
|
|
DetourGate::Guard guard(g_gate);
|
|
hook_note_call(g_id_active);
|
|
if (g_focus_ipc != nullptr) {
|
|
g_focus_ipc->note_focus_query(FocusApi_Active);
|
|
}
|
|
return g_game_hwnd;
|
|
}
|
|
|
|
HWND WINAPI hk_GetFocus()
|
|
{
|
|
DetourGate::Guard guard(g_gate);
|
|
hook_note_call(g_id_focus);
|
|
if (g_focus_ipc != nullptr) {
|
|
g_focus_ipc->note_focus_query(FocusApi_Focus);
|
|
}
|
|
return g_game_hwnd;
|
|
}
|
|
|
|
// When cursor release is requested (the default), free any clip the game asks for so
|
|
// the operator's mouse isn't trapped; otherwise honor the game's clip.
|
|
BOOL WINAPI hk_ClipCursor(const RECT* rect)
|
|
{
|
|
DetourGate::Guard guard(g_gate);
|
|
hook_note_call(g_id_clipcursor);
|
|
const bool allow = g_focus_ipc != nullptr && g_focus_ipc->cursor_clip_allowed();
|
|
return g_hk_clipcursor.stdcall<BOOL>(allow ? rect : nullptr);
|
|
}
|
|
|
|
// Swallow the game's per-frame cursor re-centering while releasing, so the operator's
|
|
// mouse can move freely (e.g. to reach the overlay); pass it through when clipping.
|
|
BOOL WINAPI hk_SetCursorPos(int x, int y)
|
|
{
|
|
DetourGate::Guard guard(g_gate);
|
|
hook_note_call(g_id_setcursorpos);
|
|
const bool allow = g_focus_ipc != nullptr && g_focus_ipc->cursor_clip_allowed();
|
|
if (!allow) {
|
|
return TRUE;
|
|
}
|
|
return g_hk_setcursorpos.stdcall<BOOL>(x, y);
|
|
}
|
|
|
|
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
|
|
{
|
|
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name))) {
|
|
g_focus_hooks.emplace_back();
|
|
install_inline(g_focus_hooks.back(), target, detour); // assign-then-enable (no install race)
|
|
hook_set_installed(registry_id, true);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool install_focus_spoof(IpcClient& ipc)
|
|
{
|
|
g_focus_ipc = &ipc;
|
|
if (g_game_hwnd != nullptr) {
|
|
return true; // already active
|
|
}
|
|
|
|
g_id_foreground = hook_register("GetForegroundWindow", HookSubsys_Focus);
|
|
g_id_active = hook_register("GetActiveWindow", HookSubsys_Focus);
|
|
g_id_focus = hook_register("GetFocus", HookSubsys_Focus);
|
|
g_id_wndproc = hook_register("WndProc (deactivation guard)", HookSubsys_Focus);
|
|
g_id_clipcursor = hook_register("ClipCursor (cursor release)", HookSubsys_Focus);
|
|
g_id_setcursorpos = hook_register("SetCursorPos (cursor release)", HookSubsys_Focus);
|
|
|
|
HWND hwnd = find_main_window(GetCurrentProcessId());
|
|
if (hwnd == nullptr) {
|
|
return false; // window not created yet; caller retries
|
|
}
|
|
|
|
g_game_hwnd = hwnd;
|
|
g_unicode = IsWindowUnicode(hwnd) != FALSE;
|
|
|
|
// Publish g_orig_proc BEFORE activating the subclass, so a message that dispatches the instant the
|
|
// subclass goes live finds a valid original (not the null/stale value from a prior install cycle)
|
|
// -- the WNDPROC analogue of the inline-hook install race. Replacing GWLP_WNDPROC from another
|
|
// thread is safe (the new proc runs on the window's own thread); match A/W for CallWindowProc.
|
|
g_orig_proc = g_unicode ? reinterpret_cast<WNDPROC>(GetWindowLongPtrW(hwnd, GWLP_WNDPROC))
|
|
: reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hwnd, GWLP_WNDPROC));
|
|
if (g_unicode) {
|
|
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
|
} else {
|
|
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
|
}
|
|
hook_set_installed(g_id_wndproc, true);
|
|
|
|
if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) {
|
|
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow), g_id_foreground);
|
|
hook_export(user32, "GetActiveWindow", reinterpret_cast<void*>(&hk_GetActiveWindow), g_id_active);
|
|
hook_export(user32, "GetFocus", reinterpret_cast<void*>(&hk_GetFocus), g_id_focus);
|
|
|
|
if (void* clip = reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor"))) {
|
|
install_inline(g_hk_clipcursor, clip, &hk_ClipCursor);
|
|
hook_set_installed(g_id_clipcursor, static_cast<bool>(g_hk_clipcursor));
|
|
}
|
|
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos"))) {
|
|
install_inline(g_hk_setcursorpos, setpos, &hk_SetCursorPos);
|
|
hook_set_installed(g_id_setcursorpos, static_cast<bool>(g_hk_setcursorpos));
|
|
}
|
|
}
|
|
|
|
// Free any clip the game already set, so release takes effect immediately.
|
|
if (!ipc.cursor_clip_allowed()) {
|
|
ClipCursor(nullptr);
|
|
}
|
|
|
|
ipc.mark_focus_spoof(true, reinterpret_cast<std::uint64_t>(hwnd));
|
|
return true;
|
|
}
|
|
|
|
void update_input_diagnostics(IpcClient& ipc)
|
|
{
|
|
// Inspect how the game currently reads input, to find a focus-gated path that
|
|
// would explain a controller working only when the game has true focus.
|
|
bool raw_registered = false;
|
|
bool raw_gamepad = false;
|
|
bool raw_gamepad_sink = false;
|
|
|
|
UINT count = 0;
|
|
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0) {
|
|
std::vector<RAWINPUTDEVICE> devices(count);
|
|
const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE));
|
|
if (got != static_cast<UINT>(-1)) {
|
|
raw_registered = got > 0;
|
|
for (UINT i = 0; i < got; ++i) {
|
|
// Generic Desktop (0x01) joystick (0x04) / gamepad (0x05).
|
|
const bool is_pad =
|
|
devices[i].usUsagePage == 0x01 && (devices[i].usUsage == 0x04 || devices[i].usUsage == 0x05);
|
|
if (is_pad) {
|
|
raw_gamepad = true;
|
|
raw_gamepad_sink = (devices[i].dwFlags & RIDEV_INPUTSINK) != 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool dinput = GetModuleHandleW(L"dinput8.dll") != nullptr || GetModuleHandleW(L"dinput.dll") != nullptr;
|
|
ipc.set_input_diagnostics(raw_registered, raw_gamepad, raw_gamepad_sink, dinput);
|
|
}
|
|
|
|
void release_cursor_tick()
|
|
{
|
|
if (g_focus_ipc != nullptr && g_game_hwnd != nullptr && !g_focus_ipc->cursor_clip_allowed()) {
|
|
ClipCursor(nullptr); // routes through hk_ClipCursor -> frees the cursor
|
|
}
|
|
}
|
|
|
|
void remove_focus_spoof()
|
|
{
|
|
if (g_game_hwnd != nullptr && g_orig_proc != nullptr) {
|
|
if (g_unicode) {
|
|
SetWindowLongPtrW(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
|
} else {
|
|
SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
|
}
|
|
}
|
|
// Disable the inline focus hooks in REVERSE install order. GetForegroundWindow/GetActiveWindow
|
|
// share user32 code (the real GetForegroundWindow's path runs through GetActiveWindow's body), so
|
|
// GetActiveWindow's inline patch corrupts that shared code. GetForegroundWindow is installed
|
|
// first, so disabling in reverse unhooks GetActiveWindow (restoring the shared bytes) while
|
|
// GetForegroundWindow is still hooked -- its detour returns g_game_hwnd and never reaches the
|
|
// patched bytes. The reverse of the enable order (GFW first) keeps the invariant "GetActiveWindow
|
|
// hooked => GetForegroundWindow hooked" across the whole install/remove cycle, so a call never
|
|
// lands in a half-patched shared region.
|
|
for (auto it = g_focus_hooks.rbegin(); it != g_focus_hooks.rend(); ++it) {
|
|
disable_for_removal(*it);
|
|
}
|
|
disable_for_removal(g_hk_clipcursor);
|
|
disable_for_removal(g_hk_setcursorpos);
|
|
ClipCursor(nullptr); // leave the cursor free when the spoof is removed
|
|
hook_set_installed(g_id_foreground, false);
|
|
hook_set_installed(g_id_active, false);
|
|
hook_set_installed(g_id_focus, false);
|
|
hook_set_installed(g_id_wndproc, false);
|
|
hook_set_installed(g_id_clipcursor, false);
|
|
hook_set_installed(g_id_setcursorpos, false);
|
|
// The WNDPROC is restored and the inline hooks disabled above, so no NEW detour can start. Drain
|
|
// any focus / WNDPROC detour still in-flight on the game's window thread before nulling the
|
|
// state they read (g_orig_proc / g_game_hwnd / g_focus_ipc) -- otherwise a dispatch mid-flight
|
|
// could call a null original WNDPROC or a dangling IPC pointer.
|
|
g_gate.drain();
|
|
// The focus-query hooks (GetForegroundWindow/GetActiveWindow/GetFocus) return g_game_hwnd and
|
|
// never call the trampoline, so destroying them is safe; recreate on re-install. The cursor hooks
|
|
// DO call the trampoline, so keep them ALIVE (disabled) -- persistent, re-enabled on re-install
|
|
// (see hook_install.hpp) -- so a stale detour never hits a freed trampoline.
|
|
g_focus_hooks.clear();
|
|
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;
|
|
}
|
|
|
|
} // namespace coop::hook
|