forked from mirrors/0ad
a0058c533b
That kind of file can be written in case the partition has no more space left when ending the game. This was SVN commit r18560.
343 lines
10 KiB
C++
343 lines
10 KiB
C++
/* Copyright (C) 2016 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 "gui/GUIManager.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::GetDirectoryName()
|
|
{
|
|
const Paths paths(g_args);
|
|
return OsPath(paths.UserData() / "replays" / engine_version);
|
|
}
|
|
|
|
void VisualReplay::StartVisualReplay(const CStrW& directory)
|
|
{
|
|
ENSURE(!g_NetServer);
|
|
ENSURE(!g_NetClient);
|
|
ENSURE(!g_Game);
|
|
|
|
const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt";
|
|
|
|
if (!FileExists(replayFile))
|
|
return;
|
|
|
|
g_Game = new CGame(false, false);
|
|
g_Game->StartVisualReplay(replayFile.string8());
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
JSAutoRequest rq(cx);
|
|
|
|
u32 i = 0;
|
|
DirectoryNames directories;
|
|
JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
|
|
|
|
if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK)
|
|
for (OsPath& directory : directories)
|
|
{
|
|
if (SDL_QuitRequested())
|
|
return JSVAL_NULL;
|
|
|
|
JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
|
|
if (!replayData.isNull())
|
|
JS_SetElement(cx, replays, i++, replayData);
|
|
}
|
|
return JS::ObjectValue(*replays);
|
|
}
|
|
|
|
/**
|
|
* 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 int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64& 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.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.c_str());
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Compute game duration. 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 CStr& fileName, const u64& 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)
|
|
{
|
|
int 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.c_str());
|
|
return -1;
|
|
}
|
|
|
|
// Found last turn, compute duration.
|
|
if ((u64) 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.c_str());
|
|
return -1;
|
|
}
|
|
|
|
JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory)
|
|
{
|
|
// The directory argument must not be constant, otherwise concatenating will fail
|
|
const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt";
|
|
|
|
if (!FileExists(replayFile))
|
|
return JSVAL_NULL;
|
|
|
|
// Get file size and modification date
|
|
CFileInfo fileInfo;
|
|
GetFileInfo(replayFile, &fileInfo);
|
|
const u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath)
|
|
const u64 fileSize = (u64)fileInfo.Size();
|
|
|
|
if (fileSize == 0)
|
|
return JSVAL_NULL;
|
|
|
|
// Open file
|
|
const CStr fileName = replayFile.string8();
|
|
std::ifstream* replayStream = new std::ifstream(fileName.c_str());
|
|
|
|
// File must begin with "start"
|
|
CStr type;
|
|
if (!(*replayStream >> type).good())
|
|
{
|
|
LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str());
|
|
SAFE_DELETE(replayStream);
|
|
return JSVAL_NULL;
|
|
}
|
|
if (type != "start")
|
|
{
|
|
LOGWARNING("The replay %s is broken!", fileName.c_str());
|
|
SAFE_DELETE(replayStream);
|
|
return JSVAL_NULL;
|
|
}
|
|
|
|
// Parse header / first line
|
|
CStr header;
|
|
std::getline(*replayStream, header);
|
|
JSContext* cx = scriptInterface.GetContext();
|
|
JSAutoRequest rq(cx);
|
|
JS::RootedValue attribs(cx);
|
|
if (!scriptInterface.ParseJSON(header, &attribs))
|
|
{
|
|
LOGERROR("Couldn't parse replay header of %s", fileName.c_str());
|
|
SAFE_DELETE(replayStream);
|
|
return JSVAL_NULL;
|
|
}
|
|
|
|
// Ensure "turn" after header
|
|
if (!(*replayStream >> type).good() || type != "turn")
|
|
{
|
|
SAFE_DELETE(replayStream);
|
|
return JSVAL_NULL; // 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 JSVAL_NULL;
|
|
}
|
|
|
|
int duration = getReplayDuration(replayStream, fileName, fileSize);
|
|
|
|
SAFE_DELETE(replayStream);
|
|
|
|
// Ensure minimum duration
|
|
if (duration < minimumReplayDuration)
|
|
return JSVAL_NULL;
|
|
|
|
// Return the actual data
|
|
JS::RootedValue replayData(cx);
|
|
scriptInterface.Eval("({})", &replayData);
|
|
scriptInterface.SetProperty(replayData, "file", replayFile);
|
|
scriptInterface.SetProperty(replayData, "directory", directory);
|
|
scriptInterface.SetProperty(replayData, "filemod_timestamp", std::to_string(fileTime));
|
|
scriptInterface.SetProperty(replayData, "attribs", attribs);
|
|
scriptInterface.SetProperty(replayData, "duration", duration);
|
|
return replayData;
|
|
}
|
|
|
|
bool VisualReplay::DeleteReplay(const CStrW& replayDirectory)
|
|
{
|
|
if (replayDirectory.empty())
|
|
return false;
|
|
|
|
const OsPath directory = GetDirectoryName() / replayDirectory;
|
|
return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
|
|
}
|
|
|
|
|
|
JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
|
|
{
|
|
// Create empty JS object
|
|
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
|
|
JSAutoRequest rq(cx);
|
|
JS::RootedValue attribs(cx);
|
|
pCxPrivate->pScriptInterface->Eval("({})", &attribs);
|
|
|
|
// Return empty object if file doesn't exist
|
|
const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt";
|
|
if (!FileExists(replayFile))
|
|
return attribs;
|
|
|
|
// Open file
|
|
std::istream* replayStream = new std::ifstream(replayFile.string8().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;
|
|
}
|
|
|
|
// TODO: enhancement: how to save the data if the process is killed? (case SDL_QUIT in main.cpp)
|
|
void VisualReplay::SaveReplayMetadata(const CStrW& data)
|
|
{
|
|
// TODO: enhancement: use JS::HandleValue similar to SaveGame
|
|
if (!g_Game)
|
|
return;
|
|
|
|
// Get the directory of the currently active replay
|
|
const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
|
|
CreateDirectories(fileName.Parent(), 0700);
|
|
|
|
std::ofstream stream (fileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc);
|
|
stream << utf8_from_wstring(data);
|
|
stream.close();
|
|
}
|
|
|
|
bool VisualReplay::HasReplayMetadata(const CStrW& directoryName)
|
|
{
|
|
const OsPath filePath(GetDirectoryName() / 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 CStrW& directoryName)
|
|
{
|
|
if (!HasReplayMetadata(directoryName))
|
|
return JSVAL_NULL;
|
|
|
|
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
|
|
JSAutoRequest rq(cx);
|
|
JS::RootedValue metadata(cx);
|
|
|
|
std::ifstream* stream = new std::ifstream(OsPath(GetDirectoryName() / directoryName / L"metadata.json").string8());
|
|
ENSURE(stream->good());
|
|
CStr line;
|
|
std::getline(*stream, line);
|
|
stream->close();
|
|
SAFE_DELETE(stream);
|
|
pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
|
|
|
|
return metadata;
|
|
}
|