From d5ba72d55475dd55fe9e70d0a5630daf451f9448 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Mon, 3 Aug 2026 21:11:52 +0200 Subject: [PATCH] add ModalPauseScope for the pause-around-modal idiom --- src/ui/CMakeLists.txt | 1 + src/ui/MainWindow.cpp | 38 ++++++++------------------- src/ui/ModalPauseScope.h | 55 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 src/ui/ModalPauseScope.h diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 96d8679..738218b 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -4,6 +4,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h ${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.h + ${CMAKE_CURRENT_SOURCE_DIR}/ModalPauseScope.h ${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h ${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 896eb59..2b2acb5 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -28,6 +28,7 @@ #include "ShipLayoutBlueprintSerializer.h" #include "ShipLayoutDialog.h" #include "ItemIconCache.h" +#include "ModalPauseScope.h" #include "Simulation.h" #include "Tick.h" #include "VisualsLoader.h" @@ -167,8 +168,7 @@ void MainWindow::layoutPanels() void MainWindow::handleEvent(std::shared_ptr event) { - const double prevSpeed = m_gameWorldView->getGameSpeed(); - m_gameWorldView->setGameSpeed(0.0); + ModalPauseScope pause(*m_gameWorldView); ModalDimScope dim(*m_dimOverlay); SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this); @@ -179,15 +179,11 @@ void MainWindow::handleEvent(std::shared_ptrchoiceIndex = dialog.getChosenIndex(); EventManager::getInstance()->sendEventImmediately( std::make_shared(command)); - - m_gameWorldView->setGameSpeed(prevSpeed); - m_gameWorldView->resetFrameTimer(); } void MainWindow::handleEvent(std::shared_ptr /*event*/) { - const double prevSpeed = m_gameWorldView->getGameSpeed(); - m_gameWorldView->setGameSpeed(0.0); + ModalPauseScope pause(*m_gameWorldView); ModalDimScope dim(*m_dimOverlay); QMessageBox box(this); @@ -204,12 +200,13 @@ void MainWindow::handleEvent(std::shared_ptr /*e std::optional newConfig = reloadConfig(); if (!newConfig.has_value()) { - m_gameWorldView->setGameSpeed(prevSpeed); - m_gameWorldView->resetFrameTimer(); return; } // Restart is a command boundary; the view resets when the drain applies // it (see GameWorldView::onFrame). A fresh random seed starts a new run. + // resetForNewGame() sets the speed for the new run, so the pre-restart + // speed is deliberately not restored here. + pause.release(); std::shared_ptr command = std::make_shared(); command->config = std::make_shared(std::move(*newConfig)); command->seed = std::random_device{}(); @@ -218,13 +215,9 @@ void MainWindow::handleEvent(std::shared_ptr /*e } else if (clicked == quitBtn) { + pause.release(); close(); } - else - { - m_gameWorldView->setGameSpeed(prevSpeed); - m_gameWorldView->resetFrameTimer(); - } } std::optional MainWindow::reloadConfig() @@ -252,8 +245,7 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId, const std::string& schematicId, const ShipLayoutConfig& currentLayout) { - const double prevSpeed = m_gameWorldView->getGameSpeed(); - m_gameWorldView->setGameSpeed(0.0); + ModalPauseScope pause(*m_gameWorldView); std::set unlockedModuleIds; for (const ModuleDef& def : m_sim->getConfig().modules.modules) @@ -279,9 +271,6 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId, EventManager::getInstance()->sendEventImmediately( std::make_shared(command)); } - - m_gameWorldView->setGameSpeed(prevSpeed); - m_gameWorldView->resetFrameTimer(); } void MainWindow::handleEvent(std::shared_ptr event) @@ -311,8 +300,7 @@ void MainWindow::handleEvent(std::shared_ptr e void MainWindow::handleEvent(std::shared_ptr event) { - const double prevSpeed = m_gameWorldView->getGameSpeed(); - m_gameWorldView->setGameSpeed(0.0); + ModalPauseScope pause(*m_gameWorldView); // A construction site has no Building yet; fall back to its site record so // the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG). @@ -321,8 +309,6 @@ void MainWindow::handleEvent(std::shared_ptrgetBuildings().findSite(event->buildingId); if (!b && !s) { - m_gameWorldView->setGameSpeed(prevSpeed); - m_gameWorldView->resetFrameTimer(); return; } @@ -361,13 +347,11 @@ void MainWindow::handleEvent(std::shared_ptrsetGameSpeed(prevSpeed); - m_gameWorldView->resetFrameTimer(); - // The SetRecipeCommand above is queued (drains on a later frame) and clears // the shipyard's layout, so open the dialog with the chosen schematic and an // empty layout rather than reading the not-yet-updated building state. Speed - // is already restored so the helper snapshots the real speed to restore. + // is restored first so the helper snapshots the real speed to restore. + pause.restore(); if (autoOpenLayout) { openShipLayoutDialog(event->buildingId, chosenSchematic, ShipLayoutConfig{}); diff --git a/src/ui/ModalPauseScope.h b/src/ui/ModalPauseScope.h new file mode 100644 index 0000000..c3085fc --- /dev/null +++ b/src/ui/ModalPauseScope.h @@ -0,0 +1,55 @@ +#pragma once + +#include "GameWorldView.h" + +// RAII guard for the "pause the game while a modal is open" idiom (REQ-UI-SPEED). +// Constructing it snapshots the current game speed and pauses the game; on scope +// exit it restores the snapshotted speed and rebases the render frame timer, so the +// wall time the player spent in the dialog is not converted into simulation ticks. +// +// Pairs with ModalDimScope, which the same call sites use for the dim overlay. +// +// Two escape hatches for the paths that must not simply restore at scope exit: +// restore() — restore now instead of at scope exit, for when more work has to run +// at the player's real speed before the scope ends (e.g. a follow-up +// dialog that snapshots the speed itself). +// release() — abandon the restore entirely, for when the game is about to be +// reset or the window closed and the old speed is meaningless. +// Both are idempotent and the destructor does nothing once either has run. +class ModalPauseScope +{ +public: + explicit ModalPauseScope(GameWorldView& view) + : m_view(view) + , m_previousGameSpeed(view.getGameSpeed()) + , m_restorePending(true) + { + m_view.setGameSpeed(0.0); + } + + ~ModalPauseScope() + { + restore(); + } + + void restore() + { + if (!m_restorePending) { return; } + m_restorePending = false; + m_view.setGameSpeed(m_previousGameSpeed); + m_view.resetFrameTimer(); + } + + void release() + { + m_restorePending = false; + } + + ModalPauseScope(const ModalPauseScope&) = delete; + ModalPauseScope& operator=(const ModalPauseScope&) = delete; + +private: + GameWorldView& m_view; + double m_previousGameSpeed; + bool m_restorePending; +};