0ad/source/ps/scripting/JSInterface_VFS.cpp
wraitii 1c9efa6fb5 Implement Single-Player campaigns - Barebones tutorial campaign included.
This implements necessary tooling to create a simple SP campaign.
The architecture is intended to be easily extensible in the future.

'Campaign Run' contains the metadata of a campaign, e.g. maps
played/won. It's saved in the user folder under
saves/campaigns/*.0adcampaign
Campaign templates are JSON files in campaigns/

Campaigns can specify which Menu interface they will use. This is
intended to allow more complex layouts/presentation.
For now, a simple list interface is provided. This allows making
campaigns without any fancy art required (and effectively mimics AoE1's
campaign interface).

The behaviour on game end is also intended to be extensible, supporting
things such as carrying over units between scenarios - for now, it
simply records won games.

GameSetup is not available for now - scenarios are triggered with the
settings defined in the map/default settings. Improving on this requires
refactoring the gamesetup further.

The load/save game page has been extended slightly to support
showing/hiding campaign games (campaign gamed are saved under saves/
directly, there is no strong motivation to do otherwise at this point)

Closes #4387

Differential Revision: https://code.wildfiregames.com/D11
This was SVN commit r24979.
2021-03-02 15:43:44 +00:00

289 lines
10 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "JSInterface_VFS.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Filesystem.h"
#include "scriptinterface/ScriptExtraHeaders.h"
#include "scriptinterface/ScriptInterface.h"
#include <sstream>
// Only allow engine compartments to read files they may be concerned about.
#define PathRestriction_GUI {L""}
#define PathRestriction_Simulation {L"simulation/"}
#define PathRestriction_Maps {L"simulation/", L"maps/"}
// shared error handling code
#define JS_CHECK_FILE_ERR(err)\
/* this is liable to happen often, so don't complain */\
if (err == ERR::VFS_FILE_NOT_FOUND)\
{\
return 0; \
}\
/* unknown failure. We output an error message. */\
else if (err < 0)\
LOGERROR("Unknown failure in VFS %i", err );
/* else: success */
// state held across multiple BuildDirEntListCB calls; init by BuildDirEntList.
struct BuildDirEntListState
{
ScriptInterface* pScriptInterface;
JS::PersistentRootedObject filename_array;
int cur_idx;
BuildDirEntListState(ScriptInterface* scriptInterface)
: pScriptInterface(scriptInterface),
filename_array(scriptInterface->GetGeneralJSContext()),
cur_idx(0)
{
ScriptRequest rq(pScriptInterface);
filename_array = JS::NewArrayObject(rq.cx, JS::HandleValueArray::empty());
}
};
// called for each matching directory entry; add its full pathname to array.
static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData)
{
BuildDirEntListState* s = (BuildDirEntListState*)cbData;
ScriptRequest rq(s->pScriptInterface);
JS::RootedObject filenameArrayObj(rq.cx, s->filename_array);
JS::RootedValue val(rq.cx);
ScriptInterface::ToJSVal(rq, &val, CStrW(pathname.string()) );
JS_SetElement(rq.cx, filenameArrayObj, s->cur_idx++, val);
return INFO::OK;
}
// Return an array of pathname strings, one for each matching entry in the
// specified directory.
// filter_string: default "" matches everything; otherwise, see vfs_next_dirent.
// recurse: should subdirectories be included in the search? default false.
JS::Value JSI_VFS::BuildDirEntList(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector<CStrW>& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse)
{
if (!PathRestrictionMet(pCmptPrivate, validPaths, path))
return JS::NullValue();
// convert to const wchar_t*; if there's no filter, pass 0 for speed
// (interpreted as: "accept all files without comparing").
const wchar_t* filter = 0;
if (!filterStr.empty())
filter = filterStr.c_str();
int flags = recurse ? vfs::DIR_RECURSIVE : 0;
// build array in the callback function
BuildDirEntListState state(pCmptPrivate->pScriptInterface);
vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags);
return JS::ObjectValue(*state.filename_array);
}
// Return true iff the file exits
bool JSI_VFS::FileExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector<CStrW>& validPaths, const CStrW& filename)
{
return PathRestrictionMet(pCmptPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK;
}
// Return time [seconds since 1970] of the last modification to the specified file.
double JSI_VFS::GetFileMTime(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (double)fileInfo.MTime();
}
// Return current size of file.
unsigned int JSI_VFS::GetFileSize(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (unsigned int)fileInfo.Size();
}
// Return file contents in a string. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// Decode as UTF-8
ScriptRequest rq(pCmptPrivate->pScriptInterface);
JS::RootedValue ret(rq.cx);
ScriptInterface::ToJSVal(rq, &ret, contents.FromUTF8());
return ret;
}
// Return file contents as an array of lines. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFileLines(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// split into array of strings (one per line)
std::stringstream ss(contents);
const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface;
ScriptRequest rq(scriptInterface);
JS::RootedValue line_array(rq.cx);
ScriptInterface::CreateArray(rq, &line_array);
std::string line;
int cur_line = 0;
while (std::getline(ss, line))
{
// Decode each line as UTF-8
JS::RootedValue val(rq.cx);
ScriptInterface::ToJSVal(rq, &val, CStr(line).FromUTF8());
scriptInterface.SetPropertyInt(line_array, cur_line++, val);
}
return line_array;
}
JS::Value JSI_VFS::ReadJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector<CStrW>& validPaths, const CStrW& filePath)
{
if (!PathRestrictionMet(pCmptPrivate, validPaths, filePath))
return JS::NullValue();
const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface;
ScriptRequest rq(scriptInterface);
JS::RootedValue out(rq.cx);
scriptInterface.ReadJSONFile(filePath, &out);
return out;
}
void JSI_VFS::WriteJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath, JS::HandleValue val1)
{
const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface;
ScriptRequest rq(scriptInterface);
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
JS::RootedValue val(rq.cx, val1);
std::string str(scriptInterface.StringifyJSON(&val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
bool JSI_VFS::DeleteCampaignSave(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& filePath)
{
OsPath realPath;
if (filePath.Left(16) != L"saves/campaigns/" || filePath.Right(12) != L".0adcampaign")
return false;
return VfsFileExists(filePath) &&
g_VFS->GetRealPath(filePath, realPath) == INFO::OK &&
g_VFS->RemoveFile(filePath) == INFO::OK &&
wunlink(realPath) == 0;
}
bool JSI_VFS::PathRestrictionMet(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector<CStrW>& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
if (filePath.find(validPath) == 0)
return true;
CStrW allowedPaths;
for (std::size_t i = 0; i < validPaths.size(); ++i)
{
if (i != 0)
allowedPaths += L", ";
allowedPaths += L"\"" + validPaths[i] + L"\"";
}
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str());
return false;
}
#define VFS_ScriptFunctions(context)\
JS::Value Script_ReadJSONFile_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::ReadJSONFile(pCmptPrivate, PathRestriction_##context, filePath);\
}\
JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\
{\
return JSI_VFS::BuildDirEntList(pCmptPrivate, PathRestriction_##context, path, filterStr, recurse);\
}\
bool Script_FileExists_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::FileExists(pCmptPrivate, PathRestriction_##context, filePath);\
}\
VFS_ScriptFunctions(GUI);
VFS_ScriptFunctions(Simulation);
VFS_ScriptFunctions(Maps);
#undef VFS_ScriptFunctions
void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &Script_ListDirectoryFiles_GUI>("ListDirectoryFiles");
scriptInterface.RegisterFunction<bool, std::wstring, Script_FileExists_GUI>("FileExists");
scriptInterface.RegisterFunction<double, std::wstring, &JSI_VFS::GetFileMTime>("GetFileMTime");
scriptInterface.RegisterFunction<unsigned int, std::wstring, &JSI_VFS::GetFileSize>("GetFileSize");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &JSI_VFS::ReadFile>("ReadFile");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &JSI_VFS::ReadFileLines>("ReadFileLines");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &Script_ReadJSONFile_GUI>("ReadJSONFile");
scriptInterface.RegisterFunction<void, std::wstring, JS::HandleValue, &WriteJSONFile>("WriteJSONFile");
scriptInterface.RegisterFunction<bool, CStrW, &DeleteCampaignSave>("DeleteCampaignSave");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &Script_ListDirectoryFiles_Simulation>("ListDirectoryFiles");
scriptInterface.RegisterFunction<bool, std::wstring, Script_FileExists_Simulation>("FileExists");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &Script_ReadJSONFile_Simulation>("ReadJSONFile");
}
void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &Script_ListDirectoryFiles_Maps>("ListDirectoryFiles");
scriptInterface.RegisterFunction<bool, std::wstring, Script_FileExists_Maps>("FileExists");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &Script_ReadJSONFile_Maps>("ReadJSONFile");
}