Skip to content

bugfix(skirmish): Set consistent seed values for skirmish games to prevent mismatches for restarted skirmish games#2270

Open
Caball009 wants to merge 6 commits intoTheSuperHackers:mainfrom
Caball009:fix_bug_skirmish_restart_seed_values
Open

bugfix(skirmish): Set consistent seed values for skirmish games to prevent mismatches for restarted skirmish games#2270
Caball009 wants to merge 6 commits intoTheSuperHackers:mainfrom
Caball009:fix_bug_skirmish_restart_seed_values

Conversation

@Caball009
Copy link

@Caball009 Caball009 commented Feb 8, 2026

This PR makes the seed value consistent for started vs. restarted skirmish games, and fixes a bug that causes replays of restarted skirmish game to always mismatch.

TODO:

  • Replicate in Generals.

@Caball009 Caball009 added Bug Something is not working right, typically is user facing Major Severity: Minor < Major < Critical < Blocker Stability Concerns stability of the runtime labels Feb 8, 2026
@greptile-apps
Copy link

greptile-apps bot commented Feb 8, 2026

Greptile Overview

Greptile Summary

This PR adjusts random seed initialization for skirmish games so that starting a skirmish and restarting it reuse consistent seed values, preventing replay mismatches (especially for restarted skirmish games).

Key changes:

  • SkirmishGameOptionsMenu.cpp now seeds the game-logic RNG with TheSkirmishGameInfo->getSeed() only for true skirmish (multiplayer) maps, and explicitly uses seed 0 for campaign/scenario maps launched via the skirmish menu.
  • QuitMenu.cpp caches a seed before clearGameData(FALSE) and uses it when restarting the mission (non-replay restart path) so skirmish restarts can preserve the original seed.

Main issue found: the new skirmish seed caching relies on a debug-only assertion for TheSkirmishGameInfo validity; in non-debug builds a null TheSkirmishGameInfo with GAME_SKIRMISH could still lead to a null dereference when computing seed.

Confidence Score: 3/5

  • This PR is close to mergeable but has a potential null-dereference path in non-debug builds that should be addressed.
  • Seed handling changes are localized and align with deterministic/non-deterministic expectations, but restartMissionMenu() currently relies on DEBUG_ASSERTCRASH to guarantee TheSkirmishGameInfo validity before dereferencing it to compute seed. If asserts are compiled out, an unexpected null TheSkirmishGameInfo with GAME_SKIRMISH would crash.
  • GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/QuitMenu.cpp

Important Files Changed

Filename Overview
GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/QuitMenu.cpp Caches skirmish seed before clearGameData and uses it in InitRandom() on restart; adds debug assertion tying TheSkirmishGameInfo presence to GAME_SKIRMISH. Potential issue: release builds can still dereference null if TheSkirmishGameInfo is unexpectedly null.
GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/SkirmishGameOptionsMenu.cpp Moves InitGameLogicRandom() seeding into branches: skirmish uses TheSkirmishGameInfo seed, non-skirmish forces seed 0 for solo campaign maps selectable via skirmish menu. Change appears consistent with intended deterministic non-skirmish behavior.

Sequence Diagram

sequenceDiagram
    participant UI as GUI (Quit/Skirmish Menus)
    participant SGI as TheSkirmishGameInfo
    participant GL as TheGameLogic
    participant RNG as RandomValue (InitRandom/InitGameLogicRandom)
    participant REC as TheRecorder
    participant MS as TheMessageStream

    Note over UI: Starting game from Skirmish menu
    UI->>SGI: startGame(0)
    UI->>UI: determine isSkirmish via MapMetaData.m_isMultiplayer
    alt isSkirmish
        UI->>RNG: InitGameLogicRandom(SGI.getSeed())
        UI->>MS: append MSG_NEW_GAME(GAME_SKIRMISH, ...)
    else non-skirmish scenario map
        UI->>RNG: InitGameLogicRandom(0)
        UI->>MS: append MSG_NEW_GAME(GAME_SINGLE_PLAYER, ...)
    end

    Note over UI: Restarting from Quit menu
    UI->>UI: gameMode = GL.getGameMode()
    UI->>UI: seed = (SGI != nullptr ? SGI.getSeed() : 0)
    UI->>GL: clearGameData(FALSE)
    UI->>REC: getCurrentReplayFilename()
    alt replayFile not empty
        UI->>REC: playbackFile(replayFile)
    else new game restart
        UI->>MS: append MSG_NEW_GAME(gameMode, diff, rankPoints, fps)
        UI->>RNG: InitRandom(seed)
    end
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@Caball009
Copy link
Author

