Release the operator cursor for cursor-clipping games
Games that ClipCursor / re-center via SetCursorPos while focused trap the operator's mouse (the focus spoof makes them think they're always focused), so the operator can't reach the overlay. The Focus subsystem now inline-hooks ClipCursor and SetCursorPos (stdcall trampolines): while "release" is requested it forces ClipCursor(NULL) and swallows the re-centering SetCursorPos; otherwise it passes them through. It frees any existing clip at install and re-frees each worker tick (covers one-time clippers and a runtime clip->release toggle). Host: protocol v10->v11 adds HookControl::allow_cursor_clip (0 = release, the default). The Injection panel gets a "Release operator cursor" checkbox and an F2 hotkey (InjectionPanel::toggle_cursor_release); default released, since the guest plays via the pad so the game's clip is operator-only. Verified: full build x64 + x86 clean; ctest x64 9/9, x86 3/3. The cursor behavior against a real clipping game (Trails through Daybreak) needs a live injected session to confirm; logic reviewed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,12 @@ 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;
|
||||
|
||||
struct EnumContext
|
||||
{
|
||||
@@ -125,6 +131,28 @@ HWND WINAPI hk_GetFocus()
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)))
|
||||
@@ -148,6 +176,8 @@ bool install_focus_spoof(IpcClient& ipc)
|
||||
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)
|
||||
@@ -172,6 +202,23 @@ bool install_focus_spoof(IpcClient& ipc)
|
||||
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")))
|
||||
{
|
||||
g_hk_clipcursor = safetyhook::create_inline(clip, reinterpret_cast<void*>(&hk_ClipCursor));
|
||||
hook_set_installed(g_id_clipcursor, static_cast<bool>(g_hk_clipcursor));
|
||||
}
|
||||
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos")))
|
||||
{
|
||||
g_hk_setcursorpos = safetyhook::create_inline(setpos, reinterpret_cast<void*>(&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));
|
||||
@@ -212,6 +259,14 @@ void update_input_diagnostics(IpcClient& ipc)
|
||||
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)
|
||||
@@ -226,10 +281,15 @@ void remove_focus_spoof()
|
||||
}
|
||||
}
|
||||
g_focus_hooks.clear();
|
||||
g_hk_clipcursor = {}; // restore ClipCursor / SetCursorPos before clearing state
|
||||
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);
|
||||
if (g_focus_ipc != nullptr)
|
||||
{
|
||||
g_focus_ipc->mark_focus_spoof(false, 0);
|
||||
|
||||
Reference in New Issue
Block a user