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
17 changes: 17 additions & 0 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,20 @@ Int parseLoadSave(char *args[], int num)
return 1;
}

Int parseLoadReplay(char *args[], int num)
{
if (num > 1)
{
TheWritableGlobalData->m_loadReplayGame = args[1];
TheWritableGlobalData->m_shellMapOn = FALSE;
TheWritableGlobalData->m_playIntro = FALSE;
TheWritableGlobalData->m_playSizzle = FALSE;

return 2;
}
return 1;
}

//=============================================================================
//=============================================================================

Expand Down Expand Up @@ -1201,6 +1215,9 @@ static CommandLineParam paramsForEngineInit[] =
// TheSuperHackers @feature bobtista 22/07/2026 Load a save game file from the command line.
{ "-loadsave", parseLoadSave },

// TheSuperHackers @feature bobtista 08/08/2026 Play a replay file from the command line.
{ "-loadreplay", parseLoadReplay },
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

// TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it.
{ "-forcefullviewport", parseFullViewport },

Expand Down
1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ class GlobalData : public SubsystemInterface
AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line
AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start
AsciiString m_loadSaveGame; ///< If this is specified, load a save game file from the command-line
AsciiString m_loadReplayGame; ///< If this is specified, play a replay file from the command-line

std::vector<AsciiString> m_simulateReplays; ///< If not empty, simulate this list of replays and exit.
Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation
Expand Down
5 changes: 3 additions & 2 deletions Generals/Code/GameEngine/Include/Common/Recorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class RecorderClass : public SubsystemInterface
// Methods dealing with playback.
void updatePlayback(); ///< The update function for playing back a file.
Bool playbackFile(AsciiString filename); ///< Starts playback of the specified file.
void loadQueuedReplay(); ///< Play the replay file requested on startup.
Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version.
static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version.
AsciiString getCurrentReplayFilename(); ///< valid during playback only
Expand All @@ -117,7 +118,6 @@ class RecorderClass : public SubsystemInterface
struct ReplayHeader
{
AsciiString filename;
Bool forPlayback;
UnicodeString replayName;
SYSTEMTIME timeVal;
UnicodeString versionString;
Expand All @@ -134,13 +134,14 @@ class RecorderClass : public SubsystemInterface
AsciiString gameOptions;
Int localPlayerIndex;
};
Bool readReplayHeader( ReplayHeader& header );
Bool readReplayHeader( ReplayHeader& header, const AsciiString& filename, Bool forPlayback );

RecorderModeType getMode(); ///< Returns the current operating mode.
Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; }
void initControls(); ///< Show or Hide the Replay controls

static AsciiString getReplayDir(); ///< Returns the directory that holds the replay files.
static AsciiString getReplayPathForRead(const AsciiString& filenameOrPath); ///< Returns the path to open for a replay filename or absolute replay path.
static AsciiString getReplayArchiveDir(); ///< Returns the directory that holds the archived replay files.
static AsciiString getReplayExtention(); ///< Returns the file extention for replay files.
static AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay.
Expand Down
94 changes: 81 additions & 13 deletions Generals/Code/GameEngine/Source/Common/Recorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include "Common/GlobalData.h"
#include "Common/GameEngine.h"
#include "GameClient/ClientInstance.h"
#include "GameClient/MapUtil.h"
#include "GameClient/MessageBox.h"
#include "GameClient/GameWindow.h"
#include "GameClient/GameWindowManager.h"
#include "GameClient/InGameUI.h"
Expand All @@ -47,6 +49,7 @@
#include "Common/CRCDebug.h"
#include "Common/OptionPreferences.h"
#include "Common/version.h"
#include "Lib/PathUtil.h"

