1
0
forked from 0ad/0ad

Add a mod installer, fixes #4027.

pyrogenesis can now take a zip file (rename it to .pyromod for direct
file association following 943a61e4ea) and install it. It then starts
the mod selector.

Patch by vladislavbelov, with contributions from Imarok, elexis and
myself.
Differential Revision: https://code.wildfiregames.com/D1142
This was SVN commit r21726.
This commit is contained in:
Nicolas Auvray 2018-04-15 01:46:28 +00:00
parent 3eaaddf3ef
commit 404e1a9a4a
13 changed files with 353 additions and 36 deletions

View File

@ -48,17 +48,33 @@ var g_Mods = {};
var g_ModsEnabled = [];
var g_ModsDisabled = [];
/**
* Name of the mods installed by the ModInstaller.
*/
var g_InstalledMods;
var g_ColorNoModSelected = "255 255 100";
var g_ColorDependenciesMet = "100 255 100";
var g_ColorDependenciesNotMet = "255 100 100";
function init(data)
function init(data, hotloadData)
{
g_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || [];
initMods();
initGUIButtons(data);
}
function initMods()
{
loadMods();
loadEnabledMods();
validateMods();
initGUIFilters();
initGUIButtons(data);
}
function getHotloadData()
{
return { "installedMods": g_InstalledMods };
}
function loadMods()
@ -129,7 +145,7 @@ function displayModList(listObjectName, folders)
folders = folders.filter(filterMod);
listObject.list_name = folders.map(folder => g_Mods[folder].name);
listObject.list_name = folders.map(folder => g_Mods[folder].name).map(name => g_InstalledMods.indexOf(name) == -1 ? name : coloredText(name, "green"));
listObject.list_folder = folders;
listObject.list_label = folders.map(folder => g_Mods[folder].label);
listObject.list_url = folders.map(folder => g_Mods[folder].url || "");

View File

