From 1e69887e25e5da25fa285f33f208a7fca24dda22 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 22 Jul 2026 13:15:21 +0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 002e7f094dcf6d2c3d4285347738254c149b914d Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 17 Aug 2026 21:46:46 -0400 Subject: [PATCH 6/7] refactor(saveload): Reduce the scope of selectedGameInfo in doLoadGame --- .../GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 45 ++++++++++--------- .../GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 45 ++++++++++--------- 2 files changed, 50 insertions(+), 40 deletions(-) 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 ff6d7b7e7d9..d19ed30448c 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -381,32 +381,37 @@ static void doLoadGame() //GameWindow *listboxGames = TheWindowManager->winGetWindowFromId( parent, NAMEKEY( "PopupSaveLoad.wnd:ListboxGames" ) ); DEBUG_ASSERTCRASH( listboxGames, ("doLoadGame: Unable to find game listbox") ); - // get selected game info - AvailableGameInfo *selectedGameInfo = getSelectedSaveFileInfo( listboxGames ); - DEBUG_ASSERTCRASH( selectedGameInfo, ("doLoadGame: No selected game info found") ); - - // when loading a game we also close the quit/esc menu for the user when in-game - if( TheShell->isShellActive() == FALSE ) + AsciiString filename; + SaveCode result = SC_INVALID; { - destroyQuitMenu(); + // get selected game info + AvailableGameInfo *selectedGameInfo = getSelectedSaveFileInfo( listboxGames ); + DEBUG_ASSERTCRASH( selectedGameInfo, ("doLoadGame: No selected game info found") ); + + // when loading a game we also close the quit/esc menu for the user when in-game + if( TheShell->isShellActive() == FALSE ) + { + destroyQuitMenu(); // ToggleQuitMenu(); // TheTransitionHandler->remove("QuitNoSave"); // TheTransitionHandler->remove("QuitFull"); - } - else - { - TheTransitionHandler->remove("MainMenuLoadReplayMenu"); - TheTransitionHandler->remove("MainMenuLoadReplayMenuBack"); - TheGameLogic->prepareNewGame( GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0 ); + } + else + { + TheTransitionHandler->remove("MainMenuLoadReplayMenu"); + TheTransitionHandler->remove("MainMenuLoadReplayMenuBack"); + TheGameLogic->prepareNewGame( GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0 ); + } + + // + // load game, note the *copy* of the selected game info is passed here because we will + // loose these allocated user data pointers attached as listbox item data when the + // engine resets + // + filename = selectedGameInfo->filename; + result = TheGameState->loadGame( *selectedGameInfo ); } - // - // load game, note the *copy* of the selected game info is passed here because we will - // loose these allocated user data pointers attached as listbox item data when the - // engine resets - // - const AsciiString filename = selectedGameInfo->filename; - const SaveCode result = TheGameState->loadGame( *selectedGameInfo ); presentLoadResult( result, filename ); if (result != SC_OK) { 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 0831837b275..69a1d1c3b12 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -391,32 +391,37 @@ static void doLoadGame() //GameWindow *listboxGames = TheWindowManager->winGetWindowFromId( parent, NAMEKEY( "PopupSaveLoad.wnd:ListboxGames" ) ); DEBUG_ASSERTCRASH( listboxGames, ("doLoadGame: Unable to find game listbox") ); - // get selected game info - AvailableGameInfo *selectedGameInfo = getSelectedSaveFileInfo( listboxGames ); - DEBUG_ASSERTCRASH( selectedGameInfo, ("doLoadGame: No selected game info found") ); - - // when loading a game we also close the quit/esc menu for the user when in-game - if( TheShell->isShellActive() == FALSE ) + AsciiString filename; + SaveCode result = SC_INVALID; { - destroyQuitMenu(); + // get selected game info + AvailableGameInfo *selectedGameInfo = getSelectedSaveFileInfo( listboxGames ); + DEBUG_ASSERTCRASH( selectedGameInfo, ("doLoadGame: No selected game info found") ); + + // when loading a game we also close the quit/esc menu for the user when in-game + if( TheShell->isShellActive() == FALSE ) + { + destroyQuitMenu(); // ToggleQuitMenu(); // TheTransitionHandler->remove("QuitNoSave"); // TheTransitionHandler->remove("QuitFull"); - } - else - { - TheTransitionHandler->remove("MainMenuLoadReplayMenu"); - TheTransitionHandler->remove("MainMenuLoadReplayMenuBack"); - TheGameLogic->prepareNewGame( GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0 ); + } + else + { + TheTransitionHandler->remove("MainMenuLoadReplayMenu"); + TheTransitionHandler->remove("MainMenuLoadReplayMenuBack"); + TheGameLogic->prepareNewGame( GAME_SINGLE_PLAYER, DIFFICULTY_NORMAL, 0 ); + } + + // + // load game, note the *copy* of the selected game info is passed here because we will + // loose these allocated user data pointers attached as listbox item data when the + // engine resets + // + filename = selectedGameInfo->filename; + result = TheGameState->loadGame( *selectedGameInfo ); } - // - // load game, note the *copy* of the selected game info is passed here because we will - // loose these allocated user data pointers attached as listbox item data when the - // engine resets - // - const AsciiString filename = selectedGameInfo->filename; - const SaveCode result = TheGameState->loadGame( *selectedGameInfo ); presentLoadResult( result, filename ); if (result != SC_OK) { From bc642af82eb8f2ebdbf18001ab19341503552565 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 21 Aug 2026 15:30:33 -0400 Subject: [PATCH 7/7] style(saveload): Put the saveGame call on one line --- .../Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 3 +-- .../Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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 d19ed30448c..f3c946ec266 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -780,8 +780,7 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, // save the game AsciiString filename; filename = selectedGameInfo->filename; - presentSaveResult( TheGameState->saveGame( filename, - selectedGameInfo->saveGameInfo.description, fileType ) ); + presentSaveResult( TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, fileType ) ); /* // set the description text entry field to default value 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 69a1d1c3b12..18b148a08d2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupSaveLoad.cpp @@ -796,8 +796,7 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg, // save the game AsciiString filename; filename = selectedGameInfo->filename; - presentSaveResult( TheGameState->saveGame( filename, - selectedGameInfo->saveGameInfo.description, fileType ) ); + presentSaveResult( TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, fileType ) ); /* // set the description text entry field to default value