constexpr const char s_genrep[] = "GENREP";
constexpr const UnsignedInt replayBufferBytes = 8192;
Expand Down Expand Up @@ -845,18 +848,17 @@ void RecorderClass::writeArgument(GameMessageArgumentDataType type, const GameMe
* Read in a replay header, for (1) populating a replay listbox or (2) starting playback. In
* case (2), set FILE *m_file.
*/
Bool RecorderClass::readReplayHeader(ReplayHeader& header)
Bool RecorderClass::readReplayHeader(ReplayHeader& header, const AsciiString& filename, Bool forPlayback)
{
AsciiString filepath = getReplayDir();
filepath.concat(header.filename.str());
header.filename = getReplayPathForRead(filename);

// TheSuperHackers @performance More buffered data reduces disk overhead and will improve fast forward playback
const UnsignedInt buffersize = header.forPlayback ? replayBufferBytes : File::BUFFERSIZE;
m_file = TheFileSystem->openFile(filepath.str(), File::READ | File::BINARY, buffersize);
const UnsignedInt buffersize = forPlayback ? replayBufferBytes : File::BUFFERSIZE;
m_file = TheFileSystem->openFile(header.filename.str(), File::READ | File::BINARY, buffersize);

if (m_file == nullptr)
{
DEBUG_LOG(("Can't open %s (%s)", filepath.str(), header.filename.str()));
DEBUG_LOG(("Can't open %s (%s)", header.filename.str(), filename.str()));
return FALSE;
}

Expand Down Expand Up @@ -930,7 +932,7 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header)
m_gameInfo.setLocalIP(localIP);
}

