Skip to content

bugfix(logic): Restore retail compatibility after change to frozen time check#2803

Open
Caball009 wants to merge 2 commits into
TheSuperHackers:mainfrom
Caball009:fix_bug_time_frozen
Open

bugfix(logic): Restore retail compatibility after change to frozen time check#2803
Caball009 wants to merge 2 commits into
TheSuperHackers:mainfrom
Caball009:fix_bug_time_frozen

Conversation

@Caball009

@Caball009 Caball009 commented Jun 18, 2026

Copy link
Copy Markdown

#1528 introduced a small change to the game logic as is shown below.

Implementation before approximately:

TheScriptEngine->update();

if (!TheScriptEngine->isTimeFrozenScript())
{
    TheGameLogic->update();
}

Implementation after approximately:

m_isTimeFrozen = TheScriptEngine->isTimeFrozenScript();

TheScriptEngine->update();

if (!m_isTimeFrozen)
{
    TheGameLogic->update();
}

Replay vc6_replay_scripted_frozen_time.zip on map "[Mod] Football v3" mismatches because of that change. This PR fixes the mismatch.

@Caball009 Caball009 added Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour ThisProject The issue was introduced by this project, or this task is specific to this project Script Is related to Script Engine, SCB labels Jun 18, 2026
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a replay mismatch introduced by PR #1528, where the frozen-time state was captured before the script engine ran, causing the rest of game logic to incorrectly use a stale value. The fix moves the frozen-time re-evaluation inside GameLogic::update(), immediately after TheScriptEngine->UPDATE(), so that any freeze toggled by a script during that frame takes effect before terrain, object, and client updates run.

  • GameLogic.cpp (both targets): After TheScriptEngine->UPDATE(), TheFramePacer->setTimeFrozen() is called again to pick up any state change made by scripts, followed by an early return if time is now frozen.
  • GameEngine.cpp (both targets): The three-variable canUpdate / canUpdateLogic / canUpdateScript pattern is replaced with a single canUpdateGameLogic(IgnoreFrozenTime) call that always enters GameLogic->UPDATE() (letting the script engine run), and a post-call check on isTimeFrozen() gates TheGameClient->step().

Confidence Score: 5/5

Safe to merge; the fix correctly restores the pre-#1528 ordering by re-evaluating frozen time after the script engine runs rather than before.

Both code paths (Generals and GeneralsMD) are changed symmetrically. The root cause of the replay mismatch — stale pre-script frozen time being used to gate the rest of the logic tick — is directly addressed. Removing IgnoreHaltedGame from the outer canUpdateGameLogic call is safe: for network games canUpdateNetworkGameLogic gates on frame-data readiness rather than the flag, and for regular games a halted state causes getActualLogicTimeScaleFps to return 0, which propagates as IEEE-754 infinity through the accumulator check and correctly blocks the update without any crash. The change is narrow and well-scoped.

No files require special attention.

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Adds a post-script-engine frozen-time refresh and early return inside GameLogic::update(), correctly gating all downstream updates on the state set by the just-run scripts.
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp Identical fix as the Generals counterpart; symmetric and correct for the Zero Hour expansion.
Generals/Code/GameEngine/Source/Common/GameEngine.cpp Simplifies the update dispatch by removing the separate script-only branch; IgnoreFrozenTime flag ensures timing still fires when frozen, while a post-UPDATE() freeze check gates the client step.
GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp Mirror of the Generals fix; no divergence between the two codebases.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GE as GameEngine::update()
    participant GL as GameLogic::UPDATE()
    participant SE as ScriptEngine::UPDATE()
    participant FP as FramePacer
    participant GC as GameClient

    GE->>FP: "canUpdateGameLogic(IgnoreFrozenTime)<br/>sets initial timeFrozen state"
    alt timing gate passes
        GE->>GL: "TheGameLogic->UPDATE()"
        GL->>SE: "TheScriptEngine->UPDATE()<br/>(may toggle frozen time)"
        GL->>FP: "setTimeFrozen(TheGameEngine->isTimeFrozen())<br/>re-evaluates AFTER script ran"
        alt time is now frozen
            GL-->>GE: return early (skip terrain, objects, etc.)
        else not frozen
            GL->>GL: continue terrain/object/CRC updates
            GL-->>GE: return normally
        end
        GE->>FP: isTimeFrozen()?
        alt not frozen
            GE->>GC: "TheGameClient->step()"
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GE as GameEngine::update()
    participant GL as GameLogic::UPDATE()
    participant SE as ScriptEngine::UPDATE()
    participant FP as FramePacer
    participant GC as GameClient

    GE->>FP: "canUpdateGameLogic(IgnoreFrozenTime)<br/>sets initial timeFrozen state"
    alt timing gate passes
        GE->>GL: "TheGameLogic->UPDATE()"
        GL->>SE: "TheScriptEngine->UPDATE()<br/>(may toggle frozen time)"
        GL->>FP: "setTimeFrozen(TheGameEngine->isTimeFrozen())<br/>re-evaluates AFTER script ran"
        alt time is now frozen
            GL-->>GE: return early (skip terrain, objects, etc.)
        else not frozen
            GL->>GL: continue terrain/object/CRC updates
            GL-->>GE: return normally
        end
        GE->>FP: isTimeFrozen()?
        alt not frozen
            GE->>GC: "TheGameClient->step()"
        end
    end
