Index: ps/trunk/source/ps/Replay.cpp =================================================================== --- ps/trunk/source/ps/Replay.cpp (revision 22283) +++ ps/trunk/source/ps/Replay.cpp (revision 22284) @@ -1,314 +1,314 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Replay.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/file/file_system.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/Game.h" #include "ps/CLogger.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Pyrogenesis.h" #include "ps/Mod.h" #include "ps/Util.h" #include "ps/VisualReplay.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRuntime.h" #include "scriptinterface/ScriptStats.h" #include "simulation2/Simulation2.h" #include "simulation2/helpers/SimulationCommand.h" #include #include /** * Number of turns between two saved profiler snapshots. * Keep in sync with source/tools/replayprofile/graph.js */ static const int PROFILE_TURN_INTERVAL = 20; CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) : m_ScriptInterface(scriptInterface), m_Stream(NULL) { } CReplayLogger::~CReplayLogger() { delete m_Stream; } void CReplayLogger::StartGame(JS::MutableHandleValue attribs) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); // Add timestamp, since the file-modification-date can change m_ScriptInterface.SetProperty(attribs, "timestamp", (double)std::time(nullptr)); // Add engine version and currently loaded mods for sanity checks when replaying m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version)); JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface)); m_ScriptInterface.SetProperty(attribs, "mods", mods); - m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryName()); + m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath()); debug_printf("Writing replay to %s\n", m_Directory.string8().c_str()); m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc); *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n"; } void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); *m_Stream << "turn " << n << " " << turnLength << "\n"; for (SimulationCommand& command : commands) *m_Stream << "cmd " << command.player << " " << m_ScriptInterface.StringifyJSON(&command.data, false) << "\n"; *m_Stream << "end\n"; m_Stream->flush(); } void CReplayLogger::Hash(const std::string& hash, bool quick) { if (quick) *m_Stream << "hash-quick " << Hexify(hash) << "\n"; else *m_Stream << "hash " << Hexify(hash) << "\n"; } OsPath CReplayLogger::GetDirectory() const { return m_Directory; } //////////////////////////////////////////////////////////////// CReplayPlayer::CReplayPlayer() : m_Stream(NULL) { } CReplayPlayer::~CReplayPlayer() { delete m_Stream; } void CReplayPlayer::Load(const OsPath& path) { ENSURE(!m_Stream); m_Stream = new std::ifstream(OsString(path).c_str()); ENSURE(m_Stream->good()); } CStr CReplayPlayer::ModListToString(const std::vector>& list) const { CStr text; for (const std::vector& mod : list) text += mod[0] + " (" + mod[1] + ")\n"; return text; } void CReplayPlayer::CheckReplayMods(const ScriptInterface& scriptInterface, JS::HandleValue attribs) const { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); std::vector> replayMods; scriptInterface.GetProperty(attribs, "mods", replayMods); std::vector> enabledMods; JS::RootedValue enabledModsJS(cx, Mod::GetLoadedModsWithVersions(scriptInterface)); scriptInterface.FromJSVal(cx, enabledModsJS, enabledMods); CStr warn; if (replayMods.size() != enabledMods.size()) warn = "The number of enabled mods does not match the mods of the replay."; else for (size_t i = 0; i < replayMods.size(); ++i) { if (replayMods[i][0] != enabledMods[i][0]) { warn = "The enabled mods don't match the mods of the replay."; break; } else if (replayMods[i][1] != enabledMods[i][1]) { warn = "The mod '" + replayMods[i][0] + "' with version '" + replayMods[i][1] + "' is required by the replay file, but version '" + enabledMods[i][1] + "' is present!"; break; } } if (!warn.empty()) LOGWARNING("%s\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s", warn, ModListToString(replayMods), ModListToString(enabledMods)); } void CReplayPlayer::Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick) { ENSURE(m_Stream); new CProfileViewer; new CProfileManager; g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); const int runtimeSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptRuntime); g_Game = new CGame(true, false); if (serializationtest) g_Game->GetSimulation2()->EnableSerializationTest(); if (rejointestturn > 0) g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn); if (ooslog) g_Game->GetSimulation2()->EnableOOSLog(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); // Initialise h_mgr so it doesn't crash when emitting sounds h_mgr_init(); std::vector commands; u32 turn = 0; u32 turnLength = 0; { JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); std::string type; while ((*m_Stream >> type).good()) { if (type == "start") { std::string line; std::getline(*m_Stream, line); JS::RootedValue attribs(cx); ENSURE(g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &attribs)); CheckReplayMods(g_Game->GetSimulation2()->GetScriptInterface(), attribs); g_Game->StartGame(&attribs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } else if (type == "turn") { *m_Stream >> turn >> turnLength; debug_printf("Turn %u (%u)...\n", turn, turnLength); } else if (type == "cmd") { player_id_t player; *m_Stream >> player; std::string line; std::getline(*m_Stream, line); JS::RootedValue data(cx); g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data); g_Game->GetSimulation2()->GetScriptInterface().FreezeObject(data, true); commands.emplace_back(SimulationCommand(player, cx, data)); } else if (type == "hash" || type == "hash-quick") { std::string replayHash; *m_Stream >> replayHash; TestHash(type, replayHash, testHashFull, testHashQuick); } else if (type == "end") { { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); g_Game->GetSimulation2()->Update(turnLength, commands); commands.clear(); } g_Profiler.Frame(); if (turn % PROFILE_TURN_INTERVAL == 0) g_ProfileViewer.SaveToFile(); } else debug_printf("Unrecognised replay token %s\n", type.c_str()); } } SAFE_DELETE(m_Stream); g_Profiler2.SaveToFile(); std::string hash; bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false); ENSURE(ok); debug_printf("# Final state: %s\n", Hexify(hash).c_str()); timer_DisplayClientTotals(); SAFE_DELETE(g_Game); // Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when // it's already destructed. g_ScriptRuntime.reset(); // Clean up delete &g_TexMan; delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); } void CReplayPlayer::TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick) { bool quick = (hashType == "hash-quick"); if ((quick && !testHashQuick) || (!quick && !testHashFull)) return; std::string hash; ENSURE(g_Game->GetSimulation2()->ComputeStateHash(hash, quick)); std::string hexHash = Hexify(hash); if (hexHash == replayHash) debug_printf("%s ok (%s)\n", hashType.c_str(), hexHash.c_str()); else debug_printf("%s MISMATCH (%s != %s)\n", hashType.c_str(), hexHash.c_str(), replayHash.c_str()); } Index: ps/trunk/source/ps/VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/VisualReplay.cpp (revision 22283) +++ ps/trunk/source/ps/VisualReplay.cpp (revision 22284) @@ -1,515 +1,521 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "VisualReplay.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "lib/allocators/shared_ptr.h" #include "lib/external_libraries/libsdl.h" #include "lib/utf8.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Paths.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/Util.h" #include "scriptinterface/ScriptInterface.h" /** * Filter too short replays (value in seconds). */ const u8 minimumReplayDuration = 3; -static const OsPath tempCacheFileName = VisualReplay::GetDirectoryName() / L"replayCache_temp.json"; -static const OsPath cacheFileName = VisualReplay::GetDirectoryName() / L"replayCache.json"; +OsPath VisualReplay::GetDirectoryPath() +{ + return Paths(g_args).UserData() / "replays" / engine_version; +} + +OsPath VisualReplay::GetCacheFilePath() +{ + return GetDirectoryPath() / L"replayCache.json"; +} -OsPath VisualReplay::GetDirectoryName() +OsPath VisualReplay::GetTempCacheFilePath() { - const Paths paths(g_args); - return OsPath(paths.UserData() / "replays" / engine_version); + return GetDirectoryPath() / L"replayCache_temp.json"; } bool VisualReplay::StartVisualReplay(const OsPath& directory) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); - const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt"; + const OsPath replayFile = VisualReplay::GetDirectoryPath() / directory / L"commands.txt"; if (!FileExists(replayFile)) return false; g_Game = new CGame(false, false); return g_Game->StartVisualReplay(replayFile); } bool VisualReplay::ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); - if (!FileExists(cacheFileName)) + if (!FileExists(GetCacheFilePath())) return false; - std::ifstream cacheStream(OsString(cacheFileName).c_str()); + std::ifstream cacheStream(OsString(GetCacheFilePath()).c_str()); CStr cacheStr((std::istreambuf_iterator(cacheStream)), std::istreambuf_iterator()); cacheStream.close(); JS::RootedValue cachedReplays(cx); if (scriptInterface.ParseJSON(cacheStr, &cachedReplays)) { cachedReplaysObject.set(&cachedReplays.toObject()); if (JS_IsArrayObject(cx, cachedReplaysObject)) return true; } LOGWARNING("The replay cache file is corrupted, it will be deleted"); - wunlink(cacheFileName); + wunlink(GetCacheFilePath()); return false; } void VisualReplay::StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue replaysRooted(cx, JS::ObjectValue(*replays)); - std::ofstream cacheStream(OsString(tempCacheFileName).c_str(), std::ofstream::out | std::ofstream::trunc); + std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc); cacheStream << scriptInterface.StringifyJSON(&replaysRooted); cacheStream.close(); - wunlink(cacheFileName); - if (wrename(tempCacheFileName, cacheFileName)) + wunlink(GetCacheFilePath()); + if (wrename(GetTempCacheFilePath(), GetCacheFilePath())) LOGERROR("Could not store the replay cache"); } JS::HandleObject VisualReplay::ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles) { TIMER(L"ReloadReplayCache"); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); // Maps the filename onto the index and size typedef std::map> replayCacheMap; replayCacheMap fileList; JS::RootedObject cachedReplaysObject(cx); if (ReadCacheFile(scriptInterface, &cachedReplaysObject)) { // Create list of files included in the cache u32 cacheLength = 0; JS_GetArrayLength(cx, cachedReplaysObject, &cacheLength); for (u32 j = 0; j < cacheLength; ++j) { JS::RootedValue replay(cx); JS_GetElement(cx, cachedReplaysObject, j, &replay); JS::RootedValue file(cx); OsPath fileName; double fileSize; scriptInterface.GetProperty(replay, "directory", fileName); scriptInterface.GetProperty(replay, "fileSize", fileSize); fileList[fileName] = std::make_pair(j, fileSize); } } JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0)); DirectoryNames directories; - if (GetDirectoryEntries(GetDirectoryName(), nullptr, &directories) != INFO::OK) + if (GetDirectoryEntries(GetDirectoryPath(), nullptr, &directories) != INFO::OK) return replays; bool newReplays = false; std::vector copyFromOldCache; // Specifies where the next replay should be kept u32 i = 0; for (const OsPath& directory : directories) { // This cannot use IsQuitRequested(), because the current loop and that function both run in the main thread. // So SDL events are not processed unless called explicitly here. if (SDL_QuitRequested()) // Don't return, because we want to save our progress break; - const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; + const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt"; bool isNew = true; replayCacheMap::iterator it = fileList.find(directory); if (it != fileList.end()) { if (compareFiles) { if (!FileExists(replayFile)) continue; CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); if (fileInfo.Size() == it->second.second) isNew = false; } else isNew = false; } if (isNew) { JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory)); if (replayData.isNull()) { if (!FileExists(replayFile)) continue; CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); scriptInterface.Eval("({})", &replayData); scriptInterface.SetProperty(replayData, "directory", directory.string()); scriptInterface.SetProperty(replayData, "fileSize", (double)fileInfo.Size()); } JS_SetElement(cx, replays, i++, replayData); newReplays = true; } else copyFromOldCache.push_back(it->second.first); } debug_printf( "Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n", (unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i); if (!newReplays && fileList.empty()) return replays; // No replay was changed, so just return the cache if (!newReplays && fileList.size() == copyFromOldCache.size()) return cachedReplaysObject; { // Copy the replays from the old cache that are not deleted if (!copyFromOldCache.empty()) for (u32 j : copyFromOldCache) { JS::RootedValue replay(cx); JS_GetElement(cx, cachedReplaysObject, j, &replay); JS_SetElement(cx, replays, i++, replay); } } StoreCacheFile(scriptInterface, replays); return replays; } JS::Value VisualReplay::GetReplays(const ScriptInterface& scriptInterface, bool compareFiles) { TIMER(L"GetReplays"); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedObject replays(cx, ReloadReplayCache(scriptInterface, compareFiles)); // Only take entries with data JS::RootedObject replaysWithoutNullEntries(cx, JS_NewArrayObject(cx, 0)); u32 replaysLength = 0; JS_GetArrayLength(cx, replays, &replaysLength); for (u32 j = 0, i = 0; j < replaysLength; ++j) { JS::RootedValue replay(cx); JS_GetElement(cx, replays, j, &replay); if (scriptInterface.HasProperty(replay, "attribs")) JS_SetElement(cx, replaysWithoutNullEntries, i++, replay); } return JS::ObjectValue(*replaysWithoutNullEntries); } /** * Move the cursor backwards until a newline was read or the beginning of the file was found. * Either way the cursor points to the beginning of a newline. * * @return The current cursor position or -1 on error. */ inline off_t goBackToLineBeginning(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { int currentPos; char character; for (int characters = 0; characters < 10000; ++characters) { currentPos = (int) replayStream->tellg(); // Stop when reached the beginning of the file if (currentPos == 0) return currentPos; if (!replayStream->good()) { LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.string8().c_str()); return -1; } // Stop when reached newline replayStream->get(character); if (character == '\n') return currentPos; // Otherwise go back one character. // Notice: -1 will set the cursor back to the most recently read character. replayStream->seekg(-2, std::ios_base::cur); } LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.string8().c_str()); return -1; } /** * Compute game duration in seconds. Assume constant turn length. * Find the last line that starts with "turn" by reading the file backwards. * * @return seconds or -1 on error */ inline int getReplayDuration(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { CStr type; // Move one character before the file-end replayStream->seekg(-2, std::ios_base::end); // Infinite loop protection, should never occur. // There should be about 5 lines to read until a turn is found. for (int linesRead = 1; linesRead < 1000; ++linesRead) { off_t currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); // Read error or reached file beginning. No turns exist. if (currentPosition < 1) return -1; if (!replayStream->good()) { LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.string8().c_str()); return -1; } // Found last turn, compute duration. if (currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") { u32 turn = 0, turnLength = 0; *replayStream >> turn >> turnLength; return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0 } // Otherwise move cursor back to the character before the last newline replayStream->seekg(currentPosition - 2, std::ios_base::beg); } LOGERROR("Infinite loop when determining replay duration for %s", fileName.string8().c_str()); return -1; } JS::Value VisualReplay::LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory) { // The directory argument must not be constant, otherwise concatenating will fail - const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; + const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt"; if (!FileExists(replayFile)) return JS::NullValue(); // Get file size and modification date CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); const off_t fileSize = fileInfo.Size(); if (fileSize == 0) return JS::NullValue(); std::ifstream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type; if (!(*replayStream >> type).good()) { LOGERROR("Couldn't open %s.", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } if (type != "start") { LOGWARNING("The replay %s doesn't begin with 'start'!", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } // Parse header / first line CStr header; std::getline(*replayStream, header); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attribs(cx); if (!scriptInterface.ParseJSON(header, &attribs)) { LOGERROR("Couldn't parse replay header of %s", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } // Ensure "turn" after header if (!(*replayStream >> type).good() || type != "turn") { SAFE_DELETE(replayStream); return JS::NullValue(); // there are no turns at all } // Don't process files of rejoined clients u32 turn = 1; *replayStream >> turn; if (turn != 0) { SAFE_DELETE(replayStream); return JS::NullValue(); } int duration = getReplayDuration(replayStream, replayFile, fileSize); SAFE_DELETE(replayStream); // Ensure minimum duration if (duration < minimumReplayDuration) return JS::NullValue(); // Return the actual data JS::RootedValue replayData(cx); scriptInterface.Eval("({})", &replayData); scriptInterface.SetProperty(replayData, "directory", directory.string()); scriptInterface.SetProperty(replayData, "fileSize", (double)fileSize); scriptInterface.SetProperty(replayData, "attribs", attribs); scriptInterface.SetProperty(replayData, "duration", duration); return replayData; } bool VisualReplay::DeleteReplay(const OsPath& replayDirectory) { if (replayDirectory.empty()) return false; - const OsPath directory = GetDirectoryName() / replayDirectory; + const OsPath directory = GetDirectoryPath() / replayDirectory; return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; } JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName) { // Create empty JS object JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue attribs(cx); pCxPrivate->pScriptInterface->Eval("({})", &attribs); // Return empty object if file doesn't exist - const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt"; + const OsPath replayFile = GetDirectoryPath() / directoryName / L"commands.txt"; if (!FileExists(replayFile)) return attribs; // Open file std::istream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type, line; ENSURE((*replayStream >> type).good() && type == "start"); // Read and return first line std::getline(*replayStream, line); pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); SAFE_DELETE(replayStream);; return attribs; } void VisualReplay::AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName) { TIMER(L"AddReplayToCache"); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, OsPath(directoryName))); if (replayData.isNull()) return; JS::RootedObject cachedReplaysObject(cx); if (!ReadCacheFile(scriptInterface, &cachedReplaysObject)) cachedReplaysObject = JS_NewArrayObject(cx, 0); u32 cacheLength = 0; JS_GetArrayLength(cx, cachedReplaysObject, &cacheLength); JS_SetElement(cx, cachedReplaysObject, cacheLength, replayData); StoreCacheFile(scriptInterface, cachedReplaysObject); } void VisualReplay::SaveReplayMetadata(ScriptInterface* scriptInterface) { JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue metadata(cx); JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); if (!scriptInterface->CallFunction(global, "getReplayMetadata", &metadata)) { LOGERROR("Could not save replay metadata!"); return; } // Get the directory of the currently active replay const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json"; CreateDirectories(fileName.Parent(), 0700); std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc); stream << scriptInterface->StringifyJSON(&metadata, false); stream.close(); debug_printf("Saved replay metadata to %s\n", fileName.string8().c_str()); } bool VisualReplay::HasReplayMetadata(const OsPath& directoryName) { - const OsPath filePath(GetDirectoryName() / directoryName / L"metadata.json"); + const OsPath filePath(GetDirectoryPath() / directoryName / L"metadata.json"); if (!FileExists(filePath)) return false; CFileInfo fileInfo; GetFileInfo(filePath, &fileInfo); return fileInfo.Size() > 0; } JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName) { if (!HasReplayMetadata(directoryName)) return JS::NullValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue metadata(cx); - std::ifstream* stream = new std::ifstream(OsString(GetDirectoryName() / directoryName / L"metadata.json").c_str()); + std::ifstream* stream = new std::ifstream(OsString(GetDirectoryPath() / directoryName / L"metadata.json").c_str()); ENSURE(stream->good()); CStr line; std::getline(*stream, line); stream->close(); SAFE_DELETE(stream); pCxPrivate->pScriptInterface->ParseJSON(line, &metadata); return metadata; } Index: ps/trunk/source/ps/VisualReplay.h =================================================================== --- ps/trunk/source/ps/VisualReplay.h (revision 22283) +++ ps/trunk/source/ps/VisualReplay.h (revision 22284) @@ -1,120 +1,128 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_REPlAY #define INCLUDED_REPlAY #include "scriptinterface/ScriptInterface.h" class CSimulation2; class CGUIManager; /** * Contains functions for visually replaying past games. */ namespace VisualReplay { /** - * Returns the path to the sim-log directory (that contains the directories with the replay files. - * - * @param scriptInterface - the ScriptInterface in which to create the return data. - * @return OsPath the absolute file path + * Returns the absolute path to the sim-log directory (that contains the directories with the replay files. + */ +OsPath GetDirectoryPath(); + +/** + * Returns the absolute path to the replay cache file. + */ +OsPath GetCacheFilePath(); + +/** + * Returns the absolute path to the temporary replay cache file used to + * always have a valid cache file in place even if bad things happen. */ -OsPath GetDirectoryName(); +OsPath GetTempCacheFilePath(); /** * Replays the commands.txt file in the given subdirectory visually. */ bool StartVisualReplay(const OsPath& directory); /** * Reads the replay Cache file and parses it into a jsObject * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param cachedReplaysObject - the cached replays. * @return true on succes */ bool ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject); /** * Stores the replay list in the replay cache file * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param replays - the replay list to store. */ void StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays); /** * Load the replay cache and check if there are new/deleted replays. If so, update the cache. * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param compareFiles - compare the directory name and the FileSize of the replays and the cache. * @return cache entries */ JS::HandleObject ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles); /** * Get a list of replays to display in the GUI. * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param compareFiles - reload the cache, which takes more time, * but nearly ensures, that no changed replay is missed. * @return array of objects containing replay data */ JS::Value GetReplays(const ScriptInterface& scriptInterface, bool compareFiles); /** * Parses a commands.txt file and extracts metadata. * Works similarly to CGame::LoadReplayData(). */ JS::Value LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory); /** * Permanently deletes the visual replay (including the parent directory) * * @param replayFile - path to commands.txt, whose parent directory will be deleted. * @return true if deletion was successful, false on error */ bool DeleteReplay(const OsPath& replayFile); /** * Returns the parsed header of the replay file (commands.txt). */ JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName); /** * Returns whether or not the metadata / summary screen data has been saved properly when the game ended. */ bool HasReplayMetadata(const OsPath& directoryName); /** * Returns the metadata of a replay. */ JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName); /** * Saves the metadata from the session to metadata.json. */ void SaveReplayMetadata(ScriptInterface* scriptInterface); /** * Adds a replay to the replayCache. */ void AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName); } #endif Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 22283) +++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 22284) @@ -1,77 +1,77 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_VisualReplay.h" #include "ps/CStr.h" #include "ps/Profile.h" #include "ps/VisualReplay.h" #include "scriptinterface/ScriptInterface.h" bool JSI_VisualReplay::StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directory) { return VisualReplay::StartVisualReplay(directory); } bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& replayFile) { return VisualReplay::DeleteReplay(replayFile); } JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate, bool compareFiles) { return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface), compareFiles); } JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) { return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName); } bool JSI_VisualReplay::HasReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName) { return VisualReplay::HasReplayMetadata(directoryName); } JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) { return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName); } void JSI_VisualReplay::AddReplayToCache(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) { VisualReplay::AddReplayToCache(*(pCxPrivate->pScriptInterface), directoryName); } CStrW JSI_VisualReplay::GetReplayDirectoryName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName) { - return wstring_from_utf8(OsPath(VisualReplay::GetDirectoryName() / directoryName).string8()); + return wstring_from_utf8(OsPath(VisualReplay::GetDirectoryPath() / directoryName).string8()); } void JSI_VisualReplay::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetReplays"); scriptInterface.RegisterFunction("DeleteReplay"); scriptInterface.RegisterFunction("StartVisualReplay"); scriptInterface.RegisterFunction("GetReplayAttributes"); scriptInterface.RegisterFunction("GetReplayMetadata"); scriptInterface.RegisterFunction("HasReplayMetadata"); scriptInterface.RegisterFunction("AddReplayToCache"); scriptInterface.RegisterFunction("GetReplayDirectoryName"); }