Index: ps/trunk/source/ps/VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/VisualReplay.cpp (revision 19812) +++ ps/trunk/source/ps/VisualReplay.cpp (revision 19813) @@ -1,510 +1,514 @@ /* Copyright (C) 2017 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::GetDirectoryName() { const Paths paths(g_args); return OsPath(paths.UserData() / "replays" / engine_version); } void VisualReplay::StartVisualReplay(const CStrW& directory) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt"; if (!FileExists(replayFile)) return; g_Game = new CGame(false, false); g_Game->StartVisualReplay(replayFile.string8()); } bool VisualReplay::ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); if (!FileExists(cacheFileName)) return false; std::ifstream cacheStream(cacheFileName.string8().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); return false; } void VisualReplay::StoreCacheFile(ScriptInterface& scriptInterface, JS::HandleObject replays) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue replaysRooted(cx, JS::ObjectValue(*replays)); std::ofstream cacheStream(tempCacheFileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc); cacheStream << scriptInterface.StringifyJSON(&replaysRooted); cacheStream.close(); wunlink(cacheFileName); if (wrename(tempCacheFileName, cacheFileName)) LOGERROR("Could not store the replay cache"); } JS::HandleObject VisualReplay::ReloadReplayCache(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); CStr 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) return replays; bool newReplays = false; std::vector copyFromOldCache; // Specifies where the next replay should be kept u32 i = 0; for (const OsPath& directory : directories) { if (SDL_QuitRequested()) // We want to save our progress in searching through the replays break; + const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; + if (!FileExists(replayFile)) + continue; + bool isNew = true; replayCacheMap::iterator it = fileList.find(directory.string8()); if (it != fileList.end()) { if (compareFiles) { CFileInfo fileInfo; - GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &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()) { CFileInfo fileInfo; - GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &fileInfo); + GetFileInfo(replayFile, &fileInfo); scriptInterface.Eval("({})", &replayData); scriptInterface.SetProperty(replayData, "directory", directory); 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(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 int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64& 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.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.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 CStr& fileName, const u64& 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) { int 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.c_str()); return -1; } // Found last turn, compute duration. if ((u64) 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.c_str()); return -1; } JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory) { // The directory argument must not be constant, otherwise concatenating will fail const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; if (!FileExists(replayFile)) return JSVAL_NULL; // Get file size and modification date CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); const u64 fileSize = (u64)fileInfo.Size(); if (fileSize == 0) return JSVAL_NULL; // Open file const CStr fileName = replayFile.string8(); std::ifstream* replayStream = new std::ifstream(fileName.c_str()); // File must begin with "start" CStr type; if (!(*replayStream >> type).good()) { LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } if (type != "start") { LOGWARNING("The replay %s is broken!", fileName.c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } // 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", fileName.c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } // Ensure "turn" after header if (!(*replayStream >> type).good() || type != "turn") { SAFE_DELETE(replayStream); return JSVAL_NULL; // 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 JSVAL_NULL; } int duration = getReplayDuration(replayStream, fileName, fileSize); SAFE_DELETE(replayStream); // Ensure minimum duration if (duration < minimumReplayDuration) return JSVAL_NULL; // Return the actual data JS::RootedValue replayData(cx); scriptInterface.Eval("({})", &replayData); scriptInterface.SetProperty(replayData, "file", replayFile); scriptInterface.SetProperty(replayData, "directory", directory); scriptInterface.SetProperty(replayData, "fileSize", (double)fileSize); scriptInterface.SetProperty(replayData, "attribs", attribs); scriptInterface.SetProperty(replayData, "duration", duration); return replayData; } bool VisualReplay::DeleteReplay(const CStrW& replayDirectory) { if (replayDirectory.empty()) return false; const OsPath directory = GetDirectoryName() / replayDirectory; return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; } JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& 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"; if (!FileExists(replayFile)) return attribs; // Open file std::istream* replayStream = new std::ifstream(replayFile.string8().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(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 (fileName.string8().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 CStrW& directoryName) { const OsPath filePath(GetDirectoryName() / 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 CStrW& directoryName) { if (!HasReplayMetadata(directoryName)) return JSVAL_NULL; JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue metadata(cx); std::ifstream* stream = new std::ifstream(OsPath(GetDirectoryName() / directoryName / L"metadata.json").string8()); ENSURE(stream->good()); CStr line; std::getline(*stream, line); stream->close(); SAFE_DELETE(stream); pCxPrivate->pScriptInterface->ParseJSON(line, &metadata); return metadata; }