diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 0daeb20e9ff..daab043a0f9 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -751,6 +751,20 @@ Int parseLoadSave(char *args[], int num) return 1; } +Int parseLoadReplay(char *args[], int num) +{ + if (num > 1) + { + TheWritableGlobalData->m_loadReplayGame = args[1]; + TheWritableGlobalData->m_shellMapOn = FALSE; + TheWritableGlobalData->m_playIntro = FALSE; + TheWritableGlobalData->m_playSizzle = FALSE; + + return 2; + } + return 1; +} + //============================================================================= //============================================================================= @@ -1201,6 +1215,9 @@ static CommandLineParam paramsForEngineInit[] = // TheSuperHackers @feature bobtista 22/07/2026 Load a save game file from the command line. { "-loadsave", parseLoadSave }, + // TheSuperHackers @feature bobtista 08/08/2026 Play a replay file from the command line. + { "-loadreplay", parseLoadReplay }, + // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. { "-forcefullviewport", parseFullViewport }, diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index c00862a980b..6b52265f66e 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -354,6 +354,7 @@ class GlobalData : public SubsystemInterface 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, play a replay file from the command-line std::vector 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 diff --git a/Generals/Code/GameEngine/Include/Common/Recorder.h b/Generals/Code/GameEngine/Include/Common/Recorder.h index cb96ac8ccba..9902a18c009 100644 --- a/Generals/Code/GameEngine/Include/Common/Recorder.h +++ b/Generals/Code/GameEngine/Include/Common/Recorder.h @@ -99,6 +99,7 @@ class RecorderClass : public SubsystemInterface // Methods dealing with playback. void updatePlayback(); ///< The update function for playing back a file. Bool playbackFile(AsciiString filename); ///< Starts playback of the specified file. + void loadQueuedReplay(); ///< Play the replay file requested on startup. Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version. static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version. AsciiString getCurrentReplayFilename(); ///< valid during playback only @@ -117,7 +118,6 @@ class RecorderClass : public SubsystemInterface struct ReplayHeader { AsciiString filename; - Bool forPlayback; UnicodeString replayName; SYSTEMTIME timeVal; UnicodeString versionString; @@ -134,13 +134,14 @@ class RecorderClass : public SubsystemInterface AsciiString gameOptions; Int localPlayerIndex; }; - Bool readReplayHeader( ReplayHeader& header ); + Bool readReplayHeader( ReplayHeader& header, const AsciiString& filename, Bool forPlayback ); RecorderModeType getMode(); ///< Returns the current operating mode. Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; } void initControls(); ///< Show or Hide the Replay controls static AsciiString getReplayDir(); ///< Returns the directory that holds the replay files. + static AsciiString getReplayPathForRead(const AsciiString& filenameOrPath); ///< Returns the path to open for a replay filename or absolute replay path. static AsciiString getReplayArchiveDir(); ///< Returns the directory that holds the archived replay files. static AsciiString getReplayExtention(); ///< Returns the file extention for replay files. static AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay. diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index 7a955895827..91c44c437c8 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -32,6 +32,8 @@ #include "Common/GlobalData.h" #include "Common/GameEngine.h" #include "GameClient/ClientInstance.h" +#include "GameClient/MapUtil.h" +#include "GameClient/MessageBox.h" #include "GameClient/GameWindow.h" #include "GameClient/GameWindowManager.h" #include "GameClient/InGameUI.h" @@ -47,6 +49,7 @@ #include "Common/CRCDebug.h" #include "Common/OptionPreferences.h" #include "Common/version.h" +#include "Lib/PathUtil.h" constexpr const char s_genrep[] = "GENREP"; constexpr const UnsignedInt replayBufferBytes = 8192; @@ -845,18 +848,17 @@ void RecorderClass::writeArgument(GameMessageArgumentDataType type, const GameMe * Read in a replay header, for (1) populating a replay listbox or (2) starting playback. In * case (2), set FILE *m_file. */ -Bool RecorderClass::readReplayHeader(ReplayHeader& header) +Bool RecorderClass::readReplayHeader(ReplayHeader& header, const AsciiString& filename, Bool forPlayback) { - AsciiString filepath = getReplayDir(); - filepath.concat(header.filename.str()); + header.filename = getReplayPathForRead(filename); // TheSuperHackers @performance More buffered data reduces disk overhead and will improve fast forward playback - const UnsignedInt buffersize = header.forPlayback ? replayBufferBytes : File::BUFFERSIZE; - m_file = TheFileSystem->openFile(filepath.str(), File::READ | File::BINARY, buffersize); + const UnsignedInt buffersize = forPlayback ? replayBufferBytes : File::BUFFERSIZE; + m_file = TheFileSystem->openFile(header.filename.str(), File::READ | File::BINARY, buffersize); if (m_file == nullptr) { - DEBUG_LOG(("Can't open %s (%s)", filepath.str(), header.filename.str())); + DEBUG_LOG(("Can't open %s (%s)", header.filename.str(), filename.str())); return FALSE; } @@ -930,7 +932,7 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header) m_gameInfo.setLocalIP(localIP); } - if (!header.forPlayback) + if (!forPlayback) { m_gameInfo.endGame(); m_gameInfo.reset(); @@ -1047,9 +1049,7 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f Bool RecorderClass::replayMatchesGameVersion(AsciiString filename) { ReplayHeader header; - header.forPlayback = TRUE; - header.filename = filename; - if ( readReplayHeader( header ) ) + if ( readReplayHeader( header, filename, TRUE ) ) { return replayMatchesGameVersion( header ); } @@ -1070,6 +1070,61 @@ Bool RecorderClass::replayMatchesGameVersion(const ReplayHeader& header) return true; } +static void showQueuedReplayLoadFailure() +{ + UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayLoadFailedTitle", L"REPLAY CANNOT BE LOADED"); + UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayLoadFailed", L"The replay file could not be opened or is invalid."); + + MessageBoxOk(title, body, nullptr); +} + +static void showQueuedReplayMapNotFound() +{ + UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFoundTitle", L"MAP NOT FOUND"); + UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFound", L"This replay cannot be loaded because the map was not found on this device."); + + MessageBoxOk(title, body, nullptr); +} + +/** + * Play the replay requested on startup, after the shell has been initialized + */ +void RecorderClass::loadQueuedReplay() +{ + const AsciiString filename = TheGlobalData->m_loadReplayGame; + TheWritableGlobalData->m_loadReplayGame.clear(); + + ReplayHeader header; + if (!readReplayHeader(header, filename, FALSE)) + { + DEBUG_LOG(("Replay '%s' could not be read", filename.str())); + showQueuedReplayLoadFailure(); + return; + } + + // A replay whose map is missing starts a game that cannot load, so reject it here instead + ReplayGameInfo gameInfo; + if (!ParseAsciiStringToGameInfo(&gameInfo, header.gameOptions)) + { + DEBUG_LOG(("Replay '%s' contains invalid game options", filename.str())); + showQueuedReplayLoadFailure(); + return; + } + + if (TheMapCache == nullptr || TheMapCache->findMap(gameInfo.getMap()) == nullptr) + { + DEBUG_LOG(("Replay '%s' requires unavailable map '%s'", filename.str(), gameInfo.getMap().str())); + showQueuedReplayMapNotFound(); + return; + } + + if (!playbackFile(filename)) + { + DEBUG_LOG(("Failed to play replay '%s'", filename.str())); + showQueuedReplayLoadFailure(); + } +} + /** * Start playback of the file. Return true or false depending on if the file is * a valid replay file or not. @@ -1085,9 +1140,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) } ReplayHeader header; - header.forPlayback = TRUE; - header.filename = filename; - Bool success = readReplayHeader( header ); + Bool success = readReplayHeader( header, filename, TRUE ); if (!success) { return FALSE; @@ -1555,6 +1608,21 @@ AsciiString RecorderClass::getReplayDir() return tmp; } +/** + * returns the path to open for a replay filename in the replay directory or an absolute replay path. + */ +AsciiString RecorderClass::getReplayPathForRead(const AsciiString& filenameOrPath) +{ + if (isAbsolutePath(filenameOrPath.str())) + { + return filenameOrPath; + } + + AsciiString tmp = getReplayDir(); + tmp.concat(filenameOrPath); + return tmp; +} + /** * returns the directory that holds the archived replay files. */ diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 4579369240b..930e0618815 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -107,10 +107,7 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index) static Bool readReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader &header, ReplayGameInfo &info, const MapMetaData *&mapData) { - header.forPlayback = FALSE; - header.filename = filename; - - if (TheRecorder != nullptr && TheRecorder->readReplayHeader(header)) + if (TheRecorder != nullptr && TheRecorder->readReplayHeader(header, filename, FALSE)) { if (ParseAsciiStringToGameInfo(&info, header.gameOptions)) { diff --git a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp index d556d8b4855..7fc03e4e0ea 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -35,6 +35,7 @@ #include "Common/ActionManager.h" #include "Common/GameEngine.h" #include "Common/GameState.h" +#include "Common/Recorder.h" #include "Common/GameUtility.h" #include "Common/GlobalData.h" #include "Common/PerfTimer.h" @@ -520,6 +521,10 @@ void GameClient::update() { TheGameState->loadQueuedSaveGame(); } + else if (TheGlobalData->m_loadReplayGame.isNotEmpty()) + { + TheRecorder->loadQueuedReplay(); + } } } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 92761fd3c33..bbdc79e6183 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -355,6 +355,7 @@ class GlobalData : public SubsystemInterface 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, play a replay file from the command-line std::vector 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 diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index fba73ad5ec3..2149dd2caf0 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -99,6 +99,7 @@ class RecorderClass : public SubsystemInterface // Methods dealing with playback. void updatePlayback(); ///< The update function for playing back a file. Bool playbackFile(AsciiString filename); ///< Starts playback of the specified file. + void loadQueuedReplay(); ///< Play the replay file requested on startup. Bool replayMatchesGameVersion(AsciiString filename); ///< Returns true if the playback is a valid playback file for this version. static Bool replayMatchesGameVersion(const ReplayHeader& header); ///< Returns true if the playback is a valid playback file for this version. AsciiString getCurrentReplayFilename(); ///< valid during playback only @@ -117,7 +118,6 @@ class RecorderClass : public SubsystemInterface struct ReplayHeader { AsciiString filename; - Bool forPlayback; UnicodeString replayName; SYSTEMTIME timeVal; UnicodeString versionString; @@ -134,13 +134,14 @@ class RecorderClass : public SubsystemInterface AsciiString gameOptions; Int localPlayerIndex; }; - Bool readReplayHeader( ReplayHeader& header ); + Bool readReplayHeader( ReplayHeader& header, const AsciiString& filename, Bool forPlayback ); RecorderModeType getMode(); ///< Returns the current operating mode. Bool isPlaybackMode() const { return m_mode == RECORDERMODETYPE_PLAYBACK || m_mode == RECORDERMODETYPE_SIMULATION_PLAYBACK; } void initControls(); ///< Show or Hide the Replay controls static AsciiString getReplayDir(); ///< Returns the directory that holds the replay files. + static AsciiString getReplayPathForRead(const AsciiString& filenameOrPath); ///< Returns the path to open for a replay filename or absolute replay path. static AsciiString getReplayArchiveDir(); ///< Returns the directory that holds the archived replay files. static AsciiString getReplayExtention(); ///< Returns the file extention for replay files. static AsciiString getLastReplayFileName(); ///< Returns the filename used for the default replay. diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 9d71eb45b1b..8b9ecc5acbb 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -32,6 +32,8 @@ #include "Common/GlobalData.h" #include "Common/GameEngine.h" #include "GameClient/ClientInstance.h" +#include "GameClient/MapUtil.h" +#include "GameClient/MessageBox.h" #include "GameClient/GameWindow.h" #include "GameClient/GameWindowManager.h" #include "GameClient/InGameUI.h" @@ -47,6 +49,7 @@ #include "Common/CRCDebug.h" #include "Common/OptionPreferences.h" #include "Common/version.h" +#include "Lib/PathUtil.h" constexpr const char s_genrep[] = "GENREP"; constexpr const UnsignedInt replayBufferBytes = 8192; @@ -847,18 +850,17 @@ void RecorderClass::writeArgument(GameMessageArgumentDataType type, const GameMe * Read in a replay header, for (1) populating a replay listbox or (2) starting playback. In * case (2), set FILE *m_file. */ -Bool RecorderClass::readReplayHeader(ReplayHeader& header) +Bool RecorderClass::readReplayHeader(ReplayHeader& header, const AsciiString& filename, Bool forPlayback) { - AsciiString filepath = getReplayDir(); - filepath.concat(header.filename.str()); + header.filename = getReplayPathForRead(filename); // TheSuperHackers @performance More buffered data reduces disk overhead and will improve fast forward playback - const UnsignedInt buffersize = header.forPlayback ? replayBufferBytes : File::BUFFERSIZE; - m_file = TheFileSystem->openFile(filepath.str(), File::READ | File::BINARY, buffersize); + const UnsignedInt buffersize = forPlayback ? replayBufferBytes : File::BUFFERSIZE; + m_file = TheFileSystem->openFile(header.filename.str(), File::READ | File::BINARY, buffersize); if (m_file == nullptr) { - DEBUG_LOG(("Can't open %s (%s)", filepath.str(), header.filename.str())); + DEBUG_LOG(("Can't open %s (%s)", header.filename.str(), filename.str())); return FALSE; } @@ -932,7 +934,7 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header) m_gameInfo.setLocalIP(localIP); } - if (!header.forPlayback) + if (!forPlayback) { m_gameInfo.endGame(); m_gameInfo.reset(); @@ -1050,9 +1052,7 @@ void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool f Bool RecorderClass::replayMatchesGameVersion(AsciiString filename) { ReplayHeader header; - header.forPlayback = TRUE; - header.filename = filename; - if ( readReplayHeader( header ) ) + if ( readReplayHeader( header, filename, TRUE ) ) { return replayMatchesGameVersion( header ); } @@ -1073,6 +1073,61 @@ Bool RecorderClass::replayMatchesGameVersion(const ReplayHeader& header) return true; } +static void showQueuedReplayLoadFailure() +{ + UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayLoadFailedTitle", L"REPLAY CANNOT BE LOADED"); + UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayLoadFailed", L"The replay file could not be opened or is invalid."); + + MessageBoxOk(title, body, nullptr); +} + +static void showQueuedReplayMapNotFound() +{ + UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFoundTitle", L"MAP NOT FOUND"); + UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFound", L"This replay cannot be loaded because the map was not found on this device."); + + MessageBoxOk(title, body, nullptr); +} + +/** + * Play the replay requested on startup, after the shell has been initialized + */ +void RecorderClass::loadQueuedReplay() +{ + const AsciiString filename = TheGlobalData->m_loadReplayGame; + TheWritableGlobalData->m_loadReplayGame.clear(); + + ReplayHeader header; + if (!readReplayHeader(header, filename, FALSE)) + { + DEBUG_LOG(("Replay '%s' could not be read", filename.str())); + showQueuedReplayLoadFailure(); + return; + } + + // A replay whose map is missing starts a game that cannot load, so reject it here instead + ReplayGameInfo gameInfo; + if (!ParseAsciiStringToGameInfo(&gameInfo, header.gameOptions)) + { + DEBUG_LOG(("Replay '%s' contains invalid game options", filename.str())); + showQueuedReplayLoadFailure(); + return; + } + + if (TheMapCache == nullptr || TheMapCache->findMap(gameInfo.getMap()) == nullptr) + { + DEBUG_LOG(("Replay '%s' requires unavailable map '%s'", filename.str(), gameInfo.getMap().str())); + showQueuedReplayMapNotFound(); + return; + } + + if (!playbackFile(filename)) + { + DEBUG_LOG(("Failed to play replay '%s'", filename.str())); + showQueuedReplayLoadFailure(); + } +} + /** * Start playback of the file. Return true or false depending on if the file is * a valid replay file or not. @@ -1088,9 +1143,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) } ReplayHeader header; - header.forPlayback = TRUE; - header.filename = filename; - Bool success = readReplayHeader( header ); + Bool success = readReplayHeader( header, filename, TRUE ); if (!success) { return FALSE; @@ -1558,6 +1611,21 @@ AsciiString RecorderClass::getReplayDir() return tmp; } +/** + * returns the path to open for a replay filename in the replay directory or an absolute replay path. + */ +AsciiString RecorderClass::getReplayPathForRead(const AsciiString& filenameOrPath) +{ + if (isAbsolutePath(filenameOrPath.str())) + { + return filenameOrPath; + } + + AsciiString tmp = getReplayDir(); + tmp.concat(filenameOrPath); + return tmp; +} + /** * returns the directory that holds the archived replay files. */ diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp index 4188d26ba91..4aafd7330e2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -107,10 +107,7 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index) static Bool readReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader &header, ReplayGameInfo &info, const MapMetaData *&mapData) { - header.forPlayback = FALSE; - header.filename = filename; - - if (TheRecorder != nullptr && TheRecorder->readReplayHeader(header)) + if (TheRecorder != nullptr && TheRecorder->readReplayHeader(header, filename, FALSE)) { if (ParseAsciiStringToGameInfo(&info, header.gameOptions)) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index af5f407209b..e3d83bb7323 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -35,6 +35,7 @@ #include "Common/ActionManager.h" #include "Common/GameEngine.h" #include "Common/GameState.h" +#include "Common/Recorder.h" #include "Common/GameUtility.h" #include "Common/GlobalData.h" #include "Common/PerfTimer.h" @@ -541,6 +542,10 @@ void GameClient::update() { TheGameState->loadQueuedSaveGame(); } + else if (TheGlobalData->m_loadReplayGame.isNotEmpty()) + { + TheRecorder->loadQueuedReplay(); + } } }