Skip to content

Commit 2a31a7e

Browse files
committed
feat(input): Implement alternative key mappings for Game Pause and Step Frame that are usable as a Solo player
1 parent 40ac1c0 commit 2a31a7e

File tree

8 files changed

+147
-37
lines changed

8 files changed

+147
-37
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

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
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t)
402402

403403
CASE_LABEL(MSG_META_TOGGLE_FAST_FORWARD_REPLAY)
404404
CASE_LABEL(MSG_META_TOGGLE_PAUSE)
405+
CASE_LABEL(MSG_META_TOGGLE_PAUSE_ALT)
405406
CASE_LABEL(MSG_META_STEP_FRAME)
407+
CASE_LABEL(MSG_META_STEP_FRAME_ALT)
406408

407409
#if defined(RTS_DEBUG)
408410
CASE_LABEL(MSG_META_DEMO_TOGGLE_BEHIND_BUILDINGS)

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

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

34533453
}
34543454
case GameMessage::MSG_META_TOGGLE_PAUSE:
3455+
case GameMessage::MSG_META_TOGGLE_PAUSE_ALT:
34553456
{
3456-
#if !defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)//may be defined in GameCommon.h
3457-
if (TheGameLogic->isInReplayGame())
3458-
#endif
3457+
if (!TheGameLogic->isInMultiplayerGame())
34593458
{
34603459
if (TheGameLogic->isGamePaused())
34613460
{
@@ -3468,17 +3467,18 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
34683467
Bool pauseInput = FALSE;
34693468
TheGameLogic->setGamePaused(pause, pauseMusic, pauseInput);
34703469
}
3470+
disp = DESTROY_MESSAGE;
34713471
}
34723472
break;
34733473
}
34743474
case GameMessage::MSG_META_STEP_FRAME:
3475+
case GameMessage::MSG_META_STEP_FRAME_ALT:
34753476
{
3476-
#if !defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)//may be defined in GameCommon.h
3477-
if (TheGameLogic->isInReplayGame())
3478-
#endif
3477+
if (!TheGameLogic->isInMultiplayerGame())
34793478
{
34803479
TheGameLogic->setGamePaused(FALSE);
34813480
TheGameLogic->setGamePausedInFrame(TheGameLogic->getFrame() + 1, TRUE);
3481+
disp = DESTROY_MESSAGE;
34823482
}
34833483
break;
34843484
}

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

Lines changed: 58 additions & 26 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"
@@ -183,8 +184,10 @@ static const LookupListRec GameMessageMetaTypeNames[] =
183184
{ "TOGGLE_CAMERA_TRACKING_DRAWABLE", GameMessage::MSG_META_TOGGLE_CAMERA_TRACKING_DRAWABLE },
184185
{ "TOGGLE_FAST_FORWARD_REPLAY", GameMessage::MSG_META_TOGGLE_FAST_FORWARD_REPLAY },
185186
{ "TOGGLE_PAUSE", GameMessage::MSG_META_TOGGLE_PAUSE },
187+
{ "TOGGLE_PAUSE_ALT", GameMessage::MSG_META_TOGGLE_PAUSE_ALT },
186188
{ "STEP_FRAME", GameMessage::MSG_META_STEP_FRAME },
187-
{ "DEMO_INSTANT_QUIT", GameMessage::MSG_META_DEMO_INSTANT_QUIT },
189+
{ "STEP_FRAME_ALT", GameMessage::MSG_META_STEP_FRAME_ALT },
190+
{ "DEMO_INSTANT_QUIT", GameMessage::MSG_META_DEMO_INSTANT_QUIT },
188191

189192
#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)//may be defined in GameCommon.h
190193
{ "CHEAT_RUNSCRIPT1", GameMessage::MSG_CHEAT_RUNSCRIPT1 },
@@ -400,6 +403,33 @@ static const char * findGameMessageNameByType(GameMessage::Type type)
400403
return "???";
401404
}
402405

