Skip to content

fix(input): Prevent modified key releases from firing plain hotkeys - #3233

Open
CryoTheRenegade wants to merge 4 commits into
TheSuperHackers:mainfrom
CryoTheRenegade:fix/modifier-key-release-fix
Open

CryoTheRenegade wants to merge 4 commits into
TheSuperHackers:mainfrom
CryoTheRenegade:fix/modifier-key-release-fix

Conversation

@CryoTheRenegade

@CryoTheRenegade CryoTheRenegade commented Aug 28, 2026 •

Copy link
Copy Markdown

Thanks to DrGoldFish, who reported this issue and tested the fix.

The bug can happen in this order using Legi's keybinds:

  1. Hold Ctrl.
  2. Press F.
  3. Release Ctrl.
  4. Release F.

The game can treat the last step as a normal F key press and run the F command. It should remember that F was pressed with Ctrl.

There is also a focus problem. If Ctrl or Shift is released while the game is not focused, the game may miss the release. This can leave force-attack or selection mode active.

The keyboard code reads several events at once. It previously gave every event the modifier state from the end of that group. This could give an event the wrong Ctrl, Shift, or Alt state.

The keyboard code now saves the modifier state when each event is handled. It remembers which modifier was held when a key was pressed. It passes that information to the matching key release.

HotKeyTranslator then knows that the release is part of a modified key press and does not run the normal hotkey.

This state is stored in Keyboard because another message handler may remove the key-down event before HotKeyTranslator receives it.

The reset code now sends key-up events for Ctrl, Shift, and Alt. The existing MetaEvent code is unchanged.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Prevent stuck modifier combos on out-of-order key releases

🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Tracks modified presses so combo UP mappings fire regardless of release order.
• Suppresses plain GUI hotkeys when releasing keys previously used in modifier combos.
• Synthesizes all modifier releases after focus loss and clears stale combo tracking.
Diagram

sequenceDiagram
    actor User
    participant Keyboard
    participant Stream as Message Stream
    participant Meta as Meta Events
    participant Hotkey as Hotkey Translator
    participant Manager as Hotkey Manager
    User->>Keyboard: Press modified key
    Keyboard->>Stream: Raw key down
    Stream->>Meta: Track combo state
    Meta->>Manager: Suppress key up
    User->>Keyboard: Release keys
    Keyboard->>Stream: Raw key up
    Stream->>Hotkey: Check GUI hotkey
    Hotkey->>Manager: Consume suppression
    Stream->>Meta: Resolve combo up
    Keyboard->>Stream: Synthetic modifier ups
    Stream->>Meta: Flush reset state
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Unified chord state machine
  • ➕ Centralizes modifier tracking, UP mapping, and GUI suppression ownership.
  • ➕ Reduces coordination through global translator state.
  • ➖ Requires a broad input-pipeline refactor with substantially higher regression risk.
  • ➖ Touches mature event ordering and message-disposition behavior beyond this bug.

Recommendation: Keep the PR's targeted coordination between existing translators because it preserves raw key-up propagation and limits risk. A unified chord state machine would be cleaner long term, but its larger scope is not justified for this compatibility-sensitive fix; add focused regression coverage if the input harness supports synthetic event sequences.

Files changed (6) +115 / -31

Bug fix (6) +115 / -31
HotKey.hAdd one-shot key-up suppression state +5/-0

Add one-shot key-up suppression state

• Extends HotKeyManager with per-key suppression storage and APIs to set, consume, and clear suppression. This lets modified releases bypass GUI hotkey execution exactly once.

Core/GameEngine/Include/GameClient/HotKey.h

Keyboard.hExpose keyboard reset generations +3/-1

Expose keyboard reset generations

• Adds a reset generation counter and generalizes the focus-recovery helper from ALT-only handling to all modifier keys. Translators can now detect that keyboard state was reset.

Core/GameEngine/Include/GameClient/Keyboard.h

MetaEvent.hTrack reset synchronization in meta events +2/-0

Track reset synchronization in meta events

• Adds the last observed keyboard reset generation and a helper for clearing tracked key-down combinations. These declarations support safe recovery after focus loss.

Core/GameEngine/Include/GameClient/MetaEvent.h

Keyboard.cppSynthesize releases for every held modifier +19/-12

Synthesize releases for every held modifier

• Initializes and increments the keyboard reset generation whenever key state is cleared. Focus recovery now emits raw key-up messages for held CTRL and SHIFT keys as well as ALT.

Core/GameEngine/Source/GameClient/Input/Keyboard.cpp

HotKey.cppSkip GUI hotkeys for modified releases +34/-5

Skip GUI hotkeys for modified releases

• Consumes one-shot suppression before translating a raw key-up into a plain GUI hotkey, while leaving the message available to later translators. Initializes and manages the per-key suppression array.

Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp

MetaEvent.cppPreserve combo state across release ordering +52/-13

Preserve combo state across release ordering

• Records modifier-only holds, suppresses GUI handling for modified key releases, and retains combo state through same-frame release ordering. It also detects keyboard resets, emits pending UP mappings, and clears stale tracking.

Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@greptile-apps

