diff --git a/docs/architecture.md b/docs/architecture.md index fef47f9..655b949 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -97,6 +97,12 @@ All UI interactions — building selection, builder/blueprint mode transitions, Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`). +### Reading Simulation State + +The simulation is the single source of truth for every game value (building block stock, expansion cost, threat level, tick, etc.). A UI widget that needs such a value holds the `Simulation*` it was constructed with and **pulls the value on demand** via the corresponding getter (e.g., `m_sim->getBuildingBlocksStock()`), rather than caching its own copy. + +State-change events (e.g., `BuildingBlocksChangedEvent`) are treated as *refresh signals*, not as carriers of truth: a widget subscribes to the event to learn *when* the value changed and then re-reads it from the simulation to learn *what* it now is. The value carried in the event payload is not authoritative and should not be stored. This keeps a single copy of each value and avoids stale-cache bugs (a widget acting on a value that has since moved on because nothing refreshed its local copy). + ## Tick Order Within a single simulation tick, subsystems run in this fixed order. The order is load-bearing for determinism and for avoiding one-tick-delay artifacts (e.g., items landing on a belt but not advancing in the same tick). diff --git a/src/ui/BuildButtonGrid.cpp b/src/ui/BuildButtonGrid.cpp index 1e01449..98c08ae 100644 --- a/src/ui/BuildButtonGrid.cpp +++ b/src/ui/BuildButtonGrid.cpp @@ -12,12 +12,13 @@ #include "DisplayName.h" #include "EventManager.h" #include "ExitBuilderModeRequestedEvent.h" +#include "Simulation.h" -BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent) +BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent) : QWidget(parent) + , m_sim(sim) , m_config(config) - , m_activeIndex(-1) { QGridLayout* layout = new QGridLayout(this); layout->setSpacing(4); @@ -79,24 +80,42 @@ BuildButtonGrid::~BuildButtonGrid() unregisterForEvents(); } -void BuildButtonGrid::updateAffordability(int buildingBlocks) +void BuildButtonGrid::updateAffordability() { + const int buildingBlocks = m_sim->getBuildingBlocksStock(); + + // If the currently selected tool can no longer be afforded, exit builder mode + // before recomputing button states so it does not stay selected. Clearing the + // active index first lets the loop below disable the now-unaffordable button. + if (m_activeIndex) + { + const BuildingType activeType = m_types[*m_activeIndex]; + const std::map::const_iterator it = m_costs.find(activeType); + const int cost = (it != m_costs.end()) ? it->second : 0; + if (buildingBlocks < cost) + { + clearActiveButton(); + EventManager::getInstance()->sendEventImmediately( + std::make_shared()); + } + } + for (std::size_t i = 0; i < m_buttons.size(); ++i) { const BuildingType type = m_types[i]; const std::map::const_iterator it = m_costs.find(type); const int cost = (it != m_costs.end()) ? it->second : 0; - m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == static_cast(i)); + m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == i); } } void BuildButtonGrid::clearActiveButton() { - if (m_activeIndex >= 0 && m_activeIndex < static_cast(m_buttons.size())) + if (m_activeIndex) { - m_buttons[static_cast(m_activeIndex)]->setChecked(false); + m_buttons[*m_activeIndex]->setChecked(false); } - m_activeIndex = -1; + m_activeIndex.reset(); } void BuildButtonGrid::onBuildButton(int index) @@ -105,8 +124,9 @@ void BuildButtonGrid::onBuildButton(int index) { return; } + const std::size_t idx = static_cast(index); - if (m_activeIndex == index) + if (m_activeIndex == idx) { clearActiveButton(); EventManager::getInstance()->sendEventImmediately( @@ -114,15 +134,15 @@ void BuildButtonGrid::onBuildButton(int index) return; } - if (m_activeIndex >= 0 && m_activeIndex < static_cast(m_buttons.size())) + if (m_activeIndex) { - m_buttons[static_cast(m_activeIndex)]->setChecked(false); + m_buttons[*m_activeIndex]->setChecked(false); } - m_activeIndex = index; - m_buttons[static_cast(index)]->setChecked(true); + m_activeIndex = idx; + m_buttons[idx]->setChecked(true); EventManager::getInstance()->sendEventImmediately( - std::make_shared(m_types[static_cast(index)])); + std::make_shared(m_types[idx])); } void BuildButtonGrid::handleEvent(std::shared_ptr /*event*/) @@ -130,6 +150,11 @@ void BuildButtonGrid::handleEvent(std::shared_ptr clearActiveButton(); } +void BuildButtonGrid::handleEvent(std::shared_ptr /*event*/) +{ + updateAffordability(); +} + void BuildButtonGrid::handleEvent(std::shared_ptr event) { m_demolishButton->setChecked(event->active); diff --git a/src/ui/BuildButtonGrid.h b/src/ui/BuildButtonGrid.h index ec81fa4..7bfbc0d 100644 --- a/src/ui/BuildButtonGrid.h +++ b/src/ui/BuildButtonGrid.h @@ -1,46 +1,56 @@ #pragma once #include +#include #include #include #include "BuilderModeExitedEvent.h" #include "BuildHotkeyPressedEvent.h" +#include "BuildingBlocksChangedEvent.h" #include "BuildingType.h" #include "DemolishModeChangedEvent.h" #include "EventHandler.h" #include "GameConfig.h" class QPushButton; +class Simulation; class BuildButtonGrid : public QWidget, public CombinedEventHandler + BuildHotkeyPressedEvent, + BuildingBlocksChangedEvent> { Q_OBJECT public: - BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr); + BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr); ~BuildButtonGrid() override; - void updateAffordability(int buildingBlocks); void clearActiveButton(); private: + // Re-evaluates which build buttons are enabled from the current building block + // stock (read from the simulation). If the currently selected tool can no longer + // be afforded, it exits builder mode so the button does not stay selected. + void updateAffordability(); + void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; + void handleEvent(std::shared_ptr event) override; private slots: void onBuildButton(int index); private: + Simulation* m_sim; const GameConfig* m_config; std::vector m_types; std::vector m_buttons; std::map m_costs; - int m_activeIndex; + std::optional m_activeIndex; QPushButton* m_demolishButton; }; diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 1b24069..c6c53ff 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -14,7 +14,6 @@ #include "BlueprintPanel.h" #include "BuildButtonGrid.h" -#include "BuildingBlocksChangedEvent.h" #include "BuildingSystem.h" #include "Command.h" #include "CommandRequestedEvent.h" @@ -53,7 +52,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, sideLayout->setSpacing(1); m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel); - m_buildButtonGrid = new BuildButtonGrid(&sim->getConfig(), m_sidePanel); + m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), m_sidePanel); m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel); sideLayout->addWidget(m_selectedBuildingPanel, 1); @@ -151,11 +150,6 @@ void MainWindow::layoutPanels() m_dimOverlay->setGeometry(0, 0, totalW, totalH); } -void MainWindow::handleEvent(std::shared_ptr event) -{ - m_buildButtonGrid->updateAffordability(event->blocks); -} - void MainWindow::handleEvent(std::shared_ptr event) { const double prevSpeed = m_gameWorldView->getGameSpeed(); diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index dfa5632..37e8930 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -6,7 +6,6 @@ #include -#include "BuildingBlocksChangedEvent.h" #include "BuildingId.h" #include "EscapeMenuRequestedEvent.h" #include "EventHandler.h" @@ -32,8 +31,7 @@ class QCloseEvent; class QResizeEvent; class MainWindow : public QWidget, - public CombinedEventHandler event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override;