@ -51,6 +51,9 @@ Configuration:
-xres=N set screen X resolution to 'N'
-yres=N set screen Y resolution to 'N'
Installing mods:
PATHS install mods located at PATHS. For instance: "./pyrogenesis mod1.pyromod mod2.zip"
Advanced / diagnostic:
-version print the version of the engine and exit
-dumpSchema creates a file entity.rng in the working directory, containing

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -191,3 +191,25 @@ Status DeleteDirectory(const OsPath& path)
return INFO::OK;
}
Status CopyFile(const OsPath& path, const OsPath& newPath, bool override_if_exists/* = false*/)
{
if(path.empty())
return INFO::OK;
try
{
if(override_if_exists)
fs::copy_file(path.string8(), newPath.string8(), boost::filesystem::copy_option::overwrite_if_exists);
else
fs::copy_file(path.string8(), newPath.string8());
}
catch(fs::filesystem_error& err)
{
debug_printf("CopyFile: failed to copy %s to %s.\n%s\n", path.string8().c_str(), path.string8().c_str(), err.what());
return ERR::EXCEPTION;
}
return INFO::OK;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -85,4 +85,6 @@ LIB_API Status CreateDirectories(const OsPath& path, mode_t mode, bool breakpoin
LIB_API Status DeleteDirectory(const OsPath& dirPath);
LIB_API Status CopyFile(const OsPath& path, const OsPath& newPath, bool override_if_exists = false);
#endif // #ifndef INCLUDED_FILE_SYSTEM

View File

@ -51,6 +51,7 @@ that of Atlas depending on commandline parameters.
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Loader.h"
#include "ps/ModInstaller.h"
#include "ps/Profile.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h"
@ -495,6 +496,28 @@ static void RunGameOrAtlas(int argc, const char* argv[])
}
}
std::vector<OsPath> modsToInstall;
for (const CStr& arg : args.GetArgsWithoutName())
{
const OsPath modPath(arg);
if (!CModInstaller::IsDefaultModExtension(modPath.Extension()))
{
debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str());
continue;
}
if (!FileExists(modPath))
{
debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
continue;
}
if (DirectoryExists(modPath))
{
debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
continue;
}
modsToInstall.emplace_back(std::move(modPath));
}
// We need to initialize SpiderMonkey and libxml2 in the main thread before
// any thread uses them. So initialize them here before we might run Atlas.
ScriptEngine scriptEngine;
@ -577,6 +600,19 @@ static void RunGameOrAtlas(int argc, const char* argv[])
continue;
}
std::vector<CStr> installedMods;
if (!modsToInstall.empty())
{
Paths paths(args);
CModInstaller installer(paths.UserData() / "mods", paths.Cache());
// Install the mods without deleting the pyromod files
for (const OsPath& modPath : modsToInstall)
installer.Install(modPath, g_ScriptRuntime, false);
installedMods = installer.GetInstalledMods();
}
if (isNonVisual)
{
InitNonVisual(args);
@ -585,12 +621,15 @@ static void RunGameOrAtlas(int argc, const char* argv[])
}
else
{
InitGraphics(args, 0);
InitGraphics(args, 0, installedMods);
MainControllerInit();
while (!quit)
Frame();
}
// Do not install mods again in case of restart (typically from the mod selector)
modsToInstall.clear();
Shutdown(0);
MainControllerShutdown();
flags &= ~INIT_MODS;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -20,6 +20,26 @@
#include "lib/sysdep/sysdep.h"
namespace
{
// Simple matcher for elements of the arguments container.
class IsKeyEqualTo
{
public:
IsKeyEqualTo(const CStr& value) : m_Value(value) {}
bool operator()(const std::pair<CStr, CStr>& p) const
{
return p.first == m_Value;
}
private:
const CStr m_Value;
};
} // namespace
CmdLineArgs::CmdLineArgs(int argc, const char* argv[])
{
if (argc >= 1)
@ -35,7 +55,10 @@ CmdLineArgs::CmdLineArgs(int argc, const char* argv[])
{
// Only accept arguments that start with '-'
if (argv[i][0] != '-')
{
m_ArgsWithoutName.emplace_back(argv[i]);
continue;
}
// Allow -arg and --arg
char offset = argv[i][1] == '-' ? 2 : 1;
@ -55,34 +78,26 @@ CmdLineArgs::CmdLineArgs(int argc, const char* argv[])
}
}
bool CmdLineArgs::Has(const char* name) const
bool CmdLineArgs::Has(const CStr& name) const
{
return m_Args.end() != find_if(m_Args.begin(), m_Args.end(),
[&name](const std::pair<CStr, CStr>& a) { return a.first == name; });
return std::any_of(m_Args.begin(), m_Args.end(), IsKeyEqualTo(name));
}
CStr CmdLineArgs::Get(const char* name) const
CStr CmdLineArgs::Get(const CStr& name) const
{
ArgsT::const_iterator it = find_if(m_Args.begin(), m_Args.end(),
[&name](const std::pair<CStr, CStr>& a) { return a.first == name; });
if (it != m_Args.end())
return it->second;
else
return "";
ArgsT::const_iterator it = std::find_if(m_Args.begin(), m_Args.end(), IsKeyEqualTo(name));
return it != m_Args.end() ? it->second : "";
}
std::vector<CStr> CmdLineArgs::GetMultiple(const char* name) const
std::vector<CStr> CmdLineArgs::GetMultiple(const CStr& name) const
{
std::vector<CStr> values;
ArgsT::const_iterator it = m_Args.begin();
while (1)
while ((it = std::find_if(it, m_Args.end(), IsKeyEqualTo(name))) != m_Args.end())
{
it = find_if(it, m_Args.end(),
[&name](const std::pair<CStr, CStr>& a) { return a.first == name; });
if (it == m_Args.end())
break;
values.push_back(it->second);
++it; // start searching from the next one in the next iteration
// Start searching from the next one in the next iteration
++it;
}
return values;
}
@ -91,3 +106,8 @@ OsPath CmdLineArgs::GetArg0() const
{
return m_Arg0;
}
std::vector<CStr> CmdLineArgs::GetArgsWithoutName() const
{
return m_ArgsWithoutName;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2009 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -40,20 +40,20 @@ public:
* Test whether the given name was specified, as either <tt>-name</tt> or
* <tt>-name=value</tt>
*/
bool Has(const char* name) const;
bool Has(const CStr& name) const;
/**
* Get the value of the named parameter. If it was not specified, returns
* the empty string. If it was specified multiple times, returns the value
* from the first occurrence.
*/
CStr Get(const char* name) const;
CStr Get(const CStr& name) const;
/**
* Get all the values given to the named parameter. Returns values in the
* same order as they were given in argv.
*/
std::vector<CStr> GetMultiple(const char* name) const;
std::vector<CStr> GetMultiple(const CStr& name) const;
/**
* Get the value of argv[0], which is typically meant to be the name/path of
@ -61,10 +61,16 @@ public:
*/
OsPath GetArg0() const;
/**
* Returns all arguments that don't have a name (string started with '-').
*/
std::vector<CStr> GetArgsWithoutName() const;
private:
typedef std::vector<std::pair<CStr, CStr> > ArgsT;
ArgsT m_Args;
OsPath m_Arg0;
std::vector<CStr> m_ArgsWithoutName;
};
#endif // INCLUDED_CMDLINEARGS

