Recenter Han emblem logo slightly and fix the name.
[0ad.git] / source / ps / VisualReplay.cpp
blobc7e02696918afcbcda9bf27dcae04f147cd9e7c3
1 /* Copyright (C) 2021 Wildfire Games.
2 * This file is part of 0 A.D.
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.
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.
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/>.
18 #include "precompiled.h"
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"
40 #include <fstream>
42 /**
43 * Filter too short replays (value in seconds).
45 const u8 minimumReplayDuration = 3;
47 OsPath VisualReplay::GetDirectoryPath()
49 return Paths(g_CmdLineArgs).UserData() / "replays" / engine_version;
52 OsPath VisualReplay::GetCacheFilePath()
54 return GetDirectoryPath() / L"replayCache.json";
57 OsPath VisualReplay::GetTempCacheFilePath()
59 return GetDirectoryPath() / L"replayCache_temp.json";
62 bool VisualReplay::StartVisualReplay(const OsPath& directory)
64 ENSURE(!g_NetServer);
65 ENSURE(!g_NetClient);
66 ENSURE(!g_Game);
68 const OsPath replayFile = VisualReplay::GetDirectoryPath() / directory / L"commands.txt";
70 if (!FileExists(replayFile))
71 return false;
73 g_Game = new CGame(false);
74 return g_Game->StartVisualReplay(replayFile);
77 bool VisualReplay::ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject)
79 if (!FileExists(GetCacheFilePath()))
80 return false;
82 std::ifstream cacheStream(OsString(GetCacheFilePath()).c_str());
83 CStr cacheStr((std::istreambuf_iterator<char>(cacheStream)), std::istreambuf_iterator<char>());
84 cacheStream.close();
86 ScriptRequest rq(scriptInterface);
88 JS::RootedValue cachedReplays(rq.cx);
89 if (Script::ParseJSON(rq, cacheStr, &cachedReplays))
91 cachedReplaysObject.set(&cachedReplays.toObject());
92 bool isArray;
93 if (JS::IsArrayObject(rq.cx, cachedReplaysObject, &isArray) && isArray)
94 return true;
97 LOGWARNING("The replay cache file is corrupted, it will be deleted");
98 wunlink(GetCacheFilePath());
99 return false;
102 void VisualReplay::StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays)
104 ScriptRequest rq(scriptInterface);
106 JS::RootedValue replaysRooted(rq.cx, JS::ObjectValue(*replays));
107 std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc);
108 cacheStream << Script::StringifyJSON(rq, &replaysRooted);
109 cacheStream.close();
111 wunlink(GetCacheFilePath());
112 if (RenameFile(GetTempCacheFilePath(), GetCacheFilePath()))
113 LOGERROR("Could not store the replay cache");
116 JS::HandleObject VisualReplay::ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles)
118 TIMER(L"ReloadReplayCache");
119 ScriptRequest rq(scriptInterface);
121 // Maps the filename onto the index, mtime and size
122 using replayCacheMap = std::map<OsPath, std::tuple<u32, u64, off_t>>;
124 replayCacheMap fileList;
126 JS::RootedObject cachedReplaysObject(rq.cx);
127 if (ReadCacheFile(scriptInterface, &cachedReplaysObject))
129 // Create list of files included in the cache
130 u32 cacheLength = 0;
131 JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength);
132 for (u32 j = 0; j < cacheLength; ++j)
134 JS::RootedValue replay(rq.cx);
135 JS_GetElement(rq.cx, cachedReplaysObject, j, &replay);
137 JS::RootedValue file(rq.cx);
138 OsPath fileName;
139 double fileSize;
140 double fileMtime;
141 Script::GetProperty(rq, replay, "directory", fileName);
142 Script::GetProperty(rq, replay, "fileSize", fileSize);
143 Script::GetProperty(rq, replay, "fileMTime", fileMtime);
145 fileList[fileName] = std::make_tuple(j, fileMtime, fileSize);
149 JS::RootedObject replays(rq.cx, JS::NewArrayObject(rq.cx, 0));
150 DirectoryNames directories;
152 if (GetDirectoryEntries(GetDirectoryPath(), nullptr, &directories) != INFO::OK)
153 return replays;
155 bool newReplays = false;
156 std::vector<u32> copyFromOldCache;
157 // Specifies where the next replay should be kept
158 u32 i = 0;
160 for (const OsPath& directory : directories)
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 if (SDL_QuitRequested())
165 // Don't return, because we want to save our progress
166 break;
168 const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt";
170 bool isNew = true;
171 replayCacheMap::iterator it = fileList.find(directory);
172 if (it != fileList.end())
174 if (compareFiles)
176 if (!FileExists(replayFile))
177 continue;
178 CFileInfo fileInfo;
179 GetFileInfo(replayFile, &fileInfo);
180 if ((u64)fileInfo.MTime() == std::get<1>(it->second) && (off_t)fileInfo.Size() == std::get<2>(it->second))
181 isNew = false;
183 else
184 isNew = false;
187 if (isNew)
189 JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, directory));
190 if (replayData.isNull())
192 if (!FileExists(replayFile))
193 continue;
194 CFileInfo fileInfo;
195 GetFileInfo(replayFile, &fileInfo);
197 Script::CreateObject(
199 &replayData,
200 "directory", directory.string(),
201 "fileMTime", static_cast<double>(fileInfo.MTime()),
202 "fileSize", static_cast<double>(fileInfo.Size()));
204 JS_SetElement(rq.cx, replays, i++, replayData);
205 newReplays = true;
207 else
208 copyFromOldCache.push_back(std::get<0>(it->second));
211 debug_printf(
212 "Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n",
213 (unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i);
215 if (!newReplays && fileList.empty())
216 return replays;
218 // No replay was changed, so just return the cache
219 if (!newReplays && fileList.size() == copyFromOldCache.size())
220 return cachedReplaysObject;
223 // Copy the replays from the old cache that are not deleted
224 if (!copyFromOldCache.empty())
225 for (u32 j : copyFromOldCache)
227 JS::RootedValue replay(rq.cx);
228 JS_GetElement(rq.cx, cachedReplaysObject, j, &replay);
229 JS_SetElement(rq.cx, replays, i++, replay);
232 StoreCacheFile(scriptInterface, replays);
233 return replays;
236 JS::Value VisualReplay::GetReplays(const ScriptInterface& scriptInterface, bool compareFiles)
238 TIMER(L"GetReplays");
240 ScriptRequest rq(scriptInterface);
241 JS::RootedObject replays(rq.cx, ReloadReplayCache(scriptInterface, compareFiles));
242 // Only take entries with data
243 JS::RootedValue replaysWithoutNullEntries(rq.cx);
244 Script::CreateArray(rq, &replaysWithoutNullEntries);
246 u32 replaysLength = 0;
247 JS::GetArrayLength(rq.cx, replays, &replaysLength);
248 for (u32 j = 0, i = 0; j < replaysLength; ++j)
250 JS::RootedValue replay(rq.cx);
251 JS_GetElement(rq.cx, replays, j, &replay);
252 if (Script::HasProperty(rq, replay, "attribs"))
253 Script::SetPropertyInt(rq, replaysWithoutNullEntries, i++, replay);
255 return replaysWithoutNullEntries;
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.
262 * @return The current cursor position or -1 on error.
264 inline off_t goBackToLineBeginning(std::istream* replayStream, const OsPath& fileName, off_t fileSize)
266 int currentPos;
267 char character;
268 for (int characters = 0; characters < 10000; ++characters)
270 currentPos = (int) replayStream->tellg();
272 // Stop when reached the beginning of the file
273 if (currentPos == 0)
274 return currentPos;
276 if (!replayStream->good())
278 LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.string8().c_str());
279 return -1;
282 // Stop when reached newline
283 replayStream->get(character);
284 if (character == '\n')
285 return currentPos;
287 // Otherwise go back one character.
288 // Notice: -1 will set the cursor back to the most recently read character.
289 replayStream->seekg(-2, std::ios_base::cur);
292 LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.string8().c_str());
293 return -1;
297 * Compute game duration in seconds. Assume constant turn length.
298 * Find the last line that starts with "turn" by reading the file backwards.
300 * @return seconds or -1 on error
302 inline int getReplayDuration(std::istream* replayStream, const OsPath& fileName, off_t fileSize)
304 CStr type;
306 // Move one character before the file-end
307 replayStream->seekg(-2, std::ios_base::end);
309 // Infinite loop protection, should never occur.
310 // There should be about 5 lines to read until a turn is found.
311 for (int linesRead = 1; linesRead < 1000; ++linesRead)
313 off_t currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize);
315 // Read error or reached file beginning. No turns exist.
316 if (currentPosition < 1)
317 return -1;
319 if (!replayStream->good())
321 LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.string8().c_str());
322 return -1;
325 // Found last turn, compute duration.
326 if (currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn")
328 u32 turn = 0, turnLength = 0;
329 *replayStream >> turn >> turnLength;
330 return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0
333 // Otherwise move cursor back to the character before the last newline
334 replayStream->seekg(currentPosition - 2, std::ios_base::beg);
337 LOGERROR("Infinite loop when determining replay duration for %s", fileName.string8().c_str());
338 return -1;
341 JS::Value VisualReplay::LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory)
343 // The directory argument must not be constant, otherwise concatenating will fail
344 const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt";
346 if (!FileExists(replayFile))
347 return JS::NullValue();
349 // Get file size and modification date
350 CFileInfo fileInfo;
351 GetFileInfo(replayFile, &fileInfo);
352 const off_t fileSize = fileInfo.Size();
354 if (fileSize == 0)
355 return JS::NullValue();
357 std::ifstream* replayStream = new std::ifstream(OsString(replayFile).c_str());
359 CStr type;
360 if (!(*replayStream >> type).good())
362 LOGERROR("Couldn't open %s.", replayFile.string8().c_str());
363 SAFE_DELETE(replayStream);
364 return JS::NullValue();
367 if (type != "start")
369 LOGWARNING("The replay %s doesn't begin with 'start'!", replayFile.string8().c_str());
370 SAFE_DELETE(replayStream);
371 return JS::NullValue();
374 // Parse header / first line
375 CStr header;
376 std::getline(*replayStream, header);
377 ScriptRequest rq(scriptInterface);
378 JS::RootedValue attribs(rq.cx);
379 if (!Script::ParseJSON(rq, header, &attribs))
381 LOGERROR("Couldn't parse replay header of %s", replayFile.string8().c_str());
382 SAFE_DELETE(replayStream);
383 return JS::NullValue();
386 // Ensure "turn" after header
387 if (!(*replayStream >> type).good() || type != "turn")
389 SAFE_DELETE(replayStream);
390 return JS::NullValue(); // there are no turns at all
393 // Don't process files of rejoined clients
394 u32 turn = 1;
395 *replayStream >> turn;
396 if (turn != 0)
398 SAFE_DELETE(replayStream);
399 return JS::NullValue();
402 int duration = getReplayDuration(replayStream, replayFile, fileSize);
404 SAFE_DELETE(replayStream);
406 // Ensure minimum duration
407 if (duration < minimumReplayDuration)
408 return JS::NullValue();
410 // Return the actual data
411 JS::RootedValue replayData(rq.cx);
413 Script::CreateObject(
415 &replayData,
416 "directory", directory.string(),
417 "fileSize", static_cast<double>(fileSize),
418 "fileMTime", static_cast<double>(fileInfo.MTime()),
419 "duration", duration);
421 Script::SetProperty(rq, replayData, "attribs", attribs);
423 return replayData;
426 bool VisualReplay::DeleteReplay(const OsPath& replayDirectory)
428 if (replayDirectory.empty())
429 return false;
431 const OsPath directory = GetDirectoryPath() / replayDirectory;
432 return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
435 JS::Value VisualReplay::GetReplayAttributes(const ScriptInterface& scriptInterface, const OsPath& directoryName)
437 // Create empty JS object
438 ScriptRequest rq(scriptInterface);
439 JS::RootedValue attribs(rq.cx);
440 Script::CreateObject(rq, &attribs);
442 // Return empty object if file doesn't exist
443 const OsPath replayFile = GetDirectoryPath() / directoryName / L"commands.txt";
444 if (!FileExists(replayFile))
445 return attribs;
447 // Open file
448 std::istream* replayStream = new std::ifstream(OsString(replayFile).c_str());
449 CStr type, line;
450 ENSURE((*replayStream >> type).good() && type == "start");
452 // Read and return first line
453 std::getline(*replayStream, line);
454 Script::ParseJSON(rq, line, &attribs);
455 SAFE_DELETE(replayStream);;
456 return attribs;
459 void VisualReplay::AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName)
461 TIMER(L"AddReplayToCache");
462 ScriptRequest rq(scriptInterface);
464 JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, OsPath(directoryName)));
465 if (replayData.isNull())
466 return;
468 JS::RootedObject cachedReplaysObject(rq.cx);
469 if (!ReadCacheFile(scriptInterface, &cachedReplaysObject))
470 cachedReplaysObject = JS::NewArrayObject(rq.cx, 0);
472 u32 cacheLength = 0;
473 JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength);
474 JS_SetElement(rq.cx, cachedReplaysObject, cacheLength, replayData);
476 StoreCacheFile(scriptInterface, cachedReplaysObject);
479 bool VisualReplay::HasReplayMetadata(const OsPath& directoryName)
481 const OsPath filePath(GetDirectoryPath() / directoryName / L"metadata.json");
483 if (!FileExists(filePath))
484 return false;
486 CFileInfo fileInfo;
487 GetFileInfo(filePath, &fileInfo);
489 return fileInfo.Size() > 0;
492 JS::Value VisualReplay::GetReplayMetadata(const ScriptInterface& scriptInterface, const OsPath& directoryName)
494 if (!HasReplayMetadata(directoryName))
495 return JS::NullValue();
497 ScriptRequest rq(scriptInterface);
498 JS::RootedValue metadata(rq.cx);
500 std::ifstream* stream = new std::ifstream(OsString(GetDirectoryPath() / directoryName / L"metadata.json").c_str());
501 ENSURE(stream->good());
502 CStr line;
503 std::getline(*stream, line);
504 stream->close();
505 SAFE_DELETE(stream);
506 Script::ParseJSON(rq, line, &metadata);
508 return metadata;