Skip to content
Draft
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
1 change: 1 addition & 0 deletions Core/GameEngine/Include/Common/CRCDebug.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@
extern Int NET_CRC_INTERVAL;
extern Int REPLAY_CRC_INTERVAL;
extern Bool TheDebugIgnoreSyncErrors;
extern Bool TheDebugIgnoreReplaySyncErrors;
34 changes: 34 additions & 0 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@


Bool TheDebugIgnoreSyncErrors = FALSE;
Bool TheDebugIgnoreReplaySyncErrors = FALSE;
extern Int DX8Wrapper_PreserveFPU;

#ifdef DEBUG_CRC
Expand Down Expand Up @@ -751,6 +752,37 @@ Int parseLoadSave(char *args[], int num)
return 1;
}

// TheSuperHackers @feature bobtista 08/08/2026 Play a replay visually from the command line.
Int parseLoadReplay(char *args[], int num)
{
if (num > 1)
{
AsciiString filename = args[1];
if (!filename.endsWithNoCase(RecorderClass::getReplayExtention()))
{
printf("Invalid replay name \"%s\"\n", filename.str());
exit(1);
}

TheWritableGlobalData->m_loadReplayGame = filename;
TheWritableGlobalData->m_shellMapOn = FALSE;
TheWritableGlobalData->m_playIntro = FALSE;
TheWritableGlobalData->m_playSizzle = FALSE;

return 2;
}
return 1;
}

// TheSuperHackers @feature bobtista 08/08/2026 Let diagnostic replay playback continue past a CRC
// mismatch without the UI report and pause that normal playback uses.
Int parseIgnoreReplaySyncErrors(char *args[], int)
{
TheDebugIgnoreReplaySyncErrors = true;

return 1;
}

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

Expand Down Expand Up @@ -1200,6 +1232,8 @@ static CommandLineParam paramsForEngineInit[] =

// TheSuperHackers @feature bobtista 22/07/2026 Load a save game file from the command line.
{ "-loadsave", parseLoadSave },
{ "-loadreplay", parseLoadReplay },
{ "-ignoreReplaySyncErrors", parseIgnoreReplaySyncErrors },

