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

@@ -17,6 +17,7 @@
#include "ShipStatsCalculator.h"
#include "ShipSystem.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "Tick.h"
#include "WeaponComponent.h"
@@ -62,7 +63,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
{
return sim.buildings().placeImmediate(
return SimulationTestAccess::buildings(sim).placeImmediate(
BuildingType::Shipyard,
yardDef.surfaceMask,
QPoint(0, 0),
@@ -73,7 +74,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
const ShipDef& def,
const ShipLayoutConfig& layout)
{
sim.buildings().forEachBuilding([&](Building& b) {
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) {
if (b.id != yardId)
{
return;
@@ -216,7 +217,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
sim.buildings().setRecipe(yardId, "interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
ShipLayoutConfig layout;
PlacedModule pm;
@@ -225,7 +226,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
sim.buildings().setShipLayout(yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b = sim.buildings().findBuilding(yardId);
REQUIRE(b != nullptr);
@@ -245,7 +246,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
sim.buildings().setRecipe(yardId, "interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
// Fill materials and tick to start production.
ShipLayoutConfig emptyLayout;
@@ -264,7 +265,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
sim.buildings().setShipLayout(yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b2 = sim.buildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
@@ -278,7 +279,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
REQUIRE(yardDef != nullptr);
const BuildingId yardId = placeShipyard(sim, *yardDef);
sim.buildings().setRecipe(yardId, "interceptor");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
ShipLayoutConfig layout;
PlacedModule pm;
@@ -286,13 +287,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
pm.position = QPoint(0, 0);
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
sim.buildings().setShipLayout(yardId, layout);
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
const Building* b1 = sim.buildings().findBuilding(yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value());
sim.buildings().setRecipe(yardId, "destroyer");
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
const Building* b2 = sim.buildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);