Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1a8d339
bugfix(savegame): Add headless mode checks to prevent UI calls and sk…
bobtista Jan 17, 2026
f8e6753
feat(recorder): Add save/load serialization support for RecorderClass
bobtista Jan 15, 2026
b90eeb8
feat(cli): Add command line options for replay checkpoint save/load
bobtista Jan 15, 2026
a7d6f57
feat(replay): Add checkpoint save and resume functionality for replays
bobtista Jan 15, 2026
964c8fa
bugfix(recorder): Preserve mode and game info during checkpoint loading
bobtista Jan 19, 2026
3a2d0fc
bugfix(logic): Call prepareForMP_or_Skirmish when loading replay chec…
bobtista Jan 19, 2026
16b7b6d
bugfix(savegame): Fix save file path lookup in getSaveGameInfoFromFile
bobtista Jan 19, 2026
2d3886f
bugfix(cli): Fix command line parsing order for -loadCheckpoint and -…
bobtista Jan 19, 2026
305bff0
feat(replay): Implement continueReplayFromCheckpoint for headless che…
bobtista Jan 19, 2026
3b69948
bugfix(recorder): Capture replay file position before serialization
bobtista Jan 20, 2026
c0f492f
feat(random): Add functions to save/restore full RNG state for checkp…
bobtista Jan 20, 2026
8d57996
feat(object): Add friend methods to set next/prev pointers for list r…
bobtista Jan 20, 2026
0b91a7b
feat(savegame): Add TheAI to save block list for checkpoint serializa…
bobtista Jan 20, 2026
89a1959
refactor(replay): Simplify checkpoint save error handling
bobtista Jan 20, 2026
75d45b1
feat(replay): Add RNG state serialization and object list reversal fo…
bobtista Jan 20, 2026
9b4884b
feat(replay): Reset transient AI state after checkpoint load for CRC …
bobtista Jan 20, 2026
704094e
bugfix(replay): Fix shroud state corruption after checkpoint load by …
bobtista Jan 20, 2026
492ecd3
bugfix(replay): Initialize m_lastCell for dirty modules after checkpo…
bobtista Jan 20, 2026
33515e1
bugfix(replay): Serialize path timestamp and blocked state for checkp…
bobtista Jan 20, 2026
90213b7
bugfix(replay): Fix weapon timing corruption by comparing WeaponSet b…
bobtista Jan 20, 2026
a941470
feat(savegame): Serialize AI, Pathfinder, and AIGroup state for check…
bobtista Jan 20, 2026
bf1768e
feat(savegame): Serialize PartitionCell coordinates for checkpoint CR…
bobtista Jan 20, 2026
9a280f0
feat(replay): Preload CRC from replay file after checkpoint load for …
bobtista Jan 20, 2026
67f736d
refactor(replay): Move RNG restoration to start of update and add aut…
bobtista Jan 20, 2026
93c79d0
cleanup(replay): Remove obsolete resetTransientStateForCheckpoint wor…
bobtista Jan 21, 2026
8beb024
feat(replay): Call pathfinder loadPostProcess from AI to trigger zone…
bobtista Jan 21, 2026
eaf2c2d
cleanup(replay): Remove debug logging added during checkpoint investi…
bobtista Jan 22, 2026
b34884f
feat(replay): Add checkpoint load tracking and sleepy update order re…
bobtista Jan 22, 2026
e85c0ce
fix(replay): Call loadPostProcess on modules during checkpoint load
bobtista Jan 22, 2026
99cc98c
refactor(savegame): Fix weapon timing root cause by syncing template …
bobtista Jan 22, 2026
9b32872
bugfix(pathfind): Fix out-of-bounds array access in Pathfinder::crc()
bobtista Jan 22, 2026
cea56d7
bugfix(savegame): Handle absolute paths in getFilePathInSaveDirectory
bobtista Jan 22, 2026
54f675d
bugfix(checkpoint): Add protection for adjustDestination and goal pos…
bobtista Jan 26, 2026
94e8f06
bugfix(checkpoint): Add 3-frame protection window to justLoadedFromCh…
bobtista Jan 26, 2026
6b8c8f4
bugfix(checkpoint): Set checkpoint load frame for all units not just …
bobtista Jan 26, 2026
943b6a1
bugfix(checkpoint): Preserve cell flags in Pathfinder::loadPostProces…
bobtista Jan 26, 2026
9d2fc0c
bugfix(replay): Fix CRC queue handling and preload tracking after che…
bobtista Jan 26, 2026
5077a9d
bugfix(replay): Use isPlaybackMode for CRC message routing instead of…
bobtista Jan 26, 2026
c3c0ee7
refactor(checkpoint): Remove duplicate pathfinder loadPostProcess cal…
bobtista Jan 26, 2026
f94c413
bugfix(savegame): Delete pre-existing weapons during load when saved …
bobtista Jan 26, 2026
07dffa1
fix(savegame): Serialize script m_frameToEvaluateAt to prevent RNG di…
bobtista Jan 26, 2026
e4d251e
fix(savegame): Save checkpoint at correct frame to capture state befo…
bobtista Jan 26, 2026
2347317
fix(savegame): Support headless mode in BaseHeightMapRenderObjClass::…
bobtista Jan 27, 2026
9662300
fix(savegame): Include TerrainVisual chunk in headless mode saves
bobtista Jan 27, 2026
8c2a820
fix(savegame): Handle null pointers in W3DTerrainVisual::xfer for hea…
bobtista Jan 27, 2026
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
6 changes: 6 additions & 0 deletions Core/GameEngine/Include/Common/RandomValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ extern void InitGameLogicRandom( UnsignedInt seed ); ///< Set the GameLogic seed
extern UnsignedInt GetGameLogicRandomSeed( void ); ///< Get the seed (used for replays)
extern UnsignedInt GetGameLogicRandomSeedCRC( void );///< Get the seed (used for CRCs)

