From 8fccab4dd2e05e9539e31eeeb9abc0159bc3497c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 12:42:49 +0100 Subject: [PATCH 01/12] bugfix(replay): Handle replay file deleted while the version mismatch prompt is open --- .../GameEngine/Source/Common/Recorder.cpp | 7 +++-- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 6e5791e5972..7b86944051c 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; @@ -1202,6 +1200,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..6ed39aec0dd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -564,11 +564,31 @@ 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); + 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); + + GadgetListBoxReset(listboxReplayFiles); + PopulateReplayFileListbox(listboxReplayFiles); + return; + } + + if(TheRecorder->playbackFile(asciiFilename)) + { + if(parentReplayMenu != nullptr) + { + parentReplayMenu->winHide(TRUE); + } } } From 89eec2b4fa3931fab470e3c58801b6a0720992be Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 12:44:15 +0100 Subject: [PATCH 02/12] bugfix(replay): Handle replay file deleted while the version mismatch prompt is open (Generals) --- .../GameEngine/Source/Common/Recorder.cpp | 7 +++-- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index 264193f8126..977cf94c4d6 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; @@ -1199,6 +1197,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..b1cea0ce032 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -564,11 +564,31 @@ 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); + 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); + + GadgetListBoxReset(listboxReplayFiles); + PopulateReplayFileListbox(listboxReplayFiles); + return; + } + + if(TheRecorder->playbackFile(asciiFilename)) + { + if(parentReplayMenu != nullptr) + { + parentReplayMenu->winHide(TRUE); + } } } From 961aac91df57e1d59b664a285e78f9bb82d27ca7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 13:12:15 +0100 Subject: [PATCH 03/12] bugfix(replay): Show the replay not found message when the playback fails to start --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) 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 6ed39aec0dd..97144288ae0 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,19 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } +static void showReplayNotFoundAndRefreshList() +{ + 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); + + GadgetListBoxReset(listboxReplayFiles); + PopulateReplayFileListbox(listboxReplayFiles); +} + +//------------------------------------------------------------------------------------------------- + void reallyLoadReplay() { UnicodeString filename; @@ -573,13 +586,7 @@ void reallyLoadReplay() if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - 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); - - GadgetListBoxReset(listboxReplayFiles); - PopulateReplayFileListbox(listboxReplayFiles); + showReplayNotFoundAndRefreshList(); return; } @@ -590,6 +597,10 @@ void reallyLoadReplay() parentReplayMenu->winHide(TRUE); } } + else + { + showReplayNotFoundAndRefreshList(); + } } static void loadReplay(UnicodeString filename) @@ -605,10 +616,7 @@ 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); + showReplayNotFoundAndRefreshList(); } else if(mapData == nullptr) { @@ -627,11 +635,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 + { + showReplayNotFoundAndRefreshList(); } } } From 989c2e4806bce77ebc715733881386656e2c68c5 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 13:12:15 +0100 Subject: [PATCH 04/12] bugfix(replay): Show the replay not found message when the playback fails to start (Generals) --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) 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 b1cea0ce032..934a2bec118 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,19 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } +static void showReplayNotFoundAndRefreshList() +{ + 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); + + GadgetListBoxReset(listboxReplayFiles); + PopulateReplayFileListbox(listboxReplayFiles); +} + +//------------------------------------------------------------------------------------------------- + void reallyLoadReplay() { UnicodeString filename; @@ -573,13 +586,7 @@ void reallyLoadReplay() if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - 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); - - GadgetListBoxReset(listboxReplayFiles); - PopulateReplayFileListbox(listboxReplayFiles); + showReplayNotFoundAndRefreshList(); return; } @@ -590,6 +597,10 @@ void reallyLoadReplay() parentReplayMenu->winHide(TRUE); } } + else + { + showReplayNotFoundAndRefreshList(); + } } static void loadReplay(UnicodeString filename) @@ -605,10 +616,7 @@ 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); + showReplayNotFoundAndRefreshList(); } else if(mapData == nullptr) { @@ -627,11 +635,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 + { + showReplayNotFoundAndRefreshList(); } } } From c57915382e100587b3e89aabb99bf60459005f80 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 13:37:56 +0100 Subject: [PATCH 05/12] bugfix(replay): Use generic replay load failure message --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 97144288ae0..7a7bba7a20f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -548,10 +548,10 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } -static void showReplayNotFoundAndRefreshList() +static void showReplayLoadFailedAndRefreshList() { - 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."); + 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); @@ -586,7 +586,7 @@ void reallyLoadReplay() if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); return; } @@ -599,7 +599,7 @@ void reallyLoadReplay() } else { - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); } } @@ -616,7 +616,7 @@ 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. - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); } else if(mapData == nullptr) { @@ -646,7 +646,7 @@ static void loadReplay(UnicodeString filename) } else { - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); } } } @@ -861,4 +861,3 @@ void copyReplay() } } - From 926eba884b5a25f9af8c2661639e4887f8b22e00 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 13:39:35 +0100 Subject: [PATCH 06/12] bugfix(replay): Use generic replay load failure message (Generals) --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 934a2bec118..214829eb960 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -548,10 +548,10 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } -static void showReplayNotFoundAndRefreshList() +static void showReplayLoadFailedAndRefreshList() { - 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."); + 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); @@ -586,7 +586,7 @@ void reallyLoadReplay() if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); return; } @@ -599,7 +599,7 @@ void reallyLoadReplay() } else { - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); } } @@ -616,7 +616,7 @@ 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. - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); } else if(mapData == nullptr) { @@ -646,7 +646,7 @@ static void loadReplay(UnicodeString filename) } else { - showReplayNotFoundAndRefreshList(); + showReplayLoadFailedAndRefreshList(); } } } @@ -861,4 +861,3 @@ void copyReplay() } } - From 3e82593ec4516acb9a4fab4c883c71082f87e390 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 14:12:03 +0100 Subject: [PATCH 07/12] bugfix(replay): Recheck replay map after version prompt --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 7a7bba7a20f..549017b83ae 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -559,6 +559,14 @@ static void showReplayLoadFailedAndRefreshList() 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() @@ -590,6 +598,12 @@ void reallyLoadReplay() return; } + if(mapData == nullptr) + { + showReplayMapNotFound(); + return; + } + if(TheRecorder->playbackFile(asciiFilename)) { if(parentReplayMenu != nullptr) @@ -622,10 +636,7 @@ static void loadReplay(UnicodeString filename) { // 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)) { From 341dbb4ac992043bf585e7ed2a782b6098447b78 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 14:12:05 +0100 Subject: [PATCH 08/12] bugfix(replay): Recheck replay map after version prompt (Generals) --- .../GUI/GUICallbacks/Menus/ReplayMenu.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 214829eb960..543fd36c218 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp @@ -559,6 +559,14 @@ static void showReplayLoadFailedAndRefreshList() 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() @@ -590,6 +598,12 @@ void reallyLoadReplay() return; } + if(mapData == nullptr) + { + showReplayMapNotFound(); + return; + } + if(TheRecorder->playbackFile(asciiFilename)) { if(parentReplayMenu != nullptr) @@ -622,10 +636,7 @@ static void loadReplay(UnicodeString filename) { // 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)) { From e70184dbc4c9a9076e7c64cf350f52efd447cc03 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 14:37:44 +0100 Subject: [PATCH 09/12] refactor(replay): Clarify replay load failure handler --- .../GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 549017b83ae..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,7 +548,7 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } -static void showReplayLoadFailedAndRefreshList() +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."); @@ -594,7 +594,7 @@ void reallyLoadReplay() if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); return; } @@ -613,7 +613,7 @@ void reallyLoadReplay() } else { - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); } } @@ -630,7 +630,7 @@ 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. - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); } else if(mapData == nullptr) { @@ -657,7 +657,7 @@ static void loadReplay(UnicodeString filename) } else { - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); } } } From 35192d4205e5120d0febe11390b7ea73316143bc Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 14:37:47 +0100 Subject: [PATCH 10/12] refactor(replay): Clarify replay load failure handler (Generals) --- .../GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 543fd36c218..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,7 +548,7 @@ WindowMsgHandledType ReplayMenuInput( GameWindow *window, UnsignedInt msg, } -static void showReplayLoadFailedAndRefreshList() +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."); @@ -594,7 +594,7 @@ void reallyLoadReplay() if(!readReplayMapInfo(asciiFilename, header, info, mapData)) { - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); return; } @@ -613,7 +613,7 @@ void reallyLoadReplay() } else { - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); } } @@ -630,7 +630,7 @@ 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. - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); } else if(mapData == nullptr) { @@ -657,7 +657,7 @@ static void loadReplay(UnicodeString filename) } else { - showReplayLoadFailedAndRefreshList(); + handleReplayLoadFailure(); } } } From 7880e99073a208a7659960cf60f57ba9fb474d03 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 16:42:03 +0100 Subject: [PATCH 11/12] bugfix(replay): Reject playback after failed frame read --- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 7b86944051c..2d9bfea3a16 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1148,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) { @@ -1182,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) From 77215a87f78d157bd963a81804a0bafba4ac25d8 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 25 Jul 2026 16:42:04 +0100 Subject: [PATCH 12/12] bugfix(replay): Reject playback after failed frame read (Generals) --- Generals/Code/GameEngine/Source/Common/Recorder.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index 977cf94c4d6..a6f51ade405 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -1145,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) { @@ -1179,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)