Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/source/ps/VisualReplay.cpp
Show All 34 Lines | |||||
#include "ps/Util.h" | #include "ps/Util.h" | ||||
#include "scriptinterface/ScriptInterface.h" | #include "scriptinterface/ScriptInterface.h" | ||||
/** | /** | ||||
* Filter too short replays (value in seconds). | * Filter too short replays (value in seconds). | ||||
*/ | */ | ||||
const u8 minimumReplayDuration = 3; | 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() | OsPath VisualReplay::GetDirectoryName() | ||||
{ | { | ||||
const Paths paths(g_args); | const Paths paths(g_args); | ||||
return OsPath(paths.UserData() / "replays" / engine_version); | return OsPath(paths.UserData() / "replays" / engine_version); | ||||
} | } | ||||
void VisualReplay::StartVisualReplay(const CStrW& directory) | void VisualReplay::StartVisualReplay(const CStrW& directory) | ||||
{ | { | ||||
ENSURE(!g_NetServer); | ENSURE(!g_NetServer); | ||||
ENSURE(!g_NetClient); | ENSURE(!g_NetClient); | ||||
ENSURE(!g_Game); | ENSURE(!g_Game); | ||||
const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt"; | const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt"; | ||||
if (!FileExists(replayFile)) | if (!FileExists(replayFile)) | ||||
return; | return; | ||||
g_Game = new CGame(false, false); | g_Game = new CGame(false, false); | ||||
g_Game->StartVisualReplay(replayFile.string8()); | g_Game->StartVisualReplay(replayFile.string8()); | ||||
} | } | ||||
/** | bool VisualReplay::ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject) | ||||
* Load all replays found in the directory. | |||||
* | |||||
* Since files are spread across the harddisk, | |||||
* loading hundreds of them can consume a lot of time. | |||||
*/ | |||||
JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface) | |||||
{ | { | ||||
TIMER(L"GetReplays"); | |||||
JSContext* cx = scriptInterface.GetContext(); | JSContext* cx = scriptInterface.GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
u32 i = 0; | if (!FileExists(cacheFileName)) | ||||
DirectoryNames directories; | return false; | ||||
std::ifstream cacheStream(cacheFileName.string8().c_str()); | |||||
CStr cacheStr((std::istreambuf_iterator<char>(cacheStream)), std::istreambuf_iterator<char>()); | |||||
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<CStr, std::pair<u32, off_t>> 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)); | JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0)); | ||||
DirectoryNames directories; | |||||
if (GetDirectoryEntries(GetDirectoryName(), nullptr, &directories) != INFO::OK) | |||||
return replays; | |||||
bool newReplays = false; | |||||
std::vector<u32> copyFromOldCache; | |||||
// Specifies where the next replay should be kept | |||||
u32 i = 0; | |||||
if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK) | for (const OsPath& directory : directories) | ||||
for (OsPath& directory : directories) | |||||
{ | { | ||||
if (SDL_QuitRequested()) | if (SDL_QuitRequested()) | ||||
return JSVAL_NULL; | // We want to save our progress in searching through the replays | ||||
break; | |||||
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); | |||||
if (fileInfo.Size() == it->second.second) | |||||
isNew = false; | |||||
} | |||||
else | |||||
isNew = false; | |||||
} | |||||
if (isNew) | |||||
{ | |||||
JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory)); | JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory)); | ||||
if (!replayData.isNull()) | if (replayData.isNull()) | ||||
{ | |||||
CFileInfo fileInfo; | |||||
GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &fileInfo); | |||||
scriptInterface.Eval("({})", &replayData); | |||||
scriptInterface.SetProperty(replayData, "directory", directory); | |||||
scriptInterface.SetProperty(replayData, "fileSize", (double)fileInfo.Size()); | |||||
} | |||||
JS_SetElement(cx, replays, i++, replayData); | JS_SetElement(cx, replays, i++, replayData); | ||||
newReplays = true; | |||||
} | } | ||||
return JS::ObjectValue(*replays); | 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. | * 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. | * Either way the cursor points to the beginning of a newline. | ||||
* | * | ||||
* @return The current cursor position or -1 on error. | * @return The current cursor position or -1 on error. | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 69 Lines • ▼ Show 20 Lines | for (int linesRead = 1; linesRead < 1000; ++linesRead) | ||||
// Otherwise move cursor back to the character before the last newline | // Otherwise move cursor back to the character before the last newline | ||||
replayStream->seekg(currentPosition - 2, std::ios_base::beg); | replayStream->seekg(currentPosition - 2, std::ios_base::beg); | ||||
} | } | ||||
LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str()); | LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str()); | ||||
return -1; | return -1; | ||||
} | } | ||||
JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory) | JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory) | ||||
{ | { | ||||
// The directory argument must not be constant, otherwise concatenating will fail | // The directory argument must not be constant, otherwise concatenating will fail | ||||
const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; | const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; | ||||
if (!FileExists(replayFile)) | if (!FileExists(replayFile)) | ||||
return JSVAL_NULL; | return JSVAL_NULL; | ||||
// Get file size and modification date | // Get file size and modification date | ||||
▲ Show 20 Lines • Show All 60 Lines • ▼ Show 20 Lines | JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory) | ||||
if (duration < minimumReplayDuration) | if (duration < minimumReplayDuration) | ||||
return JSVAL_NULL; | return JSVAL_NULL; | ||||
// Return the actual data | // Return the actual data | ||||
JS::RootedValue replayData(cx); | JS::RootedValue replayData(cx); | ||||
scriptInterface.Eval("({})", &replayData); | scriptInterface.Eval("({})", &replayData); | ||||
scriptInterface.SetProperty(replayData, "file", replayFile); | scriptInterface.SetProperty(replayData, "file", replayFile); | ||||
scriptInterface.SetProperty(replayData, "directory", directory); | scriptInterface.SetProperty(replayData, "directory", directory); | ||||
scriptInterface.SetProperty(replayData, "fileSize", (double)fileSize); | |||||
scriptInterface.SetProperty(replayData, "attribs", attribs); | scriptInterface.SetProperty(replayData, "attribs", attribs); | ||||
scriptInterface.SetProperty(replayData, "duration", duration); | scriptInterface.SetProperty(replayData, "duration", duration); | ||||
return replayData; | return replayData; | ||||
} | } | ||||
bool VisualReplay::DeleteReplay(const CStrW& replayDirectory) | bool VisualReplay::DeleteReplay(const CStrW& replayDirectory) | ||||
{ | { | ||||
if (replayDirectory.empty()) | if (replayDirectory.empty()) | ||||
return false; | return false; | ||||
const OsPath directory = GetDirectoryName() / replayDirectory; | const OsPath directory = GetDirectoryName() / replayDirectory; | ||||
return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; | return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; | ||||
} | } | ||||
JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) | JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) | ||||
{ | { | ||||
// Create empty JS object | // Create empty JS object | ||||
JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); | JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
JS::RootedValue attribs(cx); | JS::RootedValue attribs(cx); | ||||
pCxPrivate->pScriptInterface->Eval("({})", &attribs); | pCxPrivate->pScriptInterface->Eval("({})", &attribs); | ||||
Show All 9 Lines | JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) | ||||
// Read and return first line | // Read and return first line | ||||
std::getline(*replayStream, line); | std::getline(*replayStream, line); | ||||
pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); | pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); | ||||
SAFE_DELETE(replayStream);; | SAFE_DELETE(replayStream);; | ||||
return attribs; | 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) | void VisualReplay::SaveReplayMetadata(ScriptInterface* scriptInterface) | ||||
{ | { | ||||
JSContext* cx = scriptInterface->GetContext(); | JSContext* cx = scriptInterface->GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
JS::RootedValue metadata(cx); | JS::RootedValue metadata(cx); | ||||
JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | ||||
▲ Show 20 Lines • Show All 48 Lines • Show Last 20 Lines |
Wildfire Games · Phabricator