-
Notifications
You must be signed in to change notification settings - Fork 254
fix(input): Prevent modified key releases from firing plain hotkeys #3233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3311ee3
d4687be
23e1ebc
3b71ae4
167bbc8
66d74be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,7 @@ struct KeyboardIO | |
| UnsignedByte key; // KeyDefType, key data | ||
| UnsignedByte status; // StatusType, above | ||
| UnsignedShort state; // KEY_STATE_* in KeyDefs.h | ||
| UnsignedShort pressedState; // Modifier flags from the matching press, for key-up events | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This field increases the struct size from 8 to 12 bytes. Can it not be outside of it, specific for down key presses? It is confusing why we need |
||
| UnsignedInt keyDownTimeMsec; // real-time in milliseconds when key went down | ||
|
|
||
| }; | ||
|
|
@@ -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 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 +141,7 @@ class Keyboard : public SubsystemInterface | |
| void setKeyStateData( KeyDefType key, UnsignedByte data ); ///< get key state | ||
|
|
||
| UnsignedShort m_modifiers; | ||
| UnsignedShort m_lastPressedKeyState[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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,8 @@ void Keyboard::createStreamMessages() | |
| { | ||
| msg->appendIntegerArgument( key->key ); | ||
| msg->appendIntegerArgument( key->state ); | ||
| if( BitIsSet( key->state, KEY_STATE_UP ) ) | ||
| msg->appendIntegerArgument( key->pressedState ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would |
||
| } | ||
|
|
||
| // next key please | ||
|
|
@@ -96,6 +98,23 @@ void Keyboard::createStreamMessages() | |
|
|
||
| } | ||
|
|
||
| //------------------------------------------------------------------------------------------------- | ||
| static Bool isCtrlShiftAltKey(KeyDefType key) | ||
| { | ||
| switch (key) | ||
| { | ||
| case KEY_LCTRL: | ||
| case KEY_RCTRL: | ||
| case KEY_LSHIFT: | ||
| case KEY_RSHIFT: | ||
| case KEY_LALT: | ||
| case KEY_RALT: | ||
| return TRUE; | ||
| } | ||
|
|
||
| return FALSE; | ||
| } | ||
|
|
||
| //------------------------------------------------------------------------------------------------- | ||
| /** update all our key state data */ | ||
| //------------------------------------------------------------------------------------------------- | ||
|
|
@@ -138,65 +157,64 @@ 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 isModifier = isCtrlShiftAltKey(key) || key == m_shift2Key; | ||
|
|
||
| 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 ) ) | ||
| { | ||
| 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 || isModifier ) | ||
|
|
||
| { | ||
|
|
||
| // | ||
| // 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 the current | ||
| // modifier state for each buffered event. | ||
| BitSet( m_keys[ index ].state, m_modifiers ); | ||
| m_keys[ index ].pressedState = KEY_STATE_NONE; | ||
| if( !isModifier ) | ||
| { | ||
| if( BitIsSet( m_keys[ index ].state, KEY_STATE_DOWN ) ) | ||
| { | ||
| m_lastPressedKeyState[key] = m_modifiers; | ||
| } | ||
| else | ||
| { | ||
| // Keep the press state separate so a modified release cannot fire a plain hotkey. | ||
| m_keys[ index ].pressedState = m_lastPressedKeyState[key]; | ||
| m_lastPressedKeyState[key] = KEY_STATE_NONE; | ||
| } | ||
| } | ||
|
|
||
| index++; | ||
|
|
||
| } | ||
|
|
||
| // 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 +251,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_keys[ index ].pressedState = KEY_STATE_NONE; | ||
| m_keys[ index ].status = KeyboardIO::STATUS_UNUSED; | ||
|
|
||
| // Set End Flag | ||
|
|
@@ -699,6 +719,7 @@ Keyboard::Keyboard() | |
|
|
||
| memset( m_keys, 0, sizeof( m_keys ) ); | ||
| memset( m_keyStatus, 0, sizeof( m_keyStatus ) ); | ||
| memset( m_lastPressedKeyState, 0, sizeof( m_lastPressedKeyState ) ); | ||
| m_modifiers = KEY_STATE_NONE; | ||
| m_shift2Key = KEY_NONE; | ||
|
|
||
|
|
@@ -749,13 +770,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. | ||
| refreshAltKeys(); | ||
| // CTRL and SHIFT have the same stuck-mode problem (force-attack, prefer-selection). | ||
| 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_lastPressedKeyState until that release or a new press arrives. | ||
| m_modifiers = KEY_STATE_NONE; | ||
| if( getCapsState() ) | ||
| { | ||
|
|
@@ -765,24 +788,32 @@ void Keyboard::resetKeys() | |
| } | ||
|
|
||
| //------------------------------------------------------------------------------------------------- | ||
| // Refresh the state of the alt keys, necessary after alt tab | ||
| //------------------------------------------------------------------------------------------------- | ||
| void Keyboard::refreshAltKeys() const | ||
| static void emitRawKeyUpIfDown(const KeyboardIO *keyStatus, KeyDefType key) | ||
| { | ||
| if (BitIsSet(m_keyStatus[KEY_LALT].state, KEY_STATE_DOWN)) | ||
| { | ||
| 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)) | ||
| if (BitIsSet(keyStatus[key].state, KEY_STATE_DOWN)) | ||
| { | ||
| GameMessage* msg = TheMessageStream->appendMessage(GameMessage::MSG_RAW_KEY_UP); | ||
| msg->appendIntegerArgument(KEY_RALT); | ||
| msg->appendIntegerArgument(key); | ||
| msg->appendIntegerArgument(KEY_STATE_UP); | ||
| msg->appendIntegerArgument(KEY_STATE_NONE); | ||
| } | ||
| } | ||
|
|
||
| //------------------------------------------------------------------------------------------------- | ||
| // Emit RAW_KEY_UP for still-held modifiers so MetaEvent can end force-attack / waypoints / etc. | ||
| //------------------------------------------------------------------------------------------------- | ||
| void Keyboard::emitModifierKeyUps() 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); | ||
|
xezon marked this conversation as resolved.
|
||
| if (m_shift2Key != KEY_NONE && !isCtrlShiftAltKey(m_shift2Key)) | ||
| emitRawKeyUpIfDown(m_keyStatus, m_shift2Key); | ||
| } | ||
|
|
||
| //------------------------------------------------------------------------------------------------- | ||
| /** get the first key in our current state of the keyboard */ | ||
| //------------------------------------------------------------------------------------------------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -74,33 +73,15 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage | |
|
|
||
| if ( t == GameMessage::MSG_RAW_KEY_UP) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| { | ||
|
|
||
| //char key = msg->getArgument(0)->integer; | ||
| Int keyState = msg->getArgument(1)->integer; | ||
|
|
||
| // 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 ) | ||
| { | ||
| newModState |= CTRL; | ||
| } | ||
|
|
||
| if( keyState & KEY_STATE_SHIFT ) | ||
| { | ||
| newModState |= SHIFT; | ||
| } | ||
|
|
||
| if( keyState & KEY_STATE_ALT ) | ||
| { | ||
| newModState |= ALT; | ||
| } | ||
| if(newModState != 0) | ||
| const KeyDefType key = (KeyDefType)msg->getArgument(0)->integer; | ||
| const Int keyState = msg->getArgument(1)->integer; | ||
| const Int pressedKeyState = msg->getArgument(2)->integer; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe cast it to the correct types. |
||
| if( (keyState | pressedKeyState) & KEY_STATE_MODIFIERS ) | ||
| 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)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest put a
typedef UnsignedShort KeyState;below the KEY_STATE_* enum and then use that type to refer to the KeyStates everywhere it is used.