Caball009 commented Feb 8, 2026

I did find a single case where the game would start a new GAME_SINGLE_PLAYER game bit without using a non-zero seed value. I've added that to this PR for more consistent seed values.

@Caball009 Caball009 force-pushed the fix_bug_skirmish_restart_seed_values branch from 6dacb8a to 45fa3ea Compare February 9, 2026 01:49
@Caball009 Caball009 changed the title bugfix(skirmish): Avoid mismatches for restarted skirmish games bugfix(skirmish): Set consistent seed value for skirmish games to prevent mismatches for restarted skirmish games Feb 9, 2026
@Caball009
Copy link
Author

Caball009 commented Feb 9, 2026

EDIT: Maybe I didn't properly explain what I documented here. The findings below explain how the seed value is set for different game modes. I noted the execution flow with the current changes of this PR. TheGameInfo ... && TheSkirmishGameInfo ... indicate what the values of these globals are inside the restartMissionMenu function.

Here are the results of my testing with this PR:

1. Campaign (GAME_SINGLE_PLAYER):
Start: doGameStart -> InitRandom( 0 )
Restart: restartMissionMenu -> InitRandom( (gameMode == GAME_SKIRMISH) ? TheSkirmishGameInfo->getSeed() : 0 )
TheGameInfo == nullptr && TheSkirmishGameInfo == nullptr

2. Challenges (GAME_SINGLE_PLAYER):
Start: ChallengeMenuSystem -> InitRandom( 0 )
Restart: restartMissionMenu -> InitRandom( (gameMode == GAME_SKIRMISH) ? TheSkirmishGameInfo->getSeed() : 0 )
TheGameInfo != nullptr && TheSkirmishGameInfo == nullptr

3. Skirmish, single player map / scenario (GAME_SINGLE_PLAYER):
Start: reallyDoStart -> InitGameLogicRandom( 0 )
Restart: restartMissionMenu -> InitRandom( (gameMode == GAME_SKIRMISH) ? TheSkirmishGameInfo->getSeed() : 0 )
TheGameInfo == nullptr && TheSkirmishGameInfo == nullptr

4. Skirmish, multiplayer map (GAME_SKIRMISH):
Start: reallyDoStart -> InitGameLogicRandom( TheSkirmishGameInfo->getSeed() )
Restart: restartMissionMenu -> InitRandom( (gameMode == GAME_SKIRMISH) ? TheSkirmishGameInfo->getSeed() : 0 )
TheGameInfo != nullptr && TheSkirmishGameInfo != nullptr

@Caball009
Copy link
Author

Caball009 commented Feb 9, 2026

If we ever to enable replays for campaign / challenge / single player scenarios, we may need to verify that this issue doesn't happen there as well. I don't see anything that could still present an issue in that regard, though.

@Caball009 Caball009 changed the title bugfix(skirmish): Set consistent seed value for skirmish games to prevent mismatches for restarted skirmish games bugfix(skirmish): Set consistent seed values for skirmish games to prevent mismatches for restarted skirmish games Feb 9, 2026

