Browse Source

Refactor ModConfiguration

rubenwardy 2 years ago
parent
commit
06de82fd86

+ 16 - 1
src/client/client.cpp

@@ -58,6 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "game.h"
 #include "chatmessage.h"
 #include "translation.h"
+#include "content/mod_configuration.h"
 
 extern gui::IGUIEnvironment* guienv;
 
@@ -196,7 +197,21 @@ void Client::loadMods()
 	scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath());
 	m_script->loadModFromMemory(BUILTIN_MOD_NAME);
 
-	ClientModConfiguration modconf(getClientModsLuaPath());
+	ModConfiguration modconf;
+	{
+		std::unordered_map<std::string, std::string> paths;
+		std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
+		const auto modsPath = getClientModsLuaPath();
+		if (modsPath != path_user) {
+			paths["share"] = modsPath;
+		}
+		paths["mods"] = path_user;
+
+		std::string settings_path = path_user + DIR_DELIM + "mods.conf";
+		modconf.addModsFromConfig(settings_path, paths);
+		modconf.checkConflictsAndDeps();
+	}
+
 	m_mods = modconf.getMods();
 	// complain about mods with unsatisfied dependencies
 	if (!modconf.isConsistent()) {

+ 1 - 0
src/content/CMakeLists.txt

@@ -1,5 +1,6 @@
 set(content_SRCS
 	${CMAKE_CURRENT_SOURCE_DIR}/content.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/mod_configuration.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/mods.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/subgames.cpp
 	PARENT_SCOPE

+ 255 - 0
src/content/mod_configuration.cpp

@@ -0,0 +1,255 @@
+/*
+Minetest
+Copyright (C) 2013-22 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "mod_configuration.h"
+#include "log.h"
+#include "settings.h"
+#include "filesys.h"
+
+void ModConfiguration::printUnsatisfiedModsError() const
+{
+	for (const ModSpec &mod : m_unsatisfied_mods) {
+		errorstream << "mod \"" << mod.name
+					<< "\" has unsatisfied dependencies: ";
+		for (const std::string &unsatisfied_depend : mod.unsatisfied_depends)
+			errorstream << " \"" << unsatisfied_depend << "\"";
+		errorstream << std::endl;
+	}
+}
+
+void ModConfiguration::addModsInPath(const std::string &path, const std::string &virtual_path)
+{
+	addMods(flattenMods(getModsInPath(path, virtual_path)));
+}
+
+void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
+{
+	// Maintain a map of all existing m_unsatisfied_mods.
+	// Keys are mod names and values are indices into m_unsatisfied_mods.
+	std::map<std::string, u32> existing_mods;
+	for (u32 i = 0; i < m_unsatisfied_mods.size(); ++i) {
+		existing_mods[m_unsatisfied_mods[i].name] = i;
+	}
+
+	// Add new mods
+	for (int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack) {
+		// First iteration:
+		// Add all the mods that come from modpacks
+		// Second iteration:
+		// Add all the mods that didn't come from modpacks
+
+		std::set<std::string> seen_this_iteration;
+
+		for (const ModSpec &mod : new_mods) {
+			if (mod.part_of_modpack != (bool)want_from_modpack)
+				continue;
+
+			if (existing_mods.count(mod.name) == 0) {
+				// GOOD CASE: completely new mod.
+				m_unsatisfied_mods.push_back(mod);
+				existing_mods[mod.name] = m_unsatisfied_mods.size() - 1;
+			} else if (seen_this_iteration.count(mod.name) == 0) {
+				// BAD CASE: name conflict in different levels.
+				u32 oldindex = existing_mods[mod.name];
+				const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
+				warningstream << "Mod name conflict detected: \""
+						<< mod.name << "\"" << std::endl
+						<< "Will not load: " << oldmod.path
+						<< std::endl
+						<< "Overridden by: " << mod.path
+						<< std::endl;
+				m_unsatisfied_mods[oldindex] = mod;
+
+				// If there was a "VERY BAD CASE" name conflict
+				// in an earlier level, ignore it.
+				m_name_conflicts.erase(mod.name);
+			} else {
+				// VERY BAD CASE: name conflict in the same level.
+				u32 oldindex = existing_mods[mod.name];
+				const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
+				warningstream << "Mod name conflict detected: \""
+						<< mod.name << "\"" << std::endl
+						<< "Will not load: " << oldmod.path
+						<< std::endl
+						<< "Will not load: " << mod.path
+						<< std::endl;
+				m_unsatisfied_mods[oldindex] = mod;
+				m_name_conflicts.insert(mod.name);
+			}
+
+			seen_this_iteration.insert(mod.name);
+		}
+	}
+}
+
+void ModConfiguration::addGameMods(const SubgameSpec &gamespec)
+{
+	std::string game_virtual_path;
+	game_virtual_path.append("games/").append(gamespec.id).append("/mods");
+	addModsInPath(gamespec.gamemods_path, game_virtual_path);
+}
+
+void ModConfiguration::addModsFromConfig(
+		const std::string &settings_path,
+		const std::unordered_map<std::string, std::string> &modPaths)
+{
+	Settings conf;
+	std::unordered_map<std::string, std::string> load_mod_names;
+
+	conf.readConfigFile(settings_path.c_str());
+	std::vector<std::string> names = conf.getNames();
+	for (const std::string &name : names) {
+		const auto &value = conf.get(name);
+		if (name.compare(0, 9, "load_mod_") == 0 && value != "false" &&
+				value != "nil")
+			load_mod_names[name.substr(9)] = value;
+	}
+
+	// List of enabled non-game non-world mods
+	std::vector<ModSpec> addon_mods;
+
+	// Map of modname to a list candidate mod paths. Used to list
+	// alternatives if a particular mod cannot be found.
+	std::unordered_map<std::string, std::vector<std::string>> candidates;
+
+	/*
+	 * Iterate through all installed mods except game mods and world mods
+	 *
+	 * If the mod is enabled, add it to `addon_mods`. *
+	 *
+	 * Alternative candidates for a modname are stored in `candidates`,
+	 * and used in an error message later.
+	 *
+	 * If not enabled, add `load_mod_modname = false` to world.mt
+	 */
+	for (const auto &modPath : modPaths) {
+		std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(modPath.second, modPath.first));
+		for (const auto &mod : addon_mods_in_path) {
+			const auto &pair = load_mod_names.find(mod.name);
+			if (pair != load_mod_names.end()) {
+				if (is_yes(pair->second) || pair->second == mod.virtual_path) {
+					addon_mods.push_back(mod);
+				} else {
+					candidates[pair->first].emplace_back(mod.virtual_path);
+				}
+			} else {
+				conf.setBool("load_mod_" + mod.name, false);
+			}
+		}
+	}
+	conf.updateConfigFile(settings_path.c_str());
+
+	addMods(addon_mods);
+
+	// Remove all loaded mods from `load_mod_names`
+	// NB: as deps have not yet been resolved, `m_unsatisfied_mods` will contain all mods.
+	for (const ModSpec &mod : m_unsatisfied_mods)
+		load_mod_names.erase(mod.name);
+
+	// Complain about mods declared to be loaded, but not found
+	if (!load_mod_names.empty()) {
+		errorstream << "The following mods could not be found:";
+		for (const auto &pair : load_mod_names)
+			errorstream << " \"" << pair.first << "\"";
+		errorstream << std::endl;
+
+		for (const auto &pair : load_mod_names) {
+			const auto &candidate = candidates.find(pair.first);
+			if (candidate != candidates.end()) {
+				errorstream << "Unable to load " << pair.first << " as the specified path "
+							<< pair.second << " could not be found. "
+							<< "However, it is available in the following locations:"
+							<< std::endl;
+				for (const auto &path : candidate->second) {
+					errorstream << " - " << path << std::endl;
+				}
+			}
+		}
+	}
+}
+
+void ModConfiguration::checkConflictsAndDeps()
+{
+	// report on name conflicts
+	if (!m_name_conflicts.empty()) {
+		std::string s = "Unresolved name conflicts for mods ";
+
+		bool add_comma = false;
+		for (const auto& it : m_name_conflicts) {
+			if (add_comma)
+				s.append(", ");
+			s.append("\"").append(it).append("\"");
+			add_comma = true;
+		}
+		s.append(".");
+
+		throw ModError(s);
+	}
+
+	// get the mods in order
+	resolveDependencies();
+}
+
+void ModConfiguration::resolveDependencies()
+{
+	// Step 1: Compile a list of the mod names we're working with
+	std::set<std::string> modnames;
+	for (const ModSpec &mod : m_unsatisfied_mods) {
+		modnames.insert(mod.name);
+	}
+
+	// Step 2: get dependencies (including optional dependencies)
+	// of each mod, split mods into satisfied and unsatisfied
+	std::list<ModSpec> satisfied;
+	std::list<ModSpec> unsatisfied;
+	for (ModSpec mod : m_unsatisfied_mods) {
+		mod.unsatisfied_depends = mod.depends;
+		// check which optional dependencies actually exist
+		for (const std::string &optdep : mod.optdepends) {
+			if (modnames.count(optdep) != 0)
+				mod.unsatisfied_depends.insert(optdep);
+		}
+		// if a mod has no depends it is initially satisfied
+		if (mod.unsatisfied_depends.empty())
+			satisfied.push_back(mod);
+		else
+			unsatisfied.push_back(mod);
+	}
+
+	// Step 3: mods without unmet dependencies can be appended to
+	// the sorted list.
+	while (!satisfied.empty()) {
+		ModSpec mod = satisfied.back();
+		m_sorted_mods.push_back(mod);
+		satisfied.pop_back();
+		for (auto it = unsatisfied.begin(); it != unsatisfied.end();) {
+			ModSpec &mod2 = *it;
+			mod2.unsatisfied_depends.erase(mod.name);
+			if (mod2.unsatisfied_depends.empty()) {
+				satisfied.push_back(mod2);
+				it = unsatisfied.erase(it);
+			} else {
+				++it;
+			}
+		}
+	}
+
+	// Step 4: write back list of unsatisfied mods
+	m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end());
+}

+ 111 - 0
src/content/mod_configuration.h

@@ -0,0 +1,111 @@
+/*
+Minetest
+Copyright (C) 2013-22 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "mods.h"
+
+
+/**
+ * ModConfiguration is a subset of installed mods. This class
+ * is used to resolve dependencies and return a sorted list of mods.
+ *
+ * This class should not be extended from, but instead used as a
+ * component in other classes.
+ */
+class ModConfiguration
+{
+public:
+	/**
+	 * @returns true if all dependencies are fullfilled.
+	 */
+	inline bool isConsistent() const { return m_unsatisfied_mods.empty(); }
+
+	inline const std::vector<ModSpec> &getUnsatisfiedMods() const
+	{
+		return m_unsatisfied_mods;
+	}
+
+	/**
+	 * List of mods sorted such that they can be loaded in the
+	 * given order with all dependencies being fulfilled.
+	 *
+	 * I.e: every mod in this list has only dependencies on mods which
+	 * appear earlier in the vector.
+	 */
+	const std::vector<ModSpec> &getMods() const { return m_sorted_mods; }
+
+	void printUnsatisfiedModsError() const;
+
+	/**
+	 * Adds all mods in the given path. used for games, modpacks
+	 * and world-specific mods (worldmods-folders)
+	 *
+	 * @param path To search, should be absolute
+	 * @param virtual_path Virtual path for this directory, see comment in ModSpec
+	 */
+	void addModsInPath(const std::string &path, const std::string &virtual_path);
+
+	/**
+	 * Adds all mods in `new_mods`
+	 */
+	void addMods(const std::vector<ModSpec> &new_mods);
+
+	/**
+	 * Adds game mods
+	 */
+	void addGameMods(const SubgameSpec &gamespec);
+
+	/**
+	 * Adds mods specifed by a world.mt config
+	 *
+	 * @param settings_path Path to world.mt
+	 * @param modPaths Map from virtual name to mod path
+	 */
+	void addModsFromConfig(const std::string &settings_path,
+			const std::unordered_map<std::string, std::string> &modPaths);
+
+	/**
+	 * Call this function once all mods have been added
+	 */
+	void checkConflictsAndDeps();
+
+private:
+	std::vector<ModSpec> m_sorted_mods;
+
+	/**
+	 * move mods from m_unsatisfied_mods to m_sorted_mods
+	 * in an order that satisfies dependencies
+	 */
+	void resolveDependencies();
+
+	// mods with unmet dependencies. Before dependencies are resolved,
+	// this is where all mods are stored. Afterwards this contains
+	// only the ones with really unsatisfied dependencies.
+	std::vector<ModSpec> m_unsatisfied_mods;
+
+	// set of mod names for which an unresolved name conflict
+	// exists. A name conflict happens when two or more mods
+	// at the same level have the same name but different paths.
+	// Levels (mods in higher levels override mods in lower levels):
+	// 1. game mod in modpack; 2. game mod;
+	// 3. world mod in modpack; 4. world mod;
+	// 5. addon mod in modpack; 6. addon mod.
+	std::unordered_set<std::string> m_name_conflicts;
+};

+ 68 - 304
src/content/mods.cpp

@@ -69,7 +69,7 @@ bool parseDependsString(std::string &dep, std::unordered_set<char> &symbols)
 	return !dep.empty();
 }
 
-void parseModContents(ModSpec &spec)
+bool parseModContents(ModSpec &spec)
 {
 	// NOTE: this function works in mutual recursion with getModsInPath
 
@@ -79,91 +79,89 @@ void parseModContents(ModSpec &spec)
 	spec.modpack_content.clear();
 
 	// Handle modpacks (defined by containing modpack.txt)
-	std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
-	std::ifstream modpack2_is((spec.path + DIR_DELIM + "modpack.conf").c_str());
-	if (modpack_is.good() || modpack2_is.good()) {
-		if (modpack_is.good())
-			modpack_is.close();
-
-		if (modpack2_is.good())
-			modpack2_is.close();
-
+	if (fs::IsFile(spec.path + DIR_DELIM + "modpack.txt") ||
+			fs::IsFile(spec.path + DIR_DELIM + "modpack.conf")) {
 		spec.is_modpack = true;
 		spec.modpack_content = getModsInPath(spec.path, spec.virtual_path, true);
+		return true;
+	} else if (!fs::IsFile(spec.path + DIR_DELIM + "init.lua")) {
+		return false;
+	}
 
-	} else {
-		Settings info;
-		info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str());
 
-		if (info.exists("name"))
-			spec.name = info.get("name");
-		else
-			spec.deprecation_msgs.push_back("Mods not having a mod.conf file with the name is deprecated.");
-
-		if (info.exists("author"))
-			spec.author = info.get("author");
-
-		if (info.exists("release"))
-			spec.release = info.getS32("release");
-
-		// Attempt to load dependencies from mod.conf
-		bool mod_conf_has_depends = false;
-		if (info.exists("depends")) {
-			mod_conf_has_depends = true;
-			std::string dep = info.get("depends");
-			// clang-format off
-			dep.erase(std::remove_if(dep.begin(), dep.end(),
-					static_cast<int (*)(int)>(&std::isspace)), dep.end());
-			// clang-format on
-			for (const auto &dependency : str_split(dep, ',')) {
-				spec.depends.insert(dependency);
-			}
+	Settings info;
+	info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str());
+
+	if (info.exists("name"))
+		spec.name = info.get("name");
+	else
+		spec.deprecation_msgs.push_back("Mods not having a mod.conf file with the name is deprecated.");
+
+	if (info.exists("author"))
+		spec.author = info.get("author");
+
+	if (info.exists("release"))
+		spec.release = info.getS32("release");
+
+	// Attempt to load dependencies from mod.conf
+	bool mod_conf_has_depends = false;
+	if (info.exists("depends")) {
+		mod_conf_has_depends = true;
+		std::string dep = info.get("depends");
+		// clang-format off
+		dep.erase(std::remove_if(dep.begin(), dep.end(),
+				static_cast<int (*)(int)>(&std::isspace)), dep.end());
+		// clang-format on
+		for (const auto &dependency : str_split(dep, ',')) {
+			spec.depends.insert(dependency);
 		}
+	}
 
-		if (info.exists("optional_depends")) {
-			mod_conf_has_depends = true;
-			std::string dep = info.get("optional_depends");
-			// clang-format off
-			dep.erase(std::remove_if(dep.begin(), dep.end(),
-					static_cast<int (*)(int)>(&std::isspace)), dep.end());
-			// clang-format on
-			for (const auto &dependency : str_split(dep, ',')) {
-				spec.optdepends.insert(dependency);
-			}
+	if (info.exists("optional_depends")) {
+		mod_conf_has_depends = true;
+		std::string dep = info.get("optional_depends");
+		// clang-format off
+		dep.erase(std::remove_if(dep.begin(), dep.end(),
+				static_cast<int (*)(int)>(&std::isspace)), dep.end());
+		// clang-format on
+		for (const auto &dependency : str_split(dep, ',')) {
+			spec.optdepends.insert(dependency);
 		}
+	}
 
-		// Fallback to depends.txt
-		if (!mod_conf_has_depends) {
-			std::vector<std::string> dependencies;
+	// Fallback to depends.txt
+	if (!mod_conf_has_depends) {
+		std::vector<std::string> dependencies;
 
-			std::ifstream is((spec.path + DIR_DELIM + "depends.txt").c_str());
+		std::ifstream is((spec.path + DIR_DELIM + "depends.txt").c_str());
 
-			if (is.good())
-				spec.deprecation_msgs.push_back("depends.txt is deprecated, please use mod.conf instead.");
+		if (is.good())
+			spec.deprecation_msgs.push_back("depends.txt is deprecated, please use mod.conf instead.");
 
-			while (is.good()) {
-				std::string dep;
-				std::getline(is, dep);
-				dependencies.push_back(dep);
-			}
+		while (is.good()) {
+			std::string dep;
+			std::getline(is, dep);
+			dependencies.push_back(dep);
+		}
 
-			for (auto &dependency : dependencies) {
-				std::unordered_set<char> symbols;
-				if (parseDependsString(dependency, symbols)) {
-					if (symbols.count('?') != 0) {
-						spec.optdepends.insert(dependency);
-					} else {
-						spec.depends.insert(dependency);
-					}
+		for (auto &dependency : dependencies) {
+			std::unordered_set<char> symbols;
+			if (parseDependsString(dependency, symbols)) {
+				if (symbols.count('?') != 0) {
+					spec.optdepends.insert(dependency);
+				} else {
+					spec.depends.insert(dependency);
 				}
 			}
 		}
-
-		if (info.exists("description"))
-			spec.desc = info.get("description");
-		else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc))
-			spec.deprecation_msgs.push_back("description.txt is deprecated, please use mod.conf instead.");
 	}
+
+	if (info.exists("description"))
+		spec.desc = info.get("description");
+	else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc))
+		spec.deprecation_msgs.push_back("description.txt is deprecated, please use mod.conf instead.");
+
+	return true;
 }
 
 std::map<std::string, ModSpec> getModsInPath(
@@ -218,240 +216,6 @@ std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods)
 	return result;
 }
 
-ModConfiguration::ModConfiguration(const std::string &worldpath)
-{
-}
-
-void ModConfiguration::printUnsatisfiedModsError() const
-{
-	for (const ModSpec &mod : m_unsatisfied_mods) {
-		errorstream << "mod \"" << mod.name
-			    << "\" has unsatisfied dependencies: ";
-		for (const std::string &unsatisfied_depend : mod.unsatisfied_depends)
-			errorstream << " \"" << unsatisfied_depend << "\"";
-		errorstream << std::endl;
-	}
-}
-
-void ModConfiguration::addModsInPath(const std::string &path, const std::string &virtual_path)
-{
-	addMods(flattenMods(getModsInPath(path, virtual_path)));
-}
-
-void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
-{
-	// Maintain a map of all existing m_unsatisfied_mods.
-	// Keys are mod names and values are indices into m_unsatisfied_mods.
-	std::map<std::string, u32> existing_mods;
-	for (u32 i = 0; i < m_unsatisfied_mods.size(); ++i) {
-		existing_mods[m_unsatisfied_mods[i].name] = i;
-	}
-
-	// Add new mods
-	for (int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack) {
-		// First iteration:
-		// Add all the mods that come from modpacks
-		// Second iteration:
-		// Add all the mods that didn't come from modpacks
-
-		std::set<std::string> seen_this_iteration;
-
-		for (const ModSpec &mod : new_mods) {
-			if (mod.part_of_modpack != (bool)want_from_modpack)
-				continue;
-
-			if (existing_mods.count(mod.name) == 0) {
-				// GOOD CASE: completely new mod.
-				m_unsatisfied_mods.push_back(mod);
-				existing_mods[mod.name] = m_unsatisfied_mods.size() - 1;
-			} else if (seen_this_iteration.count(mod.name) == 0) {
-				// BAD CASE: name conflict in different levels.
-				u32 oldindex = existing_mods[mod.name];
-				const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
-				warningstream << "Mod name conflict detected: \""
-					      << mod.name << "\"" << std::endl
-					      << "Will not load: " << oldmod.path
-					      << std::endl
-					      << "Overridden by: " << mod.path
-					      << std::endl;
-				m_unsatisfied_mods[oldindex] = mod;
-
-				// If there was a "VERY BAD CASE" name conflict
-				// in an earlier level, ignore it.
-				m_name_conflicts.erase(mod.name);
-			} else {
-				// VERY BAD CASE: name conflict in the same level.
-				u32 oldindex = existing_mods[mod.name];
-				const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
-				warningstream << "Mod name conflict detected: \""
-					      << mod.name << "\"" << std::endl
-					      << "Will not load: " << oldmod.path
-					      << std::endl
-					      << "Will not load: " << mod.path
-					      << std::endl;
-				m_unsatisfied_mods[oldindex] = mod;
-				m_name_conflicts.insert(mod.name);
-			}
-
-			seen_this_iteration.insert(mod.name);
-		}
-	}
-}
-
-void ModConfiguration::addModsFromConfig(
-		const std::string &settings_path,
-		const std::unordered_map<std::string, std::string> &modPaths)
-{
-	Settings conf;
-	std::unordered_map<std::string, std::string> load_mod_names;
-
-	conf.readConfigFile(settings_path.c_str());
-	std::vector<std::string> names = conf.getNames();
-	for (const std::string &name : names) {
-		const auto &value = conf.get(name);
-		if (name.compare(0, 9, "load_mod_") == 0 && value != "false" &&
-				value != "nil")
-			load_mod_names[name.substr(9)] = value;
-	}
-
-	std::vector<ModSpec> addon_mods;
-	std::unordered_map<std::string, std::vector<std::string>> candidates;
-
-	for (const auto &modPath : modPaths) {
-		std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(modPath.second, modPath.first));
-		for (std::vector<ModSpec>::const_iterator it = addon_mods_in_path.begin();
-				it != addon_mods_in_path.end(); ++it) {
-			const ModSpec &mod = *it;
-			const auto &pair = load_mod_names.find(mod.name);
-			if (pair != load_mod_names.end()) {
-				if (is_yes(pair->second) || pair->second == mod.virtual_path) {
-					addon_mods.push_back(mod);
-				} else {
-					candidates[pair->first].emplace_back(mod.virtual_path);
-				}
-			} else {
-				conf.setBool("load_mod_" + mod.name, false);
-			}
-		}
-	}
-	conf.updateConfigFile(settings_path.c_str());
-
-	addMods(addon_mods);
-	checkConflictsAndDeps();
-
-	// complain about mods declared to be loaded, but not found
-	for (const ModSpec &addon_mod : addon_mods)
-		load_mod_names.erase(addon_mod.name);
-
-	std::vector<ModSpec> unsatisfiedMods = getUnsatisfiedMods();
-
-	for (const ModSpec &unsatisfiedMod : unsatisfiedMods)
-		load_mod_names.erase(unsatisfiedMod.name);
-
-	if (!load_mod_names.empty()) {
-		errorstream << "The following mods could not be found:";
-		for (const auto &pair : load_mod_names)
-			errorstream << " \"" << pair.first << "\"";
-		errorstream << std::endl;
-
-		for (const auto &pair : load_mod_names) {
-			const auto &candidate = candidates.find(pair.first);
-			if (candidate != candidates.end()) {
-				errorstream << "Unable to load " << pair.first << " as the specified path "
-					<< pair.second << " could not be found. "
-					<< "However, it is available in the following locations:"
-					<< std::endl;
-				for (const auto &path : candidate->second) {
-					errorstream << " - " << path << std::endl;
-				}
-			}
-		}
-	}
-}
-
-void ModConfiguration::checkConflictsAndDeps()
-{
-	// report on name conflicts
-	if (!m_name_conflicts.empty()) {
-		std::string s = "Unresolved name conflicts for mods ";
-		for (std::unordered_set<std::string>::const_iterator it =
-						m_name_conflicts.begin();
-				it != m_name_conflicts.end(); ++it) {
-			if (it != m_name_conflicts.begin())
-				s += ", ";
-			s += std::string("\"") + (*it) + "\"";
-		}
-		s += ".";
-		throw ModError(s);
-	}
-
-	// get the mods in order
-	resolveDependencies();
-}
-
-void ModConfiguration::resolveDependencies()
-{
-	// Step 1: Compile a list of the mod names we're working with
-	std::set<std::string> modnames;
-	for (const ModSpec &mod : m_unsatisfied_mods) {
-		modnames.insert(mod.name);
-	}
-
-	// Step 2: get dependencies (including optional dependencies)
-	// of each mod, split mods into satisfied and unsatisfied
-	std::list<ModSpec> satisfied;
-	std::list<ModSpec> unsatisfied;
-	for (ModSpec mod : m_unsatisfied_mods) {
-		mod.unsatisfied_depends = mod.depends;
-		// check which optional dependencies actually exist
-		for (const std::string &optdep : mod.optdepends) {
-			if (modnames.count(optdep) != 0)
-				mod.unsatisfied_depends.insert(optdep);
-		}
-		// if a mod has no depends it is initially satisfied
-		if (mod.unsatisfied_depends.empty())
-			satisfied.push_back(mod);
-		else
-			unsatisfied.push_back(mod);
-	}
-
-	// Step 3: mods without unmet dependencies can be appended to
-	// the sorted list.
-	while (!satisfied.empty()) {
-		ModSpec mod = satisfied.back();
-		m_sorted_mods.push_back(mod);
-		satisfied.pop_back();
-		for (auto it = unsatisfied.begin(); it != unsatisfied.end();) {
-			ModSpec &mod2 = *it;
-			mod2.unsatisfied_depends.erase(mod.name);
-			if (mod2.unsatisfied_depends.empty()) {
-				satisfied.push_back(mod2);
-				it = unsatisfied.erase(it);
-			} else {
-				++it;
-			}
-		}
-	}
-
-	// Step 4: write back list of unsatisfied mods
-	m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end());
-}
-
-#ifndef SERVER
-ClientModConfiguration::ClientModConfiguration(const std::string &path) :
-		ModConfiguration(path)
-{
-	std::unordered_map<std::string, std::string> paths;
-	std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
-	if (path != path_user) {
-		paths["share"] = path;
-	}
-	paths["mods"] = path_user;
-
-	std::string settings_path = path_user + DIR_DELIM + "mods.conf";
-	addModsFromConfig(settings_path, paths);
-}
-#endif
 
 ModMetadata::ModMetadata(const std::string &mod_name, ModMetadataDatabase *database):
 	m_mod_name(mod_name), m_database(database)

