Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Core/GameEngine/Include/Common/OptionPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ class OptionPreferences : public UserPreferences
Real getScrollFactor();
Bool getDrawScrollAnchor();
Bool getMoveScrollAnchor();
Bool getGridHotkeysEnabled() const;
AsciiString getGridHotkeyLayout() const;
Int getGridHotkeyColumns() const;
AsciiString getNonGridHotkeys() const;
Bool isNonGridHotkey(const AsciiString& key) const;
// TheSuperHackers @feature Exposed statically so GlobalData can test its cached copy of the
// list without re-reading Options.ini on every command bar rebuild.
static Bool isNonGridHotkeyInList(const AsciiString& list, const AsciiString& key);
Bool getCursorCaptureEnabledInWindowedGame() const;
Bool getCursorCaptureEnabledInWindowedMenu() const;
Bool getCursorCaptureEnabledInFullscreenGame() const;
Expand Down
4 changes: 4 additions & 0 deletions Core/GameEngine/Include/GameClient/ControlBar.h
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,10 @@ class ControlBar : public SubsystemInterface

/// set the command data into the button
void setControlCommand( GameWindow *button, const CommandButton *commandButton );
// TheSuperHackers @feature Grid hotkey for a command bar slot, empty if not applicable.
// isGridSlot, if given, reports whether the button is a command bar slot the grid covers,
// which is how the caller tells "excluded, so no key" apart from "not in the grid at all".
AsciiString getGridHotKeyForButton( GameWindow *button, Bool *isGridSlot = nullptr ) const;

void getForegroundMarkerPos(Int *x, Int *y);
void getBackgroundMarkerPos(Int *x, Int *y);
Expand Down
5 changes: 5 additions & 0 deletions Core/GameEngine/Include/GameClient/HotKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ class HotKeyManager : public SubsystemInterface
AsciiString searchHotKey( const AsciiString& label);
AsciiString searchHotKey( const UnicodeString& uStr );

// TheSuperHackers @feature Is this key currently claimed by a visible, enabled command
// button? Used so grid hotkeys can take precedence over a meta event bound to the same
// letter, but only while a button actually wants it.
Bool isHotKeyClaimed( const AsciiString& key ) const;

private:
typedef std::map<AsciiString, HotKey> HotKeyMap;
HotKeyMap m_hotKeyMap;
Expand Down
90 changes: 90 additions & 0 deletions Core/GameEngine/Source/Common/OptionPreferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,96 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const
return FALSE;
}

// TheSuperHackers @feature Grid hotkeys assign a command bar key by slot position rather than
// by whatever letter the string file marked with an ampersand.
// Options.ini: GridHotkeys = Yes
Bool OptionPreferences::getGridHotkeysEnabled() const
{
OptionPreferences::const_iterator it = find("GridHotkeys");
if (it == end())
return FALSE;

if (stricmp(it->second.str(), "yes") == 0) {
return TRUE;
}
return FALSE;
}

// TheSuperHackers @feature The key for each slot, in reading order. Defaults to two rows of
// nine, matching a command bar that is two rows deep. Mods with a different bar can set their
// own; slots past the end of the layout simply get no grid key. The layout should cover whole
// rows -- pad any slot that should stay keyless with a non letter, e.g. "QWERT....", since the
// row shape is derived from the layout's length.
// Options.ini: GridHotkeyLayout = QWERTYUIOASDFGHJKL
AsciiString OptionPreferences::getGridHotkeyLayout() const
{
OptionPreferences::const_iterator it = find("GridHotkeyLayout");
if (it == end() || it->second.isEmpty())
return AsciiString("QWERTYUIOASDFGHJKL");

return it->second;
}

// TheSuperHackers @feature How many columns the command bar is wide. The engine numbers command
// slots down each column rather than across each row, so the layout string -- which is written in
// reading order -- has to be mapped through this to land the right key on the right button.
// 0 means do not remap, i.e. the layout string is already in slot order.
// Options.ini: GridHotkeyColumns = 9
Int OptionPreferences::getGridHotkeyColumns() const
{
OptionPreferences::const_iterator it = find("GridHotkeyColumns");
if (it == end())
return 9; // two rows of nine, matching a bar two rows deep

Int columns = atoi(it->second.str());
if (columns < 0)
columns = 0;

return columns;
}

