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:
@@ -181,6 +181,7 @@ void InjectionPanel::inject_selected()
|
||||
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Video, want_video_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Mkb, want_mkb_);
|
||||
server_.set_cursor_clip_allowed(!release_cursor_);
|
||||
|
||||
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
||||
if (result.status == InjectStatus::Ok)
|
||||
@@ -399,6 +400,13 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Cursor release is a Focus sub-option for games that clip/recenter the mouse
|
||||
// (e.g. Trails through Daybreak), which would otherwise trap the operator.
|
||||
if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_))
|
||||
{
|
||||
server_.set_cursor_clip_allowed(!release_cursor_);
|
||||
}
|
||||
}
|
||||
|
||||
void InjectionPanel::draw_hook_status(bool debug_details)
|
||||
|
||||
@@ -128,6 +128,21 @@ public:
|
||||
server_.push_mkb(ev);
|
||||
}
|
||||
|
||||
// --- Cursor release (for cursor-clipping games) ----------------------------
|
||||
|
||||
// Toggle whether the operator's mouse is released from the game's cursor clip
|
||||
// (bound to a hotkey in the host). Republishes to the hook immediately.
|
||||
void toggle_cursor_release()
|
||||
{
|
||||
release_cursor_ = !release_cursor_;
|
||||
server_.set_cursor_clip_allowed(!release_cursor_);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool cursor_released() const
|
||||
{
|
||||
return release_cursor_;
|
||||
}
|
||||
|
||||
private:
|
||||
void refresh_targets(); // refresh both the window list and the process list
|
||||
void refresh_processes();
|
||||
@@ -159,6 +174,7 @@ private:
|
||||
bool want_audio_ = true;
|
||||
bool want_video_ = false; // Present-hook video path: opt-in (WGC is the default)
|
||||
bool want_mkb_ = false; // mouse+keyboard forwarding: opt-in
|
||||
bool release_cursor_ = true; // free the operator's mouse from the game's clip (default)
|
||||
bool injected_ = false; // a hook DLL is loaded in the target
|
||||
|
||||
// Heartbeat liveness tracking (is the injected DLL responding?).
|
||||
|
||||
@@ -87,6 +87,16 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Let the game clip/position the cursor (true) or release the operator's mouse
|
||||
// (false, the default). No-op if not started.
|
||||
void set_cursor_clip_allowed(bool allowed)
|
||||
{
|
||||
if (block_ != nullptr)
|
||||
{
|
||||
block_->control.allow_cursor_clip.store(allowed ? 1u : 0u, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
// Drain new log lines streamed by the hook, calling `emit(const LogRecord&)`
|
||||
// for each. No-op if not started. Header-only so the callback can stay generic.
|
||||
template <typename F>
|
||||
|
||||
@@ -184,6 +184,10 @@ int run()
|
||||
overlay_hidden_at = ImGui::GetTime();
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F2, false))
|
||||
{
|
||||
injection.toggle_cursor_release(); // free/clip the operator's mouse for clipping games
|
||||
}
|
||||
|
||||
if (show_overlay)
|
||||
{
|
||||
|
||||
@@ -104,6 +104,7 @@ float draw_main_menu_bar(UiState& ui, const FrameStats& stats)
|
||||
if (ImGui::BeginMenu("Help"))
|
||||
{
|
||||
ImGui::TextDisabled("F1 hide/show this overlay");
|
||||
ImGui::TextDisabled("F2 release/clip the operator cursor");
|
||||
ImGui::TextDisabled("Esc quit");
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("This window is what Remote Play");
|
||||
|
||||
Reference in New Issue
Block a user