+ 7 - 81
src/content/mods.h

@@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/basic_macros.h"
 #include "config.h"
 #include "metadata.h"
+#include "subgames.h"
 
 class ModMetadataDatabase;
 
@@ -87,8 +88,12 @@ struct ModSpec
 	void checkAndLog() const;
 };
 
-// Retrieves depends, optdepends, is_modpack and modpack_content
-void parseModContents(ModSpec &mod);
+/**
+ * Retrieves depends, optdepends, is_modpack and modpack_content
+ *
+ * @returns false if not a mod
+ */
+bool parseModContents(ModSpec &mod);
 
 /**
  * Gets a list of all mods and modpacks in path
@@ -104,85 +109,6 @@ std::map<std::string, ModSpec> getModsInPath(const std::string &path,
 // replaces modpack Modspecs with their content
 std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods);
 
-// a ModConfiguration is a subset of installed mods, expected to have
-// all dependencies fullfilled, so it can be used as a list of mods to
-// load when the game starts.
-class ModConfiguration
-{
-public:
-	// checks if all dependencies are fullfilled.
-	bool isConsistent() const { return m_unsatisfied_mods.empty(); }
-
-	const std::vector<ModSpec> &getMods() const { return m_sorted_mods; }
-
-	const std::vector<ModSpec> &getUnsatisfiedMods() const
-	{
-		return m_unsatisfied_mods;
-	}
-
-	void printUnsatisfiedModsError() const;
-
-protected:
-	ModConfiguration(const std::string &worldpath);
-
-	/**
-	 * adds all mods in the given path. used for games, modpacks
-	 * and world-specific mods (worldmods-folders)
-	 *
-	 * @param path To search, should be absolute
-	 * @param virtual_path Virtual path for this directory, see comment in ModSpec
-	 */
-	void addModsInPath(const std::string &path, const std::string &virtual_path);
-
-	// adds all mods in the set.
-	void addMods(const std::vector<ModSpec> &new_mods);
-
-	/**
-	 * @param settings_path Path to world.mt
-	 * @param modPaths Map from virtual name to mod path
-	 */
-	void addModsFromConfig(const std::string &settings_path,
-			const std::unordered_map<std::string, std::string> &modPaths);
-
-	void checkConflictsAndDeps();
-
-protected:
-	// list of mods sorted such that they can be loaded in the
-	// given order with all dependencies being fullfilled. I.e.,
-	// every mod in this list has only dependencies on mods which
-	// appear earlier in the vector.
-	std::vector<ModSpec> m_sorted_mods;
-
-private:
-	// move mods from m_unsatisfied_mods to m_sorted_mods
-	// in an order that satisfies dependencies
-	void resolveDependencies();
-
-	// mods with unmet dependencies. Before dependencies are resolved,
-	// this is where all mods are stored. Afterwards this contains
-	// only the ones with really unsatisfied dependencies.
-	std::vector<ModSpec> m_unsatisfied_mods;
-
-	// set of mod names for which an unresolved name conflict
-	// exists. A name conflict happens when two or more mods
-	// at the same level have the same name but different paths.
-	// Levels (mods in higher levels override mods in lower levels):
-	// 1. game mod in modpack; 2. game mod;
-	// 3. world mod in modpack; 4. world mod;
-	// 5. addon mod in modpack; 6. addon mod.
-	std::unordered_set<std::string> m_name_conflicts;
-
-	// Deleted default constructor
-	ModConfiguration() = default;
-};
-
-#ifndef SERVER
-class ClientModConfiguration : public ModConfiguration
-{
-public:
-	ClientModConfiguration(const std::string &path);
-};
-#endif
 
 class ModMetadata : public Metadata
 {

+ 0 - 1
src/filesys.cpp

@@ -877,4 +877,3 @@ bool Rename(const std::string &from, const std::string &to)
 }
 
 } // namespace fs
