Index: binaries/data/mods/mod/gui/incompatible_mods/incompatible_mods.js
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/incompatible_mods/incompatible_mods.js
@@ -0,0 +1,11 @@
+var g_IncompatibleModsFile = "gui/incompatible_mods/incompatible_mods.txt";
+
+function init(data)
+{
+ Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile(g_IncompatibleModsFile));
+}
+
+function closePage()
+{
+ Engine.PopGuiPage();
+}
Index: binaries/data/mods/mod/gui/incompatible_mods/incompatible_mods.txt
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/incompatible_mods/incompatible_mods.txt
@@ -0,0 +1,4 @@
+[font="sans-bold-20"] You tried to start game with incompatible or missing mods!
+[font="sans-16"]
+Solve the compatibility and start game again.
+You might want to save configuration before starting, when you see this page again.
Index: binaries/data/mods/mod/gui/incompatible_mods/incompatible_mods.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/incompatible_mods/incompatible_mods.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/mod/gui/modmod/modmod.js
===================================================================
--- binaries/data/mods/mod/gui/modmod/modmod.js
+++ binaries/data/mods/mod/gui/modmod/modmod.js
@@ -61,6 +61,17 @@
*/
var g_InstalledMods;
+var g_HasFailedMods;
+
+var g_FakeMod = {
+ "name": translate("This mod does not exist"),
+ "version": "",
+ "label": "",
+ "url": "",
+ "description": "",
+ "dependencies": []
+};
+
var g_ColorNoModSelected = "255 255 100";
var g_ColorDependenciesMet = "100 255 100";
var g_ColorDependenciesNotMet = "255 100 100";
@@ -68,9 +79,12 @@
function init(data, hotloadData)
{
g_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || [];
+ g_HasFailedMods = Engine.HasFailedMods();
initMods();
initGUIButtons(data);
+ if (g_HasFailedMods)
+ Engine.PushGuiPage("page_incompatible_mods.xml", {});
}
function initMods()
@@ -93,9 +107,20 @@
deepfreeze(g_Mods);
}
+/**
+ * Return fake mod for mods which do not exist
+*/
+function getMod(folder)
+{
+ return !!g_Mods[folder] ? g_Mods[folder] : g_FakeMod;
+}
+
function loadEnabledMods()
{
- g_ModsEnabled = Engine.ConfigDB_GetValue("user", "mod.enabledmods").split(/\s+/).filter(folder => !!g_Mods[folder]);
+ if (g_HasFailedMods)
+ g_ModsEnabled = Engine.GetFailedMods().filter(folder => folder != "mod");
+ else
+ g_ModsEnabled = Engine.GetEnabledMods().filter(folder => !!g_Mods[folder]);
g_ModsDisabled = Object.keys(g_Mods).filter(folder => g_ModsEnabled.indexOf(folder) == -1);
g_ModsEnabledFiltered = g_ModsEnabled;
g_ModsDisabledFiltered = g_ModsDisabled;
@@ -118,9 +143,11 @@
function initGUIButtons(data)
{
// Either get back to the previous page or quit if there is no previous page
- let cancelButton = !data || data.cancelbutton;
- Engine.GetGUIObjectByName("cancelButton").hidden = !cancelButton;
- Engine.GetGUIObjectByName("quitButton").hidden = cancelButton;
+ let hasPreviousPage = !data || data.cancelbutton;
+ Engine.GetGUIObjectByName("cancelButton").hidden = !hasPreviousPage;
+ Engine.GetGUIObjectByName("quitButton").hidden = hasPreviousPage;
+ Engine.GetGUIObjectByName("startModsButton").hidden = !hasPreviousPage;
+ Engine.GetGUIObjectByName("startButton").hidden = hasPreviousPage;
Engine.GetGUIObjectByName("toggleModButton").caption = translateWithContext("mod activation", "Enable");
}
@@ -134,8 +161,8 @@
function startMods()
{
sortEnabledMods();
- Engine.SetMods(["mod"].concat(g_ModsEnabled));
- Engine.RestartEngine();
+ if (!Engine.SetModsAndRestartEngine(["mod"].concat(g_ModsEnabled)))
+ Engine.GetGUIObjectByName("message").caption = coloredText(translate('Dependencies not met'), g_ColorDependenciesNotMet);
}
function displayModLists()
@@ -150,7 +177,7 @@
if (listObjectName == "modsDisabledList")
{
- let sortFolder = folder => String(g_Mods[folder][listObject.selected_column] || folder);
+ let sortFolder = folder => String(getMod(folder)[listObject.selected_column] || folder);
folders.sort((folder1, folder2) =>
listObject.selected_column_order *
sortFolder(folder1).localeCompare(sortFolder(folder2)));
@@ -162,12 +189,12 @@
let selected = listObject.selected !== -1 ? listObject.list_name[listObject.selected] : null;
- listObject.list_name = folders.map(folder => colorMod(folder, g_Mods[folder].name, enabled));
+ listObject.list_name = folders.map(folder => colorMod(folder, getMod(folder).name, enabled));
listObject.list_folder = folders.map(folder => colorMod(folder, folder, enabled));
- listObject.list_label = folders.map(folder => colorMod(folder, g_Mods[folder].label, enabled));
- listObject.list_url = folders.map(folder => colorMod(folder, g_Mods[folder].url || "", enabled));
- listObject.list_version = folders.map(folder => colorMod(folder, g_Mods[folder].version, enabled));
- listObject.list_dependencies = folders.map(folder => colorMod(folder, g_Mods[folder].dependencies.join(" "), enabled));
+ listObject.list_label = folders.map(folder => colorMod(folder, getMod(folder).label, enabled));
+ listObject.list_url = folders.map(folder => colorMod(folder, getMod(folder).url || "", enabled));
+ listObject.list_version = folders.map(folder => colorMod(folder, getMod(folder).version, enabled));
+ listObject.list_dependencies = folders.map(folder => colorMod(folder, getMod(folder).dependencies.join(" "), enabled));
listObject.list = folders;
listObject.selected = selected ? listObject.list_name.indexOf(selected) : -1;
@@ -179,7 +206,7 @@
{
if (!g_ModsCompatibility[folder])
return enabled ? g_ColorDependenciesNotMet : "gray";
- if (g_InstalledMods.indexOf(g_Mods[folder].name) != -1)
+ if (g_InstalledMods.indexOf(getMod(folder).name) != -1)
return "green";
return false;
}
@@ -211,6 +238,7 @@
--pos;
displayModLists();
+ Engine.GetGUIObjectByName("message").caption = "";
modsDisabledList.selected = pos;
}
@@ -230,7 +258,8 @@
break;
}
- g_ModsDisabled.push(disabledMod);
+ if (!!g_Mods[disabledMod])
+ g_ModsDisabled.push(disabledMod);
// Remove mods that required the removed mod and cascade
// Sort them, so we know which ones can depend on the removed mod
@@ -247,12 +276,13 @@
recomputeCompatibility(true);
displayModLists();
+ Engine.GetGUIObjectByName("message").caption = "";
modsEnabledList.selected = Math.min(pos, g_ModsEnabledFiltered.length - 1);
}
function filterMod(folder)
{
- let mod = g_Mods[folder];
+ let mod = getMod(folder);
let negateFilter = Engine.GetGUIObjectByName("negateFilter").checked;
let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption;
@@ -316,7 +346,10 @@
if (disabledAction && !g_ModsCompatibility[folder])
return g_ModsCompatibility[folder];
- for (let dependency of g_Mods[folder].dependencies)
+ if (!g_Mods[folder])
+ return false;
+
+ for (let dependency of getMod(folder).dependencies)
{
if (!isDependencyMet(dependency))
return false;
@@ -340,8 +373,8 @@
let [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined];
return g_ModsEnabled.some(folder =>
- g_Mods[folder].name == name &&
- (!operator || versionSatisfied(g_Mods[folder].version, operator[0], version)));
+ getMod(folder).name == name &&
+ (!operator || versionSatisfied(getMod(folder).version, operator[0], version)));
}
/**
@@ -385,11 +418,11 @@
{
let dependencies = {};
for (let folder of g_ModsEnabled)
- dependencies[folder] = g_Mods[folder].dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]);
+ dependencies[folder] = getMod(folder).dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]);
g_ModsEnabled.sort((folder1, folder2) =>
- dependencies[folder1].indexOf(g_Mods[folder2].name) != -1 ? 1 :
- dependencies[folder2].indexOf(g_Mods[folder1].name) != -1 ? -1 : 0);
+ dependencies[folder1].indexOf(getMod(folder2).name) != -1 ? 1 :
+ dependencies[folder2].indexOf(getMod(folder1).name) != -1 ? -1 : 0);
g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled, true);
}
@@ -419,8 +452,21 @@
Engine.GetGUIObjectByName("globalModDescription").caption =
listObject.list[listObject.selected] ?
- g_Mods[listObject.list[listObject.selected]].description :
+ getMod(listObject.list[listObject.selected]).description :
'[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]';
+
+ if (!g_ModsEnabled.length)
+ {
+ if (!Engine.GetGUIObjectByName("startButton").hidden)
+ Engine.GetGUIObjectByName("message").caption = coloredText(translate('Enable at least 0ad mod and save configuration'), g_ColorDependenciesNotMet);
+ else
+ Engine.GetGUIObjectByName("message").caption = coloredText(translate('Enable at least 0ad mod'), g_ColorDependenciesNotMet);
+ }
+ if (!Engine.GetGUIObjectByName("startButton").hidden)
+ Engine.GetGUIObjectByName("startButton").enabled = g_ModsEnabled.length > 0;
+ if (!Engine.GetGUIObjectByName("startModsButton").hidden)
+ Engine.GetGUIObjectByName("startModsButton").enabled = g_ModsEnabled.length > 0;
+
}
/**
@@ -433,7 +479,7 @@
let list = modsEnabledList.selected == -1 ? modsDisabledList : modsEnabledList;
let folder = list.list[list.selected];
- return folder && g_Mods[folder] && g_Mods[folder].url || undefined;
+ return folder && getMod(folder) && getMod(folder).url || undefined;
}
function visitModWebsite()
Index: binaries/data/mods/mod/gui/modmod/modmod.xml
===================================================================
--- binaries/data/mods/mod/gui/modmod/modmod.xml
+++ binaries/data/mods/mod/gui/modmod/modmod.xml
@@ -216,5 +216,10 @@
Start Mods
startMods();
+
+
+ Start
+ startMods();
+
Index: binaries/data/mods/mod/gui/page_incompatible_mods.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/mod/gui/page_incompatible_mods.xml
@@ -0,0 +1,9 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ modmod/styles.xml
+ incompatible_mods/incompatible_mods.xml
+
Index: source/main.cpp
===================================================================
--- source/main.cpp
+++ source/main.cpp
@@ -49,6 +49,7 @@
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Loader.h"
+#include "ps/Mod.h"
#include "ps/ModInstaller.h"
#include "ps/Profile.h"
#include "ps/Profiler2.h"
@@ -595,7 +596,7 @@
g_VFS = CreateVfs();
// Mount with highest priority, we don't want mods overwriting this.
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY);
- MountMods(paths, GetMods(args, INIT_MODS));
+ MountMods(paths, Mod::GetModsFromArguments(args, INIT_MODS));
{
CReplayPlayer replay;
@@ -695,6 +696,7 @@
// Do not install mods again in case of restart (typically from the mod selector)
modsToInstall.clear();
+ Mod::ClearIncompatibleMods();
Shutdown(0);
MainControllerShutdown();
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -103,6 +103,7 @@
#include
#include
+#include
#include
ERROR_GROUP(System);
@@ -379,24 +380,6 @@
return ERI_NOT_IMPLEMENTED;
}
-const std::vector& GetMods(const CmdLineArgs& args, int flags)
-{
- const bool init_mods = (flags & INIT_MODS) == INIT_MODS;
- const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
-
- if (!init_mods)
- return g_modsLoaded;
-
- g_modsLoaded = args.GetMultiple("mod");
-
- if (add_public)
- g_modsLoaded.insert(g_modsLoaded.begin(), "public");
-
- g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
-
- return g_modsLoaded;
-}
-
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
@@ -457,7 +440,7 @@
// Engine localization files (regular priority, these can be overwritten).
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
- MountMods(paths, GetMods(args, flags));
+ MountMods(paths, Mod::GetModsFromArguments(args, flags));
// note: don't bother with g_VFS->TextRepresentation - directories
// haven't yet been populated and are empty.
@@ -864,6 +847,26 @@
*/
bool AutostartVisualReplay(const std::string& replayFile);
+bool EnableModsOrSetDefault(const CmdLineArgs& args, int flags, const std::vector& mods, bool fromConfig)
+{
+ ScriptInterface scriptInterface("Engine", "CheckAndEnableMods", g_ScriptContext);
+ if (Mod::CheckAndEnableMods(scriptInterface, mods))
+ return true;
+ // Here we refuse to start as there is no gui anyway
+ if (args.Has("autostart-nonvisual"))
+ {
+ if (fromConfig)
+ LOGERROR("Trying to start with incompatible mods from configuration file: %s.", boost::algorithm::join(Mod::GetIncompatibleMods(), ", "));
+ else
+ LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(Mod::GetIncompatibleMods(), ", "));
+ return false;
+ }
+ Mod::SetDefaultMods(args, flags);
+ RestartEngine();
+ return false;
+}
+
+
bool Init(const CmdLineArgs& args, int flags)
{
h_mgr_init();
@@ -924,20 +927,25 @@
// else check if there are mods that should be loaded specified
// in the config and load those (by aborting init and restarting
// the engine).
- if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS)
+ if ((flags & INIT_MODS) == INIT_MODS)
{
- CStr modstring;
- CFG_GET_VAL("mod.enabledmods", modstring);
- if (!modstring.empty())
+ if (!args.Has("mod"))
{
- std::vector mods;
- boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
- std::swap(g_modsLoaded, mods);
+ CStr modstring;
+ CFG_GET_VAL("mod.enabledmods", modstring);
+ if (!modstring.empty())
+ {
+ std::vector mods;
+ boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
+ if (!EnableModsOrSetDefault(args, flags, mods, true))
+ return false;
- // Abort init and restart
- RestartEngine();
- return false;
+ RestartEngine();
+ return false;
+ }
}
+ else if (!EnableModsOrSetDefault(args, flags, g_modsLoaded, false))
+ return false;
}
new L10n;
Index: source/ps/Mod.h
===================================================================
--- source/ps/Mod.h
+++ source/ps/Mod.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* 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
@@ -30,7 +30,9 @@
namespace Mod
{
JS::Value GetAvailableMods(const ScriptInterface& scriptInterface);
-
+ const std::vector& GetEnabledMods();
+ const std::vector& GetIncompatibleMods();
+ const std::vector& GetFailedMods();
/**
* This reads the version numbers from the launched mods.
* It caches the result, since the reading of zip files is slow and
@@ -39,6 +41,13 @@
*/
void CacheEnabledModVersions(const shared_ptr& scriptContext);
+ const std::vector& GetModsFromArguments(const CmdLineArgs& args, int flags);
+ bool AreModsCompatible(const ScriptInterface& scriptInterface, const std::vector& mods, const JS::RootedValue& availableMods);
+ bool CheckAndEnableMods(const ScriptInterface& scriptInterface, const std::vector& mods);
+ bool CompareVersionStrings(const CStr& required, const CStr& op, const CStr& version);
+ void SetDefaultMods(const CmdLineArgs& args, int flags);
+ void ClearIncompatibleMods();
+
/**
* Get the loaded mods and their version.
* "user" mod and "mod" mod are ignored as they are irrelevant for compatibility checks.
Index: source/ps/Mod.cpp
===================================================================
--- source/ps/Mod.cpp
+++ source/ps/Mod.cpp
@@ -19,8 +19,6 @@
#include "ps/Mod.h"
-#include
-
#include "lib/file/file_system.h"
#include "lib/file/vfs/vfs.h"
#include "lib/utf8.h"
@@ -30,7 +28,14 @@
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
+#include
+#include
+#include
+#include
+
std::vector g_modsLoaded;
+std::vector g_incompatibleMods;
+std::vector g_failedMods;
std::vector> g_LoadedModVersions;
@@ -105,6 +110,183 @@
return JS::ObjectValue(*obj);
}
+const std::vector& Mod::GetEnabledMods()
+{
+ return g_modsLoaded;
+}
+
+const std::vector& Mod::GetIncompatibleMods()
+{
+ return g_incompatibleMods;
+}
+
+const std::vector& Mod::GetFailedMods()
+{
+ return g_failedMods;
+}
+
+const std::vector& Mod::GetModsFromArguments(const CmdLineArgs& args, int flags)
+{
+ const bool initMods = (flags & INIT_MODS) == INIT_MODS;
+ const bool addPublic = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
+
+ if (!initMods)
+ return g_modsLoaded;
+
+ g_modsLoaded = args.GetMultiple("mod");
+
+ if (addPublic)
+ g_modsLoaded.insert(g_modsLoaded.begin(), "public");
+
+ g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
+
+ return g_modsLoaded;
+}
+
+void Mod::SetDefaultMods(const CmdLineArgs& args, int flags)
+{
+ g_modsLoaded.clear();
+ g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
+}
+
+void Mod::ClearIncompatibleMods()
+{
+ g_incompatibleMods.clear();
+ g_failedMods.clear();
+}
+
+bool Mod::CheckAndEnableMods(const ScriptInterface& scriptInterface, const std::vector& mods)
+{
+ ScriptRequest rq(scriptInterface);
+
+ JS::RootedValue availableMods(rq.cx, GetAvailableMods(scriptInterface));
+ if (!AreModsCompatible(scriptInterface, mods, availableMods))
+ {
+ g_failedMods = mods;
+ return false;
+ }
+ g_modsLoaded = mods;
+ return true;
+}
+
+bool Mod::AreModsCompatible(const ScriptInterface& scriptInterface, const std::vector& mods, const JS::RootedValue& availableMods)
+{
+ ScriptRequest rq(scriptInterface);
+ std::unordered_map> modDependencies;
+ std::unordered_map modNameVersions;
+ for (const CStr& mod : mods)
+ {
+ if (mod == "mod")
+ continue;
+
+ JS::RootedValue modData(rq.cx);
+
+ // Requested mod is not available, fail
+ if (!scriptInterface.HasProperty(availableMods, mod.c_str()))
+ {
+ g_incompatibleMods.push_back(mod);
+ continue;
+ }
+ if (!scriptInterface.GetProperty(availableMods, mod.c_str(), &modData))
+ {
+ g_incompatibleMods.push_back(mod);
+ continue;
+ }
+
+ CStr dependencies;
+ CStr version;
+ CStr name;
+ scriptInterface.GetProperty(modData, "dependencies", dependencies);
+ scriptInterface.GetProperty(modData, "version", version);
+ scriptInterface.GetProperty(modData, "name", name);
+
+ modNameVersions.emplace(name, version);
+ std::vector deps;
+ boost::split(deps, dependencies, boost::is_any_of(","), boost::token_compress_on);
+ modDependencies.emplace(mod, deps);
+ }
+
+ static const std::vector toCheck = { "<=", ">=", "=", "<", ">" };
+ for (const CStr& mod : mods)
+ {
+ if (mod == "mod")
+ continue;
+
+ const std::unordered_map>::iterator res = modDependencies.find(mod);
+ if (res == modDependencies.end())
+ continue;
+ const std::vector deps = res->second;
+ if (deps.empty())
+ continue;
+
+ for (const CStr& dep : deps)
+ {
+ if (dep.empty())
+ continue;
+ // 0ad<=0.0.24
+ for (const CStr& op : toCheck)
+ {
+ const int pos = dep.Find(op.c_str());
+ if (pos == -1)
+ continue;
+ //0ad
+ const CStr modToCheck = dep.substr(0, pos);
+ //0.0.24
+ const CStr versionToCheck = dep.substr(pos + op.size());
+ const std::unordered_map::iterator it = modNameVersions.find(modToCheck);
+ if (it == modNameVersions.end())
+ {
+ g_incompatibleMods.push_back(mod);
+ continue;
+ }
+ // 0.0.25(0ad) , <=, 0.0.24(required version)
+ if (!CompareVersionStrings(it->second, op, versionToCheck))
+ {
+ g_incompatibleMods.push_back(mod);
+ continue;
+ }
+ }
+ }
+
+ }
+
+ return g_incompatibleMods.empty();
+}
+
+bool Mod::CompareVersionStrings(const CStr& version, const CStr& op, const CStr& required)
+{
+ std::vector versionSplit;
+ std::vector requiredSplit;
+ static const std::string toIgnore = "-,_";
+ boost::split(versionSplit, version, boost::is_any_of(toIgnore), boost::token_compress_on);
+ boost::split(requiredSplit, required, boost::is_any_of(toIgnore), boost::token_compress_on);
+ boost::split(versionSplit, versionSplit[0], boost::is_any_of("."), boost::token_compress_on);
+ boost::split(requiredSplit, requiredSplit[0], boost::is_any_of("."), boost::token_compress_on);
+
+ const bool eq = op.Find("=") != -1;
+ const bool lt = op.Find("<") != -1;
+ const bool gt = op.Find(">") != -1;
+
+ const size_t min = std::min(versionSplit.size(), requiredSplit.size());
+
+ for (size_t i = 0; i < min; ++i)
+ {
+ const int diff = versionSplit[i].ToInt() - requiredSplit[i].ToInt();
+ if (gt && diff > 0 || lt && diff < 0)
+ return true;
+
+ if (gt && diff < 0 || lt && diff > 0 || eq && diff)
+ return false;
+ }
+
+ const size_t versionSize = versionSplit.size();
+ const size_t requiredSize = requiredSplit.size();
+ if (versionSize == requiredSize)
+ return eq;
+ return versionSize < requiredSize ? lt : gt;
+}
+
+
void Mod::CacheEnabledModVersions(const shared_ptr& scriptContext)
{
ScriptInterface scriptInterface("Engine", "CacheEnabledModVersions", scriptContext);
Index: source/ps/scripting/JSInterface_Mod.cpp
===================================================================
--- source/ps/scripting/JSInterface_Mod.cpp
+++ source/ps/scripting/JSInterface_Mod.cpp
@@ -27,36 +27,28 @@
namespace
{
-JS::Value GetEngineInfo(ScriptInterface::CmptPrivate* pCmptPrivate)
+bool SetModsAndRestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods)
{
- return Mod::GetEngineInfo(*(pCmptPrivate->pScriptInterface));
-}
+ Mod::ClearIncompatibleMods();
+ if (!Mod::CheckAndEnableMods(*(pCmptPrivate->pScriptInterface), mods))
+ return false;
-/**
- * Returns a JS object containing a listing of available mods that
- * have a modname.json file in their modname folder. The returned
- * object looks like { modname1: json1, modname2: json2, ... } where
- * jsonN is the content of the modnameN/modnameN.json file as a JS
- * object.
- *
- * @return JS object with available mods as the keys of the modname.json
- * properties.
- */
-JS::Value GetAvailableMods(ScriptInterface::CmptPrivate* pCmptPrivate)
-{
- return Mod::GetAvailableMods(*(pCmptPrivate->pScriptInterface));
+ RestartEngine();
+ return true;
+}
}
-void SetMods(const std::vector& mods)
+bool HasFailedMods(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
- g_modsLoaded = mods;
-}
+ return Mod::GetFailedMods().size() > 0;
}
void JSI_Mod::RegisterScriptFunctions(const ScriptRequest& rq)
{
- ScriptFunction::Register<&GetEngineInfo>(rq, "GetEngineInfo");
- ScriptFunction::Register<&GetAvailableMods>(rq, "GetAvailableMods");
- ScriptFunction::Register<&RestartEngine>(rq, "RestartEngine");
- ScriptFunction::Register<&SetMods>(rq, "SetMods");
+ ScriptFunction::Register<&Mod::GetEngineInfo>(rq, "GetEngineInfo");
+ ScriptFunction::Register<&Mod::GetAvailableMods>(rq, "GetAvailableMods");
+ ScriptFunction::Register<&Mod::GetEnabledMods>(rq, "GetEnabledMods");
+ ScriptFunction::Register (rq, "HasFailedMods");
+ ScriptFunction::Register<&Mod::GetFailedMods>(rq, "GetFailedMods");
+ ScriptFunction::Register<&SetModsAndRestartEngine>(rq, "SetModsAndRestartEngine");
}
Index: source/ps/tests/test_Mod.h
===================================================================
--- /dev/null
+++ source/ps/tests/test_Mod.h
@@ -0,0 +1,164 @@
+/* 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 "lib/self_test.h"
+
+#include "ps/CLogger.h"
+#include "ps/Mod.h"
+#include "scriptinterface/ScriptInterface.h"
+
+class TestMod : public CxxTest::TestSuite
+{
+public:
+ void test_version_check()
+ {
+ CStr eq = "=";
+ CStr lt = "<";
+ CStr gt = ">";
+ CStr leq = "<=";
+ CStr geq = ">=";
+
+ CStr required = "0.0.24";// 0ad <= required
+ CStr version = "0.0.24";// 0ad version
+
+ // 0.0.24 = 0.0.24
+ TS_ASSERT(Mod::CompareVersionStrings(version, eq, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, lt, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, gt, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, leq, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, geq, required));
+
+ // 0.0.23 <= 0.0.24
+ version = "0.0.23";
+ TS_ASSERT(!Mod::CompareVersionStrings(version, eq, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, lt, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, gt, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, leq, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, geq, required));
+
+ // 0.0.25 >= 0.0.24
+ version = "0.0.25";
+ TS_ASSERT(!Mod::CompareVersionStrings(version, eq, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, lt, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, gt, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, leq, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, geq, required));
+
+ // 0.0.9 <= 0.1.0
+ version = "0.0.9";
+ required = "0.1.0";
+ TS_ASSERT(!Mod::CompareVersionStrings(version, eq, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, lt, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, gt, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, leq, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, geq, required));
+
+ // 5.3 <= 5.3.0
+ version = "5.3";
+ required = "5.3.0";
+ TS_ASSERT(!Mod::CompareVersionStrings(version, eq, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, lt, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, gt, required));
+ TS_ASSERT(Mod::CompareVersionStrings(version, leq, required));
+ TS_ASSERT(!Mod::CompareVersionStrings(version, geq, required));
+ }
+
+ void test_compatible()
+ {
+ ScriptInterface script("Test", "Test", g_ScriptContext);
+
+ ScriptRequest rq(script);
+ JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx));
+
+ CStr jsonString = "{\
+ \"name\": \"0ad\",\
+ \"version\" : \"0.0.25\",\
+ \"label\" : \"0 A.D. Empires Ascendant\",\
+ \"url\" : \"https://play0ad.com\",\
+ \"description\" : \"A free, open-source, historical RTS game.\",\
+ \"dependencies\" : []\
+ }\
+ ";
+ JS::RootedValue json(rq.cx);
+ TS_ASSERT(script.ParseJSON(jsonString, &json));
+ JS_SetProperty(rq.cx, obj, "public", json);
+
+ JS::RootedValue jsonW(rq.cx);
+ CStr jsonStringW = "{\
+ \"name\": \"wrong\",\
+ \"version\" : \"0.0.25\",\
+ \"label\" : \"wrong mod\",\
+ \"url\" : \"\",\
+ \"description\" : \"fail\",\
+ \"dependencies\" : [\"0ad=0.0.24\"]\
+ }\
+ ";
+ TS_ASSERT(script.ParseJSON(jsonStringW, &jsonW));
+ JS_SetProperty(rq.cx, obj, "wrong", jsonW);
+
+ JS::RootedValue jsonG(rq.cx);
+ CStr jsonStringG = "{\
+ \"name\": \"good\",\
+ \"version\" : \"0.0.25\",\
+ \"label\" : \"good mod\",\
+ \"url\" : \"\",\
+ \"description\" : \"ok\",\
+ \"dependencies\" : [\"0ad=0.0.25\"]\
+ }\
+ ";
+ TS_ASSERT(script.ParseJSON(jsonStringG, &jsonG));
+ JS_SetProperty(rq.cx, obj, "good", jsonG);
+
+ JS::RootedValue availableMods(rq.cx, JS::ObjectValue(*obj));
+
+ std::vector mods;
+
+ mods.push_back("public");
+ TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
+
+ mods.clear();
+ mods.push_back("mod");
+ mods.push_back("public");
+ TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
+
+ mods.clear();
+ mods.push_back("user");
+ mods.push_back("public");
+ TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
+
+ mods.clear();
+ mods.push_back("public");
+ mods.push_back("good");
+ TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
+
+ mods.clear();
+ mods.push_back("public");
+ mods.push_back("wrong");
+ TS_ASSERT(!Mod::AreModsCompatible(script, mods, availableMods));
+
+ mods.clear();
+ mods.push_back("public");
+ mods.push_back("does_not_exist");
+ TS_ASSERT(!Mod::AreModsCompatible(script, mods, availableMods));
+
+ }
+};