diff --git a/Core/GameEngine/Include/Common/Snapshot.h b/Core/GameEngine/Include/Common/Snapshot.h index fa223c6802d..e21a1476388 100644 --- a/Core/GameEngine/Include/Common/Snapshot.h +++ b/Core/GameEngine/Include/Common/Snapshot.h @@ -51,6 +51,10 @@ friend class XferCRC; Snapshot(); ~Snapshot(); + // Snapshots can disable xfer to be excluded from save game data, + // for example dummy implementations in headless mode. + virtual Bool isXferEnabled() const { return TRUE; } + protected: /// run the "light" crc check on this data structure diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index acd504f6838..2ae6db8cc74 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -862,6 +862,8 @@ class ParticleSystemManagerDummy : public ParticleSystemManager virtual Bool isDummy() const override { return true; } + virtual Bool isXferEnabled() const override { return FALSE; } + virtual Int getOnScreenParticleCount() override { return 0; } virtual void doParticles(RenderInfoClass &rinfo) override {} virtual void queueParticleRender() override {} diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..ff36f833073 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -719,6 +719,18 @@ Int parseVTune ( char *args[], int num ) #endif // defined(RTS_DEBUG) +Int parseLoadSave(char *args[], int num) +{ + if (num > 1) + { + TheWritableGlobalData->m_loadSaveGame = args[1]; + TheWritableGlobalData->m_shellMapOn = FALSE; + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_playSizzle = FALSE; + } + return 2; +} + //============================================================================= //============================================================================= @@ -1160,6 +1172,9 @@ static CommandLineParam paramsForEngineInit[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, + // TheSuperHackers @feature bobtista 22/07/2026 Load a save game file from the command line. + { "-loadsave", parseLoadSave }, + // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. { "-forcefullviewport", parseFullViewport }, diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainVisual.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainVisual.h index 8d2a1a492c7..5b2172c11fa 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainVisual.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainVisual.h @@ -147,6 +147,7 @@ class W3DTerrainVisual : public TerrainVisual protected: // snapshot methods + virtual Bool isXferEnabled() const override; virtual void crc( Xfer *xfer ) override; virtual void xfer( Xfer *xfer ) override; virtual void loadPostProcess() override; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp index e50d92a7a38..c40ca0c5041 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp @@ -1134,6 +1134,14 @@ void W3DTerrainVisual::replaceSkyboxTextures(const AsciiString *oldTexName[5], c } } +// ------------------------------------------------------------------------------------------------ +/** Terrain visual state is only partially initialized in headless mode, so it is excluded from save game data */ +// ------------------------------------------------------------------------------------------------ +Bool W3DTerrainVisual::isXferEnabled() const +{ + return TheGlobalData->m_headless == FALSE; +} + // ------------------------------------------------------------------------------------------------ /** CRC */ // ------------------------------------------------------------------------------------------------ diff --git a/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h index 76f2443f558..8ce8a1bfb32 100644 --- a/Generals/Code/GameEngine/Include/Common/GameState.h +++ b/Generals/Code/GameEngine/Include/Common/GameState.h @@ -171,6 +171,7 @@ class GameState : public SubsystemInterface, SnapshotType which = SNAPSHOT_SAVELOAD ); ///< save a game SaveResult missionSave(); ///< do a in between mission save SaveCode loadGame( AvailableGameInfo gameInfo ); ///< load a save file + void loadQueuedSaveGame(); ///< load the save file requested on startup SaveGameInfo *getSaveGameInfo() { return &m_gameInfo; } // snapshot interaction diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index e631654250d..0c8e8820660 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -350,6 +350,7 @@ class GlobalData : public SubsystemInterface Bool m_buildMapCache; 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 std::vector 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 diff --git a/Generals/Code/GameEngine/Include/GameLogic/GhostObject.h b/Generals/Code/GameEngine/Include/GameLogic/GhostObject.h index 7a657fa8a7c..3a198a6e261 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/GhostObject.h +++ b/Generals/Code/GameEngine/Include/GameLogic/GhostObject.h @@ -110,10 +110,10 @@ inline Bool GhostObjectManager::trackAllPlayers() const // TheSuperHackers @feature bobtista 19/01/2026 // GhostObjectManager that does nothing for headless mode. -// Note: Does NOT override crc/xfer/loadPostProcess to maintain save compatibility. class GhostObjectManagerDummy : public GhostObjectManager { public: + virtual Bool isXferEnabled() const override { return FALSE; } virtual void reset() override {} virtual GhostObject *addGhostObject(Object *object, PartitionData *pd) override { return nullptr; } virtual void removeGhostObject(GhostObject *mod) override {} diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 067c8404279..a55cdb45a2b 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -382,6 +382,13 @@ void GameState::addSnapshotBlock( AsciiString blockName, Snapshot *snapshot, Sna } + // Snapshots with xfer disabled are not registered, so their blocks are omitted when saving + // and are skipped like unknown blocks when loading. + if( !snapshot->isXferEnabled() ) + { + return; + } + // add to the list SnapshotBlock blockInfo; blockInfo.snapshot = snapshot; @@ -733,6 +740,52 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) } +// ------------------------------------------------------------------------------------------------ +/** Load the save game requested on startup, after the shell has been initialized */ +// ------------------------------------------------------------------------------------------------ +void GameState::loadQueuedSaveGame() +{ + AvailableGameInfo gameInfo; + gameInfo.filename = TheGlobalData->m_loadSaveGame; + gameInfo.next = nullptr; + gameInfo.prev = nullptr; + + TheWritableGlobalData->m_loadSaveGame.clear(); + + // getSaveGameInfoFromFile throws when the file is missing, so check before reading it + if( doesSaveGameExist( gameInfo.filename ) == FALSE ) + { + DEBUG_LOG(("Save game '%s' was not found", gameInfo.filename.str())); + TheGameEngine->setQuitting( TRUE ); + return; + } + + // getSaveGameInfoFromFile throws on a malformed file instead of returning a SaveCode + try + { + AsciiString filepath = getFilePathInSaveDirectory( gameInfo.filename ); + getSaveGameInfoFromFile( filepath, &gameInfo.saveGameInfo ); + } + catch( ... ) + { + DEBUG_LOG(("Save game '%s' could not be read", gameInfo.filename.str())); + TheGameEngine->setQuitting( TRUE ); + return; + } + + // this hides the shell, keeping the menu screens on the stack for when the game ends + TheGameLogic->prepareNewGame( GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0 ); + + if( loadGame( gameInfo ) != SC_OK ) + { + DEBUG_LOG(("Failed to load save game '%s'", gameInfo.filename.str())); + if( TheGameLogic->isInGame() ) + TheGameLogic->clearGameData( FALSE ); + TheGameEngine->reset(); + TheGameEngine->setQuitting( TRUE ); + } +} + //------------------------------------------------------------------------------------------------- AsciiString GameState::getSaveDirectory() const { diff --git a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp index 405de91c5e9..d556d8b4855 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -515,6 +515,11 @@ void GameClient::update() TheShell->showShellMap(TRUE); TheShell->showShell(); + + if (TheGlobalData->m_loadSaveGame.isNotEmpty()) + { + TheGameState->loadQueuedSaveGame(); + } } } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index bc2f21eb854..565aedbdfef 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -171,6 +171,7 @@ class GameState : public SubsystemInterface, SnapshotType which = SNAPSHOT_SAVELOAD ); ///< save a game SaveResult missionSave(); ///< do a in between mission save SaveCode loadGame( AvailableGameInfo gameInfo ); ///< load a save file + void loadQueuedSaveGame(); ///< load the save file requested on startup SaveGameInfo *getSaveGameInfo() { return &m_gameInfo; } // snapshot interaction diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 7f484111672..89a5fa08f9d 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -351,6 +351,7 @@ class GlobalData : public SubsystemInterface Bool m_buildMapCache; 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 std::vector 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 diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GhostObject.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GhostObject.h index 67c375946de..9ec1c4cacae 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GhostObject.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GhostObject.h @@ -110,10 +110,10 @@ inline Bool GhostObjectManager::trackAllPlayers() const // TheSuperHackers @feature bobtista 19/01/2026 // GhostObjectManager that does nothing for headless mode. -// Note: Does NOT override crc/xfer/loadPostProcess to maintain save compatibility. class GhostObjectManagerDummy : public GhostObjectManager { public: + virtual Bool isXferEnabled() const override { return FALSE; } virtual void reset() override {} virtual GhostObject *addGhostObject(Object *object, PartitionData *pd) override { return nullptr; } virtual void removeGhostObject(GhostObject *mod) override {} diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index be41b368041..47f399fa731 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -382,6 +382,13 @@ void GameState::addSnapshotBlock( AsciiString blockName, Snapshot *snapshot, Sna } + // Snapshots with xfer disabled are not registered, so their blocks are omitted when saving + // and are skipped like unknown blocks when loading. + if( !snapshot->isXferEnabled() ) + { + return; + } + // add to the list SnapshotBlock blockInfo; blockInfo.snapshot = snapshot; @@ -733,6 +740,52 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) } +// ------------------------------------------------------------------------------------------------ +/** Load the save game requested on startup, after the shell has been initialized */ +// ------------------------------------------------------------------------------------------------ +void GameState::loadQueuedSaveGame() +{ + AvailableGameInfo gameInfo; + gameInfo.filename = TheGlobalData->m_loadSaveGame; + gameInfo.next = nullptr; + gameInfo.prev = nullptr; + + TheWritableGlobalData->m_loadSaveGame.clear(); + + // getSaveGameInfoFromFile throws when the file is missing, so check before reading it + if( doesSaveGameExist( gameInfo.filename ) == FALSE ) + { + DEBUG_LOG(("Save game '%s' was not found", gameInfo.filename.str())); + TheGameEngine->setQuitting( TRUE ); + return; + } + + // getSaveGameInfoFromFile throws on a malformed file instead of returning a SaveCode + try + { + AsciiString filepath = getFilePathInSaveDirectory( gameInfo.filename ); + getSaveGameInfoFromFile( filepath, &gameInfo.saveGameInfo ); + } + catch( ... ) + { + DEBUG_LOG(("Save game '%s' could not be read", gameInfo.filename.str())); + TheGameEngine->setQuitting( TRUE ); + return; + } + + // this hides the shell, keeping the menu screens on the stack for when the game ends + TheGameLogic->prepareNewGame( GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0 ); + + if( loadGame( gameInfo ) != SC_OK ) + { + DEBUG_LOG(("Failed to load save game '%s'", gameInfo.filename.str())); + if( TheGameLogic->isInGame() ) + TheGameLogic->clearGameData( FALSE ); + TheGameEngine->reset(); + TheGameEngine->setQuitting( TRUE ); + } +} + //------------------------------------------------------------------------------------------------- AsciiString GameState::getSaveDirectory() const { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index e396c3861b2..af5f407209b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -536,6 +536,11 @@ void GameClient::update() TheShell->showShellMap(TRUE); TheShell->showShell(); + + if (TheGlobalData->m_loadSaveGame.isNotEmpty()) + { + TheGameState->loadQueuedSaveGame(); + } } }