View File

@ -973,7 +973,7 @@ bool Init(const CmdLineArgs& args, int flags)
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags)
void InitGraphics(const CmdLineArgs& args, int flags, const std::vector<CStr>& installedMods)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
@ -1077,8 +1077,10 @@ void InitGraphics(const CmdLineArgs& args, int flags)
{
scriptInterface->Eval("({})", &data);
scriptInterface->SetProperty(data, "isStartup", true);
if (!installedMods.empty())
scriptInterface->SetProperty(data, "installedMods", installedMods);
}
InitPs(setup_gui, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), data);
InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data);
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)

View File

@ -85,7 +85,8 @@ extern void MountMods(const Paths& paths, const std::vector<CStr>& mods);
* In the latter case the caller should call Shutdown() with SHUTDOWN_FROM_CONFIG.
*/
extern bool Init(const CmdLineArgs& args, int flags);
extern void InitGraphics(const CmdLineArgs& args, int flags);
extern void InitGraphics(const CmdLineArgs& args, int flags,
const std::vector<CStr>& installedMods = std::vector<CStr>());
extern void InitNonVisual(const CmdLineArgs& args);
extern void Shutdown(int flags);
extern void CancelLoad(const CStrW& message);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -34,8 +34,9 @@ public:
void test_get()
{
const char* argv[] = { "program", "-test1=", "-test2=x", "-test3=-y=y-", "-=z" };
const char* argv[] = { "program", "-test1=", "--test2=x", "-test3=-y=y-", "-=z" };
CmdLineArgs c(ARRAY_SIZE(argv), argv);
TS_ASSERT(!c.Has("program"));
TS_ASSERT_STR_EQUALS(c.Get("test0"), "");
TS_ASSERT_STR_EQUALS(c.Get("test1"), "");
TS_ASSERT_STR_EQUALS(c.Get("test2"), "x");
@ -45,7 +46,7 @@ public:
void test_multiple()
{
const char* argv[] = { "program", "-test1=one", "-test1=two", "-test2=none", "-test1=three" };
const char* argv[] = { "program", "-test1=one", "--test1=two", "-test2=none", "-test1=three" };
CmdLineArgs c(ARRAY_SIZE(argv), argv);
TS_ASSERT_STR_EQUALS(c.Get("test1"), "one");
@ -65,7 +66,9 @@ public:
void test_get_invalid()
{
const char* argv[] = { "-test1", "-test2", "test3", " -test4" };
const char* argv[] = {
"-test1", "--test2", "test3-", " -test4", "--", "-=="
};
CmdLineArgs c(ARRAY_SIZE(argv), argv);
TS_ASSERT(!c.Has("test1"));
@ -91,4 +94,14 @@ public:
TS_ASSERT_WSTR_EQUALS(c3.GetArg0().string(), L"ab/cd/ef/gh/../ij");
#endif
}
void test_get_without_names()
{
const char* argv[] = { "program", "test0", "-test1", "test2", "test3", "--test4=test5" };
CmdLineArgs c(ARRAY_SIZE(argv), argv);
TS_ASSERT(c.Has("test1"));
TS_ASSERT_STR_EQUALS(c.Get("test4"), "test5");
CStr expected_args[] = { "test0", "test2", "test3" };
TS_ASSERT_VECTOR_EQUALS_ARRAY(c.GetArgsWithoutName(), expected_args);
}
};

111
source/ps/ModInstaller.cpp Normal file
View File

@ -0,0 +1,111 @@
/* Copyright (C) 2018 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 "ModInstaller.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include <fstream>
CModInstaller::CModInstaller(const OsPath& modsdir, const OsPath& tempdir) :
m_ModsDir(modsdir), m_TempDir(tempdir / "_modscache"), m_CacheDir("cache/")
{
m_VFS = CreateVfs();
CreateDirectories(m_TempDir, 0700);
}
CModInstaller::~CModInstaller()
{
m_VFS.reset();
DeleteDirectory(m_TempDir);
}
CModInstaller::ModInstallationResult CModInstaller::Install(
const OsPath& mod,
const std::shared_ptr<ScriptRuntime>& scriptRuntime,
bool deleteAfterInstall)
{
const OsPath modTemp = m_TempDir / mod.Basename() / mod.Filename().ChangeExtension(L".zip");
CreateDirectories(modTemp.Parent(), 0700);
CopyFile(mod, modTemp, true);
// Load the mod to VFS
if (m_VFS->Mount(m_CacheDir, m_TempDir / "") != INFO::OK)
return FAIL_ON_VFS_MOUNT;
CVFSFile modinfo;
PSRETURN modinfo_status = modinfo.Load(m_VFS, m_CacheDir / modTemp.Basename() / "mod.json", false);
m_VFS->Clear();
if (modinfo_status != PSRETURN_OK)
return FAIL_ON_MOD_LOAD;
// Extract the name of the mod
ScriptInterface scriptInterface("Engine", "ModInstaller", scriptRuntime);
JSContext* cx = scriptInterface.GetContext();
JS::RootedValue json_val(cx);
if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json_val))
return FAIL_ON_PARSE_JSON;
JS::RootedObject json_obj(cx, json_val.toObjectOrNull());
JS::RootedValue name_val(cx);
if (!JS_GetProperty(cx, json_obj, "name", &name_val))
return FAIL_ON_EXTRACT_NAME;
CStr modName;
ScriptInterface::FromJSVal(cx, name_val, modName);
if (modName.empty())
return FAIL_ON_EXTRACT_NAME;
const OsPath modDir = m_ModsDir / modName;
const OsPath modPath = modDir / (modName + ".zip");
// Create a directory with the following structure:
// mod-name/
// mod-name.zip
CreateDirectories(modDir, 0700);
if (wrename(modTemp, modPath) != 0)
return FAIL_ON_MOD_MOVE;
DeleteDirectory(modTemp.Parent());
#ifdef OS_WIN
// On Windows, write the contents of mod.json to a separate file next to the archive:
// mod-name/
// mod-name.zip
// mod.json
std::ofstream mod_json((modDir / "mod.json").string8());
if (mod_json.good())
{
mod_json << modinfo.GetAsString();
mod_json.close();
}
#endif // OS_WIN
// Remove the original file if requested
if (deleteAfterInstall)
wunlink(mod);
m_InstalledMods.emplace_back(modName);
return SUCCESS;
}
const std::vector<CStr>& CModInstaller::GetInstalledMods() const
{
return m_InstalledMods;
}

82
source/ps/ModInstaller.h Normal file
View File

@ -0,0 +1,82 @@
/* Copyright (C) 2018 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/>.
*/
#ifndef INCLUDED_MODINSTALLER
#define INCLUDED_MODINSTALLER
#include "lib/file/vfs/vfs.h"
#include "scriptinterface/ScriptInterface.h"
#include <vector>
/**
* Install a mod into the mods directory.
*/
class CModInstaller
{
public:
enum ModInstallationResult
{
SUCCESS,
FAIL_ON_VFS_MOUNT,
FAIL_ON_MOD_LOAD,
FAIL_ON_PARSE_JSON,
FAIL_ON_EXTRACT_NAME,
FAIL_ON_MOD_MOVE
};
/**
* Initialise the mod installer for processing the given mod.
*
* @param modsdir path to the data directory that contains mods
* @param tempdir path to a writable directory for temporary files
*/
CModInstaller(const OsPath& modsdir, const OsPath& tempdir);
~CModInstaller();
/**
* Process and unpack the mod.
* @param mod path of .pyromod/.zip file
*/
ModInstallationResult Install(
const OsPath& mod,
const std::shared_ptr<ScriptRuntime>& scriptRuntime,
bool deleteAfterInstall);
/**
* @return a list of all mods installed so far by this CModInstaller.
*/
const std::vector<CStr>& GetInstalledMods() const;
/**
* @return whether the path has a mod-like extension.
*/
static bool IsDefaultModExtension(const Path& ext)
{
return ext == ".pyromod" || ext == ".zip";
}
private:
PIVFS m_VFS;
OsPath m_ModsDir;
OsPath m_TempDir;
VfsPath m_CacheDir;
std::vector<CStr> m_InstalledMods;
};
#endif // INCLUDED_MODINSTALLER

View File

@ -87,7 +87,7 @@ MESSAGEHANDLER(InitGraphics)
ogl_Init();
InitGraphics(g_AtlasGameLoop->args, g_InitFlags);
InitGraphics(g_AtlasGameLoop->args, g_InitFlags, {});
#if OS_WIN
// HACK (to stop things looking very ugly when scrolling) - should