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: binaries/data/mods/public/gui/pregame/MainMenuPage.js =================================================================== --- binaries/data/mods/public/gui/pregame/MainMenuPage.js +++ binaries/data/mods/public/gui/pregame/MainMenuPage.js @@ -7,17 +7,20 @@ { this.backgroundHandler = new BackgroundHandler(pickRandom(backgroundLayerData)); this.menuHandler = new MainMenuItemHandler(mainMenuItems); + this.incompatibleModsHanlder = new IncompatibleModsHandler(data, hotloadData && hotloadData.incompatibleModsHanlder); this.splashScreenHandler = new SplashScreenHandler(data, hotloadData && hotloadData.splashScreenHandler); new MusicHandler(); new ProjectInformationHandler(projectInformation); new CommunityButtonHandler(communityButtons); + warn(uneval(data)); } getHotloadData() { return { - "splashScreenHandler": this.splashScreenHandler.getHotloadData() + "splashScreenHandler": this.splashScreenHandler.getHotloadData(), + "incompatibleModsHanlder": this.incompatibleModsHanlder.getHotloadData() }; } } Index: binaries/data/mods/public/gui/pregame/SplashscreenHandler.js =================================================================== --- binaries/data/mods/public/gui/pregame/SplashscreenHandler.js +++ binaries/data/mods/public/gui/pregame/SplashscreenHandler.js @@ -2,10 +2,13 @@ { constructor(initData, hotloadData) { - this.showSplashScreen = hotloadData ? hotloadData.showSplashScreen : initData && initData.isStartup; + this.showSplashScreen = hotloadData ? hotloadData.showSplashScreen && !hotloadData.incompatibleMods : initData && initData.isStartup && !initData.incompatibleMods; - this.mainMenuPage = Engine.GetGUIObjectByName("mainMenuPage"); - this.mainMenuPage.onTick = this.onFirstTick.bind(this); + if (this.showSplashScreen) + { + this.mainMenuPage = Engine.GetGUIObjectByName("mainMenuPage"); + this.mainMenuPage.onTick = this.onFirstTick.bind(this); + } } getHotloadData() 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 @@ -57,7 +57,10 @@ // mount the public mod // needed by the map editor as "mod" does not provide everything it needs - INIT_MODS_PUBLIC = 16 + INIT_MODS_PUBLIC = 16, + + // inform user about incompatible mods + INIT_MODS_INFORM_INCOMPATIBLE = 32 }; enum ShutdownFlags @@ -82,7 +85,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. @@ -93,7 +95,7 @@ * Returns true if successful, false if mods changed and restart_engine was called. * In the latter case the caller should call Shutdown() with SHUTDOWN_FROM_CONFIG. */ -extern bool Init(const CmdLineArgs& args, int flags); +extern bool Init(const CmdLineArgs& args, int& flags); extern void InitInput(); extern void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods = std::vector()); extern void InitNonVisual(const CmdLineArgs& args); Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -372,36 +372,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"; @@ -460,7 +430,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"/""); @@ -871,7 +841,29 @@ */ bool AutostartVisualReplay(const std::string& replayFile); -bool Init(const CmdLineArgs& args, int flags) +bool EnableModsOrSetDefault(const CmdLineArgs& args, int flags, const std::vector& mods, bool fromConfig) +{ + if (Mod::CheckAndEnableMods(g_ScriptContext, mods)) + return true; + // Here we refuse to start as there is no gui anyway + if (args.Has("autostart-nonvisual")) + { + if (fromConfig) + LOGERROR("Mods defined in config file are not compatible."); + else + LOGERROR("Trying to start with incompatible mods."); + return false; + } + // When mods are incompatible, we enforce default ones so user is able + // to start the game without wall of error strings + if (!fromConfig) + LOGERROR("Trying to start with incompatible mods. Setting default mods."); + Mod::SetDefaultMods(g_ScriptContext, args, flags); + RestartEngine(); + return false; +} + +bool Init(const CmdLineArgs& args, int& flags) { h_mgr_init(); @@ -931,21 +923,30 @@ // 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, true)) + { + flags &= INIT_MODS_INFORM_INCOMPATIBLE; + return false; + } + + RestartEngine(); + return false; + } } } + else if (!EnableModsOrSetDefault(args, flags, g_modsLoaded, false)) + return false; new L10n; @@ -1078,6 +1079,8 @@ ScriptInterface::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) scriptInterface->SetProperty(data, "installedMods", installedMods); + if ((flags & INIT_MODS_INFORM_INCOMPATIBLE) == 0) + scriptInterface->SetProperty(data, "incompatibleMods", true); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } 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.cpp =================================================================== --- source/ps/scripting/JSInterface_Mod.cpp +++ source/ps/scripting/JSInterface_Mod.cpp @@ -47,9 +47,21 @@ return Mod::GetAvailableMods(*(pCmptPrivate->pScriptInterface)); } -void SetMods(const std::vector& mods) +/** + * Returns a vector of currently enabled mods. + */ +std::vector GetEnabledMods(ScriptInterface::CmptPrivate* pCmptPrivate) +{ + return Mod::GetEnabledMods(*(pCmptPrivate->pScriptInterface)); +} + +bool SetModsAndRestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods) { - g_modsLoaded = mods; + if (!Mod::CheckAndEnableMods(*(pCmptPrivate->pScriptInterface), mods)) + return false; + + ::RestartEngine(); + return true; } } @@ -57,6 +69,6 @@ { ScriptFunction::Register<&GetEngineInfo>(rq, "GetEngineInfo"); ScriptFunction::Register<&GetAvailableMods>(rq, "GetAvailableMods"); - ScriptFunction::Register<&RestartEngine>(rq, "RestartEngine"); - ScriptFunction::Register<&SetMods>(rq, "SetMods"); + ScriptFunction::Register<&GetEnabledMods>(rq, "GetEnabledMods"); + ScriptFunction::Register<&SetModsAndRestartEngine>(rq, "SetModsAndRestartEngine"); } Index: source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp =================================================================== --- source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp +++ source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp @@ -58,12 +58,16 @@ g_Quickstart = true; // Mount mods if there are any specified as command line parameters - if (!Init(g_AtlasGameLoop->args, g_InitFlags | INIT_MODS|INIT_MODS_PUBLIC)) + int flags = g_InitFlags | INIT_MODS | INIT_MODS_PUBLIC; + if (!Init(g_AtlasGameLoop->args, flags)) { // There are no mods specified on the command line, // but there are in the config file, so mount those. Shutdown(SHUTDOWN_FROM_CONFIG); - ENSURE(Init(g_AtlasGameLoop->args, g_InitFlags)); + //TODO: Init might set INIT_MODS_INFORM_INCOMPATIBLE flag, + // show that information in atlas + int initFlags = g_InitFlags; + ENSURE(Init(g_AtlasGameLoop->args, initFlags)); } // Initialise some graphics state for Atlas.