diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index 264193f8126..a6f51ade405 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -1088,8 +1088,6 @@ Bool RecorderClass::playbackFile(AsciiString filename) } } - m_mode = RECORDERMODETYPE_PLAYBACK; - ReplayHeader header; header.forPlayback = TRUE; header.filename = filename; @@ -1147,8 +1145,6 @@ Bool RecorderClass::playbackFile(AsciiString filename) DEBUG_ASSERTCRASH(!exeDifferent && !iniDifferent, (debugString.str())); #endif - TheWritableGlobalData->m_pendingFile = m_gameInfo.getMap(); - #ifdef DEBUG_LOGGING if (header.localPlayerIndex >= 0) { @@ -1181,6 +1177,13 @@ Bool RecorderClass::playbackFile(AsciiString filename) TheCommandList->reset(); readNextFrame(); + // readNextFrame() closes m_file via stopPlayback() if the first frame cannot be read. + if(m_file == nullptr) + { + return FALSE; + } + + TheWritableGlobalData->m_pendingFile = m_gameInfo.getMap(); // send a message to the logic for a new game if (!m_doingAnalysis) @@ -1199,6 +1202,11 @@ Bool RecorderClass::playbackFile(AsciiString filename) InitRandom( m_gameInfo.getSeed() ); } + // TheSuperHackers @bugfix bobtista 25/07/2026 Enter playback mode only once the playback is ready. + // Previously a failed open left the recorder in playback mode with a NULL m_file, and the next + // update dereferenced it, for example when the replay is deleted during the version mismatch prompt. + m_mode = RECORDERMODETYPE_PLAYBACK; + m_currentReplayFilename = filename; m_playbackFrameCount = header.frameCount; return TRUE; 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 4b02271bfb7..4579369240b 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -548,6 +548,27 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } +static void handleReplayLoadFailure() +{ + 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); + + GadgetListBoxReset(listboxReplayFiles); + PopulateReplayFileListbox(listboxReplayFiles); +} + +static void showReplayMapNotFound() +{ + 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); +} + +//------------------------------------------------------------------------------------------------- + void reallyLoadReplay() { UnicodeString filename; @@ -564,11 +585,35 @@ void reallyLoadReplay() AsciiString asciiFilename; asciiFilename.translate(filename); - TheRecorder->playbackFile(asciiFilename); + // TheSuperHackers @bugfix bobtista 25/07/2026 Re-validate the replay before starting playback. + // The user can delete the file while the version mismatch prompt is open, in which case the + // listbox entry is stale. Prompts the same message box as loadReplay and refreshes the list. + RecorderClass::ReplayHeader header; + ReplayGameInfo info; + const MapMetaData *mapData; - if(parentReplayMenu != nullptr) + if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - parentReplayMenu->winHide(TRUE); + handleReplayLoadFailure(); + return; + } + + if(mapData == nullptr) + { + showReplayMapNotFound(); + return; + } + + if(TheRecorder->playbackFile(asciiFilename)) + { + if(parentReplayMenu != nullptr) + { + parentReplayMenu->winHide(TRUE); + } + } + else + { + handleReplayLoadFailure(); } } @@ -585,19 +630,13 @@ static void loadReplay(UnicodeString filename) { // TheSuperHackers @bugfix Prompts a message box when the replay was deleted by the user while the Replay Menu was opened. - UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayFileNotFoundTitle", L"REPLAY NOT FOUND"); - UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayFileNotFound", L"This replay cannot be loaded because the file no longer exists on this device."); - - MessageBoxOk(title, body, nullptr); + handleReplayLoadFailure(); } else if(mapData == nullptr) { // TheSuperHackers @bugfix Prompts a message box when the map used by the replay was not found. - 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); + showReplayMapNotFound(); } else if(!TheRecorder->replayMatchesGameVersion(header)) { @@ -607,11 +646,18 @@ static void loadReplay(UnicodeString filename) } else { - TheRecorder->playbackFile(asciiFilename); - - if(parentReplayMenu != nullptr) + // TheSuperHackers @bugfix bobtista 25/07/2026 Keep the Replay Menu open when the playback + // could not be started, for example when the replay was deleted after it was validated above. + if(TheRecorder->playbackFile(asciiFilename)) { - parentReplayMenu->winHide(TRUE); + if(parentReplayMenu != nullptr) + { + parentReplayMenu->winHide(TRUE); + } + } + else + { + handleReplayLoadFailure(); } } } @@ -826,4 +872,3 @@ void copyReplay() } } - diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 6e5791e5972..2d9bfea3a16 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1091,8 +1091,6 @@ Bool RecorderClass::playbackFile(AsciiString filename) } } - m_mode = RECORDERMODETYPE_PLAYBACK; - ReplayHeader header; header.forPlayback = TRUE; header.filename = filename; @@ -1150,8 +1148,6 @@ Bool RecorderClass::playbackFile(AsciiString filename) DEBUG_ASSERTCRASH(!exeDifferent && !iniDifferent, (debugString.str())); #endif - TheWritableGlobalData->m_pendingFile = m_gameInfo.getMap(); - #ifdef DEBUG_LOGGING if (header.localPlayerIndex >= 0) { @@ -1184,6 +1180,13 @@ Bool RecorderClass::playbackFile(AsciiString filename) TheCommandList->reset(); readNextFrame(); + // readNextFrame() closes m_file via stopPlayback() if the first frame cannot be read. + if(m_file == nullptr) + { + return FALSE; + } + + TheWritableGlobalData->m_pendingFile = m_gameInfo.getMap(); // send a message to the logic for a new game if (!m_doingAnalysis) @@ -1202,6 +1205,11 @@ Bool RecorderClass::playbackFile(AsciiString filename) InitRandom( m_gameInfo.getSeed() ); } + // TheSuperHackers @bugfix bobtista 25/07/2026 Enter playback mode only once the playback is ready. + // Previously a failed open left the recorder in playback mode with a NULL m_file, and the next + // update dereferenced it, for example when the replay is deleted during the version mismatch prompt. + m_mode = RECORDERMODETYPE_PLAYBACK; + m_currentReplayFilename = filename; m_playbackFrameCount = header.frameCount; return TRUE; 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 4fc9577f0de..4188d26ba91 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -548,6 +548,27 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } +static void handleReplayLoadFailure() +{ + 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); + + GadgetListBoxReset(listboxReplayFiles); + PopulateReplayFileListbox(listboxReplayFiles); +} + +static void showReplayMapNotFound() +{ + 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); +} + +//------------------------------------------------------------------------------------------------- + void reallyLoadReplay() { UnicodeString filename; @@ -564,11 +585,35 @@ void reallyLoadReplay() AsciiString asciiFilename; asciiFilename.translate(filename); - TheRecorder->playbackFile(asciiFilename); + // TheSuperHackers @bugfix bobtista 25/07/2026 Re-validate the replay before starting playback. + // The user can delete the file while the version mismatch prompt is open, in which case the + // listbox entry is stale. Prompts the same message box as loadReplay and refreshes the list. + RecorderClass::ReplayHeader header; + ReplayGameInfo info; + const MapMetaData *mapData; - if(parentReplayMenu != nullptr) + if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - parentReplayMenu->winHide(TRUE); + handleReplayLoadFailure(); + return; + } + + if(mapData == nullptr) + { + showReplayMapNotFound(); + return; + } + + if(TheRecorder->playbackFile(asciiFilename)) + { + if(parentReplayMenu != nullptr) + { + parentReplayMenu->winHide(TRUE); + } + } + else + { + handleReplayLoadFailure(); } } @@ -585,19 +630,13 @@ static void loadReplay(UnicodeString filename) { // TheSuperHackers @bugfix Prompts a message box when the replay was deleted by the user while the Replay Menu was opened. - UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayFileNotFoundTitle", L"REPLAY NOT FOUND"); - UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayFileNotFound", L"This replay cannot be loaded because the file no longer exists on this device."); - - MessageBoxOk(title, body, nullptr); + handleReplayLoadFailure(); } else if(mapData == nullptr) { // TheSuperHackers @bugfix Prompts a message box when the map used by the replay was not found. - 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); + showReplayMapNotFound(); } else if(!TheRecorder->replayMatchesGameVersion(header)) { @@ -607,11 +646,18 @@ static void loadReplay(UnicodeString filename) } else { - TheRecorder->playbackFile(asciiFilename); - - if(parentReplayMenu != nullptr) + // TheSuperHackers @bugfix bobtista 25/07/2026 Keep the Replay Menu open when the playback + // could not be started, for example when the replay was deleted after it was validated above. + if(TheRecorder->playbackFile(asciiFilename)) { - parentReplayMenu->winHide(TRUE); + if(parentReplayMenu != nullptr) + { + parentReplayMenu->winHide(TRUE); + } + } + else + { + handleReplayLoadFailure(); } } } @@ -826,4 +872,3 @@ void copyReplay() } } -