Skip to content

fix(scripting): make Framework.Key input gating robust (alt-tab + reconnect)#233

Open
JEssou115 wants to merge 1 commit into
MafiaHub:developfrom
JEssou115:fix/framework-key-input-gate
Open

fix(scripting): make Framework.Key input gating robust (alt-tab + reconnect)#233
JEssou115 wants to merge 1 commit into
MafiaHub:developfrom
JEssou115:fix/framework-key-input-gate

Conversation

@JEssou115

@JEssou115 JEssou115 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

Two independent robustness fixes to Framework.Key (client keybinding) input gating, both in keybinds.cpp.

1. Don't read global key state while the window is backgrounded (alt-tab)

IsPhysicallyDown used GetAsyncKeyState, which returns a global key state: a bound key reads as "down" even when our window isn't focused, so pressing that key in another application still fired the binding. Key.isDown is documented as false while the window is backgrounded, but the implementation didn't honor it.

Fix: require our own process to own the foreground window (GetWindowThreadProcessId(GetForegroundWindow()) == GetCurrentProcessId()).

2. Keep the host input gate across a scripting-engine restart (reconnect)

The host registers an "active gate" once via SetActiveCallback(...) — a predicate that suppresses key dispatch while a UI/chat view owns input. That callback captures host state which outlives scripting sessions. But Keybinds::Shutdown() reset _activeCallback on every scripting-engine teardown (i.e. on every server disconnect), so after the first disconnect the gate was gone until a full process restart, and on reconnect bound keys fired again while typing into chat.

Fix: don't clear _activeCallback in Shutdown() (still clear the per-session _buckets and _resourceManager). The host can clear it explicitly with SetActiveCallback(nullptr) if needed.

Why

Found while building a client mod that binds letter keys (e.g. a debug fly toggle). Symptoms:

  • pressing the bound key in another app (after alt-tab) toggled the feature in-game;
  • after reconnecting to a server, the key started firing while typing into the chat box again.

Notes

  • Both changes are self-contained to keybinds.cpp and backward compatible.
  • #2 only removes an over-eager reset; a host that relied on the gate being cleared on shutdown can call SetActiveCallback(nullptr) explicitly.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Keybind detection now ignores key presses when the application is running in the background or has been switched away from.
    • Preserved callback activation state across shutdowns for more consistent scripting behavior.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JEssou115, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de03920d-b0d4-4415-96c9-c498443d3c8c

📥 Commits

Reviewing files that changed from the base of the PR and between 5f34a71 and 4610201.

📒 Files selected for processing (1)
  • code/framework/src/integrations/client/scripting/builtins/keybinds.cpp

Walkthrough

Keybind physical-state detection now requires the process to own the foreground window. Shutdown no longer clears the host-provided active callback, which remains externally managed across scripting sessions.

Changes

Keybind behavior

Layer / File(s) Summary
Key state and lifecycle handling
code/framework/src/integrations/client/scripting/builtins/keybinds.cpp
IsPhysicallyDown verifies foreground-window ownership before reporting a pressed key, while Shutdown preserves _activeCallback for external management.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit hops where key presses gleam,
Foreground focus guards the stream.
Shutdown leaves the callback bright,
Waiting through the session’s night.
“Set it outside,” the rabbit sings,
While safer bindings sprout new wings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Framework.Key input gating now handles alt-tab/background checks and reconnect behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
code/framework/src/integrations/client/scripting/builtins/keybinds.cpp (2)

430-435: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Comment is written in French while the rest of the file is English.

This block documents a subtle, important lifetime contract (_activeCallback outliving scripting sessions), but it's the only non-English comment in the file. Non-French-speaking maintainers won't be able to follow the rationale.

📝 Suggested translation
-        // NOTE: on NE réinitialise PAS _activeCallback ici. C'est un gate HOST (posé par le mod via
-        // SetActiveCallback) qui capture l'application, laquelle vit pour tout le process — bien au-delà
-        // d'une session scripting. L'effacer à chaque Shutdown (déconnexion) faisait perdre la
-        // suppression des keybinds après une RECONNEXION (le mod ne ré-installe le gate qu'au démarrage
-        // complet du jeu), d'où des hotkeys qui se déclenchaient à nouveau en tapant dans le chat.
-        // Le host peut toujours l'annuler explicitement via SetActiveCallback(nullptr) s'il le veut.
+        // NOTE: _activeCallback is intentionally NOT reset here. It's a HOST gate (set by the mod via
+        // SetActiveCallback) that captures application-level state living for the whole process — well
+        // beyond a single scripting session. Clearing it on every Shutdown (disconnect) used to lose the
+        // keybind suppression after a RECONNECT (the mod only re-installs the gate on full game startup),
+        // causing hotkeys to fire again while typing in chat.
+        // The host can still explicitly clear it via SetActiveCallback(nullptr) if desired.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/framework/src/integrations/client/scripting/builtins/keybinds.cpp`
around lines 430 - 435, Translate the explanatory comment above _activeCallback
into English, preserving its full lifetime contract: Shutdown must not reset the
host-installed callback, it must survive scripting reconnections, and
SetActiveCallback(nullptr) remains the explicit cancellation path.

