Files
dota_factory/src/lib/sim/ReplayReader.cpp
Malte Langkabel d74ba5bfad 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>
2026-07-01 19:20:08 +00:00

146 lines
3.6 KiB
C++

#include "ReplayReader.h"
#include <fstream>
#include <sstream>
#include <string>
#include "Command.h"
#include "CommandSerializer.h"
namespace
{
void stripCarriageReturn(std::string& line)
{
if (!line.empty() && line.back() == '\r')
{
line.pop_back();
}
}
// Splits "key value with spaces" into (key, remainder). Remainder may be empty.
void splitKeyValue(const std::string& line, std::string& key, std::string& value)
{
const std::string::size_type space = line.find(' ');
if (space == std::string::npos)
{
key = line;
value.clear();
}
else
{
key = line.substr(0, space);
value = line.substr(space + 1);
}
}
} // namespace
std::optional<ParsedReplay> readReplayFile(const std::string& path)
{
std::ifstream stream(path, std::ios::in);
if (!stream.is_open())
{
return std::nullopt;
}
ParsedReplay replay;
std::string line;
// --- Header (up to the "---" separator) ---
bool sawSeparator = false;
while (std::getline(stream, line))
{
stripCarriageReturn(line);
if (line == "---")
{
sawSeparator = true;
break;
}
if (line.empty() || line[0] == '#')
{
continue; // banner / blank
}
std::string key;
std::string value;
splitKeyValue(line, key, value);
try
{
if (key == "version") { replay.header.version = std::stoi(value); }
else if (key == "build") { replay.header.build = value; }
else if (key == "seed") { replay.header.seed = static_cast<unsigned int>(std::stoul(value)); }
else if (key == "config_hash") { replay.header.configHash = value; }
else if (key == "timestamp") { replay.header.timestamp = value; }
}
catch (const std::exception&)
{
return std::nullopt;
}
}
if (!sawSeparator)
{
return std::nullopt;
}
// --- Command / checksum stream ---
while (std::getline(stream, line))
{
stripCarriageReturn(line);
if (line.empty())
{
continue;
}
if (line[0] == '#')
{
// "# checksum <tick> <hex>"
std::istringstream in(line);
std::string hash;
std::string keyword;
ReplayEntry entry;
in >> hash >> keyword >> entry.tick >> hash;
if (keyword != "checksum")
{
continue; // unknown comment line — ignore
}
try
{
entry.fingerprint = std::stoull(hash, nullptr, 16);
}
catch (const std::exception&)
{
return std::nullopt;
}
entry.isCommand = false;
replay.entries.push_back(std::move(entry));
continue;
}
// "<tick> <serialized command>"
const std::string::size_type space = line.find(' ');
if (space == std::string::npos)
{
return std::nullopt;
}
ReplayEntry entry;
try
{
entry.tick = std::stoll(line.substr(0, space));
}
catch (const std::exception&)
{
return std::nullopt;
}
std::shared_ptr<Command> command = parseCommand(line.substr(space + 1));
if (!command)
{
return std::nullopt;
}
entry.isCommand = true;
entry.command = std::move(command);
replay.entries.push_back(std::move(entry));
}
return replay;
}