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

@@ -0,0 +1,92 @@
#include "CommandManager.h"
#include <utility>
#include "Command.h"
#include "ReplayRecorder.h"
#include "Simulation.h"
#include "Tick.h"
namespace
{
// Periodic RNG checksum cadence (see docs/replay_design.md "Cadence").
constexpr Tick kChecksumIntervalTicks = 30;
} // namespace
CommandManager::CommandManager(Simulation& simulation)
: m_simulation(simulation)
{
}
CommandManager::~CommandManager() = default;
void CommandManager::enqueue(std::shared_ptr<const Command> command)
{
if (m_replayMode)
{
return; // playback is driven by the recorded stream; ignore live input
}
if (command)
{
m_queue.push_back(std::move(command));
}
}
void CommandManager::drain()
{
// Apply in FIFO order. A queued ResetCommand reinitializes the simulation in
// place; the reference stays valid and any commands after it apply to the
// fresh state.
for (const std::shared_ptr<const Command>& command : m_queue)
{
if (command->kind == CommandKind::Reset)
{
m_simulation.apply(*command);
if (m_recorder)
{
// Restart is a file boundary: a fresh file with the new seed.
m_recorder->startNewRun(m_simulation.getSeed(),
m_simulation.rngFingerprint());
}
}
else
{
// Commands drain before the tick batch, so currentTick is the count of
// completed ticks the command is pinned to.
const Tick tick = m_simulation.currentTick();
m_simulation.apply(*command);
if (m_recorder)
{
m_recorder->recordCommand(tick, *command, m_simulation.rngFingerprint());
}
}
}
m_queue.clear();
}
bool CommandManager::hasPending() const
{
return !m_queue.empty();
}
void CommandManager::setRecorder(std::unique_ptr<ReplayRecorder> recorder)
{
m_recorder = std::move(recorder);
if (m_recorder)
{
m_recorder->startNewRun(m_simulation.getSeed(), m_simulation.rngFingerprint());
}
}
void CommandManager::setReplayMode(bool replayMode)
{
m_replayMode = replayMode;
}
void CommandManager::recordTickCheckpoint()
{
if (m_recorder && (m_simulation.currentTick() % kChecksumIntervalTicks == 0))
{
m_recorder->recordChecksum(m_simulation.currentTick(), m_simulation.rngFingerprint());
}
}