0ad/source/ps/VisualReplay.cpp
wraitii ee0d204bf6 Wrap JSAutoRequest and replace usage of JSContext* with the wrapper.
JSAutoRequest is required before calling into most JSAPI methods, for GC
reasons.
Calling it is required and fragile as one must not forget.
Further, SM52 and later make manipulating JSContext* dangerous as that
can cross Compartment(Realm in SM68) barriers (and ScriptInterface now
matches a Compartment).

The solution to both problems is to avoid using JSContext* in 0 A.D.
itself. To achieve this, a Request class is introduced, and must be used
to access a JSContext* from a scriptInterface. Further, Request is
passed to other ScriptInterface functions isntead of JSContext*, making
it obvious that the caller has already called it, reducing errors and
redundant JSAutoRequest calls.
Only JSNative functions now get a naked JSContext* without protection,
but the likelihood of forgetting a request is lower since many
ScriptInterface functions now expect it.

JSContext* is directly passed to JSAPI functions only.

Part of the SM52 migration, stage: SM45 compatible

Based on a patch by: Itms
Tested By: Freagarach
Refs #4893

Differential Revision: https://code.wildfiregames.com/D3088
This was SVN commit r24176.
2020-11-13 13:18:22 +00:00

500 lines
14 KiB
C++

/* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "VisualReplay.h"
#include "graphics/GameView.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;
OsPath VisualReplay::GetDirectoryPath()
{
return Paths(g_args).UserData() / "replays" / engine_version;
}
OsPath VisualReplay::GetCacheFilePath()
{
return GetDirectoryPath() / L"replayCache.json";
}
OsPath VisualReplay::GetTempCacheFilePath()
{
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::GetDirectoryPath() / directory / L"commands.txt";
if (!FileExists(replayFile))
return false;
g_Game = new CGame(false);
return g_Game->StartVisualReplay(replayFile);
}
bool VisualReplay::ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject)
{
if (!FileExists(GetCacheFilePath()))
return false;
std::ifstream cacheStream(OsString(GetCacheFilePath()).c_str());
CStr cacheStr((std::istreambuf_iterator<char>(cacheStream)), std::istreambuf_iterator<char>());
cacheStream.close();
ScriptInterface::Request rq(scriptInterface);
JS::RootedValue cachedReplays(rq.cx);
if (scriptInterface.ParseJSON(cacheStr, &cachedReplays))
{
cachedReplaysObject.set(&cachedReplays.toObject());
bool isArray;
if (JS_IsArrayObject(rq.cx, cachedReplaysObject, &isArray) && isArray)
return true;
}
LOGWARNING("The replay cache file is corrupted, it will be deleted");
wunlink(GetCacheFilePath());
return false;
}
void VisualReplay::StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays)
{
ScriptInterface::Request rq(scriptInterface);
JS::RootedValue replaysRooted(rq.cx, JS::ObjectValue(*replays));
std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc);
cacheStream << scriptInterface.StringifyJSON(&replaysRooted);
cacheStream.close();
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");
ScriptInterface::Request rq(scriptInterface);
// Maps the filename onto the index and size
typedef std::map<OsPath, std::pair<u32, off_t>> replayCacheMap;
replayCacheMap fileList;
JS::RootedObject cachedReplaysObject(rq.cx);
if (ReadCacheFile(scriptInterface, &cachedReplaysObject))
{
// Create list of files included in the cache
u32 cacheLength = 0;
JS_GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength);
for (u32 j = 0; j < cacheLength; ++j)
{
JS::RootedValue replay(rq.cx);
JS_GetElement(rq.cx, cachedReplaysObject, j, &replay);
JS::RootedValue file(rq.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(rq.cx, JS_NewArrayObject(rq.cx, 0));
DirectoryNames directories;
if (GetDirectoryEntries(GetDirectoryPath(), nullptr, &directories) != INFO::OK)
return replays;
bool newReplays = false;
std::vector<u32> 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 = 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(rq.cx, LoadReplayData(scriptInterface, directory));
if (replayData.isNull())
{
if (!FileExists(replayFile))
continue;
CFileInfo fileInfo;
GetFileInfo(replayFile, &fileInfo);
ScriptInterface::CreateObject(
rq,
&replayData,
"directory", directory.string(),
"fileSize", static_cast<double>(fileInfo.Size()));
}
JS_SetElement(rq.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(rq.cx);
JS_GetElement(rq.cx, cachedReplaysObject, j, &replay);
JS_SetElement(rq.cx, replays, i++, replay);
}
}
StoreCacheFile(scriptInterface, replays);
return replays;
}
JS::Value VisualReplay::GetReplays(const ScriptInterface& scriptInterface, bool compareFiles)
{
TIMER(L"GetReplays");
ScriptInterface::Request rq(scriptInterface);
JS::RootedObject replays(rq.cx, ReloadReplayCache(scriptInterface, compareFiles));
// Only take entries with data
JS::RootedValue replaysWithoutNullEntries(rq.cx);
ScriptInterface::CreateArray(rq, &replaysWithoutNullEntries);
u32 replaysLength = 0;
JS_GetArrayLength(rq.cx, replays, &replaysLength);
for (u32 j = 0, i = 0; j < replaysLength; ++j)
{
JS::RootedValue replay(rq.cx);
JS_GetElement(rq.cx, replays, j, &replay);
if (scriptInterface.HasProperty(replay, "attribs"))
scriptInterface.SetPropertyInt(replaysWithoutNullEntries, i++, replay);
}
return 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 = 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);
ScriptInterface::Request rq(scriptInterface);
JS::RootedValue attribs(rq.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(rq.cx);
ScriptInterface::CreateObject(
rq,
&replayData,
"directory", directory.string(),
"fileSize", static_cast<double>(fileSize),
"duration", duration);
scriptInterface.SetProperty(replayData, "attribs", attribs);
return replayData;
}
bool VisualReplay::DeleteReplay(const OsPath& replayDirectory)
{
if (replayDirectory.empty())
return false;
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
ScriptInterface::Request rq(pCxPrivate);
JS::RootedValue attribs(rq.cx);
ScriptInterface::CreateObject(rq, &attribs);
// Return empty object if file doesn't exist
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");
ScriptInterface::Request rq(scriptInterface);
JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, OsPath(directoryName)));
if (replayData.isNull())
return;
JS::RootedObject cachedReplaysObject(rq.cx);
if (!ReadCacheFile(scriptInterface, &cachedReplaysObject))
cachedReplaysObject = JS_NewArrayObject(rq.cx, 0);
u32 cacheLength = 0;
JS_GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength);
JS_SetElement(rq.cx, cachedReplaysObject, cacheLength, replayData);
StoreCacheFile(scriptInterface, cachedReplaysObject);
}
bool VisualReplay::HasReplayMetadata(const OsPath& directoryName)
{
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();
ScriptInterface::Request rq(pCxPrivate);
JS::RootedValue metadata(rq.cx);
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;
}