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:
2026-07-01 19:20:08 +00:00
committed by mlangkabel
parent cf68ac2862
commit d74ba5bfad
40 changed files with 3385 additions and 129 deletions

View File

@@ -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;