Skip to content

Commit 67f736d

Browse files
bobtistaclaude
andcommitted
refactor(replay): Move RNG restoration to start of update and add auto-checkpoint feature
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9a280f0 commit 67f736d

File tree

2 files changed

+60
-41
lines changed

2 files changed

+60
-41
lines changed

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,16 +2070,20 @@ void RecorderClass::preloadNextCRCFromReplay( void )
20702070
UnsignedByte numTypes = 0;
20712071
m_file->read( &numTypes, sizeof(numTypes) );
20722072

2073-
// Calculate total arguments and their sizes
2073+
// Calculate total arguments and their sizes (use fixed arrays for VC6 compatibility)
2074+
enum { MAX_ARG_TYPES = 32 };
2075+
GameMessageArgumentDataType argTypes[MAX_ARG_TYPES];
2076+
Int argCounts[MAX_ARG_TYPES];
20742077
Int totalArgs = 0;
2075-
std::vector<std::pair<GameMessageArgumentDataType, Int>> argInfo;
2076-
for ( UnsignedByte i = 0; i < numTypes; ++i )
2078+
Int actualNumTypes = (numTypes < MAX_ARG_TYPES) ? numTypes : MAX_ARG_TYPES;
2079+
for ( Int i = 0; i < actualNumTypes; ++i )
20772080
{
20782081
UnsignedByte argType = 0;
20792082
UnsignedByte argCount = 0;
20802083
m_file->read( &argType, sizeof(argType) );
20812084
m_file->read( &argCount, sizeof(argCount) );
2082-
argInfo.push_back( std::make_pair( static_cast<GameMessageArgumentDataType>(argType), static_cast<Int>(argCount) ) );
2085+
argTypes[i] = static_cast<GameMessageArgumentDataType>(argType);
2086+
argCounts[i] = static_cast<Int>(argCount);
20832087
totalArgs += argCount;
20842088
}
20852089