// TheSuperHackers @feature Keys listed here opt out of the grid entirely. The slot that would
// have taken such a key falls back to its string file letter, and the key keeps whatever it
// normally does -- so a mod can leave S, G and friends on their usual bindings while everything
// else grids. Letters only, case insensitive, separators optional: "SG" and "S,G" both work.
// Options.ini: NonGridHotkeys = SG
AsciiString OptionPreferences::getNonGridHotkeys() const
{
OptionPreferences::const_iterator it = find("NonGridHotkeys");
if (it == end())
return AsciiString::TheEmptyString;

return it->second;
}

// TheSuperHackers @feature Is this key one the player asked to keep out of the grid?
Bool OptionPreferences::isNonGridHotkey(const AsciiString& key) const
{
if (key.isEmpty())
return FALSE;

return isNonGridHotkeyInList(getNonGridHotkeys(), key);
}

// TheSuperHackers @feature Shared by the option lookup above and by GlobalData's cached copy,
// so both read the exclusion list exactly the same way.
Bool OptionPreferences::isNonGridHotkeyInList(const AsciiString& list, const AsciiString& key)
{
if (list.isEmpty() || key.isEmpty())
return FALSE;

const char wanted = tolower(key.getCharAt(0));
const char *c = list.str();
for (; *c; ++c)
{
// a plain scan, so commas, spaces or nothing at all all work as separators
if (tolower(*c) == wanted)
return TRUE;
}

return FALSE;
}