if (isSkirmish)
{
InitGameLogicRandom(TheSkirmishGameInfo->getSeed());
Copy link

Choose a reason for hiding this comment

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

Why do this change? Shouldn't it be responsibility of whoever set seed in TheSkirmishGameInfo? Now this responsibility has shifted to this function instead.

Copy link
Author

@Caball009 Caball009 Feb 9, 2026

Choose a reason for hiding this comment

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

This line was only changed because of the change below this for single player maps.

With regard to the setting of the seed value, that's already part of the GameInfo constructor.

Copy link
Author

@Caball009 Caball009 Feb 9, 2026

Choose a reason for hiding this comment

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

The restart function sets 0 as seed value for skirmish single player maps, so this function should as well. The restart function cannot use the seed value for skirmish single player maps because TheSkirmishGameInfo is null.

Copy link

Choose a reason for hiding this comment

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

I do not understand. Does this mean, when !isSkirmish, then TheSkirmishGameInfo not null here, but will be null on mission restart?

If yes, does that not indicate a bug elsewhere?

Copy link
Author

@Caball009 Caball009 Feb 10, 2026

Choose a reason for hiding this comment

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

TheSkirmishGameInfo is never null in reallyDoStart, otherwise it'd have crashed in the original code.

It's null in restartMissionMenu for skirmish single player mission. I agree that's unexpected, but it works without apparent issue.

Copy link

Choose a reason for hiding this comment

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

Ok that is very strange. Perhaps that warrants a follow up investigation?

//else
// InitGameLogicRandom(GameClientRandomValue(0, INT_MAX - 1));

InitRandom(seed);
Copy link

Choose a reason for hiding this comment

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

According to your list:

reallyDoStart -> InitGameLogicRandom( TheSkirmishGameInfo->getSeed() )

Wouldn't this mean that this needs to do:

InitGameLogicRandom( TheSkirmishGameInfo->getSeed() ) then?

Copy link
Author

Choose a reason for hiding this comment

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

Isn't that what I'm doing here?

Copy link

Choose a reason for hiding this comment

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

Currently:

InitRandom( TheSkirmishGameInfo->getSeed() );

Expected:

InitGameLogicRandom( TheSkirmishGameInfo->getSeed() );

Copy link
Author

@Caball009 Caball009 Feb 10, 2026

Choose a reason for hiding this comment

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

That's a good point. It's not clear why the former is used and sometimes the latter. The important point, though, is that InitRandom also sets the logical seed values, which are the only seed values that are part of the CRC computation.

I think we should make reallyDoStart use InitRandom, and perhaps check other places if they should also use InitRandom instead of InitGameLogicRandom.

Copy link

Choose a reason for hiding this comment

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

Ok maybe make a follow up investigation. Is not directly related to this fix.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@Caball009 Caball009 force-pushed the fix_bug_skirmish_restart_seed_values branch from 3208aa9 to 4b6d3dc Compare February 9, 2026 19:34
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

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

Looks like this is correct then.

// TheSuperHackers @bugfix Caball009 07/02/2026 Reuse the previous seed value for the new skirmish match to prevent mismatches.
// Campaign, challenge, and skirmish single-player scenarios all use GAME_SINGLE_PLAYER and are expected to use 0 as seed value.
DEBUG_ASSERTCRASH((TheSkirmishGameInfo != nullptr) == (gameMode == GAME_SKIRMISH), ("Unexpected game mode on map / mission restart"));
const UnsignedInt seed = (TheSkirmishGameInfo) ? TheSkirmishGameInfo->getSeed() : 0;
Copy link

Choose a reason for hiding this comment

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

Superfluous ()

//else
// InitGameLogicRandom(GameClientRandomValue(0, INT_MAX - 1));

InitRandom(seed);
Copy link

Choose a reason for hiding this comment

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

Ok maybe make a follow up investigation. Is not directly related to this fix.

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 Major Severity: Minor < Major < Critical < Blocker Stability Concerns stability of the runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replay will mismatch 100% when Skirmish Game is recorded after RESTART GAME

2 participants