greptile-apps Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Changes keyboard input handling and hotkey dispatch order.

The PR appears safe to merge; no new actionable issue was found.

Summary

The PR records modifier state per keyboard event, sends modifier key-up events during reset, and runs plain hotkeys on key-down. It also places the hotkey translator before the meta-event translator so overlapping bindings can reach it.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Keyboard event] --> B[Capture modifier state]
  B --> C[Window translator]
  C --> D[Hotkey translator]
  D --> E[Meta-event translator]
Loading

Reviews (11) · Last reviewed commit: "fix(input): Release held modifiers when ..."

@xezon xezon 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.

I assume this was entirely generated by LLM? It looks like slop the way the new logic is laid out. It is incomprehensible and unmaintainable. It needs to be unsloppified.

@xezon

xezon commented Aug 31, 2026

Copy link
Copy Markdown

How do we know the new generated revision is no slop?

@CryoTheRenegade

Copy link
Copy Markdown
Author

Fair criticism. I used LLM assistance on the first pass, and I should have reviewed and simplified the result before asking you to review it. I own that.

I’ve since rewritten the fix. The hotkey translator is stateless now. Modifier press state lives in Keyboard, where buffered events are processed in order, and each release records whether its matching press used Ctrl, Shift, or Alt.

@xezon

xezon commented Sep 3, 2026

Copy link
Copy Markdown

I tried to understand this change but I was unable to. It is lacking context. What was the issue and how was it reproduced, how was it fixed and why is it fixed the way it was fixed.

@CryoTheRenegade CryoTheRenegade changed the title fix(input): Keep modifier combos from sticking when keys are released out of order fix(input): Prevent modified key releases from firing plain hotkeys Sep 4, 2026
@CryoTheRenegade

Copy link
Copy Markdown
Author

I've reworded the PR description with a simple example and an explanation of the cause, the fix, and why the state is stored in Keyboard.

@xezon xezon added Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Input labels Sep 10, 2026
Comment thread GeneralsMD/Code/GameEngine/Include/GameClient/KeyDefs.h Outdated
Comment thread Core/GameEngine/Include/GameClient/Keyboard.h Outdated
Comment thread Generals/Code/GameEngine/Include/GameClient/KeyDefs.h Outdated
Comment thread Core/GameEngine/Source/GameClient/Input/Keyboard.cpp
Comment thread Core/GameEngine/Source/GameClient/Input/Keyboard.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/Input/Keyboard.cpp Outdated
Comment thread Core/GameEngine/Include/GameClient/Keyboard.h Outdated
Comment thread Core/GameEngine/Include/Common/MessageStream.h Outdated
Comment thread Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/Input/Keyboard.cpp Outdated
@@ -74,33 +73,15 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage

if ( t == GameMessage::MSG_RAW_KEY_UP)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would MSG_RAW_KEY_DOWN perhaps be an option for hotkeys? All the MetaEvents are key down events. Or is there a good reason why hotkey needs to be posted on down?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Key-down could work. I found no comment explaining why hotkeys use key-up. I kept the current timing for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I suggest test MSG_RAW_KEY_DOWN. It would be the simpler option and get rid of the new stuff you added to accomodate this event. Players will also be happier if their key presses register faster.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bump.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e895f905-13d8-4e86-91da-268267be8fea

📥 Commits

Reviewing files that changed from the base of the PR and between b4ed7f3 and e52d4a4.

📒 Files selected for processing (3)
  • Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp
  • Generals/Code/GameEngine/Source/GameClient/GameClient.cpp
  • GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


Walkthrough

The changes add a KeyState alias and a modifier mask. Keyboard updates include modifier bits in key states, including repeat states, and emit key-up messages for tracked modifier keys during reset. Hotkey translation processes eligible raw key-down messages before meta-event translation.

Changes

Keyboard input state and message handling

Layer / File(s) Summary
Key-state types and declarations
Generals/.../GameClient/KeyDefs.h, GeneralsMD/.../GameClient/KeyDefs.h, Core/GameEngine/Include/Common/MessageStream.h, Core/GameEngine/Include/GameClient/Keyboard.h, Core/GameEngine/Include/GameClient/MetaEvent.h
The key definitions add KeyState and KEY_STATE_MODIFIERS. Keyboard and message declarations use KeyState for key-state values.
Modifier tracking and reset messages
Core/GameEngine/Source/GameClient/Input/Keyboard.cpp
Keyboard updates apply modifier bits before repeat processing. Reset handling emits raw key-up messages for tracked Ctrl, Shift, Alt, and configured shift-2 keys.
Key message translation and ordering
Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp, Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp, Generals/.../GameClient/GameClient.cpp, GeneralsMD/.../GameClient/GameClient.cpp
Hotkey translation handles raw key-down messages and filters modifier and autorepeat states. Meta-event handling uses KeyState. Both game variants register HotKeyTranslator before MetaEventTranslator.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Keyboard
  participant HotKeyTranslator
  participant MetaEventTranslator
  Keyboard->>HotKeyTranslator: Send raw key-down with key state
  HotKeyTranslator->>HotKeyTranslator: Filter modifier and autorepeat states
  HotKeyTranslator->>MetaEventTranslator: Continue message translation
