#include "ReplayRecorder.h" #include #include #include #include #include #include #include #include #include #include "Command.h" #include "CommandSerializer.h" #include "StateChecksum.h" namespace { constexpr const char* kReplayFormatVersion = "1"; // Build fingerprint: even a new local build can desync old replays (float // reasons), so the header carries a per-build tag to warn on mismatch. const std::string kBuildTag = std::string(__DATE__) + " " + __TIME__; std::string toHex(std::uint64_t value) { std::ostringstream out; out << std::hex << std::setw(16) << std::setfill('0') << value; return out.str(); } } // namespace ReplayRecorder::ReplayRecorder(std::string configDir, std::string outputDir) : m_configDir(std::move(configDir)) , m_outputDir(std::move(outputDir)) { } ReplayRecorder::~ReplayRecorder() { close(); } std::string computeReplayConfigHash(const std::string& configDir) { Hasher hasher; QDir dir(QString::fromStdString(configDir)); const QStringList files = dir.entryList(QStringList() << "*.toml", QDir::Files, QDir::Name); for (const QString& name : files) { hasher.append(name.toStdString()); QFile file(dir.filePath(name)); if (file.open(QIODevice::ReadOnly)) { const QByteArray bytes = file.readAll(); hasher.appendBytes(bytes.constData(), static_cast(bytes.size())); } } return toHex(hasher.getValue()); } void ReplayRecorder::startNewRun(unsigned int seed, std::uint64_t initialRngFingerprint) { close(); QDir().mkpath(QString::fromStdString(m_outputDir)); const QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); const QString fileName = timestamp + "_" + QString::number(seed) + ".replay"; m_filePath = QDir(QString::fromStdString(m_outputDir)).filePath(fileName).toStdString(); m_stream.open(m_filePath, std::ios::out | std::ios::trunc); if (!m_stream.is_open()) { return; } m_stream << "# dota_factory replay\n"; m_stream << "version " << kReplayFormatVersion << "\n"; m_stream << "build " << kBuildTag << "\n"; m_stream << "seed " << seed << "\n"; m_stream << "config_hash " << computeReplayConfigHash(m_configDir) << "\n"; m_stream << "timestamp " << QDateTime::currentDateTime().toString(Qt::ISODate).toStdString() << "\n"; m_stream << "---\n"; m_stream << "# checksum 0 " << toHex(initialRngFingerprint) << "\n"; m_stream.flush(); } void ReplayRecorder::recordCommand(Tick tick, const Command& command, std::uint64_t rngFingerprint) { if (!m_stream.is_open()) { return; } m_stream << tick << ' ' << serializeCommand(command) << "\n"; m_stream << "# checksum " << tick << ' ' << toHex(rngFingerprint) << "\n"; m_stream.flush(); } void ReplayRecorder::recordChecksum(Tick tick, std::uint64_t rngFingerprint) { if (!m_stream.is_open()) { return; } m_stream << "# checksum " << tick << ' ' << toHex(rngFingerprint) << "\n"; m_stream.flush(); } void ReplayRecorder::close() { if (m_stream.is_open()) { m_stream.flush(); m_stream.close(); } } bool ReplayRecorder::isOpen() const { return m_stream.is_open(); } const std::string& ReplayRecorder::getCurrentFilePath() const { return m_filePath; }