Replay: name replay folders after matchID instead of a per-machine index

Replay folders were named YYYY-MM-DD_NNNN, a per-machine sequence
counter. The same multiplayer match got a different folder name per
participant, and unrelated matches could share a name, making it hard
to correlate replays across players (rating aggregation, replay
sharing, bug reports) without parsing commands.txt first.

The engine already generates a matchID once per match, synced to all
participants before the match starts. Replay folders are now named
YYYY-MM-DD_<matchID>, so the same match produces the same folder name
everywhere.

The date comes from a new "startTime" attribute, set once by the host
(StartTime.js, alongside MatchID.js) and synced like matchID, instead
of each machine reading its own local clock. createDateIndexSubdirectory
is untouched (still used by OOS logs and serialization tests); a new
createReplaySubdirectory is added for replays only, following the same
collision-handling pattern (incrementing suffix) as the existing
function. This also means resuming a match (network rejoin, or a
singleplayer game reloaded via StartSavedGame, both of which reuse the
same matchID) saves an additional replay instead of overwriting the
original.

Fixes #8922
This commit is contained in:
vyordan 2026-07-06 23:49:19 -06:00
parent 1709149740
commit 3808ca0965
4 changed files with 40 additions and 5 deletions

View file

@ -0,0 +1,7 @@
GameSettings.prototype.Attributes.StartTime = class extends GameSetting
{
onFinalizeAttributes(attribs)
{
attribs.startTime = Date.now();
}
};

View file

@ -85,7 +85,14 @@ void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData());
Script::SetProperty(rq, attribs, "mods", mods);
m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath());
std::wstring matchID;
Script::GetProperty(rq, attribs, "matchID", matchID);
ENSURE(!matchID.empty());
double startTime = 0;
Script::GetProperty(rq, attribs, "startTime", startTime);
m_Directory = createReplaySubdirectory(VisualReplay::GetDirectoryPath(), matchID, startTime);
debug_printf("FILES| Replay written to '%s'\n", m_Directory.string8().c_str());
m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt"), std::ofstream::out | std::ofstream::trunc);
@ -95,12 +102,9 @@ void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands)
{
Script::Request rq(m_ScriptInterface);
*m_Stream << "turn " << n << " " << turnLength << "\n";
for (SimulationCommand& command : commands)
*m_Stream << "cmd " << command.player << " " << Script::StringifyJSON(rq, &command.data, false) << "\n";
*m_Stream << "end\n";
m_Stream->flush();
}

View file

@ -33,8 +33,10 @@
#include "ps/Filesystem.h"
#include "ps/Pyrogenesis.h"
#include <chrono>
#include <ctime>
#include <filesystem>
#include <fmt/format.h>
#include <iomanip>
#include <memory>
#include <sstream>
@ -119,6 +121,26 @@ OsPath createDateIndexSubdirectory(const OsPath& parentDir)
return path;
}
OsPath createReplaySubdirectory(const OsPath& parentDir, const std::wstring& matchID, double startTimeMillis)
{
using namespace std::chrono;
const system_clock::time_point timePoint{milliseconds(static_cast<i64>(startTimeMillis))};
const year_month_day ymd{floor<days>(timePoint)};
const std::string baseName = fmt::format("{:04}-{:02}-{:02}_{}",
static_cast<int>(ymd.year()), static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day()),
utf8_from_wstring(matchID));
for (int i = 0; ; ++i)
{
const OsPath path = parentDir / (i == 0 ? baseName : fmt::format("{}_{:04}", baseName, i));
if (DirectoryExists(path) || std::filesystem::is_regular_file(path.string()))
continue;
ENSURE(CreateDirectories(path, 0700) == INFO::OK);
return path;
}
}
std::string Hexify(const std::string& s)
{
std::stringstream str;

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -35,6 +35,8 @@ CStr GetStatusAsString(Status status);
OsPath createDateIndexSubdirectory(const OsPath& parentDir);
OsPath createReplaySubdirectory(const OsPath& parentDir, const std::wstring& matchID, double startTimeMillis);
Status tex_write(Tex* t, const VfsPath& filename);
std::string Hexify(const std::string& s);