Do not allow to start pyrogenesis with incompatible mods

Fixing following problems:
Issue number one:
Enable mod with a23 compatibility in a23b.
Save configuration.
Start a24.
Better result:
Mod will be enabled and invisible in mod selection screen producing
various errors.
Worse result:
Game will crash and refuse to start.

Issue number two:
Mods can silently set loaded mods without restarting the engine, so mods
can unlist themselves from compatibility detection.

Solution:
Enable necessary mods instead if running with gui and open mod page.
Open information window on top of mod page to infom why mod page is
showing up.
On mod page show mods which failed in compatibility check and color the
resposnible ones.
Disable start button without enabled mods.
Show non existed mods if they failed in compatibility check.

Else just log to mainlog and close.

Another fixes:
Display in enabled mods really enabled mods as current logic confuses
players about which mods they have enabled and is not helpful (ref
#4881)

Note:
this will not solve issue with mods claiming being compatible with
engine version while in fact being incompatible.

Comments by: @vladislavbelov, @Stan, @Imarok
Tested by: @wraitii
Differential revision: D3592
Fixes: #6044 #4881

This was SVN commit r25410.
This commit is contained in:
Angen 2021-05-09 13:53:25 +00:00
parent 933b331c1b
commit f1acd22455
12 changed files with 536 additions and 81 deletions

View File

@ -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();
}

View File

@ -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.

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script directory="gui/common/"/>
<script directory="gui/incompatible_mods/"/>
<!-- Add a translucent black background to fade out the menu page -->
<object type="image" sprite="ModernFade"/>
<object type="image" style="ModernDialog" size="50%-274 50%-200 50%+274 50%+200">
<object type="text" style="ModernLabelText" size="50%-128 -18 50%+128 14">
<translatableAttribute id="caption">Welcome!</translatableAttribute>
</object>
<object name="mainText" type="text" style="ModernTextPanel" size="20 20 100%-20 100%-52">
</object>
<object name="btnClose" type="button" style="ModernButtonRed" size="18 100%-45 100%-18 100%-17" hotkey="cancel">
<translatableAttribute id="caption">Ok</translatableAttribute>
<action on="Press">closePage();</action>
</object>
</object>
</objects>

View File

