From 2fcda48c65d68f8e91dc3c99c6d1c24724036e27 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 14 Nov 2024 20:56:19 +0000 Subject: [PATCH] Implemented enabling and disabling of mods with dependencies resolving --- launcher/modManager/cmodlistview_moc.cpp | 47 ++---- launcher/modManager/modstate.cpp | 2 +- launcher/modManager/modstatecontroller.cpp | 34 +--- launcher/modManager/modstateitemmodel_moc.h | 4 +- launcher/modManager/modstatemodel.cpp | 15 +- launcher/modManager/modstatemodel.h | 4 +- lib/modding/ModManager.cpp | 165 +++++++++++++++++--- lib/modding/ModManager.h | 32 +++- 8 files changed, 201 insertions(+), 102 deletions(-) diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 01dd1d88f..240424344 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -446,8 +446,10 @@ void CModListView::selectMod(const QModelIndex & index) Helper::enableScrollBySwiping(ui->modInfoBrowser); Helper::enableScrollBySwiping(ui->changelogBrowser); + //FIXME: this function should be recursive + //FIXME: ensure that this is also reflected correctly in "Notes" section of mod description bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); - bool hasBlockingMods = !findBlockingMods(modName).empty(); + //bool hasBlockingMods = !findBlockingMods(modName).empty(); bool hasDependentMods = !findDependentMods(modName, true).empty(); ui->disableButton->setVisible(modStateModel->isModEnabled(mod.getID())); @@ -459,8 +461,8 @@ void CModListView::selectMod(const QModelIndex & index) // Block buttons if action is not allowed at this time // TODO: automate handling of some of these cases instead of forcing player // to resolve all conflicts manually. - ui->disableButton->setEnabled(!hasDependentMods && !mod.isHidden()); - ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps); + ui->disableButton->setEnabled(true); + ui->enableButton->setEnabled(!hasInvalidDeps); ui->installButton->setEnabled(!hasInvalidDeps); ui->uninstallButton->setEnabled(!hasDependentMods && !mod.isHidden()); ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods); @@ -564,16 +566,8 @@ void CModListView::on_enableButton_clicked() void CModListView::enableModByName(QString modName) { - assert(findBlockingMods(modName).empty()); - assert(findInvalidDependencies(modName).empty()); - - auto mod = modStateModel->getMod(modName); - - for(const auto & name : mod.getDependencies()) - { - if(!modStateModel->isModEnabled(name)) - manager->enableMod(name); - } + manager->enableMod(modName); + modModel->reloadRepositories(); } void CModListView::on_disableButton_clicked() @@ -587,8 +581,8 @@ void CModListView::on_disableButton_clicked() void CModListView::disableModByName(QString modName) { - if(modStateModel->isModExists(modName) && modStateModel->isModEnabled(modName)) - manager->disableMod(modName); + manager->disableMod(modName); + modModel->reloadRepositories(); } void CModListView::on_updateButton_clicked() @@ -943,30 +937,9 @@ void CModListView::installMods(QStringList archives) manager->installMod(modNames[i], archives[i]); } - std::function enableMod; - - enableMod = [&](QString modName) - { - auto mod = modStateModel->getMod(modName); - if(mod.isInstalled() && !mod.isKeptDisabled()) - { - for(const auto & dependencyName : mod.getDependencies()) - { - if(!modStateModel->isModEnabled(dependencyName)) - manager->enableMod(dependencyName); - } - - if(!modStateModel->isModEnabled(modName) && manager->enableMod(modName)) - { - for(QString child : modStateModel->getSubmods(modName)) - enableMod(child); - } - } - }; - for(QString mod : modsToEnable) { - enableMod(mod); + manager->enableMod(mod); // TODO: make it as a single action, so if mod 1 depends on mod 2 it would still activate } checkManagerErrors(); diff --git a/launcher/modManager/modstate.cpp b/launcher/modManager/modstate.cpp index 27144bf4d..796ed2336 100644 --- a/launcher/modManager/modstate.cpp +++ b/launcher/modManager/modstate.cpp @@ -47,7 +47,7 @@ QString ModState::getParentID() const QString ModState::getTopParentID() const { - return QString::fromStdString(impl.getParentID()); + return QString::fromStdString(impl.getParentID()); // TODO } template diff --git a/launcher/modManager/modstatecontroller.cpp b/launcher/modManager/modstatecontroller.cpp index b88826a22..8c8676429 100644 --- a/launcher/modManager/modstatecontroller.cpp +++ b/launcher/modManager/modstatecontroller.cpp @@ -152,28 +152,8 @@ bool ModStateController::canEnableMod(QString modname) { if(!modList->isModExists(modEntry)) // required mod is not available return addError(modname, tr("Required mod %1 is missing").arg(modEntry)); - - ModState modData = modList->getMod(modEntry); - - if(!modData.isCompatibility() && !modList->isModEnabled(modEntry)) - return addError(modname, tr("Required mod %1 is not enabled").arg(modEntry)); } - for(QString modEntry : modList->getAllMods()) - { - auto otherMod = modList->getMod(modEntry); - - // "reverse conflict" - enabled mod has this one as conflict - if(modList->isModEnabled(modname) && otherMod.getConflicts().contains(modname)) - return addError(modname, tr("This mod conflicts with %1").arg(modEntry)); - } - - for(const auto & modEntry : mod.getConflicts()) - { - // check if conflicting mod installed and enabled - if(modList->isModExists(modEntry) && modList->isModEnabled(modEntry)) - return addError(modname, tr("This mod conflicts with %1").arg(modEntry)); - } return true; } @@ -187,20 +167,16 @@ bool ModStateController::canDisableMod(QString modname) if(!mod.isInstalled()) return addError(modname, tr("Mod must be installed first")); - for(QString modEntry : modList->getAllMods()) - { - auto current = modList->getMod(modEntry); - - if(current.getDependencies().contains(modname) && modList->isModEnabled(modEntry)) - return addError(modname, tr("This mod is needed to run %1").arg(modEntry)); - } return true; } bool ModStateController::doEnableMod(QString mod, bool on) { - //modSettings->setModActive(mod, on); - //modList->modChanged(mod); + if (on) + modList->doEnableMod(mod); + else + modList->doDisableMod(mod); + return true; } diff --git a/launcher/modManager/modstateitemmodel_moc.h b/launcher/modManager/modstateitemmodel_moc.h index 529a461be..af29dbce1 100644 --- a/launcher/modManager/modstateitemmodel_moc.h +++ b/launcher/modManager/modstateitemmodel_moc.h @@ -46,7 +46,7 @@ enum EModRoles }; } -class ModStateItemModel : public QAbstractItemModel +class ModStateItemModel final : public QAbstractItemModel { friend class CModFilterModel; Q_OBJECT @@ -87,7 +87,7 @@ public: Qt::ItemFlags flags(const QModelIndex & index) const override; }; -class CModFilterModel : public QSortFilterProxyModel +class CModFilterModel final : public QSortFilterProxyModel { ModStateItemModel * base; ModFilterMask filterMask; diff --git a/launcher/modManager/modstatemodel.cpp b/launcher/modManager/modstatemodel.cpp index f3413d848..31f2975e7 100644 --- a/launcher/modManager/modstatemodel.cpp +++ b/launcher/modManager/modstatemodel.cpp @@ -53,11 +53,6 @@ QStringList ModStateModel::getAllMods() const return stringListStdToQt(modManager->getAllMods()); } -QStringList ModStateModel::getSubmods(QString modName) const -{ - return {}; //TODO -} - bool ModStateModel::isModExists(QString modName) const { return vstd::contains(modManager->getAllMods(), modName.toStdString()); @@ -92,3 +87,13 @@ double ModStateModel::getInstalledModSizeMegabytes(QString modName) const { return modManager->getInstalledModSizeMegabytes(modName.toStdString()); } + +void ModStateModel::doEnableMod(QString modname) +{ + modManager->tryEnableMod(modname.toStdString()); +} + +void ModStateModel::doDisableMod(QString modname) +{ + modManager->tryDisableMod(modname.toStdString()); +} diff --git a/launcher/modManager/modstatemodel.h b/launcher/modManager/modstatemodel.h index 6046a160e..750fd2297 100644 --- a/launcher/modManager/modstatemodel.h +++ b/launcher/modManager/modstatemodel.h @@ -32,7 +32,6 @@ public: ModState getMod(QString modName) const; QStringList getAllMods() const; - QStringList getSubmods(QString modName) const; QString getInstalledModSizeFormatted(QString modName) const; double getInstalledModSizeMegabytes(QString modName) const; @@ -42,4 +41,7 @@ public: bool isModEnabled(QString modName) const; bool isModUpdateAvailable(QString modName) const; bool isModVisible(QString modName) const; + + void doEnableMod(QString modname); + void doDisableMod(QString modname); }; diff --git a/lib/modding/ModManager.cpp b/lib/modding/ModManager.cpp index 5315b4de4..f7e4602c3 100644 --- a/lib/modding/ModManager.cpp +++ b/lib/modding/ModManager.cpp @@ -172,8 +172,6 @@ ModsPresetState::ModsPresetState() createInitialPreset(); // new install else importInitialPreset(); // 1.5 format import - - saveConfigurationState(); } } @@ -229,7 +227,35 @@ std::optional ModsPresetState::getValidatedChecksum(const TModID & mod return node.Integer(); } -void ModsPresetState::setSettingActiveInPreset(const TModID & modName, const TModID & settingName, bool isActive) +void ModsPresetState::setModActive(const TModID & modID, bool isActive) +{ + size_t dotPos = modID.find('.'); + + if(dotPos != std::string::npos) + { + std::string rootMod = modID.substr(0, dotPos); + std::string settingID = modID.substr(dotPos + 1); + setSettingActive(rootMod, settingID, isActive); + } + else + { + if (isActive) + addRootMod(modID); + else + eraseRootMod(modID); + } +} + +void ModsPresetState::addRootMod(const TModID & modName) +{ + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + + if (!vstd::contains(currentPreset["mods"].Vector(), JsonNode(modName))) + currentPreset["mods"].Vector().emplace_back(modName); +} + +void ModsPresetState::setSettingActive(const TModID & modName, const TModID & settingName, bool isActive) { const std::string & currentPresetName = modConfig["activePreset"].String(); JsonNode & currentPreset = modConfig["presets"][currentPresetName]; @@ -237,16 +263,18 @@ void ModsPresetState::setSettingActiveInPreset(const TModID & modName, const TMo currentPreset["settings"][modName][settingName].Bool() = isActive; } -void ModsPresetState::eraseModInAllPresets(const TModID & modName) +void ModsPresetState::eraseRootMod(const TModID & modName) { - for (auto & preset : modConfig["presets"].Struct()) - vstd::erase(preset.second["mods"].Vector(), JsonNode(modName)); + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + vstd::erase(currentPreset["mods"].Vector(), JsonNode(modName)); } -void ModsPresetState::eraseModSettingInAllPresets(const TModID & modName, const TModID & settingName) +void ModsPresetState::eraseModSetting(const TModID & modName, const TModID & settingName) { - for (auto & preset : modConfig["presets"].Struct()) - preset.second["settings"][modName].Struct().erase(modName); + const std::string & currentPresetName = modConfig["activePreset"].String(); + JsonNode & currentPreset = modConfig["presets"][currentPresetName]; + currentPreset["settings"][modName].Struct().erase(modName); } std::vector ModsPresetState::getActiveMods() const @@ -344,7 +372,7 @@ ModManager::ModManager(const JsonNode & repositoryList) addNewModsToPreset(); std::vector desiredModList = modsPreset->getActiveMods(); - generateLoadOrder(desiredModList); + depedencyResolver = std::make_unique(desiredModList, *modsStorage); } ModManager::~ModManager() = default; @@ -357,12 +385,12 @@ const ModDescription & ModManager::getModDescription(const TModID & modID) const bool ModManager::isModActive(const TModID & modID) const { - return vstd::contains(activeMods, modID); + return vstd::contains(getActiveMods(), modID); } const TModList & ModManager::getActiveMods() const { - return activeMods; + return depedencyResolver->getActiveMods(); } uint32_t ModManager::computeChecksum(const TModID & modName) const @@ -404,7 +432,7 @@ void ModManager::eraseMissingModsFromPreset() { if(!vstd::contains(installedMods, rootMod)) { - modsPreset->eraseModInAllPresets(rootMod); + modsPreset->eraseRootMod(rootMod); continue; } @@ -415,7 +443,7 @@ void ModManager::eraseMissingModsFromPreset() TModID fullModID = rootMod + '.' + modSetting.first; if(!vstd::contains(installedMods, fullModID)) { - modsPreset->eraseModSettingInAllPresets(rootMod, modSetting.first); + modsPreset->eraseModSetting(rootMod, modSetting.first); continue; } } @@ -439,17 +467,110 @@ void ModManager::addNewModsToPreset() const auto & modSettings = modsPreset->getModSettings(rootMod); if (!modSettings.count(settingID)) - modsPreset->setSettingActiveInPreset(rootMod, settingID, modsStorage->getMod(modID).keepDisabled()); + modsPreset->setSettingActive(rootMod, settingID, modsStorage->getMod(modID).keepDisabled()); } } -void ModManager::generateLoadOrder(std::vector modsToResolve) +TModList ModManager::collectDependenciesRecursive(const TModID & modID) const +{ + TModList result; + TModList toTest; + + toTest.push_back(modID); + while (!toTest.empty()) + { + TModID currentMod = toTest.back(); + toTest.pop_back(); + result.push_back(currentMod); + + for (const auto & dependency : getModDescription(currentMod).getDependencies()) + { + if (!vstd::contains(result, dependency)) + toTest.push_back(dependency); + } + } + + return result; +} + +void ModManager::tryEnableMod(const TModID & modName) +{ + auto requiredActiveMods = collectDependenciesRecursive(modName); + auto additionalActiveMods = getActiveMods(); + + assert(!vstd::contains(additionalActiveMods, modName)); + + ModDependenciesResolver testResolver(requiredActiveMods, *modsStorage); + assert(testResolver.getBrokenMods().empty()); + assert(vstd::contains(testResolver.getActiveMods(), modName)); + + testResolver.tryAddMods(additionalActiveMods, *modsStorage); + + if (!vstd::contains(testResolver.getActiveMods(), modName)) + { + // FIXME: report? + return; + } + + updatePreset(testResolver); +} + +void ModManager::tryDisableMod(const TModID & modName) +{ + auto desiredActiveMods = getActiveMods(); + assert(vstd::contains(desiredActiveMods, modName)); + + vstd::erase(desiredActiveMods, modName); + + ModDependenciesResolver testResolver(desiredActiveMods, *modsStorage); + + if (vstd::contains(testResolver.getActiveMods(), modName)) + { + // FIXME: report? + return; + } + + updatePreset(testResolver); +} + +void ModManager::updatePreset(const ModDependenciesResolver & testResolver) +{ + const auto & newActiveMods = testResolver.getActiveMods(); + const auto & newBrokenMods = testResolver.getBrokenMods(); + + for (const auto & modID : newActiveMods) + modsPreset->setModActive(modID, true); + + for (const auto & modID : newBrokenMods) + modsPreset->setModActive(modID, false); + + std::vector desiredModList = modsPreset->getActiveMods(); + depedencyResolver = std::make_unique(desiredModList, *modsStorage); + + modsPreset->saveConfigurationState(); +} + +ModDependenciesResolver::ModDependenciesResolver(const TModList & modsToResolve, const ModsStorage & storage) +{ + tryAddMods(modsToResolve, storage); +} + +const TModList & ModDependenciesResolver::getActiveMods() const +{ + return activeMods; +} + +const TModList & ModDependenciesResolver::getBrokenMods() const +{ + return brokenMods; +} + +void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStorage & storage) { // Topological sort algorithm. boost::range::sort(modsToResolve); // Sort mods per name - std::vector sortedValidMods; // Vector keeps order of elements (LIFO) - sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation - std::set resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements + std::vector sortedValidMods(activeMods.begin(), activeMods.end()); // Vector keeps order of elements (LIFO) + 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 // Mod is resolved if it has no dependencies or all its dependencies are already resolved @@ -474,7 +595,7 @@ void ModManager::generateLoadOrder(std::vector modsToResolve) return false; for(const TModID & reverseConflict : resolvedModIDs) - if(vstd::contains(modsStorage->getMod(reverseConflict).getConflicts(), mod.getID())) + if(vstd::contains(storage.getMod(reverseConflict).getConflicts(), mod.getID())) return false; return true; @@ -485,7 +606,7 @@ void ModManager::generateLoadOrder(std::vector modsToResolve) std::set resolvedOnCurrentTreeLevel; for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree { - if(isResolved(modsStorage->getMod(*it))) + if(isResolved(storage.getMod(*it))) { resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration sortedValidMods.push_back(*it); @@ -507,7 +628,7 @@ void ModManager::generateLoadOrder(std::vector modsToResolve) assert(!sortedValidMods.empty()); activeMods = sortedValidMods; - brokenMods = modsToResolve; + brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end()); } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModManager.h b/lib/modding/ModManager.h index dc7e9b284..5ba5c8e0d 100644 --- a/lib/modding/ModManager.h +++ b/lib/modding/ModManager.h @@ -50,9 +50,13 @@ class ModsPresetState : boost::noncopyable public: ModsPresetState(); - void setSettingActiveInPreset(const TModID & modName, const TModID & settingName, bool isActive); - void eraseModInAllPresets(const TModID & modName); - void eraseModSettingInAllPresets(const TModID & modName, const TModID & settingName); + void setModActive(const TModID & modName, bool isActive); + + void addRootMod(const TModID & modName); + void eraseRootMod(const TModID & modName); + + void setSettingActive(const TModID & modName, const TModID & settingName, bool isActive); + void eraseModSetting(const TModID & modName, const TModID & settingName); /// Returns list of all mods active in current preset. Mod order is unspecified TModList getActiveMods() const; @@ -81,8 +85,7 @@ public: TModList getAllMods() const; }; -/// Provides public interface to access mod state -class DLL_LINKAGE ModManager : boost::noncopyable +class ModDependenciesResolver : boost::noncopyable { /// all currently active mods, in their load order TModList activeMods; @@ -90,13 +93,29 @@ class DLL_LINKAGE ModManager : boost::noncopyable /// Mods from current preset that failed to load due to invalid dependencies TModList brokenMods; +public: + ModDependenciesResolver(const TModList & modsToResolve, const ModsStorage & storage); + + void tryAddMods(TModList modsToResolve, const ModsStorage & storage); + + const TModList & getActiveMods() const; + const TModList & getBrokenMods() const; +}; + +/// Provides public interface to access mod state +class DLL_LINKAGE ModManager : boost::noncopyable +{ std::unique_ptr modsState; std::unique_ptr modsPreset; std::unique_ptr modsStorage; + std::unique_ptr depedencyResolver; void generateLoadOrder(TModList desiredModList); void eraseMissingModsFromPreset(); void addNewModsToPreset(); + void updatePreset(const ModDependenciesResolver & newData); + + TModList collectDependenciesRecursive(const TModID & modID) const; public: ModManager(const JsonNode & repositoryList); @@ -113,6 +132,9 @@ public: void setValidatedChecksum(const TModID & modName, std::optional value); void saveConfigurationState() const; double getInstalledModSizeMegabytes(const TModID & modName) const; + + void tryEnableMod(const TModID & modName); + void tryDisableMod(const TModID & modName); }; VCMI_LIB_NAMESPACE_END