406+
//-------------------------------------------------------------------------------------------------
407+
static Bool isMessageUsable(CommandUsableInType usableIn)
408+
{
409+
// We will ignore all commands if the game client has not yet incremented to frame 1.
410+
// It prevents the user from doing commands during a map load, which throws the input
411+
// system into whack because there isn't a client frame for the input event, and in
412+
// the case of a command that pauses the game, like the quit menu, the client frame
413+
// will never get beyond 0 and we lose the ability to process any input.
414+
if (TheGameClient->getFrame() == 0)
415+
return false;
416+
417+
const Bool usableInShell = (usableIn & COMMANDUSABLE_SHELL);
418+
const Bool usableInGame = (usableIn & COMMANDUSABLE_GAME);
419+
const Bool usableAsObserver = (usableIn & COMMANDUSABLE_OBSERVER);
420+
421+
if (usableInShell && TheShell && TheShell->isShellActive())
422+
return true;
423+
424+
if (usableInGame && (!TheShell || !TheShell->isShellActive()))
425+
return true;
426+
427+
if (usableAsObserver && rts::localPlayerIsObserving())
428+
return true;
429+
430+
return false;
431+
}
432+
403433
//-------------------------------------------------------------------------------------------------
404434
GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessage *msg)
405435
{
@@ -431,34 +461,14 @@ GameMessageDisposition MetaEventTranslator::translateGameMessage(const GameMessa
431461
}
432462

433463

434-
for (const MetaMapRec *map = TheMetaMap->getFirstMetaMapRec(); map; map = map->m_next)
464+
for (const MetaMapRec *map = TheMetaMap->getFirstMetaMapRec(); map; map = map->m_next)
435465
{
436466
DEBUG_ASSERTCRASH(map->m_meta > GameMessage::MSG_BEGIN_META_MESSAGES &&
437467
map->m_meta < GameMessage::MSG_END_META_MESSAGES, ("hmm, expected only meta-msgs here"));
438468

439-
//
440-
// if this command is *only* usable in the game, we will ignore it if the game client
441-
// has not yet incremented to frame 1 (keeps us from doing in-game commands during
442-
// a map load, which throws the input system into wack because there isn't a
443-
// client frame for the input event, and in the case of a command that pauses the
444-
// game, like the quit menu, the client frame will never get beyond 0 and we
445-
// lose the ability to process any input
446-
//
447-
if( map->m_usableIn == COMMANDUSABLE_GAME && TheGameClient->getFrame() < 1 )
469+
if (!isMessageUsable(map->m_usableIn))
448470
continue;
449471

450-
// if the shell is active, and this command is not usable in shell, continue
451-
if (TheShell && TheShell->isShellActive() && !(map->m_usableIn & COMMANDUSABLE_SHELL) )
452-
continue;
453-
454-
// if the shell is not active and this command is not usable in the game, continue
455-
if (TheShell && !TheShell->isShellActive() && !(map->m_usableIn & COMMANDUSABLE_GAME) )
456-
continue;
457-
458-
459-
460-
461-
462472
// check for the special case of mods-only-changed.
463473
if (
464474
map->m_key == MK_NONE &&
@@ -781,7 +791,7 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
781791
map->m_key = MK_F;
782792
map->m_transition = DOWN;
783793
map->m_modState = NONE;
784-
map->m_usableIn = COMMANDUSABLE_GAME;
794+
map->m_usableIn = COMMANDUSABLE_GAME; // @todo COMMANDUSABLE_OBSERVER
785795
}
786796
}
787797
{
@@ -792,7 +802,18 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
792802
map->m_key = MK_P;
793803
map->m_transition = DOWN;
794804
map->m_modState = NONE;
795-
map->m_usableIn = COMMANDUSABLE_GAME;
805+
map->m_usableIn = COMMANDUSABLE_OBSERVER;
806+
}
807+
}
808+
{
809+
// Is useful for Generals and Zero Hour.
810+
MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TOGGLE_PAUSE_ALT);
811+
if (map->m_key == MK_NONE)
812+
{
813+
map->m_key = MK_P;
814+
map->m_transition = DOWN;
815+
map->m_modState = SHIFT; // Requires modifier to avoid key conflicts as a player.
816+
map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
796817
}
797818
}
798819
{
@@ -803,7 +824,18 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
803824
map->m_key = MK_O;
804825
map->m_transition = DOWN;
805826
map->m_modState = NONE;
806-
map->m_usableIn = COMMANDUSABLE_GAME;
827+
map->m_usableIn = COMMANDUSABLE_OBSERVER;
828+
}
829+
}
830+
{
831+
// Is useful for Generals and Zero Hour.
832+
MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_STEP_FRAME_ALT);
833+
if (map->m_key == MK_NONE)
834+
{
835+
map->m_key = MK_O;
836+
map->m_transition = DOWN;
837+
map->m_modState = SHIFT; // Requires modifier to avoid key conflicts as a player.
838+
map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
807839
}
808840
}
809841
{

0 commit comments

Comments
 (0)