@ -61,6 +61,17 @@ var g_ModsCompatibility = [];
*/
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 @@ var g_ColorDependenciesNotMet = "255 100 100";
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 @@ function loadMods()
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 initGUIFilters()
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 saveMods()
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 @@ function displayModList(listObjectName, folders, enabled)
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 @@ function displayModList(listObjectName, folders, enabled)
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 @@ function getModColor(folder, enabled)
{
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 @@ function enableMod()
--pos;
displayModLists();
Engine.GetGUIObjectByName("message").caption = "";
modsDisabledList.selected = pos;
}
@ -230,7 +258,8 @@ function disableMod()
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 @@ function disableMod()
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 @@ function areDependenciesMet(folder, disabledAction = false)
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 @@ function isDependencyMet(dependency)
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 @@ function sortEnabledMods()
{
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 @@ function selectedMod(listObjectName)
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 @@ function getSelectedModUrl()
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()

View File

@ -216,5 +216,10 @@
<translatableAttribute id="caption">Start Mods</translatableAttribute>
<action on="Press">startMods();</action>
</object>
<object name="startButton" type="button" style="ModernButtonRed" size="100%-196 100%-44 100%-16 100%-16">
<translatableAttribute id="caption">Start</translatableAttribute>
<action on="Press">startMods();</action>
</object>
</object>
</objects>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/modern/setup.xml</include>
<include>common/modern/styles.xml</include>
<include>common/modern/sprites.xml</include>
<include>modmod/styles.xml</include>
<include>incompatible_mods/incompatible_mods.xml</include>
</page>

View File

@ -49,6 +49,7 @@ that of Atlas depending on commandline parameters.
#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 @@ static void RunGameOrAtlas(int argc, const char* argv[])
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 @@ static void RunGameOrAtlas(int argc, const char* argv[])
// Do not install mods again in case of restart (typically from the mod selector)
modsToInstall.clear();
Mod::ClearIncompatibleMods();
Shutdown(0);
MainControllerShutdown();

View File

@ -103,6 +103,7 @@ extern void RestartEngine();
#include <iostream>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/algorithm/string/split.hpp>
ERROR_GROUP(System);
@ -379,24 +380,6 @@ ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(
return ERI_NOT_IMPLEMENTED;
}
const std::vector<CStr>& 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<CStr>& mods)
{
OsPath modPath = paths.RData()/"mods";
@ -457,7 +440,7 @@ static void InitVfs(const CmdLineArgs& args, int flags)
// 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 Autostart(const CmdLineArgs& args);
*/
bool AutostartVisualReplay(const std::string& replayFile);
bool EnableModsOrSetDefault(const CmdLineArgs& args, int flags, const std::vector<CStr>& 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 @@ bool Init(const CmdLineArgs& args, int flags)
// 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<CStr> 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<CStr> 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;

View File

@ -19,8 +19,6 @@
#include "ps/Mod.h"
#include <algorithm>
#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 <algorithm>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <unordered_map>
std::vector<CStr> g_modsLoaded;
std::vector<CStr> g_incompatibleMods;
std::vector<CStr> g_failedMods;
std::vector<std::vector<CStr>> g_LoadedModVersions;
@ -105,6 +110,181 @@ JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface)
return JS::ObjectValue(*obj);
}
const std::vector<CStr>& Mod::GetEnabledMods()
{
return g_modsLoaded;
}
const std::vector<CStr>& Mod::GetIncompatibleMods()
{
return g_incompatibleMods;
}
const std::vector<CStr>& Mod::GetFailedMods()
{
return g_failedMods;
}
const std::vector<CStr>& 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<CStr>& 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<CStr>& mods, const JS::RootedValue& availableMods)
{
ScriptRequest rq(scriptInterface);
std::unordered_map<CStr, std::vector<CStr>> modDependencies;
std::unordered_map<CStr, CStr> 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;
}
std::vector<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);
modDependencies.emplace(mod, dependencies);
}
static const std::vector<CStr> toCheck = { "<=", ">=", "=", "<", ">" };
for (const CStr& mod : mods)
{
if (mod == "mod")
continue;
const std::unordered_map<CStr, std::vector<CStr>>::iterator res = modDependencies.find(mod);
if (res == modDependencies.end())
continue;
const std::vector<CStr> 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<CStr, CStr>::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<CStr> versionSplit;
std::vector<CStr> 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>& scriptContext)
{
ScriptInterface scriptInterface("Engine", "CacheEnabledModVersions", scriptContext);

View File

@ -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 @@ extern CmdLineArgs g_args;
namespace Mod
{
JS::Value GetAvailableMods(const ScriptInterface& scriptInterface);
const std::vector<CStr>& GetEnabledMods();
const std::vector<CStr>& GetIncompatibleMods();
const std::vector<CStr>& 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 @@ namespace Mod
*/
void CacheEnabledModVersions(const shared_ptr<ScriptContext>& scriptContext);
const std::vector<CStr>& GetModsFromArguments(const CmdLineArgs& args, int flags);
bool AreModsCompatible(const ScriptInterface& scriptInterface, const std::vector<CStr>& mods, const JS::RootedValue& availableMods);
bool CheckAndEnableMods(const ScriptInterface& scriptInterface, const std::vector<CStr>& 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.

View File

@ -27,36 +27,28 @@ extern void RestartEngine();
namespace
{
JS::Value GetEngineInfo(ScriptInterface::CmptPrivate* pCmptPrivate)
bool SetModsAndRestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector<CStr>& mods)
{
return Mod::GetEngineInfo(*(pCmptPrivate->pScriptInterface));
Mod::ClearIncompatibleMods();
if (!Mod::CheckAndEnableMods(*(pCmptPrivate->pScriptInterface), mods))
return false;
RestartEngine();
return true;
}
}
/**
* 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)
bool HasFailedMods(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
return Mod::GetAvailableMods(*(pCmptPrivate->pScriptInterface));
}
void SetMods(const std::vector<CStr>& mods)
{
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<HasFailedMods> (rq, "HasFailedMods");
ScriptFunction::Register<&Mod::GetFailedMods>(rq, "GetFailedMods");
ScriptFunction::Register<&SetModsAndRestartEngine>(rq, "SetModsAndRestartEngine");
}

165
source/ps/tests/test_Mod.h Normal file
View File

@ -0,0 +1,165 @@
/* 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<CStr> mods;
mods.clear();
mods.push_back("public");
Mod::ClearIncompatibleMods();
TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
mods.clear();
mods.push_back("mod");
mods.push_back("public");
Mod::ClearIncompatibleMods();
TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
mods.clear();
mods.push_back("public");
mods.push_back("good");
Mod::ClearIncompatibleMods();
TS_ASSERT(Mod::AreModsCompatible(script, mods, availableMods));
mods.clear();
mods.push_back("public");
mods.push_back("wrong");
Mod::ClearIncompatibleMods();
TS_ASSERT(!Mod::AreModsCompatible(script, mods, availableMods));
mods.clear();
mods.push_back("public");
mods.push_back("does_not_exist");
Mod::ClearIncompatibleMods();
TS_ASSERT(!Mod::AreModsCompatible(script, mods, availableMods));
}
};