From d5e9dfab0951213e3d1baac021d4d7c4b7fbfd5b Mon Sep 17 00:00:00 2001 From: triatomic <32312517+triatomic@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:11:12 +0300 Subject: [PATCH 1/2] feat(client): Add KeyboardOverlay option to show hotkeys on command bar cameos Draws each command bar cameo's keyboard hotkey letter over the cameo, so the player can learn the shortcuts without hunting through tooltips. The letter comes from what actually got registered in the hotkey manager, not from the button's label - colliding hotkeys are dropped at registration, and drawing those would advertise a key that does nothing. KeyboardOverlay = Yes ; off by default KeyboardOverlayRed / Green / Blue ; letter colour, default white KeyboardOverlayBackdrop = Yes ; translucent plate, on when ; the overlay itself is on KeyboardOverlayBackdropRed / Green / Blue / Opacity Display strings are cached per letter and returned to the manager by the W3DDisplayStringManager destructor before the manager itself is torn down, which its base class asserts on. Available in both Zero Hour and Generals: the drawing lives in the shared W3D push button gadget. Ported from the Contra mod's engine fork, collapsing its three overlay commits into the final state. --- .../Include/Common/OptionPreferences.h | 9 ++ Core/GameEngine/Include/GameClient/HotKey.h | 5 + .../Source/Common/OptionPreferences.cpp | 65 +++++++++++ .../GameClient/MessageStream/HotKey.cpp | 18 +++ .../Include/W3DDevice/GameClient/W3DGadget.h | 3 + .../GameClient/GUI/Gadget/W3DPushButton.cpp | 104 ++++++++++++++++++ .../GameEngine/Include/Common/GlobalData.h | 5 + .../GameEngine/Source/Common/GlobalData.cpp | 8 ++ .../GameClient/W3DDisplayStringManager.cpp | 7 ++ .../GameEngine/Include/Common/GlobalData.h | 5 + .../GameEngine/Source/Common/GlobalData.cpp | 8 ++ .../GameClient/W3DDisplayStringManager.cpp | 7 ++ 12 files changed, 244 insertions(+) diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 85aba4228be..2742d666403 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -34,6 +34,7 @@ #include "WW3D2/texturefilter.h" #include "Common/UserPreferences.h" +#include "GameClient/Color.h" typedef UnsignedInt CursorCaptureMode; typedef UnsignedInt ScreenEdgeScrollMode; @@ -76,6 +77,10 @@ class OptionPreferences : public UserPreferences Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); + Bool getKeyboardOverlayEnabled() const; + Color getKeyboardOverlayColor() const; + Bool getKeyboardOverlayBackdropEnabled() const; + Color getKeyboardOverlayBackdropColor() const; Bool getCursorCaptureEnabledInWindowedGame() const; Bool getCursorCaptureEnabledInWindowedMenu() const; Bool getCursorCaptureEnabledInFullscreenGame() const; @@ -131,4 +136,8 @@ class OptionPreferences : public UserPreferences Bool getShowMoneyPerMinute() const; Real getGameWindowTransitionSpeedMultiplier() const; + +private: + // TheSuperHackers @feature Read one 0-255 colour channel, clamped, with a fallback. + UnsignedByte getColorChannel(const char *keyName, UnsignedByte defaultValue) const; }; diff --git a/Core/GameEngine/Include/GameClient/HotKey.h b/Core/GameEngine/Include/GameClient/HotKey.h index 70206f24cf6..cbd75d5a948 100644 --- a/Core/GameEngine/Include/GameClient/HotKey.h +++ b/Core/GameEngine/Include/GameClient/HotKey.h @@ -98,6 +98,11 @@ class HotKeyManager : public SubsystemInterface AsciiString searchHotKey( const AsciiString& label); AsciiString searchHotKey( const UnicodeString& uStr ); + // TheSuperHackers @feature Which hotkey actually got registered to this window, if any. + // Deliberately not derived from the window's label -- addHotKey drops keys that collide + // with an earlier button, so only this reports the key that will really work. + AsciiString getHotKeyForWindow( const GameWindow *win ) const; + private: typedef std::map HotKeyMap; HotKeyMap m_hotKeyMap; diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index e681ef8b192..7def0daac2e 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -216,6 +216,71 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const return FALSE; } +// TheSuperHackers @feature Draw each command bar cameo's keyboard hotkey over the cameo. +// Options.ini: KeyboardOverlay = Yes +Bool OptionPreferences::getKeyboardOverlayEnabled() const +{ + OptionPreferences::const_iterator it = find("KeyboardOverlay"); + if (it == end()) + return FALSE; + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + +UnsignedByte OptionPreferences::getColorChannel(const char *keyName, UnsignedByte defaultValue) const +{ + OptionPreferences::const_iterator it = find(AsciiString(keyName)); + if (it == end()) + return defaultValue; + + Int value = atoi(it->second.str()); + if (value < 0) + value = 0; + else if (value > 255) + value = 255; + + return (UnsignedByte)value; +} + +// TheSuperHackers @feature Colour of the command bar hotkey overlay letters. +// Options.ini: KeyboardOverlayRed / KeyboardOverlayGreen / KeyboardOverlayBlue, +// each 0-255, defaulting to white. A missing or out of range channel falls +// back to full brightness rather than silently drawing an invisible letter. +Color OptionPreferences::getKeyboardOverlayColor() const +{ + return GameMakeColor( + getColorChannel("KeyboardOverlayRed", 255), + getColorChannel("KeyboardOverlayGreen", 255), + getColorChannel("KeyboardOverlayBlue", 255), + 255); +} + +// TheSuperHackers @feature Translucent plate drawn behind the hotkey letter. +Bool OptionPreferences::getKeyboardOverlayBackdropEnabled() const +{ + OptionPreferences::const_iterator it = find("KeyboardOverlayBackdrop"); + if (it == end()) + return TRUE; // on by default -- it is what makes the letter readable + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + +Color OptionPreferences::getKeyboardOverlayBackdropColor() const +{ + // black at 50% opacity, matching the darkened plate look + return GameMakeColor( + getColorChannel("KeyboardOverlayBackdropRed", 0), + getColorChannel("KeyboardOverlayBackdropGreen", 0), + getColorChannel("KeyboardOverlayBackdropBlue", 0), + getColorChannel("KeyboardOverlayBackdropOpacity", 128)); +} + Bool OptionPreferences::getRetaliationModeEnabled() { OptionPreferences::const_iterator it = find("Retaliation"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp index 0b7ecf22d1e..91e1f133199 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp @@ -157,6 +157,24 @@ void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn) m_hotKeyMap[key] = newHK; } +//----------------------------------------------------------------------------- +// TheSuperHackers @feature Reverse lookup for the hotkey overlay on command bar cameos. +//----------------------------------------------------------------------------- +AsciiString HotKeyManager::getHotKeyForWindow( const GameWindow *win ) const +{ + if( win == nullptr ) + return AsciiString::TheEmptyString; + + // the map only ever holds the currently displayed commands, so this stays tiny + for( HotKeyMap::const_iterator it = m_hotKeyMap.begin(); it != m_hotKeyMap.end(); ++it ) + { + if( it->second.m_win == win ) + return it->second.m_key; + } + + return AsciiString::TheEmptyString; +} + //----------------------------------------------------------------------------- Bool HotKeyManager::executeHotKey( const AsciiString& keyIn ) { diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h index 5979a8a5f4f..218893bee48 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DGadget.h @@ -65,6 +65,9 @@ /////////////////////////////////////////////////////////////////////////////// extern void W3DGadgetPushButtonDraw( GameWindow *window, WinInstanceData *instData ); +// TheSuperHackers @feature Free the hotkey overlay's display strings; must run before +// TheDisplayStringManager is destroyed. +extern void W3DGadgetPushButtonFreeHotKeyStrings( void ); extern void W3DGadgetPushButtonImageDraw( GameWindow *window, WinInstanceData *instData ); extern void W3DGadgetCheckBoxDraw( GameWindow *window, WinInstanceData *instData ); extern void W3DGadgetCheckBoxImageDraw( GameWindow *window, WinInstanceData *instData ); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp index 2b89eb1e564..b8ef542be69 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp @@ -53,6 +53,12 @@ #include "GameClient/GameWindowManager.h" #include "GameClient/GadgetPushButton.h" #include "GameClient/Display.h" +// TheSuperHackers @feature for the command bar hotkey overlay +#include "Common/GlobalData.h" +#include "GameClient/DisplayStringManager.h" +#include "GameClient/GameFont.h" +#include "GameClient/GlobalLanguage.h" +#include "GameClient/HotKey.h" #include "W3DDevice/GameClient/W3DGameWindow.h" #include "W3DDevice/GameClient/W3DDisplay.h" #include "W3DDevice/GameClient/W3DGadget.h" @@ -78,6 +84,100 @@ void W3DGadgetPushButtonImageDrawOne(GameWindow *window, WinInstanceData *instDa // drawButtonText ============================================================= /** Draw button text to the screen */ //============================================================================= +// TheSuperHackers @feature Command bar hotkey overlay (Options.ini: KeyboardOverlay). +// One display string per letter, so that drawing many cameos in a row does not +// rebuild sentence geometry over and over. There are only ever a handful of +// distinct hotkeys on screen, so this stays small. Returned to the manager by +// W3DGadgetPushButtonFreeHotKeyStrings before the manager is torn down. +static DisplayString *s_hotKeyStrings[ 256 ] = { nullptr }; + +//============================================================================= +/** Return the overlay's display strings to the manager. Must run before + * TheDisplayStringManager is destroyed, which asserts on any string still + * registered -- the W3DDisplayStringManager destructor calls this. */ +//============================================================================= +void W3DGadgetPushButtonFreeHotKeyStrings( void ) +{ + for( Int i = 0; i < 256; ++i ) + { + if( s_hotKeyStrings[ i ] != nullptr && TheDisplayStringManager != nullptr ) + TheDisplayStringManager->freeDisplayString( s_hotKeyStrings[ i ] ); + + s_hotKeyStrings[ i ] = nullptr; + } +} + +// drawButtonHotKeyOverlay ==================================================== +/** Draw the keyboard hotkey letter over a command bar cameo, so the player can + * learn the shortcuts without hunting through tooltips. + * + * The letter comes from what actually got registered in the hotkey manager, not + * from the button's label -- colliding hotkeys are dropped at registration, and + * drawing those would advertise a key that does nothing. */ +//============================================================================= +static void drawButtonHotKeyOverlay( GameWindow *window ) +{ + if( !TheGlobalData || !TheGlobalData->m_keyboardOverlayEnabled ) + return; + + if( TheHotKeyManager == nullptr || TheDisplayStringManager == nullptr ) + return; + + // only cameo style buttons opt into overlay states, so this leaves menu buttons alone + if( !BitIsSet( window->winGetStatus(), WIN_STATUS_USE_OVERLAY_STATES ) ) + return; + + AsciiString hotKey = TheHotKeyManager->getHotKeyForWindow( window ); + if( hotKey.isEmpty() ) + return; + + const UnsignedByte index = (UnsignedByte)hotKey.getCharAt( 0 ); + DisplayString *hotKeyString = s_hotKeyStrings[ index ]; + + if( hotKeyString == nullptr ) + { + hotKeyString = TheDisplayStringManager->newDisplayString(); + if( hotKeyString == nullptr ) + return; + + Int pointSize = 10; + if( TheGlobalLanguageData ) + pointSize = TheGlobalLanguageData->adjustFontSize( pointSize ); + hotKeyString->setFont( TheFontLibrary->getFont( AsciiString( "Arial" ), pointSize, TRUE ) ); + + // the manager stores keys lowercased, but shortcuts read better as capitals + UnicodeString text; + WideChar upper = (WideChar)toupper( (Int)index ); + text.concat( upper ); + hotKeyString->setText( text ); + + s_hotKeyStrings[ index ] = hotKeyString; + } + + ICoord2D origin; + window->winGetScreenPosition( &origin.x, &origin.y ); + + // tuck it into the top left of the cameo, where no existing decoration lives + const Int inset = 2; + const Int textX = origin.x + inset; + const Int textY = origin.y + inset; + + // Optional plate behind the letter, so it stays readable over busy cameo art. + if( TheGlobalData->m_keyboardOverlayBackdrop ) + { + Int width, height; + hotKeyString->getSize( &width, &height ); + + const Int pad = 1; + TheDisplay->drawFillRect( textX - pad, textY - pad, + width + pad * 2, height + pad * 2, + TheGlobalData->m_keyboardOverlayBackdropColor ); + } + + hotKeyString->draw( textX, textY, + TheGlobalData->m_keyboardOverlayColor, GameMakeColor( 0, 0, 0, 255 ) ); +} + static void drawButtonText( GameWindow *window, WinInstanceData *instData ) { ICoord2D origin, size, textPos; @@ -481,6 +581,10 @@ void W3DGadgetPushButtonImageDrawOne( GameWindow *window, } } } + + // TheSuperHackers @feature Draw the hotkey letter last, so it stays readable on top + // of the hilite and pushed overlays. + drawButtonHotKeyOverlay( window ); } diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 0c8e8820660..011469dd5eb 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -141,6 +141,11 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Show each command bar cameo's hotkey letter on the cameo. + Bool m_keyboardOverlayEnabled; + Color m_keyboardOverlayColor; + Bool m_keyboardOverlayBackdrop; + Color m_keyboardOverlayBackdropColor; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index f7720c351a2..598447d3f3f 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1047,6 +1047,10 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_keyboardOverlayEnabled = FALSE; + m_keyboardOverlayColor = GameMakeColor( 255, 255, 255, 255 ); + m_keyboardOverlayBackdrop = TRUE; + m_keyboardOverlayBackdropColor = GameMakeColor( 0, 0, 0, 128 ); m_doubleClickAttackMove = FALSE; } @@ -1199,6 +1203,10 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_keyboardOverlayEnabled = optionPref.getKeyboardOverlayEnabled(); + TheWritableGlobalData->m_keyboardOverlayColor = optionPref.getKeyboardOverlayColor(); + TheWritableGlobalData->m_keyboardOverlayBackdrop = optionPref.getKeyboardOverlayBackdropEnabled(); + TheWritableGlobalData->m_keyboardOverlayBackdropColor = optionPref.getKeyboardOverlayBackdropColor(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp index d67d6282d30..549c00afcfa 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp @@ -36,6 +36,8 @@ #include "GameClient/DrawGroupInfo.h" #include "GameClient/GlobalLanguage.h" #include "W3DDevice/GameClient/W3DDisplayStringManager.h" +// TheSuperHackers @feature for the hotkey overlay string teardown +#include "W3DDevice/GameClient/W3DGadget.h" /////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS @@ -56,6 +58,11 @@ W3DDisplayStringManager::W3DDisplayStringManager() //------------------------------------------------------------------------------------------------- W3DDisplayStringManager::~W3DDisplayStringManager() { + // TheSuperHackers @feature The hotkey overlay's shared strings must come back to the + // manager while it is still alive; the base class destructor asserts on any string + // left registered. + W3DGadgetPushButtonFreeHotKeyStrings(); + for (Int i = 0; i < MAX_GROUPS; ++i) { if (m_groupNumeralStrings[i]) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..0d2bea5da0b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,11 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Show each command bar cameo's hotkey letter on the cameo. + Bool m_keyboardOverlayEnabled; + Color m_keyboardOverlayColor; + Bool m_keyboardOverlayBackdrop; + Color m_keyboardOverlayBackdropColor; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..ca8158d35e1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1062,6 +1062,10 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_keyboardOverlayEnabled = FALSE; + m_keyboardOverlayColor = GameMakeColor( 255, 255, 255, 255 ); + m_keyboardOverlayBackdrop = TRUE; + m_keyboardOverlayBackdropColor = GameMakeColor( 0, 0, 0, 128 ); m_doubleClickAttackMove = FALSE; } @@ -1206,6 +1210,10 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_keyboardOverlayEnabled = optionPref.getKeyboardOverlayEnabled(); + TheWritableGlobalData->m_keyboardOverlayColor = optionPref.getKeyboardOverlayColor(); + TheWritableGlobalData->m_keyboardOverlayBackdrop = optionPref.getKeyboardOverlayBackdropEnabled(); + TheWritableGlobalData->m_keyboardOverlayBackdropColor = optionPref.getKeyboardOverlayBackdropColor(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp index 34d1c0e0046..681c9ed074b 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp @@ -36,6 +36,8 @@ #include "GameClient/DrawGroupInfo.h" #include "GameClient/GlobalLanguage.h" #include "W3DDevice/GameClient/W3DDisplayStringManager.h" +// TheSuperHackers @feature for the hotkey overlay string teardown +#include "W3DDevice/GameClient/W3DGadget.h" /////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS @@ -56,6 +58,11 @@ W3DDisplayStringManager::W3DDisplayStringManager() //------------------------------------------------------------------------------------------------- W3DDisplayStringManager::~W3DDisplayStringManager() { + // TheSuperHackers @feature The hotkey overlay's shared strings must come back to the + // manager while it is still alive; the base class destructor asserts on any string + // left registered. + W3DGadgetPushButtonFreeHotKeyStrings(); + for (Int i = 0; i < MAX_GROUPS; ++i) { if (m_groupNumeralStrings[i]) From b07092b110c8c1078398a19d0d329243abc51239 Mon Sep 17 00:00:00 2001 From: triatomic <32312517+triatomic@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:20:36 +0300 Subject: [PATCH 2/2] fix(client): Draw the hotkey overlay only for single byte printable keys A localized mnemonic outside ASCII arrives as a multi byte sequence, and drawing its first byte showed a wrong or garbled shortcut while sharing its cache slot with other keys starting on the same byte. Draw nothing for those instead. --- .../W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp index b8ef542be69..5c03845ba0a 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DPushButton.cpp @@ -131,6 +131,12 @@ static void drawButtonHotKeyOverlay( GameWindow *window ) if( hotKey.isEmpty() ) return; + // Only a single byte printable key can be shown faithfully. A localized mnemonic outside + // ASCII arrives as a multi byte sequence here, and drawing its first byte would show a + // wrong or garbled shortcut - better to draw nothing for those. + if( hotKey.getLength() != 1 || !isprint( (unsigned char)hotKey.getCharAt( 0 ) ) ) + return; + const UnsignedByte index = (UnsignedByte)hotKey.getCharAt( 0 ); DisplayString *hotKeyString = s_hotKeyStrings[ index ];