diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 26023c229db..a4403fd5649 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 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/CommandLine.cpp b/Generals/Code/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..152b5ecb2ba 100644 --- a/Generals/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/Generals/Code/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/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/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index 25d5432c51f..0a36c130c3d 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -987,6 +987,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/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 7390ff5ed85..a7125a3ce0b 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -73,6 +73,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() @@ -1351,6 +1358,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 @@ -1448,8 +1459,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 fff2cf12c94..a98391cbaf7 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -466,7 +466,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; } @@ -523,7 +524,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) { diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 5218ff802d9..f0d85209526 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/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 5235b763fd1..1c30ae9405e 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 Bool m_buildMapCache; AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start + AsciiString m_loadSaveGame; ///< If this is specified, load a save game file from the command-line std::vector m_simulateReplays; ///< If not empty, simulate this list of replays and exit. Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation diff --git a/GeneralsMD/Code/GameEngine/Include/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/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..152b5ecb2ba 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/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/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 131d20513ff..7ff9ddf8723 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -994,6 +994,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/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 04cc701b5e1..205720928ce 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" @@ -73,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() @@ -533,7 +539,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 +553,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 +572,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 +601,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 +610,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 +617,7 @@ SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc, // ------------------------------------------------------------------------------------------------ /** A mission save */ // ------------------------------------------------------------------------------------------------ -SaveCode GameState::missionSave() +SaveCode GameState::missionSave( AsciiString *resolvedFilename ) { // get campaign @@ -635,7 +632,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 +714,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! } @@ -1351,6 +1339,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 @@ -1448,8 +1440,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/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 38c88601e42..9f1e2430c06 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" @@ -924,7 +925,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 ); + } +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp index e7d1333905b..95af96093dd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/Shell/Shell.cpp @@ -466,7 +466,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; } @@ -528,7 +529,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) {