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.
|
* `name`: (Deprecated) same as title.
|
||||||
* `description`: Short description to be shown in the content tab.
|
* `description`: Short description to be shown in the content tab.
|
||||||
See [Translating content meta](#translating-content-meta).
|
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>`
|
* `allowed_mapgens = <comma-separated mapgens>`
|
||||||
e.g. `allowed_mapgens = v5,v6,flat`
|
e.g. `allowed_mapgens = v5,v6,flat`
|
||||||
Mapgens not in this list are removed from the list of mapgens for the
|
Mapgens not in this list are removed from the list of mapgens for the
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
title = Development Test
|
title = Development Test
|
||||||
description = Testing environment to help with testing the engine features of Minetest. It can also be helpful in mod development.
|
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
|
name = unittests
|
||||||
description = Adds automated unit tests for the engine
|
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 "gettext.h"
|
||||||
#include "exceptions.h"
|
#include "exceptions.h"
|
||||||
#include "util/numeric.h"
|
#include "util/numeric.h"
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
std::string ModConfiguration::getUnsatisfiedModsError() const
|
std::string ModConfiguration::getUnsatisfiedModsError() const
|
||||||
{
|
{
|
||||||
@ -116,6 +116,9 @@ void ModConfiguration::addGameMods(const SubgameSpec &gamespec)
|
|||||||
std::string game_virtual_path;
|
std::string game_virtual_path;
|
||||||
game_virtual_path.append("games/").append(gamespec.id).append("/mods");
|
game_virtual_path.append("games/").append(gamespec.id).append("/mods");
|
||||||
addModsInPath(gamespec.gamemods_path, game_virtual_path);
|
addModsInPath(gamespec.gamemods_path, game_virtual_path);
|
||||||
|
|
||||||
|
m_first_mod = gamespec.first_mod;
|
||||||
|
m_last_mod = gamespec.last_mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModConfiguration::addModsFromConfig(
|
void ModConfiguration::addModsFromConfig(
|
||||||
@ -221,13 +224,20 @@ void ModConfiguration::checkConflictsAndDeps()
|
|||||||
|
|
||||||
void ModConfiguration::resolveDependencies()
|
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;
|
std::set<std::string> modnames;
|
||||||
for (const ModSpec &mod : m_unsatisfied_mods) {
|
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);
|
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")) {
|
if (g_settings->getBool("random_mod_load_order")) {
|
||||||
MyRandGenerator rg;
|
MyRandGenerator rg;
|
||||||
std::shuffle(m_unsatisfied_mods.begin(),
|
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
|
// of each mod, split mods into satisfied and unsatisfied
|
||||||
std::vector<ModSpec> satisfied;
|
std::vector<ModSpec> satisfied;
|
||||||
std::list<ModSpec> unsatisfied;
|
std::list<ModSpec> unsatisfied;
|
||||||
for (ModSpec mod : m_unsatisfied_mods) {
|
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;
|
mod.unsatisfied_depends = mod.depends;
|
||||||
// check which optional dependencies actually exist
|
// check which optional dependencies actually exist
|
||||||
for (const std::string &optdep : mod.optdepends) {
|
for (const std::string &optdep : mod.optdepends) {
|
||||||
if (modnames.count(optdep) != 0)
|
if (modnames.count(optdep) != 0)
|
||||||
mod.unsatisfied_depends.insert(optdep);
|
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 a mod has no depends it is initially satisfied
|
||||||
if (mod.unsatisfied_depends.empty())
|
if (mod.unsatisfied_depends.empty()) {
|
||||||
satisfied.push_back(mod);
|
satisfied.push_back(mod);
|
||||||
else
|
} else {
|
||||||
unsatisfied.push_back(mod);
|
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.
|
// the sorted list.
|
||||||
while (!satisfied.empty()) {
|
while (!satisfied.empty()) {
|
||||||
ModSpec mod = satisfied.back();
|
ModSpec mod = satisfied.back();
|
||||||
@ -270,8 +310,18 @@ void ModConfiguration::resolveDependencies()
|
|||||||
++it;
|
++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());
|
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();
|
void checkConflictsAndDeps();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::string m_first_mod; // "" <=> no mod
|
||||||
|
std::string m_last_mod; // "" <=> no mod
|
||||||
|
|
||||||
std::vector<ModSpec> m_sorted_mods;
|
std::vector<ModSpec> m_sorted_mods;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,6 +94,50 @@ std::string getSubgamePathEnv()
|
|||||||
return "";
|
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)
|
SubgameSpec findSubgame(const std::string &id)
|
||||||
{
|
{
|
||||||
if (id.empty())
|
if (id.empty())
|
||||||
@ -137,8 +181,6 @@ SubgameSpec findSubgame(const std::string &id)
|
|||||||
if (game_path.empty())
|
if (game_path.empty())
|
||||||
return SubgameSpec();
|
return SubgameSpec();
|
||||||
|
|
||||||
std::string gamemod_path = game_path + DIR_DELIM + "mods";
|
|
||||||
|
|
||||||
// Find mod directories
|
// Find mod directories
|
||||||
std::unordered_map<std::string, std::string> mods_paths;
|
std::unordered_map<std::string, std::string> mods_paths;
|
||||||
mods_paths["mods"] = user + DIR_DELIM + "mods";
|
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;
|
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;
|
std::string menuicon_path;
|
||||||
#ifndef SERVER
|
#ifndef SERVER
|
||||||
menuicon_path = getImagePath(
|
menuicon_path = getImagePath(
|
||||||
game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png");
|
game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
SubgameSpec spec(id, game_path, gamemod_path, mods_paths, game_title,
|
return getSubgameSpec(id, game_path, mods_paths, menuicon_path);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SubgameSpec findWorldSubgame(const std::string &world_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);
|
std::string world_gameid = getWorldGameId(world_path, true);
|
||||||
// See if world contains an embedded game; if so, use it.
|
// See if world contains an embedded game; if so, use it.
|
||||||
std::string world_gamepath = world_path + DIR_DELIM + "game";
|
std::string world_gamepath = world_path + DIR_DELIM + "game";
|
||||||
if (fs::PathExists(world_gamepath)) {
|
if (fs::PathExists(world_gamepath))
|
||||||
SubgameSpec gamespec;
|
return getSubgameSpec(world_gameid, world_gamepath, {}, "");
|
||||||
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;
|
|
||||||
}
|
|
||||||
return findSubgame(world_gameid);
|
return findSubgame(world_gameid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ struct SubgameSpec
|
|||||||
std::string title;
|
std::string title;
|
||||||
std::string author;
|
std::string author;
|
||||||
int release;
|
int release;
|
||||||
|
std::string first_mod; // "" <=> no mod
|
||||||
|
std::string last_mod; // "" <=> no mod
|
||||||
std::string path;
|
std::string path;
|
||||||
std::string gamemods_path;
|
std::string gamemods_path;
|
||||||
|
|
||||||
@ -49,10 +51,16 @@ struct SubgameSpec
|
|||||||
const std::unordered_map<std::string, std::string> &addon_mods_paths = {},
|
const std::unordered_map<std::string, std::string> &addon_mods_paths = {},
|
||||||
const std::string &title = "",
|
const std::string &title = "",
|
||||||
const std::string &menuicon_path = "",
|
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),
|
id(id),
|
||||||
title(title), author(author), release(release), path(path),
|
title(title), author(author), release(release),
|
||||||
gamemods_path(gamemods_path), addon_mods_paths(addon_mods_paths),
|
first_mod(first_mod),
|
||||||
|
last_mod(last_mod),
|
||||||
|
path(path),
|
||||||
|
gamemods_path(gamemods_path),
|
||||||
|
addon_mods_paths(addon_mods_paths),
|
||||||
menuicon_path(menuicon_path)
|
menuicon_path(menuicon_path)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,8 @@ void TestServerModManager::testGetMods()
|
|||||||
{
|
{
|
||||||
ServerModManager sm(m_worlddir);
|
ServerModManager sm(m_worlddir);
|
||||||
const auto &mods = sm.getMods();
|
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)
|
// Ensure we found basenodes mod (part of devtest)
|
||||||
// and test_mod (for testing MINETEST_MOD_PATH).
|
// and test_mod (for testing MINETEST_MOD_PATH).
|
||||||
@ -139,6 +140,9 @@ void TestServerModManager::testGetMods()
|
|||||||
|
|
||||||
UASSERTEQ(bool, default_found, true);
|
UASSERTEQ(bool, default_found, true);
|
||||||
UASSERTEQ(bool, test_mod_found, true);
|
UASSERTEQ(bool, test_mod_found, true);
|
||||||
|
|
||||||
|
UASSERT(mods.front().name == "first_mod");
|
||||||
|
UASSERT(mods.back().name == "last_mod");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestServerModManager::testGetModspec()
|
void TestServerModManager::testGetModspec()
|
||||||
|
Loading…
Reference in New Issue
Block a user