Fix restart fast-forwarding by the time spent in a modal dialog
The Game Over, Win, and escape-menu Restart paths did not rebase the wall-clock time source. Time spent with one of those dialogs open accumulated in the frame timer and, on the first frame after the reset, was converted into ticks on the fresh simulation, fast-forwarding the new run by the dialog-open duration. resetForNewGame() now restarts m_frameTimer and resets m_tickDriver so a new run starts from a clean time base. The drained-ResetCommand path (Game Over / escape) also samples elapsed before the reset applies, so onFrame() now skips the tick advance on the frame it resets, discarding that stale delta. Adds a TickDriver regression test for the invariant the fix relies on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -131,3 +131,26 @@ TEST_CASE("TickDriver::reset clears the accumulator", "[simulation]")
|
||||
// Nothing in the accumulator: zero elapsed time should not fire.
|
||||
REQUIRE(driver.advance(0.0, 1.0) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("TickDriver::reset discards a large pending delta so a restart does not "
|
||||
"fast-forward", "[simulation]")
|
||||
{
|
||||
// Regression guard for the "restart fast-forwards by the time spent in the
|
||||
// Game Over / Win / escape dialog" bug: on restart the frame timer holds the
|
||||
// whole wall-clock duration the modal was open. GameWorldView::resetForNewGame()
|
||||
// now calls TickDriver::reset() to discard that pending delta; without it the
|
||||
// fresh run would burst forward by the dialog duration on its first frame.
|
||||
TickDriver driver;
|
||||
|
||||
// 30 seconds spent in the dialog would otherwise be ~900 ticks at 30 Hz.
|
||||
const double dialogOpenMs = 30000.0;
|
||||
driver.reset();
|
||||
|
||||
// A brand-new run's first normal frame (~16 ms) advances only its own ticks;
|
||||
// the discarded dialog time contributes nothing.
|
||||
REQUIRE(driver.advance(16.0, 1.0) == 0);
|
||||
|
||||
// Sanity: had the dialog delta not been discarded, it would have fired ~900 ticks.
|
||||
TickDriver leaked;
|
||||
REQUIRE(leaked.advance(dialogOpenMs, 1.0) > 800);
|
||||
}
|
||||
|
||||
@@ -227,6 +227,12 @@ void GameWorldView::onFrame()
|
||||
{
|
||||
m_viewResetPending = false;
|
||||
resetForNewGame();
|
||||
// The reset command may have drained after a long-open modal (e.g. the
|
||||
// Game Over dialog), so `elapsed` above still holds the whole time that
|
||||
// dialog was open. Feeding it into the tick driver would fast-forward the
|
||||
// brand-new run by that duration. resetForNewGame() has rebased the time
|
||||
// source; skip this frame's tick advance so the stale delta is discarded.
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify presentation widgets that queued commands were applied, so a
|
||||
@@ -2391,6 +2397,12 @@ void GameWorldView::resetForNewGame()
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
|
||||
setGameSpeed(1.0);
|
||||
// Rebase the wall-clock time source so a fresh run starts from a clean time
|
||||
// base. Without this, wall time accumulated while a modal (Game Over, Win, or
|
||||
// the escape menu) was open would be converted into ticks on the new run,
|
||||
// fast-forwarding it by the time the player spent in the dialog.
|
||||
m_frameTimer.restart();
|
||||
m_tickDriver.reset();
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user