// TheSuperHackers @info bobtista 19/01/2026
// Functions to save/restore the full RNG state for checkpoints.
// The state array must have room for 6 UnsignedInts.
extern void GetGameLogicRandomState( UnsignedInt* state, UnsignedInt* baseSeed );
extern void SetGameLogicRandomState( const UnsignedInt* state, UnsignedInt baseSeed );

//--------------------------------------------------------------------------------------------------------------
3 changes: 3 additions & 0 deletions Core/GameEngine/Include/Common/ReplaySimulation.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class ReplaySimulation
// Returns exit code 0 if all replays were successfully simulated without mismatches
static int simulateReplays(const std::vector<AsciiString> &filenames, int maxProcesses);

// Continue a replay from a checkpoint file in headless mode
static int continueReplayFromCheckpoint(const AsciiString &checkpointFile);

static void stop() { s_isRunning = false; }

static Bool isRunning() { return s_isRunning; }
Expand Down
22 changes: 22 additions & 0 deletions Core/GameEngine/Source/Common/RandomValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ UnsignedInt GetGameLogicRandomSeedCRC( void )
return c.get();
}

// TheSuperHackers @info bobtista 19/01/2026
// Get the full RNG state for serialization in checkpoints.
void GetGameLogicRandomState( UnsignedInt* state, UnsignedInt* baseSeed )
{
for (int i = 0; i < 6; ++i)
{
state[i] = theGameLogicSeed[i];
}
*baseSeed = theGameLogicBaseSeed;
}

// TheSuperHackers @info bobtista 19/01/2026
// Restore the full RNG state after loading a checkpoint.
void SetGameLogicRandomState( const UnsignedInt* state, UnsignedInt baseSeed )
{
for (int i = 0; i < 6; ++i)
{
theGameLogicSeed[i] = state[i];
}
theGameLogicBaseSeed = baseSeed;
}

void InitRandom( void )
{
#ifdef DETERMINISTIC
Expand Down
112 changes: 112 additions & 0 deletions Core/GameEngine/Source/Common/ReplaySimulation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "Common/ReplaySimulation.h"

#include "Common/GameEngine.h"
#include "Common/GameState.h"
#include "Common/LocalFileSystem.h"
#include "Common/Recorder.h"
#include "Common/WorkerProcess.h"
Expand Down Expand Up @@ -99,8 +100,27 @@
fflush(stdout);
}
TheGameLogic->UPDATE();

