Skip to content

fix(skirmish): Improve determinism for restarted games by resetting slot values#2373

Merged
xezon merged 5 commits intoTheSuperHackers:mainfrom
Caball009:fix_reset_slot_values_for_logical_seed
Apr 4, 2026
Merged

fix(skirmish): Improve determinism for restarted games by resetting slot values#2373
xezon merged 5 commits intoTheSuperHackers:mainfrom
Caball009:fix_reset_slot_values_for_logical_seed

Conversation

@Caball009
Copy link
Copy Markdown

@Caball009 Caball009 commented Mar 1, 2026

Restarted skirmish games may not start with the same logical seed value as the first start. This depends on whether there are players with a random color / position / faction. Those random values are determined by using and updating the logical seed on the first start, after which the random values are stored. That means that for restarted games the logical seed isn't used or updated for those purposes. This PR resets those values to improve determinism for restarted games.

You can put a breakpoint after GameLogic::startNewGame and compare the values of theGameLogicSeed for the first start and following starts and see how the values for the first start deviate from restarts.

TODO:

  • Address feedback.
  • Replicate in Generals.

@Caball009 Caball009 added Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Fix Is fixing something, but is not user facing labels Mar 1, 2026
@Caball009 Caball009 marked this pull request as ready for review March 19, 2026 13:50
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR fixes a determinism bug in restarted skirmish games. When a skirmish game was restarted, the logical seed (theGameLogicSeed) would diverge from its first-start value because slots with random color/position/faction had already consumed seed state on the initial run — their resolved values were stored, but the seed advanced past them. On restart those values were not random any more, so the seed was never advanced for those purposes, causing the first-start and restarted games to diverge.

The fix introduces a m_hasSavedOriginalSetup flag on GameSlot. On the first call to startNewGame the pre-randomisation slot values are captured via saveOriginalSetup(). On any subsequent call (i.e. a restart), the slots are reset to those saved values before populateRandomSideAndColor / populateRandomStartPosition run, so the seed is consumed identically to the first start and determinism is preserved.

Key changes:

  • GameSlot::reset() now initialises m_hasSavedOriginalSetup = FALSE, ensuring the flag is clean for fresh game sessions.
  • saveOffOriginalInfo() is renamed to saveOriginalSetup() (cleaner name) and sets the flag to TRUE upon completion.
  • startNewGame in both Generals and GeneralsMD replaces the unconditional saveOriginalSetup() call with a flag-guarded branch: save on first start, restore on restarts.
  • A DEBUG_ASSERTCRASH guards the restore path to catch unexpected non-skirmish restarts early.
  • A toString(GameMode) helper is added to produce human-readable mode names in the assert message.

Confidence Score: 5/5

Safe to merge — the fix is logically sound, the flag is correctly initialised on reset, and the assert provides a useful safety net for unexpected non-skirmish restarts.

No P0/P1 issues found. The restore logic correctly resets slots to their pre-randomisation state before the seed-consuming populate calls, which achieves the stated determinism goal. The m_hasSavedOriginalSetup flag is initialised in reset(), so fresh sessions start clean. The setPlayerTemplate side-effect (resetting m_startPos for observer slots) is benign because the stored m_origStartPos for observers would already be -1. All remaining observations are P2 or lower.

No files require special attention.

Important Files Changed

Filename Overview
Core/GameEngine/Include/GameNetwork/GameInfo.h Adds m_hasSavedOriginalSetup bool field and renames saveOffOriginalInfo to saveOriginalSetup; exposes new hasSavedOriginalSetup() accessor.
Core/GameEngine/Source/GameNetwork/GameInfo.cpp Initialises m_hasSavedOriginalSetup to FALSE in reset(), sets it to TRUE at the end of saveOriginalSetup(), and updates call-site rename inside SkirmishGameInfo::xfer.
Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Adds toString(GameMode) helper and replaces the unconditional saveOriginalSetup() call in startNewGame with the flag-guarded restore-or-save pattern for deterministic restarts.
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Mirror of Generals change: identical toString(GameMode) helper and restore-or-save logic added to Zero Hour's startNewGame.
Generals/Code/GameEngine/Include/GameLogic/GameLogic.h Adds const char* toString(GameMode mode) free-function declaration.
GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h Mirror of Generals header: adds const char* toString(GameMode mode) declaration.

Sequence Diagram

sequenceDiagram
    participant GL as GameLogic::startNewGame
    participant GS as GameSlot
    participant Rng as populateRandom*

    Note over GL,Rng: First start
    GL->>GS: hasSavedOriginalSetup() → false
    GL->>GS: saveOriginalSetup() [stores color/pos/template, sets flag=true]
    GL->>Rng: populateRandomSideAndColor / populateRandomStartPosition
    Rng->>GS: setColor / setStartPos / setPlayerTemplate (resolved values)

    Note over GL,Rng: Restart
    GL->>GS: hasSavedOriginalSetup() → true
    GL->>GS: setColor(origColor) / setStartPos(origPos) / setPlayerTemplate(origTemplate)
    Note over GS: Slots reset to pre-random values
    GL->>Rng: populateRandomSideAndColor / populateRandomStartPosition
    Rng->>GS: setColor / setStartPos / setPlayerTemplate (same resolved values as first start)
Loading

Reviews (6): Last reviewed commit: "Changed from 'getSaveOriginalSetup' to '..." | Re-trigger Greptile

Copy link
Copy Markdown

@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.

OK

Needs to be replicated in Generals

Copy link
Copy Markdown

@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.

Greptile is still complaining about something?

@Caball009
Copy link
Copy Markdown
Author

Greptile is still complaining about something?

Yes, that was a good point. I think it could have affected the randomization of AI players if a game was loaded from inside another game or replay.

m_slot[slot]->setState((SlotState)state, name);
if (isAccepted) m_slot[slot]->setAccept();
m_slot[slot]->setPlayerTemplate(origPlayerTemplate);
m_slot[slot]->setStartPos(origStartPos);
m_slot[slot]->setColor(origColor);
m_slot[slot]->saveOffOriginalInfo();

It should work correctly now because GameSlot::setState sets m_saveOffOriginalInfo back to true before GameSlot::saveOffOriginalInfo is called.


Replication in Generals still needs doing.

@Caball009 Caball009 force-pushed the fix_reset_slot_values_for_logical_seed branch from e5d4698 to 0239428 Compare March 27, 2026 15:20
@Caball009
Copy link
Copy Markdown
Author

I can replicate in Generals unless there are other changes desired.

@xezon xezon merged commit d31c6c5 into TheSuperHackers:main Apr 4, 2026
23 checks passed
@Caball009 Caball009 deleted the fix_reset_slot_values_for_logical_seed branch April 4, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix Is fixing something, but is not user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants