Index: ps/trunk/source/lib/sysdep/filesystem.h
===================================================================
--- ps/trunk/source/lib/sysdep/filesystem.h (revision 25906)
+++ ps/trunk/source/lib/sysdep/filesystem.h (revision 25907)
@@ -1,123 +1,116 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* wchar_t versions of POSIX filesystem functions
*/
#ifndef INCLUDED_SYSDEP_FILESYSTEM
#define INCLUDED_SYSDEP_FILESYSTEM
#include "lib/os_path.h"
#include "lib/posix/posix_filesystem.h" // mode_t
//
// dirent.h
//
struct WDIR;
struct wdirent
{
// note: SUSv3 describes this as a "char array" but of unspecified size.
// we declare as a pointer to avoid having to copy the string.
wchar_t* d_name;
};
extern WDIR* wopendir(const OsPath& path);
extern wdirent* wreaddir(WDIR*);
// return status for the file returned by the last successful
// wreaddir call from the given directory stream.
// currently sets st_size, st_mode, and st_mtime; the rest are zeroed.
// non-portable, but considerably faster than stat(). used by dir_ForEachSortedEntry.
extern int wreaddir_stat_np(WDIR*, struct stat*);
extern int wclosedir(WDIR*);
//
// fcntl.h
//
// transfer directly to/from user's buffer.
// treated as a request to enable aio.
#ifndef O_DIRECT // i.e. Windows or OS X
#define O_DIRECT 0x10000000 // (value does not conflict with any current Win32 _O_* flags.)
#endif
// Win32 _wsopen_s does not open files in a manner compatible with waio.
// if its aio_read/aio_write are to be used, waio_open must (also) be called.
// calling both is possible but wasteful and unsafe, since it prevents
// file sharing restrictions, which are the only way to prevent
// exposing previous data as a side effect of waio_Preallocate.
// applications shouldn't mix aio and synchronous I/O anyway, so we
// want wopen to call either waio_open or _wsopen_s.
// since waio requires callers to respect the FILE_FLAG_NO_BUFFERING
// restrictions (sector alignment), and IRIX/BSD/Linux O_DIRECT imposes
// similar semantics, we treat that flag as a request to enable aio.
extern int wopen(const OsPath& pathname, int oflag);
extern int wopen(const OsPath& pathname, int oflag, mode_t mode);
extern int wclose(int fd);
//
// unistd.h
//
// waio requires offsets and sizes to be multiples of the sector size.
// to allow arbitrarily sized files, we truncate them after I/O.
// however, ftruncate cannot be used since it is also subject to the
// sector-alignment requirement. instead, the file must be closed and
// this function called.
LIB_API int wtruncate(const OsPath& pathname, off_t length);
LIB_API int wunlink(const OsPath& pathname);
LIB_API int wrmdir(const OsPath& path);
//
-// stdio.h
-//
-
-LIB_API int wrename(const OsPath& pathnameOld, const OsPath& pathnameNew);
-
-
-//
// stdlib.h
//
LIB_API OsPath wrealpath(const OsPath& pathname);
//
// sys/stat.h
//
LIB_API int wstat(const OsPath& pathname, struct stat* buf);
LIB_API int wmkdir(const OsPath& path, mode_t mode);
#endif // #ifndef INCLUDED_SYSDEP_FILESYSTEM
Index: ps/trunk/source/lib/sysdep/os/win/wposix/wfilesystem.cpp
===================================================================
--- ps/trunk/source/lib/sysdep/os/win/wposix/wfilesystem.cpp (revision 25906)
+++ ps/trunk/source/lib/sysdep/os/win/wposix/wfilesystem.cpp (revision 25907)
@@ -1,355 +1,349 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "precompiled.h"
#include "lib/sysdep/filesystem.h"
#include "lib/sysdep/cpu.h" // cpu_CAS
#include "lib/sysdep/os/win/wutil.h" // StatusFromWin
#include "lib/sysdep/os/win/wposix/waio.h" // waio_reopen
#include "lib/sysdep/os/win/wposix/wtime_internal.h" // wtime_utc_filetime_to_time_t
#include "lib/sysdep/os/win/wposix/crt_posix.h" // _close, _lseeki64 etc.
//-----------------------------------------------------------------------------
// WDIR suballocator
//-----------------------------------------------------------------------------
// most applications only need a single WDIR at a time. we avoid expensive
// heap allocations by reusing a single static instance. if it is already
// in use, we allocate further instances dynamically.
// NB: this is thread-safe due to CAS.
struct WDIR // POD
{
HANDLE hFind;
WIN32_FIND_DATAW findData; // indeterminate if hFind == INVALID_HANDLE_VALUE
// wreaddir will return the address of this member.
// (must be stored in WDIR to allow multiple independent
// wopendir/wreaddir sequences).
struct wdirent ent;
// used by wreaddir to skip the first FindNextFileW. (a counter is
// easy to test/update and also provides useful information.)
size_t numCalls;
};
static WDIR wdir_storage;
static volatile intptr_t wdir_in_use;
static inline WDIR* wdir_alloc()
{
if(cpu_CAS(&wdir_in_use, 0, 1)) // gained ownership
return &wdir_storage;
// already in use (rare) - allocate from heap
return new WDIR;
}
static inline void wdir_free(WDIR* d)
{
if(d == &wdir_storage)
{
const bool ok = cpu_CAS(&wdir_in_use, 1, 0); // relinquish ownership
ENSURE(ok); // ensure it wasn't double-freed
}
else // allocated from heap
delete d;
}
//-----------------------------------------------------------------------------
// dirent.h
//-----------------------------------------------------------------------------
static bool IsValidDirectory(const OsPath& path)
{
const DWORD fileAttributes = GetFileAttributesW(OsString(path).c_str());
// path not found
if(fileAttributes == INVALID_FILE_ATTRIBUTES)
return false;
// not a directory
if((fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
return false;
// NB: no longer reject hidden or system attributes since
// wsnd's add_oal_dlls_in_dir opens the Windows system directory,
// which sometimes has these attributes set.
return true;
}
WDIR* wopendir(const OsPath& path)
{
WinScopedPreserveLastError s;
if(!IsValidDirectory(path))
{
errno = ENOENT;
return 0;
}
WDIR* d = wdir_alloc();
d->numCalls = 0;
// NB: "c:\\path" only returns information about that directory;
// trailing slashes aren't allowed. append "\\*" to retrieve its entries.
OsPath searchPath = path/"*";
// (we don't defer FindFirstFileW until wreaddir because callers
// expect us to return 0 if directory reading will/did fail.)
d->hFind = FindFirstFileW(OsString(searchPath).c_str(), &d->findData);
if(d->hFind != INVALID_HANDLE_VALUE)
return d; // success
if(GetLastError() == ERROR_NO_MORE_FILES)
return d; // success, but directory is empty
Status status = StatusFromWin();
// release the WDIR allocated above (this is preferable to
// always copying the large WDIR or findData from a temporary)
wdir_free(d);
WARN_IF_ERR(status);
errno = ErrnoFromStatus(status);
return 0;
}
struct wdirent* wreaddir(WDIR* d)
{
// directory is empty and d->findData is indeterminate
if(d->hFind == INVALID_HANDLE_VALUE)
return 0;
WinScopedPreserveLastError s;
// until end of directory or a valid entry was found:
for(;;)
{
if(d->numCalls++ != 0) // (skip first call to FindNextFileW - see wopendir)
{
if(!FindNextFileW(d->hFind, &d->findData))
{
if(GetLastError() == ERROR_NO_MORE_FILES)
SetLastError(0);
else // unexpected error
DEBUG_WARN_ERR(StatusFromWin());
return 0; // end of directory or error
}
}
// only accept non-hidden and non-system entries - otherwise,
// callers might encounter errors when attempting to open them.
if((d->findData.dwFileAttributes & (FILE_ATTRIBUTE_HIDDEN|FILE_ATTRIBUTE_SYSTEM)) == 0)
{
d->ent.d_name = d->findData.cFileName; // (NB: d_name is a pointer)
return &d->ent;
}
}
}
int wreaddir_stat_np(WDIR* d, struct stat* s)
{
// NTFS stores UTC but FAT stores local times, which are incorrectly
// translated to UTC based on the _current_ DST settings. we no longer
// bother checking the filesystem, since that's either unreliable or
// expensive. timestamps may therefore be off after a DST transition,
// which means our cached files would be regenerated.
FILETIME* filetime = &d->findData.ftLastWriteTime;
memset(s, 0, sizeof(*s));
s->st_size = (off_t)u64_from_u32(d->findData.nFileSizeHigh, d->findData.nFileSizeLow);
s->st_mode = (unsigned short)((d->findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)? S_IFDIR : S_IFREG);
s->st_mtime = wtime_utc_filetime_to_time_t(filetime);
return 0;
}
int wclosedir(WDIR* d)
{
FindClose(d->hFind);
wdir_free(d);
return 0;
}
//-----------------------------------------------------------------------------
// fcntl.h
//-----------------------------------------------------------------------------
int wopen(const OsPath& pathname, int oflag)
{
ENSURE(!(oflag & O_CREAT)); // must specify mode_arg if O_CREAT
return wopen(OsString(pathname).c_str(), oflag, _S_IREAD|_S_IWRITE);
}
int wopen(const OsPath& pathname, int oflag, mode_t mode)
{
if(oflag & O_DIRECT)
{
Status ret = waio_open(pathname, oflag);
if(ret < 0)
{
errno = ErrnoFromStatus(ret);
return -1;
}
return (int)ret; // file descriptor
}
else
{
WinScopedPreserveLastError s; // _wsopen_s's CreateFileW
int fd;
oflag |= _O_BINARY;
if(oflag & O_WRONLY)
oflag |= O_CREAT|O_TRUNC;
// NB: _wsopen_s ignores mode unless oflag & O_CREAT
errno_t ret = _wsopen_s(&fd, OsString(pathname).c_str(), oflag, _SH_DENYRD, mode);
if(ret != 0)
{
errno = ret;
return -1; // NOWARN
}
return fd;
}
}
int wclose(int fd)
{
ENSURE(fd >= 3); // not invalid nor stdin/out/err
if(waio_close(fd) != 0)
return _close(fd);
return 0;
}
//-----------------------------------------------------------------------------
// unistd.h
//-----------------------------------------------------------------------------
// we don't want to #define read to _read, since that's a fairly common
// identifier. therefore, translate from MS CRT names via thunk functions.
// efficiency is less important, and the overhead could be optimized away.
int read(int fd, void* buf, size_t nbytes)
{
return _read(fd, buf, (int)nbytes);
}
int write(int fd, void* buf, size_t nbytes)
{
return _write(fd, buf, (int)nbytes);
}
off_t lseek(int fd, off_t ofs, int whence)
{
return _lseeki64(fd, ofs, whence);
}
int wtruncate(const OsPath& pathname, off_t length)
{
// (re-open the file to avoid the FILE_FLAG_NO_BUFFERING
// sector-alignment restriction)
HANDLE hFile = CreateFileW(OsString(pathname).c_str(), GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
ENSURE(hFile != INVALID_HANDLE_VALUE);
LARGE_INTEGER ofs; ofs.QuadPart = length;
WARN_IF_FALSE(SetFilePointerEx(hFile, ofs, 0, FILE_BEGIN));
WARN_IF_FALSE(SetEndOfFile(hFile));
WARN_IF_FALSE(CloseHandle(hFile));
return 0;
}
int wunlink(const OsPath& pathname)
{
return _wunlink(OsString(pathname).c_str());
}
int wrmdir(const OsPath& path)
{
return _wrmdir(OsString(path).c_str());
}
-int wrename(const OsPath& pathnameOld, const OsPath& pathnameNew)
-{
- return _wrename(OsString(pathnameOld).c_str(), OsString(pathnameNew).c_str());
-}
-
-
OsPath wrealpath(const OsPath& pathname)
{
wchar_t resolved[PATH_MAX];
if(!GetFullPathNameW(OsString(pathname).c_str(), PATH_MAX, resolved, 0))
return OsPath();
return resolved;
}
static int ErrnoFromCreateDirectory()
{
switch(GetLastError())
{
case ERROR_ALREADY_EXISTS:
return EEXIST;
case ERROR_PATH_NOT_FOUND:
return ENOENT;
case ERROR_ACCESS_DENIED:
return EACCES;
case ERROR_WRITE_PROTECT:
return EROFS;
case ERROR_DIRECTORY:
return ENOTDIR;
default:
return 0;
}
}
int wmkdir(const OsPath& path, mode_t UNUSED(mode))
{
if(!CreateDirectoryW(OsString(path).c_str(), (LPSECURITY_ATTRIBUTES)NULL))
{
errno = ErrnoFromCreateDirectory();
return -1;
}
return 0;
}
int wstat(const OsPath& pathname, struct stat* buf)
{
return _wstat64(OsString(pathname).c_str(), buf);
}
Index: ps/trunk/source/ps/VisualReplay.cpp
===================================================================
--- ps/trunk/source/ps/VisualReplay.cpp (revision 25906)
+++ ps/trunk/source/ps/VisualReplay.cpp (revision 25907)
@@ -1,508 +1,509 @@
/* Copyright (C) 2021 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 .
*/
#include "precompiled.h"
#include "VisualReplay.h"
#include "graphics/GameView.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lib/allocators/shared_ptr.h"
+#include "lib/file/file_system.h"
#include "lib/external_libraries/libsdl.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/Paths.h"
#include "ps/Mod.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "scriptinterface/JSON.h"
#include
/**
* Filter too short replays (value in seconds).
*/
const u8 minimumReplayDuration = 3;
OsPath VisualReplay::GetDirectoryPath()
{
return Paths(g_CmdLineArgs).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(cacheStream)), std::istreambuf_iterator());
cacheStream.close();
ScriptRequest rq(scriptInterface);
JS::RootedValue cachedReplays(rq.cx);
if (Script::ParseJSON(rq, 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)
{
ScriptRequest rq(scriptInterface);
JS::RootedValue replaysRooted(rq.cx, JS::ObjectValue(*replays));
std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc);
cacheStream << Script::StringifyJSON(rq, &replaysRooted);
cacheStream.close();
wunlink(GetCacheFilePath());
- if (wrename(GetTempCacheFilePath(), GetCacheFilePath()))
+ if (RenameFile(GetTempCacheFilePath(), GetCacheFilePath()))
LOGERROR("Could not store the replay cache");
}
JS::HandleObject VisualReplay::ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles)
{
TIMER(L"ReloadReplayCache");
ScriptRequest rq(scriptInterface);
// Maps the filename onto the index, mtime and size
using replayCacheMap = std::map>;
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;
double fileMtime;
Script::GetProperty(rq, replay, "directory", fileName);
Script::GetProperty(rq, replay, "fileSize", fileSize);
Script::GetProperty(rq, replay, "fileMTime", fileMtime);
fileList[fileName] = std::make_tuple(j, fileMtime, 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 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 ((u64)fileInfo.MTime() == std::get<1>(it->second) && (off_t)fileInfo.Size() == std::get<2>(it->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);
Script::CreateObject(
rq,
&replayData,
"directory", directory.string(),
"fileMTime", static_cast(fileInfo.MTime()),
"fileSize", static_cast(fileInfo.Size()));
}
JS_SetElement(rq.cx, replays, i++, replayData);
newReplays = true;
}
else
copyFromOldCache.push_back(std::get<0>(it->second));
}
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");
ScriptRequest rq(scriptInterface);
JS::RootedObject replays(rq.cx, ReloadReplayCache(scriptInterface, compareFiles));
// Only take entries with data
JS::RootedValue replaysWithoutNullEntries(rq.cx);
Script::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 (Script::HasProperty(rq, replay, "attribs"))
Script::SetPropertyInt(rq, 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);
ScriptRequest rq(scriptInterface);
JS::RootedValue attribs(rq.cx);
if (!Script::ParseJSON(rq, 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);
Script::CreateObject(
rq,
&replayData,
"directory", directory.string(),
"fileSize", static_cast(fileSize),
"fileMTime", static_cast(fileInfo.MTime()),
"duration", duration);
Script::SetProperty(rq, 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(const ScriptInterface& scriptInterface, const OsPath& directoryName)
{
// Create empty JS object
ScriptRequest rq(scriptInterface);
JS::RootedValue attribs(rq.cx);
Script::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);
Script::ParseJSON(rq, line, &attribs);
SAFE_DELETE(replayStream);;
return attribs;
}
void VisualReplay::AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName)
{
TIMER(L"AddReplayToCache");
ScriptRequest 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(const ScriptInterface& scriptInterface, const OsPath& directoryName)
{
if (!HasReplayMetadata(directoryName))
return JS::NullValue();
ScriptRequest rq(scriptInterface);
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);
Script::ParseJSON(rq, line, &metadata);
return metadata;
}