// Check for checkpoint save BEFORE checking for CRC mismatch
// so we can save checkpoints even if the replay will eventually fail
if (TheGlobalData->m_replaySaveAtFrame != 0 &&

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6+t+e

'm_replaySaveAtFrame' : is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-profile+t+e

'm_replaySaveAtFrame' : is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-debug+t+e

'm_replaySaveAtFrame' : is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-debug+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-debug+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-profile+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-profile+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 106 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'
TheGameLogic->getFrame() == TheGlobalData->m_replaySaveAtFrame &&

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6+t+e

'm_replaySaveAtFrame' : is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-profile+t+e

'm_replaySaveAtFrame' : is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-debug+t+e

'm_replaySaveAtFrame' : is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-debug+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-debug+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-profile+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-profile+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'

Check failure on line 107 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg+t+e

'm_replaySaveAtFrame': is not a member of 'GlobalData'
!TheGlobalData->m_replaySaveTo.isEmpty())

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6+t+e

left of '.isEmpty' must have class/struct/union type

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6+t+e

'm_replaySaveTo' : is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-profile+t+e

left of '.isEmpty' must have class/struct/union type

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-profile+t+e

'm_replaySaveTo' : is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-debug+t+e

left of '.isEmpty' must have class/struct/union type

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-debug+t+e

'm_replaySaveTo' : is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-debug+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-debug+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-profile+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-profile+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 108 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg+t+e

'm_replaySaveTo': is not a member of 'GlobalData'
{
// TheSuperHackers @info bobtista 19/01/2026
// Pass just the filename to saveGame() - it will be saved to the Save directory.
SaveCode result = TheGameState->saveGame(TheGlobalData->m_replaySaveTo, UnicodeString::TheEmptyString, SAVE_FILE_TYPE_NORMAL);

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6+t+e

'm_replaySaveTo' : is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-profile+t+e

'm_replaySaveTo' : is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-debug+t+e

'm_replaySaveTo' : is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-debug+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-debug+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-profile+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-profile+t+e

'm_replaySaveTo': is not a member of 'GlobalData'

Check failure on line 112 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg+t+e

'm_replaySaveTo': is not a member of 'GlobalData'
if (result != SC_OK)
{
numErrors++;
}
TheRecorder->stopPlayback();
break;
}

if (TheRecorder->sawCRCMismatch())
{
DEBUG_LOG(("CRC mismatch at frame %d", TheGameLogic->getFrame()));
numErrors++;
break;
}
Expand Down Expand Up @@ -253,3 +273,95 @@
else
return simulateReplaysInWorkerProcesses(filenamesResolved, maxProcesses);
}