-

+ 5 - 0
src/filesys.h

@@ -60,6 +60,11 @@ bool IsPathAbsolute(const std::string &path);
 
 bool IsDir(const std::string &path);
 
+inline bool IsFile(const std::string &path)
+{
+	return PathExists(path) && !IsDir(path);
+}
+
 bool IsDirDelimiter(char c);
 
 // Only pass full paths to this one. True on success.

+ 14 - 17
src/server/mods.cpp

@@ -23,7 +23,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "scripting_server.h"
 #include "content/subgames.h"
 #include "porting.h"
-#include "util/metricsbackend.h"
 
 /**
  * Manage server mods
@@ -35,20 +34,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
  * Creates a ServerModManager which targets worldpath
  * @param worldpath
  */
-ServerModManager::ServerModManager(const std::string &worldpath) :
-		ModConfiguration(worldpath)
+ServerModManager::ServerModManager(const std::string &worldpath):
+	configuration()
 {
 	SubgameSpec gamespec = findWorldSubgame(worldpath);
 
 	// Add all game mods and all world mods
-	std::string game_virtual_path;
-	game_virtual_path.append("games/").append(gamespec.id).append("/mods");
-	addModsInPath(gamespec.gamemods_path, game_virtual_path);
-	addModsInPath(worldpath + DIR_DELIM + "worldmods", "worldmods");
+	configuration.addGameMods(gamespec);
+	configuration.addModsInPath(worldpath + DIR_DELIM + "worldmods", "worldmods");
 
 	// Load normal mods
 	std::string worldmt = worldpath + DIR_DELIM + "world.mt";
-	addModsFromConfig(worldmt, gamespec.addon_mods_paths);
+	configuration.addModsFromConfig(worldmt, gamespec.addon_mods_paths);
+	configuration.checkConflictsAndDeps();
 }
 
 // clang-format off
