LCOV - code coverage report
Current view: top level - source/ps - VisualReplay.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 1 250 0.4 %
Date: 2023-01-19 00:18:29 Functions: 2 18 11.1 %

          Line data    Source code
       1             : /* Copyright (C) 2021 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "VisualReplay.h"
      21             : #include "graphics/GameView.h"
      22             : #include "lib/timer.h"
      23             : #include "lib/utf8.h"
      24             : #include "lib/allocators/shared_ptr.h"
      25             : #include "lib/file/file_system.h"
      26             : #include "lib/external_libraries/libsdl.h"
      27             : #include "network/NetClient.h"
      28             : #include "network/NetServer.h"
      29             : #include "ps/CLogger.h"
      30             : #include "ps/Filesystem.h"
      31             : #include "ps/Game.h"
      32             : #include "ps/GameSetup/CmdLineArgs.h"
      33             : #include "ps/GameSetup/Paths.h"
      34             : #include "ps/Mod.h"
      35             : #include "ps/Pyrogenesis.h"
      36             : #include "ps/Replay.h"
      37             : #include "ps/Util.h"
      38             : #include "scriptinterface/JSON.h"
      39             : 
      40             : #include <fstream>
      41             : 
      42             : /**
      43             :  * Filter too short replays (value in seconds).
      44             :  */
      45             : const u8 minimumReplayDuration = 3;
      46             : 
      47           0 : OsPath VisualReplay::GetDirectoryPath()
      48             : {
      49           0 :     return Paths(g_CmdLineArgs).UserData() / "replays" / engine_version;
      50             : }
      51             : 
      52           0 : OsPath VisualReplay::GetCacheFilePath()
      53             : {
      54           0 :     return GetDirectoryPath() / L"replayCache.json";
      55             : }
      56             : 
      57           0 : OsPath VisualReplay::GetTempCacheFilePath()
      58             : {
      59           0 :     return GetDirectoryPath() / L"replayCache_temp.json";
      60             : }
      61             : 
      62           0 : bool VisualReplay::StartVisualReplay(const OsPath& directory)
      63             : {
      64           0 :     ENSURE(!g_NetServer);
      65           0 :     ENSURE(!g_NetClient);
      66           0 :     ENSURE(!g_Game);
      67             : 
      68           0 :     const OsPath replayFile = VisualReplay::GetDirectoryPath() / directory / L"commands.txt";
      69             : 
      70           0 :     if (!FileExists(replayFile))
      71           0 :         return false;
      72             : 
      73           0 :     g_Game = new CGame(false);
      74           0 :     return g_Game->StartVisualReplay(replayFile);
      75             : }
      76             : 
      77           0 : bool VisualReplay::ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject)
      78             : {
      79           0 :     if (!FileExists(GetCacheFilePath()))
      80           0 :         return false;
      81             : 
      82           0 :     std::ifstream cacheStream(OsString(GetCacheFilePath()).c_str());
      83           0 :     CStr cacheStr((std::istreambuf_iterator<char>(cacheStream)), std::istreambuf_iterator<char>());
      84           0 :     cacheStream.close();
      85             : 
      86           0 :     ScriptRequest rq(scriptInterface);
      87             : 
      88           0 :     JS::RootedValue cachedReplays(rq.cx);
      89           0 :     if (Script::ParseJSON(rq, cacheStr, &cachedReplays))
      90             :     {
      91           0 :         cachedReplaysObject.set(&cachedReplays.toObject());
      92             :         bool isArray;
      93           0 :         if (JS::IsArrayObject(rq.cx, cachedReplaysObject, &isArray) && isArray)
      94           0 :             return true;
      95             :     }
      96             : 
      97           0 :     LOGWARNING("The replay cache file is corrupted, it will be deleted");
      98           0 :     wunlink(GetCacheFilePath());
      99           0 :     return false;
     100             : }
     101             : 
     102           0 : void VisualReplay::StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays)
     103             : {
     104           0 :     ScriptRequest rq(scriptInterface);
     105             : 
     106           0 :     JS::RootedValue replaysRooted(rq.cx, JS::ObjectValue(*replays));
     107           0 :     std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc);
     108           0 :     cacheStream << Script::StringifyJSON(rq, &replaysRooted);
     109           0 :     cacheStream.close();
     110             : 
     111           0 :     wunlink(GetCacheFilePath());
     112           0 :     if (RenameFile(GetTempCacheFilePath(), GetCacheFilePath()))
     113           0 :         LOGERROR("Could not store the replay cache");
     114           0 : }
     115             : 
     116           0 : JS::HandleObject VisualReplay::ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles)
     117             : {
     118           0 :     TIMER(L"ReloadReplayCache");
     119           0 :     ScriptRequest rq(scriptInterface);
     120             : 
     121             :     // Maps the filename onto the index, mtime and size
     122             :     using replayCacheMap = std::map<OsPath, std::tuple<u32, u64, off_t>>;
     123             : 
     124           0 :     replayCacheMap fileList;
     125             : 
     126           0 :     JS::RootedObject cachedReplaysObject(rq.cx);
     127           0 :     if (ReadCacheFile(scriptInterface, &cachedReplaysObject))
     128             :     {
     129             :         // Create list of files included in the cache
     130           0 :         u32 cacheLength = 0;
     131           0 :         JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength);
     132           0 :         for (u32 j = 0; j < cacheLength; ++j)
     133             :         {
     134           0 :             JS::RootedValue replay(rq.cx);
     135           0 :             JS_GetElement(rq.cx, cachedReplaysObject, j, &replay);
     136             : 
     137           0 :             JS::RootedValue file(rq.cx);
     138           0 :             OsPath fileName;
     139             :             double fileSize;
     140             :             double fileMtime;
     141           0 :             Script::GetProperty(rq, replay, "directory", fileName);
     142           0 :             Script::GetProperty(rq, replay, "fileSize", fileSize);
     143           0 :             Script::GetProperty(rq, replay, "fileMTime", fileMtime);
     144             : 
     145           0 :             fileList[fileName] = std::make_tuple(j, fileMtime, fileSize);
     146             :         }
     147             :     }
     148             : 
     149           0 :     JS::RootedObject replays(rq.cx, JS::NewArrayObject(rq.cx, 0));
     150           0 :     DirectoryNames directories;
     151             : 
     152           0 :     if (GetDirectoryEntries(GetDirectoryPath(), nullptr, &directories) != INFO::OK)
     153           0 :         return replays;
     154             : 
     155           0 :     bool newReplays = false;
     156           0 :     std::vector<u32> copyFromOldCache;
     157             :     // Specifies where the next replay should be kept
     158           0 :     u32 i = 0;
     159             : 
     160           0 :     for (const OsPath& directory : directories)
     161             :     {
     162             :         // This cannot use IsQuitRequested(), because the current loop and that function both run in the main thread.
     163             :         // So SDL events are not processed unless called explicitly here.
     164           0 :         if (SDL_QuitRequested())
     165             :             // Don't return, because we want to save our progress
     166           0 :             break;
     167             : 
     168           0 :         const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt";
     169             : 
     170           0 :         bool isNew = true;
     171           0 :         replayCacheMap::iterator it = fileList.find(directory);
     172           0 :         if (it != fileList.end())
     173             :         {
     174           0 :             if (compareFiles)
     175             :             {
     176           0 :                 if (!FileExists(replayFile))
     177           0 :                     continue;
     178           0 :                 CFileInfo fileInfo;
     179           0 :                 GetFileInfo(replayFile, &fileInfo);
     180           0 :                 if ((u64)fileInfo.MTime() == std::get<1>(it->second) && (off_t)fileInfo.Size() == std::get<2>(it->second))
     181           0 :                     isNew = false;
     182             :             }
     183             :             else
     184           0 :                 isNew = false;
     185             :         }
     186             : 
     187           0 :         if (isNew)
     188             :         {
     189           0 :             JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, directory));
     190           0 :             if (replayData.isNull())
     191             :             {
     192           0 :                 if (!FileExists(replayFile))
     193           0 :                     continue;
     194           0 :                 CFileInfo fileInfo;
     195           0 :                 GetFileInfo(replayFile, &fileInfo);
     196             : 
     197           0 :                 Script::CreateObject(
     198             :                     rq,
     199             :                     &replayData,
     200           0 :                     "directory", directory.string(),
     201           0 :                     "fileMTime", static_cast<double>(fileInfo.MTime()),
     202           0 :                     "fileSize", static_cast<double>(fileInfo.Size()));
     203             :             }
     204           0 :             JS_SetElement(rq.cx, replays, i++, replayData);
     205           0 :             newReplays = true;
     206             :         }
     207             :         else
     208           0 :             copyFromOldCache.push_back(std::get<0>(it->second));
     209             :     }
     210             : 
     211           0 :     debug_printf(
     212             :         "Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n",
     213           0 :         (unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i);
     214             : 
     215           0 :     if (!newReplays && fileList.empty())
     216           0 :         return replays;
     217             : 
     218             :     // No replay was changed, so just return the cache
     219           0 :     if (!newReplays && fileList.size() == copyFromOldCache.size())
     220           0 :         return cachedReplaysObject;
     221             : 
     222             :     {
     223             :         // Copy the replays from the old cache that are not deleted
     224           0 :         if (!copyFromOldCache.empty())
     225           0 :             for (u32 j : copyFromOldCache)
     226             :             {
     227           0 :                 JS::RootedValue replay(rq.cx);
     228           0 :                 JS_GetElement(rq.cx, cachedReplaysObject, j, &replay);
     229           0 :                 JS_SetElement(rq.cx, replays, i++, replay);
     230             :             }
     231             :     }
     232           0 :     StoreCacheFile(scriptInterface, replays);
     233           0 :     return replays;
     234             : }
     235             : 
     236           0 : JS::Value VisualReplay::GetReplays(const ScriptInterface& scriptInterface, bool compareFiles)
     237             : {
     238           0 :     TIMER(L"GetReplays");
     239             : 
     240           0 :     ScriptRequest rq(scriptInterface);
     241           0 :     JS::RootedObject replays(rq.cx, ReloadReplayCache(scriptInterface, compareFiles));
     242             :     // Only take entries with data
     243           0 :     JS::RootedValue replaysWithoutNullEntries(rq.cx);
     244           0 :     Script::CreateArray(rq, &replaysWithoutNullEntries);
     245             : 
     246           0 :     u32 replaysLength = 0;
     247           0 :     JS::GetArrayLength(rq.cx, replays, &replaysLength);
     248           0 :     for (u32 j = 0, i = 0; j < replaysLength; ++j)
     249             :     {
     250           0 :         JS::RootedValue replay(rq.cx);
     251           0 :         JS_GetElement(rq.cx, replays, j, &replay);
     252           0 :         if (Script::HasProperty(rq, replay, "attribs"))
     253           0 :             Script::SetPropertyInt(rq, replaysWithoutNullEntries, i++, replay);
     254             :     }
     255           0 :     return replaysWithoutNullEntries;
     256             : }
     257             : 
     258             : /**
     259             :  * Move the cursor backwards until a newline was read or the beginning of the file was found.
     260             :  * Either way the cursor points to the beginning of a newline.
     261             :  *
     262             :  * @return The current cursor position or -1 on error.
     263             :  */
     264           0 : inline off_t goBackToLineBeginning(std::istream* replayStream, const OsPath& fileName, off_t fileSize)
     265             : {
     266             :     int currentPos;
     267             :     char character;
     268           0 :     for (int characters = 0; characters < 10000; ++characters)
     269             :     {
     270           0 :         currentPos = (int) replayStream->tellg();
     271             : 
     272             :         // Stop when reached the beginning of the file
     273           0 :         if (currentPos == 0)
     274           0 :             return currentPos;
     275             : 
     276           0 :         if (!replayStream->good())
     277             :         {
     278           0 :             LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.string8().c_str());
     279           0 :             return -1;
     280             :         }
     281             : 
     282             :         // Stop when reached newline
     283           0 :         replayStream->get(character);
     284           0 :         if (character == '\n')
     285           0 :             return currentPos;
     286             : 
     287             :         // Otherwise go back one character.
     288             :         // Notice: -1 will set the cursor back to the most recently read character.
     289           0 :         replayStream->seekg(-2, std::ios_base::cur);
     290             :     }
     291             : 
     292           0 :     LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.string8().c_str());
     293           0 :     return -1;
     294             : }
     295             : 
     296             : /**
     297             :  * Compute game duration in seconds. Assume constant turn length.
     298             :  * Find the last line that starts with "turn" by reading the file backwards.
     299             :  *
     300             :  * @return seconds or -1 on error
     301             :  */
     302           0 : inline int getReplayDuration(std::istream* replayStream, const OsPath& fileName, off_t fileSize)
     303             : {
     304           0 :     CStr type;
     305             : 
     306             :     // Move one character before the file-end
     307           0 :     replayStream->seekg(-2, std::ios_base::end);
     308             : 
     309             :     // Infinite loop protection, should never occur.
     310             :     // There should be about 5 lines to read until a turn is found.
     311           0 :     for (int linesRead = 1; linesRead < 1000; ++linesRead)
     312             :     {
     313           0 :         off_t currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize);
     314             : 
     315             :         // Read error or reached file beginning. No turns exist.
     316           0 :         if (currentPosition < 1)
     317           0 :             return -1;
     318             : 
     319           0 :         if (!replayStream->good())
     320             :         {
     321           0 :             LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.string8().c_str());
     322           0 :             return -1;
     323             :         }
     324             : 
     325             :         // Found last turn, compute duration.
     326           0 :         if (currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn")
     327             :         {
     328           0 :             u32 turn = 0, turnLength = 0;
     329           0 :             *replayStream >> turn >> turnLength;
     330           0 :             return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0
     331             :         }
     332             : 
     333             :         // Otherwise move cursor back to the character before the last newline
     334           0 :         replayStream->seekg(currentPosition - 2, std::ios_base::beg);
     335             :     }
     336             : 
     337           0 :     LOGERROR("Infinite loop when determining replay duration for %s", fileName.string8().c_str());
     338           0 :     return -1;
     339             : }
     340             : 
     341           0 : JS::Value VisualReplay::LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory)
     342             : {
     343             :     // The directory argument must not be constant, otherwise concatenating will fail
     344           0 :     const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt";
     345             : 
     346           0 :     if (!FileExists(replayFile))
     347           0 :         return JS::NullValue();
     348             : 
     349             :     // Get file size and modification date
     350           0 :     CFileInfo fileInfo;
     351           0 :     GetFileInfo(replayFile, &fileInfo);
     352           0 :     const off_t fileSize = fileInfo.Size();
     353             : 
     354           0 :     if (fileSize == 0)
     355           0 :         return JS::NullValue();
     356             : 
     357           0 :     std::ifstream* replayStream = new std::ifstream(OsString(replayFile).c_str());
     358             : 
     359           0 :     CStr type;
     360           0 :     if (!(*replayStream >> type).good())
     361             :     {
     362           0 :         LOGERROR("Couldn't open %s.", replayFile.string8().c_str());
     363           0 :         SAFE_DELETE(replayStream);
     364           0 :         return JS::NullValue();
     365             :     }
     366             : 
     367           0 :     if (type != "start")
     368             :     {
     369           0 :         LOGWARNING("The replay %s doesn't begin with 'start'!", replayFile.string8().c_str());
     370           0 :         SAFE_DELETE(replayStream);
     371           0 :         return JS::NullValue();
     372             :     }
     373             : 
     374             :     // Parse header / first line
     375           0 :     CStr header;
     376           0 :     std::getline(*replayStream, header);
     377           0 :     ScriptRequest rq(scriptInterface);
     378           0 :     JS::RootedValue attribs(rq.cx);
     379           0 :     if (!Script::ParseJSON(rq, header, &attribs))
     380             :     {
     381           0 :         LOGERROR("Couldn't parse replay header of %s", replayFile.string8().c_str());
     382           0 :         SAFE_DELETE(replayStream);
     383           0 :         return JS::NullValue();
     384             :     }
     385             : 
     386             :     // Ensure "turn" after header
     387           0 :     if (!(*replayStream >> type).good() || type != "turn")
     388             :     {
     389           0 :         SAFE_DELETE(replayStream);
     390           0 :         return JS::NullValue(); // there are no turns at all
     391             :     }
     392             : 
     393             :     // Don't process files of rejoined clients
     394           0 :     u32 turn = 1;
     395           0 :     *replayStream >> turn;
     396           0 :     if (turn != 0)
     397             :     {
     398           0 :         SAFE_DELETE(replayStream);
     399           0 :         return JS::NullValue();
     400             :     }
     401             : 
     402           0 :     int duration = getReplayDuration(replayStream, replayFile, fileSize);
     403             : 
     404           0 :     SAFE_DELETE(replayStream);
     405             : 
     406             :     // Ensure minimum duration
     407           0 :     if (duration < minimumReplayDuration)
     408           0 :         return JS::NullValue();
     409             : 
     410             :     // Return the actual data
     411           0 :     JS::RootedValue replayData(rq.cx);
     412             : 
     413           0 :     Script::CreateObject(
     414             :         rq,
     415             :         &replayData,
     416           0 :         "directory", directory.string(),
     417           0 :         "fileSize", static_cast<double>(fileSize),
     418           0 :         "fileMTime", static_cast<double>(fileInfo.MTime()),
     419             :         "duration", duration);
     420             : 
     421           0 :     Script::SetProperty(rq, replayData, "attribs", attribs);
     422             : 
     423           0 :     return replayData;
     424             : }
     425             : 
     426           0 : bool VisualReplay::DeleteReplay(const OsPath& replayDirectory)
     427             : {
     428           0 :     if (replayDirectory.empty())
     429           0 :         return false;
     430             : 
     431           0 :     const OsPath directory = GetDirectoryPath() / replayDirectory;
     432           0 :     return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
     433             : }
     434             : 
     435           0 : JS::Value VisualReplay::GetReplayAttributes(const ScriptInterface& scriptInterface, const OsPath& directoryName)
     436             : {
     437             :     // Create empty JS object
     438           0 :     ScriptRequest rq(scriptInterface);
     439           0 :     JS::RootedValue attribs(rq.cx);
     440           0 :     Script::CreateObject(rq, &attribs);
     441             : 
     442             :     // Return empty object if file doesn't exist
     443           0 :     const OsPath replayFile = GetDirectoryPath() / directoryName / L"commands.txt";
     444           0 :     if (!FileExists(replayFile))
     445           0 :         return attribs;
     446             : 
     447             :     // Open file
     448           0 :     std::istream* replayStream = new std::ifstream(OsString(replayFile).c_str());
     449           0 :     CStr type, line;
     450           0 :     ENSURE((*replayStream >> type).good() && type == "start");
     451             : 
     452             :     // Read and return first line
     453           0 :     std::getline(*replayStream, line);
     454           0 :     Script::ParseJSON(rq, line, &attribs);
     455           0 :     SAFE_DELETE(replayStream);;
     456           0 :     return attribs;
     457             : }
     458             : 
     459           0 : void VisualReplay::AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName)
     460             : {
     461           0 :     TIMER(L"AddReplayToCache");
     462           0 :     ScriptRequest rq(scriptInterface);
     463             : 
     464           0 :     JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, OsPath(directoryName)));
     465           0 :     if (replayData.isNull())
     466           0 :         return;
     467             : 
     468           0 :     JS::RootedObject cachedReplaysObject(rq.cx);
     469           0 :     if (!ReadCacheFile(scriptInterface, &cachedReplaysObject))
     470           0 :         cachedReplaysObject = JS::NewArrayObject(rq.cx, 0);
     471             : 
     472           0 :     u32 cacheLength = 0;
     473           0 :     JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength);
     474           0 :     JS_SetElement(rq.cx, cachedReplaysObject, cacheLength, replayData);
     475             : 
     476           0 :     StoreCacheFile(scriptInterface, cachedReplaysObject);
     477             : }
     478             : 
     479           0 : bool VisualReplay::HasReplayMetadata(const OsPath& directoryName)
     480             : {
     481           0 :     const OsPath filePath(GetDirectoryPath() / directoryName / L"metadata.json");
     482             : 
     483           0 :     if (!FileExists(filePath))
     484           0 :         return false;
     485             : 
     486           0 :     CFileInfo fileInfo;
     487           0 :     GetFileInfo(filePath, &fileInfo);
     488             : 
     489           0 :     return fileInfo.Size() > 0;
     490             : }
     491             : 
     492           0 : JS::Value VisualReplay::GetReplayMetadata(const ScriptInterface& scriptInterface, const OsPath& directoryName)
     493             : {
     494           0 :     if (!HasReplayMetadata(directoryName))
     495           0 :         return JS::NullValue();
     496             : 
     497           0 :     ScriptRequest rq(scriptInterface);
     498           0 :     JS::RootedValue metadata(rq.cx);
     499             : 
     500           0 :     std::ifstream* stream = new std::ifstream(OsString(GetDirectoryPath() / directoryName / L"metadata.json").c_str());
     501           0 :     ENSURE(stream->good());
     502           0 :     CStr line;
     503           0 :     std::getline(*stream, line);
     504           0 :     stream->close();
     505           0 :     SAFE_DELETE(stream);
     506           0 :     Script::ParseJSON(rq, line, &metadata);
     507             : 
     508           0 :     return metadata;
     509           3 : }

Generated by: LCOV version 1.13