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

// TheSuperHackers @feature bobtista 14/08/2026 Write a save at a chosen logic frame so a save
// and load round trip can be run without a person at the keyboard.
Int parseSaveAtFrame(char *args[], int num)
{
if (num > 1)
{
TheWritableGlobalData->m_saveAtFrame = atoi(args[1]);
}
return 2;
}

Int parseSaveTo(char *args[], int num)
{
if (num > 1)
{
TheWritableGlobalData->m_saveToFile = args[1];
}
return 2;
}

Int parseFile(char *args[], int num)
{
if (num > 1)
Expand Down Expand Up @@ -1254,6 +1274,8 @@ static CommandLineParam paramsForEngineInit[] =
{ "-munkee", parseMunkee },
{ "-displayDebug", parseDisplayDebug },
{ "-file", parseFile },
{ "-saveatframe", parseSaveAtFrame },
{ "-saveto", parseSaveTo },

// { "-preload", parsePreload },

Expand Down
2 changes: 2 additions & 0 deletions Generals/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ class GlobalData : public SubsystemInterface
Bool m_enforceMaxCameraHeight; ///< Enforce max camera height while scrolling?
Bool m_buildMapCache;
AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line
Int m_saveAtFrame; ///< If greater than zero, write a save when this logic frame is reached
AsciiString m_saveToFile; ///< Filename used by m_saveAtFrame
AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start

std::vector<AsciiString> m_simulateReplays; ///< If not empty, simulate this list of replays and exit.
Expand Down
2 changes: 2 additions & 0 deletions Generals/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,8 @@ GlobalData::GlobalData()

m_buildMapCache = FALSE;
m_initialFile.clear();
m_saveAtFrame = 0;
m_saveToFile.clear();
m_pendingFile.clear();

m_simulateReplays.clear();
Expand Down
32 changes: 32 additions & 0 deletions Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3201,6 +3201,38 @@ void GameLogic::update()

PROFILER_PLOT("LogicFrame", static_cast<int64_t>(now));

// TheSuperHackers @feature bobtista 14/08/2026 Write a save at the requested logic frame and quit.
// This sits ahead of everything the frame does, because the engine's own save runs from
// TheGameClient->UPDATE(), which precedes TheGameLogic->UPDATE(). Saving further down would
// capture a mid-frame state that no player save can produce, and reloading it would re-run the
// part of the frame that had already executed.
if (TheGlobalData->m_saveAtFrame > 0 && (Int)m_frame >= TheGlobalData->m_saveAtFrame && getGameMode() != GAME_SHELL)
{
// TheSuperHackers @bugfix bobtista 15/08/2026 Only write a save the game itself would let the
// player write. The Save button is disabled whenever input is disabled. Wait for the first
// frame that allows it instead, which is why the frame test above is >= rather than ==.
if (TheInGameUI != nullptr && TheInGameUI->getInputEnabled() == FALSE)
{
if ((Int)m_frame == TheGlobalData->m_saveAtFrame)
{
DEBUG_LOG(("Command line save deferred at frame %d: input is disabled, waiting for a frame that allows saving", m_frame));
}
}
else
{
AsciiString saveName = TheGlobalData->m_saveToFile;
if (saveName.isEmpty())
{
saveName = "commandline.sav";
}
MAYBE_UNUSED const SaveCode saveResult = TheGameState->saveGame(saveName, UnicodeString(L"Command line save"), SAVE_FILE_TYPE_NORMAL);
(void)saveResult;
DEBUG_LOG(("Command line save to '%s' at frame %d returned %d", saveName.str(), m_frame, (Int)saveResult));
TheWritableGlobalData->m_saveAtFrame = 0;
TheGameEngine->setQuitting(TRUE);
}
}

// update (execute) scripts
{
TheScriptEngine->UPDATE();
Expand Down
2 changes: 2 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ class GlobalData : public SubsystemInterface
Bool m_enforceMaxCameraHeight; ///< Enforce max camera height while scrolling?
Bool m_buildMapCache;
AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line
Int m_saveAtFrame; ///< If greater than zero, write a save when this logic frame is reached
AsciiString m_saveToFile; ///< Filename used by m_saveAtFrame
AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start

std::vector<AsciiString> m_simulateReplays; ///< If not empty, simulate this list of replays and exit.
Expand Down
2 changes: 2 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,8 @@ GlobalData::GlobalData()

m_buildMapCache = FALSE;
m_initialFile.clear();
m_saveAtFrame = 0;
m_saveToFile.clear();
m_pendingFile.clear();

m_simulateReplays.clear();
Expand Down
32 changes: 32 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3740,6 +3740,38 @@ void GameLogic::update()

PROFILER_PLOT("LogicFrame", static_cast<int64_t>(now));

// TheSuperHackers @feature bobtista 14/08/2026 Write a save at the requested logic frame and quit.
// This sits ahead of everything the frame does, because the engine's own save runs from
// TheGameClient->UPDATE(), which precedes TheGameLogic->UPDATE(). Saving further down would
// capture a mid-frame state that no player save can produce, and reloading it would re-run the
// part of the frame that had already executed.
if (TheGlobalData->m_saveAtFrame > 0 && (Int)m_frame >= TheGlobalData->m_saveAtFrame && getGameMode() != GAME_SHELL)
{
// TheSuperHackers @bugfix bobtista 15/08/2026 Only write a save the game itself would let the
// player write. The Save button is disabled whenever input is disabled. Wait for the first
// frame that allows it instead, which is why the frame test above is >= rather than ==.
if (TheInGameUI != nullptr && TheInGameUI->getInputEnabled() == FALSE)
{
if ((Int)m_frame == TheGlobalData->m_saveAtFrame)
{
DEBUG_LOG(("Command line save deferred at frame %d: input is disabled, waiting for a frame that allows saving", m_frame));
}
}
else
{
AsciiString saveName = TheGlobalData->m_saveToFile;
if (saveName.isEmpty())
{
saveName = "commandline.sav";
}
MAYBE_UNUSED const SaveCode saveResult = TheGameState->saveGame(saveName, UnicodeString(L"Command line save"), SAVE_FILE_TYPE_NORMAL);
(void)saveResult;
DEBUG_LOG(("Command line save to '%s' at frame %d returned %d", saveName.str(), m_frame, (Int)saveResult));
TheWritableGlobalData->m_saveAtFrame = 0;
TheGameEngine->setQuitting(TRUE);
}
}

// update (execute) scripts
{
TheScriptEngine->UPDATE();
Expand Down
Loading