Skip to content

bugfix(crc): Fix spurious mismatches for disconnected players at low CRC intervals#2796

Open
Caball009 wants to merge 1 commit into
TheSuperHackers:mainfrom
Caball009:fix_mismatch_disconnected_player
Open

bugfix(crc): Fix spurious mismatches for disconnected players at low CRC intervals#2796
Caball009 wants to merge 1 commit into
TheSuperHackers:mainfrom
Caball009:fix_mismatch_disconnected_player

Conversation

@Caball009

@Caball009 Caball009 commented Jun 14, 2026

Copy link
Copy Markdown

This PR introduces a small to fix to avoid spurious mismatches that can occur if the following conditions are met simultaneously:

  1. Low CRC intervals; e.g. once per frame instead of once per 100 frames (default value).
  2. Runahead decreases, so that multiple frames are executed in one go.
  3. Player disconnects during one of these frames.

As players disconnect they stop sending data to other players to speed up the disconnect process. If that coincides with a decrease in runahead, it's possible that a single execution frame contains e.g. 5 CRC messages per player, but fewer for the disconnecting player. The game only checks a player's most recent CRC value. This would lead to a mismatch because the most recent CRC value for the disconnecting player is not up-to-date with the other (connected) players.

You can see it mismatch here as the player disconnects (this was with a special test build):
https://www.youtube.com/live/RM27jEkTGfw?t=2099s
https://www.youtube.com/live/V_l-q9Y-DmA?t=7576s

Here's a replay for the second match. The mismatch happens at frame 3836 because of player index 6 disconnecting from the game: MM NED.zip

I have no idea how to reproduce this in a test environment, unfortunately.

TODO:

  • Replicate in Generals.

@Caball009 Caball009 added Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Network Anything related to network, servers Gen Relates to Generals ZH Relates to Zero Hour labels Jun 14, 2026
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a false-positive CRC mismatch that arises when a player disconnects during a frame where multiple CRC checks fire in a single processCommandList call (e.g., runahead contraction at a 1-frame CRC interval). The disconnecting player stops sending data mid-batch, leaving a stale CRC entry in the map that was previously used as the comparison baseline.

  • The old loop unconditionally used m_cachedCRCs.begin()->second as the validator, so a stale entry at the lowest slot index became the reference and triggered false mismatches against every connected player's fresh CRC.
  • The new loop skips entries whose player index fails isPlayerConnected, then pins referenceCRC from the first connected entry via hasReferenceCRC — fixing the validator-selection bug noted in the prior review.
  • The CachedCRCMap typedef extracted to the header removes repeated std::map<Int, UnsignedInt> spellings throughout the .cpp.

Confidence Score: 5/5

Safe to merge; the new loop correctly skips disconnected players on both the validator and validated sides, and the Generals counterpart gap is an acknowledged TODO.

The rewritten comparison loop correctly addresses the false-positive scenario described in the PR: disconnected players are filtered before the reference CRC is selected, so a stale lowest-slot entry can no longer become the baseline. The pre-existing m_cachedCRCs.size() < numPlayers guard is unchanged and continues to catch the missing-CRC case. The only unfixed path is the Generals (non-ZH) counterpart, which is explicitly listed as a TODO.

No files require special attention. The Generals counterpart (Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp) still carries the old loop and is the natural next target for a follow-up PR.

Important Files Changed

Filename Overview
GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h Extracts CachedCRCMap typedef from the inline member declaration, enabling use of the type alias in GameLogic.cpp without repeating the full std::map<Int, UnsignedInt> spelling.
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Rewrites the CRC comparison loop to skip disconnected players; uses a hasReferenceCRC flag so the reference value is always taken from the first connected player, correctly handling the edge case where the lowest-indexed slot belongs to the disconnecting player.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[processCommandList called] --> B{m_shouldValidateCRCs &&\nnot sawCRCMismatch?}
    B -- No --> Z[Done]
    B -- Yes --> C[Count connected players\nnumPlayers]
    C --> D{m_cachedCRCs.size\n< numPlayers?}
    D -- Yes --> E[sawCRCMismatch = TRUE\nNot enough CRCs!]
    D -- No --> F[Iterate m_cachedCRCs]
    F --> G{isPlayerConnected\nit->first ?}
    G -- No disconnecting player --> F
    G -- Yes --> H{hasReferenceCRC?}
    H -- No --> I[Set referenceCRC = crc\nhasReferenceCRC = TRUE]
    I --> F
    H -- Yes --> J{referenceCRC != crc?}
    J -- No --> F
    J -- Yes --> E
    E --> K[TheNetwork::setSawCRCMismatch]
