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:
141
src/lib/sim/Command.h
Normal file
141
src/lib/sim/Command.h
Normal file
@@ -0,0 +1,141 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ItemType.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
class GameConfig;
|
||||
|
||||
// Player intent, resolved to domain ids / tile coordinates and serializable, that
|
||||
// mutates the simulation. Every sim mutation during play flows through a Command
|
||||
// applied at the single Simulation::apply chokepoint, so it can be recorded and
|
||||
// replayed (see docs/replay_design.md).
|
||||
//
|
||||
// Commands form a closed set dispatched by kind. They reference stable,
|
||||
// deterministic ids (BuildingId, tile coordinates, choice indices) — never raw
|
||||
// entt handles — so a recorded command resolves to the same target on replay.
|
||||
|
||||
enum class CommandKind
|
||||
{
|
||||
PlaceBuilding,
|
||||
Demolish,
|
||||
RotateInPlace,
|
||||
SetRecipe,
|
||||
SetShipLayout,
|
||||
SetSiteSplitterFilters,
|
||||
SetSplitterFilters,
|
||||
ClearBeltTiles,
|
||||
ApplySchematicChoice,
|
||||
Reset
|
||||
};
|
||||
|
||||
struct Command
|
||||
{
|
||||
explicit Command(CommandKind kind) : kind(kind) {}
|
||||
virtual ~Command() = default;
|
||||
|
||||
CommandKind kind;
|
||||
// Source of the command. Always 0 in single-player; lockstep multiplayer
|
||||
// later merges commands from multiple sources into one ordered stream.
|
||||
int playerId = 0;
|
||||
};
|
||||
|
||||
// Places a building and, atomically, configures it. The configuration fields are
|
||||
// bundled here (rather than as follow-up commands) because commands are applied
|
||||
// at a deferred tick boundary, so the caller never sees the new BuildingId — the
|
||||
// place-and-configure must happen as one unit inside apply().
|
||||
struct PlaceBuildingCommand : Command
|
||||
{
|
||||
PlaceBuildingCommand() : Command(CommandKind::PlaceBuilding) {}
|
||||
|
||||
BuildingType type = BuildingType::Miner;
|
||||
QPoint anchor;
|
||||
Rotation rotation = Rotation::East;
|
||||
|
||||
// Optional configuration applied to the freshly placed (still-construction)
|
||||
// building. The caller (UI) is responsible for unlock/validity pre-filtering;
|
||||
// only fields that should apply are set.
|
||||
std::optional<std::string> recipeId;
|
||||
std::optional<ShipLayoutConfig> shipLayout;
|
||||
bool hasSplitterFilters = false;
|
||||
std::vector<ItemType> splitterFilterA;
|
||||
std::vector<ItemType> splitterFilterB;
|
||||
};
|
||||
|
||||
struct DemolishCommand : Command
|
||||
{
|
||||
DemolishCommand() : Command(CommandKind::Demolish) {}
|
||||
BuildingId id = kInvalidBuildingId;
|
||||
};
|
||||
|
||||
struct RotateInPlaceCommand : Command
|
||||
{
|
||||
RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {}
|
||||
BuildingId id = kInvalidBuildingId;
|
||||
Rotation newRotation = Rotation::East;
|
||||
};
|
||||
|
||||
struct SetRecipeCommand : Command
|
||||
{
|
||||
SetRecipeCommand() : Command(CommandKind::SetRecipe) {}
|
||||
BuildingId id = kInvalidBuildingId;
|
||||
std::string recipeId;
|
||||
};
|
||||
|
||||
struct SetShipLayoutCommand : Command
|
||||
{
|
||||
SetShipLayoutCommand() : Command(CommandKind::SetShipLayout) {}
|
||||
BuildingId id = kInvalidBuildingId;
|
||||
ShipLayoutConfig layout;
|
||||
};
|
||||
|
||||
// Splitter filters for a queued / under-construction Splitter site (configured by
|
||||
// BuildingSystem before the splitter is registered with BeltSystem).
|
||||
struct SetSiteSplitterFiltersCommand : Command
|
||||
{
|
||||
SetSiteSplitterFiltersCommand() : Command(CommandKind::SetSiteSplitterFilters) {}
|
||||
BuildingId id = kInvalidBuildingId;
|
||||
std::vector<ItemType> filterA;
|
||||
std::vector<ItemType> filterB;
|
||||
};
|
||||
|
||||
// Splitter filters for an operational splitter, configured by tile via BeltSystem.
|
||||
struct SetSplitterFiltersCommand : Command
|
||||
{
|
||||
SetSplitterFiltersCommand() : Command(CommandKind::SetSplitterFilters) {}
|
||||
QPoint tile;
|
||||
std::vector<ItemType> filterA;
|
||||
std::vector<ItemType> filterB;
|
||||
};
|
||||
|
||||
struct ClearBeltTilesCommand : Command
|
||||
{
|
||||
ClearBeltTilesCommand() : Command(CommandKind::ClearBeltTiles) {}
|
||||
std::vector<QPoint> tiles;
|
||||
};
|
||||
|
||||
struct ApplySchematicChoiceCommand : Command
|
||||
{
|
||||
ApplySchematicChoiceCommand() : Command(CommandKind::ApplySchematicChoice) {}
|
||||
int choiceIndex = 0;
|
||||
};
|
||||
|
||||
// Restart boundary: reinitializes the simulation with a fresh seed and, if
|
||||
// config is set, a reloaded config (GameConfig is move-only, so it is carried by
|
||||
// shared_ptr and moved into the sim on apply). A null config keeps the current
|
||||
// config. One replay file = one run between Reset boundaries.
|
||||
struct ResetCommand : Command
|
||||
{
|
||||
ResetCommand() : Command(CommandKind::Reset) {}
|
||||
std::shared_ptr<GameConfig> config; // null = keep current config
|
||||
unsigned int seed = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user