Bool OptionPreferences::getRetaliationModeEnabled()
{
OptionPreferences::const_iterator it = find("Retaliation");
Expand Down
126 changes: 125 additions & 1 deletion Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
#include "Common/GameType.h"
#include "Common/MultiplayerSettings.h"
#include "Common/NameKeyGenerator.h"
// TheSuperHackers @feature for the grid hotkey exclusion list
#include "Common/OptionPreferences.h"
#include "Common/Override.h"
#include "Common/PlayerTemplate.h"
#include "Common/Player.h"
Expand Down Expand Up @@ -2456,6 +2458,78 @@ void ControlBar::setCommandBarBorder( GameWindow *button, CommandButtonMappedBor
//-------------------------------------------------------------------------------------------------
/** Set the command data into the control */
//-------------------------------------------------------------------------------------------------
// TheSuperHackers @feature Grid hotkeys.
//-------------------------------------------------------------------------------------------------
/** Which grid key belongs to this command bar button, or empty if it is not one of them.
*
* The slot is found by looking the window up in m_commandWindows, so the mapping follows the
* bar's own ordering and needs no assumption about how many slots a mod uses. Slots past the
* end of the configured layout simply get no grid key. */
//-------------------------------------------------------------------------------------------------
AsciiString ControlBar::getGridHotKeyForButton( GameWindow *button, Bool *isGridSlot ) const
{
if( button == nullptr || TheGlobalData == nullptr )
return AsciiString::TheEmptyString;

const AsciiString &layout = TheGlobalData->m_gridHotkeyLayout;
if( layout.isEmpty() )
return AsciiString::TheEmptyString;

for( Int i = 0; i < MAX_COMMANDS_PER_SET; ++i )
{
if( m_commandWindows[ i ] != button )
continue;

// The command bar numbers its slots down each column, but the layout string is written
// in reading order, so translate between the two. Without this, sequential letters land
// on alternating rows.
Int layoutIndex = i;
const Int columns = TheGlobalData->m_gridHotkeyColumns;
if( columns > 0 )
{
// Rows come from the layout's own length, not MAX_COMMANDS_PER_SET, which is the
// internal cap of 32 rather than however many slots the mod actually shows.
const Int rows = ( layout.getLength() + columns - 1 ) / columns;
Comment on lines +2490 to +2492

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Short layouts lose keys 🐞 Bug ≡ Correctness

The remapping derives the row count from layout.length / columns, so a valid layout shorter than
GridHotkeyColumns produces one row and maps every slot after slot zero beyond the string. For
example, layout QWER with the default nine columns assigns only Q, contradicting the stated
behavior that only slots past the layout end should be keyless.
Agent Prompt
## Issue description
Short custom layouts are treated as changing the bar's row count, causing covered prefix slots to map out of bounds instead of receiving their configured keys.

## Issue Context
The layout length controls how many reading-order slots have keys; it should not redefine the command bar's physical row count. Preserve the documented `columns = 0` slot-order behavior.

## Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[2483-2502]
- Core/GameEngine/Source/Common/OptionPreferences.cpp[234-262]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

Fixed in afd0926: a non-alphanumeric layout character is now a placeholder - the slot is covered by the grid but keyless - so a short layout keeps its row shape by padding (e.g. QWERT....). The option's documentation now states the whole-rows contract, since the row count is derived from the layout's length.

if( rows > 0 )
{
const Int column = i / rows; // slots run down a column before moving right
const Int row = i % rows;
layoutIndex = row * columns + column;
}
}

if( layoutIndex >= layout.getLength() )
break; // this mod has more slots than the layout covers

// this slot belongs to the grid, whatever we decide its key is
if( isGridSlot != nullptr )
*isGridSlot = TRUE;

// A non alphanumeric layout character is a placeholder: the slot is covered by the
// grid but gets no key of its own. That lets a layout shorter than a full bar keep
// its row shape by padding with dots -- the row count above is derived from the
// layout's length, so a partial row would otherwise shift every key after it.
const char layoutChar = layout.getCharAt( layoutIndex );
if( !isalnum( (unsigned char)layoutChar ) )
return AsciiString::TheEmptyString;

AsciiString key;
key.concat( layoutChar );

// A key on the exclusion list is not used for this slot, so the slot falls back to its
// string file letter -- which is the point of the exclusion list: the key is freed for
// whatever the player wants it for, and the button keys the way it did before grid
// hotkeys existed. isGridSlot stays set, so the caller knows to check for a collision
// before handing the slot a letter the grid is already using.
if( OptionPreferences::isNonGridHotkeyInList( TheGlobalData->m_nonGridHotkeys, key ) )
return AsciiString::TheEmptyString;

return key;
}

return AsciiString::TheEmptyString;
}

void ControlBar::setControlCommand( GameWindow *button, const CommandButton *commandButton )
{

Expand Down Expand Up @@ -2527,7 +2601,57 @@ void ControlBar::setControlCommand( GameWindow *button, const CommandButton *com

if (TheHotKeyManager)
{
AsciiString hotKey = TheHotKeyManager->searchHotKey(commandButton->getTextLabel());
// TheSuperHackers @feature Grid hotkeys key a button by where it sits in the command
// bar rather than by the letter its string file marked with an ampersand, so the keys
// stay in the same place whatever is being built.
AsciiString hotKey;
Bool isGridSlot = FALSE;

if( TheGlobalData && TheGlobalData->m_gridHotkeysEnabled )
hotKey = getGridHotKeyForButton( button, &isGridSlot );

// Fall back to the string file letter whenever the grid did not supply one. That covers a
// button the grid does not reach -- past the end of the layout, or not in the command bar
// at all -- and also a slot whose key the player put on NonGridHotkeys, which is what
// excluding a key is for: the key goes back to the game, and the button keys the way it
// did before grid hotkeys existed.
if( hotKey.isEmpty() )
{
const AsciiString strHotKey = TheHotKeyManager->searchHotKey(commandButton->getTextLabel());

// A string file letter that the grid layout also uses would be a straight collision:
// addHotKey keeps whichever slot registered first and silently drops the other, so one
// of the two buttons would end up with no key and no overlay letter, seemingly at
// random. Leave the excluded slot without a hotkey instead -- the grid keeps its own
// letters, which is the behaviour the fixed layout is there to guarantee.
//
// Tested against the layout rather than against what is currently registered, so the
// result does not depend on the order the slots happen to be populated in.
Bool collidesWithGrid = FALSE;
if( isGridSlot && strHotKey.isNotEmpty() && TheGlobalData )
{
const AsciiString &layout = TheGlobalData->m_gridHotkeyLayout;
const char wanted = tolower( strHotKey.getCharAt( 0 ) );
for( const char *c = layout.str(); *c; ++c )
{
// A letter the player excluded is no longer the grid's, so it does not collide.
if( tolower( *c ) != wanted )
continue;

AsciiString gridKey;
gridKey.concat( *c );
if( !OptionPreferences::isNonGridHotkeyInList( TheGlobalData->m_nonGridHotkeys, gridKey ) )
{
collidesWithGrid = TRUE;
}
break;
}
}

if( !collidesWithGrid )
hotKey = strHotKey;
}

if(hotKey.isNotEmpty())
TheHotKeyManager->addHotKey(button, hotKey);
}
Expand Down
31 changes: 30 additions & 1 deletion Core/GameEngine/Source/GameClient/MessageStream/HotKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ GameMessageDisposition HotKeyTranslator::translateGameMessage(const GameMessage
{
newModState |= ALT;
}
if(newModState != 0)

// TheSuperHackers @feature Let Shift through so shift+hotkey batches production the same
// way shift+clicking the cameo does. Ctrl and Alt still bail, since those carry their own
// bindings (control groups and so on) that must not be shadowed by command hotkeys.
if( (newModState & ~SHIFT) != 0 )
return disp;
WideChar key = TheKeyboard->getPrintableKey((KeyDefType)msg->getArgument(0)->integer, 0);
UnicodeString uKey;
Expand Down Expand Up @@ -157,6 +161,31 @@ void HotKeyManager::addHotKey( GameWindow *win, const AsciiString& keyIn)
m_hotKeyMap[key] = newHK;
}

// TheSuperHackers @feature See HotKey.h.
//-----------------------------------------------------------------------------
Bool HotKeyManager::isHotKeyClaimed( const AsciiString& keyIn ) const
{
AsciiString key = keyIn;
key.toLower();

HotKeyMap::const_iterator it = m_hotKeyMap.find(key);
if( it == m_hotKeyMap.end() )
return FALSE;

GameWindow *win = it->second.m_win;
if( win == nullptr )
return FALSE;

// Only a visible button counts as claiming the key. A visible but currently disabled
// button claims it deliberately: the player pressing a greyed out cameo's key should get
// the disabled click feedback from executeHotKey, not have the press fall through to a
// meta event bound to the same letter and do something unrelated to the button they see.
if( BitIsSet( win->winGetStatus(), WIN_STATUS_HIDDEN ) )
return FALSE;

return TRUE;
}

//-----------------------------------------------------------------------------
Bool HotKeyManager::executeHotKey( const AsciiString& keyIn )
{
Expand Down
38 changes: 38 additions & 0 deletions Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
#include "GameClient/Keyboard.h"
#endif

// TheSuperHackers @feature for grid hotkeys yielding to command bar buttons
#include "Common/GlobalData.h"
#include "Common/OptionPreferences.h"
#include "GameClient/HotKey.h"
#include "GameClient/Keyboard.h"

MetaMap *TheMetaMap = nullptr;


Expand Down Expand Up @@ -623,6 +629,38 @@ void MetaEventTranslator::onKeyPressed(GameMessageDisposition &disp, Int systemK
}
else
{
// TheSuperHackers @feature With grid hotkeys on, a command bar button that has
// claimed this key wins over the meta event bound to the same letter. Checked
// here rather than by reordering translators, because the order is fixed before
// Options.ini is ever read. The meta event still fires whenever no button wants
// the key, so select all and friends keep working the rest of the time.
// Only ever yield for an unmodified key. A command bar hotkey is a bare letter, or
// Shift plus one for batching, so anything carrying Ctrl or Alt cannot be a cameo
// press and must keep its meta event. Without this a Ctrl combo whose base letter
// happens to sit on a visible button is swallowed: the meta event steps aside and
// HotKeyTranslator then rejects the modifiers, so the key does nothing at all.
if( (keyModState & ~SHIFT) == 0 &&
TheGlobalData && TheGlobalData->m_gridHotkeysEnabled && TheHotKeyManager )
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
{
WideChar wKey = TheKeyboard->getPrintableKey( (KeyDefType)keyType, 0 );
UnicodeString uKey;
uKey.concat( wKey );
AsciiString aKey;
aKey.translate( uKey );

// A key on the exclusion list keeps its meta event no matter what, which is the
// whole point of listing it -- otherwise a button that took the key through the
// string file fallback would still shadow it.
const Bool nonGrid = OptionPreferences::isNonGridHotkeyInList(
TheGlobalData->m_nonGridHotkeys, aKey );

if( !nonGrid && aKey.isNotEmpty() && TheHotKeyManager->isHotKeyClaimed( aKey ) )
{
disp = KEEP_MESSAGE; // let HotKeyTranslator have it
break;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
}

// THIS IS A GREASY HACK... MESSAGE SHOULD BE HANDLED IN A TRANSLATOR, BUT DURING CINEMATICS THE TRANSLATOR IS DISABLED
if( map->m_meta == GameMessage::MSG_META_TOGGLE_FAST_FORWARD_REPLAY)
{
Expand Down
6 changes: 6 additions & 0 deletions Generals/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ class GlobalData : public SubsystemInterface
Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode.
Bool m_clientRetaliationModeEnabled;
Bool m_doubleClickAttackMove;
// TheSuperHackers @feature Command bar keys by slot position instead of by string file.
Bool m_gridHotkeysEnabled;
AsciiString m_gridHotkeyLayout;
Int m_gridHotkeyColumns;
// Keys the player asked to leave out of the grid; those slots keep their string file letter.
AsciiString m_nonGridHotkeys;
Bool m_rightMouseAlwaysScrolls;
Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots.
Bool m_useWaterPlane;
Expand Down
Loading