Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js +++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js @@ -48,17 +48,33 @@ 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 @@ 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 || ""); Index: ps/trunk/binaries/system/readme.txt =================================================================== --- ps/trunk/binaries/system/readme.txt +++ ps/trunk/binaries/system/readme.txt @@ -51,6 +51,9 @@ -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 Index: ps/trunk/source/lib/file/file_system.h =================================================================== --- ps/trunk/source/lib/file/file_system.h +++ ps/trunk/source/lib/file/file_system.h @@ -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 DeleteDirectory(const OsPath& dirPath); +LIB_API Status CopyFile(const OsPath& path, const OsPath& newPath, bool override_if_exists = false); + #endif // #ifndef INCLUDED_FILE_SYSTEM Index: ps/trunk/source/lib/file/file_system.cpp =================================================================== --- ps/trunk/source/lib/file/file_system.cpp +++ ps/trunk/source/lib/file/file_system.cpp @@ -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 @@ 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; +} Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp +++ ps/trunk/source/main.cpp @@ -51,6 +51,7 @@ #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 @@ } } + std::vector 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 @@ continue; } + std::vector 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 @@ } 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; Index: ps/trunk/source/ps/GameSetup/CmdLineArgs.h =================================================================== --- ps/trunk/source/ps/GameSetup/CmdLineArgs.h +++ ps/trunk/source/ps/GameSetup/CmdLineArgs.h @@ -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 @@ * Test whether the given name was specified, as either -name or * -name=value */ - 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 GetMultiple(const char* name) const; + std::vector 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 @@ */ OsPath GetArg0() const; + /** + * Returns all arguments that don't have a name (string started with '-'). + */ + std::vector GetArgsWithoutName() const; + private: typedef std::vector > ArgsT; ArgsT m_Args; OsPath m_Arg0; + std::vector m_ArgsWithoutName; }; #endif // INCLUDED_CMDLINEARGS Index: ps/trunk/source/ps/GameSetup/CmdLineArgs.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/CmdLineArgs.cpp +++ ps/trunk/source/ps/GameSetup/CmdLineArgs.cpp @@ -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& 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 @@ { // 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 @@ } } -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& 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& 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 CmdLineArgs::GetMultiple(const char* name) const +std::vector CmdLineArgs::GetMultiple(const CStr& name) const { std::vector 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& 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 @@ { return m_Arg0; } + +std::vector CmdLineArgs::GetArgsWithoutName() const +{ + return m_ArgsWithoutName; +} Index: ps/trunk/source/ps/GameSetup/GameSetup.h =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.h +++ ps/trunk/source/ps/GameSetup/GameSetup.h @@ -85,7 +85,8 @@ * 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& installedMods = std::vector()); extern void InitNonVisual(const CmdLineArgs& args); extern void Shutdown(int flags); extern void CancelLoad(const CStrW& message); Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp @@ -973,7 +973,7 @@ return true; } -void InitGraphics(const CmdLineArgs& args, int flags) +void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; @@ -1077,8 +1077,10 @@ { 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) Index: ps/trunk/source/ps/GameSetup/tests/test_CmdLineArgs.h =================================================================== --- ps/trunk/source/ps/GameSetup/tests/test_CmdLineArgs.h +++ ps/trunk/source/ps/GameSetup/tests/test_CmdLineArgs.h @@ -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 @@ 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 @@ 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 @@ 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 @@ 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); + } }; Index: ps/trunk/source/ps/ModInstaller.h =================================================================== --- ps/trunk/source/ps/ModInstaller.h +++ ps/trunk/source/ps/ModInstaller.h @@ -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 . + */ + +#ifndef INCLUDED_MODINSTALLER +#define INCLUDED_MODINSTALLER + +#include "lib/file/vfs/vfs.h" +#include "scriptinterface/ScriptInterface.h" + +#include + +/** + * 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, + bool deleteAfterInstall); + + /** + * @return a list of all mods installed so far by this CModInstaller. + */ + const std::vector& 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 m_InstalledMods; +}; + +#endif // INCLUDED_MODINSTALLER Index: ps/trunk/source/ps/ModInstaller.cpp =================================================================== --- ps/trunk/source/ps/ModInstaller.cpp +++ ps/trunk/source/ps/ModInstaller.cpp @@ -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 . + */ + +#include "precompiled.h" + +#include "ModInstaller.h" + +#include "lib/file/vfs/vfs_util.h" +#include "ps/Filesystem.h" +#include "ps/XML/Xeromyces.h" + +#include + +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, + 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& CModInstaller::GetInstalledMods() const +{ + return m_InstalledMods; +} Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp @@ -87,7 +87,7 @@ 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