diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index 8af78e0e16c..acb1ef725a9 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/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h b/Core/GameEngine/Include/GameClient/SaveLoadFeedback.h
new file mode 100644
index 00000000000..df90a7a0cce
--- /dev/null
+++ b/Core/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( 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
new file mode 100644
index 00000000000..f9c8e535fce
--- /dev/null
+++ b/Core/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( const SaveResult &result )
+{
+ switch( result.saveCode )
+ {
+ 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(result.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/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h
index 20e8910174f..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,11 +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 ); ///< save a game
- SaveCode missionSave(); ///< 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 7390ff5ed85..067c8404279 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"
@@ -532,8 +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 )
+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,7 +542,7 @@ 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 );
}
@@ -561,10 +560,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 SaveResult( SC_UNABLE_TO_OPEN_FILE, filename );
}
// save our save file type
@@ -592,35 +589,23 @@ 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;
+ return SaveResult( SC_ERROR, filename );
}
// 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;
+ return SaveResult( SC_OK, filename );
}
// ------------------------------------------------------------------------------------------------
/** A mission save */
// ------------------------------------------------------------------------------------------------
-SaveCode GameState::missionSave()
+SaveResult GameState::missionSave()
{
// get campaign
@@ -717,15 +702,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..8c7857e2c61 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,8 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg,
// save the game
AsciiString filename;
filename = selectedGameInfo->filename;
- TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, fileType );
+ presentSaveResult( TheGameState->saveGame( filename,
+ selectedGameInfo->saveGameInfo.description, fileType ) );
/*
// set the description text entry field to default value
@@ -835,7 +840,7 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg,
AsciiString filename;
if( selectedGameInfo )
filename = selectedGameInfo->filename;
- TheGameState->saveGame( filename, desc, fileType );
+ 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 a217b8023c5..be184fb1947 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"
@@ -763,7 +764,7 @@ void finishSinglePlayerInit()
GadgetButtonSetText(buttonContinue, TheGameText->fetch("GUI:SaveAndContinue"));
// auto save game
- TheGameState->missionSave();
+ 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 16bc991b69a..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,11 +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 ); ///< save a game
- SaveCode missionSave(); ///< 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 04cc701b5e1..be41b368041 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"
@@ -532,8 +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 )
+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,7 +542,7 @@ 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 );
}
@@ -561,10 +560,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 SaveResult( SC_UNABLE_TO_OPEN_FILE, filename );
}
// save our save file type
@@ -592,35 +589,23 @@ 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;
+ return SaveResult( SC_ERROR, filename );
}
// 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;
+ return SaveResult( SC_OK, filename );
}
// ------------------------------------------------------------------------------------------------
/** A mission save */
// ------------------------------------------------------------------------------------------------
-SaveCode GameState::missionSave()
+SaveResult GameState::missionSave()
{
// get campaign
@@ -717,15 +702,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..e290489e9de 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,8 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg,
// save the game
AsciiString filename;
filename = selectedGameInfo->filename;
- TheGameState->saveGame( filename, selectedGameInfo->saveGameInfo.description, fileType );
+ presentSaveResult( TheGameState->saveGame( filename,
+ selectedGameInfo->saveGameInfo.description, fileType ) );
/*
// set the description text entry field to default value
@@ -851,7 +856,7 @@ WindowMsgHandledType SaveLoadMenuSystem( GameWindow *window, UnsignedInt msg,
AsciiString filename;
if( selectedGameInfo )
filename = selectedGameInfo->filename;
- TheGameState->saveGame( filename, desc, fileType );
+ 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 38c88601e42..666425c3786 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,7 @@ void finishSinglePlayerInit()
GadgetButtonSetText(buttonContinue, TheGameText->fetch("GUI:SaveAndContinue"));
// auto save game
- TheGameState->missionSave();
+ presentSaveResult( TheGameState->missionSave() );
if(staticTextGameSaved)
staticTextGameSaved->winHide(FALSE);
}