Replay: deterministic record & playback (#4)
Add deterministic record/playback for a run. Recording captures `(seed, config hash, ordered tick-tagged commands)` and re-simulates on playback — no state snapshots. `DotaFactory.exe --replay <file>` re-plays a recorded run view-only with manual speed/pause. Reviewed-on: #4 Co-authored-by: Malte Langkabel <malte.langkabel@gmail.com> Co-committed-by: Malte Langkabel <malte.langkabel@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
@@ -6,10 +6,12 @@
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QColor>
|
||||
#include <QCursor>
|
||||
#include <QDir>
|
||||
#include <QFont>
|
||||
#include <QKeyEvent>
|
||||
#include <QMessageBox>
|
||||
@@ -24,6 +26,10 @@
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Command.h"
|
||||
#include "ReplayPlayer.h"
|
||||
#include "ReplayReader.h"
|
||||
#include "ReplayRecorder.h"
|
||||
#include "DemolishModeChangedEvent.h"
|
||||
#include "EntityHitTest.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
@@ -112,11 +118,13 @@ QPoint portBodyTile(QPoint portTile, Rotation direction)
|
||||
|
||||
|
||||
GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals, QWidget* parent)
|
||||
const VisualsConfig* visuals, const std::string& configDir,
|
||||
const ParsedReplay* replay, QWidget* parent)
|
||||
: QOpenGLWidget(parent)
|
||||
, m_sim(sim)
|
||||
, m_config(config)
|
||||
, m_visuals(visuals)
|
||||
, m_commandManager(*sim)
|
||||
, m_gameSpeedMultiplier(1.0)
|
||||
, m_prevNonZeroSpeed(1.0)
|
||||
, m_scrollXTiles(0.0f)
|
||||
@@ -143,6 +151,25 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
m_frameTimer.start();
|
||||
|
||||
registerForEvents();
|
||||
|
||||
if (replay)
|
||||
{
|
||||
// View-only playback: ignore live input and drive ticks from the recorded
|
||||
// stream. No recorder (we are not creating a new run).
|
||||
m_commandManager.setReplayMode(true);
|
||||
m_replayPlayer = std::make_unique<ReplayPlayer>(*sim, replay->entries);
|
||||
m_replayPlayer->start(); // process tick-0 entries before the first tick
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record every run to disk. Replays live under <data>/replays, alongside
|
||||
// the config dir that was loaded. Attaching the recorder opens the first
|
||||
// file and writes the header for the initial run.
|
||||
QDir replayDir(QString::fromStdString(configDir));
|
||||
replayDir.cdUp();
|
||||
m_commandManager.setRecorder(std::make_unique<ReplayRecorder>(
|
||||
configDir, replayDir.filePath("replays").toStdString()));
|
||||
}
|
||||
}
|
||||
|
||||
GameWorldView::~GameWorldView()
|
||||
@@ -159,13 +186,40 @@ void GameWorldView::onFrame()
|
||||
{
|
||||
const qint64 elapsed = m_frameTimer.restart();
|
||||
|
||||
// Advance simulation
|
||||
if (m_replayPlayer)
|
||||
{
|
||||
// Playback: apply recorded commands at their ticks and verify checksums.
|
||||
// Manual speed/pause still works; playback only moves forward.
|
||||
const int ticks = m_tickDriver.advance(
|
||||
static_cast<double>(elapsed), m_gameSpeedMultiplier);
|
||||
for (int i = 0; i < ticks; ++i)
|
||||
{
|
||||
if (m_replayPlayer->isFinished()) { break; }
|
||||
m_sim->tick();
|
||||
m_replayPlayer->advanceTo(m_sim->currentTick());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drain queued player commands once per frame, before the tick batch. This
|
||||
// runs even at 0x so a paused player sees placed construction sites
|
||||
// immediately, while staying deterministic (see docs/replay_design.md).
|
||||
m_commandManager.drain();
|
||||
|
||||
// A drained Reset reinitialized the simulation; reset the view to match.
|
||||
if (m_viewResetPending)
|
||||
{
|
||||
m_viewResetPending = false;
|
||||
resetForNewGame();
|
||||
}
|
||||
|
||||
const int ticks = m_tickDriver.advance(
|
||||
static_cast<double>(elapsed), m_gameSpeedMultiplier);
|
||||
for (int i = 0; i < ticks; ++i)
|
||||
{
|
||||
m_sim->tick();
|
||||
// Periodic checksum (every 30 ticks) for replay desync detection.
|
||||
m_commandManager.recordTickCheckpoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,25 +293,31 @@ void GameWorldView::onFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Schematic choice available
|
||||
if (m_sim->hasSchematicChoicesPending() && !m_schematicChoiceShown)
|
||||
// Sim-state polls are input sources for the live game; in replay these are
|
||||
// gated off (the recorded ApplySchematicChoice resolves the choice with no UI,
|
||||
// and game-over becomes the passive "replay ended" state below).
|
||||
if (!m_replayPlayer)
|
||||
{
|
||||
m_schematicChoiceShown = true;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SchematicChoicesAvailableEvent>(m_sim->getPendingSchematicChoices()));
|
||||
}
|
||||
if (!m_sim->hasSchematicChoicesPending())
|
||||
{
|
||||
m_schematicChoiceShown = false;
|
||||
}
|
||||
// Schematic choice available
|
||||
if (m_sim->hasSchematicChoicesPending() && !m_schematicChoiceShown)
|
||||
{
|
||||
m_schematicChoiceShown = true;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SchematicChoicesAvailableEvent>(m_sim->getPendingSchematicChoices()));
|
||||
}
|
||||
if (!m_sim->hasSchematicChoicesPending())
|
||||
{
|
||||
m_schematicChoiceShown = false;
|
||||
}
|
||||
|
||||
// Game over check
|
||||
if (m_sim->isGameOver() && !m_gameOverShown)
|
||||
{
|
||||
m_gameOverShown = true;
|
||||
m_gameSpeedMultiplier = 0.0;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<GameOverEvent>());
|
||||
// Game over check
|
||||
if (m_sim->isGameOver() && !m_gameOverShown)
|
||||
{
|
||||
m_gameOverShown = true;
|
||||
m_gameSpeedMultiplier = 0.0;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<GameOverEvent>());
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
@@ -283,6 +343,7 @@ void GameWorldView::paintGL()
|
||||
drawBeams(painter);
|
||||
drawOverlays(painter);
|
||||
drawScreenSpace(painter);
|
||||
drawReplayOverlay(painter);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -515,12 +576,22 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
||||
m_sim->buildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
|
||||
if (rotateTarget.has_value())
|
||||
{
|
||||
m_sim->buildings().rotateInPlace(*rotateTarget, bb.rotation);
|
||||
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
|
||||
std::make_shared<RotateInPlaceCommand>();
|
||||
rotateCommand->id = *rotateTarget;
|
||||
rotateCommand->newRotation = bb.rotation;
|
||||
enqueueCommand(rotateCommand);
|
||||
continue;
|
||||
}
|
||||
|
||||
const BuildingId id = m_sim->tryPlaceBuilding(bb.type, anchor, bb.rotation);
|
||||
if (id == kInvalidBuildingId) { continue; }
|
||||
// Place-and-configure is one atomic command: commands apply at a deferred
|
||||
// tick boundary, so the caller never sees the new BuildingId. Unlock
|
||||
// gating stays here (UI-side pre-filter); only fields that should apply
|
||||
// are set on the command.
|
||||
std::shared_ptr<PlaceBuildingCommand> command = std::make_shared<PlaceBuildingCommand>();
|
||||
command->type = bb.type;
|
||||
command->anchor = anchor;
|
||||
command->rotation = bb.rotation;
|
||||
|
||||
if (!bb.recipeId.empty())
|
||||
{
|
||||
@@ -528,7 +599,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
||||
{
|
||||
if (m_sim->isSchematicUnlocked(bb.recipeId))
|
||||
{
|
||||
m_sim->buildings().setRecipe(id, bb.recipeId);
|
||||
command->recipeId = bb.recipeId;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -537,15 +608,12 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
||||
|| bb.type == BuildingType::Assembler;
|
||||
if (!needsUnlockCheck || m_sim->isRecipeUnlocked(bb.recipeId))
|
||||
{
|
||||
m_sim->buildings().setRecipe(id, bb.recipeId);
|
||||
command->recipeId = bb.recipeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bb.shipLayout.has_value())
|
||||
{
|
||||
m_sim->buildings().setShipLayout(id, *bb.shipLayout);
|
||||
}
|
||||
command->shipLayout = bb.shipLayout;
|
||||
|
||||
if (bb.type == BuildingType::Splitter
|
||||
&& (!bb.splitterFilterA.empty() || !bb.splitterFilterB.empty()))
|
||||
@@ -553,11 +621,12 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
||||
// The splitter is still a construction site, so the filters carry
|
||||
// over when it finishes building (REQ-UI-BLUEPRINT-PLACE). Locked
|
||||
// item types are dropped per REQ-LOCK-UI-BLUEPRINT.
|
||||
m_sim->buildings().setSiteSplitterFilters(
|
||||
id,
|
||||
filterUnlockedItems(bb.splitterFilterA, *m_sim),
|
||||
filterUnlockedItems(bb.splitterFilterB, *m_sim));
|
||||
command->hasSplitterFilters = true;
|
||||
command->splitterFilterA = filterUnlockedItems(bb.splitterFilterA, *m_sim);
|
||||
command->splitterFilterB = filterUnlockedItems(bb.splitterFilterB, *m_sim);
|
||||
}
|
||||
|
||||
enqueueCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,49 +647,50 @@ void GameWorldView::placeAtTile(QPoint tile)
|
||||
m_sim->buildings().findRotateInPlaceTarget(type, tile, m_ghostRotation);
|
||||
if (rotateTarget.has_value())
|
||||
{
|
||||
m_sim->buildings().rotateInPlace(*rotateTarget, m_ghostRotation);
|
||||
std::shared_ptr<RotateInPlaceCommand> command =
|
||||
std::make_shared<RotateInPlaceCommand>();
|
||||
command->id = *rotateTarget;
|
||||
command->newRotation = m_ghostRotation;
|
||||
enqueueCommand(command);
|
||||
return;
|
||||
}
|
||||
|
||||
// For placements whose UI follow-up depends on success (belt-drag bookkeeping,
|
||||
// tunnel entry/exit toggle), pre-validate occupancy + affordability so the
|
||||
// optimistic UI update matches what the deferred command will do — isValidPlacement
|
||||
// (above) already covered terrain/bounds.
|
||||
if (type == BuildingType::Belt)
|
||||
{
|
||||
if (m_beltDragTiles.count(tile) > 0)
|
||||
{
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!m_sim->buildings().isTileOccupied(tile))
|
||||
if (!m_sim->buildings().isTileOccupied(tile) && canAfford(type))
|
||||
{
|
||||
const BuildingId id = m_sim->tryPlaceBuilding(
|
||||
type, tile, m_ghostRotation);
|
||||
if (id != kInvalidBuildingId)
|
||||
{
|
||||
m_beltDragTiles.insert(tile);
|
||||
}
|
||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
||||
m_beltDragTiles.insert(tile);
|
||||
}
|
||||
}
|
||||
else if (type == BuildingType::Splitter
|
||||
|| type == BuildingType::TunnelEntry
|
||||
|| type == BuildingType::TunnelExit)
|
||||
{
|
||||
if (!m_sim->buildings().isTileOccupied(tile))
|
||||
if (!m_sim->buildings().isTileOccupied(tile) && canAfford(type))
|
||||
{
|
||||
const BuildingId id = m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
|
||||
if (id != kInvalidBuildingId)
|
||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
||||
if (type == BuildingType::TunnelEntry)
|
||||
{
|
||||
if (type == BuildingType::TunnelEntry)
|
||||
{
|
||||
m_builderType = BuildingType::TunnelExit;
|
||||
}
|
||||
else if (type == BuildingType::TunnelExit)
|
||||
{
|
||||
m_builderType = BuildingType::TunnelEntry;
|
||||
}
|
||||
m_builderType = BuildingType::TunnelExit;
|
||||
}
|
||||
else if (type == BuildingType::TunnelExit)
|
||||
{
|
||||
m_builderType = BuildingType::TunnelEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
|
||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1209,6 +1279,45 @@ void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
|
||||
{
|
||||
}
|
||||
|
||||
void GameWorldView::drawReplayOverlay(QPainter& painter)
|
||||
{
|
||||
if (!m_replayPlayer) { return; }
|
||||
|
||||
painter.save();
|
||||
|
||||
QFont tag = painter.font();
|
||||
tag.setPixelSize(16);
|
||||
tag.setBold(true);
|
||||
painter.setFont(tag);
|
||||
painter.setPen(QColor(255, 220, 80));
|
||||
painter.drawText(QRect(0, 8, width(), 24), Qt::AlignHCenter | Qt::AlignTop, tr("REPLAY"));
|
||||
|
||||
if (m_replayPlayer->isFinished())
|
||||
{
|
||||
// Dim the world so the end message reads clearly over it.
|
||||
painter.fillRect(rect(), QColor(0, 0, 0, 140));
|
||||
|
||||
const std::optional<Tick> desync = m_replayPlayer->getDesyncTick();
|
||||
QString message;
|
||||
if (desync.has_value())
|
||||
{
|
||||
message = tr("Desync at tick %1").arg(static_cast<qlonglong>(*desync));
|
||||
painter.setPen(QColor(255, 90, 90));
|
||||
}
|
||||
else
|
||||
{
|
||||
message = tr("Replay ended");
|
||||
painter.setPen(QColor(255, 255, 255));
|
||||
}
|
||||
QFont big = painter.font();
|
||||
big.setPixelSize(28);
|
||||
painter.setFont(big);
|
||||
painter.drawText(rect(), Qt::AlignCenter, message);
|
||||
}
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Input
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1369,7 +1478,10 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
const bool isProtected = b && b->type == BuildingType::Hq;
|
||||
if (!isProtected)
|
||||
{
|
||||
m_sim->demolish(hovered);
|
||||
std::shared_ptr<DemolishCommand> command =
|
||||
std::make_shared<DemolishCommand>();
|
||||
command->id = hovered;
|
||||
enqueueCommand(command);
|
||||
m_demolishHoverBuildingId = kInvalidBuildingId;
|
||||
}
|
||||
}
|
||||
@@ -1743,3 +1855,35 @@ void GameWorldView::handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent>
|
||||
{
|
||||
setGameSpeed(event->multiplier);
|
||||
}
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const CommandRequestedEvent> event)
|
||||
{
|
||||
// Other widgets (MainWindow, SelectedBuildingPanel) request commands via this
|
||||
// event; GameWorldView owns the CommandManager and enqueues them.
|
||||
if (event->command && event->command->kind == CommandKind::Reset)
|
||||
{
|
||||
m_viewResetPending = true;
|
||||
}
|
||||
enqueueCommand(event->command);
|
||||
}
|
||||
|
||||
void GameWorldView::enqueueCommand(std::shared_ptr<const Command> command)
|
||||
{
|
||||
m_commandManager.enqueue(std::move(command));
|
||||
}
|
||||
|
||||
void GameWorldView::enqueuePlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
||||
{
|
||||
std::shared_ptr<PlaceBuildingCommand> command = std::make_shared<PlaceBuildingCommand>();
|
||||
command->type = type;
|
||||
command->anchor = anchor;
|
||||
command->rotation = rotation;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
|
||||
bool GameWorldView::canAfford(BuildingType type) const
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
if (!def) { return false; }
|
||||
return m_sim->buildingBlocksStock() >= def->cost;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
@@ -26,10 +27,12 @@
|
||||
#include "ExitBuilderModeRequestedEvent.h"
|
||||
#include "DebugDrawToggledEvent.h"
|
||||
#include "BeamFiredEvent.h"
|
||||
#include "CommandRequestedEvent.h"
|
||||
#include "SchematicChoiceOption.h"
|
||||
#include "SpeedChangeRequestedEvent.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "CommandManager.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Rotation.h"
|
||||
@@ -37,6 +40,9 @@
|
||||
#include "TickDriver.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
struct Command;
|
||||
struct ParsedReplay;
|
||||
class ReplayPlayer;
|
||||
class Simulation;
|
||||
class QPainter;
|
||||
|
||||
@@ -56,13 +62,15 @@ class GameWorldView : public QOpenGLWidget,
|
||||
DemolishModeToggleRequestedEvent,
|
||||
BlueprintPlacementRequestedEvent,
|
||||
ExitBlueprintModeRequestedEvent,
|
||||
SpeedChangeRequestedEvent>
|
||||
SpeedChangeRequestedEvent,
|
||||
CommandRequestedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
||||
const VisualsConfig* visuals, const std::string& configDir,
|
||||
const ParsedReplay* replay, QWidget* parent = nullptr);
|
||||
~GameWorldView() override;
|
||||
|
||||
double gameSpeed() const;
|
||||
@@ -91,6 +99,17 @@ private:
|
||||
void handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const CommandRequestedEvent> event) override;
|
||||
|
||||
// Enqueue a sim command onto the CommandManager (the single mutation path).
|
||||
void enqueueCommand(std::shared_ptr<const Command> command);
|
||||
|
||||
// Enqueue a plain (unconfigured) building placement.
|
||||
void enqueuePlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
|
||||
|
||||
// True if the player can currently afford to place one building of `type`.
|
||||
// Used to pre-validate placements whose UI follow-up depends on success.
|
||||
bool canAfford(BuildingType type) const;
|
||||
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
@@ -104,6 +123,7 @@ private:
|
||||
void drawBeams(QPainter& painter);
|
||||
void drawOverlays(QPainter& painter);
|
||||
void drawScreenSpace(QPainter& painter);
|
||||
void drawReplayOverlay(QPainter& painter);
|
||||
|
||||
float tilePx() const;
|
||||
float viewportWidthTiles() const;
|
||||
@@ -157,6 +177,14 @@ private:
|
||||
const GameConfig* m_config;
|
||||
const VisualsConfig* m_visuals;
|
||||
|
||||
// Funnels all player input into the single Simulation::apply chokepoint.
|
||||
CommandManager m_commandManager;
|
||||
// A Reset command was enqueued; reset the view after the next drain applies it.
|
||||
bool m_viewResetPending = false;
|
||||
// Non-null => view-only playback: ticks are driven by the recorded stream and
|
||||
// live input is ignored (the CommandManager is in replay mode).
|
||||
std::unique_ptr<ReplayPlayer> m_replayPlayer;
|
||||
|
||||
TickDriver m_tickDriver;
|
||||
QElapsedTimer m_frameTimer;
|
||||
std::mt19937 m_rng;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
|
||||
#include <QApplication>
|
||||
@@ -15,7 +16,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"
|
||||
@@ -27,18 +31,21 @@
|
||||
#include "Tick.h"
|
||||
#include "VisualsLoader.h"
|
||||
|
||||
MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent)
|
||||
MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
std::shared_ptr<ParsedReplay> replay, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_configDir(configDir)
|
||||
, m_visuals(VisualsLoader::load(configDir + "/visuals.toml"))
|
||||
, m_sim(sim)
|
||||
, m_replay(std::move(replay))
|
||||
{
|
||||
setWindowTitle(tr("Dota Factory"));
|
||||
resize(1280, 768);
|
||||
|
||||
m_headerBar = new HeaderBar(this);
|
||||
|
||||
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this);
|
||||
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, m_configDir,
|
||||
m_replay.get(), this);
|
||||
|
||||
m_sidePanel = new QWidget(this);
|
||||
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||
@@ -138,7 +145,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 +171,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 +187,13 @@ 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). A fresh random seed starts a new run.
|
||||
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
||||
command->config = std::move(newConfig);
|
||||
command->seed = std::random_device{}();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else if (clicked == quitBtn)
|
||||
{
|
||||
@@ -234,7 +252,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 +291,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 +320,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 +334,12 @@ 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);
|
||||
command->seed = std::random_device{}();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "Tick.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
struct ParsedReplay;
|
||||
class Simulation;
|
||||
class GameWorldView;
|
||||
class HeaderBar;
|
||||
@@ -37,7 +39,8 @@ class MainWindow : public QWidget,
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent = nullptr);
|
||||
MainWindow(Simulation* sim, const std::string& configDir,
|
||||
std::shared_ptr<ParsedReplay> replay = nullptr, QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
protected:
|
||||
@@ -65,4 +68,5 @@ private:
|
||||
QWidget* m_sidePanel;
|
||||
|
||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Command.h"
|
||||
#include "CommandRequestedEvent.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
@@ -720,17 +722,23 @@ void SelectedBuildingPanel::onSplitterFilterChanged()
|
||||
|
||||
if (m_singleIsSite)
|
||||
{
|
||||
m_sim->buildings().setSiteSplitterFilters(
|
||||
m_singleBuildingId,
|
||||
collectFilter(m_filterAList),
|
||||
collectFilter(m_filterBList));
|
||||
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
|
||||
std::make_shared<SetSiteSplitterFiltersCommand>();
|
||||
command->id = m_singleBuildingId;
|
||||
command->filterA = collectFilter(m_filterAList);
|
||||
command->filterB = collectFilter(m_filterBList);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_sim->belts().setSplitterFilters(
|
||||
m_splitterTile,
|
||||
collectFilter(m_filterAList),
|
||||
collectFilter(m_filterBList));
|
||||
std::shared_ptr<SetSplitterFiltersCommand> command =
|
||||
std::make_shared<SetSplitterFiltersCommand>();
|
||||
command->tile = m_splitterTile;
|
||||
command->filterA = collectFilter(m_filterAList);
|
||||
command->filterB = collectFilter(m_filterBList);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,7 +775,11 @@ void SelectedBuildingPanel::onClearBelt()
|
||||
}
|
||||
if (!tiles.empty())
|
||||
{
|
||||
m_sim->belts().clearTiles(tiles);
|
||||
std::shared_ptr<ClearBeltTilesCommand> command =
|
||||
std::make_shared<ClearBeltTilesCommand>();
|
||||
command->tiles = std::move(tiles);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user