Loading

Reviews (3): Last reviewed commit: "Replicated in Generals." | Re-trigger Greptile

@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 is unfortunate. Can we fix this somehow without moving the early return back into the Game Logic Update function? It makes the logic more complicated to understand again. It would be good to implement it in simple terms.

@Caball009

Copy link
Copy Markdown
Author

This is unfortunate. Can we fix this somehow without moving the early return back into the Game Logic Update function? It makes the logic more complicated to understand again. It would be good to implement it in simple terms.

I don't see how to avoid the early return in a way that doesn't have major downsides.

@xezon

xezon commented Jun 21, 2026

Copy link
Copy Markdown

What downsides do you see?

@Caball009

Caball009 commented Jun 21, 2026

Copy link
Copy Markdown
Author

What downsides do you see?

Either code duplication, or a potential risk of game logic changes.

If you want to keep the exact same logic, you'd need to keep these in place:

  1. GameLogic::m_isInUpdate is set to true.
  2. GameClient::m_frame is updated.
  3. setFPMode resets the floating point state.
  4. ScriptEngine::update always runs directly after loading a map.

Only solution I see at the moment to keep the changes as is, or perhaps put the code up to the script engine update in a separate function, although I'm not entirely sure if that's going to work.

@Caball009 Caball009 added Major Severity: Minor < Major < Critical < Blocker and removed Minor Severity: Minor < Major < Critical < Blocker labels Jun 22, 2026
@Caball009

Copy link
Copy Markdown
Author

Unless you have a better suggestion, can we just merge this?

I can create an issue / task to refactor this so that we can perhaps find a better way to implement this in the future.

@xezon

xezon commented Jun 28, 2026

Copy link
Copy Markdown

As last resort, maybe only do the hack in RETAIL_COMPATIBLE_CRC builds?

@Caball009

Caball009 commented Jun 28, 2026

Copy link
Copy Markdown
Author

As last resort, maybe only do the hack in RETAIL_COMPATIBLE_CRC builds?

The non-retail compatible code would always be one frame behind with freezing and unfreezing time; e.g. if a script is supposed to freeze the game at frame 1000 and unfreeze at frame 1100, it actually does that at frames 1001 and 1101. It probably is rarely used so we didn't notice it before, but I don't like the idea of keeping that unexpected behavior.

@xezon

xezon commented Jul 4, 2026

Copy link
Copy Markdown

I took the liberty and polished the code a bit, but fundamentally I have no better idea than what you already had.

Can you test it again?

@Caball009

Caball009 commented Jul 4, 2026

Copy link
Copy Markdown
Author
TheGameLogic->UPDATE();

if (!TheFramePacer->isTimeFrozen())
{
	TheGameClient->step();
}
  1. You put GameClient::step after GameLogic::update. I think that makes sense, but that's not the way it was before.
  2. GameClient::step eventually processes W3DView::stepView, which updates camera shake variables. ScriptActions::doScreenShake can also trigger shakes, so does it make sense to keep the if (!TheFramePacer->isTimeFrozen()) check?
    In other words, is the script engine supposed to be able to trigger camera shakes when time is frozen?

@Caball009 Caball009 force-pushed the fix_bug_time_frozen branch from 5888813 to cc04254 Compare July 4, 2026 21:31
@Caball009

Caball009 commented Jul 4, 2026

Copy link
Copy Markdown
Author

I found no mismatches with cc04254.

@xezon

xezon commented Jul 5, 2026

Copy link
Copy Markdown

I do not expect that the camera can shake during frozen time. The 3D World is supposed to be frozen. Camera shakes typically happens because of battles. Do you see a use case for camera shake during frozen time?

@Caball009

Copy link
Copy Markdown
Author

Do you see a use case for camera shake during frozen time?

No, I don't see any at the moment.

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

I tested game pause and frozen time and it worked.

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

Labels

Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker Script Is related to Script Engine, SCB ThisProject The issue was introduced by this project, or this task is specific to this project ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants