Skip to content

Commit 95ac37d

Browse files
authored
feat(input): Implement alternative key mappings for Game Pause and Step Frame that are usable as a Solo player (#1530)
The new default key mappings are SHIFT+P to Pause and SHIFT+O to Step Frame when playing
1 parent 40ac1c0 commit 95ac37d

File tree

13 files changed

+220
-68
lines changed

13 files changed

+220
-68
lines changed

Core/GameEngine/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ set(GAMEENGINE_SRC
5353
# Include/Common/GameState.h
5454
# Include/Common/GameStateMap.h
5555
# Include/Common/GameType.h
56+
Include/Common/GameUtility.h
5657
# Include/Common/Geometry.h
5758
# Include/Common/GlobalData.h
5859
# Include/Common/Handicap.h
@@ -574,6 +575,7 @@ set(GAMEENGINE_SRC
574575
# Source/Common/GameEngine.cpp
575576
# Source/Common/GameLOD.cpp
576577
# Source/Common/GameMain.cpp
578+
Source/Common/GameUtility.cpp
577579
# Source/Common/GlobalData.cpp
578580
# Source/Common/INI/INI.cpp
579581
# Source/Common/INI/INIAiData.cpp
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#pragma once
20+
21+
// For miscellaneous game utility functions.
22+
23+
namespace rts
24+
{
25+
26+
Bool localPlayerIsObserving();
27+
28+
} // namespace rts
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#include "PreRTS.h"
20+
21+
#include "Common/GameUtility.h"
22+
#include "Common/PlayerList.h"
23+
#include "Common/Player.h"
24+
25+
#include "GameLogic/GameLogic.h"
26+
27+
28+
namespace rts
29+
{
30+
31+
Bool localPlayerIsObserving()
32+
{
33+
if (TheGameLogic->isInReplayGame() || TheGameLogic->isInShellGame())
34+
return true;
35+
36+
if (ThePlayerList->getLocalPlayer()->isPlayerObserver())
37+
return true;
38+
39+
return false;
40+
}
41+
42+
} // namespace rts

Generals/Code/GameEngine/Include/Common/MessageStream.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ class GameMessage : public MemoryPoolObject
273273
MSG_META_END_CAMERA_ZOOM_OUT,
274274
MSG_META_CAMERA_RESET,
275275
MSG_META_TOGGLE_FAST_FORWARD_REPLAY, ///< Toggle the fast forward feature
276-
MSG_META_TOGGLE_PAUSE, ///< TheSuperHackers @feature Toggle game pause (in replay playbacks)
277-
MSG_META_STEP_FRAME, ///< TheSuperHackers @feature Step one frame (in replay playbacks)
276+
MSG_META_TOGGLE_PAUSE, ///< TheSuperHackers @feature Toggle game pause
277+
MSG_META_TOGGLE_PAUSE_ALT, ///< TheSuperHackers @feature Toggle game pause (alternative mapping)
278+
MSG_META_STEP_FRAME, ///< TheSuperHackers @feature Step one frame
279+
MSG_META_STEP_FRAME_ALT, ///< TheSuperHackers @feature Step one frame (alternative mapping)
278280

279281

280282
// META items that are really for debug/demo/development use only...

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,16 +314,18 @@ enum CommandUsableInType CPP_11(: Int)
314314
{
315315
COMMANDUSABLE_NONE = 0,
316316

317-
COMMANDUSABLE_SHELL = (1 << 0),
318-
COMMANDUSABLE_GAME = (1 << 1),
317+
COMMANDUSABLE_SHELL = (1 << 0), // Command is usable when in Shell (Menus)
318+
COMMANDUSABLE_GAME = (1 << 1), // Command is usable when not in Shell
319+
COMMANDUSABLE_OBSERVER = (1 << 2), // TheSuperHackers @feature Command is usable when observing
319320

320-
COMMANDUSABLE_EVERYWHERE = COMMANDUSABLE_SHELL | COMMANDUSABLE_GAME,
321+
COMMANDUSABLE_EVERYWHERE = ~0,
321322
};
322323

323324
static const char* TheCommandUsableInNames[] =
324325
{
325326
"SHELL",
326327
"GAME",
328+
"OBSERVER",
327329

328330
NULL
329331
};

Generals/Code/GameEngine/Source/Common/MessageStream.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,9 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t)
376376
CASE_LABEL(MSG_META_CAMERA_RESET)
377377
CASE_LABEL(MSG_META_TOGGLE_FAST_FORWARD_REPLAY)
378378
CASE_LABEL(MSG_META_TOGGLE_PAUSE)
379+
CASE_LABEL(MSG_META_TOGGLE_PAUSE_ALT)
379380
CASE_LABEL(MSG_META_STEP_FRAME)
381+
CASE_LABEL(MSG_META_STEP_FRAME_ALT)
380382

381383
#if defined(RTS_DEBUG)
382384
CASE_LABEL(MSG_META_DEMO_TOGGLE_BEHIND_BUILDINGS)

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3302,10 +3302,9 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
33023302

33033303
}
33043304
case GameMessage::MSG_META_TOGGLE_PAUSE:
3305+
case GameMessage::MSG_META_TOGGLE_PAUSE_ALT:
33053306
{
3306-
#if !defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)//may be defined in GameCommon.h
3307-
if (TheGameLogic->isInReplayGame())
3308-
#endif
3307+
if (!TheGameLogic->isInMultiplayerGame())
33093308
{
33103309
if (TheGameLogic->isGamePaused())
33113310
{
@@ -3318,17 +3317,18 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
33183317
Bool pauseInput = FALSE;
33193318
TheGameLogic->setGamePaused(pause, pauseMusic, pauseInput);
33203319
}
3320+
disp = DESTROY_MESSAGE;
33213321
}
33223322
break;
33233323
}
33243324
case GameMessage::MSG_META_STEP_FRAME:
3325+
case GameMessage::MSG_META_STEP_FRAME_ALT:
33253326
{
3326-
#if !defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)//may be defined in GameCommon.h
3327-
if (TheGameLogic->isInReplayGame())
3328-
#endif
3327+
if (!TheGameLogic->isInMultiplayerGame())
33293328
{
33303329
TheGameLogic->setGamePaused(FALSE);
33313330
TheGameLogic->setGamePausedInFrame(TheGameLogic->getFrame() + 1, TRUE);
3331+
disp = DESTROY_MESSAGE;
33323332
}
33333333
break;
33343334
}

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

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
// INCLUDES ///////////////////////////////////////////////////////////////////////////////////////
3131
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
3232

33+
#include "Common/GameUtility.h"
3334
#include "Common/INI.h"
3435
#include "Common/MessageStream.h"
3536
#include "Common/Player.h"
@@ -174,7 +175,9 @@ static const LookupListRec GameMessageMetaTypeNames[] =
174175
{ "CAMERA_RESET", GameMessage::MSG_META_CAMERA_RESET },
175176
{ "TOGGLE_FAST_FORWARD_REPLAY", GameMessage::MSG_META_TOGGLE_FAST_FORWARD_REPLAY },
176177
{ "TOGGLE_PAUSE", GameMessage::MSG_META_TOGGLE_PAUSE },
178+
{ "TOGGLE_PAUSE_ALT", GameMessage::MSG_META_TOGGLE_PAUSE_ALT },
177179
{ "STEP_FRAME", GameMessage::MSG_META_STEP_FRAME },
180+
{ "STEP_FRAME_ALT", GameMessage::MSG_META_STEP_FRAME_ALT },
178181

179182
#if defined(RTS_DEBUG)
180183
{ "HELP", GameMessage::MSG_META_HELP },
@@ -362,6 +365,33 @@ static const char * findGameMessageNameByType(GameMessage::Type type)
362365
return "???";
363366
}
364367

368+
//-------------------------------------------------------------------------------------------------
369+
static Bool isMessageUsable(CommandUsableInType usableIn)
370+
{
371+
// We will ignore all commands if the game client has not yet incremented to frame 1.
372+
// It prevents the user from doing commands during a map load, which throws the input
373+
// system into whack because there isn't a client frame for the input event, and in
374+
// the case of a command that pauses the game, like the quit menu, the client frame
375+
// will never get beyond 0 and we lose the ability to process any input.
376+
if (TheGameClient->getFrame() == 0)
377+
return false;
378+
379+
const Bool usableInShell = (usableIn & COMMANDUSABLE_SHELL);
380+
const Bool usableInGame = (usableIn & COMMANDUSABLE_GAME);
381+
const Bool usableAsObserver = (usableIn & COMMANDUSABLE_OBSERVER);
382+
383+
if (usableInShell && TheShell && TheShell->isShellActive())
384+
return true;
385+
386+
if (usableInGame && (!TheShell || !TheShell->isShellActive()))
387+
return true;
388+
389+
if (usableAsObserver && rts::localPlayerIsObserving())
390+
return true;
391+
392+
return false;
393+
}
394+
365395
//-------------------------------------------------------------------------------------------------
366396
GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessage *msg)
367397
{
@@ -397,23 +427,7 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
397427
DEBUG_ASSERTCRASH(map->m_meta > GameMessage::MSG_BEGIN_META_MESSAGES &&
398428
map->m_meta < GameMessage::MSG_END_META_MESSAGES, ("hmm, expected only meta-msgs here"));
399429

400-
//
401-
// if this command is *only* usable in the game, we will ignore it if the game client
402-
// has not yet incremented to frame 1 (keeps us from doing in-game commands during
403-
// a map load, which throws the input system into wack because there isn't a
404-
// client frame for the input event, and in the case of a command that pauses the
405-
// game, like the quit menu, the client frame will never get beyond 0 and we
406-
// lose the ability to process any input
407-
//
408-
if( map->m_usableIn == COMMANDUSABLE_GAME && TheGameClient->getFrame() < 1 )
409-
continue;
410-
411-
// if the shell is active, and this command is not usable in shell, continue
412-
if (TheShell && TheShell->isShellActive() && !(map->m_usableIn & COMMANDUSABLE_SHELL) )
413-
continue;
414-
415-
// if the shell is not active and this command is not usable in the game, continue
416-
if (TheShell && !TheShell->isShellActive() && !(map->m_usableIn & COMMANDUSABLE_GAME) )
430+
if (!isMessageUsable(map->m_usableIn))
417431
continue;
418432

419433
// check for the special case of mods-only-changed.
@@ -719,7 +733,7 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
719733
map->m_key = MK_F;
720734
map->m_transition = DOWN;
721735
map->m_modState = NONE;
722-
map->m_usableIn = COMMANDUSABLE_GAME;
736+
map->m_usableIn = COMMANDUSABLE_GAME; // @todo COMMANDUSABLE_OBSERVER
723737
}
724738
}
725739
{
@@ -730,7 +744,18 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
730744
map->m_key = MK_P;
731745
map->m_transition = DOWN;
732746
map->m_modState = NONE;
733-
map->m_usableIn = COMMANDUSABLE_GAME;
747+
map->m_usableIn = COMMANDUSABLE_OBSERVER;
748+
}
749+
}
750+
{
751+
// Is useful for Generals and Zero Hour.
752+
MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TOGGLE_PAUSE_ALT);
753+
if (map->m_key == MK_NONE)
754+
{
755+
map->m_key = MK_P;
756+
map->m_transition = DOWN;
757+
map->m_modState = SHIFT; // Requires modifier to avoid key conflicts as a player.
758+
map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
734759
}
735760
}
736761
{
@@ -741,7 +766,18 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
741766
map->m_key = MK_O;
742767
map->m_transition = DOWN;
743768
map->m_modState = NONE;
744-
map->m_usableIn = COMMANDUSABLE_GAME;
769+
map->m_usableIn = COMMANDUSABLE_OBSERVER;
770+
}
771+
}
772+
{
773+
// Is useful for Generals and Zero Hour.
774+
MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_STEP_FRAME_ALT);
775+
if (map->m_key == MK_NONE)
776+
{
777+
map->m_key = MK_O;
778+
map->m_transition = DOWN;
779+
map->m_modState = SHIFT; // Requires modifier to avoid key conflicts as a player.
780+
map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
745781
}
746782
}
747783
{

GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,10 @@ class GameMessage : public MemoryPoolObject
274274
MSG_META_CAMERA_RESET,
275275
MSG_META_TOGGLE_CAMERA_TRACKING_DRAWABLE,
276276
MSG_META_TOGGLE_FAST_FORWARD_REPLAY, ///< Toggle the fast forward feature
277-
MSG_META_TOGGLE_PAUSE, ///< TheSuperHackers @feature Toggle game pause (in replay playbacks)
278-
MSG_META_STEP_FRAME, ///< TheSuperHackers @feature Step one frame (in replay playbacks)
277+
MSG_META_TOGGLE_PAUSE, ///< TheSuperHackers @feature Toggle game pause
278+
MSG_META_TOGGLE_PAUSE_ALT, ///< TheSuperHackers @feature Toggle game pause (alternative mapping)
279+
MSG_META_STEP_FRAME, ///< TheSuperHackers @feature Step one frame
280+
MSG_META_STEP_FRAME_ALT, ///< TheSuperHackers @feature Step one frame (alternative mapping)
279281
MSG_META_DEMO_INSTANT_QUIT, ///< bail out of game immediately
280282

281283

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,16 +314,18 @@ enum CommandUsableInType CPP_11(: Int)
314314
{
315315
COMMANDUSABLE_NONE = 0,
316316

317-
COMMANDUSABLE_SHELL = (1 << 0),
318-
COMMANDUSABLE_GAME = (1 << 1),
317+
COMMANDUSABLE_SHELL = (1 << 0), // Command is usable when in Shell (Menus)
318+
COMMANDUSABLE_GAME = (1 << 1), // Command is usable when not in Shell
319+
COMMANDUSABLE_OBSERVER = (1 << 2), // TheSuperHackers @feature Command is usable when observing
319320

320-
COMMANDUSABLE_EVERYWHERE = COMMANDUSABLE_SHELL | COMMANDUSABLE_GAME,
321+
COMMANDUSABLE_EVERYWHERE = ~0,
321322
};
322323

323324
static const char* TheCommandUsableInNames[] =
324325
{
325326
"SHELL",
326327
"GAME",
328+
"OBSERVER",
327329

328330
NULL
329331
};

0 commit comments

Comments
 (0)