From 14be7892b6396ce7525b6e9cc878aac9c64c7855 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:42:42 +0200 Subject: [PATCH 1/5] Refactored GameLogic::processCommandList. --- .../GameEngine/Include/GameLogic/GameLogic.h | 2 + .../Source/GameLogic/System/GameLogic.cpp | 108 +++++++++--------- 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index 8192d3fbe68..afbad00e701 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -370,6 +370,8 @@ class GameLogic : public SubsystemInterface, public Snapshot static void createOptimizedTree(const ThingTemplate *thingTemplate, Coord3D *pos, Real angle); + void checkForMismatch(); + private: /** diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 8fcf237274b..4e301c0bcd4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -2634,9 +2634,7 @@ void GameLogic::processCommandList( CommandList *list ) m_cachedCRCs.clear(); m_shouldValidateCRCs = FALSE; - GameMessage* msg; - - for( msg = list->getFirstMessage(); msg; msg = msg->next() ) + for( GameMessage* msg = list->getFirstMessage(); msg; msg = msg->next() ) { #ifdef RTS_DEBUG DEBUG_ASSERTCRASH(msg != nullptr && msg != (GameMessage*)0xdeadbeef, ("bad msg")); @@ -2646,67 +2644,73 @@ void GameLogic::processCommandList( CommandList *list ) if (m_shouldValidateCRCs && !TheNetwork->sawCRCMismatch()) { - Bool sawCRCMismatch = FALSE; - Int numPlayers = 0; - DEBUG_ASSERTCRASH(TheNetwork, ("No Network!")); - if (TheNetwork) - { - for (Int i=0; iisPlayerConnected(i)) - ++numPlayers; - } + checkForMismatch(); + } +} - if (m_cachedCRCs.size() < numPlayers) - { - DEBUG_CRASH(("Not enough CRCs!")); - sawCRCMismatch = TRUE; - } - else - { - Bool hasReferenceCRC = FALSE; - UnsignedInt referenceCRC = 0; +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void GameLogic::checkForMismatch() +{ + DEBUG_ASSERTCRASH(TheNetwork, ("No Network!")); - for (CachedCRCMap::const_iterator it = m_cachedCRCs.begin(); it != m_cachedCRCs.end(); ++it) - { - // TheSuperHackers @bugfix Caball009 14/06/2026 Check if player is still connected, - // to avoid spurious mismatches at low CRC intervals, e.g. every frame. - if (!TheNetwork->isPlayerConnected(it->first)) - continue; + Bool sawCRCMismatch = FALSE; + Int numPlayers = 0; - const UnsignedInt crc = it->second; + for (Int i=0; iisPlayerConnected(i)) + ++numPlayers; + } - if (!hasReferenceCRC) - { - hasReferenceCRC = TRUE; - referenceCRC = crc; - continue; - } + if (m_cachedCRCs.size() < numPlayers) + { + DEBUG_CRASH(("Not enough CRCs!")); + sawCRCMismatch = TRUE; + } + else + { + Bool hasReferenceCRC = FALSE; + UnsignedInt referenceCRC = 0; - if (referenceCRC != crc) - { - DEBUG_CRASH(("CRC mismatch!")); - sawCRCMismatch = TRUE; - } - } + for (CachedCRCMap::const_iterator it = m_cachedCRCs.begin(); it != m_cachedCRCs.end(); ++it) + { + // TheSuperHackers @bugfix Caball009 14/06/2026 Check if player is still connected, + // to avoid spurious mismatches at low CRC intervals, e.g. every frame. + if (!TheNetwork->isPlayerConnected(it->first)) + continue; + + const UnsignedInt crc = it->second; + + if (!hasReferenceCRC) + { + hasReferenceCRC = TRUE; + referenceCRC = crc; + continue; } - } - if (sawCRCMismatch) - { -#ifdef DEBUG_LOGGING - DEBUG_LOG(("CRC Mismatch - saw %d CRCs from %d players", m_cachedCRCs.size(), numPlayers)); - for (CachedCRCMap::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + if (referenceCRC != crc) { - Player *player = ThePlayerList->getNthPlayer(crcIt->first); - DEBUG_LOG(("CRC from player %d (%ls) = %X", crcIt->first, - player?player->getPlayerDisplayName().str():L"", crcIt->second)); + DEBUG_CRASH(("CRC mismatch!")); + sawCRCMismatch = TRUE; } -#endif // DEBUG_LOGGING - TheNetwork->setSawCRCMismatch(); } } + if (sawCRCMismatch) + { +#ifdef DEBUG_LOGGING + DEBUG_LOG(("CRC Mismatch - saw %d CRCs from %d players", m_cachedCRCs.size(), numPlayers)); + for (CachedCRCMap::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + Player* player = ThePlayerList->getNthPlayer(crcIt->first); + DEBUG_LOG(("CRC from player %d (%ls) = %X", crcIt->first, + player ? player->getPlayerDisplayName().str() : L"", crcIt->second)); + } +#endif // DEBUG_LOGGING + + TheNetwork->setSawCRCMismatch(); + } } // ------------------------------------------------------------------------------------------------ From 4650eca29fa8907b2592f8f78f322b4ae590ad60 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:44:53 +0200 Subject: [PATCH 2/5] Made Player::getPlayerDisplayName const. --- GeneralsMD/Code/GameEngine/Include/Common/Player.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Player.h b/GeneralsMD/Code/GameEngine/Include/Common/Player.h index d8c7940bb66..24cc16925aa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Player.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Player.h @@ -221,7 +221,7 @@ class Player : public Snapshot void deletePlayerAI(); - UnicodeString getPlayerDisplayName() { return m_playerDisplayName; } + UnicodeString getPlayerDisplayName() const { return m_playerDisplayName; } NameKeyType getPlayerNameKey() const { return m_playerNameKey; } AsciiString getSide() const { return m_side; } From dd8976024ee483561760df36cbc430649404a1dd Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:23:08 +0200 Subject: [PATCH 3/5] Deprecated the CRC playback argument. --- .../Source/GameLogic/System/GameLogicDispatch.cpp | 14 +++++++++++++- .../Source/GameLogic/System/GameLogic.cpp | 9 +++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index 0f3ab3b5616..db417934a68 100644 --- a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -2416,11 +2416,23 @@ bool GameLogic::onLogicCrc(MAYBE_UNUSED GameMessage *msg) } else if (TheRecorder && TheRecorder->isPlaybackMode()) { +#if RETAIL_COMPATIBLE_CRC + DEBUG_ASSERTCRASH(msg->getArgument(1)->boolean == msgPlayer->isLocalPlayer(), + ("CRC message origin is unexpected; playback message argument doesn't match message player index")); +#endif + UnsignedInt newCRC = msg->getArgument(0)->integer; //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d", //newCRC, msgPlayer->getPlayerIndex(), getCRC(), msg->getArgumentCount())); - TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), (msg->getArgument(1)->boolean)); + if (msgPlayer->isLocalPlayer()) + { + TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), true); + } + else + { + TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), false); + } } return true; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 4e301c0bcd4..a1248ac5091 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -3768,11 +3768,16 @@ void GameLogic::update() if (generateForSolo || generateForMP) { m_CRC = getCRC( CRC_RECALC ); - bool isPlayback = (TheRecorder && TheRecorder->isPlaybackMode()); GameMessage *msg = newInstance(GameMessage)(GameMessage::MSG_LOGIC_CRC); msg->appendIntegerArgument(m_CRC); + +#if RETAIL_COMPATIBLE_CRC + // TheSuperHackers @tweak Caball009 21/06/2026 Playback argument serves no purpose anymore + // other than to be able play replays from newer retail compatible builds on older builds or retail. + const bool isPlayback = (TheRecorder && TheRecorder->isPlaybackMode()); msg->appendBooleanArgument(isPlayback); +#endif // TheSuperHackers @info helmutbuhler 13/04/2025 // During replay simulation, we bypass TheMessageStream and instead put the CRC message @@ -3782,7 +3787,7 @@ void GameLogic::update() messageList = TheCommandList; messageList->appendMessage(msg); - DEBUG_LOG(("Appended %sCRC on frame %d: %8.8X", isPlayback ? "Playback " : "", m_frame, m_CRC)); + DEBUG_LOG(("Appended %sCRC on frame %d: %8.8X", (TheRecorder && TheRecorder->isPlaybackMode()) ? "Playback " : "", m_frame, m_CRC)); } // collect stats From d098b051243840243478721334ae0b95829a3f94 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:27:22 +0200 Subject: [PATCH 4/5] Updated RecorderClass::CRCInfo and related code. --- .../GameLogic/System/GameLogicDispatch.cpp | 26 +- .../Code/GameEngine/Include/Common/Recorder.h | 60 +++- .../GameEngine/Include/GameLogic/GameLogic.h | 9 +- .../GameEngine/Source/Common/Recorder.cpp | 265 +++++++++++++----- .../Source/GameLogic/System/GameLogic.cpp | 14 +- 5 files changed, 269 insertions(+), 105 deletions(-) diff --git a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index db417934a68..2e5e2509f28 100644 --- a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -2383,6 +2383,9 @@ bool GameLogic::onLogicCrc(MAYBE_UNUSED GameMessage *msg) Player *msgPlayer = getMessagePlayer(msg); if (TheNetwork) { + if (TheNetwork->sawCRCMismatch()) + return false; + Int slotIndex = -1; for (Int i=0; igetArgument(0)->integer; + const UnsignedInt newCRC = msg->getArgument(0)->integer; //DEBUG_LOG(("Received CRC of %8.8X from %ls on frame %d", newCRC, //msgPlayer->getPlayerDisplayName().str(), m_frame)); + m_cachedCRCs[msgPlayer->getPlayerIndex()] = newCRC; } else if (TheRecorder && TheRecorder->isPlaybackMode()) @@ -2421,17 +2421,23 @@ bool GameLogic::onLogicCrc(MAYBE_UNUSED GameMessage *msg) ("CRC message origin is unexpected; playback message argument doesn't match message player index")); #endif - UnsignedInt newCRC = msg->getArgument(0)->integer; - //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d", + if (TheRecorder->sawCRCMismatch()) + return false; + + const UnsignedInt newCRC = msg->getArgument(0)->integer; + //DEBUG_LOG(("Saw CRC of %X from player %d. Our CRC is %X. Arg count is %d", //newCRC, msgPlayer->getPlayerIndex(), getCRC(), msg->getArgumentCount())); if (msgPlayer->isLocalPlayer()) { - TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), true); + // TheSuperHackers @info The replay observer / playback player is the local player during playback mode. + TheRecorder->handlePlaybackCRCMessage(newCRC); } else { - TheRecorder->handleCRCMessage(newCRC, msgPlayer->getPlayerIndex(), false); + TheRecorder->handlePlayerCRCMessage(msgPlayer->getPlayerIndex(), newCRC); + + m_validationModeCRC = CRCMODE_REPLAY; } } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index fba73ad5ec3..c9159e05770 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -67,20 +67,55 @@ class RecorderClass : public SubsystemInterface class CRCInfo { public: + struct MismatchData + { + enum CPP_11(: Byte) + { + PLAYER_PLAYBACK = -1, + PLAYER_UNKNOWN = -2, + }; + + MismatchData() : + mismatched(false), + playerIndex(0), + queueSize(0), + playbackCRC(0), + playerCRC(0) + {} + + MismatchData(Byte playerIndex, UnsignedShort queueSize, UnsignedInt playbackCRC, UnsignedInt playerCRC) : + mismatched(true), + playerIndex(playerIndex), + queueSize(queueSize), + playbackCRC(playbackCRC), + playerCRC(playerCRC) + {} + + Bool mismatched; + Byte playerIndex; + UnsignedShort queueSize; + UnsignedInt playbackCRC; + UnsignedInt playerCRC; + }; + CRCInfo(); - CRCInfo(UnsignedInt localPlayer, Bool isMultiplayer); - void addCRC(UnsignedInt val); - UnsignedInt readCRC(); - int GetQueueSize() const { return m_data.size(); } - UnsignedInt getLocalPlayer() const { return m_localPlayer; } - void setSawCRCMismatch() { m_sawCRCMismatch = TRUE; } - Bool sawCRCMismatch() const { return m_sawCRCMismatch; } + void init(Bool isMultiplayer, Int localPlayerIndex); + void pushPlaybackCRC(UnsignedInt val); + void pushPlayerCRC(Int playerIndex, UnsignedInt val); + void setSawCRCMismatch(); + Bool sawCRCMismatch() const; + Byte getLocalPlayerIndex() const; + MismatchData generateMismatchData(); protected: - Bool m_sawCRCMismatch; + UnsignedInt getLargestQueueSize() const; + UnsignedInt popPlaybackCRC(); + Bool m_skippedOne; - UnsignedInt m_localPlayer; - std::list m_data; + Bool m_sawCRCMismatch; + Byte m_localPlayerIndex; + std::list m_playbackData; + std::vector m_playerData[MAX_PLAYER_COUNT]; }; public: @@ -110,8 +145,9 @@ class RecorderClass : public SubsystemInterface #endif Bool isPlaybackInProgress() const; -public: - void handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback); + void handlePlaybackCRCMessage(UnsignedInt newCRC); + void handlePlayerCRCMessage(Int playerIndex, UnsignedInt newCRC); + void checkForMismatch(); // read in info relating to a replay, conditionally setting up m_file for playback struct ReplayHeader diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index afbad00e701..9f7f3f444a7 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -374,6 +374,13 @@ class GameLogic : public SubsystemInterface, public Snapshot private: + enum CRCValidationMode CPP_11(: UnsignedByte) + { + CRCMODE_NONE, + CRCMODE_NETWORK, + CRCMODE_REPLAY, + }; + /** overrides to thing template buildable status. doesn't really belong here, but has to go somewhere. (srj) @@ -394,7 +401,7 @@ class GameLogic : public SubsystemInterface, public Snapshot UnsignedInt m_CRC; ///< Cache of previous CRC value typedef std::map CachedCRCMap; CachedCRCMap m_cachedCRCs; ///< CRCs we've seen this frame - Bool m_shouldValidateCRCs; ///< Should we validate CRCs this frame? + CRCValidationMode m_validationModeCRC; ///< (How) should we validate CRCs this frame? //----------------------------------------------------------------------------------------------- //Bool m_loadingScene; Bool m_loadingMap; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 6e5791e5972..3d37f9b8174 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -101,19 +101,31 @@ static FILE* openStatsLogFile() #endif RecorderClass::CRCInfo::CRCInfo() : - m_sawCRCMismatch(FALSE), - m_skippedOne(FALSE), - m_localPlayer(0) -{} + m_skippedOne(false), + m_sawCRCMismatch(false), + m_localPlayerIndex(-1) +{ + static_assert(ARRAY_SIZE(m_playerData) == MAX_PLAYER_COUNT, "array size must be equal to player count"); +} -RecorderClass::CRCInfo::CRCInfo(UnsignedInt localPlayer, Bool isMultiplayer) +void RecorderClass::CRCInfo::init(Bool isMultiplayer, Int localPlayerIndex) { - m_sawCRCMismatch = FALSE; + DEBUG_ASSERTCRASH((localPlayerIndex >= 0 && localPlayerIndex < MAX_PLAYER_COUNT) || localPlayerIndex == -1, + ("replay local player index is unexpected")); + m_skippedOne = !isMultiplayer; - m_localPlayer = localPlayer; + m_sawCRCMismatch = false; + m_localPlayerIndex = static_cast(localPlayerIndex); + + m_playbackData.clear(); + + for (size_t i = 0; i < ARRAY_SIZE(m_playerData); ++i) + { + m_playerData[i].clear(); + } } -void RecorderClass::CRCInfo::addCRC(UnsignedInt val) +void RecorderClass::CRCInfo::pushPlaybackCRC(UnsignedInt val) { // TheSuperHackers @fix helmutbuhler 03/04/2025 // In Multiplayer, the first MSG_LOGIC_CRC message somehow doesn't make it through the network. @@ -126,22 +138,122 @@ void RecorderClass::CRCInfo::addCRC(UnsignedInt val) return; } - m_data.push_back(val); - //DEBUG_LOG(("CRCInfo::addCRC() - crc %8.8X pushes list to %d entries (full=%d)", val, m_data.size(), !m_data.empty())); + m_playbackData.push_back(val); + //DEBUG_LOG(("CRCInfo::addPlaybackCRC() - crc %8.8X pushes list to %d entries", val, m_playbackData.size())); } -UnsignedInt RecorderClass::CRCInfo::readCRC() +void RecorderClass::CRCInfo::pushPlayerCRC(Int playerIndex, UnsignedInt val) { - if (m_data.empty()) + if (const Bool isAllowedToAddPlayerCRC = (m_localPlayerIndex < 0 || playerIndex == m_localPlayerIndex)) + { + const UnsignedInt index = static_cast(playerIndex); + if (index < ARRAY_SIZE(m_playerData)) + { + m_playerData[index].push_back(val); + //DEBUG_LOG(("CRCInfo::addPlayerCRC() - crc %8.8X pushes list to %d entries", val, m_playerData[index].size())); + } + } +} + +void RecorderClass::CRCInfo::setSawCRCMismatch() +{ + m_sawCRCMismatch = TRUE; +} + +Bool RecorderClass::CRCInfo::sawCRCMismatch() const +{ + return m_sawCRCMismatch; +} + +Byte RecorderClass::CRCInfo::getLocalPlayerIndex() const +{ + return m_localPlayerIndex; +} + +RecorderClass::CRCInfo::MismatchData RecorderClass::CRCInfo::generateMismatchData() +{ + CRCInfo::MismatchData mmData; + + for (size_t largestQueueSize = getLargestQueueSize(), j = 0; j < largestQueueSize; ++j) + { + const UnsignedInt playbackCRC = popPlaybackCRC(); + UnsignedInt playerCount = 0; + UnsignedInt mismatchPlayerCount = 0; + + for (size_t i = 0; i < ARRAY_SIZE(m_playerData); ++i) + { + if (j >= m_playerData[i].size()) + continue; + + ++playerCount; + + const UnsignedInt playerCRC = m_playerData[i][j]; + if (playbackCRC == playerCRC) + continue; + + ++mismatchPlayerCount; + + if (!mmData.mismatched) + { + mmData = CRCInfo::MismatchData(static_cast(i), + static_cast(m_playbackData.size()), playbackCRC, playerCRC); + } + } + + if (mmData.mismatched) + { + if (const Bool allPlayersMismatch = (playerCount >= 2 && playerCount == mismatchPlayerCount)) + { + mmData.playerIndex = CRCInfo::MismatchData::PLAYER_PLAYBACK; + } + else if (const Bool cannotAttributeMismatch = (playerCount <= 1 || mismatchPlayerCount >= 2)) + { + mmData.playerIndex = CRCInfo::MismatchData::PLAYER_UNKNOWN; + } + + // leave the playback data in a valid state in case the caller ignores the mismatch + while (!m_playbackData.empty() && ++j < largestQueueSize) + { + m_playbackData.pop_front(); + } + + break; + } + } + + for (size_t i = 0; i < ARRAY_SIZE(m_playerData); ++i) + { + m_playerData[i].clear(); + } + + return mmData; +} + +UnsignedInt RecorderClass::CRCInfo::getLargestQueueSize() const +{ + UnsignedInt size = 0; + for (size_t i = 0; i < ARRAY_SIZE(m_playerData); ++i) + { + if (m_playerData[i].size() > size) + { + size = m_playerData[i].size(); + } + } + + return size; +} + +UnsignedInt RecorderClass::CRCInfo::popPlaybackCRC() +{ + if (m_playbackData.empty()) { - DEBUG_LOG(("CRCInfo::readCRC() - bailing, full=0, size=%d", m_data.size())); return 0; } - UnsignedInt val = m_data.front(); - m_data.pop_front(); - //DEBUG_LOG(("CRCInfo::readCRC() - returning %8.8X, full=%d, size=%d", val, !m_data.empty(), m_data.size())); - return val; + const UnsignedInt crc = m_playbackData.front(); + m_playbackData.pop_front(); + + return crc; } void RecorderClass::logGameStart(AsciiString options) @@ -981,71 +1093,70 @@ Bool RecorderClass::sawCRCMismatch() const return m_crcInfo.sawCRCMismatch(); } -void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback) +void RecorderClass::handlePlaybackCRCMessage(UnsignedInt newCRC) { - if (fromPlayback) - { - //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Adding CRC of %X from %d to m_crcInfo", newCRC, playerIndex)); - m_crcInfo.addCRC(newCRC); - return; - } + m_crcInfo.pushPlaybackCRC(newCRC); + + //DEBUG_LOG(("RecorderClass::handlePlaybackCRCMessage() - Adding CRC of %X from playback to m_crcInfo", newCRC)); +} - Int localPlayerIndex = m_crcInfo.getLocalPlayer(); - Bool samePlayer = FALSE; - AsciiString playerName; - playerName.format("player%d", localPlayerIndex); - const Player *p = ThePlayerList->getNthPlayer(playerIndex); - if (!p || (p->getPlayerNameKey() == NAMEKEY(playerName))) - samePlayer = TRUE; - if (samePlayer || (localPlayerIndex < 0)) +void RecorderClass::handlePlayerCRCMessage(Int playerIndex, UnsignedInt newCRC) +{ + m_crcInfo.pushPlayerCRC(playerIndex, newCRC); + + //DEBUG_LOG(("RecorderClass::handlePlayerCRCMessage() - Adding CRC of %X from %d to m_crcInfo", newCRC, playerIndex)); +} + +void RecorderClass::checkForMismatch() +{ + const CRCInfo::MismatchData mmData = m_crcInfo.generateMismatchData(); + if (mmData.mismatched) { - UnsignedInt playbackCRC = m_crcInfo.readCRC(); - //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Comparing CRCs of InGame:%8.8X Replay:%8.8X Frame:%d from Player %d", - // playbackCRC, newCRC, TheGameLogic->getFrame()-m_crcInfo.GetQueueSize()-1, playerIndex)); - if (TheGameLogic->getFrame() > 0 && newCRC != playbackCRC && !m_crcInfo.sawCRCMismatch()) + // Kris: Patch 1.01 November 10, 2003 (integrated changes from Matt Campbell) + // Since we don't seem to have any *visible* desyncs when replaying games, but get this warning + // virtually every replay, the assumption is our CRC checking is faulty. Since we're at the + // tail end of patch season, let's just disable the message, and hope the users believe the + // problem is fixed. -MDC 3/20/2003 + // + // TheSuperHackers @tweak helmutbuhler 03/04/2025 + // More than 20 years later, but finally fixed and re-enabled! + TheInGameUI->message("GUI:CRCMismatch"); + + // TheSuperHackers @info helmutbuhler 03/04/2025 + // Note: We subtract the queue size from the frame number. This way we calculate the correct frame + // the mismatch first happened in case the NetCRCInterval is set to 1 during the game. + const UnsignedInt mismatchFrame = TheGameLogic->getFrame() - mmData.queueSize - 1; + + // Now also prints a UI message for it. + const Player* player = ThePlayerList->getNthPlayer(mmData.playerIndex); + const UnicodeString mismatchDetailsStr = TheGameText->FETCH_OR_SUBSTITUTE("GUI:CRCMismatchDetails", + L"InGame:%8.8X Replay:%8.8X Frame:%d Player:%ls"); + TheInGameUI->message(mismatchDetailsStr, mmData.playbackCRC, mmData.playerCRC, mismatchFrame, player + ? player->getPlayerDisplayName().str() + : mmData.playerIndex == CRCInfo::MismatchData::PLAYER_PLAYBACK ? L"Playback" : L"Unknown"); + + DEBUG_LOG(("Replay has gone out of sync!\nInGame:%8.8X Replay:%8.8X\nFrame:%d\nPlayer:%ls", + mmData.playbackCRC, mmData.playerCRC, mismatchFrame, player + ? player->getPlayerDisplayName().str() + : mmData.playerIndex == CRCInfo::MismatchData::PLAYER_PLAYBACK ? L"Playback" : L"Unknown")); + + // Print mismatch in case we are simulating replays from console. + printf("CRC Mismatch in Frame %d, Local PlayerIndex %d, Mismatch PlayerIndex %d\n", + mismatchFrame, m_crcInfo.getLocalPlayerIndex(), mmData.playerIndex); + + // TheSuperHackers @tweak Pause the game on mismatch. + // But not when a window with focus is opened, because that can make resuming difficult. + if (TheWindowManager->winGetFocus() == nullptr) { - //Kris: Patch 1.01 November 10, 2003 (integrated changes from Matt Campbell) - // Since we don't seem to have any *visible* desyncs when replaying games, but get this warning - // virtually every replay, the assumption is our CRC checking is faulty. Since we're at the - // tail end of patch season, let's just disable the message, and hope the users believe the - // problem is fixed. -MDC 3/20/2003 - // - // TheSuperHackers @tweak helmutbuhler 03/04/2025 - // More than 20 years later, but finally fixed and re-enabled! - TheInGameUI->message("GUI:CRCMismatch"); - - // TheSuperHackers @info helmutbuhler 03/04/2025 - // Note: We subtract the queue size from the frame number. This way we calculate the correct frame - // the mismatch first happened in case the NetCRCInterval is set to 1 during the game. - const UnsignedInt mismatchFrame = TheGameLogic->getFrame() - m_crcInfo.GetQueueSize() - 1; - - // Now also prints a UI message for it. - const UnicodeString mismatchDetailsStr = TheGameText->FETCH_OR_SUBSTITUTE("GUI:CRCMismatchDetails", L"InGame:%8.8X Replay:%8.8X Frame:%d"); - TheInGameUI->message(mismatchDetailsStr, playbackCRC, newCRC, mismatchFrame); - - DEBUG_LOG(("Replay has gone out of sync!\nInGame:%8.8X Replay:%8.8X\nFrame:%d", - playbackCRC, newCRC, mismatchFrame)); - - // Print Mismatch in case we are simulating replays from console. - printf("CRC Mismatch in Frame %d\n", mismatchFrame); - - // TheSuperHackers @tweak Pause the game on mismatch. - // But not when a window with focus is opened, because that can make resuming difficult. - if (TheWindowManager->winGetFocus() == nullptr) - { - Bool pause = TRUE; - Bool pauseMusic = FALSE; - Bool pauseInput = FALSE; - TheGameLogic->setGamePaused(pause, pauseMusic, pauseInput); + const Bool pause = TRUE; + const Bool pauseMusic = FALSE; + const Bool pauseInput = FALSE; + TheGameLogic->setGamePaused(pause, pauseMusic, pauseInput); - // Mark this mismatch as seen when we had the chance to pause once. - m_crcInfo.setSawCRCMismatch(); - } + // Mark this mismatch as seen when we had the chance to pause once. + m_crcInfo.setSawCRCMismatch(); } - return; } - - //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Skipping CRC of %8.8X from %d (our index is %d)", newCRC, playerIndex, localPlayerIndex)); } /** @@ -1161,9 +1272,9 @@ Bool RecorderClass::playbackFile(AsciiString filename) #endif Bool isMultiplayer = m_gameInfo.getSlot(header.localPlayerIndex)->getIP() != 0; - m_crcInfo = CRCInfo(header.localPlayerIndex, isMultiplayer); + m_crcInfo.init(isMultiplayer, -1); REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval(); - DEBUG_LOG(("Player index is %d, replay CRC interval is %d", m_crcInfo.getLocalPlayer(), REPLAY_CRC_INTERVAL)); + DEBUG_LOG(("Player index is %d, replay CRC interval is %d", header.localPlayerIndex, REPLAY_CRC_INTERVAL)); Int difficulty = 0; m_file->read(&difficulty, sizeof(difficulty)); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index a1248ac5091..3944e1edb66 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -256,7 +256,7 @@ GameLogic::GameLogic() m_progressCompleteTimeout[i] = 0; } - m_shouldValidateCRCs = FALSE; + m_validationModeCRC = CRCMODE_NONE; m_startNewGame = FALSE; @@ -2632,7 +2632,7 @@ void GameLogic::processDestroyList() void GameLogic::processCommandList( CommandList *list ) { m_cachedCRCs.clear(); - m_shouldValidateCRCs = FALSE; + m_validationModeCRC = CRCMODE_NONE; for( GameMessage* msg = list->getFirstMessage(); msg; msg = msg->next() ) { @@ -2642,10 +2642,14 @@ void GameLogic::processCommandList( CommandList *list ) logicMessageDispatcher( msg, nullptr ); } - if (m_shouldValidateCRCs && !TheNetwork->sawCRCMismatch()) + if (m_validationModeCRC == CRCMODE_NETWORK) { checkForMismatch(); } + else if (m_validationModeCRC == CRCMODE_REPLAY) + { + TheRecorder->checkForMismatch(); + } } // ------------------------------------------------------------------------------------------------ @@ -2655,7 +2659,7 @@ void GameLogic::checkForMismatch() DEBUG_ASSERTCRASH(TheNetwork, ("No Network!")); Bool sawCRCMismatch = FALSE; - Int numPlayers = 0; + UnsignedInt numPlayers = 0; for (Int i=0; igetNthPlayer(crcIt->first); + const Player* player = ThePlayerList->getNthPlayer(crcIt->first); DEBUG_LOG(("CRC from player %d (%ls) = %X", crcIt->first, player ? player->getPlayerDisplayName().str() : L"", crcIt->second)); } From eec0f3dfb1c2efa781ad17bde185af2e1e368ec7 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:27:39 +0200 Subject: [PATCH 5/5] Added '-replayLocalPlayerCRC' command line option. --- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 ++ .../Code/GameEngine/Source/Common/CommandLine.cpp | 10 ++++++++++ .../Code/GameEngine/Source/Common/GlobalData.cpp | 1 + GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 4c5130e1c05..2188082507b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -368,6 +368,8 @@ class GlobalData : public SubsystemInterface Bool m_afterIntro; ///< we need to tell the game our intro is done Bool m_allowExitOutOfMovies; ///< flag to allow exit out of movies only after the Intro has played + Bool m_replayLocalPlayerCRC; ///< flag to validate CRC messages only from the player who recorded a replay + Bool m_loadScreenRender; ///< flag to disallow rendering of almost everything during a loadscreen Real m_keyboardScrollFactor; ///< Factor applied to game scrolling speed via keyboard scrolling diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 8289418703a..61c9708b4f9 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -465,6 +465,13 @@ Int parseJobs(char *args[], int num) return 1; } +Int parseReplayLocalPlayerCRC(char* args[], int num) +{ + TheWritableGlobalData->m_replayLocalPlayerCRC = TRUE; + + return 1; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -1166,6 +1173,9 @@ static CommandLineParam paramsForEngineInit[] = // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. { "-forcefullviewport", parseFullViewport }, + // TheSuperHackers @feature Caball009 21/06/2026 Validate CRC messages only from the player who recorded a replay. + { "-replayLocalPlayerCRC", parseReplayLocalPlayerCRC }, + #if defined(RTS_DEBUG) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 9900029d48d..f1d78e2d032 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1014,6 +1014,7 @@ GlobalData::GlobalData() m_playSizzle = TRUE; m_afterIntro = FALSE; m_allowExitOutOfMovies = FALSE; + m_replayLocalPlayerCRC = FALSE; m_loadScreenRender = FALSE; m_keyboardDefaultScrollFactor = m_keyboardScrollFactor = 0.5f; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp index 3d37f9b8174..eda166a1d62 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Recorder.cpp @@ -1272,7 +1272,7 @@ Bool RecorderClass::playbackFile(AsciiString filename) #endif Bool isMultiplayer = m_gameInfo.getSlot(header.localPlayerIndex)->getIP() != 0; - m_crcInfo.init(isMultiplayer, -1); + m_crcInfo.init(isMultiplayer, TheGlobalData->m_replayLocalPlayerCRC ? header.localPlayerIndex : -1); REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval(); DEBUG_LOG(("Player index is %d, replay CRC interval is %d", header.localPlayerIndex, REPLAY_CRC_INTERVAL));