Bläddra i källkod

Implemented enabling and disabling of mods with dependencies resolving

Ivan Savenko 11 månader sedan
förälder
incheckning
2fcda48c65

+ 10 - 37
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<void(QString)> 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();

+ 1 - 1
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<typename Container>

+ 5 - 29
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;
 }
 

+ 2 - 2
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;

+ 10 - 5
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());
+}

+ 3 - 1
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);
 };

+ 143 - 22
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<uint32_t> 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<TModID> ModsPresetState::getActiveMods() const
@@ -344,7 +372,7 @@ ModManager::ModManager(const JsonNode & repositoryList)
 	addNewModsToPreset();
 
 	std::vector<TModID> desiredModList = modsPreset->getActiveMods();
-	generateLoadOrder(desiredModList);
+	depedencyResolver = std::make_unique<ModDependenciesResolver>(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());
+	}
+}
+
+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<TModID> desiredModList = modsPreset->getActiveMods();
+	depedencyResolver = std::make_unique<ModDependenciesResolver>(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 ModManager::generateLoadOrder(std::vector<TModID> modsToResolve)
+void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStorage & storage)
 {
 	// Topological sort algorithm.
 	boost::range::sort(modsToResolve); // Sort mods per name
-	std::vector<TModID> sortedValidMods; // Vector keeps order of elements (LIFO)
-	sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation
-	std::set<TModID> resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements
+	std::vector<TModID> sortedValidMods(activeMods.begin(), activeMods.end()); // Vector keeps order of elements (LIFO)
+	std::set<TModID> resolvedModIDs(activeMods.begin(), activeMods.end()); // Use a set for validation for performance reason, but set does not keep order of elements
 	std::set<TModID> 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<TModID> 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<TModID> modsToResolve)
 		std::set<TModID> 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<TModID> modsToResolve)
 
 	assert(!sortedValidMods.empty());
 	activeMods = sortedValidMods;
-	brokenMods = modsToResolve;
+	brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end());
 }
 
 VCMI_LIB_NAMESPACE_END

+ 27 - 5
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> modsState;
 	std::unique_ptr<ModsPresetState> modsPreset;
 	std::unique_ptr<ModsStorage> modsStorage;
+	std::unique_ptr<ModDependenciesResolver> 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<uint32_t> value);
 	void saveConfigurationState() const;
 	double getInstalledModSizeMegabytes(const TModID & modName) const;
+
+	void tryEnableMod(const TModID & modName);
+	void tryDisableMod(const TModID & modName);
 };
 
 VCMI_LIB_NAMESPACE_END