Skip to content

Commit d7f7203

Browse files
authored
fix(metaevent): Ignore order in which modifier keys are released to trigger meta events (TheSuperHackers#2577)
1 parent 8a06f35 commit d7f7203

5 files changed

Lines changed: 311 additions & 72 deletions

File tree

Core/Libraries/Include/Lib/BaseType.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ inline Real deg2rad(Real rad) { return rad * (PI/180); }
139139
//-----------------------------------------------------------------------------
140140
// TheSuperHackers @build xezon 17/03/2025 Renames BitTest to BitIsSet to prevent conflict with BitTest macro from winnt.h
141141
#define BitIsSet( x, i ) ( ( (x) & (i) ) != 0 )
142+
#define BitsAreSet( x, i ) ( ( (x) & (i) ) == (i) )
142143
#define BitSet( x, i ) ( (x) |= (i) )
143144
#define BitClear( x, i ) ( (x ) &= ~(i) )
144145
#define BitToggle( x, i ) ( (x) ^= (i) )

Generals/Code/GameEngine/Include/GameClient/MetaEvent.h

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,84 @@ EMPTY_DTOR(MetaMapRec)
354354
class MetaEventTranslator : public GameMessageTranslator
355355
{
356356
private:
357-
358-
Int m_lastKeyDown; // really a MappableKeyType
359-
Int m_lastModState; // really a MappableKeyModState
357+
struct KeyDownInfo
358+
{
359+
KeyDownInfo() : m_modStateBits(0) {}
360+
361+
static UnsignedInt getMaxKeyModStateCount()
362+
{
363+
return 7;
364+
}
365+
366+
static MappableKeyModState toKeyModState(UnsignedInt index)
367+
{
368+
switch (index)
369+
{
370+
case 0: return CTRL;
371+
case 1: return ALT;
372+
case 2: return SHIFT;
373+
case 3: return CTRL_ALT;
374+
case 4: return SHIFT_CTRL;
375+
case 5: return SHIFT_ALT;
376+
case 6: return SHIFT_ALT_CTRL;
377+
}
378+
return NONE;
379+
}
380+
381+
static UnsignedInt toIndex(MappableKeyModState modState)
382+
{
383+
switch (modState)
384+
{
385+
case CTRL: return 0;
386+
case ALT: return 1;
387+
case SHIFT: return 2;
388+
case CTRL_ALT: return 3;
389+
case SHIFT_CTRL: return 4;
390+
case SHIFT_ALT: return 5;
391+
case SHIFT_ALT_CTRL: return 6;
392+
}
393+
return 7;
394+
}
395+
396+
Bool isKeyDown() const
397+
{
398+
return m_modStateBits != 0;
399+
}
400+
401+
MappableKeyModState getKeyModState(UnsignedInt index)
402+
{
403+
if (BitIsSet(m_modStateBits, 1 << index))
404+
{
405+
return toKeyModState(index);
406+
}
407+
return NONE;
408+
}
409+
410+
void clearKeyModState(UnsignedInt index)
411+
{
412+
BitClear(m_modStateBits, 1 << index);
413+
}
414+
415+
Bool hasKeyModState(MappableKeyModState modState) const
416+
{
417+
return BitIsSet(m_modStateBits, 1 << toIndex(modState));
418+
}
419+
420+
void setKeyModState(MappableKeyModState modState)
421+
{
422+
BitSet(m_modStateBits, 1 << toIndex(modState));
423+
}
424+
425+
void clearKeyModState(MappableKeyModState modState)
426+
{
427+
BitClear(m_modStateBits, 1 << toIndex(modState));
428+
}
429+
430+
private:
431+
UnsignedByte m_modStateBits; ///< Fits all combinations of CTRL+ALT+SHIFT, storing 1 bit for each
432+
};
433+
434+
KeyDownInfo m_keyDownInfos[KEY_COUNT];
360435

361436
enum { NUM_MOUSE_BUTTONS = 3 };
362437
ICoord2D m_mouseDownPosition[NUM_MOUSE_BUTTONS];

Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,11 @@ static const FieldParse TheMetaMapFieldParseTable[] =
379379
// PUBLIC FUNCTIONS ///////////////////////////////////////////////////////////////////////////////
380380

381381
//-------------------------------------------------------------------------------------------------
382-
MetaEventTranslator::MetaEventTranslator() :
383-
m_lastKeyDown(MK_NONE),
384-
m_lastModState(0)
382+
MetaEventTranslator::MetaEventTranslator()
385383
{
386384
for (Int i = 0; i < NUM_MOUSE_BUTTONS; ++i) {
387385
m_nextUpShouldCreateDoubleClick[i] = FALSE;
388386
}
389-
390-
391387
}
392388

393389
//-------------------------------------------------------------------------------------------------
@@ -443,8 +439,20 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
443439

444440
if (t == GameMessage::MSG_RAW_KEY_DOWN || t == GameMessage::MSG_RAW_KEY_UP)
445441
{
446-
MappableKeyType key = (MappableKeyType)msg->getArgument(0)->integer;
447-
Int keyState = msg->getArgument(1)->integer;
442+
const Int systemKey = msg->getArgument(0)->integer;
443+
const Int keyState = msg->getArgument(1)->integer;
444+
445+
MappableKeyType key = (MappableKeyType)systemKey;
446+
switch (systemKey)
447+
{
448+
case KEY_LCTRL:
449+
case KEY_RCTRL:
450+
case KEY_LSHIFT:
451+
case KEY_RSHIFT:
452+
case KEY_LALT:
453+
case KEY_RALT:
454+
key = MK_NONE;
455+
}
448456

449457
// for our purposes here, we don't care to distinguish between right and left keys,
450458
// so just fudge a little to simplify things.
@@ -465,28 +473,58 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
465473
newModState |= ALT;
466474
}
467475

468-
for (const MetaMapRec *map = TheMetaMap->getFirstMetaMapRec(); map; map = map->m_next)
476+
const Bool modStateRemoved = (key == MK_NONE) && (t == GameMessage::MSG_RAW_KEY_UP);
477+
478+
if (modStateRemoved)
469479
{
470-
if (!isMessageUsable(map->m_usableIn))
471-
continue;
480+
// TheSuperHackers @fix The key handler now ignores the order in which modifier keys are released.
481+
// This avoids frustrating experiences where a wrong button release order would skip an important key event.
472482

473-
// check for the special case of mods-only-changed.
474-
if (
475-
map->m_key == MK_NONE &&
476-
newModState != m_lastModState &&
477-
(
478-
(map->m_transition == UP && map->m_modState == m_lastModState) ||
479-
(map->m_transition == DOWN && map->m_modState == newModState)
480-
)
481-
)
483+
for (Int keyDownIndex = 0; keyDownIndex < ARRAY_SIZE(m_keyDownInfos); ++keyDownIndex)
482484
{
483-
//DEBUG_LOG(("Frame %d: MetaEventTranslator::translateGameMessage() Mods-only change: %s", TheGameLogic->getFrame(), findGameMessageNameByType(map->m_meta)));
484-
/*GameMessage *metaMsg =*/ TheMessageStream->appendMessage(map->m_meta);
485-
disp = DESTROY_MESSAGE;
486-
break;
485+
const MappableKeyType keyDown = (MappableKeyType)keyDownIndex;
486+
KeyDownInfo &keyDownInfo = m_keyDownInfos[keyDownIndex];
487+
488+
if (!keyDownInfo.isKeyDown())
489+
continue;
490+
491+
for (UnsignedInt modStateIndex = 0; modStateIndex < KeyDownInfo::getMaxKeyModStateCount(); ++modStateIndex)
492+
{
493+
const MappableKeyModState keyDownModState = keyDownInfo.getKeyModState(modStateIndex);
494+
495+
if (keyDownModState == NONE)
496+
continue;
497+
498+
if (BitsAreSet(newModState, keyDownModState))
499+
continue;
500+
501+
// Forget that this key and mod state are pressed.
502+
keyDownInfo.clearKeyModState(modStateIndex);
503+
504+
for (const MetaMapRec *map = TheMetaMap->getFirstMetaMapRec(); map; map = map->m_next)
505+
{
506+
if (!isMessageUsable(map->m_usableIn))
507+
continue;
508+
509+
if (!(map->m_key == keyDown && map->m_modState == keyDownModState && map->m_transition == UP))
510+
continue;
511+
512+
TheMessageStream->appendMessage(map->m_meta);
513+
disp = DESTROY_MESSAGE;
514+
}
515+
}
487516
}
517+
}
518+
else
519+
{
520+
// TheSuperHackers @info The regular key handler only triggers events when the mapped key is pressed,
521+
// not when the modifier (CTRL, ALT, SHIFT) is pressed, unless the key is MK_NONE.
522+
523+
for (const MetaMapRec *map = TheMetaMap->getFirstMetaMapRec(); map; map = map->m_next)
524+
{
525+
if (!isMessageUsable(map->m_usableIn))
526+
continue;
488527

489-
// ok, now check for "normal" key transitions.
490528
if (
491529
map->m_key == key &&
492530
map->m_modState == newModState &&
@@ -497,7 +535,6 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
497535
)
498536
)
499537
{
500-
501538
if( keyState & KEY_STATE_AUTOREPEAT )
502539
{
503540
// if it's an autorepeat of a "known" key, don't generate the meta-event,
@@ -538,13 +575,8 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
538575
}
539576
}
540577

541-
542-
543578
if (t == GameMessage::MSG_RAW_KEY_DOWN)
544579
{
545-
m_lastKeyDown = key;
546-
547-
548580
#ifdef DUMP_ALL_KEYS_TO_LOG
549581

550582
WideChar Wkey = TheKeyboard->getPrintableKey(key, 0);
@@ -554,12 +586,24 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
554586
aKey.translate(uKey);
555587
DEBUG_LOG(("^%s ", aKey.str()));
556588
#endif
557-
589+
if (newModState != NONE)
590+
{
591+
// Remember that this key and mod state are pressed.
592+
m_keyDownInfos[key].setKeyModState((MappableKeyModState)newModState);
593+
}
558594
}
595+
else
596+
{
597+
if (newModState != NONE)
598+
{
599+
DEBUG_ASSERTCRASH(key != MK_NONE, ("Key is expected to be not MK_NONE"));
559600

601+
// Forget that this key and mod state are pressed.
602+
m_keyDownInfos[key].clearKeyModState((MappableKeyModState)newModState);
603+
}
604+
}
560605

561-
562-
m_lastModState = newModState;
606+
}
563607
}
564608

565609

GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,84 @@ EMPTY_DTOR(MetaMapRec)
354354
class MetaEventTranslator : public GameMessageTranslator
355355
{
356356
private:
357-
358-
Int m_lastKeyDown; // really a MappableKeyType
359-
Int m_lastModState; // really a MappableKeyModState
357+
struct KeyDownInfo
358+
{
359+
KeyDownInfo() : m_modStateBits(0) {}
360+
361+
static UnsignedInt getMaxKeyModStateCount()
362+
{
363+
return 7;
364+
}
365+
366+
static MappableKeyModState toKeyModState(UnsignedInt index)
367+
{
368+
switch (index)
369+
{
370+
case 0: return CTRL;
371+
case 1: return ALT;
372+
case 2: return SHIFT;
373+
case 3: return CTRL_ALT;
374+
case 4: return SHIFT_CTRL;
375+
case 5: return SHIFT_ALT;
376+
case 6: return SHIFT_ALT_CTRL;
377+
}
378+
return NONE;
379+
}
380+
381+
static UnsignedInt toIndex(MappableKeyModState modState)
382+
{
383+
switch (modState)
384+
{
385+
case CTRL: return 0;
386+
case ALT: return 1;
387+
case SHIFT: return 2;
388+
case CTRL_ALT: return 3;
389+
case SHIFT_CTRL: return 4;
390+
case SHIFT_ALT: return 5;
391+
case SHIFT_ALT_CTRL: return 6;
392+
}
393+
return 7;
394+
}
395+
396+
Bool isKeyDown() const
397+
{
398+
return m_modStateBits != 0;
399+
}
400+
401+
MappableKeyModState getKeyModState(UnsignedInt index)
402+
{
403+
if (BitIsSet(m_modStateBits, 1 << index))
404+
{
405+
return toKeyModState(index);
406+
}
407+
return NONE;
408+
}
409+
410+
void clearKeyModState(UnsignedInt index)
411+
{
412+
BitClear(m_modStateBits, 1 << index);
413+
}
414+
415+
Bool hasKeyModState(MappableKeyModState modState) const
416+
{
417+
return BitIsSet(m_modStateBits, 1 << toIndex(modState));
418+
}
419+
420+
void setKeyModState(MappableKeyModState modState)
421+
{
422+
BitSet(m_modStateBits, 1 << toIndex(modState));
423+
}
424+
425+
void clearKeyModState(MappableKeyModState modState)
426+
{
427+
BitClear(m_modStateBits, 1 << toIndex(modState));
428+
}
429+
430+
private:
431+
UnsignedByte m_modStateBits; ///< Fits all combinations of CTRL+ALT+SHIFT, storing 1 bit for each
432+
};
433+
434+
KeyDownInfo m_keyDownInfos[KEY_COUNT];
360435

361436
enum { NUM_MOUSE_BUTTONS = 3 };
362437
ICoord2D m_mouseDownPosition[NUM_MOUSE_BUTTONS];

0 commit comments

Comments
 (0)