Skip to content

Commit dec8476

Browse files
committed
feat(cli): Load save files from absolute paths
1 parent 45178ee commit dec8476

6 files changed

Lines changed: 112 additions & 17 deletions

File tree

Core/GameEngine/Source/Common/CommandLine.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,8 +727,10 @@ Int parseLoadSave(char *args[], int num)
727727
TheWritableGlobalData->m_shellMapOn = FALSE;
728728
TheWritableGlobalData->m_playIntro = FALSE;
729729
TheWritableGlobalData->m_playSizzle = FALSE;
730+
731+
return 2;
730732
}
731-
return 2;
733+
return 1;
732734
}
733735

734736
//=============================================================================

Core/Libraries/Include/Lib/PathUtil.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@
2323
#include "BaseType.h"
2424
#include <string.h>
2525

26+
// TheSuperHackers @feature bobtista 08/08/2026 Identify absolute paths so callers can distinguish
27+
// explicitly selected files from names relative to their managed directories.
28+
inline Bool isAbsolutePath(const char* path)
29+
{
30+
if (path == nullptr || path[0] == 0)
31+
{
32+
return FALSE;
33+
}
34+
35+
#ifdef _WIN32
36+
const Bool hasDriveLetter = (path[0] >= 'A' && path[0] <= 'Z') ||
37+
(path[0] >= 'a' && path[0] <= 'z');
38+
const Bool hasDriveRoot = hasDriveLetter && path[1] == ':' &&
39+
(path[2] == '\\' || path[2] == '/');
40+
const Bool hasCurrentDriveRoot = path[0] == '\\' || path[0] == '/';
41+
return hasDriveRoot || hasCurrentDriveRoot;
42+
#else
43+
return path[0] == '/';
44+
#endif
45+
}
46+
2647
inline const char* getExtension(const char* path)
2748
{
2849
const char* lastDot = strrchr(path, '.');

Generals/Code/GameEngine/Include/Common/GameState.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class GameState : public SubsystemInterface,
191191

192192
AsciiString getSaveDirectory() const;
193193
AsciiString getFilePathInSaveDirectory(const AsciiString& leaf) const;
194+
AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const;
194195
Bool isInSaveDirectory(const AsciiString& path) const;
195196

196197
AsciiString realMapPathToPortableMapPath(const AsciiString& in) const;

Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "GameClient/GameClient.h"
4949
#include "GameClient/GameText.h"
5050
#include "GameClient/MapUtil.h"
51+
#include "GameClient/MessageBox.h"
5152
#include "GameClient/InGameUI.h"
5253
#include "GameClient/ParticleSys.h"
5354
#include "GameClient/TerrainVisual.h"
@@ -57,6 +58,7 @@
5758
#include "GameLogic/ScriptEngine.h"
5859
#include "GameLogic/SidesList.h"
5960
#include "GameLogic/TerrainLogic.h"
61+
#include "Lib/PathUtil.h"
6062

6163

6264
// PUBLIC DATA ////////////////////////////////////////////////////////////////////////////////////
@@ -656,8 +658,9 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo )
656658
//
657659
TheGameStateMap->clearScratchPadMaps();
658660

659-
// construct path to file
660-
AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename);
661+
// Relative names come from the save menu. Absolute paths come from the command line
662+
// and are opened in place.
663+
AsciiString filepath = getSaveGamePathForRead(gameInfo.filename);
661664

662665
// open the save file
663666
XferLoad xferLoad;
@@ -740,6 +743,18 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo )
740743

741744
}
742745

746+
//-------------------------------------------------------------------------------------------------
747+
// TheSuperHackers @feature bobtista 29/08/2026 A save game requested on the command line can come
748+
// from a file association, where there is no console to print to. Report the failure in the shell
749+
// and leave the shell running so the player can carry on from the main menu.
750+
static void showQueuedSaveGameLoadFailure( void )
751+
{
752+
UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailedTitle", L"CANNOT LOAD SAVE");
753+
UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailed", L"The saved game file could not be opened or is invalid.");
754+
755+
MessageBoxOk(title, body, nullptr);
756+
}
757+
743758
// ------------------------------------------------------------------------------------------------
744759
/** Load the save game requested on startup, after the shell has been initialized */
745760
// ------------------------------------------------------------------------------------------------
@@ -752,24 +767,30 @@ void GameState::loadQueuedSaveGame()
752767

