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:
@@ -4,10 +4,13 @@
|
||||
#include <cassert>
|
||||
|
||||
#include "AiSystem.h"
|
||||
#include "Command.h"
|
||||
#include "DisplayName.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "CombatSystem.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "DynamicBodySystem.h"
|
||||
#include "FacingComponent.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "EventManager.h"
|
||||
#include "HealthComponent.h"
|
||||
@@ -16,9 +19,11 @@
|
||||
#include "PositionComponent.h"
|
||||
#include "RepairSystem.h"
|
||||
#include "SalvagerSystem.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "StateChecksum.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "SurfaceMask.h"
|
||||
#include "tracing.h"
|
||||
@@ -28,6 +33,7 @@
|
||||
Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
: m_config(std::move(config))
|
||||
, m_rng(seed)
|
||||
, m_seed(seed)
|
||||
, m_currentTick(0)
|
||||
, m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds))
|
||||
, m_nextBuildingId(1)
|
||||
@@ -129,6 +135,7 @@ void Simulation::reset(unsigned int seed)
|
||||
{
|
||||
EventManager::getInstance()->clearEvents();
|
||||
m_rng.seed(seed);
|
||||
m_seed = seed;
|
||||
m_currentTick = 0;
|
||||
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
|
||||
m_nextBuildingId = 1;
|
||||
@@ -215,6 +222,92 @@ void Simulation::reset(unsigned int seed)
|
||||
// tick
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void Simulation::apply(const Command& command)
|
||||
{
|
||||
switch (command.kind)
|
||||
{
|
||||
case CommandKind::PlaceBuilding:
|
||||
{
|
||||
const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command);
|
||||
const BuildingId id = tryPlaceBuilding(c.type, c.anchor, c.rotation);
|
||||
if (id == kInvalidBuildingId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (c.recipeId.has_value())
|
||||
{
|
||||
m_buildingSystem->setRecipe(id, *c.recipeId);
|
||||
}
|
||||
if (c.shipLayout.has_value())
|
||||
{
|
||||
m_buildingSystem->setShipLayout(id, *c.shipLayout);
|
||||
}
|
||||
if (c.hasSplitterFilters)
|
||||
{
|
||||
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CommandKind::Demolish:
|
||||
demolish(static_cast<const DemolishCommand&>(command).id);
|
||||
break;
|
||||
case CommandKind::RotateInPlace:
|
||||
{
|
||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||
m_buildingSystem->rotateInPlace(c.id, c.newRotation);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetRecipe:
|
||||
{
|
||||
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
||||
m_buildingSystem->setRecipe(c.id, c.recipeId);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetShipLayout:
|
||||
{
|
||||
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
||||
m_buildingSystem->setShipLayout(c.id, c.layout);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetSiteSplitterFilters:
|
||||
{
|
||||
const SetSiteSplitterFiltersCommand& c =
|
||||
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
||||
m_buildingSystem->setSiteSplitterFilters(c.id, c.filterA, c.filterB);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetSplitterFilters:
|
||||
{
|
||||
const SetSplitterFiltersCommand& c =
|
||||
static_cast<const SetSplitterFiltersCommand&>(command);
|
||||
m_beltSystem.setSplitterFilters(c.tile, c.filterA, c.filterB);
|
||||
break;
|
||||
}
|
||||
case CommandKind::ClearBeltTiles:
|
||||
m_beltSystem.clearTiles(static_cast<const ClearBeltTilesCommand&>(command).tiles);
|
||||
break;
|
||||
case CommandKind::ApplySchematicChoice:
|
||||
applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
|
||||
break;
|
||||
case CommandKind::Reset:
|
||||
{
|
||||
const ResetCommand& c = static_cast<const ResetCommand&>(command);
|
||||
if (c.config)
|
||||
{
|
||||
// operator* on a const shared_ptr yields a mutable GameConfig&, so
|
||||
// the move-only config moves into reset without a copy. The command
|
||||
// is applied once, so leaving its config moved-from is fine.
|
||||
reset(std::move(*c.config), c.seed);
|
||||
}
|
||||
else
|
||||
{
|
||||
reset(c.seed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Simulation::tick()
|
||||
{
|
||||
EventManager::getInstance()->processEvents();
|
||||
@@ -825,6 +918,116 @@ bool Simulation::isItemUnlocked(const std::string& itemId) const
|
||||
return m_unlockedItemIds.count(itemId) > 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Determinism (see docs/replay_design.md)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void Simulation::appendSchematicMap(Hasher& hasher,
|
||||
const std::map<std::string, SchematicState>& levels)
|
||||
{
|
||||
hasher.append(levels.size());
|
||||
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
||||
{
|
||||
hasher.append(entry.first);
|
||||
hasher.append(entry.second.unlocked);
|
||||
hasher.append(entry.second.level);
|
||||
}
|
||||
}
|
||||
|
||||
void Simulation::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
||||
{
|
||||
hasher.append(ids.size());
|
||||
for (const std::string& id : ids)
|
||||
{
|
||||
hasher.append(id);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long long Simulation::rngFingerprint() const
|
||||
{
|
||||
return fingerprintRng(m_rng);
|
||||
}
|
||||
|
||||
unsigned long long Simulation::computeStateChecksum() const
|
||||
{
|
||||
Hasher hasher;
|
||||
|
||||
// RNG stream — the most sensitive signal of divergence.
|
||||
hasher.append(fingerprintRng(m_rng));
|
||||
|
||||
// Top-level scalars.
|
||||
hasher.append(m_currentTick);
|
||||
hasher.append(m_nextDepartureTick);
|
||||
hasher.append(m_nextBuildingId);
|
||||
hasher.append(m_buildingBlocksStock);
|
||||
hasher.append(m_gameOver);
|
||||
|
||||
// WaveSystem scalar state, reached through existing accessors.
|
||||
hasher.append(threatLevel());
|
||||
hasher.append(threatAccumulationRate());
|
||||
hasher.append(bossWaveCounter());
|
||||
hasher.append(bossCountdownTicks());
|
||||
hasher.append(normalGapRemainingTicks());
|
||||
|
||||
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
||||
appendSchematicMap(hasher, m_schematicLevels);
|
||||
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
||||
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
||||
appendStringSet(hasher, m_unlockedRecipeIds);
|
||||
appendStringSet(hasher, m_unlockedItemIds);
|
||||
|
||||
// Subsystems contribute their own state.
|
||||
m_buildingSystem->appendChecksum(hasher);
|
||||
m_beltSystem.appendChecksum(hasher);
|
||||
|
||||
// ECS component state. View iteration order is a pure function of the
|
||||
// (identical) operation sequence on a fixed binary; each entity's raw id is
|
||||
// folded in so the fingerprint is keyed, not merely a sum of fields.
|
||||
m_admin.forEach<PositionComponent>(
|
||||
[&hasher](entt::entity entity, const PositionComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.value);
|
||||
});
|
||||
m_admin.forEach<HealthComponent>(
|
||||
[&hasher](entt::entity entity, const HealthComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.hp);
|
||||
hasher.append(c.maxHp);
|
||||
});
|
||||
m_admin.forEach<FacingComponent>(
|
||||
[&hasher](entt::entity entity, const FacingComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.radians);
|
||||
});
|
||||
m_admin.forEach<DynamicBodyComponent>(
|
||||
[&hasher](entt::entity entity, const DynamicBodyComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.velocity_tpt);
|
||||
hasher.append(c.angularVelocity_rpt);
|
||||
hasher.append(c.linearAcceleration_tptt);
|
||||
hasher.append(c.angularAcceleration_rptt);
|
||||
});
|
||||
m_admin.forEach<ScrapDataComponent>(
|
||||
[&hasher](entt::entity entity, const ScrapDataComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.amount);
|
||||
});
|
||||
m_admin.forEach<ShipIdentityComponent>(
|
||||
[&hasher](entt::entity entity, const ShipIdentityComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.level);
|
||||
hasher.append(c.schematicId);
|
||||
});
|
||||
|
||||
return hasher.value();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Drains
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -856,6 +1059,11 @@ Tick Simulation::currentTick() const
|
||||
return m_currentTick;
|
||||
}
|
||||
|
||||
unsigned int Simulation::getSeed() const
|
||||
{
|
||||
return m_seed;
|
||||
}
|
||||
|
||||
int Simulation::buildingBlocksStock() const
|
||||
{
|
||||
return m_buildingBlocksStock;
|
||||
@@ -974,7 +1182,7 @@ void Simulation::demolish(BuildingId id)
|
||||
m_buildingBlocksStock += m_buildingSystem->demolish(id);
|
||||
}
|
||||
|
||||
BuildingSystem& Simulation::buildings()
|
||||
BuildingSystem& Simulation::buildingsMutable()
|
||||
{
|
||||
return *m_buildingSystem;
|
||||
}
|
||||
@@ -984,7 +1192,7 @@ const BuildingSystem& Simulation::buildings() const
|
||||
return *m_buildingSystem;
|
||||
}
|
||||
|
||||
BeltSystem& Simulation::belts()
|
||||
BeltSystem& Simulation::beltsMutable()
|
||||
{
|
||||
return m_beltSystem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user