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 @@ -127,8 +127,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() 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"/""); @@ -937,13 +907,28 @@ { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); - std::swap(g_modsLoaded, mods); + if (!Mod::CheckAndEnableMods(g_ScriptContext, mods)) + { + // Force default mods when current are not compatible + Mod::SetDefaultMods(g_ScriptContext, args, flags); + } // Abort init and restart RestartEngine(); return false; } } + else if (args.Has("mod")) + { + if (!Mod::CheckAndEnableMods(g_ScriptContext, g_modsLoaded)) + { + // Force default mods when current are not compatible + Mod::SetDefaultMods(g_ScriptContext, flags); + // Abort init and restart + RestartEngine(); + 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 @@ -39,6 +39,13 @@ */ 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); + void SetDefaultMods(const shared_ptr& scriptContext, 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; @@ -127,6 +130,179 @@ } } + +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 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; + + g_modsLoaded.clear(); + + 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; + } + + 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"); +} + +void Mod::SetDefaultMods(const shared_ptr& scriptContext, int flags) +{ + SetDefaultMods(scriptContext, CmdLineArgs(), flags); +} + +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); + + CStr engine = engine_version; + JS::RootedValue availableMods(rq.cx, GetAvailableMods(scriptInterface)); + std::map> modDependencies; + std::map map; + std::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::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::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; + boost::split(vSplit, version, boost::is_any_of("-"), boost::token_compress_on); + boost::split(vSplit, vSplit.at(0), boost::is_any_of("_"), boost::token_compress_on); + boost::split(tSplit, target, boost::is_any_of("-"), boost::token_compress_on); + boost::split(tSplit, tSplit.at(0), boost::is_any_of("_"), 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 @@ -28,7 +28,7 @@ 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); + 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 @@ -49,9 +49,13 @@ ::RestartEngine(); } -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)) { + ::RestartEngine(); + return true; + } + return false; } void JSI_Mod::RegisterScriptFunctions(const ScriptInterface& scriptInterface) @@ -59,5 +63,5 @@ scriptInterface.RegisterFunction("GetEngineInfo"); scriptInterface.RegisterFunction("GetAvailableMods"); scriptInterface.RegisterFunction("RestartEngine"); - scriptInterface.RegisterFunction, &JSI_Mod::SetMods>("SetMods"); + scriptInterface.RegisterFunction, &JSI_Mod::SetModsAndRestartEngine>("SetModsAndRestartEngine"); }