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 @@ -89,7 +89,7 @@ function loadEnabledMods() { - g_ModsEnabled = Engine.ConfigDB_GetValue("user", "mod.enabledmods").split(/\s+/).filter(folder => !!g_Mods[folder]); + 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; @@ -111,9 +111,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"); } @@ -127,8 +129,13 @@ function startMods() { sortEnabledMods(); - Engine.SetMods(["mod"].concat(g_ModsEnabled)); - Engine.RestartEngine(); + if (!g_ModsEnabled.length) + { + Engine.GetGUIObjectByName("message").caption = coloredText(translate('Enable at least one mod'), g_ColorDependenciesNotMet); + return; + } + if (!Engine.SetModsAndRestartEngine(["mod"].concat(g_ModsEnabled))) + Engine.GetGUIObjectByName("message").caption = coloredText(translate('Dependencies not met'), g_ColorDependenciesNotMet); } function displayModLists() 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 @@ -202,5 +202,10 @@ Start Mods startMods(); + + + Start + startMods(); + 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" @@ -594,7 +595,7 @@ Paths paths(args); g_VFS = CreateVfs(); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); - MountMods(paths, GetMods(args, INIT_MODS)); + MountMods(paths, Mod::GetModsFromArguments(args, INIT_MODS)); { CReplayPlayer replay; Index: source/ps/GameSetup/GameSetup.h =================================================================== --- source/ps/GameSetup/GameSetup.h +++ source/ps/GameSetup/GameSetup.h @@ -82,7 +82,6 @@ class CmdLineArgs; class Paths; -extern const std::vector& GetMods(const CmdLineArgs& args, int flags); /** * Mounts all files of the given mods in the global VFS. Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -370,36 +370,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_user = !InDevelopmentCopy() && !args.Has("noUserMod"); - const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; - - if (!init_mods) - { - // Add the user mod if it should be present - if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) - g_modsLoaded.push_back("user"); - - 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"); - - // Add the user mod if not explicitly disabled or we have a dev copy so - // that saved files end up in version control and not in the user mod. - if (add_user) - g_modsLoaded.push_back("user"); - - return g_modsLoaded; -} - void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; @@ -458,7 +428,7 @@ // Engine localization files. g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); - MountMods(paths, GetMods(args, flags)); + MountMods(paths, Mod::GetModsFromArguments(args, flags)); // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); @@ -869,6 +839,23 @@ */ bool AutostartVisualReplay(const std::string& replayFile); +bool EnableModsOrSetDefault(const CmdLineArgs& args, int flags, const std::vector& mods) +{ + if (Mod::CheckAndEnableMods(g_ScriptContext, mods)) + return true; + // Here we refuse to start as there is no gui anyway + if (args.Has("autostart-nonvisual")) + { + LOGERROR("Mods defined in config file are not compatible."); + return false; + } + // When mods are incompatible, we enforce default ones so user is able + // to start the game without wall of error strings + Mod::SetDefaultMods(g_ScriptContext, args, flags); + RestartEngine(); + return false; +} + bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); @@ -929,21 +916,27 @@ // 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 (!args.Has("mod")) { - 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); - std::swap(g_modsLoaded, mods); - - // Abort init and restart - RestartEngine(); - return false; + if ((flags & INIT_MODS) == INIT_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)) + return false; + + RestartEngine(); + return false; + } } } + else if (!EnableModsOrSetDefault(args, flags, g_modsLoaded)) + 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,7 @@ namespace Mod { JS::Value GetAvailableMods(const ScriptInterface& scriptInterface); - + std::vector GetEnabledMods(const ScriptInterface& scriptInterface); /** * This reads the version numbers from the launched mods. * It caches the result, since the reading of zip files is slow and @@ -39,6 +39,12 @@ */ void CacheEnabledModVersions(const shared_ptr& scriptContext); + const std::vector& GetModsFromArguments(const CmdLineArgs& args, int flags); + bool CheckAndEnableMods(const shared_ptr& scriptContext, const std::vector& mods); + bool CheckAndEnableMods(const ScriptInterface& scriptInterface, const std::vector& mods); + bool CompareVersionStrings(const CStr& version, const CStr& op, const CStr& target); + void SetDefaultMods(const shared_ptr& scriptContext, const CmdLineArgs& args, int flags); + /** * 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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 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 @@ -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,6 +28,11 @@ #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" +#include +#include +#include +#include + std::vector g_modsLoaded; std::vector> g_LoadedModVersions; @@ -103,6 +106,11 @@ return JS::ObjectValue(*obj); } +std::vector Mod::GetEnabledMods(const ScriptInterface& scriptInterface) +{ + return g_modsLoaded; +} + void Mod::CacheEnabledModVersions(const shared_ptr& scriptContext) { ScriptInterface scriptInterface("Engine", "CacheEnabledModVersions", scriptContext); @@ -127,6 +135,160 @@ } } + +const std::vector& Mod::GetModsFromArguments(const CmdLineArgs& args, int flags) +{ + const bool init_mods = (flags & INIT_MODS) == INIT_MODS; + const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); + const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; + + if (!init_mods) + { + // Add the user mod if it should be present + if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) + g_modsLoaded.push_back("user"); + + 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"); + + // Add the user mod if not explicitly disabled or we have a dev copy so + // that saved files end up in version control and not in the user mod. + if (add_user) + g_modsLoaded.push_back("user"); + + return g_modsLoaded; +} + +void Mod::SetDefaultMods(const shared_ptr& scriptContext, const CmdLineArgs& args, int flags) +{ + const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); + + g_modsLoaded.clear(); + + g_modsLoaded.insert(g_modsLoaded.begin(), "public"); + g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); + + // Add the user mod if not explicitly disabled or we have a dev copy so + // that saved files end up in version control and not in the user mod. + if (add_user) + g_modsLoaded.push_back("user"); +} + +bool Mod::CheckAndEnableMods(const shared_ptr& scriptContext, const std::vector& mods) +{ + ScriptInterface scriptInterface("Engine", "CheckAndEnableMods", scriptContext); + return Mod::CheckAndEnableMods(scriptInterface, mods); +} + +bool Mod::CheckAndEnableMods(const ScriptInterface& scriptInterface, const std::vector& mods) +{ + ScriptRequest rq(scriptInterface); + + JS::RootedValue availableMods(rq.cx, GetAvailableMods(scriptInterface)); + std::unordered_map> modDependencies; + std::unordered_map map; + std::unordered_map names; + for (const CStr& mod : mods) + { + if (mod == "mod" || mod == "user") + continue; + + CStr dependencies; + CStr version; + CStr name; + JS::RootedValue modData(rq.cx); + if (scriptInterface.GetProperty(availableMods, mod.c_str(), &modData)) + { + scriptInterface.GetProperty(modData, "dependencies", dependencies); + scriptInterface.GetProperty(modData, "version", version); + scriptInterface.GetProperty(modData, "name", name); + } + map.emplace(name, version); + std::vector deps; + boost::split(deps, dependencies, boost::is_any_of(","), boost::token_compress_on); + modDependencies.emplace(mod, deps); + } + + const std::vector toCheck = { "<=", ">=", "=", "<", ">" }; + for (const CStr& mod : mods) + { + if (mod == "mod" || mod == "user") + 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; + + for (const CStr& op : toCheck) + { + const int pos = dep.Find(op.c_str()); + if (pos == -1) + continue; + const CStr modToCheck = dep.substr(0, pos); + const CStr versionToCheck = dep.substr(pos + op.size()); + const std::unordered_map::iterator it = map.find(modToCheck); + if (it == map.end()) + return false; + if (!CompareVersionStrings(versionToCheck, op, it->second)) + return false; + break; + } + } + + } + + g_modsLoaded = mods; + return true; +} + +bool Mod::CompareVersionStrings(const CStr& target, const CStr& op, const CStr& version) +{ + std::vector vSplit; + std::vector tSplit; + std::string toIgnore = "-,_"; + boost::split(vSplit, version, boost::is_any_of(toIgnore), boost::token_compress_on); + boost::split(tSplit, target, boost::is_any_of(toIgnore), boost::token_compress_on); + boost::split(vSplit, vSplit.at(0), boost::is_any_of("."), boost::token_compress_on); + boost::split(tSplit, tSplit.at(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 int min = std::min(vSplit.size(), tSplit.size()); + + for (int i = 0; i < min; ++i) + { + const int diff = vSplit.at(i).ToInt() - tSplit.at(i).ToInt(); + if (gt && diff > 0 || lt && diff < 0) + return true; + + if (gt && diff < 0 || lt && diff > 0 || eq && diff) + return false; + } + + const int diff = vSplit.size() - tSplit.size(); + if (diff == 0) + return eq; + + return diff < 0 ? lt : gt; +} + JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); Index: source/ps/scripting/JSInterface_Mod.h =================================================================== --- source/ps/scripting/JSInterface_Mod.h +++ source/ps/scripting/JSInterface_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 @@ -27,8 +27,8 @@ JS::Value GetEngineInfo(ScriptInterface::CmptPrivate* pCmptPrivate); JS::Value GetAvailableMods(ScriptInterface::CmptPrivate* pCmptPrivate); - void RestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetMods(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods); + std::vector GetEnabledMods(ScriptInterface::CmptPrivate* pCmptPrivate); + bool SetModsAndRestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods); } #endif // INCLUDED_JSI_MOD Index: source/ps/scripting/JSInterface_Mod.cpp =================================================================== --- source/ps/scripting/JSInterface_Mod.cpp +++ source/ps/scripting/JSInterface_Mod.cpp @@ -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 @@ -44,20 +44,27 @@ return Mod::GetAvailableMods(*(pCmptPrivate->pScriptInterface)); } -void JSI_Mod::RestartEngine(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +/** + * Returns a vector of currently enabled mods. + */ +std::vector JSI_Mod::GetEnabledMods(ScriptInterface::CmptPrivate* pCmptPrivate) { - ::RestartEngine(); + return Mod::GetEnabledMods(*(pCmptPrivate->pScriptInterface)); } -void JSI_Mod::SetMods(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::vector& mods) +bool JSI_Mod::SetModsAndRestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods) { - g_modsLoaded = mods; + if (!Mod::CheckAndEnableMods(*(pCmptPrivate->pScriptInterface), mods)) + return false; + + ::RestartEngine(); + return true; } void JSI_Mod::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetEngineInfo"); scriptInterface.RegisterFunction("GetAvailableMods"); - scriptInterface.RegisterFunction("RestartEngine"); - scriptInterface.RegisterFunction, &JSI_Mod::SetMods>("SetMods"); + scriptInterface.RegisterFunction, &JSI_Mod::GetEnabledMods>("GetEnabledMods"); + scriptInterface.RegisterFunction, &JSI_Mod::SetModsAndRestartEngine>("SetModsAndRestartEngine"); }