753768
TheWritableGlobalData->m_loadSaveGame.clear();
754769

770+
if( gameInfo.filename.endsWithNoCase( SAVE_GAME_EXTENSION ) == FALSE )
771+
{
772+
DEBUG_LOG(("Save game '%s' is not a save game file", gameInfo.filename.str()));
773+
showQueuedSaveGameLoadFailure();
774+
return;
775+
}
776+
755777
// getSaveGameInfoFromFile throws when the file is missing, so check before reading it
756778
if( doesSaveGameExist( gameInfo.filename ) == FALSE )
757779
{
758780
DEBUG_LOG(("Save game '%s' was not found", gameInfo.filename.str()));
759-
TheGameEngine->setQuitting( TRUE );
781+
showQueuedSaveGameLoadFailure();
760782
return;
761783
}
762784

763785
// getSaveGameInfoFromFile throws on a malformed file instead of returning a SaveCode
764786
try
765787
{
766-
AsciiString filepath = getFilePathInSaveDirectory( gameInfo.filename );
767-
getSaveGameInfoFromFile( filepath, &gameInfo.saveGameInfo );
788+
getSaveGameInfoFromFile( gameInfo.filename, &gameInfo.saveGameInfo );
768789
}
769790
catch( ... )
770791
{
771792
DEBUG_LOG(("Save game '%s' could not be read", gameInfo.filename.str()));
772-
TheGameEngine->setQuitting( TRUE );
793+
showQueuedSaveGameLoadFailure();
773794
return;
774795
}
775796

@@ -802,6 +823,19 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const
802823
return tmp;
803824
}
804825

826+
//-------------------------------------------------------------------------------------------------
827+
// TheSuperHackers @feature bobtista 08/08/2026 Open explicitly selected save paths in place while
828+
// preserving the managed Save directory for the relative names the menus use.
829+
AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const
830+
{
831+
if (isAbsolutePath(filenameOrPath.str()))
832+
{
833+
return filenameOrPath;
834+
}
835+
836+
return getFilePathInSaveDirectory(filenameOrPath);
837+
}
838+
805839
//-------------------------------------------------------------------------------------------------
806840
Bool GameState::isInSaveDirectory(const AsciiString& path) const
807841
{
@@ -959,8 +993,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const
959993
Bool GameState::doesSaveGameExist( AsciiString filename )
960994
{
961995

962-
// construct full path to file
963-
AsciiString filepath = getFilePathInSaveDirectory(filename);
996+
AsciiString filepath = getSaveGamePathForRead(filename);
964997

965998
// open file
966999
XferLoad xfer;
@@ -1005,6 +1038,8 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav
10051038

10061039
}
10071040

1041+
filename = getSaveGamePathForRead( filename );
1042+
10081043
// open file for partial loading
10091044
XferLoad xferLoad;
10101045
xferLoad.open( filename );

GeneralsMD/Code/GameEngine/Include/Common/GameState.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class GameState : public SubsystemInterface,
191191

192192
AsciiString getSaveDirectory() const;
193193
AsciiString getFilePathInSaveDirectory(const AsciiString& leaf) const;
194+
AsciiString getSaveGamePathForRead(const AsciiString& filenameOrPath) const;
194195
Bool isInSaveDirectory(const AsciiString& path) const;
195196

196197
AsciiString realMapPathToPortableMapPath(const AsciiString& in) const;

GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "GameClient/GameClient.h"
4949
#include "GameClient/GameText.h"
5050
#include "GameClient/MapUtil.h"
51+
#include "GameClient/MessageBox.h"
5152
#include "GameClient/InGameUI.h"
5253
#include "GameClient/ParticleSys.h"
5354
#include "GameClient/TerrainVisual.h"
@@ -57,6 +58,7 @@
5758
#include "GameLogic/ScriptEngine.h"
5859
#include "GameLogic/SidesList.h"
5960
#include "GameLogic/TerrainLogic.h"
61+
#include "Lib/PathUtil.h"
6062

6163

6264
// PUBLIC DATA ////////////////////////////////////////////////////////////////////////////////////
@@ -656,8 +658,9 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo )
656658
//
657659
TheGameStateMap->clearScratchPadMaps();
658660