int ReplaySimulation::continueReplayFromCheckpoint(const AsciiString &checkpointFile)
{
int numErrors = 0;

// TheSuperHackers @info bobtista 19/01/2026
// We need to initialize the replay BEFORE loading the checkpoint because:
// 1. loadGame() calls startNewGame() which creates team prototypes based on the map
// 2. During replay playback, startNewGame() uses TheRecorder->getGameInfo() to get the correct team setup
// 3. But CHUNK_Recorder is loaded AFTER startNewGame() runs
// So we must initialize the replay first to provide the correct game info.
if (TheGlobalData->m_simulateReplays.empty())
{
DEBUG_LOG(("No replay file specified for checkpoint loading"));
return 1;
}

AsciiString replayFile = TheGlobalData->m_simulateReplays[0];
DEBUG_LOG(("Initializing replay from %s before loading checkpoint", replayFile.str()));

// Initialize the recorder from the replay file so that when loadGame() calls
// startNewGame(), it will have the correct game info from the replay header.
if (!TheRecorder->initializeReplayForCheckpointLoad(replayFile))

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6+t+e

'initializeReplayForCheckpointLoad' : is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-profile+t+e

'initializeReplayForCheckpointLoad' : is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / vc6-debug+t+e

'initializeReplayForCheckpointLoad' : is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-debug+t+e

'initializeReplayForCheckpointLoad': is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-debug+t+e

'initializeReplayForCheckpointLoad': is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-profile+t+e

'initializeReplayForCheckpointLoad': is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32+t+e

'initializeReplayForCheckpointLoad': is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg-profile+t+e

'initializeReplayForCheckpointLoad': is not a member of 'RecorderClass'

Check failure on line 298 in Core/GameEngine/Source/Common/ReplaySimulation.cpp

View workflow job for this annotation

GitHub Actions / Build Generals / win32-vcpkg+t+e

'initializeReplayForCheckpointLoad': is not a member of 'RecorderClass'
{
DEBUG_LOG(("Failed to initialize replay from %s", replayFile.str()));
return 1;
}

DEBUG_LOG(("Loading checkpoint from %s", checkpointFile.str()));

AvailableGameInfo gameInfo;
gameInfo.filename = checkpointFile;
TheGameState->getSaveGameInfoFromFile(checkpointFile, &gameInfo.saveGameInfo);

SaveCode result = TheGameState->loadGame(gameInfo);
if (result != SC_OK)
{
DEBUG_LOG(("Failed to load checkpoint (error %d)", result));
return 1;
}

if (!TheRecorder->isPlaybackMode())
{
DEBUG_LOG(("Checkpoint was not saved during replay playback"));
return 1;
}

DEBUG_LOG(("Resuming replay from frame %d", TheGameLogic->getFrame()));

#ifdef _DEBUG
DWORD startTimeMillis = GetTickCount();
UnsignedInt totalTimeSec = TheRecorder->getPlaybackFrameCount() / LOGICFRAMES_PER_SECOND;
#endif

while (TheRecorder->isPlaybackInProgress())
{
TheGameClient->updateHeadless();

#ifdef _DEBUG
const int progressFrameInterval = 10*60*LOGICFRAMES_PER_SECOND;
if (TheGameLogic->getFrame() != 0 && TheGameLogic->getFrame() % progressFrameInterval == 0)
{
UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND;
UnsignedInt realTimeSec = (GetTickCount()-startTimeMillis) / 1000;
DEBUG_LOG(("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d",
realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60));
}
#endif
TheGameLogic->UPDATE();
if (TheRecorder->sawCRCMismatch())
{
numErrors++;
break;
}
Comment on lines +345 to +349
Copy link

@Caball009 Caball009 Jan 20, 2026

Choose a reason for hiding this comment

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

There are some issues with the CRC computation for the replay from save game.

}

#ifdef _DEBUG
{
UnsignedInt gameTimeSec = TheGameLogic->getFrame() / LOGICFRAMES_PER_SECOND;
UnsignedInt realTimeSec = (GetTickCount()-startTimeMillis) / 1000;
DEBUG_LOG(("Elapsed Time: %02d:%02d Game Time: %02d:%02d/%02d:%02d",
realTimeSec/60, realTimeSec%60, gameTimeSec/60, gameTimeSec%60, totalTimeSec/60, totalTimeSec%60));

if (TheRecorder->sawCRCMismatch())
DEBUG_LOG(("CRC Mismatch detected!"));
else
DEBUG_LOG(("Replay completed successfully"));
}
#endif

return numErrors != 0 ? 1 : 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2984,18 +2984,53 @@ void BaseHeightMapRenderObjClass::crc( Xfer *xfer )
// ------------------------------------------------------------------------------------------------
/** Xfer
* Version Info:
* 1: Initial version */
* 1: Initial version
* 2: Added support for headless mode where tree/prop buffers may be null */
// ------------------------------------------------------------------------------------------------
void BaseHeightMapRenderObjClass::xfer( Xfer *xfer )
{

// version
XferVersion currentVersion = 1;
XferVersion currentVersion = 2;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );

xfer->xferSnapshot( m_treeBuffer );
xfer->xferSnapshot( m_propBuffer );
if (version >= 2)
{
// TheSuperHackers @info bobtista 26/01/2026
// In headless mode, m_treeBuffer and m_propBuffer are not allocated.
// We need to track whether they were present when saved so we can properly
// skip the data when loading in headless mode (or vice versa).
Bool hasTreeBuffer = (m_treeBuffer != nullptr);
xfer->xferBool(&hasTreeBuffer);
if (hasTreeBuffer)
{
if (xfer->getXferMode() == XFER_LOAD && m_treeBuffer == nullptr)
{
DEBUG_CRASH(("BaseHeightMapRenderObjClass::xfer - Cannot load tree buffer in headless mode"));
throw XFER_INVALID_PARAMETERS;
}
xfer->xferSnapshot(m_treeBuffer);
}

Bool hasPropBuffer = (m_propBuffer != nullptr);
xfer->xferBool(&hasPropBuffer);
if (hasPropBuffer)
{
if (xfer->getXferMode() == XFER_LOAD && m_propBuffer == nullptr)
{
DEBUG_CRASH(("BaseHeightMapRenderObjClass::xfer - Cannot load prop buffer in headless mode"));
throw XFER_INVALID_PARAMETERS;
}
xfer->xferSnapshot(m_propBuffer);
}
}
else
{
// Version 1: unconditional serialization (legacy - requires both buffers)
xfer->xferSnapshot( m_treeBuffer );
xfer->xferSnapshot( m_propBuffer );
}


}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,9 @@ void W3DTerrainVisual::crc( Xfer *xfer )
// ------------------------------------------------------------------------------------------------
void W3DTerrainVisual::xfer( Xfer *xfer )
{
// In headless mode, terrain may not be fully initialized
// Only proceed if we have the height map data (which is the critical logic data)
Bool hasHeightMap = (m_logicHeightMap != nullptr);

// version
#if RTS_GENERALS && RETAIL_COMPATIBLE_XFER_SAVE
Expand All @@ -1164,9 +1167,9 @@ void W3DTerrainVisual::xfer( Xfer *xfer )
TerrainVisual::xfer( xfer );

// flag for whether or not the water grid is enabled
Bool gridEnabled = m_isWaterGridRenderingEnabled;
Bool gridEnabled = hasHeightMap ? m_isWaterGridRenderingEnabled : FALSE;
xfer->xferBool( &gridEnabled );
if( gridEnabled != m_isWaterGridRenderingEnabled )
if( hasHeightMap && gridEnabled != m_isWaterGridRenderingEnabled )
{

DEBUG_CRASH(( "W3DTerrainVisual::xfer - m_isWaterGridRenderingEnabled mismatch" ));
Expand All @@ -1175,7 +1178,7 @@ void W3DTerrainVisual::xfer( Xfer *xfer )
}

// xfer grid data if enabled
if( gridEnabled )
if( gridEnabled && m_waterRenderObject != nullptr )
xfer->xferSnapshot( m_waterRenderObject );

/*
Expand Down Expand Up @@ -1210,25 +1213,31 @@ void W3DTerrainVisual::xfer( Xfer *xfer )

// Write out the terrain height data.
if (version >= 2) {
UnsignedByte *data = m_logicHeightMap->getDataPtr();
Int len = m_logicHeightMap->getXExtent()*m_logicHeightMap->getYExtent();
Int xferLen = len;
xfer->xferInt(&xferLen);
if (len!=xferLen) {
DEBUG_CRASH(("Bad height map length."));
if (len>xferLen) {
len = xferLen;
// In headless mode, m_logicHeightMap may be nullptr if terrain wasn't loaded
if (m_logicHeightMap == nullptr) {
Int xferLen = 0;
xfer->xferInt(&xferLen);
} else {
UnsignedByte *data = m_logicHeightMap->getDataPtr();
Int len = m_logicHeightMap->getXExtent()*m_logicHeightMap->getYExtent();
Int xferLen = len;
xfer->xferInt(&xferLen);
if (len!=xferLen) {
DEBUG_CRASH(("Bad height map length."));
if (len>xferLen) {
len = xferLen;
}
}
xfer->xferUser(data, len);
if (xfer->getXferMode() == XFER_LOAD && m_terrainRenderObject != nullptr)
{
// Update the display height map.
m_terrainRenderObject->staticLightingChanged();
}
}
xfer->xferUser(data, len);
if (xfer->getXferMode() == XFER_LOAD)
{
// Update the display height map.
m_terrainRenderObject->staticLightingChanged();
}
}

if (version >= 3) {
if (version >= 3 && m_terrainRenderObject != nullptr) {
xfer->xferSnapshot(m_terrainRenderObject);
}

Expand Down
4 changes: 4 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ class GlobalData : public SubsystemInterface
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

UnsignedInt m_replaySaveAtFrame; ///< If non-zero, auto-save when this frame is reached during replay
AsciiString m_replaySaveTo; ///< Filename for auto-save during replay
AsciiString m_loadReplayCheckpoint; ///< If set, load this checkpoint and continue replay

Int m_maxParticleCount; ///< maximum number of particles that can exist
Int m_maxFieldParticleCount; ///< maximum number of field-type particles that can exist (roughly)
WeaponBonusSet* m_weaponBonusSet;
Expand Down
Loading
Loading