@@ -2103,10 +2107,10 @@ void RecorderClass::preloadNextCRCFromReplay( void )
21032107
{
21042108
// Skip this message's arguments
21052109
Bool skipFailed = FALSE;
2106-
for ( size_t i = 0; i < argInfo.size() && !skipFailed; ++i )
2110+
for ( Int i = 0; i < actualNumTypes && !skipFailed; ++i )
21072111
{
2108-
GameMessageArgumentDataType argType = argInfo[i].first;
2109-
Int argCount = argInfo[i].second;
2112+
GameMessageArgumentDataType argType = argTypes[i];
2113+
Int argCount = argCounts[i];
21102114

21112115
Int argSize = getArgumentByteSize( argType );
21122116
if ( argSize < 0 )

GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
#include "GameLogic/Module/CreateModule.h"
9292
#include "GameLogic/Module/DestroyModule.h"
9393
#include "GameLogic/Module/OpenContain.h"
94+
#include "GameLogic/Module/PhysicsUpdate.h"
9495
#include "GameLogic/PartitionManager.h"
9596
#include "GameLogic/PolygonTrigger.h"
9697
#include "GameLogic/ScriptActions.h"
@@ -3662,6 +3663,15 @@ void GameLogic::update( void )
36623663
UnsignedInt now = getFrame();
36633664
TheGameClient->setFrame(now);
36643665

3666+
// TheSuperHackers @info bobtista 20/01/2026
3667+
// Restore RNG state immediately at start of first logic update after checkpoint load.
3668+
// This must happen before any scripts, terrain, or object updates run, since they may call random.
3669+
if ( m_pendingRngRestore )
3670+
{
3671+
SetGameLogicRandomState( m_pendingRngState, m_pendingRngBaseSeed );
3672+
m_pendingRngRestore = FALSE;
3673+
}
3674+
36653675
// update (execute) scripts
36663676
{
36673677
TheScriptEngine->UPDATE();
@@ -3692,21 +3702,10 @@ void GameLogic::update( void )
36923702
// doesn't perfectly match what CRC calculation expects due to timing differences in the
36933703
// frame lifecycle. Skip multiple checks to allow state to stabilize.
36943704
// NOTE: Don't decrement here - it's decremented in handleCRCMessage() after validation skipped.
3705+
// NOTE: RNG state is now restored at start of update(), so no need to check here.
36953706
if ( m_skipCRCCheckCount > 0 )
36963707
{
3697-
// Still restore RNG and reset transient state if pending
3698-
if ( m_pendingRngRestore )
3699-
{
3700-
SetGameLogicRandomState( m_pendingRngState, m_pendingRngBaseSeed );
3701-
m_pendingRngRestore = FALSE;
3702-
3703-
// TheSuperHackers @info bobtista 19/01/2026
3704-
// Reset transient AI state that affects CRC but isn't properly serialized.
3705-
if ( TheAI )
3706-
{
3707-
TheAI->resetTransientStateForCheckpoint();
3708-
}
3709-
}
3708+
// Nothing to do here - skip CRC generation
37103709
}
37113710
else
37123711
{
@@ -3865,6 +3864,36 @@ void GameLogic::update( void )
38653864
m_frame++;
38663865
m_hasUpdated = TRUE;
38673866
}
3867+
3868+
// TheSuperHackers @feature bobtista 20/01/2026 Auto-save checkpoint at specified frame during replay playback
3869+
// Save AFTER m_frame is incremented so the checkpoint correctly represents
3870+
// "ready to play frame N+1" when saved after frame N completes.
3871+
// We check for m_frame == saveAtFrame + 1 since m_frame was just incremented.
3872+
if (TheGlobalData->m_replaySaveAtFrame > 0 && m_frame == TheGlobalData->m_replaySaveAtFrame + 1)
3873+
{
3874+
AsciiString saveName = TheGlobalData->m_replaySaveTo;
3875+
if (saveName.isEmpty())
3876+
{
3877+
saveName.format("checkpoint_%u.sav", TheGlobalData->m_replaySaveAtFrame);
3878+
}
3879+
UnicodeString desc;
3880+
desc.format(L"Replay checkpoint after frame %u", TheGlobalData->m_replaySaveAtFrame);
3881+
DEBUG_LOG(("Auto-saving checkpoint after frame %u (current frame %u) to %s",
3882+
TheGlobalData->m_replaySaveAtFrame, m_frame, saveName.str()));
3883+
SaveCode result = TheGameState->saveGame(saveName, desc, SAVE_FILE_TYPE_NORMAL, SNAPSHOT_SAVELOAD);
3884+
if (result != SC_OK)
3885+
{
3886+
DEBUG_LOG(("WARNING: Failed to save checkpoint, error code %d", result));
3887+
}
3888+
else
3889+
{
3890+
DEBUG_LOG(("Checkpoint saved successfully, exiting replay simulation"));
3891+
// Exit the game after saving the checkpoint
3892+
TheGameEngine->setQuitting(TRUE);
3893+
}
3894+
// Clear the save frame so we don't try to save again
3895+
TheWritableGlobalData->m_replaySaveAtFrame = 0;
3896+
}
38683897
}
38693898

38703899
// ------------------------------------------------------------------------------------------------
@@ -4074,23 +4103,9 @@ UnsignedInt GameLogic::getCRC( Int mode, AsciiString deepCRCFileName )
40744103

40754104
setFPMode();
40764105

4077-
// TheSuperHackers @info bobtista 19/01/2026
4078-
// Restore the RNG state right before CRC calculation if we just loaded from a checkpoint.
4079-
// This ensures the RNG state is exactly what it was when the checkpoint was saved,
4080-
// even if something called random between loadPostProcess() and now.
4081-
if ( m_pendingRngRestore )
4082-
{
4083-
SetGameLogicRandomState( m_pendingRngState, m_pendingRngBaseSeed );
4084-
m_pendingRngRestore = FALSE;
4085-
4086-
// TheSuperHackers @info bobtista 19/01/2026
4087-
// Reset transient AI state that affects CRC but isn't properly serialized.
4088-
// This must happen after checkpoint load but before CRC calculation.
4089-
if ( TheAI )
4090-
{
4091-
TheAI->resetTransientStateForCheckpoint();
4092-
}
4093-
}
4106+
// TheSuperHackers @info bobtista 20/01/2026
4107+
// RNG state is now restored at start of update() instead of here.
4108+
// This ensures it happens before any game logic runs, not just before CRC check.
40944109

40954110
LatchRestore<Bool> latch(inCRCGen, !isInGameLogicUpdate());
40964111

@@ -5315,8 +5330,8 @@ void GameLogic::loadPostProcess( void )
53155330
// re-sort the priority queue all at once now that all modules are on it
53165331
remakeSleepyUpdate();
53175332

5318-
// TheSuperHackers @info bobtista 19/01/2026
5319-
// Note: RNG state restoration is deferred to getCRC() to ensure it happens right before
5320-
// CRC calculation. This is necessary because code that runs between loadPostProcess() and
5321-
// getCRC() may call random functions (e.g., updateHeadless(), ScriptEngine, TerrainLogic).
5333+
// TheSuperHackers @info bobtista 20/01/2026
5334+
// Note: RNG state restoration is handled in update() at the start of the first logic update
5335+
// after checkpoint load. This ensures the RNG is restored before any scripts or game logic
5336+
// that might call random functions. The m_pendingRngRestore flag signals when restoration is needed.
53225337
}

0 commit comments

Comments
 (0)