From d5afe8de5450f2c7da611f6c36a91c8b0d68da39 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 26 Nov 2025 21:04:47 +0200 Subject: [PATCH] Fix chain disabling of mods with soft dependencies Happens if: - Main mod that player attempts to disable has submods - There is another mod that has soft-depend on such submod This leads to scenario where submod is never added to load order, and neither is another mod since it can only be added after submod. Now mods (including submods) with known missing dependencies will be dropped from load order immediately, allowing mods with soft-depends on them to be added to load order --- lib/modding/ModManager.cpp | 44 +++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/modding/ModManager.cpp b/lib/modding/ModManager.cpp index 01e961197..9a6bc11e9 100644 --- a/lib/modding/ModManager.cpp +++ b/lib/modding/ModManager.cpp @@ -767,35 +767,45 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora std::set resolvedModIDs(activeMods.begin(), activeMods.end()); // Use a set for validation for performance reason, but set does not keep order of elements std::set notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason + enum class ModResolveStatus { + RESOLVED, // ok - mod can be added to load order + WAITING, // maybe - wait for more iterations before deciding + BROKEN // fail - this mod definitely can't be loaded + }; + // Mod is resolved if it has no dependencies or all its dependencies are already resolved - auto isResolved = [&](const ModDescription & mod) -> bool + auto isResolved = [&](const ModDescription & mod) -> ModResolveStatus { if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage()) - return false; + return ModResolveStatus::BROKEN; if(!mod.isCompatible()) - return false; - - if(mod.getDependencies().size() > resolvedModIDs.size()) - return false; + return ModResolveStatus::BROKEN; for(const TModID & dependency : mod.getDependencies()) + { if(!vstd::contains(resolvedModIDs, dependency)) - return false; + { + if (vstd::contains(notResolvedModIDs, dependency)) + return ModResolveStatus::WAITING; + else + return ModResolveStatus::BROKEN; + } + } for(const TModID & softDependency : mod.getSoftDependencies()) if(vstd::contains(notResolvedModIDs, softDependency)) - return false; + return ModResolveStatus::WAITING; for(const TModID & conflict : mod.getConflicts()) if(vstd::contains(resolvedModIDs, conflict)) - return false; + return ModResolveStatus::BROKEN; for(const TModID & reverseConflict : resolvedModIDs) if(vstd::contains(storage.getMod(reverseConflict).getConflicts(), mod.getID())) - return false; + return ModResolveStatus::BROKEN; - return true; + return ModResolveStatus::RESOLVED; }; while(true) @@ -803,7 +813,9 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora std::set resolvedOnCurrentTreeLevel; for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree { - if(isResolved(storage.getMod(*it))) + ModResolveStatus status = isResolved(storage.getMod(*it)); + + if (status == ModResolveStatus::RESOLVED) { resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration assert(!vstd::contains(sortedValidMods, *it)); @@ -811,6 +823,14 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora it = modsToResolve.erase(it); continue; } + if (status == ModResolveStatus::BROKEN) + { + resolvedOnCurrentTreeLevel.insert(*it); + brokenMods.push_back(*it); + it = modsToResolve.erase(it); + continue; + } + it++; } if(!resolvedOnCurrentTreeLevel.empty())