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:
145
src/lib/sim/ReplayReader.cpp
Normal file
145
src/lib/sim/ReplayReader.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user