Loading

Reviews (5): Last reviewed commit: "bugfix(crc): Fix spurious mismatches for..." | Re-trigger Greptile

@Caball009 Caball009 force-pushed the fix_mismatch_disconnected_player branch from ae476f1 to 7fbffaf Compare June 14, 2026 20:16
@Caball009 Caball009 marked this pull request as draft June 14, 2026 20:17
@Caball009 Caball009 force-pushed the fix_mismatch_disconnected_player branch from 7fbffaf to e2bd3ef Compare June 14, 2026 20:29
@Caball009 Caball009 marked this pull request as ready for review June 14, 2026 20:30
@Caball009 Caball009 force-pushed the fix_mismatch_disconnected_player branch from e2bd3ef to 872efd3 Compare June 15, 2026 17:03
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Outdated
@Caball009 Caball009 force-pushed the fix_mismatch_disconnected_player branch from 872efd3 to b9e744e Compare June 15, 2026 21:14

@Mauller Mauller left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks okay to me, i don't think it can be much simpler since zero can be a valid CRC funnily enough for crc32.

Int validatedCRC = crcIt->second;
//DEBUG_LOG(("CRC to validate is from player %d: %8.8X", crcIt->first, validatedCRC));
if (validatorCRC != validatedCRC)
// TheSuperHackers @bugfix Caball009 14/06/2026 Check if player is still connected,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be behind a retail compile guard?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think so. Do you see potential issues?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest test with a client that has this change and a client that does not in a Network match - if not already done. My first instinct would be to guard it with RETAIL_COMPATIBLE_NETWORK but I do not know.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest test with a client that has this change and a client that does not in a Network match - if not already done.

I already did testing and I wasn't able to reproduce this issue in a test environment because of the timing issue with the decrease in the runahead value.

My first instinct would be to guard it with RETAIL_COMPATIBLE_NETWORK but I do not know.

It's an issue with and without retail compatibility. I'm trying to avoid this type of 'fake' mismatches, so putting it behind a macro doesn't make sense to me.

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change assumes that all player disconnects are perfectly in sync. Is this guaranteed?

Bool sawCRCMismatch = FALSE;
Int numPlayers = 0;
DEBUG_ASSERTCRASH(TheNetwork, ("No Network!"));
if (TheNetwork)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a follow up, consider moving the Network test before the above TheNetwork->sawCRCMismatch()

Int validatedCRC = crcIt->second;
//DEBUG_LOG(("CRC to validate is from player %d: %8.8X", crcIt->first, validatedCRC));
if (validatorCRC != validatedCRC)
// TheSuperHackers @bugfix Caball009 14/06/2026 Check if player is still connected,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest test with a client that has this change and a client that does not in a Network match - if not already done. My first instinct would be to guard it with RETAIL_COMPATIBLE_NETWORK but I do not know.

if (validatorCRC != validatedCRC)
// 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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is isPlayerConnected guaranteed to return the same result on every client in the same frame? I am asking because this function is in GameNetwork, not in GameLogic.

Can Client A lose connection to Client B, while Client C is still connected to Clients A, B? Is this scenario possible?

I suggest to test this with multiple clients.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change assumes that all player disconnects are perfectly in sync. Is this guaranteed?

Is isPlayerConnected guaranteed to return the same result on every client in the same frame?

I think so, because player disconnects are handled by Network::processFrameSynchronizedNetCommand, which has a relevant description:
"This is where network commands that need to be executed on the same frame should be executed."

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (Int i=0; i<MAX_SLOTS; ++i)
{
if (TheNetwork->isPlayerConnected(i))
++numPlayers;
}
if (m_cachedCRCs.size() < numPlayers)
{
DEBUG_CRASH(("Not enough CRCs!"));
sawCRCMismatch = TRUE;
}

FWIW this original code also strongly suggests that isPlayerConnected is synchronized across clients.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, players with the fix should carry on playing and not register a mismatch, so their game will continue.

Retail players may register the mismatch and dropout.

I don't entirely see a problem with this as it just replicates how crashes are handled, As TSH clients typically continue playing but retail players drop out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker Network Anything related to network, servers ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants