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

@@ -2,6 +2,7 @@
#include <algorithm>
#include "StateChecksum.h"
#include "Tick.h"
#include "tracing.h"
@@ -970,4 +971,91 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
}
}
void BeltSystem::appendItemSlots(Hasher& hasher, const std::vector<BeltItemSlot>& slotRun)
{
hasher.append(slotRun.size());
for (const BeltItemSlot& slot : slotRun)
{
hasher.append(slot.item.type.id);
hasher.append(slot.progress);
}
}
void BeltSystem::appendChecksum(Hasher& hasher) const
{
// std::map iterates in sorted key order, so all tile loops are deterministic.
hasher.append(m_belts.size());
for (const std::pair<const std::pair<int, int>, BeltTile>& entry : m_belts)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
hasher.append(entry.second.direction);
appendItemSlots(hasher, entry.second.itemSlots);
}
hasher.append(m_splitters.size());
for (const std::pair<const std::pair<int, int>, SplitterTile>& entry : m_splitters)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
const SplitterTile& s = entry.second;
hasher.append(s.outputA);
hasher.append(s.outputB);
hasher.append(s.filterA.size());
for (const ItemType& type : s.filterA) { hasher.append(type.id); }
hasher.append(s.filterB.size());
for (const ItemType& type : s.filterB) { hasher.append(type.id); }
hasher.append(s.nextOutputIsA);
appendItemSlots(hasher, s.back);
hasher.append(s.backDir.size());
for (Rotation dir : s.backDir) { hasher.append(dir); }
hasher.append(s.frontA.has_value());
if (s.frontA.has_value())
{
hasher.append(s.frontA->item.type.id);
hasher.append(s.frontA->progress);
}
hasher.append(s.frontB.has_value());
if (s.frontB.has_value())
{
hasher.append(s.frontB->item.type.id);
hasher.append(s.frontB->progress);
}
}
hasher.append(m_tunnelEntries.size());
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
hasher.append(entry.second.direction);
hasher.append(entry.second.maxDistance);
appendItemSlots(hasher, entry.second.itemSlots);
}
hasher.append(m_tunnelExits.size());
for (const std::pair<const std::pair<int, int>, TunnelExitTile>& entry : m_tunnelExits)
{
hasher.append(entry.first.first);
hasher.append(entry.first.second);
hasher.append(entry.second.direction);
appendItemSlots(hasher, entry.second.itemSlots);
}
// m_tunnelLinks preserves insertion order, which is itself deterministic.
hasher.append(m_tunnelLinks.size());
for (const TunnelLink& link : m_tunnelLinks)
{
hasher.append(link.entryTile);
hasher.append(link.exitTile);
hasher.append(link.length);
hasher.append(link.items.size());
for (const TunnelTransitItem& item : link.items)
{
hasher.append(item.item.type.id);
hasher.append(item.progress);
}
}
}