Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Core/GameEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Core/GameEngine/Include/Common/FileSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class FileSystem : public SubsystemInterface

Bool createDirectory(AsciiString directory); ///< create a directory of the given name.

static Bool isAbsolutePath(const AsciiString& path); ///< determines if a path is absolute on the current platform.
static AsciiString normalizePath(const AsciiString& path); ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure.
static Bool isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath); ///< determines if a file path is within a base path. Both paths must be absolute, but do not need to exist.

Expand Down
24 changes: 24 additions & 0 deletions Core/GameEngine/Include/GameClient/SaveLoadFeedback.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "Common/GameState.h"

void presentSaveResult( const SaveResult &result );
void presentLoadResult( SaveCode result, const AsciiString &filename );
45 changes: 45 additions & 0 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,47 @@ Int parseVTune ( char *args[], int num )

#endif // defined(RTS_DEBUG)

// TheSuperHackers @feature bobtista 22/07/2026 Load a save game file from the command line.
Int parseLoadSave(char *args[], int num)
{
if (num > 1)
{
AsciiString filename = args[1];
if (!filename.endsWithNoCase(".sav"))
{
printf("Invalid save game name \"%s\"\n", filename.str());
exit(1);
}

TheWritableGlobalData->m_loadSaveGame = filename;
TheWritableGlobalData->m_playIntro = FALSE;
TheWritableGlobalData->m_playSizzle = FALSE;
return 2;
}
return 1;
}

// TheSuperHackers @feature bobtista 08/08/2026 Load a replay visually from the command line.
Int parseLoadReplay(char *args[], int num)
{
if (num > 1)
{
AsciiString filename = args[1];
if (!filename.endsWithNoCase(RecorderClass::getReplayExtention()))
{
printf("Invalid replay name \"%s\"\n", filename.str());
exit(1);
}

TheWritableGlobalData->m_loadReplayGame = filename;
TheWritableGlobalData->m_playIntro = FALSE;
TheWritableGlobalData->m_playSizzle = FALSE;
return 2;
}

return 1;
}

//=============================================================================
//=============================================================================

Expand Down Expand Up @@ -1159,6 +1200,10 @@ static CommandLineParam paramsForEngineInit[] =
{ "-noshaders", parseNoShaders },
{ "-quickstart", parseQuickStart },
{ "-useWaveEditor", parseUseWaveEditor },
{ "-loadsave", parseLoadSave },
{ "-loadreplay", parseLoadReplay },
// TheSuperHackers @feature bobtista 08/08/2026 Allow diagnostic replay playback to continue after CRC mismatches.
{ "-ignoreReplaySyncErrors", parseSync },

// TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it.
{ "-forcefullviewport", parseFullViewport },
Expand Down
25 changes: 25 additions & 0 deletions Core/GameEngine/Source/Common/System/FileSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,31 @@ Bool FileSystem::createDirectory(AsciiString directory)
return FALSE;
}

//============================================================================
// FileSystem::isAbsolutePath
//============================================================================
// TheSuperHackers @feature bobtista 08/08/2026 Identify absolute paths so callers can distinguish
// explicitly selected files from names relative to their managed directories.
Bool FileSystem::isAbsolutePath(const AsciiString& path)
{
const Char *value = path.str();
if (value == nullptr || value[0] == 0)
{
return FALSE;
}

#ifdef _WIN32
const Bool hasDriveLetter = (value[0] >= 'A' && value[0] <= 'Z') ||
(value[0] >= 'a' && value[0] <= 'z');
const Bool hasDriveRoot = hasDriveLetter && value[1] == ':' &&
(value[2] == '\\' || value[2] == '/');
const Bool hasCurrentDriveRoot = value[0] == '\\' || value[0] == '/';
return hasDriveRoot || hasCurrentDriveRoot;
#else
return value[0] == '/';
#endif
}

//============================================================================
// FileSystem::normalizePath
//============================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
** 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 <http://www.gnu.org/licenses/>.
*/

#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:
{
break;
}
}
}

void presentLoadResult( SaveCode result, const AsciiString &filename )
{
if( result == SC_INVALID_DATA )
{
UnicodeString msg;
msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), getUnicodeSavePath(filename).str() );
MessageBoxOk( TheGameText->fetch("GUI:Error"), msg, nullptr );
}
}
18 changes: 14 additions & 4 deletions Generals/Code/GameEngine/Include/Common/GameState.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ enum SaveCode CPP_11(: Int)
SC_ERROR,
};

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,
Expand All @@ -156,11 +165,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; }

Expand All @@ -170,7 +179,7 @@ class GameState : public SubsystemInterface,
// manipulating files
Bool doesSaveGameExist( AsciiString filename ); ///< does the save file exist
void populateSaveGameListbox( GameWindow *listbox, SaveLoadLayoutType layoutType ); ///< populate listbox with available save games
void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from file
void getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo ); ///< get save game info from a Save-directory name or absolute path

void friend_xferSaveDataForCRC( Xfer *xfer, SnapshotType which ); ///< This should only be called to DeepCRC sanity checking

Expand All @@ -181,6 +190,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;
Expand Down
2 changes: 2 additions & 0 deletions Generals/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ class GlobalData : public SubsystemInterface
Bool m_buildMapCache;
AsciiString m_initialFile; ///< If this is specified, load a specific map from the command-line
AsciiString m_pendingFile; ///< If this is specified, use this map at the next game start
AsciiString m_loadSaveGame; ///< If this is specified, load a save game file from the command-line
AsciiString m_loadReplayGame; ///< If this is specified, load a replay file from the command-line

std::vector<AsciiString> m_simulateReplays; ///< If not empty, simulate this list of replays and exit.
Int m_simulateReplayJobs; ///< Maximum number of processes to use for simulation, or SIMULATE_REPLAYS_SEQUENTIAL for sequential simulation
Expand Down
Loading
Loading