659-
// construct path to file
660-
AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename);
661+
// Relative names come from the save menu. Absolute paths come from the command line
662+
// and are opened in place.
663+
AsciiString filepath = getSaveGamePathForRead(gameInfo.filename);
661664

662665
// open the save file
663666
XferLoad xferLoad;
@@ -740,6 +743,18 @@ SaveCode GameState::loadGame( AvailableGameInfo gameInfo )
740743

741744
}
742745

746+
//-------------------------------------------------------------------------------------------------
747+
// TheSuperHackers @feature bobtista 29/08/2026 A save game requested on the command line can come
748+
// from a file association, where there is no console to print to. Report the failure in the shell
749+
// and leave the shell running so the player can carry on from the main menu.
750+
static void showQueuedSaveGameLoadFailure( void )
751+
{
752+
UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailedTitle", L"CANNOT LOAD SAVE");
753+
UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SaveGameLoadFailed", L"The saved game file could not be opened or is invalid.");
754+
755+
MessageBoxOk(title, body, nullptr);
756+
}
757+
743758
// ------------------------------------------------------------------------------------------------
744759
/** Load the save game requested on startup, after the shell has been initialized */
745760
// ------------------------------------------------------------------------------------------------
@@ -752,24 +767,30 @@ void GameState::loadQueuedSaveGame()
752767

753768
TheWritableGlobalData->m_loadSaveGame.clear();
754769

770+
if( gameInfo.filename.endsWithNoCase( SAVE_GAME_EXTENSION ) == FALSE )
771+
{
772+
DEBUG_LOG(("Save game '%s' is not a save game file", gameInfo.filename.str()));
773+
showQueuedSaveGameLoadFailure();
774+
return;
775+
}
776+
755777
// getSaveGameInfoFromFile throws when the file is missing, so check before reading it
756778
if( doesSaveGameExist( gameInfo.filename ) == FALSE )
757779
{
758780
DEBUG_LOG(("Save game '%s' was not found", gameInfo.filename.str()));
759-
TheGameEngine->setQuitting( TRUE );
781+
showQueuedSaveGameLoadFailure();
760782
return;
761783
}
762784

763785
// getSaveGameInfoFromFile throws on a malformed file instead of returning a SaveCode
764786
try
765787
{
766-
AsciiString filepath = getFilePathInSaveDirectory( gameInfo.filename );
767-
getSaveGameInfoFromFile( filepath, &gameInfo.saveGameInfo );
788+
getSaveGameInfoFromFile( gameInfo.filename, &gameInfo.saveGameInfo );
768789
}
769790
catch( ... )
770791
{
771792
DEBUG_LOG(("Save game '%s' could not be read", gameInfo.filename.str()));
772-
TheGameEngine->setQuitting( TRUE );
793+
showQueuedSaveGameLoadFailure();
773794
return;
774795
}
775796

@@ -802,6 +823,19 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const
802823
return tmp;
803824
}
804825

826+
//-------------------------------------------------------------------------------------------------
827+
// TheSuperHackers @feature bobtista 08/08/2026 Open explicitly selected save paths in place while
828+
// preserving the managed Save directory for the relative names the menus use.
829+
AsciiString GameState::getSaveGamePathForRead(const AsciiString& filenameOrPath) const
830+
{
831+
if (isAbsolutePath(filenameOrPath.str()))
832+
{
833+
return filenameOrPath;
834+
}
835+
836+
return getFilePathInSaveDirectory(filenameOrPath);
837+
}
838+
805839
//-------------------------------------------------------------------------------------------------
806840
Bool GameState::isInSaveDirectory(const AsciiString& path) const
807841
{
@@ -959,8 +993,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const
959993
Bool GameState::doesSaveGameExist( AsciiString filename )
960994
{
961995

962-
// construct full path to file
963-
AsciiString filepath = getFilePathInSaveDirectory(filename);
996+
AsciiString filepath = getSaveGamePathForRead(filename);
964997

965998
// open file
966999
XferLoad xfer;
@@ -1005,6 +1038,8 @@ void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *sav
10051038

10061039
}
10071040

1041+
filename = getSaveGamePathForRead( filename );
1042+
10081043
// open file for partial loading
10091044
XferLoad xferLoad;
10101045
xferLoad.open( filename );

0 commit comments

Comments
 (0)