if (!header.forPlayback)
if (!forPlayback)
{
m_gameInfo.endGame();
m_gameInfo.reset();
Expand Down Expand Up @@ -1047,9 +1049,7 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f
Bool RecorderClass::replayMatchesGameVersion(AsciiString filename)
{
ReplayHeader header;
header.forPlayback = TRUE;
header.filename = filename;
if ( readReplayHeader( header ) )
if ( readReplayHeader( header, filename, TRUE ) )
{
return replayMatchesGameVersion( header );
}
Expand All @@ -1070,6 +1070,61 @@ Bool RecorderClass::replayMatchesGameVersion(const ReplayHeader& header)
return true;
}

static void showQueuedReplayLoadFailure()
{
UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayLoadFailedTitle", L"REPLAY CANNOT BE LOADED");
UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayLoadFailed", L"The replay file could not be opened or is invalid.");

MessageBoxOk(title, body, nullptr);
}

static void showQueuedReplayMapNotFound()
{
UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFoundTitle", L"MAP NOT FOUND");
UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFound", L"This replay cannot be loaded because the map was not found on this device.");

MessageBoxOk(title, body, nullptr);
}

/**
* Play the replay requested on startup, after the shell has been initialized
*/
void RecorderClass::loadQueuedReplay()
{
const AsciiString filename = TheGlobalData->m_loadReplayGame;
TheWritableGlobalData->m_loadReplayGame.clear();

ReplayHeader header;
if (!readReplayHeader(header, filename, FALSE))
{
DEBUG_LOG(("Replay '%s' could not be read", filename.str()));
showQueuedReplayLoadFailure();
return;
}

// A replay whose map is missing starts a game that cannot load, so reject it here instead
ReplayGameInfo gameInfo;
if (!ParseAsciiStringToGameInfo(&gameInfo, header.gameOptions))
{
DEBUG_LOG(("Replay '%s' contains invalid game options", filename.str()));
showQueuedReplayLoadFailure();
return;
}

if (TheMapCache == nullptr || TheMapCache->findMap(gameInfo.getMap()) == nullptr)
{
DEBUG_LOG(("Replay '%s' requires unavailable map '%s'", filename.str(), gameInfo.getMap().str()));
showQueuedReplayMapNotFound();
return;
}

if (!playbackFile(filename))
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Comment thread
xezon marked this conversation as resolved.
{
DEBUG_LOG(("Failed to play replay '%s'", filename.str()));
showQueuedReplayLoadFailure();
}
}

/**
* Start playback of the file. Return true or false depending on if the file is
* a valid replay file or not.
Expand All @@ -1085,9 +1140,7 @@ Bool RecorderClass::playbackFile(AsciiString filename)
}

ReplayHeader header;
header.forPlayback = TRUE;
header.filename = filename;
Bool success = readReplayHeader( header );
Bool success = readReplayHeader( header, filename, TRUE );
if (!success)
{
return FALSE;
Expand Down Expand Up @@ -1555,6 +1608,21 @@ AsciiString RecorderClass::getReplayDir()
return tmp;
}

/**
* returns the path to open for a replay filename in the replay directory or an absolute replay path.
*/
AsciiString RecorderClass::getReplayPathForRead(const AsciiString& filenameOrPath)
{
if (isAbsolutePath(filenameOrPath.str()))
{
return filenameOrPath;
}

AsciiString tmp = getReplayDir();
tmp.concat(filenameOrPath);
return tmp;
}

/**
* returns the directory that holds the archived replay files.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index)

static Bool readReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader &header, ReplayGameInfo &info, const MapMetaData *&mapData)
{
header.forPlayback = FALSE;
header.filename = filename;

if (TheRecorder != nullptr && TheRecorder->readReplayHeader(header))
if (TheRecorder != nullptr && TheRecorder->readReplayHeader(header, filename, FALSE))
{
if (ParseAsciiStringToGameInfo(&info, header.gameOptions))
{
Expand Down
5 changes: 5 additions & 0 deletions Generals/Code/GameEngine/Source/GameClient/GameClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "Common/ActionManager.h"
#include "Common/GameEngine.h"
#include "Common/GameState.h"
#include "Common/Recorder.h"
#include "Common/GameUtility.h"
#include "Common/GlobalData.h"
#include "Common/PerfTimer.h"
Expand Down Expand Up @@ -520,6 +521,10 @@ void GameClient::update()
{
TheGameState->loadQueuedSaveGame();
}
else if (TheGlobalData->m_loadReplayGame.isNotEmpty())
{
TheRecorder->loadQueuedReplay();
}
}
}

Expand Down
1 change: 1 addition & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class GlobalData : public SubsystemInterface
AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line
AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start
AsciiString m_loadSaveGame; ///< If this is specified, load a save game file from the command-line
AsciiString m_loadReplayGame; ///< If this is specified, play a replay file from the command-line

std::vector<AsciiString> m_simulateReplays; ///< If not empty, simulate this list of replays and exit.
Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation
Expand Down
5 changes: 3 additions & 2 deletions GeneralsMD/Code/GameEngine/Include/Common/Recorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class RecorderClass : public SubsystemInterface
// Methods dealing with playback.
void updatePlayback(); ///< The update function for playing back a file.
Bool playbackFile(AsciiString filename); ///< Starts playback of the specified file.
void loadQueuedReplay(); ///< Play the replay file requested on startup.
Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version.
static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version.
AsciiString getCurrentReplayFilename(); ///< valid during playback only
Expand All @@ -117,7 +118,6 @@ class RecorderClass : public SubsystemInterface
struct ReplayHeader
{
AsciiString filename;
Bool forPlayback;
UnicodeString replayName;
SYSTEMTIME timeVal;
UnicodeString versionString;
Expand All @@ -134,13 +134,14 @@ class RecorderClass : public SubsystemInterface
AsciiString gameOptions;
Int localPlayerIndex;
};
Bool readReplayHeader( ReplayHeader& header );
Bool readReplayHeader( ReplayHeader& header, const AsciiString& filename, Bool forPlayback );

RecorderModeType getMode(); ///< Returns the current operating mode.
Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; }
void initControls(); ///< Show or Hide the Replay controls

static AsciiString getReplayDir(); ///< Returns the directory that holds the replay files.
static AsciiString getReplayPathForRead(const AsciiString& filenameOrPath); ///< Returns the path to open for a replay filename or absolute replay path.
static AsciiString getReplayArchiveDir(); ///< Returns the directory that holds the archived replay files.
static AsciiString getReplayExtention(); ///< Returns the file extention for replay files.
static AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay.
Expand Down
Loading