From 043602957985543f00f795ea0896280fe619f74c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 29 Aug 2026 18:55:59 -0400 Subject: [PATCH 1/4] bugfix(replay): Derive the replay CRC queue from the recorded game mode --- GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 8b9ecc5acbb..66f0b83fdee 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1205,16 +1205,21 @@ Bool RecorderClass::playbackFile(AsciiString filename) } #endif - Bool isMultiplayer = m_gameInfo.getSlot(header.localPlayerIndex)->getIP() != 0; - m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer); REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval(); - DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL)); Int difficulty = 0; m_file->read(&difficulty, sizeof(difficulty)); m_file->read(&m_originalGameMode, sizeof(m_originalGameMode)); + // TheSuperHackers @bugfix bobtista 30/08/2026 A replay header is allowed to carry a local player + // index of -1 and getSlot returns NULL for it, so reading the slot to tell a network game from a + // local one dereferenced NULL. The recorded game mode answers the same question directly, so the + // crc queue is now primed from the mode and the local slot is no longer read here. + const Bool isMultiplayer = m_originalGameMode == GAME_LAN || m_originalGameMode == GAME_INTERNET; + m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer); + DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL)); + Int rankPoints = 0; m_file->read(&rankPoints, sizeof(rankPoints)); From d1ebf59fde5655d650f0f502a1c9e2c9f4f630cf Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 30 Aug 2026 19:22:54 -0400 Subject: [PATCH 2/4] bugfix(replay): Replicate replay CRC queue mode derivation to Generals --- Generals/Code/GameEngine/Source/Common/Recorder.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/Recorder.cpp b/Generals/Code/GameEngine/Source/Common/Recorder.cpp index 91c44c437c8..35bb6881fce 100644 --- a/Generals/Code/GameEngine/Source/Common/Recorder.cpp +++ b/Generals/Code/GameEngine/Source/Common/Recorder.cpp @@ -1202,16 +1202,21 @@ Bool RecorderClass::playbackFile(AsciiString filename) } #endif - Bool isMultiplayer = m_gameInfo.getSlot(header.localPlayerIndex)->getIP() != 0; - m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer); REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval(); - DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL)); Int difficulty = 0; m_file->read(&difficulty, sizeof(difficulty)); m_file->read(&m_originalGameMode, sizeof(m_originalGameMode)); + // TheSuperHackers @bugfix bobtista 30/08/2026 A replay header is allowed to carry a local player + // index of -1 and getSlot returns NULL for it, so reading the slot to tell a network game from a + // local one dereferenced NULL. The recorded game mode answers the same question directly, so the + // crc queue is now primed from the mode and the local slot is no longer read here. + const Bool isMultiplayer = m_originalGameMode == GAME_LAN || m_originalGameMode == GAME_INTERNET; + m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer); + DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL)); + Int rankPoints = 0; m_file->read(&rankPoints, sizeof(rankPoints)); From f5e47e423e397a9851bd122d1fe3cc14ab9ab109 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 30 Aug 2026 19:52:03 -0400 Subject: [PATCH 3/4] bugfix(replay): Guard the local slot lookup when a replay has no local player --- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 6 +++++- .../Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 932caa24f82..ac71eb4247c 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1315,7 +1315,11 @@ void GameLogic::tryStartNewGame( Bool loadingSaveGame ) d.setInt(TheKey_multiplayerStartIndex, slot->getStartPos()); // d.setBool(TheKey_multiplayerIsLocal, slot->isLocalPlayer()); // d.setBool(TheKey_multiplayerIsLocal, slot->getIP() == game->getLocalIP()); - d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && (slot->getName().compare(TheGameInfo->getSlot(TheGameInfo->getLocalSlotNum())->getName().str()) == 0)); + // TheSuperHackers @bugfix bobtista 30/08/2026 A replay recorded without a local player has no + // local slot number and getSlot returns NULL for it, so no slot can be the local one. + const Int localSlotNum = TheGameInfo->getLocalSlotNum(); + const GameSlot *localGameSlot = localSlotNum >= 0 ? TheGameInfo->getSlot(localSlotNum) : nullptr; + d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && localGameSlot != nullptr && (slot->getName().compare(localGameSlot->getName().str()) == 0)); /* if (slot->getIP() == game->getLocalIP()) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 60bae04f186..71e8f72b235 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1476,7 +1476,11 @@ void GameLogic::tryStartNewGame( Bool loadingSaveGame ) d.setInt(TheKey_multiplayerStartIndex, slot->getStartPos()); // d.setBool(TheKey_multiplayerIsLocal, slot->isLocalPlayer()); // d.setBool(TheKey_multiplayerIsLocal, slot->getIP() == game->getLocalIP()); - d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && (slot->getName().compare(TheGameInfo->getSlot(TheGameInfo->getLocalSlotNum())->getName().str()) == 0)); + // TheSuperHackers @bugfix bobtista 30/08/2026 A replay recorded without a local player has no + // local slot number and getSlot returns NULL for it, so no slot can be the local one. + const Int localSlotNum = TheGameInfo->getLocalSlotNum(); + const GameSlot *localGameSlot = localSlotNum >= 0 ? TheGameInfo->getSlot(localSlotNum) : nullptr; + d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && localGameSlot != nullptr && (slot->getName().compare(localGameSlot->getName().str()) == 0)); /* if (slot->getIP() == game->getLocalIP()) From dd769913269a742cd2d2b3b6c117779dffb781dc Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 17 Sep 2026 00:36:48 -0400 Subject: [PATCH 4/4] bugfix(replay): Preserve the recorded local player slot --- .../Code/GameEngine/Include/Common/Recorder.h | 18 +++++++++++++++++- .../Code/GameEngine/Source/Common/Recorder.cpp | 7 +++---- .../Source/GameLogic/System/GameLogic.cpp | 8 ++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index 2149dd2caf0..766aa3b9216 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -39,11 +39,27 @@ class ReplayGameInfo : public GameInfo GameSlot m_ReplaySlot[MAX_SLOTS]; public: - ReplayGameInfo() + ReplayGameInfo() : m_localSlotNum(-1) { for (Int i = 0; i< MAX_SLOTS; ++i) setSlotPointer(i, &m_ReplaySlot[i]); } + + virtual void reset() override + { + GameInfo::reset(); + m_localSlotNum = -1; + } + + virtual Int getLocalSlotNum() const override + { + return isInGame() ? m_localSlotNum : -1; + } + + void setLocalSlotNum(Int slotNum) { m_localSlotNum = slotNum; } + +private: + Int m_localSlotNum; }; enum RecorderModeType CPP_11(: Int) { diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 66f0b83fdee..6a85be2f9e0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -928,6 +928,8 @@ Bool RecorderClass::readReplayHeader(ReplayHeader& header, const AsciiString& fi m_file = nullptr; return FALSE; } + // TheSuperHackers @bugfix bobtista 17/09/2026 Use the recorded local slot, including no local player. + m_gameInfo.setLocalSlotNum(header.localPlayerIndex); if (header.localPlayerIndex >= 0) { Int localIP = m_gameInfo.getSlot(header.localPlayerIndex)->getIP(); @@ -1212,10 +1214,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) m_file->read(&m_originalGameMode, sizeof(m_originalGameMode)); - // TheSuperHackers @bugfix bobtista 30/08/2026 A replay header is allowed to carry a local player - // index of -1 and getSlot returns NULL for it, so reading the slot to tell a network game from a - // local one dereferenced NULL. The recorded game mode answers the same question directly, so the - // crc queue is now primed from the mode and the local slot is no longer read here. + // TheSuperHackers @bugfix bobtista 30/08/2026 Use the recorded game mode because replays may have no local player. const Bool isMultiplayer = m_originalGameMode == GAME_LAN || m_originalGameMode == GAME_INTERNET; m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer); DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL)); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 71e8f72b235..c83cd7407df 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1476,11 +1476,11 @@ void GameLogic::tryStartNewGame( Bool loadingSaveGame ) d.setInt(TheKey_multiplayerStartIndex, slot->getStartPos()); // d.setBool(TheKey_multiplayerIsLocal, slot->isLocalPlayer()); // d.setBool(TheKey_multiplayerIsLocal, slot->getIP() == game->getLocalIP()); - // TheSuperHackers @bugfix bobtista 30/08/2026 A replay recorded without a local player has no - // local slot number and getSlot returns NULL for it, so no slot can be the local one. + // TheSuperHackers @bugfix bobtista 30/08/2026 Replays may have no local player. const Int localSlotNum = TheGameInfo->getLocalSlotNum(); - const GameSlot *localGameSlot = localSlotNum >= 0 ? TheGameInfo->getSlot(localSlotNum) : nullptr; - d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && localGameSlot != nullptr && (slot->getName().compare(localGameSlot->getName().str()) == 0)); + const GameSlot *localGameSlot = localSlotNum >= 0 && localSlotNum < MAX_SLOTS ? TheGameInfo->getSlot(localSlotNum) : nullptr; + d.setBool(TheKey_multiplayerIsLocal, slot->isHuman() && localGameSlot != nullptr + && (slot->getName().compare(localGameSlot->getName().str()) == 0)); /* if (slot->getIP() == game->getLocalIP())