// 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
1 change: 1 addition & 0 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 Down
64 changes: 62 additions & 2 deletions Generals/Code/GameEngine/Source/Common/Recorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "Common/GlobalData.h"
#include "Common/GameEngine.h"
#include "GameClient/ClientInstance.h"
#include "GameClient/MapUtil.h"
#include "GameClient/GameWindow.h"
#include "GameClient/GameWindowManager.h"
#include "GameClient/InGameUI.h"
Expand All @@ -47,6 +48,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 @@ -847,8 +849,14 @@ void RecorderClass::writeArgument(GameMessageArgumentDataType type, const GameMe
*/
Bool RecorderClass::readReplayHeader(ReplayHeader& header)
{
AsciiString filepath = getReplayDir();
filepath.concat(header.filename.str());
// TheSuperHackers @feature bobtista 08/08/2026 Open explicitly selected replay paths in place
// while preserving Replay directory lookup for menu filenames and existing command lines.
AsciiString filepath = header.filename;
if (!isAbsolutePath(filepath.str()))
{
filepath = getReplayDir();
filepath.concat(header.filename.str());
}

// TheSuperHackers @performance More buffered data reduces disk overhead and will improve fast forward playback
const UnsignedInt buffersize = header.forPlayback ? replayBufferBytes : File::BUFFERSIZE;
Expand Down Expand Up @@ -998,6 +1006,18 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f
// playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo.GetQueueSize()-1, playerIndex));
if (TheGameLogic->getFrame() > 0 && newCRC != playbackCRC && !m_crcInfo.sawCRCMismatch())
{
// TheSuperHackers @feature bobtista 08/08/2026 Diagnostic playback continues past a mismatch
// without the UI report and the pause that normal playback uses.
if (TheDebugIgnoreReplaySyncErrors)
{
const UnsignedInt ignoredFrame = TheGameLogic->getFrame() - m_crcInfo.GetQueueSize() - 1;
DEBUG_LOG(("Replay CRC mismatch ignored\nInGame:%8.8X Replay:%8.8X\nFrame:%d",
playbackCRC, newCRC, ignoredFrame));
printf("CRC Mismatch in Frame %d (ignored)\n", ignoredFrame);
m_crcInfo.setSawCRCMismatch();
return;
}

// Since we don't seem to have any *visible* desyncs when replaying games, but get this warning
// virtually every replay, the assumption is our CRC checking is faulty. Since we're at the
// tail end of patch season, let's just disable the message, and hope the users believe the
Expand Down Expand Up @@ -1074,6 +1094,46 @@ Bool RecorderClass::replayMatchesGameVersion(const ReplayHeader& header)
* Start playback of the file. Return true or false depending on if the file is
* a valid replay file or not.
*/
// TheSuperHackers @feature bobtista 08/08/2026 Play the replay requested on startup, once the client
// has created the shell layout that the playback returns to when it ends.
void RecorderClass::loadQueuedReplay()
{
const AsciiString filename = TheGlobalData->m_loadReplayGame;
TheWritableGlobalData->m_loadReplayGame.clear();

ReplayHeader header;
header.forPlayback = FALSE;
header.filename = filename;
if (!readReplayHeader(header))
{
DEBUG_LOG(("Replay '%s' could not be read", filename.str()));
TheGameEngine->setQuitting(TRUE);
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()));
TheGameEngine->setQuitting(TRUE);
return;
}

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

if (!playbackFile(filename))
{
DEBUG_LOG(("Failed to play replay '%s'", filename.str()));
TheGameEngine->setQuitting(TRUE);
}
}

Bool RecorderClass::playbackFile(AsciiString filename)
{
if (!m_doingAnalysis)
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
1 change: 1 addition & 0 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 Down
64 changes: 62 additions & 2 deletions GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "Common/GlobalData.h"
#include "Common/GameEngine.h"
#include "GameClient/ClientInstance.h"
#include "GameClient/MapUtil.h"
#include "GameClient/GameWindow.h"
#include "GameClient/GameWindowManager.h"
#include "GameClient/InGameUI.h"
Expand All @@ -47,6 +48,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 @@ -849,8 +851,14 @@ void RecorderClass::writeArgument(GameMessageArgumentDataType type, const GameMe
*/
Bool RecorderClass::readReplayHeader(ReplayHeader& header)
{
AsciiString filepath = getReplayDir();
filepath.concat(header.filename.str());
// TheSuperHackers @feature bobtista 08/08/2026 Open explicitly selected replay paths in place
// while preserving Replay directory lookup for menu filenames and existing command lines.
AsciiString filepath = header.filename;
if (!isAbsolutePath(filepath.str()))
{
filepath = getReplayDir();
filepath.concat(header.filename.str());
}

// TheSuperHackers @performance More buffered data reduces disk overhead and will improve fast forward playback
const UnsignedInt buffersize = header.forPlayback ? replayBufferBytes : File::BUFFERSIZE;
Expand Down Expand Up @@ -1000,6 +1008,18 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f
// playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo.GetQueueSize()-1, playerIndex));
if (TheGameLogic->getFrame() > 0 && newCRC != playbackCRC && !m_crcInfo.sawCRCMismatch())
{
// TheSuperHackers @feature bobtista 08/08/2026 Diagnostic playback continues past a mismatch
// without the UI report and the pause that normal playback uses.
if (TheDebugIgnoreReplaySyncErrors)
{
const UnsignedInt ignoredFrame = TheGameLogic->getFrame() - m_crcInfo.GetQueueSize() - 1;
DEBUG_LOG(("Replay CRC mismatch ignored\nInGame:%8.8X Replay:%8.8X\nFrame:%d",
playbackCRC, newCRC, ignoredFrame));
printf("CRC Mismatch in Frame %d (ignored)\n", ignoredFrame);
m_crcInfo.setSawCRCMismatch();
return;
}

//Kris: Patch 1.01 November 10, 2003 (integrated changes from Matt Campbell)
// Since we don't seem to have any *visible* desyncs when replaying games, but get this warning
// virtually every replay, the assumption is our CRC checking is faulty. Since we're at the
Expand Down Expand Up @@ -1077,6 +1097,46 @@ Bool RecorderClass::replayMatchesGameVersion(const ReplayHeader& header)
* Start playback of the file. Return true or false depending on if the file is
* a valid replay file or not.
*/
// TheSuperHackers @feature bobtista 08/08/2026 Play the replay requested on startup, once the client
// has created the shell layout that the playback returns to when it ends.
void RecorderClass::loadQueuedReplay()
{
const AsciiString filename = TheGlobalData->m_loadReplayGame;
TheWritableGlobalData->m_loadReplayGame.clear();

ReplayHeader header;
header.forPlayback = FALSE;
header.filename = filename;
if (!readReplayHeader(header))
{
DEBUG_LOG(("Replay '%s' could not be read", filename.str()));
TheGameEngine->setQuitting(TRUE);
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()));
TheGameEngine->setQuitting(TRUE);
return;
}

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

if (!playbackFile(filename))
{
DEBUG_LOG(("Failed to play replay '%s'", filename.str()));
TheGameEngine->setQuitting(TRUE);
}
}

Bool RecorderClass::playbackFile(AsciiString filename)
{
if (!m_doingAnalysis)
Expand Down
5 changes: 5 additions & 0 deletions GeneralsMD/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 @@ -541,6 +542,10 @@ void GameClient::update()
{
TheGameState->loadQueuedSaveGame();
}
else if (TheGlobalData->m_loadReplayGame.isNotEmpty())
{
TheRecorder->loadQueuedReplay();
}
}
}

Expand Down
Loading