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); }