mirror of
https://github.com/minetest/minetest.git
synced 2024-11-30 03:23:45 +01:00
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:
parent
a4768d1638
commit
140b9e5a5a
@ -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
|
||||
|
1
games/devtest/mods/first_mod/init.lua
Normal file
1
games/devtest/mods/first_mod/init.lua
Normal file
@ -0,0 +1 @@
|
||||
-- Nothing to do here, loading order is tested in C++ unittests.
|
2
games/devtest/mods/first_mod/mod.conf
Normal file
2
games/devtest/mods/first_mod/mod.conf
Normal file
@ -0,0 +1,2 @@
|
||||
name = first_mod
|
||||
description = Mod which should be loaded before every other mod.
|
1
games/devtest/mods/last_mod/init.lua
Normal file
1
games/devtest/mods/last_mod/init.lua
Normal file
@ -0,0 +1 @@
|
||||
-- Nothing to do here, loading order is tested in C++ unittests.
|
5
games/devtest/mods/last_mod/mod.conf
Normal file
5
games/devtest/mods/last_mod/mod.conf
Normal file
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user