Loading

Suggested reviewers: xezon

Merge Risk: ⚪ Minimal · up to e52d4

The change filters modified and repeated key presses and gives GUI hotkeys precedence. No actionable merge-blocking issue was identified; merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to e52d4

Keyboard commands now fire on press, and reset clears additional modifier states. Existing input restrictions and enabled-button checks remain in place. Risk is low, with remaining uncertainty about customized key-release bindings.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The traced effect is a change in precedence for local keyboard activation of existing GUI actions in both variants. Exact action exposure depends on registered command buttons and localized labels; downstream action authority was not exhaustively assessed.

Trust Boundaries and Controls

  • observed — WindowTranslator remains ahead of hotkey handling and retains its disabled-input consumption rule for non-Escape keys. Hotkey handling additionally rejects modifier-bearing and autorepeat presses and preserves visible/enabled window checks.

Resilience and Maintainability Implications

  • inferred — Reset queues UP messages for tracked Ctrl, Shift, Alt, and alternate-shift keys before clearing keyboard state. Repeated serial resets without intervening input therefore queue no duplicate modifier releases. When these messages reach MetaEvent, its existing modifier-removal path clears recorded combinations and emits applicable terminal UP events; delivery through every earlier consumer remains unverified.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the Ctrl+F reproduction case, missed modifier releases, stored modifier state, and hotkey handling changes. It directly matches the changeset.
Title check ✅ Passed The title clearly identifies the primary fix: preventing modified key releases from triggering plain hotkeys.
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.

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.

Comment thread Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp

@xezon xezon 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.

Ok I lost a oversight on this over time. If i am not mistaken this change now does 3 or more things:

  1. a KeyState type refactor (which is nice)
  2. Hotkey press fix
  3. Alt Tab key fix

And maybe something else.

Can you split all of these things into clean individual commits in an order that makes sense?

@CryoTheRenegade
CryoTheRenegade force-pushed the fix/modifier-key-release-fix branch from 67b80bd to b4ed7f3 Compare September 27, 2026 16:04
@CryoTheRenegade

Copy link
Copy Markdown
Author

I've split the changes into four commits, in this order:

  1. KeyState type refactor.
  2. Save the modifier state for each keyboard event.
  3. Run hotkeys on key-down, including the translator order change.
  4. Release held modifiers after focus loss.

@xezon xezon 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.

Looks promising. The split makes not entirely clear what a refactor is and what not, as it seems some refactors are mingled with non-refactor commits.

Comment thread Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp
Comment thread Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp
Comment thread Generals/Code/GameEngine/Source/GameClient/GameClient.cpp

// since we only allocate one of each, don't bother pooling 'em
m_translators[ m_numTranslators++ ] = TheMessageStream->attachTranslator( MSGNEW("GameClientSubsystem") WindowTranslator, 10 );
m_translators[ m_numTranslators++ ] = TheMessageStream->attachTranslator( MSGNEW("GameClientSubsystem") HotKeyTranslator, 15 );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you give an example what concrete button conflict this solves now or would solve on a conflict?

Comment thread Core/GameEngine/Source/GameClient/Input/Keyboard.cpp
Introduce KeyState and the combined modifier mask. Update the keyboard, meta-event, and hotkey types. Simplify the hotkey modifier check and name the buffered key and physical-modifier helper without changing event timing or modifier capture.
Capture modifiers for each buffered event instead of copying the final frame state to all events. For example, Ctrl-down, F-down, Ctrl-up in one frame must still mark F-down as modified. Include current modifiers on repeats and recognize the configured secondary shift key.
Run plain hotkeys on press and ignore repeats. Give active GUI buttons priority over overlapping meta commands after window input. Document why the event timing and translator order matter.
Extend Alt focus-reset handling to Ctrl, Shift, and a distinct secondary shift key. Emit releases before clearing the keyboard state.
@CryoTheRenegade
CryoTheRenegade force-pushed the fix/modifier-key-release-fix branch from b4ed7f3 to e52d4a4 Compare September 30, 2026 03:25

// since we only allocate one of each, don't bother pooling 'em
m_translators[ m_numTranslators++ ] = TheMessageStream->attachTranslator( MSGNEW("GameClientSubsystem") WindowTranslator, 10 );
// TheSuperHackers @fix Let active GUI hotkeys handle keys before MetaEventTranslator consumes overlapping bindings.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How did you come to the conclusion that this is the right thing to do?

If a unit has a button mapped on key "E", then pressing "E" will no longer select matching units for the selection: a deviation of the original, possibly confusing the player.

GameMessage::Type t = msg->getType();

if ( t == GameMessage::MSG_RAW_KEY_UP)
// TheSuperHackers @fix Run hotkeys on press so releasing a modifier first cannot trigger a plain hotkey.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Run hotkeys on press instead of release ..."

GameMessage::Type t = msg->getType();

if ( t == GameMessage::MSG_RAW_KEY_UP)
// TheSuperHackers @fix Run hotkeys on press so releasing a modifier first cannot trigger a plain hotkey.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can also say that the press is consistent with the typical meta event key mappings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals Input Minor Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants