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>
146 lines
3.6 KiB
C++
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;
|
|
}
|