@@ -57,12 +55,13 @@ void ServerModManager::loadMods(ServerScripting *script)
 {
 	// Print mods
 	infostream << "Server: Loading mods: ";
-	for (const ModSpec &mod : m_sorted_mods) {
+	for (const ModSpec &mod : configuration.getMods()) {
 		infostream << mod.name << " ";
 	}
+
 	infostream << std::endl;
 	// Load and run "mod" scripts
-	for (const ModSpec &mod : m_sorted_mods) {
+	for (const ModSpec &mod : configuration.getMods()) {
 		mod.checkAndLog();
 
 		std::string script_path = mod.path + DIR_DELIM + "init.lua";
@@ -79,25 +78,23 @@ void ServerModManager::loadMods(ServerScripting *script)
 // clang-format on
 const ModSpec *ServerModManager::getModSpec(const std::string &modname) const
 {
-	std::vector<ModSpec>::const_iterator it;
-	for (it = m_sorted_mods.begin(); it != m_sorted_mods.end(); ++it) {
-		const ModSpec &mod = *it;
+	for (const auto &mod : configuration.getMods()) {
 		if (mod.name == modname)
 			return &mod;
 	}
-	return NULL;
+
+	return nullptr;
 }
 
 void ServerModManager::getModNames(std::vector<std::string> &modlist) const
 {
-	for (const ModSpec &spec : m_sorted_mods)
+	for (const ModSpec &spec : configuration.getMods())
 		modlist.push_back(spec.name);
 }
 
 void ServerModManager::getModsMediaPaths(std::vector<std::string> &paths) const
 {
-	for (auto it = m_sorted_mods.crbegin(); it != m_sorted_mods.crend(); it++) {
-		const ModSpec &spec = *it;
+	for (const auto &spec : configuration.getMods()) {
 		fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "textures");
 		fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "sounds");
 		fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "media");

+ 21 - 2
src/server/mods.h

@@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #pragma once
 
-#include "content/mods.h"
+#include "content/mod_configuration.h"
 #include <memory>
 
 class MetricsBackend;
@@ -31,8 +31,10 @@ class ServerScripting;
  *
  * All new calls to this class must be tested in test_servermodmanager.cpp
  */
-class ServerModManager : public ModConfiguration
+class ServerModManager
 {
+	ModConfiguration configuration;
+
 public:
 	/**
 	 * Creates a ServerModManager which targets worldpath
@@ -42,6 +44,23 @@ public:
 	void loadMods(ServerScripting *script);
 	const ModSpec *getModSpec(const std::string &modname) const;
 	void getModNames(std::vector<std::string> &modlist) const;
+
+	inline const std::vector<ModSpec> &getMods() const {
+		return configuration.getMods();
+	}
+
+	inline const std::vector<ModSpec> &getUnsatisfiedMods() const {
+		return configuration.getUnsatisfiedMods();
+	}
+
+	inline bool isConsistent() const {
+		return configuration.isConsistent();
+	}
+
+	inline void printUnsatisfiedModsError() const {
+		return configuration.printUnsatisfiedModsError();
+	}
+
 	/**
 	 * Recursively gets all paths of mod folders that can contain media files.
 	 *