Allow game to specify first and last mod in mod loading order (#14177)

Co-authored-by: Lars Mueller <appgurulars@gmx.de>
Co-authored-by: sfan5 <sfan5@live.de>
This commit is contained in:
sfence 2024-06-01 16:36:20 +02:00 committed by GitHub
parent a4768d1638
commit 140b9e5a5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 141 additions and 64 deletions

@ -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 = <comma-separated mapgens>`
e.g. `allowed_mapgens = v5,v6,flat`
Mapgens not in this list are removed from the list of mapgens for the

@ -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

@ -0,0 +1 @@
-- Nothing to do here, loading order is tested in C++ unittests.

@ -0,0 +1,2 @@
name = first_mod
description = Mod which should be loaded before every other mod.

@ -0,0 +1 @@
-- Nothing to do here, loading order is tested in C++ unittests.

@ -0,0 +1,5 @@
name = last_mod
description = Mod which should be loaded as last mod.
# Test dependencies
optional_depends = unittests
depends = first_mod

@ -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

@ -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 <optional>
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<std::string> modnames;
for (const ModSpec &mod : m_unsatisfied_mods) {
modnames.insert(mod.name);
std::optional<ModSpec> 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<ModSpec> satisfied;
std::list<ModSpec> 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);
}
}

@ -87,6 +87,9 @@ public:
void checkConflictsAndDeps();
private:
std::string m_first_mod; // "" <=> no mod
std::string m_last_mod; // "" <=> no mod
std::vector<ModSpec> m_sorted_mods;
/**

@ -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<std::string, std::string> &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<std::string, std::string> 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);
}

@ -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<std::string, std::string> &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)
{
}

@ -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()