From 3311ee3c39da32d80ac37de0683950bdf3331780 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:25:15 -0600 Subject: [PATCH 1/4] fix(input): Keep modifier combos from sticking when keys are released out of order Signed-off-by: Jacob Ledbetter --- Core/GameEngine/Include/GameClient/HotKey.h | 5 ++ Core/GameEngine/Include/GameClient/Keyboard.h | 4 +- .../GameEngine/Include/GameClient/MetaEvent.h | 2 + .../Source/GameClient/Input/Keyboard.cpp | 31 +++++---- .../GameClient/MessageStream/HotKey.cpp | 39 +++++++++-- .../GameClient/MessageStream/MetaEvent.cpp | 65 +++++++++++++++---- 6 files changed, 115 insertions(+), 31 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index 70206f24cf6..1394449a4c3 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -94,13 +94,18 @@ class HotKeyManager : public SubsystemInterface void addHotKey( GameWindow *win, const AsciiString& key); Bool executeHotKey( const AsciiString& key); // called front eh HotKeyTranslator + void setKeyUpSuppressed(KeyDefType key, Bool suppressed); + Bool consumeKeyUpSuppression(KeyDefType key); AsciiString searchHotKey( const AsciiString& label); AsciiString searchHotKey( const UnicodeString& uStr ); private: + void clearKeyUpSuppressions(); + typedef std::map HotKeyMap; HotKeyMap m_hotKeyMap; + Bool m_suppressKeyUp[KEY_COUNT]; }; extern HotKeyManager *TheHotKeyManager; //----------------------------------------------------------------------------- diff --git a/Core/GameEngine/Include/GameClient/Keyboard.h b/Core/GameEngine/Include/GameClient/Keyboard.h index 8c21aae40b0..ee4d7b66d71 100644 --- a/Core/GameEngine/Include/GameClient/Keyboard.h +++ b/Core/GameEngine/Include/GameClient/Keyboard.h @@ -113,6 +113,7 @@ class Keyboard : public SubsystemInterface Bool isCtrl(); Bool isAlt(); Int getModifierFlags() { return m_modifiers; } + UnsignedInt getResetGeneration() const { return m_resetGeneration; } // access methods for key data void resetKeys(); ///< reset the state of the keys @@ -124,7 +125,7 @@ class Keyboard : public SubsystemInterface WideChar getPrintableKey( KeyDefType key, Int state ); enum { MAX_KEY_STATES = 3}; private: - void refreshAltKeys() const; ///< refresh the state of the alt keys, necessary after alt tab + void refreshModifierKeys() const; ///< refresh held CTRL/SHIFT/ALT keys, necessary after alt tab protected: /** get the key data for a single key, KEY_NONE should be returned when @@ -140,6 +141,7 @@ class Keyboard : public SubsystemInterface void setKeyStateData( KeyDefType key, UnsignedByte data ); ///< get key state UnsignedShort m_modifiers; + UnsignedInt m_resetGeneration; // internal keyboard data members //Bool m_capsState; // 1 if caps lock is on //Bool m_shiftState; // 1 if either shift key is pressed diff --git a/Core/GameEngine/Include/GameClient/MetaEvent.h b/Core/GameEngine/Include/GameClient/MetaEvent.h index a610c4f5e7d..ce83c5263ce 100644 --- a/Core/GameEngine/Include/GameClient/MetaEvent.h +++ b/Core/GameEngine/Include/GameClient/MetaEvent.h @@ -432,6 +432,7 @@ class MetaEventTranslator : public GameMessageTranslator }; KeyDownInfo m_keyDownInfos[KEY_COUNT]; + UnsignedInt m_keyboardResetGeneration; enum { NUM_MOUSE_BUTTONS = 3 }; ICoord2D m_mouseDownPosition[NUM_MOUSE_BUTTONS]; @@ -445,6 +446,7 @@ class MetaEventTranslator : public GameMessageTranslator private: void onMouseEvent(const GameMessage *msg); + void resetKeyDownInfos(); void onKeyEvent(const GameMessage *msg, GameMessageDisposition &disp); void onKeyModStateRemoved(GameMessageDisposition &disp, MappableKeyModState keyModState); void onKeyPressed(GameMessageDisposition &disp, Int systemKeyState, MappableKeyType keyType, MappableKeyModState keyModState); diff --git a/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp b/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp index ea1c5e4425a..6e78a017508 100644 --- a/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp +++ b/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp @@ -700,6 +700,7 @@ Keyboard::Keyboard() memset( m_keys, 0, sizeof( m_keys ) ); memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); m_modifiers = KEY_STATE_NONE; + m_resetGeneration = 0; m_shift2Key = KEY_NONE; memset( m_keyNames, 0, sizeof( m_keyNames ) ); @@ -752,7 +753,8 @@ void Keyboard::resetKeys() // TheSuperHackers @fix Caball009 13/12/2025 Fix bug where game remains in waypoint mode // because the key up state for the alt key is not detected after alt tab. - refreshAltKeys(); + refreshModifierKeys(); + ++m_resetGeneration; memset( m_keys, 0, sizeof( m_keys ) ); memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); @@ -765,21 +767,26 @@ void Keyboard::resetKeys() } //------------------------------------------------------------------------------------------------- -// Refresh the state of the alt keys, necessary after alt tab +// Refresh the state of held modifier keys, necessary after alt tab / focus loss //------------------------------------------------------------------------------------------------- -void Keyboard::refreshAltKeys() const +void Keyboard::refreshModifierKeys() const { - if (BitIsSet(m_keyStatus[KEY_LALT].state, KEY_STATE_DOWN)) + static const KeyDefType modifierKeys[] = { - GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP); - msg->appendIntegerArgument(KEY_LALT); - msg->appendIntegerArgument(KEY_STATE_UP); - } - if (BitIsSet(m_keyStatus[KEY_RALT].state, KEY_STATE_DOWN)) + KEY_LCTRL, KEY_RCTRL, + KEY_LSHIFT, KEY_RSHIFT, + KEY_LALT, KEY_RALT + }; + + for (Int i = 0; i < ARRAY_SIZE(modifierKeys); ++i) { - GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP); - msg->appendIntegerArgument(KEY_RALT); - msg->appendIntegerArgument(KEY_STATE_UP); + const KeyDefType key = modifierKeys[i]; + if (BitIsSet(m_keyStatus[key].state, KEY_STATE_DOWN)) + { + GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP); + msg->appendIntegerArgument(key); + msg->appendIntegerArgument(KEY_STATE_UP); + } } } diff --git a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp index 0b7ecf22d1e..6cda3e07cb8 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp @@ -74,9 +74,14 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage if ( t == GameMessage::MSG_RAW_KEY_UP) { - - //char key = msg->getArgument(0)->integer; + KeyDefType key = (KeyDefType)msg->getArgument(0)->integer; Int keyState = msg->getArgument(1)->integer; + Bool suppressKeyUp = TheHotKeyManager && TheHotKeyManager->consumeKeyUpSuppression(key); + + // TheSuperHackers @bugfix CryoTheRenegade 18/08/2026 + // Suppress only the hotkey action so the raw key release remains visible to later translators. + if (suppressKeyUp) + return disp; // for our purposes here, we don't care to distinguish between right and left keys, // so just fudge a little to simplify things. @@ -98,9 +103,9 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage } if(newModState != 0) return disp; - WideChar key = TheKeyboard->getPrintableKey((KeyDefType)msg->getArgument(0)->integer, 0); + WideChar printableKey = TheKeyboard->getPrintableKey(key, 0); UnicodeString uKey; - uKey.concat(key); + uKey.concat(printableKey); AsciiString aKey; aKey.translate(uKey); if(TheHotKeyManager && TheHotKeyManager->executeHotKey(aKey)) @@ -119,7 +124,7 @@ HotKey::HotKey() //----------------------------------------------------------------------------- HotKeyManager::HotKeyManager() { - + clearKeyUpSuppressions(); } //----------------------------------------------------------------------------- @@ -132,6 +137,7 @@ HotKeyManager::~HotKeyManager() void HotKeyManager::init() { m_hotKeyMap.clear(); + clearKeyUpSuppressions(); } //----------------------------------------------------------------------------- @@ -140,6 +146,29 @@ void HotKeyManager::reset() m_hotKeyMap.clear(); } +//----------------------------------------------------------------------------- +void HotKeyManager::setKeyUpSuppressed(KeyDefType key, Bool suppressed) +{ + m_suppressKeyUp[key] = suppressed; +} + +//----------------------------------------------------------------------------- +Bool HotKeyManager::consumeKeyUpSuppression(KeyDefType key) +{ + const Bool suppressKeyUp = m_suppressKeyUp[key]; + m_suppressKeyUp[key] = FALSE; + return suppressKeyUp; +} + +//----------------------------------------------------------------------------- +void HotKeyManager::clearKeyUpSuppressions() +{ + for (Int key = 0; key < ARRAY_SIZE(m_suppressKeyUp); ++key) + { + m_suppressKeyUp[key] = FALSE; + } +} + //----------------------------------------------------------------------------- void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn) { diff --git a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 31a954e67b5..a1db8ccb9eb 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -49,6 +49,8 @@ #include "GameClient/GUICallbacks.h" #include "GameClient/DebugDisplay.h" // for AudioDebugDisplay #include "GameClient/GameText.h" +#include "GameClient/HotKey.h" +#include "GameClient/Keyboard.h" #include "GameClient/MetaEvent.h" #include "GameLogic/GameLogic.h" // for TheGameLogic->getFrame() @@ -56,11 +58,6 @@ #define dont_DUMP_ALL_KEYS_TO_LOG - -#ifdef DUMP_ALL_KEYS_TO_LOG -#include "GameClient/Keyboard.h" -#endif - MetaMap *TheMetaMap = nullptr; @@ -382,6 +379,8 @@ static const FieldParse TheMetaMapFieldParseTable[] = //------------------------------------------------------------------------------------------------- MetaEventTranslator::MetaEventTranslator() { + m_keyboardResetGeneration = TheKeyboard ? TheKeyboard->getResetGeneration() : 0; + for (Int i = 0; i < NUM_MOUSE_BUTTONS; ++i) { m_nextUpShouldCreateDoubleClick[i] = FALSE; } @@ -531,9 +530,31 @@ void MetaEventTranslator::onMouseEvent(const GameMessage *msg) } } +//------------------------------------------------------------------------------------------------- +void MetaEventTranslator::resetKeyDownInfos() +{ + for (Int key = 0; key < ARRAY_SIZE(m_keyDownInfos); ++key) + { + m_keyDownInfos[key].reset(); + } +} + //------------------------------------------------------------------------------------------------- void MetaEventTranslator::onKeyEvent(const GameMessage *msg, GameMessageDisposition &disp) { + if (TheKeyboard) + { + const UnsignedInt resetGeneration = TheKeyboard->getResetGeneration(); + if (m_keyboardResetGeneration != resetGeneration) + { + // Emit UP mappings for any still-held CTRL/SHIFT/ALT combos before dropping tracking. + GameMessageDisposition unusedDisp = KEEP_MESSAGE; + onKeyModStateRemoved(unusedDisp, NONE); + resetKeyDownInfos(); + m_keyboardResetGeneration = resetGeneration; + } + } + const Int systemKey = msg->getArgument(0)->integer; const Int systemKeyState = msg->getArgument(1)->integer; @@ -665,20 +686,38 @@ void MetaEventTranslator::onKeyPressed(GameMessageDisposition &disp, Int systemK DEBUG_LOG(("^%s ", aKey.str())); #endif - if (keyModState != NONE) + if (!(systemKeyState & KEY_STATE_AUTOREPEAT)) { - // Remember that this key and mod state are pressed. - m_keyDownInfos[keyType].setKeyModState(keyModState); + if (keyType != MK_NONE) + { + m_keyDownInfos[keyType].reset(); + + if (TheHotKeyManager) + { + // TheSuperHackers @bugfix CryoTheRenegade 18/08/2026 + // Keep modified key releases from being reinterpreted as plain GUI hotkeys. + TheHotKeyManager->setKeyUpSuppressed((KeyDefType)keyType, keyModState != NONE); + } + } + + if (keyModState != NONE) + { + // Remember that this key and mod state are pressed. + // MK_NONE records CTRL, SHIFT, and ALT themselves so their UP mappings still fire on release. + m_keyDownInfos[keyType].setKeyModState(keyModState); + } } } else { - if (keyModState != NONE) + if (keyType != MK_NONE) { - DEBUG_ASSERTCRASH(keyType != MK_NONE, ("Key is expected to be not MK_NONE")); - - // Forget that this key and mod state are pressed. - m_keyDownInfos[keyType].clearKeyModState(keyModState); + // TheSuperHackers @bugfix CryoTheRenegade 19/08/2026 + // A same-frame modifier release can follow this key-up with the final modifier bits already cleared. + if (keyModState != NONE || !m_keyDownInfos[keyType].isKeyDown()) + { + m_keyDownInfos[keyType].reset(); + } } } } From d4687be23e425be2825cf0671d4582e25cded82d Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Fri, 28 Aug 2026 10:07:00 -0600 Subject: [PATCH 2/4] fix(input): Include KeyDefs and add KeyDownInfo::reset for CI HotKey.h referenced KeyDefType/KEY_COUNT without the key header, and MetaEvent.cpp called a reset helper that was never declared. Co-authored-by: Cursor --- Core/GameEngine/Include/GameClient/HotKey.h | 1 + Core/GameEngine/Include/GameClient/MetaEvent.h | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index 1394449a4c3..925a9a81a29 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -55,6 +55,7 @@ //----------------------------------------------------------------------------- #include "Common/SubsystemInterface.h" #include "Common/MessageStream.h" +#include "GameClient/KeyDefs.h" //----------------------------------------------------------------------------- // FORWARD REFERENCES ///////////////////////////////////////////////////////// //----------------------------------------------------------------------------- diff --git a/Core/GameEngine/Include/GameClient/MetaEvent.h b/Core/GameEngine/Include/GameClient/MetaEvent.h index ce83c5263ce..a0e350d333c 100644 --- a/Core/GameEngine/Include/GameClient/MetaEvent.h +++ b/Core/GameEngine/Include/GameClient/MetaEvent.h @@ -427,6 +427,11 @@ class MetaEventTranslator : public GameMessageTranslator BitClear(m_modStateBits, 1 << toIndex(modState)); } + void reset() + { + m_modStateBits = 0; + } + private: UnsignedByte m_modStateBits; ///< Fits all combinations of CTRL+ALT+SHIFT, storing 1 bit for each }; From 23e1ebc2f8351cd57797ebefa4d241d2ef92c9e2 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Mon, 31 Aug 2026 09:52:58 -0600 Subject: [PATCH 3/4] fix(input): Drop the extra translator bookkeeping from the modifier fix Keep the GUI-hotkey and focus-loss behavior, but store the modifier-down flag on HotKeyTranslator itself and reuse the existing alt-tab key-up path for CTRL and SHIFT. Co-authored-by: Cursor --- Core/GameEngine/Include/GameClient/HotKey.h | 10 +-- Core/GameEngine/Include/GameClient/Keyboard.h | 4 +- .../GameEngine/Include/GameClient/MetaEvent.h | 7 -- .../Source/GameClient/Input/Keyboard.cpp | 41 ++++++----- .../GameClient/MessageStream/HotKey.cpp | 71 ++++++------------- .../GameClient/MessageStream/MetaEvent.cpp | 65 ++++------------- 6 files changed, 60 insertions(+), 138 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index 925a9a81a29..f01a6a2f252 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -67,8 +67,13 @@ class GameWindow; class HotKeyTranslator : public GameMessageTranslator { public: + HotKeyTranslator(); virtual GameMessageDisposition translateGameMessage(const GameMessage *msg) override; virtual ~HotKeyTranslator() override { } + +private: + // True if this key's last real (non-repeat) down included CTRL, SHIFT, or ALT. + Bool m_downWithModifier[KEY_COUNT]; }; //----------------------------------------------------------------------------- @@ -95,18 +100,13 @@ class HotKeyManager : public SubsystemInterface void addHotKey( GameWindow *win, const AsciiString& key); Bool executeHotKey( const AsciiString& key); // called front eh HotKeyTranslator - void setKeyUpSuppressed(KeyDefType key, Bool suppressed); - Bool consumeKeyUpSuppression(KeyDefType key); AsciiString searchHotKey( const AsciiString& label); AsciiString searchHotKey( const UnicodeString& uStr ); private: - void clearKeyUpSuppressions(); - typedef std::map HotKeyMap; HotKeyMap m_hotKeyMap; - Bool m_suppressKeyUp[KEY_COUNT]; }; extern HotKeyManager *TheHotKeyManager; //----------------------------------------------------------------------------- diff --git a/Core/GameEngine/Include/GameClient/Keyboard.h b/Core/GameEngine/Include/GameClient/Keyboard.h index ee4d7b66d71..70570762ce8 100644 --- a/Core/GameEngine/Include/GameClient/Keyboard.h +++ b/Core/GameEngine/Include/GameClient/Keyboard.h @@ -113,7 +113,6 @@ class Keyboard : public SubsystemInterface Bool isCtrl(); Bool isAlt(); Int getModifierFlags() { return m_modifiers; } - UnsignedInt getResetGeneration() const { return m_resetGeneration; } // access methods for key data void resetKeys(); ///< reset the state of the keys @@ -125,7 +124,7 @@ class Keyboard : public SubsystemInterface WideChar getPrintableKey( KeyDefType key, Int state ); enum { MAX_KEY_STATES = 3}; private: - void refreshModifierKeys() const; ///< refresh held CTRL/SHIFT/ALT keys, necessary after alt tab + void refreshAltKeys() const; ///< emit key-ups for held CTRL/SHIFT/ALT after alt-tab protected: /** get the key data for a single key, KEY_NONE should be returned when @@ -141,7 +140,6 @@ class Keyboard : public SubsystemInterface void setKeyStateData( KeyDefType key, UnsignedByte data ); ///< get key state UnsignedShort m_modifiers; - UnsignedInt m_resetGeneration; // internal keyboard data members //Bool m_capsState; // 1 if caps lock is on //Bool m_shiftState; // 1 if either shift key is pressed diff --git a/Core/GameEngine/Include/GameClient/MetaEvent.h b/Core/GameEngine/Include/GameClient/MetaEvent.h index a0e350d333c..a610c4f5e7d 100644 --- a/Core/GameEngine/Include/GameClient/MetaEvent.h +++ b/Core/GameEngine/Include/GameClient/MetaEvent.h @@ -427,17 +427,11 @@ class MetaEventTranslator : public GameMessageTranslator BitClear(m_modStateBits, 1 << toIndex(modState)); } - void reset() - { - m_modStateBits = 0; - } - private: UnsignedByte m_modStateBits; ///< Fits all combinations of CTRL+ALT+SHIFT, storing 1 bit for each }; KeyDownInfo m_keyDownInfos[KEY_COUNT]; - UnsignedInt m_keyboardResetGeneration; enum { NUM_MOUSE_BUTTONS = 3 }; ICoord2D m_mouseDownPosition[NUM_MOUSE_BUTTONS]; @@ -451,7 +445,6 @@ class MetaEventTranslator : public GameMessageTranslator private: void onMouseEvent(const GameMessage *msg); - void resetKeyDownInfos(); void onKeyEvent(const GameMessage *msg, GameMessageDisposition &disp); void onKeyModStateRemoved(GameMessageDisposition &disp, MappableKeyModState keyModState); void onKeyPressed(GameMessageDisposition &disp, Int systemKeyState, MappableKeyType keyType, MappableKeyModState keyModState); diff --git a/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp b/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp index 6e78a017508..d88ea0c2330 100644 --- a/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp +++ b/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp @@ -700,7 +700,6 @@ Keyboard::Keyboard() memset( m_keys, 0, sizeof( m_keys ) ); memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); m_modifiers = KEY_STATE_NONE; - m_resetGeneration = 0; m_shift2Key = KEY_NONE; memset( m_keyNames, 0, sizeof( m_keyNames ) ); @@ -753,8 +752,8 @@ void Keyboard::resetKeys() // TheSuperHackers @fix Caball009 13/12/2025 Fix bug where game remains in waypoint mode // because the key up state for the alt key is not detected after alt tab. - refreshModifierKeys(); - ++m_resetGeneration; + // CTRL and SHIFT have the same stuck-mode problem (force-attack, prefer-selection). + refreshAltKeys(); memset( m_keys, 0, sizeof( m_keys ) ); memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); @@ -767,29 +766,29 @@ void Keyboard::resetKeys() } //------------------------------------------------------------------------------------------------- -// Refresh the state of held modifier keys, necessary after alt tab / focus loss -//------------------------------------------------------------------------------------------------- -void Keyboard::refreshModifierKeys() const +static void emitRawKeyUpIfDown(const KeyboardIO *keyStatus, KeyDefType key) { - static const KeyDefType modifierKeys[] = - { - KEY_LCTRL, KEY_RCTRL, - KEY_LSHIFT, KEY_RSHIFT, - KEY_LALT, KEY_RALT - }; - - for (Int i = 0; i < ARRAY_SIZE(modifierKeys); ++i) + if (BitIsSet(keyStatus[key].state, KEY_STATE_DOWN)) { - const KeyDefType key = modifierKeys[i]; - if (BitIsSet(m_keyStatus[key].state, KEY_STATE_DOWN)) - { - GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP); - msg->appendIntegerArgument(key); - msg->appendIntegerArgument(KEY_STATE_UP); - } + GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP); + msg->appendIntegerArgument(key); + msg->appendIntegerArgument(KEY_STATE_UP); } } +//------------------------------------------------------------------------------------------------- +// Emit RAW_KEY_UP for still-held modifiers so MetaEvent can end force-attack / waypoints / etc. +//------------------------------------------------------------------------------------------------- +void Keyboard::refreshAltKeys() const +{ + emitRawKeyUpIfDown(m_keyStatus, KEY_LCTRL); + emitRawKeyUpIfDown(m_keyStatus, KEY_RCTRL); + emitRawKeyUpIfDown(m_keyStatus, KEY_LSHIFT); + emitRawKeyUpIfDown(m_keyStatus, KEY_RSHIFT); + emitRawKeyUpIfDown(m_keyStatus, KEY_LALT); + emitRawKeyUpIfDown(m_keyStatus, KEY_RALT); +} + //------------------------------------------------------------------------------------------------- /** get the first key in our current state of the keyboard */ //------------------------------------------------------------------------------------------------- diff --git a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp index 6cda3e07cb8..ed04bd233af 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp @@ -66,43 +66,38 @@ // PUBLIC FUNCTIONS /////////////////////////////////////////////////////////// //----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HotKeyTranslator::HotKeyTranslator() +{ + memset(m_downWithModifier, 0, sizeof(m_downWithModifier)); +} + //----------------------------------------------------------------------------- GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage *msg) { GameMessageDisposition disp = KEEP_MESSAGE; GameMessage::Type t = msg->getType(); - if ( t == GameMessage::MSG_RAW_KEY_UP) + if ( t == GameMessage::MSG_RAW_KEY_DOWN || t == GameMessage::MSG_RAW_KEY_UP) { - KeyDefType key = (KeyDefType)msg->getArgument(0)->integer; - Int keyState = msg->getArgument(1)->integer; - Bool suppressKeyUp = TheHotKeyManager && TheHotKeyManager->consumeKeyUpSuppression(key); + const KeyDefType key = (KeyDefType)msg->getArgument(0)->integer; + const Int keyState = msg->getArgument(1)->integer; + const Bool hasModifier = (keyState & (KEY_STATE_CONTROL | KEY_STATE_SHIFT | KEY_STATE_ALT)) != 0; - // TheSuperHackers @bugfix CryoTheRenegade 18/08/2026 - // Suppress only the hotkey action so the raw key release remains visible to later translators. - if (suppressKeyUp) - return disp; - - // for our purposes here, we don't care to distinguish between right and left keys, - // so just fudge a little to simplify things. - Int newModState = 0; - - if( keyState & KEY_STATE_CONTROL ) + if ( t == GameMessage::MSG_RAW_KEY_DOWN) { - newModState |= CTRL; - } - - if( keyState & KEY_STATE_SHIFT ) - { - newModState |= SHIFT; + // TheSuperHackers @bugfix CryoTheRenegade 31/08/2026 + // CTRL+F must not fire the command-bar F hotkey on release. + if( (keyState & KEY_STATE_AUTOREPEAT) == 0 ) + m_downWithModifier[key] = hasModifier; + return disp; } - if( keyState & KEY_STATE_ALT ) - { - newModState |= ALT; - } - if(newModState != 0) + const Bool downWithModifier = m_downWithModifier[key]; + m_downWithModifier[key] = FALSE; + if( downWithModifier || hasModifier ) return disp; + WideChar printableKey = TheKeyboard->getPrintableKey(key, 0); UnicodeString uKey; uKey.concat(printableKey); @@ -124,7 +119,7 @@ HotKey::HotKey() //----------------------------------------------------------------------------- HotKeyManager::HotKeyManager() { - clearKeyUpSuppressions(); + } //----------------------------------------------------------------------------- @@ -137,7 +132,6 @@ HotKeyManager::~HotKeyManager() void HotKeyManager::init() { m_hotKeyMap.clear(); - clearKeyUpSuppressions(); } //----------------------------------------------------------------------------- @@ -146,29 +140,6 @@ void HotKeyManager::reset() m_hotKeyMap.clear(); } -//----------------------------------------------------------------------------- -void HotKeyManager::setKeyUpSuppressed(KeyDefType key, Bool suppressed) -{ - m_suppressKeyUp[key] = suppressed; -} - -//----------------------------------------------------------------------------- -Bool HotKeyManager::consumeKeyUpSuppression(KeyDefType key) -{ - const Bool suppressKeyUp = m_suppressKeyUp[key]; - m_suppressKeyUp[key] = FALSE; - return suppressKeyUp; -} - -//----------------------------------------------------------------------------- -void HotKeyManager::clearKeyUpSuppressions() -{ - for (Int key = 0; key < ARRAY_SIZE(m_suppressKeyUp); ++key) - { - m_suppressKeyUp[key] = FALSE; - } -} - //----------------------------------------------------------------------------- void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn) { diff --git a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index a1db8ccb9eb..31a954e67b5 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -49,8 +49,6 @@ #include "GameClient/GUICallbacks.h" #include "GameClient/DebugDisplay.h" // for AudioDebugDisplay #include "GameClient/GameText.h" -#include "GameClient/HotKey.h" -#include "GameClient/Keyboard.h" #include "GameClient/MetaEvent.h" #include "GameLogic/GameLogic.h" // for TheGameLogic->getFrame() @@ -58,6 +56,11 @@ #define dont_DUMP_ALL_KEYS_TO_LOG + +#ifdef DUMP_ALL_KEYS_TO_LOG +#include "GameClient/Keyboard.h" +#endif + MetaMap *TheMetaMap = nullptr; @@ -379,8 +382,6 @@ static const FieldParse TheMetaMapFieldParseTable[] = //------------------------------------------------------------------------------------------------- MetaEventTranslator::MetaEventTranslator() { - m_keyboardResetGeneration = TheKeyboard ? TheKeyboard->getResetGeneration() : 0; - for (Int i = 0; i < NUM_MOUSE_BUTTONS; ++i) { m_nextUpShouldCreateDoubleClick[i] = FALSE; } @@ -530,31 +531,9 @@ void MetaEventTranslator::onMouseEvent(const GameMessage *msg) } } -//------------------------------------------------------------------------------------------------- -void MetaEventTranslator::resetKeyDownInfos() -{ - for (Int key = 0; key < ARRAY_SIZE(m_keyDownInfos); ++key) - { - m_keyDownInfos[key].reset(); - } -} - //------------------------------------------------------------------------------------------------- void MetaEventTranslator::onKeyEvent(const GameMessage *msg, GameMessageDisposition &disp) { - if (TheKeyboard) - { - const UnsignedInt resetGeneration = TheKeyboard->getResetGeneration(); - if (m_keyboardResetGeneration != resetGeneration) - { - // Emit UP mappings for any still-held CTRL/SHIFT/ALT combos before dropping tracking. - GameMessageDisposition unusedDisp = KEEP_MESSAGE; - onKeyModStateRemoved(unusedDisp, NONE); - resetKeyDownInfos(); - m_keyboardResetGeneration = resetGeneration; - } - } - const Int systemKey = msg->getArgument(0)->integer; const Int systemKeyState = msg->getArgument(1)->integer; @@ -686,38 +665,20 @@ void MetaEventTranslator::onKeyPressed(GameMessageDisposition &disp, Int systemK DEBUG_LOG(("^%s ", aKey.str())); #endif - if (!(systemKeyState & KEY_STATE_AUTOREPEAT)) + if (keyModState != NONE) { - if (keyType != MK_NONE) - { - m_keyDownInfos[keyType].reset(); - - if (TheHotKeyManager) - { - // TheSuperHackers @bugfix CryoTheRenegade 18/08/2026 - // Keep modified key releases from being reinterpreted as plain GUI hotkeys. - TheHotKeyManager->setKeyUpSuppressed((KeyDefType)keyType, keyModState != NONE); - } - } - - if (keyModState != NONE) - { - // Remember that this key and mod state are pressed. - // MK_NONE records CTRL, SHIFT, and ALT themselves so their UP mappings still fire on release. - m_keyDownInfos[keyType].setKeyModState(keyModState); - } + // Remember that this key and mod state are pressed. + m_keyDownInfos[keyType].setKeyModState(keyModState); } } else { - if (keyType != MK_NONE) + if (keyModState != NONE) { - // TheSuperHackers @bugfix CryoTheRenegade 19/08/2026 - // A same-frame modifier release can follow this key-up with the final modifier bits already cleared. - if (keyModState != NONE || !m_keyDownInfos[keyType].isKeyDown()) - { - m_keyDownInfos[keyType].reset(); - } + DEBUG_ASSERTCRASH(keyType != MK_NONE, ("Key is expected to be not MK_NONE")); + + // Forget that this key and mod state are pressed. + m_keyDownInfos[keyType].clearKeyModState(keyModState); } } } From 3b71ae4d8612ddde2d0b6a7181a06b82dac4c196 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Mon, 31 Aug 2026 11:27:59 -0600 Subject: [PATCH 4/4] Fix modifier state tracking for hotkey releases --- Core/GameEngine/Include/GameClient/HotKey.h | 6 -- Core/GameEngine/Include/GameClient/Keyboard.h | 3 +- .../Source/GameClient/Input/Keyboard.cpp | 74 +++++++++++-------- .../GameClient/MessageStream/HotKey.cpp | 25 +------ .../GameEngine/Include/GameClient/KeyDefs.h | 1 + .../GameEngine/Include/GameClient/KeyDefs.h | 1 + 6 files changed, 49 insertions(+), 61 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index f01a6a2f252..70206f24cf6 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -55,7 +55,6 @@ //----------------------------------------------------------------------------- #include "Common/SubsystemInterface.h" #include "Common/MessageStream.h" -#include "GameClient/KeyDefs.h" //----------------------------------------------------------------------------- // FORWARD REFERENCES ///////////////////////////////////////////////////////// //----------------------------------------------------------------------------- @@ -67,13 +66,8 @@ class GameWindow; class HotKeyTranslator : public GameMessageTranslator { public: - HotKeyTranslator(); virtual GameMessageDisposition translateGameMessage(const GameMessage *msg) override; virtual ~HotKeyTranslator() override { } - -private: - // True if this key's last real (non-repeat) down included CTRL, SHIFT, or ALT. - Bool m_downWithModifier[KEY_COUNT]; }; //----------------------------------------------------------------------------- diff --git a/Core/GameEngine/Include/GameClient/Keyboard.h b/Core/GameEngine/Include/GameClient/Keyboard.h index 70570762ce8..8b150e10c66 100644 --- a/Core/GameEngine/Include/GameClient/Keyboard.h +++ b/Core/GameEngine/Include/GameClient/Keyboard.h @@ -124,7 +124,7 @@ class Keyboard : public SubsystemInterface WideChar getPrintableKey( KeyDefType key, Int state ); enum { MAX_KEY_STATES = 3}; private: - void refreshAltKeys() const; ///< emit key-ups for held CTRL/SHIFT/ALT after alt-tab + void emitModifierKeyUps() const; ///< emit key-ups for held CTRL/SHIFT/ALT after focus loss protected: /** get the key data for a single key, KEY_NONE should be returned when @@ -140,6 +140,7 @@ class Keyboard : public SubsystemInterface void setKeyStateData( KeyDefType key, UnsignedByte data ); ///< get key state UnsignedShort m_modifiers; + Bool m_pressedWithModifier[KEY_COUNT]; // internal keyboard data members //Bool m_capsState; // 1 if caps lock is on //Bool m_shiftState; // 1 if either shift key is pressed diff --git a/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp b/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp index d88ea0c2330..219773dd9a2 100644 --- a/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp +++ b/Core/GameEngine/Source/GameClient/Input/Keyboard.cpp @@ -138,17 +138,20 @@ void Keyboard::updateKeys() /** @todo -- if we don't have focus, we could destroy all the keys retrieved here so that we don't process anything */ - m_keyStatus[ m_keys[ index ].key ].state = m_keys[ index ].state; - m_keyStatus[ m_keys[ index ].key ].status = m_keys[ index ].status; + const KeyDefType key = (KeyDefType)m_keys[ index ].key; + const Bool pressedWithModifier = m_pressedWithModifier[key]; + + m_keyStatus[ key ].state = m_keys[ index ].state; + m_keyStatus[ key ].status = m_keys[ index ].status; // Update key down time for new key presses if( BitIsSet( m_keys[ index ].state, KEY_STATE_DOWN ) ) { - m_keyStatus[ m_keys[ index ].key ].keyDownTimeMsec = m_keys[ index ].keyDownTimeMsec; + m_keyStatus[ key ].keyDownTimeMsec = m_keys[ index ].keyDownTimeMsec; } // prevent ALT-TAB from causing a TAB event - if( m_keys[ index ].key == KEY_TAB ) + if( key == KEY_TAB ) { if( BitIsSet( m_keyStatus[ KEY_LALT ].state, KEY_STATE_DOWN ) || BitIsSet( m_keyStatus[ KEY_RALT ].state, KEY_STATE_DOWN ) ) @@ -156,13 +159,13 @@ void Keyboard::updateKeys() m_keys[index].status = KeyboardIO::STATUS_USED; } } - else if( m_keys[ index ].key == KEY_CAPS || - m_keys[ index ].key == KEY_LCTRL || - m_keys[ index ].key == KEY_RCTRL || - m_keys[ index ].key == KEY_LSHIFT || - m_keys[ index ].key == KEY_RSHIFT || - m_keys[ index ].key == KEY_LALT || - m_keys[ index ].key == KEY_RALT ) + else if( key == KEY_CAPS || + key == KEY_LCTRL || + key == KEY_RCTRL || + key == KEY_LSHIFT || + key == KEY_RSHIFT || + key == KEY_LALT || + key == KEY_RALT ) { @@ -170,8 +173,27 @@ void Keyboard::updateKeys() // this keeps our internal key state accurate event though we don't // use the returned translation ... kinda weird I think // - translateKey( m_keys[ index ].key ); + translateKey( key ); + + } + // TheSuperHackers @bugfix CryoTheRenegade 31/08/2026 Preserve modifier state for + // each buffered event and carry it from a key-down to its matching key-up. + BitSet( m_keys[ index ].state, m_modifiers ); + if( BitIsSet( m_keys[ index ].state, KEY_STATE_DOWN ) ) + { + const Int keyModifiers = KEY_STATE_CONTROL | KEY_STATE_SHIFT | KEY_STATE_ALT; + m_pressedWithModifier[key] = (m_modifiers & keyModifiers) != 0; + if( m_pressedWithModifier[key] ) + { + BitSet( m_keys[ index ].state, KEY_STATE_MODIFIER_ON_DOWN ); + } + } + else + { + m_pressedWithModifier[key] = FALSE; + if( pressedWithModifier ) + BitSet( m_keys[ index ].state, KEY_STATE_MODIFIER_ON_DOWN ); } index++; @@ -181,22 +203,6 @@ void Keyboard::updateKeys() // check for key repeats checkKeyRepeat(); - if( m_modifiers ) - { - index = 0; - while( m_keys[ index ].key != KEY_NONE ) - { - - // set in the modifier data into the already existing up/down state - BitSet( m_keys[ index ].state, m_modifiers ); - - // next key - index++; - - } - - } - } //------------------------------------------------------------------------------------------------- @@ -233,7 +239,9 @@ Bool Keyboard::checkKeyRepeat() { // Add key to this frame m_keys[ index ].key = (UnsignedByte)key; - m_keys[ index ].state = KEY_STATE_DOWN | KEY_STATE_AUTOREPEAT; // note: not a bitset; this is an assignment + // This is an assignment, not a bit set. + m_keys[ index ].state = KEY_STATE_DOWN | KEY_STATE_AUTOREPEAT | m_modifiers + | (m_pressedWithModifier[key] ? KEY_STATE_MODIFIER_ON_DOWN : 0); m_keys[ index ].status = KeyboardIO::STATUS_UNUSED; // Set End Flag @@ -699,6 +707,7 @@ Keyboard::Keyboard() memset( m_keys, 0, sizeof( m_keys ) ); memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); + memset( m_pressedWithModifier, 0, sizeof( m_pressedWithModifier ) ); m_modifiers = KEY_STATE_NONE; m_shift2Key = KEY_NONE; @@ -749,14 +758,15 @@ void Keyboard::update() //------------------------------------------------------------------------------------------------- void Keyboard::resetKeys() { - // TheSuperHackers @fix Caball009 13/12/2025 Fix bug where game remains in waypoint mode // because the key up state for the alt key is not detected after alt tab. // CTRL and SHIFT have the same stuck-mode problem (force-attack, prefer-selection). - refreshAltKeys(); + emitModifierKeyUps(); memset( m_keys, 0, sizeof( m_keys ) ); memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); + // A held key can still report its release after focus returns. Do not clear + // m_pressedWithModifier until that release or a new press arrives. m_modifiers = KEY_STATE_NONE; if( getCapsState() ) { @@ -779,7 +789,7 @@ static void emitRawKeyUpIfDown(const KeyboardIO *keyStatus, KeyDefType key) //------------------------------------------------------------------------------------------------- // Emit RAW_KEY_UP for still-held modifiers so MetaEvent can end force-attack / waypoints / etc. //------------------------------------------------------------------------------------------------- -void Keyboard::refreshAltKeys() const +void Keyboard::emitModifierKeyUps() const { emitRawKeyUpIfDown(m_keyStatus, KEY_LCTRL); emitRawKeyUpIfDown(m_keyStatus, KEY_RCTRL); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp index ed04bd233af..9c3dc0d1e56 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp @@ -52,7 +52,6 @@ //----------------------------------------------------------------------------- #include "GameClient/HotKey.h" #include "GameClient/KeyDefs.h" -#include "GameClient/MetaEvent.h" #include "GameClient/GameWindow.h" #include "GameClient/GameWindowManager.h" #include "GameClient/Keyboard.h" @@ -66,36 +65,18 @@ // PUBLIC FUNCTIONS /////////////////////////////////////////////////////////// //----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -HotKeyTranslator::HotKeyTranslator() -{ - memset(m_downWithModifier, 0, sizeof(m_downWithModifier)); -} - //----------------------------------------------------------------------------- GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage *msg) { GameMessageDisposition disp = KEEP_MESSAGE; GameMessage::Type t = msg->getType(); - if ( t == GameMessage::MSG_RAW_KEY_DOWN || t == GameMessage::MSG_RAW_KEY_UP) + if ( t == GameMessage::MSG_RAW_KEY_UP) { const KeyDefType key = (KeyDefType)msg->getArgument(0)->integer; const Int keyState = msg->getArgument(1)->integer; - const Bool hasModifier = (keyState & (KEY_STATE_CONTROL | KEY_STATE_SHIFT | KEY_STATE_ALT)) != 0; - - if ( t == GameMessage::MSG_RAW_KEY_DOWN) - { - // TheSuperHackers @bugfix CryoTheRenegade 31/08/2026 - // CTRL+F must not fire the command-bar F hotkey on release. - if( (keyState & KEY_STATE_AUTOREPEAT) == 0 ) - m_downWithModifier[key] = hasModifier; - return disp; - } - - const Bool downWithModifier = m_downWithModifier[key]; - m_downWithModifier[key] = FALSE; - if( downWithModifier || hasModifier ) + const Int ignoredModifiers = KEY_STATE_CONTROL | KEY_STATE_SHIFT | KEY_STATE_ALT | KEY_STATE_MODIFIER_ON_DOWN; + if( keyState & ignoredModifiers ) return disp; WideChar printableKey = TheKeyboard->getPrintableKey(key, 0); diff --git a/Generals/Code/GameEngine/Include/GameClient/KeyDefs.h b/Generals/Code/GameEngine/Include/GameClient/KeyDefs.h index 3c9f14394de..c195dd0312b 100644 --- a/Generals/Code/GameEngine/Include/GameClient/KeyDefs.h +++ b/Generals/Code/GameEngine/Include/GameClient/KeyDefs.h @@ -248,6 +248,7 @@ enum KEY_STATE_AUTOREPEAT = 0x0100, // Key is down due to autorepeat (only seen in conjunction with KEY_STATE_DOWN) KEY_STATE_CAPSLOCK = 0x0200, // Caps Lock key is on. KEY_STATE_SHIFT2 = 0x0400, // Alternate shift key is pressed (I think this is for foreign keyboards..) + KEY_STATE_MODIFIER_ON_DOWN = 0x0800, // CTRL, SHIFT, or ALT was held when this key was pressed. // modifier combinations when left/right isn't a factor KEY_STATE_CONTROL = (KEY_STATE_LCONTROL | KEY_STATE_RCONTROL), diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/KeyDefs.h b/GeneralsMD/Code/GameEngine/Include/GameClient/KeyDefs.h index 9f1978d20e0..cca8b33100b 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/KeyDefs.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/KeyDefs.h @@ -248,6 +248,7 @@ enum KEY_STATE_AUTOREPEAT = 0x0100, // Key is down due to autorepeat (only seen in conjunction with KEY_STATE_DOWN) KEY_STATE_CAPSLOCK = 0x0200, // Caps Lock key is on. KEY_STATE_SHIFT2 = 0x0400, // Alternate shift key is pressed (I think this is for foreign keyboards..) + KEY_STATE_MODIFIER_ON_DOWN = 0x0800, // CTRL, SHIFT, or ALT was held when this key was pressed. // modifier combinations when left/right isn't a factor KEY_STATE_CONTROL = (KEY_STATE_LCONTROL | KEY_STATE_RCONTROL),