From 56cee9819332ca1d4b0009599dd80a75d04b1350 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 27 Aug 2026 12:28:06 -0400 Subject: [PATCH] feat(cli): Load save files from absolute paths --- Core/GameEngine/Source/Common/CommandLine.cpp | 4 +- Core/Libraries/Include/Lib/PathUtil.h | 32 ++++++++++++++ .../GameEngine/Include/Common/GameState.h | 1 + .../Common/System/SaveGame/GameState.cpp | 44 +++++++++++++++---- .../GameEngine/Include/Common/GameState.h | 1 + .../Common/System/SaveGame/GameState.cpp | 44 +++++++++++++++---- 6 files changed, 109 insertions(+), 17 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index ff36f833073..96a6f475d30 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -727,8 +727,10 @@ Int parseLoadSave(char *args[], int num) TheWritableGlobalData->m_shellMapOn = FALSE; TheWritableGlobalData->m_playIntro = FALSE; TheWritableGlobalData->m_playSizzle = FALSE; + + return 2; } - return 2; + return 1; } //============================================================================= diff --git a/Core/Libraries/Include/Lib/PathUtil.h b/Core/Libraries/Include/Lib/PathUtil.h index cf1ce769d91..683f6fee130 100644 --- a/Core/Libraries/Include/Lib/PathUtil.h +++ b/Core/Libraries/Include/Lib/PathUtil.h @@ -23,6 +23,38 @@ #include "BaseType.h" #include +inline bool isPathSeparator(char ch) +{ +#ifdef _WIN32 + return ch == '\\' || ch == '/'; +#else + return ch == '/'; +#endif +} + +inline bool isAbsolutePath(const char* path) +{ + if (path == nullptr) + { + return false; + } + + if (isPathSeparator(path[0])) + { + return true; + } + +#ifdef _WIN32 + const bool hasDriveLetter = (path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'); + if (hasDriveLetter && path[1] == ':' && isPathSeparator(path[2])) + { + return true; + } +#endif + + return false; +} + inline const char* getExtension(const char* path) { const char* lastDot = strrchr(path, '.'); diff --git a/Generals/Code/GameEngine/Include/Common/GameState.h b/Generals/Code/GameEngine/Include/Common/GameState.h index 8ce8a1bfb32..aef93c15e6f 100644 --- a/Generals/Code/GameEngine/Include/Common/GameState.h +++ b/Generals/Code/GameEngine/Include/Common/GameState.h @@ -191,6 +191,7 @@ class GameState : public SubsystemInterface, AsciiString getSaveDirectory() const; AsciiString getFilePathInSaveDirectory(const AsciiString& leaf) const; + AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const; Bool isInSaveDirectory(const AsciiString& path) const; AsciiString realMapPathToPortableMapPath(const AsciiString& in) const; diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index a55cdb45a2b..e89d086c98e 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -48,6 +48,7 @@ #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" @@ -57,6 +58,7 @@ #include "GameLogic/ScriptEngine.h" #include "GameLogic/SidesList.h" #include "GameLogic/TerrainLogic.h" +#include "Lib/PathUtil.h" // PUBLIC DATA //////////////////////////////////////////////////////////////////////////////////// @@ -656,8 +658,7 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) // TheGameStateMap->clearScratchPadMaps(); - // construct path to file - AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename); + AsciiString filepath = getSaveGamePathForRead(gameInfo.filename); // open the save file XferLoad xferLoad; @@ -740,6 +741,15 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) } +//------------------------------------------------------------------------------------------------- +static void showQueuedSaveGameLoadFailure( void ) +{ + UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailedTitle", L"CANNOT LOAD SAVE"); + UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailed", L"The saved game file could not be opened or is invalid."); + + MessageBoxOk(title, body, nullptr); +} + // ------------------------------------------------------------------------------------------------ /** Load the save game requested on startup, after the shell has been initialized */ // ------------------------------------------------------------------------------------------------ @@ -752,24 +762,30 @@ void GameState::loadQueuedSaveGame() TheWritableGlobalData->m_loadSaveGame.clear(); + if( gameInfo.filename.endsWithNoCase( SAVE_GAME_EXTENSION ) == FALSE ) + { + DEBUG_LOG(("Save game '%s' is not a save game file", gameInfo.filename.str())); + showQueuedSaveGameLoadFailure(); + return; + } + // getSaveGameInfoFromFile throws when the file is missing, so check before reading it if( doesSaveGameExist( gameInfo.filename ) == FALSE ) { DEBUG_LOG(("Save game '%s' was not found", gameInfo.filename.str())); - TheGameEngine->setQuitting( TRUE ); + showQueuedSaveGameLoadFailure(); return; } // getSaveGameInfoFromFile throws on a malformed file instead of returning a SaveCode try { - AsciiString filepath = getFilePathInSaveDirectory( gameInfo.filename ); - getSaveGameInfoFromFile( filepath, &gameInfo.saveGameInfo ); + getSaveGameInfoFromFile( gameInfo.filename, &gameInfo.saveGameInfo ); } catch( ... ) { DEBUG_LOG(("Save game '%s' could not be read", gameInfo.filename.str())); - TheGameEngine->setQuitting( TRUE ); + showQueuedSaveGameLoadFailure(); return; } @@ -802,6 +818,17 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const return tmp; } +//------------------------------------------------------------------------------------------------- +AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const +{ + if (isAbsolutePath(filenameOrPath.str())) + { + return filenameOrPath; + } + + return getFilePathInSaveDirectory(filenameOrPath); +} + //------------------------------------------------------------------------------------------------- Bool GameState::isInSaveDirectory(const AsciiString& path) const { @@ -959,8 +986,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const Bool GameState::doesSaveGameExist( AsciiString filename ) { - // construct full path to file - AsciiString filepath = getFilePathInSaveDirectory(filename); + AsciiString filepath = getSaveGamePathForRead(filename); // open file XferLoad xfer; @@ -1005,6 +1031,8 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav } + filename = getSaveGamePathForRead( filename ); + // open file for partial loading XferLoad xferLoad; xferLoad.open( filename ); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h index 565aedbdfef..073f14f9f99 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameState.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameState.h @@ -191,6 +191,7 @@ class GameState : public SubsystemInterface, AsciiString getSaveDirectory() const; AsciiString getFilePathInSaveDirectory(const AsciiString& leaf) const; + AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const; Bool isInSaveDirectory(const AsciiString& path) const; AsciiString realMapPathToPortableMapPath(const AsciiString& in) const; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 47f399fa731..ab1029c812c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -48,6 +48,7 @@ #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" @@ -57,6 +58,7 @@ #include "GameLogic/ScriptEngine.h" #include "GameLogic/SidesList.h" #include "GameLogic/TerrainLogic.h" +#include "Lib/PathUtil.h" // PUBLIC DATA //////////////////////////////////////////////////////////////////////////////////// @@ -656,8 +658,7 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) // TheGameStateMap->clearScratchPadMaps(); - // construct path to file - AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename); + AsciiString filepath = getSaveGamePathForRead(gameInfo.filename); // open the save file XferLoad xferLoad; @@ -740,6 +741,15 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo ) } +//------------------------------------------------------------------------------------------------- +static void showQueuedSaveGameLoadFailure( void ) +{ + UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailedTitle", L"CANNOT LOAD SAVE"); + UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailed", L"The saved game file could not be opened or is invalid."); + + MessageBoxOk(title, body, nullptr); +} + // ------------------------------------------------------------------------------------------------ /** Load the save game requested on startup, after the shell has been initialized */ // ------------------------------------------------------------------------------------------------ @@ -752,24 +762,30 @@ void GameState::loadQueuedSaveGame() TheWritableGlobalData->m_loadSaveGame.clear(); + if( gameInfo.filename.endsWithNoCase( SAVE_GAME_EXTENSION ) == FALSE ) + { + DEBUG_LOG(("Save game '%s' is not a save game file", gameInfo.filename.str())); + showQueuedSaveGameLoadFailure(); + return; + } + // getSaveGameInfoFromFile throws when the file is missing, so check before reading it if( doesSaveGameExist( gameInfo.filename ) == FALSE ) { DEBUG_LOG(("Save game '%s' was not found", gameInfo.filename.str())); - TheGameEngine->setQuitting( TRUE ); + showQueuedSaveGameLoadFailure(); return; } // getSaveGameInfoFromFile throws on a malformed file instead of returning a SaveCode try { - AsciiString filepath = getFilePathInSaveDirectory( gameInfo.filename ); - getSaveGameInfoFromFile( filepath, &gameInfo.saveGameInfo ); + getSaveGameInfoFromFile( gameInfo.filename, &gameInfo.saveGameInfo ); } catch( ... ) { DEBUG_LOG(("Save game '%s' could not be read", gameInfo.filename.str())); - TheGameEngine->setQuitting( TRUE ); + showQueuedSaveGameLoadFailure(); return; } @@ -802,6 +818,17 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const return tmp; } +//------------------------------------------------------------------------------------------------- +AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const +{ + if (isAbsolutePath(filenameOrPath.str())) + { + return filenameOrPath; + } + + return getFilePathInSaveDirectory(filenameOrPath); +} + //------------------------------------------------------------------------------------------------- Bool GameState::isInSaveDirectory(const AsciiString& path) const { @@ -959,8 +986,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const Bool GameState::doesSaveGameExist( AsciiString filename ) { - // construct full path to file - AsciiString filepath = getFilePathInSaveDirectory(filename); + AsciiString filepath = getSaveGamePathForRead(filename); // open file XferLoad xfer; @@ -1005,6 +1031,8 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav } + filename = getSaveGamePathForRead( filename ); + // open file for partial loading XferLoad xferLoad; xferLoad.open( filename );