From 140b9e5a5a40476659cad689621fc01e8b1182b1 Mon Sep 17 00:00:00 2001 From: sfence Date: Sat, 1 Jun 2024 16:36:20 +0200 Subject: [PATCH] Allow game to specify first and last mod in mod loading order (#14177) Co-authored-by: Lars Mueller Co-authored-by: sfan5 --- doc/lua_api.md | 2 + games/devtest/game.conf | 2 + games/devtest/mods/first_mod/init.lua | 1 + games/devtest/mods/first_mod/mod.conf | 2 + games/devtest/mods/last_mod/init.lua | 1 + games/devtest/mods/last_mod/mod.conf | 5 ++ games/devtest/mods/unittests/mod.conf | 3 +- src/content/mod_configuration.cpp | 70 ++++++++++++++++--- src/content/mod_configuration.h | 3 + src/content/subgames.cpp | 96 +++++++++++++------------- src/content/subgames.h | 14 +++- src/unittest/test_servermodmanager.cpp | 6 +- 12 files changed, 141 insertions(+), 64 deletions(-) create mode 100644 games/devtest/mods/first_mod/init.lua create mode 100644 games/devtest/mods/first_mod/mod.conf create mode 100644 games/devtest/mods/last_mod/init.lua create mode 100644 games/devtest/mods/last_mod/mod.conf diff --git a/doc/lua_api.md b/doc/lua_api.md index 4eea2f7f3..3bdf48c01 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -63,6 +63,8 @@ The game directory can contain the following files: * `name`: (Deprecated) same as title. * `description`: Short description to be shown in the content tab. See [Translating content meta](#translating-content-meta). + * `first_mod`: Use this to specify the mod that must be loaded before any other mod. + * `last_mod`: Use this to specify the mod that must be loaded after all other mods * `allowed_mapgens = ` e.g. `allowed_mapgens = v5,v6,flat` Mapgens not in this list are removed from the list of mapgens for the diff --git a/games/devtest/game.conf b/games/devtest/game.conf index 0f5656c99..8184881a2 100644 --- a/games/devtest/game.conf +++ b/games/devtest/game.conf @@ -1,2 +1,4 @@ title = Development Test description = Testing environment to help with testing the engine features of Minetest. It can also be helpful in mod development. +first_mod = first_mod +last_mod = last_mod diff --git a/games/devtest/mods/first_mod/init.lua b/games/devtest/mods/first_mod/init.lua new file mode 100644 index 000000000..3909a39cc --- /dev/null +++ b/games/devtest/mods/first_mod/init.lua @@ -0,0 +1 @@ +-- Nothing to do here, loading order is tested in C++ unittests. diff --git a/games/devtest/mods/first_mod/mod.conf b/games/devtest/mods/first_mod/mod.conf new file mode 100644 index 000000000..d35fa8d28 --- /dev/null +++ b/games/devtest/mods/first_mod/mod.conf @@ -0,0 +1,2 @@ +name = first_mod +description = Mod which should be loaded before every other mod. diff --git a/games/devtest/mods/last_mod/init.lua b/games/devtest/mods/last_mod/init.lua new file mode 100644 index 000000000..3909a39cc --- /dev/null +++ b/games/devtest/mods/last_mod/init.lua @@ -0,0 +1 @@ +-- Nothing to do here, loading order is tested in C++ unittests. diff --git a/games/devtest/mods/last_mod/mod.conf b/games/devtest/mods/last_mod/mod.conf new file mode 100644 index 000000000..734bf4c0c --- /dev/null +++ b/games/devtest/mods/last_mod/mod.conf @@ -0,0 +1,5 @@ +name = last_mod +description = Mod which should be loaded as last mod. +# Test dependencies +optional_depends = unittests +depends = first_mod diff --git a/games/devtest/mods/unittests/mod.conf b/games/devtest/mods/unittests/mod.conf index fa94e01a6..ccff73701 100644 --- a/games/devtest/mods/unittests/mod.conf +++ b/games/devtest/mods/unittests/mod.conf @@ -1,3 +1,4 @@ name = unittests description = Adds automated unit tests for the engine -depends = basenodes +# Also test that it is possible to depend on first_mod +depends = first_mod, basenodes diff --git a/src/content/mod_configuration.cpp b/src/content/mod_configuration.cpp index 47ad3e0dc..d814c1b68 100644 --- a/src/content/mod_configuration.cpp +++ b/src/content/mod_configuration.cpp @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gettext.h" #include "exceptions.h" #include "util/numeric.h" - +#include std::string ModConfiguration::getUnsatisfiedModsError() const { @@ -116,6 +116,9 @@ 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); + + m_first_mod = gamespec.first_mod; + m_last_mod = gamespec.last_mod; } void ModConfiguration::addModsFromConfig( @@ -221,13 +224,20 @@ void ModConfiguration::checkConflictsAndDeps() void ModConfiguration::resolveDependencies() { - // Step 1: Compile a list of the mod names we're working with + // Compile a list of the mod names we're working with std::set modnames; - for (const ModSpec &mod : m_unsatisfied_mods) { - modnames.insert(mod.name); + std::optional first_mod_spec, last_mod_spec; + for (ModSpec &mod : m_unsatisfied_mods) { + if (mod.name == m_first_mod) { + first_mod_spec = mod; + } else if (mod.name == m_last_mod) { + last_mod_spec = mod; + } else { + modnames.insert(mod.name); + } } - // Step 1.5 (optional): shuffle unsatisfied mods so non declared depends get found by their devs + // Optionally shuffle unsatisfied mods so non declared depends get found by their devs if (g_settings->getBool("random_mod_load_order")) { MyRandGenerator rg; std::shuffle(m_unsatisfied_mods.begin(), @@ -236,25 +246,55 @@ void ModConfiguration::resolveDependencies() ); } - // Step 2: get dependencies (including optional dependencies) + // Check for presence of first and last mod + if (!m_first_mod.empty() && !first_mod_spec.has_value()) + throw ModError("The mod specified as first by the game was not found."); + if (!m_last_mod.empty() && !last_mod_spec.has_value()) + throw ModError("The mod specified as last by the game was not found."); + + // Check and add first mod + if (first_mod_spec.has_value()) { + // Dependencies are not allowed for first mod + if (!first_mod_spec->depends.empty() || !first_mod_spec->optdepends.empty()) + throw ModError("Mod specified by first_mod cannot have dependencies"); + + m_sorted_mods.push_back(*first_mod_spec); + } + + // Get dependencies (including optional dependencies) // of each mod, split mods into satisfied and unsatisfied std::vector satisfied; std::list unsatisfied; for (ModSpec mod : m_unsatisfied_mods) { + if (mod.name == m_first_mod || mod.name == m_last_mod) + continue; // skip, handled separately + 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); } + mod.unsatisfied_depends.erase(m_first_mod); // first is already satisfied + + if (last_mod_spec.has_value() && mod.unsatisfied_depends.count(last_mod_spec->name) != 0) { + throw ModError("Impossible to depend on the mod specified by last_mod"); + } // if a mod has no depends it is initially satisfied - if (mod.unsatisfied_depends.empty()) + if (mod.unsatisfied_depends.empty()) { satisfied.push_back(mod); - else + } else { unsatisfied.push_back(mod); + } } - // Step 3: mods without unmet dependencies can be appended to + // All dependencies of the last mod are initially unsatisfied + if (last_mod_spec.has_value()) { + last_mod_spec->unsatisfied_depends = last_mod_spec->depends; + last_mod_spec->unsatisfied_depends.erase(m_first_mod); + } + + // Mods without unmet dependencies can be appended to // the sorted list. while (!satisfied.empty()) { ModSpec mod = satisfied.back(); @@ -270,8 +310,18 @@ void ModConfiguration::resolveDependencies() ++it; } } + if (last_mod_spec.has_value()) + last_mod_spec->unsatisfied_depends.erase(mod.name); } - // Step 4: write back list of unsatisfied mods + // Write back list of unsatisfied mods m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end()); + + // Check and add last mod + if (last_mod_spec.has_value()) { + if (last_mod_spec->unsatisfied_depends.empty()) + m_sorted_mods.push_back(*last_mod_spec); + else + m_unsatisfied_mods.push_back(*last_mod_spec); + } } diff --git a/src/content/mod_configuration.h b/src/content/mod_configuration.h index e945a785a..200694652 100644 --- a/src/content/mod_configuration.h +++ b/src/content/mod_configuration.h @@ -87,6 +87,9 @@ public: void checkConflictsAndDeps(); private: + std::string m_first_mod; // "" <=> no mod + std::string m_last_mod; // "" <=> no mod + std::vector m_sorted_mods; /** diff --git a/src/content/subgames.cpp b/src/content/subgames.cpp index 611b09308..9d5d528a8 100644 --- a/src/content/subgames.cpp +++ b/src/content/subgames.cpp @@ -94,6 +94,50 @@ std::string getSubgamePathEnv() return ""; } +static SubgameSpec getSubgameSpec(const std::string &game_id, + const std::string &game_path, + const std::unordered_map &mods_paths, + const std::string &menuicon_path) +{ + const auto gamemods_path = game_path + DIR_DELIM + "mods"; + // Get meta + const std::string conf_path = game_path + DIR_DELIM + "game.conf"; + Settings conf; + conf.readConfigFile(conf_path.c_str()); + + std::string game_title; + if (conf.exists("title")) + game_title = conf.get("title"); + else if (conf.exists("name")) + game_title = conf.get("name"); + else + game_title = game_id; + + std::string game_author; + if (conf.exists("author")) + game_author = conf.get("author"); + + int game_release = 0; + if (conf.exists("release")) + game_release = conf.getS32("release"); + + std::string first_mod; + if (conf.exists("first_mod")) + first_mod = conf.get("first_mod"); + + std::string last_mod; + if (conf.exists("last_mod")) + last_mod = conf.get("last_mod"); + + SubgameSpec spec(game_id, game_path, gamemods_path, mods_paths, game_title, + menuicon_path, game_author, game_release, first_mod, last_mod); + + if (conf.exists("name") && !conf.exists("title")) + spec.deprecation_msgs.push_back("\"name\" setting in game.conf is deprecated, please use \"title\" instead"); + + return spec; +} + SubgameSpec findSubgame(const std::string &id) { if (id.empty()) @@ -137,8 +181,6 @@ SubgameSpec findSubgame(const std::string &id) if (game_path.empty()) return SubgameSpec(); - std::string gamemod_path = game_path + DIR_DELIM + "mods"; - // Find mod directories std::unordered_map mods_paths; mods_paths["mods"] = user + DIR_DELIM + "mods"; @@ -149,40 +191,13 @@ SubgameSpec findSubgame(const std::string &id) mods_paths[fs::AbsolutePath(mod_path)] = mod_path; } - // Get meta - std::string conf_path = game_path + DIR_DELIM + "game.conf"; - Settings conf; - conf.readConfigFile(conf_path.c_str()); - - std::string game_title; - if (conf.exists("title")) - game_title = conf.get("title"); - else if (conf.exists("name")) - game_title = conf.get("name"); - else - game_title = id; - - std::string game_author; - if (conf.exists("author")) - game_author = conf.get("author"); - - int game_release = 0; - if (conf.exists("release")) - game_release = conf.getS32("release"); - std::string menuicon_path; #ifndef SERVER menuicon_path = getImagePath( game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png"); #endif - SubgameSpec spec(id, game_path, gamemod_path, mods_paths, game_title, - menuicon_path, game_author, game_release); - - if (conf.exists("name") && !conf.exists("title")) - spec.deprecation_msgs.push_back("\"name\" setting in game.conf is deprecated, please use \"title\" instead"); - - return spec; + return getSubgameSpec(id, game_path, mods_paths, menuicon_path); } SubgameSpec findWorldSubgame(const std::string &world_path) @@ -190,25 +205,8 @@ SubgameSpec findWorldSubgame(const std::string &world_path) std::string world_gameid = getWorldGameId(world_path, true); // See if world contains an embedded game; if so, use it. std::string world_gamepath = world_path + DIR_DELIM + "game"; - if (fs::PathExists(world_gamepath)) { - SubgameSpec gamespec; - gamespec.id = world_gameid; - gamespec.path = world_gamepath; - gamespec.gamemods_path = world_gamepath + DIR_DELIM + "mods"; - - Settings conf; - std::string conf_path = world_gamepath + DIR_DELIM + "game.conf"; - conf.readConfigFile(conf_path.c_str()); - - if (conf.exists("title")) - gamespec.title = conf.get("title"); - else if (conf.exists("name")) - gamespec.title = conf.get("name"); - else - gamespec.title = world_gameid; - - return gamespec; - } + if (fs::PathExists(world_gamepath)) + return getSubgameSpec(world_gameid, world_gamepath, {}, ""); return findSubgame(world_gameid); } diff --git a/src/content/subgames.h b/src/content/subgames.h index d5d168243..17a219669 100644 --- a/src/content/subgames.h +++ b/src/content/subgames.h @@ -32,6 +32,8 @@ struct SubgameSpec std::string title; std::string author; int release; + std::string first_mod; // "" <=> no mod + std::string last_mod; // "" <=> no mod std::string path; std::string gamemods_path; @@ -49,10 +51,16 @@ struct SubgameSpec const std::unordered_map &addon_mods_paths = {}, const std::string &title = "", const std::string &menuicon_path = "", - const std::string &author = "", int release = 0) : + const std::string &author = "", int release = 0, + const std::string &first_mod = "", + const std::string &last_mod = "") : id(id), - title(title), author(author), release(release), path(path), - gamemods_path(gamemods_path), addon_mods_paths(addon_mods_paths), + title(title), author(author), release(release), + first_mod(first_mod), + last_mod(last_mod), + path(path), + gamemods_path(gamemods_path), + addon_mods_paths(addon_mods_paths), menuicon_path(menuicon_path) { } diff --git a/src/unittest/test_servermodmanager.cpp b/src/unittest/test_servermodmanager.cpp index f023e3a3b..90c29e125 100644 --- a/src/unittest/test_servermodmanager.cpp +++ b/src/unittest/test_servermodmanager.cpp @@ -121,7 +121,8 @@ void TestServerModManager::testGetMods() { ServerModManager sm(m_worlddir); const auto &mods = sm.getMods(); - UASSERTEQ(bool, mods.empty(), false); + // `ls ./games/devtest/mods | wc -l` + 1 (test mod) + UASSERTEQ(std::size_t, mods.size(), 31 + 1); // Ensure we found basenodes mod (part of devtest) // and test_mod (for testing MINETEST_MOD_PATH). @@ -139,6 +140,9 @@ void TestServerModManager::testGetMods() UASSERTEQ(bool, default_found, true); UASSERTEQ(bool, test_mod_found, true); + + UASSERT(mods.front().name == "first_mod"); + UASSERT(mods.back().name == "last_mod"); } void TestServerModManager::testGetModspec()