replay: route player input through a command chokepoint (Phase 1)
Reshapes every UI-driven sim mutation to flow through one path so it can be recorded and replayed later, with behaviour unchanged. - Command model (lib): Command base + derived types (PlaceBuilding, Demolish, RotateInPlace, SetRecipe, SetShipLayout, Set[Site]SplitterFilters, ClearBeltTiles, ApplySchematicChoice, Reset), each with a playerId for the future-multiplayer shape. PlaceBuilding is atomic (carries optional recipe/layout/filters) because deferred commands never return the new BuildingId to the caller. - CommandManager (lib): FIFO queue holding a Simulation&; enqueue + drain. - Simulation::apply(const Command&): the single chokepoint, dispatching by kind to the existing mutators. Mutators stay public (enforced by convention, not compile-time, so the test suite keeps driving the sim directly). - Timing: GameWorldView owns the CommandManager and drains it once per frame in onFrame, before the tick batch (runs at 0x too, so build-while-paused is preserved). A drained Reset triggers the view reset. - UI fan-in: GameWorldView enqueues its own input directly; MainWindow and SelectedBuildingPanel emit CommandRequestedEvent, which GameWorldView subscribes to and enqueues. No UI site mutates the sim directly anymore. - CommandTest: asserts apply(...) yields byte-identical state to the direct mutator path, and that CommandManager drains FIFO through apply. Full suite green (338 cases / 3353 assertions); determinism double-run still passes. Design doc updated with the atomic-PlaceBuilding and convention- enforcement decisions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUsFgd2Ga6pmLz8giS8WUn
This commit is contained in:
@@ -15,7 +15,10 @@
|
||||
#include "BuildButtonGrid.h"
|
||||
#include "BuildingBlocksChangedEvent.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Command.h"
|
||||
#include "CommandRequestedEvent.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "EventManager.h"
|
||||
#include "GameWorldView.h"
|
||||
#include "RecipeSelectionDialog.h"
|
||||
#include "SchematicChoiceDialog.h"
|
||||
@@ -138,7 +141,11 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
|
||||
SchematicChoiceDialog dialog(event->choices, this);
|
||||
dialog.exec();
|
||||
|
||||
m_sim->applySchematicChoice(dialog.getChosenIndex());
|
||||
std::shared_ptr<ApplySchematicChoiceCommand> command =
|
||||
std::make_shared<ApplySchematicChoiceCommand>();
|
||||
command->choiceIndex = dialog.getChosenIndex();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
@@ -160,12 +167,13 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == restartBtn)
|
||||
{
|
||||
std::shared_ptr<GameConfig> newConfig;
|
||||
try
|
||||
{
|
||||
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||
newConfig = std::make_shared<GameConfig>(
|
||||
ConfigLoader::loadFromDirectory(m_configDir));
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_sim->reset(std::move(newConfig));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@@ -175,7 +183,12 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
return;
|
||||
}
|
||||
m_gameWorldView->resetForNewGame();
|
||||
// Restart is a command boundary; the view resets when the drain applies
|
||||
// it (see GameWorldView::onFrame). Seed stays 0 in Phase 1.
|
||||
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
||||
command->config = std::move(newConfig);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else if (clicked == quitBtn)
|
||||
{
|
||||
@@ -234,7 +247,12 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
||||
this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||
{
|
||||
m_sim->buildings().setShipLayout(event->shipyardId, *dialog.result());
|
||||
std::shared_ptr<SetShipLayoutCommand> command =
|
||||
std::make_shared<SetShipLayoutCommand>();
|
||||
command->id = event->shipyardId;
|
||||
command->layout = *dialog.result();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
@@ -268,7 +286,11 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
RecipeSelectionDialog dialog(options, title, this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
||||
{
|
||||
m_sim->buildings().setRecipe(event->buildingId, *dialog.getChosenId());
|
||||
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
||||
command->id = event->buildingId;
|
||||
command->recipeId = *dialog.getChosenId();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
@@ -293,12 +315,13 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
|
||||
if (box.clickedButton() == restartBtn)
|
||||
{
|
||||
std::shared_ptr<GameConfig> newConfig;
|
||||
try
|
||||
{
|
||||
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||
newConfig = std::make_shared<GameConfig>(
|
||||
ConfigLoader::loadFromDirectory(m_configDir));
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_sim->reset(std::move(newConfig));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@@ -306,7 +329,11 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
tr("Failed to reload config:\n%1").arg(e.what()));
|
||||
return;
|
||||
}
|
||||
m_gameWorldView->resetForNewGame();
|
||||
// Restart is a command boundary; the view resets when the drain applies it.
|
||||
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
||||
command->config = std::move(newConfig);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user