117-127: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Foreground-ownership gating is correctly implemented; per-key syscall redundancy could be hoisted out of the loop.

The foreground-ownership check correctly rejects key state while backgrounded, including the edge case where GetForegroundWindow() returns NULL (the zero-initialized pid never matches a real PID). One optimization opportunity: Update() calls IsPhysicallyDown once per bucket, so GetForegroundWindow()/GetWindowThreadProcessId() are re-queried for every registered key every tick instead of once per tick.

♻️ Optional: compute foreground ownership once per Update() tick
-        bool IsPhysicallyDown(int vk) {
+        bool IsForegroundOwnedByUs() {
+            DWORD pid = 0;
+            ::GetWindowThreadProcessId(::GetForegroundWindow(), &pid);
+            return pid == ::GetCurrentProcessId();
+        }
+
+        bool IsPhysicallyDown(int vk, bool foregroundOwned = true) {
             if ((::GetAsyncKeyState(vk) & 0x8000) == 0) {
                 return false;
             }
-            // GetAsyncKeyState is a GLOBAL key state — it reads the key even while our window is in the
-            // background (alt-tab). Honor the documented "false while backgrounded" by requiring our own
-            // process to own the foreground window.
-            DWORD pid = 0;
-            ::GetWindowThreadProcessId(::GetForegroundWindow(), &pid);
-            return pid == ::GetCurrentProcessId();
+            return foregroundOwned;
         }

Then in Update(), compute const bool foregroundOwned = IsForegroundOwnedByUs(); once before the bucket loop and pass it through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/framework/src/integrations/client/scripting/builtins/keybinds.cpp`
around lines 117 - 127, Hoist the foreground-window ownership check out of the
per-key IsPhysicallyDown path: add or reuse an IsForegroundOwnedByUs helper,
compute its result once at the start of Update() before the bucket loop, and
pass that boolean into each key-state check while preserving false behavior when
the foreground window is unavailable or belongs to another process.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@code/framework/src/integrations/client/scripting/builtins/keybinds.cpp`:
- Around line 430-435: Translate the explanatory comment above _activeCallback
into English, preserving its full lifetime contract: Shutdown must not reset the
host-installed callback, it must survive scripting reconnections, and
SetActiveCallback(nullptr) remains the explicit cancellation path.
- Around line 117-127: Hoist the foreground-window ownership check out of the
per-key IsPhysicallyDown path: add or reuse an IsForegroundOwnedByUs helper,
compute its result once at the start of Update() before the bucket loop, and
pass that boolean into each key-state check while preserving false behavior when
the foreground window is unavailable or belongs to another process.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81fb75e8-84fe-492b-bdb5-5ac610ab6418

📥 Commits

Reviewing files that changed from the base of the PR and between b7786f6 and 5f34a71.

📒 Files selected for processing (1)
  • code/framework/src/integrations/client/scripting/builtins/keybinds.cpp

@Kheartz

Kheartz commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

I can't read french :(

@Segfaultd

Copy link
Copy Markdown
Member

Comments should actually be removed, its AI cancer comments. Code is quite self explanatory

Two issues let a bound key fire when it should have been suppressed:

1. Backgrounded window (alt-tab). IsPhysicallyDown used GetAsyncKeyState,
   which reports a global key state — the key reads as down even when our
   window is not focused, so a key pressed in another application still
   fired the binding. Require our own process to own the foreground window,
   honoring the documented "false while the window is backgrounded" contract.

2. Reconnect. Keybinds::Shutdown() reset _activeCallback (the host input
   gate) on every scripting-engine teardown. The host installs that gate
   once and it captures host state that outlives scripting sessions, so
   after the first disconnect the gate was gone until a full process
   restart — and bound keys were no longer suppressed while a UI/chat view
   owned input (e.g. a letter typed into chat also toggled its binding).
   Keep the host gate across scripting shutdown; the host can still clear it
   explicitly via SetActiveCallback(nullptr).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JEssou115 JEssou115 force-pushed the fix/framework-key-input-gate branch from 5d6e79a to 4610201 Compare July 11, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants