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"
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"
32 #include "ps/GameSetup/CmdLineArgs.h"
33 #include "ps/GameSetup/Paths.h"
35 #include "ps/Pyrogenesis.h"
36 #include "ps/Replay.h"
38 #include "scriptinterface/JSON.h"
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
)
68 const OsPath replayFile
= VisualReplay::GetDirectoryPath() / directory
/ L
"commands.txt";
70 if (!FileExists(replayFile
))
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()))
82 std::ifstream
cacheStream(OsString(GetCacheFilePath()).c_str());
83 CStr
cacheStr((std::istreambuf_iterator
<char>(cacheStream
)), std::istreambuf_iterator
<char>());
86 ScriptRequest
rq(scriptInterface
);
88 JS::RootedValue
cachedReplays(rq
.cx
);
89 if (Script::ParseJSON(rq
, cacheStr
, &cachedReplays
))
91 cachedReplaysObject
.set(&cachedReplays
.toObject());
93 if (JS::IsArrayObject(rq
.cx
, cachedReplaysObject
, &isArray
) && isArray
)
97 LOGWARNING("The replay cache file is corrupted, it will be deleted");
98 wunlink(GetCacheFilePath());
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
);
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
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
);
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
)
155 bool newReplays
= false;
156 std::vector
<u32
> copyFromOldCache
;
157 // Specifies where the next replay should be kept
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
168 const OsPath replayFile
= GetDirectoryPath() / directory
/ L
"commands.txt";
171 replayCacheMap::iterator it
= fileList
.find(directory
);
172 if (it
!= fileList
.end())
176 if (!FileExists(replayFile
))
179 GetFileInfo(replayFile
, &fileInfo
);
180 if ((u64
)fileInfo
.MTime() == std::get
<1>(it
->second
) && (off_t
)fileInfo
.Size() == std::get
<2>(it
->second
))
189 JS::RootedValue
replayData(rq
.cx
, LoadReplayData(scriptInterface
, directory
));
190 if (replayData
.isNull())
192 if (!FileExists(replayFile
))
195 GetFileInfo(replayFile
, &fileInfo
);
197 Script::CreateObject(
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
);
208 copyFromOldCache
.push_back(std::get
<0>(it
->second
));
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())
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
);
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
)
268 for (int characters
= 0; characters
< 10000; ++characters
)
270 currentPos
= (int) replayStream
->tellg();
272 // Stop when reached the beginning of the file
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());
282 // Stop when reached newline
283 replayStream
->get(character
);
284 if (character
== '\n')
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());
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
)
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)
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());
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());
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
351 GetFileInfo(replayFile
, &fileInfo
);
352 const off_t fileSize
= fileInfo
.Size();
355 return JS::NullValue();
357 std::ifstream
* replayStream
= new std::ifstream(OsString(replayFile
).c_str());
360 if (!(*replayStream
>> type
).good())
362 LOGERROR("Couldn't open %s.", replayFile
.string8().c_str());
363 SAFE_DELETE(replayStream
);
364 return JS::NullValue();
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
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
395 *replayStream
>> turn
;
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(
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
);
426 bool VisualReplay::DeleteReplay(const OsPath
& replayDirectory
)
428 if (replayDirectory
.empty())
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
))
448 std::istream
* replayStream
= new std::ifstream(OsString(replayFile
).c_str());
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
);;
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())
468 JS::RootedObject
cachedReplaysObject(rq
.cx
);
469 if (!ReadCacheFile(scriptInterface
, &cachedReplaysObject
))
470 cachedReplaysObject
= JS::NewArrayObject(rq
.cx
, 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
))
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());
503 std::getline(*stream
, line
);
506 Script::ParseJSON(rq
, line
, &metadata
);