From 1e69887e25e5da25fa285f33f208a7fca24dda22 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 22 Jul 2026 13:15:21 +0400 Subject: [PATCH 01/10] refactor(saveload): Decouple result handling from game state --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/GameState.h | 5 +- .../Include/GameClient/SaveLoadFeedback.h | 24 +++++++ .../Common/System/SaveGame/GameState.cpp | 37 +++------- .../GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 14 +++- .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 5 +- .../GUI/GUICallbacks/SaveLoadFeedback.cpp | 71 +++++++++++++++++++ 7 files changed, 124 insertions(+), 34 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index e5b82e2db38..563eed3b3db 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -198,6 +198,7 @@ set(GAMEENGINE_SRC # Include/GameClient/LookAtXlat.h # Include/GameClient/MapUtil.h # Include/GameClient/MessageBox.h + Include/GameClient/SaveLoadFeedback.h # Include/GameClient/MetaEvent.h # Include/GameClient/Module/AnimatedParticleSysBoneClientUpdate.h # Include/GameClient/Module/BeaconClientUpdate.h @@ -779,6 +780,7 @@ set(GAMEENGINE_SRC Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp # Source/GameClient/GUI/GUICallbacks/MessageBox.cpp # Source/GameClient/GUI/GUICallbacks/ReplayControls.cpp + Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp # Source/GameClient/GUI/HeaderTemplate.cpp # Source/GameClient/GUI/IMEManager.cpp # Source/GameClient/GUI/LoadScreen.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index 16bc991b69a..3703a95ba56 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -159,8 +159,9 @@ class GameState : public SubsystemInterface, SaveCode saveGame( AsciiString filename, UnicodeString desc, SaveFileType saveType, - SnapshotType which = SNAPSHOT_SAVELOAD ); ///< save a game - SaveCode missionSave(); ///< do a in between mission save + SnapshotType which = SNAPSHOT_SAVELOAD, + AsciiString *resolvedFilename = nullptr ); ///< save a game + SaveCode missionSave( AsciiString *resolvedFilename = nullptr ); ///< do a in between mission save SaveCode loadGame( AvailableGameInfo gameInfo ); ///< load a save file SaveGameInfo *getSaveGameInfo() { return &m_gameInfo; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h b/GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h new file mode 100644 index 00000000000..58939bff1b6 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h @@ -0,0 +1,24 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "Common/GameState.h" + +void presentSaveResult( SaveCode result, const AsciiString &filename ); +void presentLoadResult( SaveCode result, const AsciiString &filename ); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 04cc701b5e1..25e23632d2b 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -48,7 +48,6 @@ #include "GameClient/GameClient.h" #include "GameClient/GameText.h" #include "GameClient/MapUtil.h" -#include "GameClient/MessageBox.h" #include "GameClient/InGameUI.h" #include "GameClient/ParticleSys.h" #include "GameClient/TerrainVisual.h" @@ -533,7 +532,8 @@ AsciiString GameState::findNextSaveFilename( UnicodeString desc ) * NOTE: filename is a *filename only* */ // ------------------------------------------------------------------------------------------------ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, - SaveFileType saveType, SnapshotType which ) + SaveFileType saveType, SnapshotType which, + AsciiString *resolvedFilename ) { // if there is no filename, this is a new file being created, find an appropriate filename @@ -546,6 +546,10 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, return SC_NO_FILE_AVAILABLE; } + if( resolvedFilename != nullptr ) + { + *resolvedFilename = filename; + } // make absolutely sure the save directory exists CreateDirectory( getSaveDirectory().str(), nullptr ); @@ -561,10 +565,8 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, try { xferSave.open( filepath ); } catch(...) { - // print error message to the user - TheInGameUI->message( "GUI:Error" ); DEBUG_LOG(( "Error opening file '%s'", filepath.str() )); - return SC_ERROR; + return SC_UNABLE_TO_OPEN_FILE; } // save our save file type @@ -592,14 +594,6 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, catch( ... ) { - UnicodeString ufilepath; - ufilepath.translate(filepath); - - UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), ufilepath.str() ); - - MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, nullptr); - // close the file and get out of here xferSave.close(); return SC_ERROR; @@ -609,10 +603,6 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // close the file xferSave.close(); - // print message to the user for game successfully saved - UnicodeString msg = TheGameText->fetch( "GUI:GameSaveComplete" ); - TheInGameUI->message( msg ); - return SC_OK; } @@ -620,7 +610,7 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // ------------------------------------------------------------------------------------------------ /** A mission save */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::missionSave() +SaveCode GameState::missionSave( AsciiString *resolvedFilename ) { // get campaign @@ -635,7 +625,7 @@ SaveCode GameState::missionSave() desc.format( format, TheGameText->fetch( campaign->m_campaignNameLabel ).str(), missionNumber ); // do an automatic mission save - return saveGame( "", desc, SAVE_FILE_TYPE_MISSION ); + return saveGame( "", desc, SAVE_FILE_TYPE_MISSION, SNAPSHOT_SAVELOAD, resolvedFilename ); } @@ -717,15 +707,6 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) TheGameLogic->clearGameData( FALSE ); TheGameEngine->reset(); - // print error message to the user - UnicodeString ufilepath; - ufilepath.translate(filepath); - - UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), ufilepath.str() ); - - MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, nullptr); - return SC_INVALID_DATA; // you can't use a naked "throw" outside of a catch statement! } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp index 45cb58fd4b9..5185579bd32 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -56,6 +56,7 @@ #include "GameClient/GameText.h" #include "GameClient/GameWindowManager.h" #include "GameClient/GUICallbacks.h" +#include "GameClient/SaveLoadFeedback.h" #include "GameClient/Shell.h" #include "GameLogic/GameLogic.h" #include "GameClient/GameWindowTransitions.h" @@ -414,7 +415,10 @@ static void doLoadGame() // loose these allocated user data pointers attached as listbox item data when the // engine resets // - if (TheGameState->loadGame( *selectedGameInfo ) != SC_OK) + AsciiString filename = selectedGameInfo->filename; + SaveCode result = TheGameState->loadGame( *selectedGameInfo ); + presentLoadResult( result, filename ); + if (result != SC_OK) { if (TheGameLogic->isInGame()) TheGameLogic->clearGameData( FALSE ); @@ -787,7 +791,9 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, // save the game AsciiString filename; filename = selectedGameInfo->filename; - TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, fileType ); + SaveCode result = TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, + fileType, SNAPSHOT_SAVELOAD, &filename ); + presentSaveResult( result, filename ); /* // set the description text entry field to default value @@ -851,7 +857,9 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, AsciiString filename; if( selectedGameInfo ) filename = selectedGameInfo->filename; - TheGameState->saveGame( filename, desc, fileType ); + SaveCode result = TheGameState->saveGame( filename, desc, fileType, + SNAPSHOT_SAVELOAD, &filename ); + presentSaveResult( result, filename ); } else if( controlID == buttonSaveDescCancel ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index 3f37cfab57f..abf8babe2d6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -77,6 +77,7 @@ #include "GameLogic/VictoryConditions.h" #include "GameClient/Display.h" #include "GameClient/GUICallbacks.h" +#include "GameClient/SaveLoadFeedback.h" #include "GameClient/WindowLayout.h" #include "GameClient/GameWindowManager.h" #include "GameClient/Gadget.h" @@ -928,7 +929,9 @@ void finishSinglePlayerInit() GadgetButtonSetText(buttonContinue, TheGameText->fetch("GUI:SaveAndContinue")); // auto save game - TheGameState->missionSave(); + AsciiString filename; + SaveCode result = TheGameState->missionSave( &filename ); + presentSaveResult( result, filename ); if(staticTextGameSaved) staticTextGameSaved->winHide(FALSE); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp new file mode 100644 index 00000000000..95147fad526 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp @@ -0,0 +1,71 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" +#include "GameClient/GameText.h" +#include "GameClient/InGameUI.h" +#include "GameClient/MessageBox.h" +#include "GameClient/SaveLoadFeedback.h" + +static UnicodeString getUnicodeSavePath( const AsciiString &filename ) +{ + UnicodeString path; + path.translate( TheGameState->getFilePathInSaveDirectory(filename) ); + return path; +} + +void presentSaveResult( SaveCode result, const AsciiString &filename ) +{ + switch( result ) + { + case SC_OK: + { + TheInGameUI->message( TheGameText->fetch("GUI:GameSaveComplete") ); + break; + } + case SC_UNABLE_TO_OPEN_FILE: + { + TheInGameUI->message( "GUI:Error" ); + break; + } + case SC_ERROR: + { + UnicodeString msg; + msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), getUnicodeSavePath(filename).str() ); + MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); + break; + } + default: + { + // SC_NO_FILE_AVAILABLE (and any other early-out) returned no UI in retail + break; + } + } +} + +void presentLoadResult( SaveCode result, const AsciiString &filename ) +{ + // Retail loadGame only surfaced a dialog on the exception path; SC_FILE_NOT_FOUND + // and SC_OK presented nothing. + if( result == SC_INVALID_DATA ) + { + UnicodeString msg; + msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), getUnicodeSavePath(filename).str() ); + MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); + } +} From bc619027f120254c6256ff80c09482361a068525 Mon Sep 17 00:00:00 2001 From: bobtista Date: Wed, 22 Jul 2026 13:45:06 -0400 Subject: [PATCH 02/10] refactor(saveload): Decouple result handling from game state (Generals) --- Generals/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/GameState.h | 5 +- .../Include/GameClient/SaveLoadFeedback.h | 24 +++++++ .../Common/System/SaveGame/GameState.cpp | 37 +++------- .../GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 14 +++- .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 5 +- .../GUI/GUICallbacks/SaveLoadFeedback.cpp | 71 +++++++++++++++++++ 7 files changed, 124 insertions(+), 34 deletions(-) create mode 100644 Generals/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h create mode 100644 Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index 138424cf0d3..fbe8cf7463c 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -192,6 +192,7 @@ set(GAMEENGINE_SRC # Include/GameClient/LookAtXlat.h # Include/GameClient/MapUtil.h # Include/GameClient/MessageBox.h + Include/GameClient/SaveLoadFeedback.h # Include/GameClient/MetaEvent.h # Include/GameClient/Module/AnimatedParticleSysBoneClientUpdate.h # Include/GameClient/Module/BeaconClientUpdate.h @@ -737,6 +738,7 @@ set(GAMEENGINE_SRC Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp # Source/GameClient/GUI/GUICallbacks/MessageBox.cpp # Source/GameClient/GUI/GUICallbacks/ReplayControls.cpp + Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp # Source/GameClient/GUI/HeaderTemplate.cpp # Source/GameClient/GUI/IMEManager.cpp # Source/GameClient/GUI/LoadScreen.cpp diff --git a/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h index 20e8910174f..cdf77721b76 100644 --- a/Generals/Code/GameEngine/Include/Common/GameState.h +++ b/Generals/Code/GameEngine/Include/Common/GameState.h @@ -159,8 +159,9 @@ class GameState : public SubsystemInterface, SaveCode saveGame( AsciiString filename, UnicodeString desc, SaveFileType saveType, - SnapshotType which = SNAPSHOT_SAVELOAD ); ///< save a game - SaveCode missionSave(); ///< do a in between mission save + SnapshotType which = SNAPSHOT_SAVELOAD, + AsciiString *resolvedFilename = nullptr ); ///< save a game + SaveCode missionSave( AsciiString *resolvedFilename = nullptr ); ///< do a in between mission save SaveCode loadGame( AvailableGameInfo gameInfo ); ///< load a save file SaveGameInfo *getSaveGameInfo() { return &m_gameInfo; } diff --git a/Generals/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h b/Generals/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h new file mode 100644 index 00000000000..58939bff1b6 --- /dev/null +++ b/Generals/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h @@ -0,0 +1,24 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "Common/GameState.h" + +void presentSaveResult( SaveCode result, const AsciiString &filename ); +void presentLoadResult( SaveCode result, const AsciiString &filename ); diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 7390ff5ed85..a57500cb1d0 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -48,7 +48,6 @@ #include "GameClient/GameClient.h" #include "GameClient/GameText.h" #include "GameClient/MapUtil.h" -#include "GameClient/MessageBox.h" #include "GameClient/InGameUI.h" #include "GameClient/ParticleSys.h" #include "GameClient/TerrainVisual.h" @@ -533,7 +532,8 @@ AsciiString GameState::findNextSaveFilename( UnicodeString desc ) * NOTE: filename is a *filename only* */ // ------------------------------------------------------------------------------------------------ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, - SaveFileType saveType, SnapshotType which ) + SaveFileType saveType, SnapshotType which, + AsciiString *resolvedFilename ) { // if there is no filename, this is a new file being created, find an appropriate filename @@ -546,6 +546,10 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, return SC_NO_FILE_AVAILABLE; } + if( resolvedFilename != nullptr ) + { + *resolvedFilename = filename; + } // make absolutely sure the save directory exists CreateDirectory( getSaveDirectory().str(), nullptr ); @@ -561,10 +565,8 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, try { xferSave.open( filepath ); } catch(...) { - // print error message to the user - TheInGameUI->message( "GUI:Error" ); DEBUG_LOG(( "Error opening file '%s'", filepath.str() )); - return SC_ERROR; + return SC_UNABLE_TO_OPEN_FILE; } // save our save file type @@ -592,14 +594,6 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, catch( ... ) { - UnicodeString ufilepath; - ufilepath.translate(filepath); - - UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), ufilepath.str() ); - - MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, nullptr); - // close the file and get out of here xferSave.close(); return SC_ERROR; @@ -609,10 +603,6 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // close the file xferSave.close(); - // print message to the user for game successfully saved - UnicodeString msg = TheGameText->fetch( "GUI:GameSaveComplete" ); - TheInGameUI->message( msg ); - return SC_OK; } @@ -620,7 +610,7 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // ------------------------------------------------------------------------------------------------ /** A mission save */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::missionSave() +SaveCode GameState::missionSave( AsciiString *resolvedFilename ) { // get campaign @@ -635,7 +625,7 @@ SaveCode GameState::missionSave() desc.format( format, TheGameText->fetch( campaign->m_campaignNameLabel ).str(), missionNumber ); // do an automatic mission save - return saveGame( "", desc, SAVE_FILE_TYPE_MISSION ); + return saveGame( "", desc, SAVE_FILE_TYPE_MISSION, SNAPSHOT_SAVELOAD, resolvedFilename ); } @@ -717,15 +707,6 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) TheGameLogic->clearGameData( FALSE ); TheGameEngine->reset(); - // print error message to the user - UnicodeString ufilepath; - ufilepath.translate(filepath); - - UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), ufilepath.str() ); - - MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, nullptr); - return SC_INVALID_DATA; // you can't use a naked "throw" outside of a catch statement! } diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp index 6dbbacb9c0c..6e1a882d239 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -56,6 +56,7 @@ #include "GameClient/GameText.h" #include "GameClient/GameWindowManager.h" #include "GameClient/GUICallbacks.h" +#include "GameClient/SaveLoadFeedback.h" #include "GameClient/Shell.h" #include "GameLogic/GameLogic.h" #include "GameClient/GameWindowTransitions.h" @@ -404,7 +405,10 @@ static void doLoadGame() // loose these allocated user data pointers attached as listbox item data when the // engine resets // - if (TheGameState->loadGame( *selectedGameInfo ) != SC_OK) + AsciiString filename = selectedGameInfo->filename; + SaveCode result = TheGameState->loadGame( *selectedGameInfo ); + presentLoadResult( result, filename ); + if (result != SC_OK) { if (TheGameLogic->isInGame()) TheGameLogic->clearGameData( FALSE ); @@ -771,7 +775,9 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, // save the game AsciiString filename; filename = selectedGameInfo->filename; - TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, fileType ); + SaveCode result = TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, + fileType, SNAPSHOT_SAVELOAD, &filename ); + presentSaveResult( result, filename ); /* // set the description text entry field to default value @@ -835,7 +841,9 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, AsciiString filename; if( selectedGameInfo ) filename = selectedGameInfo->filename; - TheGameState->saveGame( filename, desc, fileType ); + SaveCode result = TheGameState->saveGame( filename, desc, fileType, + SNAPSHOT_SAVELOAD, &filename ); + presentSaveResult( result, filename ); } else if( controlID == buttonSaveDescCancel ) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index 8eda61ca615..bd372e3d6e1 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -74,6 +74,7 @@ #include "GameLogic/VictoryConditions.h" #include "GameClient/Display.h" #include "GameClient/GUICallbacks.h" +#include "GameClient/SaveLoadFeedback.h" #include "GameClient/WindowLayout.h" #include "GameClient/GameWindowManager.h" #include "GameClient/Gadget.h" @@ -767,7 +768,9 @@ void finishSinglePlayerInit() GadgetButtonSetText(buttonContinue, TheGameText->fetch("GUI:SaveAndContinue")); // auto save game - TheGameState->missionSave(); + AsciiString filename; + SaveCode result = TheGameState->missionSave( &filename ); + presentSaveResult( result, filename ); if(staticTextGameSaved) staticTextGameSaved->winHide(FALSE); } diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp new file mode 100644 index 00000000000..95147fad526 --- /dev/null +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp @@ -0,0 +1,71 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" +#include "GameClient/GameText.h" +#include "GameClient/InGameUI.h" +#include "GameClient/MessageBox.h" +#include "GameClient/SaveLoadFeedback.h" + +static UnicodeString getUnicodeSavePath( const AsciiString &filename ) +{ + UnicodeString path; + path.translate( TheGameState->getFilePathInSaveDirectory(filename) ); + return path; +} + +void presentSaveResult( SaveCode result, const AsciiString &filename ) +{ + switch( result ) + { + case SC_OK: + { + TheInGameUI->message( TheGameText->fetch("GUI:GameSaveComplete") ); + break; + } + case SC_UNABLE_TO_OPEN_FILE: + { + TheInGameUI->message( "GUI:Error" ); + break; + } + case SC_ERROR: + { + UnicodeString msg; + msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), getUnicodeSavePath(filename).str() ); + MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); + break; + } + default: + { + // SC_NO_FILE_AVAILABLE (and any other early-out) returned no UI in retail + break; + } + } +} + +void presentLoadResult( SaveCode result, const AsciiString &filename ) +{ + // Retail loadGame only surfaced a dialog on the exception path; SC_FILE_NOT_FOUND + // and SC_OK presented nothing. + if( result == SC_INVALID_DATA ) + { + UnicodeString msg; + msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), getUnicodeSavePath(filename).str() ); + MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); + } +} From 4bab379a5248ffda7276e20edc278812587b9563 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 5 Aug 2026 12:20:21 +0100 Subject: [PATCH 03/10] unify(saveload): Move SaveLoadFeedback files to Core --- Core/GameEngine/CMakeLists.txt | 2 + .../Include/GameClient/SaveLoadFeedback.h | 0 .../GUI/GUICallbacks/SaveLoadFeedback.cpp | 0 Generals/Code/GameEngine/CMakeLists.txt | 2 - GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 - .../Include/GameClient/SaveLoadFeedback.h | 24 ------- .../GUI/GUICallbacks/SaveLoadFeedback.cpp | 71 ------------------- 7 files changed, 2 insertions(+), 99 deletions(-) rename {Generals/Code => Core}/GameEngine/Include/GameClient/SaveLoadFeedback.h (100%) rename {Generals/Code => Core}/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp (100%) delete mode 100644 GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h delete mode 100644 GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 0f36ff63383..0e030311e34 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -217,6 +217,7 @@ set(GAMEENGINE_SRC Include/GameClient/ProcessAnimateWindow.h Include/GameClient/RadiusDecal.h Include/GameClient/RayEffect.h + Include/GameClient/SaveLoadFeedback.h Include/GameClient/SelectionInfo.h Include/GameClient/SelectionXlat.h # Include/GameClient/Shadow.h @@ -795,6 +796,7 @@ set(GAMEENGINE_SRC # Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp Source/GameClient/GUI/GUICallbacks/MessageBox.cpp Source/GameClient/GUI/GUICallbacks/ReplayControls.cpp + Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp Source/GameClient/GUI/HeaderTemplate.cpp Source/GameClient/GUI/IMEManager.cpp Source/GameClient/GUI/LoadScreen.cpp diff --git a/Generals/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h b/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h similarity index 100% rename from Generals/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h rename to Core/GameEngine/Include/GameClient/SaveLoadFeedback.h diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp similarity index 100% rename from Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp rename to Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index fbe8cf7463c..138424cf0d3 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -192,7 +192,6 @@ set(GAMEENGINE_SRC # Include/GameClient/LookAtXlat.h # Include/GameClient/MapUtil.h # Include/GameClient/MessageBox.h - Include/GameClient/SaveLoadFeedback.h # Include/GameClient/MetaEvent.h # Include/GameClient/Module/AnimatedParticleSysBoneClientUpdate.h # Include/GameClient/Module/BeaconClientUpdate.h @@ -738,7 +737,6 @@ set(GAMEENGINE_SRC Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp # Source/GameClient/GUI/GUICallbacks/MessageBox.cpp # Source/GameClient/GUI/GUICallbacks/ReplayControls.cpp - Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp # Source/GameClient/GUI/HeaderTemplate.cpp # Source/GameClient/GUI/IMEManager.cpp # Source/GameClient/GUI/LoadScreen.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 563eed3b3db..e5b82e2db38 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -198,7 +198,6 @@ set(GAMEENGINE_SRC # Include/GameClient/LookAtXlat.h # Include/GameClient/MapUtil.h # Include/GameClient/MessageBox.h - Include/GameClient/SaveLoadFeedback.h # Include/GameClient/MetaEvent.h # Include/GameClient/Module/AnimatedParticleSysBoneClientUpdate.h # Include/GameClient/Module/BeaconClientUpdate.h @@ -780,7 +779,6 @@ set(GAMEENGINE_SRC Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp # Source/GameClient/GUI/GUICallbacks/MessageBox.cpp # Source/GameClient/GUI/GUICallbacks/ReplayControls.cpp - Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp # Source/GameClient/GUI/HeaderTemplate.cpp # Source/GameClient/GUI/IMEManager.cpp # Source/GameClient/GUI/LoadScreen.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h b/GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h deleted file mode 100644 index 58939bff1b6..00000000000 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/SaveLoadFeedback.h +++ /dev/null @@ -1,24 +0,0 @@ -/* -** Command & Conquer Generals Zero Hour(tm) -** Copyright 2026 TheSuperHackers -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -#pragma once - -#include "Common/GameState.h" - -void presentSaveResult( SaveCode result, const AsciiString &filename ); -void presentLoadResult( SaveCode result, const AsciiString &filename ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp deleted file mode 100644 index 95147fad526..00000000000 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -** Command & Conquer Generals Zero Hour(tm) -** Copyright 2026 TheSuperHackers -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -#include "PreRTS.h" -#include "GameClient/GameText.h" -#include "GameClient/InGameUI.h" -#include "GameClient/MessageBox.h" -#include "GameClient/SaveLoadFeedback.h" - -static UnicodeString getUnicodeSavePath( const AsciiString &filename ) -{ - UnicodeString path; - path.translate( TheGameState->getFilePathInSaveDirectory(filename) ); - return path; -} - -void presentSaveResult( SaveCode result, const AsciiString &filename ) -{ - switch( result ) - { - case SC_OK: - { - TheInGameUI->message( TheGameText->fetch("GUI:GameSaveComplete") ); - break; - } - case SC_UNABLE_TO_OPEN_FILE: - { - TheInGameUI->message( "GUI:Error" ); - break; - } - case SC_ERROR: - { - UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), getUnicodeSavePath(filename).str() ); - MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); - break; - } - default: - { - // SC_NO_FILE_AVAILABLE (and any other early-out) returned no UI in retail - break; - } - } -} - -void presentLoadResult( SaveCode result, const AsciiString &filename ) -{ - // Retail loadGame only surfaced a dialog on the exception path; SC_FILE_NOT_FOUND - // and SC_OK presented nothing. - if( result == SC_INVALID_DATA ) - { - UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), getUnicodeSavePath(filename).str() ); - MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); - } -} From ce357f993501b815e75d4ddd6918502b499192f7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 5 Aug 2026 12:20:21 +0100 Subject: [PATCH 04/10] refactor(saveload): Return a SaveResult from saveGame and missionSave --- .../Include/GameClient/SaveLoadFeedback.h | 2 +- .../GUI/GUICallbacks/SaveLoadFeedback.cpp | 6 +++--- .../GameEngine/Include/Common/GameState.h | 17 +++++++++++---- .../Common/System/SaveGame/GameState.cpp | 21 +++++++------------ .../GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 9 +++----- .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 4 +--- .../GameEngine/Include/Common/GameState.h | 17 +++++++++++---- .../Common/System/SaveGame/GameState.cpp | 21 +++++++------------ .../GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 9 +++----- .../GUI/GUICallbacks/Menus/ScoreScreen.cpp | 4 +--- 10 files changed, 54 insertions(+), 56 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h b/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h index 58939bff1b6..df90a7a0cce 100644 --- a/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h +++ b/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h @@ -20,5 +20,5 @@ #include "Common/GameState.h" -void presentSaveResult( SaveCode result, const AsciiString &filename ); +void presentSaveResult( const SaveResult &result ); void presentLoadResult( SaveCode result, const AsciiString &filename ); diff --git a/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp index 95147fad526..f9c8e535fce 100644 --- a/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp @@ -29,9 +29,9 @@ static UnicodeString getUnicodeSavePath( const AsciiString &filename ) return path; } -void presentSaveResult( SaveCode result, const AsciiString &filename ) +void presentSaveResult( const SaveResult &result ) { - switch( result ) + switch( result.saveCode ) { case SC_OK: { @@ -46,7 +46,7 @@ void presentSaveResult( SaveCode result, const AsciiString &filename ) case SC_ERROR: { UnicodeString msg; - msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), getUnicodeSavePath(filename).str() ); + msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), getUnicodeSavePath(result.filename).str() ); MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr ); break; } diff --git a/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h index cdf77721b76..b6cd22e7767 100644 --- a/Generals/Code/GameEngine/Include/Common/GameState.h +++ b/Generals/Code/GameEngine/Include/Common/GameState.h @@ -132,6 +132,16 @@ enum SaveCode CPP_11(: Int) SC_ERROR, }; +// The result of a save, pairing the outcome with the file it resolved to so the two cannot drift. +struct SaveResult +{ + explicit SaveResult( SaveCode code ) : saveCode(code) { } + SaveResult( SaveCode code, const AsciiString &file ) : saveCode(code), filename(file) { } + + SaveCode saveCode; + AsciiString filename; ///< the file that was written, empty when no filename could be found +}; + enum SnapshotType CPP_11(: Int) { SNAPSHOT_SAVELOAD, SNAPSHOT_DEEPCRC_LOGICONLY, @@ -156,12 +166,11 @@ class GameState : public SubsystemInterface, virtual void update() override { } // save game methods - SaveCode saveGame( AsciiString filename, + SaveResult saveGame( AsciiString filename, UnicodeString desc, SaveFileType saveType, - SnapshotType which = SNAPSHOT_SAVELOAD, - AsciiString *resolvedFilename = nullptr ); ///< save a game - SaveCode missionSave( AsciiString *resolvedFilename = nullptr ); ///< do a in between mission save + SnapshotType which = SNAPSHOT_SAVELOAD ); ///< save a game + SaveResult missionSave(); ///< do a in between mission save SaveCode loadGame( AvailableGameInfo gameInfo ); ///< load a save file SaveGameInfo *getSaveGameInfo() { return &m_gameInfo; } diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index a57500cb1d0..067c8404279 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -531,9 +531,8 @@ AsciiString GameState::findNextSaveFilename( UnicodeString desc ) /** Save the current state of the engine in a save file * NOTE: filename is a *filename only* */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, - SaveFileType saveType, SnapshotType which, - AsciiString *resolvedFilename ) +SaveResult GameState::saveGame( AsciiString filename, UnicodeString desc, + SaveFileType saveType, SnapshotType which ) { // if there is no filename, this is a new file being created, find an appropriate filename @@ -543,13 +542,9 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, { DEBUG_CRASH(( "GameState::saveGame - Unable to find valid filename for save game" )); - return SC_NO_FILE_AVAILABLE; + return SaveResult( SC_NO_FILE_AVAILABLE ); } - if( resolvedFilename != nullptr ) - { - *resolvedFilename = filename; - } // make absolutely sure the save directory exists CreateDirectory( getSaveDirectory().str(), nullptr ); @@ -566,7 +561,7 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, xferSave.open( filepath ); } catch(...) { DEBUG_LOG(( "Error opening file '%s'", filepath.str() )); - return SC_UNABLE_TO_OPEN_FILE; + return SaveResult( SC_UNABLE_TO_OPEN_FILE, filename ); } // save our save file type @@ -596,21 +591,21 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // close the file and get out of here xferSave.close(); - return SC_ERROR; + return SaveResult( SC_ERROR, filename ); } // close the file xferSave.close(); - return SC_OK; + return SaveResult( SC_OK, filename ); } // ------------------------------------------------------------------------------------------------ /** A mission save */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::missionSave( AsciiString *resolvedFilename ) +SaveResult GameState::missionSave() { // get campaign @@ -625,7 +620,7 @@ SaveCode GameState::missionSave( AsciiString *resolvedFilename ) desc.format( format, TheGameText->fetch( campaign->m_campaignNameLabel ).str(), missionNumber ); // do an automatic mission save - return saveGame( "", desc, SAVE_FILE_TYPE_MISSION, SNAPSHOT_SAVELOAD, resolvedFilename ); + return saveGame( "", desc, SAVE_FILE_TYPE_MISSION ); } diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp index 6e1a882d239..8c7857e2c61 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -775,9 +775,8 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, // save the game AsciiString filename; filename = selectedGameInfo->filename; - SaveCode result = TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, - fileType, SNAPSHOT_SAVELOAD, &filename ); - presentSaveResult( result, filename ); + presentSaveResult( TheGameState->saveGame( filename, + selectedGameInfo->saveGameInfo.description, fileType ) ); /* // set the description text entry field to default value @@ -841,9 +840,7 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, AsciiString filename; if( selectedGameInfo ) filename = selectedGameInfo->filename; - SaveCode result = TheGameState->saveGame( filename, desc, fileType, - SNAPSHOT_SAVELOAD, &filename ); - presentSaveResult( result, filename ); + presentSaveResult( TheGameState->saveGame( filename, desc, fileType ) ); } else if( controlID == buttonSaveDescCancel ) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index bd372e3d6e1..0202b74a41e 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -768,9 +768,7 @@ void finishSinglePlayerInit() GadgetButtonSetText(buttonContinue, TheGameText->fetch("GUI:SaveAndContinue")); // auto save game - AsciiString filename; - SaveCode result = TheGameState->missionSave( &filename ); - presentSaveResult( result, filename ); + presentSaveResult( TheGameState->missionSave() ); if(staticTextGameSaved) staticTextGameSaved->winHide(FALSE); } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index 3703a95ba56..ee1f4e1e506 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -132,6 +132,16 @@ enum SaveCode CPP_11(: Int) SC_ERROR, }; +// The result of a save, pairing the outcome with the file it resolved to so the two cannot drift. +struct SaveResult +{ + explicit SaveResult( SaveCode code ) : saveCode(code) { } + SaveResult( SaveCode code, const AsciiString &file ) : saveCode(code), filename(file) { } + + SaveCode saveCode; + AsciiString filename; ///< the file that was written, empty when no filename could be found +}; + enum SnapshotType CPP_11(: Int) { SNAPSHOT_SAVELOAD, SNAPSHOT_DEEPCRC_LOGICONLY, @@ -156,12 +166,11 @@ class GameState : public SubsystemInterface, virtual void update() override { } // save game methods - SaveCode saveGame( AsciiString filename, + SaveResult saveGame( AsciiString filename, UnicodeString desc, SaveFileType saveType, - SnapshotType which = SNAPSHOT_SAVELOAD, - AsciiString *resolvedFilename = nullptr ); ///< save a game - SaveCode missionSave( AsciiString *resolvedFilename = nullptr ); ///< do a in between mission save + SnapshotType which = SNAPSHOT_SAVELOAD ); ///< save a game + SaveResult missionSave(); ///< do a in between mission save SaveCode loadGame( AvailableGameInfo gameInfo ); ///< load a save file SaveGameInfo *getSaveGameInfo() { return &m_gameInfo; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 25e23632d2b..be41b368041 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -531,9 +531,8 @@ AsciiString GameState::findNextSaveFilename( UnicodeString desc ) /** Save the current state of the engine in a save file * NOTE: filename is a *filename only* */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, - SaveFileType saveType, SnapshotType which, - AsciiString *resolvedFilename ) +SaveResult GameState::saveGame( AsciiString filename, UnicodeString desc, + SaveFileType saveType, SnapshotType which ) { // if there is no filename, this is a new file being created, find an appropriate filename @@ -543,13 +542,9 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, { DEBUG_CRASH(( "GameState::saveGame - Unable to find valid filename for save game" )); - return SC_NO_FILE_AVAILABLE; + return SaveResult( SC_NO_FILE_AVAILABLE ); } - if( resolvedFilename != nullptr ) - { - *resolvedFilename = filename; - } // make absolutely sure the save directory exists CreateDirectory( getSaveDirectory().str(), nullptr ); @@ -566,7 +561,7 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, xferSave.open( filepath ); } catch(...) { DEBUG_LOG(( "Error opening file '%s'", filepath.str() )); - return SC_UNABLE_TO_OPEN_FILE; + return SaveResult( SC_UNABLE_TO_OPEN_FILE, filename ); } // save our save file type @@ -596,21 +591,21 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // close the file and get out of here xferSave.close(); - return SC_ERROR; + return SaveResult( SC_ERROR, filename ); } // close the file xferSave.close(); - return SC_OK; + return SaveResult( SC_OK, filename ); } // ------------------------------------------------------------------------------------------------ /** A mission save */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::missionSave( AsciiString *resolvedFilename ) +SaveResult GameState::missionSave() { // get campaign @@ -625,7 +620,7 @@ SaveCode GameState::missionSave( AsciiString *resolvedFilename ) desc.format( format, TheGameText->fetch( campaign->m_campaignNameLabel ).str(), missionNumber ); // do an automatic mission save - return saveGame( "", desc, SAVE_FILE_TYPE_MISSION, SNAPSHOT_SAVELOAD, resolvedFilename ); + return saveGame( "", desc, SAVE_FILE_TYPE_MISSION ); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp index 5185579bd32..e290489e9de 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -791,9 +791,8 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, // save the game AsciiString filename; filename = selectedGameInfo->filename; - SaveCode result = TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, - fileType, SNAPSHOT_SAVELOAD, &filename ); - presentSaveResult( result, filename ); + presentSaveResult( TheGameState->saveGame( filename, + selectedGameInfo->saveGameInfo.description, fileType ) ); /* // set the description text entry field to default value @@ -857,9 +856,7 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, AsciiString filename; if( selectedGameInfo ) filename = selectedGameInfo->filename; - SaveCode result = TheGameState->saveGame( filename, desc, fileType, - SNAPSHOT_SAVELOAD, &filename ); - presentSaveResult( result, filename ); + presentSaveResult( TheGameState->saveGame( filename, desc, fileType ) ); } else if( controlID == buttonSaveDescCancel ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp index abf8babe2d6..2b49f943ff1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ScoreScreen.cpp @@ -929,9 +929,7 @@ void finishSinglePlayerInit() GadgetButtonSetText(buttonContinue, TheGameText->fetch("GUI:SaveAndContinue")); // auto save game - AsciiString filename; - SaveCode result = TheGameState->missionSave( &filename ); - presentSaveResult( result, filename ); + presentSaveResult( TheGameState->missionSave() ); if(staticTextGameSaved) staticTextGameSaved->winHide(FALSE); } From 7347dd06fee5f207f0f57aa51d492b37d3d42460 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 10 Aug 2026 11:47:26 -0400 Subject: [PATCH 05/10] refactor(saveload): Remove superfluous comments and add const to doLoadGame locals --- .../Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp | 3 --- Generals/Code/GameEngine/Include/Common/GameState.h | 1 - .../GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 4 ++-- GeneralsMD/Code/GameEngine/Include/Common/GameState.h | 1 - .../GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 4 ++-- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp index f9c8e535fce..4979ba629be 100644 --- a/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/SaveLoadFeedback.cpp @@ -52,7 +52,6 @@ void presentSaveResult( const SaveResult &result ) } default: { - // SC_NO_FILE_AVAILABLE (and any other early-out) returned no UI in retail break; } } @@ -60,8 +59,6 @@ void presentSaveResult( const SaveResult &result ) void presentLoadResult( SaveCode result, const AsciiString &filename ) { - // Retail loadGame only surfaced a dialog on the exception path; SC_FILE_NOT_FOUND - // and SC_OK presented nothing. if( result == SC_INVALID_DATA ) { UnicodeString msg; diff --git a/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h index b6cd22e7767..76f2443f558 100644 --- a/Generals/Code/GameEngine/Include/Common/GameState.h +++ b/Generals/Code/GameEngine/Include/Common/GameState.h @@ -132,7 +132,6 @@ enum SaveCode CPP_11(: Int) SC_ERROR, }; -// The result of a save, pairing the outcome with the file it resolved to so the two cannot drift. struct SaveResult { explicit SaveResult( SaveCode code ) : saveCode(code) { } diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp index 8c7857e2c61..ff6d7b7e7d9 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -405,8 +405,8 @@ static void doLoadGame() // loose these allocated user data pointers attached as listbox item data when the // engine resets // - AsciiString filename = selectedGameInfo->filename; - SaveCode result = TheGameState->loadGame( *selectedGameInfo ); + const AsciiString filename = selectedGameInfo->filename; + const SaveCode result = TheGameState->loadGame( *selectedGameInfo ); presentLoadResult( result, filename ); if (result != SC_OK) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index ee1f4e1e506..bc2f21eb854 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -132,7 +132,6 @@ enum SaveCode CPP_11(: Int) SC_ERROR, }; -// The result of a save, pairing the outcome with the file it resolved to so the two cannot drift. struct SaveResult { explicit SaveResult( SaveCode code ) : saveCode(code) { } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp index e290489e9de..0831837b275 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -415,8 +415,8 @@ static void doLoadGame() // loose these allocated user data pointers attached as listbox item data when the // engine resets // - AsciiString filename = selectedGameInfo->filename; - SaveCode result = TheGameState->loadGame( *selectedGameInfo ); + const AsciiString filename = selectedGameInfo->filename; + const SaveCode result = TheGameState->loadGame( *selectedGameInfo ); presentLoadResult( result, filename ); if (result != SC_OK) { From e0a05769b91530e19ef575eda08bb1bc507777df Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 22 Jul 2026 13:15:27 +0400 Subject: [PATCH 06/10] bugfix(headless): Omit visual state from save and load --- .../Common/System/SaveGame/GameState.cpp | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index be41b368041..86ba98ffa08 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -72,6 +72,13 @@ static const Int MAX_SAVE_FILE_NUMBER = 99999999; #define GAME_STATE_BLOCK_STRING "CHUNK_GameState" // block of save game data with game info data #define CAMPAIGN_BLOCK_STRING "CHUNK_Campaign" // block of game data that has campaign info +static Bool isHeadlessOmittedBlock( const AsciiString &blockName ) +{ + return blockName.compareNoCase( "CHUNK_ParticleSystem" ) == 0 || + blockName.compareNoCase( "CHUNK_TerrainVisual" ) == 0 || + blockName.compareNoCase( "CHUNK_GhostObject" ) == 0; +} + // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ SaveGameInfo::SaveGameInfo() @@ -1327,6 +1334,10 @@ void GameState::xferSaveData( Xfer *xfer, SnapshotType which ) blockName = blockInfo->blockName; DEBUG_LOG(("Looking at block '%s'", blockName.str())); + if( TheGlobalData->m_headless && isHeadlessOmittedBlock( blockName ) ) + { + continue; + } // // for mission save files, we only save the game state block and campaign manager @@ -1424,8 +1435,15 @@ void GameState::xferSaveData( Xfer *xfer, SnapshotType which ) // read block start blockSize = xfer->beginBlock(); - // parse this data - xfer->xferSnapshot( blockInfo->snapshot ); + if( TheGlobalData->m_headless && isHeadlessOmittedBlock( token ) ) + { + xfer->skip( blockSize ); + } + else + { + // parse this data + xfer->xferSnapshot( blockInfo->snapshot ); + } // read block end xfer->endBlock(); From 501f0f9c3710c03ea10ac349bd686d06709bbe94 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 22 Jul 2026 13:51:23 +0400 Subject: [PATCH 07/10] feat(commandline): Add -loadsave option --- Core/GameEngine/Source/Common/CommandLine.cpp | 14 +++++++++ .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Source/Common/GameEngine.cpp | 29 +++++++++++++++++++ .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/GameClient/GUI/Shell/Shell.cpp | 6 ++-- 7 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..152b5ecb2ba 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -719,6 +719,19 @@ Int parseVTune ( char *args[], int num ) #endif // defined(RTS_DEBUG) +// TheSuperHackers @feature bobtista 22/07/2026 Load a save game file from the command line. +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; +} + //============================================================================= //============================================================================= @@ -1159,6 +1172,7 @@ static CommandLineParam paramsForEngineInit[] = { "-noshaders", parseNoShaders }, { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, + { "-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/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/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index f7720c351a2..62a7b605533 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -986,6 +986,7 @@ GlobalData::GlobalData() m_buildMapCache = FALSE; m_initialFile.clear(); m_pendingFile.clear(); + m_loadSaveGame.clear(); m_simulateReplays.clear(); m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; 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/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 32b93d3dba7..7399090d1d6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -941,6 +941,35 @@ void GameEngine::execute() DWORD startTime = timeGetTime() / 1000; #endif + // TheSuperHackers @feature bobtista 22/07/2026 Load a save game directly from the command line. + if (TheGlobalData->m_loadSaveGame.isNotEmpty()) + { + AvailableGameInfo gameInfo; + gameInfo.filename = TheGlobalData->m_loadSaveGame; + gameInfo.next = nullptr; + gameInfo.prev = nullptr; + + AsciiString fullPath = TheGameState->getFilePathInSaveDirectory(gameInfo.filename); + TheGameState->getSaveGameInfoFromFile(fullPath, &gameInfo.saveGameInfo); + TheGameLogic->prepareNewGame(GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0); + + AsciiString filename = gameInfo.filename; + SaveCode result = TheGameState->loadGame(gameInfo); + TheWritableGlobalData->m_loadSaveGame.clear(); + if (result == SC_OK) + { + if (TheShell) + { + TheShell->hideShell(); + } + } + else + { + DEBUG_LOG(("Failed to load save game '%s'", filename.str())); + m_quitting = TRUE; + } + } + // pretty basic for now while( !m_quitting ) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..7bd9ac5ef35 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -993,6 +993,7 @@ GlobalData::GlobalData() m_buildMapCache = FALSE; m_initialFile.clear(); m_pendingFile.clear(); + m_loadSaveGame.clear(); m_simulateReplays.clear(); m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index 1b6278d02db..c4557d37b6d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -465,7 +465,8 @@ void Shell::showShell( Bool runInit ) { DEBUG_LOG(("Shell:showShell() - %s (%s)", TheGlobalData->m_initialFile.str(), (top())?top()->getFilename().str():"no top screen")); - if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty()) + if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty() || + TheGlobalData->m_loadSaveGame.isNotEmpty()) { return; } @@ -527,7 +528,8 @@ void Shell::showShell( Bool runInit ) void Shell::showShellMap(Bool useShellMap ) { // we don't want any of this to show if we're loading straight into a file - if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty()) + if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty() || + TheGlobalData->m_loadSaveGame.isNotEmpty()) return; if(useShellMap && TheGlobalData->m_shellMapOn) { From e2a2c836657e58d59bd3e5558a2b33de0d6aaedd Mon Sep 17 00:00:00 2001 From: bobtista Date: Wed, 22 Jul 2026 13:48:00 -0400 Subject: [PATCH 08/10] feat(commandline): Add -loadsave option and omit visual state from headless saves (Generals) --- .../GameEngine/Source/Common/GameEngine.cpp | 29 +++++++++++++++++++ .../Common/System/SaveGame/GameState.cpp | 22 ++++++++++++-- .../Source/GameClient/GUI/Shell/Shell.cpp | 6 ++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 4631cc28133..baa981ead62 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -774,6 +774,35 @@ void GameEngine::execute() DWORD startTime = timeGetTime() / 1000; #endif + // TheSuperHackers @feature bobtista 22/07/2026 Load a save game directly from the command line. + if (TheGlobalData->m_loadSaveGame.isNotEmpty()) + { + AvailableGameInfo gameInfo; + gameInfo.filename = TheGlobalData->m_loadSaveGame; + gameInfo.next = nullptr; + gameInfo.prev = nullptr; + + AsciiString fullPath = TheGameState->getFilePathInSaveDirectory(gameInfo.filename); + TheGameState->getSaveGameInfoFromFile(fullPath, &gameInfo.saveGameInfo); + TheGameLogic->prepareNewGame(GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0); + + AsciiString filename = gameInfo.filename; + SaveCode result = TheGameState->loadGame(gameInfo); + TheWritableGlobalData->m_loadSaveGame.clear(); + if (result == SC_OK) + { + if (TheShell) + { + TheShell->hideShell(); + } + } + else + { + DEBUG_LOG(("Failed to load save game '%s'", filename.str())); + m_quitting = TRUE; + } + } + // pretty basic for now while( !m_quitting ) { diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 067c8404279..02de9ddc72c 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -72,6 +72,13 @@ static const Int MAX_SAVE_FILE_NUMBER = 99999999; #define GAME_STATE_BLOCK_STRING "CHUNK_GameState" // block of save game data with game info data #define CAMPAIGN_BLOCK_STRING "CHUNK_Campaign" // block of game data that has campaign info +static Bool isHeadlessOmittedBlock( const AsciiString &blockName ) +{ + return blockName.compareNoCase( "CHUNK_ParticleSystem" ) == 0 || + blockName.compareNoCase( "CHUNK_TerrainVisual" ) == 0 || + blockName.compareNoCase( "CHUNK_GhostObject" ) == 0; +} + // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ SaveGameInfo::SaveGameInfo() @@ -1327,6 +1334,10 @@ void GameState::xferSaveData( Xfer *xfer, SnapshotType which ) blockName = blockInfo->blockName; DEBUG_LOG(("Looking at block '%s'", blockName.str())); + if( TheGlobalData->m_headless && isHeadlessOmittedBlock( blockName ) ) + { + continue; + } // // for mission save files, we only save the game state block and campaign manager @@ -1424,8 +1435,15 @@ void GameState::xferSaveData( Xfer *xfer, SnapshotType which ) // read block start blockSize = xfer->beginBlock(); - // parse this data - xfer->xferSnapshot( blockInfo->snapshot ); + if( TheGlobalData->m_headless && isHeadlessOmittedBlock( token ) ) + { + xfer->skip( blockSize ); + } + else + { + // parse this data + xfer->xferSnapshot( blockInfo->snapshot ); + } // read block end xfer->endBlock(); diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index 909e27a0e21..d4575dd27d2 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -465,7 +465,8 @@ void Shell::showShell( Bool runInit ) { DEBUG_LOG(("Shell:showShell() - %s (%s)", TheGlobalData->m_initialFile.str(), (top())?top()->getFilename().str():"no top screen")); - if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty()) + if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty() || + TheGlobalData->m_loadSaveGame.isNotEmpty()) { return; } @@ -522,7 +523,8 @@ void Shell::showShell( Bool runInit ) void Shell::showShellMap(Bool useShellMap ) { // we don't want any of this to show if we're loading straight into a file - if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty()) + if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty() || + TheGlobalData->m_loadSaveGame.isNotEmpty()) return; if(useShellMap && TheGlobalData->m_shellMapOn) { From f21549522677476b07eabbfc3726c5fcdbe51f9c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 8 Aug 2026 10:36:22 -0400 Subject: [PATCH 09/10] feat(commandline): Load save and replay files from absolute paths --- Core/GameEngine/Include/Common/FileSystem.h | 1 + Core/GameEngine/Source/Common/CommandLine.cpp | 38 ++++++++++++++++++- .../Source/Common/System/FileSystem.cpp | 22 +++++++++++ .../GameEngine/Include/Common/GameState.h | 1 + .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Source/Common/GameEngine.cpp | 22 ++++++++++- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../GameEngine/Source/Common/Recorder.cpp | 10 ++++- .../Common/System/SaveGame/GameState.cpp | 21 ++++++++-- .../Source/GameClient/GUI/Shell/Shell.cpp | 4 +- 10 files changed, 109 insertions(+), 12 deletions(-) diff --git a/Core/GameEngine/Include/Common/FileSystem.h b/Core/GameEngine/Include/Common/FileSystem.h index 2aaa30a61f1..be0fd1c99c2 100644 --- a/Core/GameEngine/Include/Common/FileSystem.h +++ b/Core/GameEngine/Include/Common/FileSystem.h @@ -157,6 +157,7 @@ class FileSystem : public SubsystemInterface Bool createDirectory(AsciiString directory); ///< create a directory of the given name. + static Bool isAbsolutePath(const AsciiString& path); ///< determines if a path is absolute on the current platform. static AsciiString normalizePath(const AsciiString& path); ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure. static Bool isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath); ///< determines if a file path is within a base path. Both paths must be absolute, but do not need to exist. diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 152b5ecb2ba..4b7bd9730a4 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -724,12 +724,42 @@ Int parseLoadSave(char *args[], int num) { if (num > 1) { - TheWritableGlobalData->m_loadSaveGame = args[1]; + AsciiString filename = args[1]; + if (!filename.endsWithNoCase(".sav")) + { + printf("Invalid save game name \"%s\"\n", filename.str()); + exit(1); + } + + TheWritableGlobalData->m_loadSaveGame = filename; TheWritableGlobalData->m_shellMapOn = FALSE; TheWritableGlobalData->m_playIntro = FALSE; TheWritableGlobalData->m_playSizzle = FALSE; + return 2; } - return 2; + return 1; +} + +// TheSuperHackers @feature bobtista 08/08/2026 Load 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_playIntro = FALSE; + TheWritableGlobalData->m_playSizzle = FALSE; + TheWritableGlobalData->m_shellMapOn = FALSE; + return 2; + } + + return 1; } //============================================================================= @@ -1173,6 +1203,10 @@ static CommandLineParam paramsForEngineInit[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, { "-loadsave", parseLoadSave }, + { "-loadreplay", parseLoadReplay }, + // Keep replay playback normal by default. Diagnostic captures can opt out of + // sync-error reporting explicitly; -ignoresync remains the legacy debug alias. + { "-ignoreReplaySyncErrors", parseSync }, // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. { "-forcefullviewport", parseFullViewport }, diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index b8e4c4695b6..cdbb9294a3b 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -331,6 +331,28 @@ Bool FileSystem::createDirectory(AsciiString directory) return FALSE; } +//============================================================================ +// FileSystem::isAbsolutePath +//============================================================================ +// TheSuperHackers @feature bobtista 08/08/2026 Identify absolute paths so callers can distinguish +// explicitly selected files from names relative to their managed directories. +Bool FileSystem::isAbsolutePath(const AsciiString& path) +{ + const Char *value = path.str(); + if (value == nullptr || value[0] == 0) + return FALSE; + +#ifdef _WIN32 + const Bool hasDriveRoot = value[0] != 0 && value[1] == ':' && + (value[2] == '\\' || value[2] == '/'); + const Bool hasUncRoot = (value[0] == '\\' || value[0] == '/') && + (value[1] == '\\' || value[1] == '/'); + return hasDriveRoot || hasUncRoot; +#else + return value[0] == '/'; +#endif +} + //============================================================================ // FileSystem::normalizePath //============================================================================ diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index bc2f21eb854..5e8f129ca57 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -206,6 +206,7 @@ class GameState : public SubsystemInterface, private: + AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const; AsciiString findNextSaveFilename( UnicodeString desc ); ///< find next acceptable filename for a new save game void iterateSaveFiles( IterateSaveFileCallback callback, void *userData ); ///< iterate save files on disk diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..a1df63ea29d 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -352,6 +352,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, load a replay 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/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 7399090d1d6..8ddc1cb8386 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -909,6 +909,25 @@ void GameEngine::update() TheGameClient->UPDATE(); TheMessageStream->propagateMessages(); + // Defer command-line replay loading until the shell has initialized the + // UI state used by normal visual replay playback. + if (TheGlobalData->m_loadReplayGame.isNotEmpty()) + { + AsciiString replayGame = TheGlobalData->m_loadReplayGame; + TheWritableGlobalData->m_loadReplayGame.clear(); + + if (TheRecorder->playbackFile(replayGame)) + { + if (TheShell) + TheShell->hideShell(); + } + else + { + DEBUG_LOG(("Failed to load replay '%s'", replayGame.str())); + m_quitting = TRUE; + } + } + if (TheNetwork != nullptr) { TheNetwork->UPDATE(); @@ -949,8 +968,7 @@ void GameEngine::execute() gameInfo.next = nullptr; gameInfo.prev = nullptr; - AsciiString fullPath = TheGameState->getFilePathInSaveDirectory(gameInfo.filename); - TheGameState->getSaveGameInfoFromFile(fullPath, &gameInfo.saveGameInfo); + TheGameState->getSaveGameInfoFromFile(gameInfo.filename, &gameInfo.saveGameInfo); TheGameLogic->prepareNewGame(GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0); AsciiString filename = gameInfo.filename; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 7bd9ac5ef35..09a9a09e899 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -994,6 +994,7 @@ GlobalData::GlobalData() m_initialFile.clear(); m_pendingFile.clear(); m_loadSaveGame.clear(); + m_loadReplayGame.clear(); m_simulateReplays.clear(); m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 9d71eb45b1b..d76c0e4df5a 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -849,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 (!FileSystem::isAbsolutePath(filepath)) + { + 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; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 86ba98ffa08..a236e994571 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -656,8 +656,9 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) // TheGameStateMap->clearScratchPadMaps(); - // construct path to file - AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename); + // Relative names come from the save menu. Absolute paths can come from + // command-line file handlers and are opened in place. + AsciiString filepath = getSaveGamePathForRead(gameInfo.filename); // open the save file XferLoad xferLoad; @@ -756,6 +757,17 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const return tmp; } +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature bobtista 08/08/2026 Open command-line save paths in place while +// preserving the managed Save directory for relative menu filenames and save writes. +AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const +{ + if (FileSystem::isAbsolutePath(filenameOrPath)) + return filenameOrPath; + + return getFilePathInSaveDirectory(filenameOrPath); +} + //------------------------------------------------------------------------------------------------- Bool GameState::isInSaveDirectory(const AsciiString& path) const { @@ -913,8 +925,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const Bool GameState::doesSaveGameExist( AsciiString filename ) { - // construct full path to file - AsciiString filepath = getFilePathInSaveDirectory(filename); + AsciiString filepath = getSaveGamePathForRead(filename); // open file XferLoad xfer; @@ -959,6 +970,8 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav } + filename = getSaveGamePathForRead(filename); + // open file for partial loading XferLoad xferLoad; xferLoad.open( filename ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index c4557d37b6d..a676ee69b14 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -466,7 +466,7 @@ void Shell::showShell( Bool runInit ) DEBUG_LOG(("Shell:showShell() - %s (%s)", TheGlobalData->m_initialFile.str(), (top())?top()->getFilename().str():"no top screen")); if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty() || - TheGlobalData->m_loadSaveGame.isNotEmpty()) + TheGlobalData->m_loadSaveGame.isNotEmpty() || TheGlobalData->m_loadReplayGame.isNotEmpty()) { return; } @@ -529,7 +529,7 @@ void Shell::showShellMap(Bool useShellMap ) { // we don't want any of this to show if we're loading straight into a file if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty() || - TheGlobalData->m_loadSaveGame.isNotEmpty()) + TheGlobalData->m_loadSaveGame.isNotEmpty() || TheGlobalData->m_loadReplayGame.isNotEmpty()) return; if(useShellMap && TheGlobalData->m_shellMapOn) { From 7494ee1d23d2b22698e70d2a58003c54d8d76f12 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 8 Aug 2026 13:09:29 -0400 Subject: [PATCH 10/10] fix(commandline): Address save and replay loading review findings --- Core/GameEngine/Source/Common/CommandLine.cpp | 5 +- .../Source/Common/System/FileSystem.cpp | 11 +- .../GameEngine/Include/Common/GameState.h | 3 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Source/Common/GameEngine.cpp | 99 +++++++++++++ .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../GameEngine/Source/Common/Recorder.cpp | 21 ++- .../Common/System/SaveGame/GameState.cpp | 23 ++- .../Source/GameClient/GUI/Shell/Shell.cpp | 11 +- .../GameEngine/Include/Common/GameState.h | 4 +- .../GameEngine/Source/Common/GameEngine.cpp | 134 ++++++++++++------ .../GameEngine/Source/Common/Recorder.cpp | 11 ++ .../Common/System/SaveGame/GameState.cpp | 2 + .../Source/GameClient/GUI/Shell/Shell.cpp | 10 +- 14 files changed, 271 insertions(+), 65 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 4b7bd9730a4..676d5546b91 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -732,7 +732,6 @@ Int parseLoadSave(char *args[], int num) } TheWritableGlobalData->m_loadSaveGame = filename; - TheWritableGlobalData->m_shellMapOn = FALSE; TheWritableGlobalData->m_playIntro = FALSE; TheWritableGlobalData->m_playSizzle = FALSE; return 2; @@ -755,7 +754,6 @@ Int parseLoadReplay(char *args[], int num) TheWritableGlobalData->m_loadReplayGame = filename; TheWritableGlobalData->m_playIntro = FALSE; TheWritableGlobalData->m_playSizzle = FALSE; - TheWritableGlobalData->m_shellMapOn = FALSE; return 2; } @@ -1204,8 +1202,7 @@ static CommandLineParam paramsForEngineInit[] = { "-useWaveEditor", parseUseWaveEditor }, { "-loadsave", parseLoadSave }, { "-loadreplay", parseLoadReplay }, - // Keep replay playback normal by default. Diagnostic captures can opt out of - // sync-error reporting explicitly; -ignoresync remains the legacy debug alias. + // TheSuperHackers @feature bobtista 08/08/2026 Allow diagnostic replay playback to continue after CRC mismatches. { "-ignoreReplaySyncErrors", parseSync }, // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index cdbb9294a3b..a3b8ee14865 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -340,14 +340,17 @@ Bool FileSystem::isAbsolutePath(const AsciiString& path) { const Char *value = path.str(); if (value == nullptr || value[0] == 0) + { return FALSE; + } #ifdef _WIN32 - const Bool hasDriveRoot = value[0] != 0 && value[1] == ':' && + const Bool hasDriveLetter = (value[0] >= 'A' && value[0] <= 'Z') || + (value[0] >= 'a' && value[0] <= 'z'); + const Bool hasDriveRoot = hasDriveLetter && value[1] == ':' && (value[2] == '\\' || value[2] == '/'); - const Bool hasUncRoot = (value[0] == '\\' || value[0] == '/') && - (value[1] == '\\' || value[1] == '/'); - return hasDriveRoot || hasUncRoot; + const Bool hasCurrentDriveRoot = value[0] == '\\' || value[0] == '/'; + return hasDriveRoot || hasCurrentDriveRoot; #else return value[0] == '/'; #endif diff --git a/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h index 76f2443f558..cbfcb58e396 100644 --- a/Generals/Code/GameEngine/Include/Common/GameState.h +++ b/Generals/Code/GameEngine/Include/Common/GameState.h @@ -179,7 +179,7 @@ class GameState : public SubsystemInterface, // manipulating files Bool doesSaveGameExist( AsciiString filename ); ///< does the save file exist void populateSaveGameListbox( GameWindow *listbox, SaveLoadLayoutType layoutType ); ///< populate listbox with available save games - void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from file + void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from a Save-directory name or absolute path void friend_xferSaveDataForCRC( Xfer *xfer, SnapshotType which ); ///< This should only be called to DeepCRC sanity checking @@ -190,6 +190,7 @@ class GameState : public SubsystemInterface, AsciiString getSaveDirectory() const; AsciiString getFilePathInSaveDirectory(const AsciiString& leaf) const; + AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const; Bool isInSaveDirectory(const AsciiString& path) const; AsciiString realMapPathToPortableMapPath(const AsciiString& in) const; diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 0c8e8820660..2031f14adf0 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -351,6 +351,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, load a replay 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/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index baa981ead62..b40c40d68d4 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -723,6 +723,40 @@ Bool GameEngine::canUpdateRegularGameLogic(UnsignedInt logicTimeQueryFlags) /// ----------------------------------------------------------------------------------------------- DECLARE_PERF_TIMER(GameEngine_update) +// TheSuperHackers @feature bobtista 08/08/2026 Validate command-line replays before starting visual playback. +static Bool validateCommandLineReplay(const AsciiString& filename) +{ + RecorderClass::ReplayHeader header; + header.forPlayback = FALSE; + header.filename = filename; + + if (TheRecorder == nullptr || !TheRecorder->readReplayHeader(header)) + { + DEBUG_LOG(("Failed to read replay '%s'", filename.str())); + return FALSE; + } + + ReplayGameInfo gameInfo; + if (!ParseAsciiStringToGameInfo(&gameInfo, header.gameOptions)) + { + DEBUG_LOG(("Replay '%s' contains invalid game options", filename.str())); + return FALSE; + } + + if (TheMapCache == nullptr || TheMapCache->findMap(gameInfo.getMap()) == nullptr) + { + DEBUG_LOG(("Replay '%s' requires unavailable map '%s'", filename.str(), gameInfo.getMap().str())); + return FALSE; + } + + if (!RecorderClass::replayMatchesGameVersion(header)) + { + DEBUG_LOG(("Replay '%s' was created by a different game version; command-line playback will continue", filename.str())); + } + + return TRUE; +} + /** ----------------------------------------------------------------------------------------------- * Update the game engine by updating the GameClient and GameLogic singletons. */ @@ -748,6 +782,71 @@ void GameEngine::update() } } + // TheSuperHackers @feature bobtista 22/07/2026 Defer command-line save loading until the client has created the shell + // layout that normal game sessions return to. + if (TheGlobalData->m_loadSaveGame.isNotEmpty()) + { + AvailableGameInfo gameInfo; + gameInfo.filename = TheGlobalData->m_loadSaveGame; + gameInfo.next = nullptr; + gameInfo.prev = nullptr; + + AsciiString filename = gameInfo.filename; + Bool loadSucceeded = FALSE; + try + { + TheGameState->getSaveGameInfoFromFile(gameInfo.filename, &gameInfo.saveGameInfo); + TheGameLogic->prepareNewGame(GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0); + loadSucceeded = TheGameState->loadGame(gameInfo) == SC_OK; + } + catch (...) + { + DEBUG_LOG(("Failed to read save game '%s'", filename.str())); + } + + TheWritableGlobalData->m_loadSaveGame.clear(); + if (loadSucceeded) + { + if (TheShell) + { + TheShell->hideShell(); + } + } + else + { + if (TheGameLogic->isInGame()) + { + TheGameLogic->clearGameData(FALSE); + } + TheGameEngine->reset(); + DEBUG_LOG(("Failed to load save game '%s'", filename.str())); + printf("Failed to load save game '%s'\n", filename.str()); + m_quitting = TRUE; + } + } + + // TheSuperHackers @feature bobtista 08/08/2026 Defer command-line replay loading until the client has initialized the + // UI state used by normal visual replay playback. + if (TheGlobalData->m_loadReplayGame.isNotEmpty()) + { + AsciiString replayGame = TheGlobalData->m_loadReplayGame; + TheWritableGlobalData->m_loadReplayGame.clear(); + + if (validateCommandLineReplay(replayGame) && TheRecorder->playbackFile(replayGame)) + { + if (TheShell) + { + TheShell->hideShell(); + } + } + else + { + DEBUG_LOG(("Failed to load replay '%s'", replayGame.str())); + printf("Failed to load replay '%s'\n", replayGame.str()); + m_quitting = TRUE; + } + } + // TheSuperHackers @info Ignores frozen time because the script engine needs updating in the logic update regardless. if (canUpdateGameLogic(FramePacer::IgnoreFrozenTime)) { diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index 62a7b605533..b983e14f441 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -987,6 +987,7 @@ GlobalData::GlobalData() m_initialFile.clear(); m_pendingFile.clear(); m_loadSaveGame.clear(); + m_loadReplayGame.clear(); m_simulateReplays.clear(); m_simulateReplayJobs = SIMULATE_REPLAYS_SEQUENTIAL; diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index 7a955895827..e9fab9877fa 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -847,8 +847,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 (!FileSystem::isAbsolutePath(filepath)) + { + 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; @@ -998,6 +1004,17 @@ 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 Allow explicit diagnostic replay playback to continue without UI reporting or pausing. + if (TheDebugIgnoreSyncErrors) + { + const UnsignedInt mismatchFrame = TheGameLogic->getFrame() - m_crcInfo.GetQueueSize() - 1; + DEBUG_LOG(("Replay CRC mismatch ignored at frame %d\nInGame:%8.8X Replay:%8.8X", + mismatchFrame, playbackCRC, newCRC)); + printf("CRC Mismatch in Frame %d (ignored)\n", mismatchFrame); + 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 diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 02de9ddc72c..8942334cf6a 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -656,8 +656,9 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) // TheGameStateMap->clearScratchPadMaps(); - // construct path to file - AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename); + // Relative names come from the save menu. Absolute paths can come from + // command-line file handlers and are opened in place. + AsciiString filepath = getSaveGamePathForRead(gameInfo.filename); // open the save file XferLoad xferLoad; @@ -756,6 +757,19 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const return tmp; } +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature bobtista 08/08/2026 Open command-line save paths in place while +// preserving the managed Save directory for relative menu filenames and save writes. +AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const +{ + if (FileSystem::isAbsolutePath(filenameOrPath)) + { + return filenameOrPath; + } + + return getFilePathInSaveDirectory(filenameOrPath); +} + //------------------------------------------------------------------------------------------------- Bool GameState::isInSaveDirectory(const AsciiString& path) const { @@ -913,8 +927,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const Bool GameState::doesSaveGameExist( AsciiString filename ) { - // construct full path to file - AsciiString filepath = getFilePathInSaveDirectory(filename); + AsciiString filepath = getSaveGamePathForRead(filename); // open file XferLoad xfer; @@ -959,6 +972,8 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav } + filename = getSaveGamePathForRead(filename); + // open file for partial loading XferLoad xferLoad; xferLoad.open( filename ); diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index d4575dd27d2..bb2093df288 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -470,6 +470,8 @@ void Shell::showShell( Bool runInit ) { return; } + const Bool isCommandLineLoadPending = TheGlobalData->m_loadSaveGame.isNotEmpty() || + TheGlobalData->m_loadReplayGame.isNotEmpty(); // runInit is used if we want show shell to run if(runInit) @@ -514,9 +516,10 @@ void Shell::showShell( Bool runInit ) // } - if (!TheGlobalData->m_shellMapOn && m_screenCount == 0) - //else + if ((!TheGlobalData->m_shellMapOn || isCommandLineLoadPending) && m_screenCount == 0) + { push( "Menus/MainMenu.wnd" ); + } m_isShellActive = TRUE; } @@ -524,8 +527,10 @@ void Shell::showShellMap(Bool useShellMap ) { // we don't want any of this to show if we're loading straight into a file if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty() || - TheGlobalData->m_loadSaveGame.isNotEmpty()) + TheGlobalData->m_loadSaveGame.isNotEmpty() || TheGlobalData->m_loadReplayGame.isNotEmpty()) + { return; + } if(useShellMap && TheGlobalData->m_shellMapOn) { // we're already in a shell game, return diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index 5e8f129ca57..e8f93386a5d 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -179,7 +179,7 @@ class GameState : public SubsystemInterface, // manipulating files Bool doesSaveGameExist( AsciiString filename ); ///< does the save file exist void populateSaveGameListbox( GameWindow *listbox, SaveLoadLayoutType layoutType ); ///< populate listbox with available save games - void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from file + void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from a Save-directory name or absolute path void friend_xferSaveDataForCRC( Xfer *xfer, SnapshotType which ); ///< This should only be called to DeepCRC sanity checking @@ -190,6 +190,7 @@ class GameState : public SubsystemInterface, AsciiString getSaveDirectory() const; AsciiString getFilePathInSaveDirectory(const AsciiString& leaf) const; + AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const; Bool isInSaveDirectory(const AsciiString& path) const; AsciiString realMapPathToPortableMapPath(const AsciiString& in) const; @@ -206,7 +207,6 @@ class GameState : public SubsystemInterface, private: - AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const; AsciiString findNextSaveFilename( UnicodeString desc ); ///< find next acceptable filename for a new save game void iterateSaveFiles( IterateSaveFileCallback callback, void *userData ); ///< iterate save files on disk diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 8ddc1cb8386..e0973c7824b 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -890,6 +890,40 @@ Bool GameEngine::canUpdateRegularGameLogic(UnsignedInt logicTimeQueryFlags) /// ----------------------------------------------------------------------------------------------- DECLARE_PERF_TIMER(GameEngine_update) +// TheSuperHackers @feature bobtista 08/08/2026 Validate command-line replays before starting visual playback. +static Bool validateCommandLineReplay(const AsciiString& filename) +{ + RecorderClass::ReplayHeader header; + header.forPlayback = FALSE; + header.filename = filename; + + if (TheRecorder == nullptr || !TheRecorder->readReplayHeader(header)) + { + DEBUG_LOG(("Failed to read replay '%s'", filename.str())); + return FALSE; + } + + ReplayGameInfo gameInfo; + if (!ParseAsciiStringToGameInfo(&gameInfo, header.gameOptions)) + { + DEBUG_LOG(("Replay '%s' contains invalid game options", filename.str())); + return FALSE; + } + + if (TheMapCache == nullptr || TheMapCache->findMap(gameInfo.getMap()) == nullptr) + { + DEBUG_LOG(("Replay '%s' requires unavailable map '%s'", filename.str(), gameInfo.getMap().str())); + return FALSE; + } + + if (!RecorderClass::replayMatchesGameVersion(header)) + { + DEBUG_LOG(("Replay '%s' was created by a different game version; command-line playback will continue", filename.str())); + } + + return TRUE; +} + /** ----------------------------------------------------------------------------------------------- * Update the game engine by updating the GameClient and GameLogic singletons. */ @@ -909,28 +943,74 @@ void GameEngine::update() TheGameClient->UPDATE(); TheMessageStream->propagateMessages(); - // Defer command-line replay loading until the shell has initialized the - // UI state used by normal visual replay playback. - if (TheGlobalData->m_loadReplayGame.isNotEmpty()) + if (TheNetwork != nullptr) { - AsciiString replayGame = TheGlobalData->m_loadReplayGame; - TheWritableGlobalData->m_loadReplayGame.clear(); + TheNetwork->UPDATE(); + } + } - if (TheRecorder->playbackFile(replayGame)) + // TheSuperHackers @feature bobtista 22/07/2026 Defer command-line save loading until the client has created the shell + // layout that normal game sessions return to. + if (TheGlobalData->m_loadSaveGame.isNotEmpty()) + { + AvailableGameInfo gameInfo; + gameInfo.filename = TheGlobalData->m_loadSaveGame; + gameInfo.next = nullptr; + gameInfo.prev = nullptr; + + AsciiString filename = gameInfo.filename; + Bool loadSucceeded = FALSE; + try + { + TheGameState->getSaveGameInfoFromFile(gameInfo.filename, &gameInfo.saveGameInfo); + TheGameLogic->prepareNewGame(GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0); + loadSucceeded = TheGameState->loadGame(gameInfo) == SC_OK; + } + catch (...) + { + DEBUG_LOG(("Failed to read save game '%s'", filename.str())); + } + + TheWritableGlobalData->m_loadSaveGame.clear(); + if (loadSucceeded) + { + if (TheShell) { - if (TheShell) - TheShell->hideShell(); + TheShell->hideShell(); } - else + } + else + { + if (TheGameLogic->isInGame()) { - DEBUG_LOG(("Failed to load replay '%s'", replayGame.str())); - m_quitting = TRUE; + TheGameLogic->clearGameData(FALSE); } + TheGameEngine->reset(); + DEBUG_LOG(("Failed to load save game '%s'", filename.str())); + printf("Failed to load save game '%s'\n", filename.str()); + m_quitting = TRUE; } + } - if (TheNetwork != nullptr) + // TheSuperHackers @feature bobtista 08/08/2026 Defer command-line replay loading until the client has initialized the + // UI state used by normal visual replay playback. + if (TheGlobalData->m_loadReplayGame.isNotEmpty()) + { + AsciiString replayGame = TheGlobalData->m_loadReplayGame; + TheWritableGlobalData->m_loadReplayGame.clear(); + + if (validateCommandLineReplay(replayGame) && TheRecorder->playbackFile(replayGame)) { - TheNetwork->UPDATE(); + if (TheShell) + { + TheShell->hideShell(); + } + } + else + { + DEBUG_LOG(("Failed to load replay '%s'", replayGame.str())); + printf("Failed to load replay '%s'\n", replayGame.str()); + m_quitting = TRUE; } } @@ -960,34 +1040,6 @@ void GameEngine::execute() DWORD startTime = timeGetTime() / 1000; #endif - // TheSuperHackers @feature bobtista 22/07/2026 Load a save game directly from the command line. - if (TheGlobalData->m_loadSaveGame.isNotEmpty()) - { - AvailableGameInfo gameInfo; - gameInfo.filename = TheGlobalData->m_loadSaveGame; - gameInfo.next = nullptr; - gameInfo.prev = nullptr; - - TheGameState->getSaveGameInfoFromFile(gameInfo.filename, &gameInfo.saveGameInfo); - TheGameLogic->prepareNewGame(GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0); - - AsciiString filename = gameInfo.filename; - SaveCode result = TheGameState->loadGame(gameInfo); - TheWritableGlobalData->m_loadSaveGame.clear(); - if (result == SC_OK) - { - if (TheShell) - { - TheShell->hideShell(); - } - } - else - { - DEBUG_LOG(("Failed to load save game '%s'", filename.str())); - m_quitting = TRUE; - } - } - // pretty basic for now while( !m_quitting ) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index d76c0e4df5a..1b581a247e0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1006,6 +1006,17 @@ 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 Allow explicit diagnostic replay playback to continue without UI reporting or pausing. + if (TheDebugIgnoreSyncErrors) + { + const UnsignedInt mismatchFrame = TheGameLogic->getFrame() - m_crcInfo.GetQueueSize() - 1; + DEBUG_LOG(("Replay CRC mismatch ignored at frame %d\nInGame:%8.8X Replay:%8.8X", + mismatchFrame, playbackCRC, newCRC)); + printf("CRC Mismatch in Frame %d (ignored)\n", mismatchFrame); + 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 diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index a236e994571..88513a979f2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -763,7 +763,9 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const { if (FileSystem::isAbsolutePath(filenameOrPath)) + { return filenameOrPath; + } return getFilePathInSaveDirectory(filenameOrPath); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index a676ee69b14..a11aac12ffb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -465,11 +465,12 @@ void Shell::showShell( Bool runInit ) { DEBUG_LOG(("Shell:showShell() - %s (%s)", TheGlobalData->m_initialFile.str(), (top())?top()->getFilename().str():"no top screen")); - if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty() || - TheGlobalData->m_loadSaveGame.isNotEmpty() || TheGlobalData->m_loadReplayGame.isNotEmpty()) + if(!TheGlobalData->m_initialFile.isEmpty() || !TheGlobalData->m_simulateReplays.empty()) { return; } + const Bool isCommandLineLoadPending = TheGlobalData->m_loadSaveGame.isNotEmpty() || + TheGlobalData->m_loadReplayGame.isNotEmpty(); // runInit is used if we want show shell to run if(runInit) @@ -514,12 +515,11 @@ void Shell::showShell( Bool runInit ) // } - if (!TheGlobalData->m_shellMapOn && m_screenCount == 0) + if ((!TheGlobalData->m_shellMapOn || isCommandLineLoadPending) && m_screenCount == 0) { #ifdef RTS_PROFILE_LEGACY Profile::StopRange("init"); #endif - //else push( "Menus/MainMenu.wnd" ); } m_isShellActive = TRUE; @@ -530,7 +530,9 @@ void Shell::showShellMap(Bool useShellMap ) // we don't want any of this to show if we're loading straight into a file if (TheGlobalData->m_initialFile.isNotEmpty() || !TheGameLogic || !TheGlobalData->m_simulateReplays.empty() || TheGlobalData->m_loadSaveGame.isNotEmpty() || TheGlobalData->m_loadReplayGame.isNotEmpty()) + { return; + } if(useShellMap && TheGlobalData->m_shellMapOn) { // we're already in a shell game, return