summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrubenwardy <rw@rubenwardy.com>2022-01-30 22:40:53 +0000
committerGitHub <noreply@github.com>2022-01-30 22:40:53 +0000
commit128f6359e936bcdc5e26409ddd73438bce9c6dd6 (patch)
treef4d396d479ae17f57f25ab78df300256e28d5945
parent8c0331d2449cf71049c7ce9c06d141e2846ebcb0 (diff)
downloadminetest-128f6359e936bcdc5e26409ddd73438bce9c6dd6.tar.gz
minetest-128f6359e936bcdc5e26409ddd73438bce9c6dd6.tar.bz2
minetest-128f6359e936bcdc5e26409ddd73438bce9c6dd6.zip
Use virtual paths to specify exact mod to enable (#11784)
-rw-r--r--builtin/mainmenu/dlg_config_world.lua25
-rw-r--r--builtin/mainmenu/dlg_settings_advanced.lua2
-rw-r--r--builtin/mainmenu/pkgmgr.lua72
-rw-r--r--doc/menu_lua_api.txt17
-rw-r--r--doc/world_format.txt13
-rw-r--r--src/content/mods.cpp76
-rw-r--r--src/content/mods.h57
-rw-r--r--src/content/subgames.cpp11
-rw-r--r--src/content/subgames.h10
-rw-r--r--src/script/lua_api/l_mainmenu.cpp12
-rw-r--r--src/server/mods.cpp6
11 files changed, 222 insertions, 79 deletions
diff --git a/builtin/mainmenu/dlg_config_world.lua b/builtin/mainmenu/dlg_config_world.lua
index 9bdf92a74..510d9f804 100644
--- a/builtin/mainmenu/dlg_config_world.lua
+++ b/builtin/mainmenu/dlg_config_world.lua
@@ -205,14 +205,19 @@ local function handle_buttons(this, fields)
local mods = worldfile:to_table()
local rawlist = this.data.list:get_raw_list()
+ local was_set = {}
for i = 1, #rawlist do
local mod = rawlist[i]
if not mod.is_modpack and
not mod.is_game_content then
if modname_valid(mod.name) then
- worldfile:set("load_mod_" .. mod.name,
- mod.enabled and "true" or "false")
+ if mod.enabled then
+ worldfile:set("load_mod_" .. mod.name, mod.virtual_path)
+ was_set[mod.name] = true
+ elseif not was_set[mod.name] then
+ worldfile:set("load_mod_" .. mod.name, "false")
+ end
elseif mod.enabled then
gamedata.errormessage = fgettext_ne("Failed to enable mo" ..
"d \"$1\" as it contains disallowed characters. " ..
@@ -256,12 +261,26 @@ local function handle_buttons(this, fields)
if fields.btn_enable_all_mods then
local list = this.data.list:get_raw_list()
+ -- When multiple copies of a mod are installed, we need to avoid enabling multiple of them
+ -- at a time. So lets first collect all the enabled mods, and then use this to exclude
+ -- multiple enables.
+
+ local was_enabled = {}
for i = 1, #list do
if not list[i].is_game_content
- and not list[i].is_modpack then
+ and not list[i].is_modpack and list[i].enabled then
+ was_enabled[list[i].name] = true
+ end
+ end
+
+ for i = 1, #list do
+ if not list[i].is_game_content and not list[i].is_modpack and
+ not was_enabled[list[i].name] then
list[i].enabled = true
+ was_enabled[list[i].name] = true
end
end
+
enabled_all = true
return true
end
diff --git a/builtin/mainmenu/dlg_settings_advanced.lua b/builtin/mainmenu/dlg_settings_advanced.lua
index 06fd32d84..772509670 100644
--- a/builtin/mainmenu/dlg_settings_advanced.lua
+++ b/builtin/mainmenu/dlg_settings_advanced.lua
@@ -378,7 +378,7 @@ local function parse_config_file(read_all, parse_mods)
-- Parse mods
local mods_category_initialized = false
local mods = {}
- get_mods(core.get_modpath(), mods)
+ get_mods(core.get_modpath(), "mods", mods)
for _, mod in ipairs(mods) do
local path = mod.path .. DIR_DELIM .. FILENAME
local file = io.open(path, "r")
diff --git a/builtin/mainmenu/pkgmgr.lua b/builtin/mainmenu/pkgmgr.lua
index 6de671529..eeeb5641b 100644
--- a/builtin/mainmenu/pkgmgr.lua
+++ b/builtin/mainmenu/pkgmgr.lua
@@ -100,12 +100,13 @@ local function load_texture_packs(txtpath, retval)
end
end
-function get_mods(path,retval,modpack)
+function get_mods(path, virtual_path, retval, modpack)
local mods = core.get_dir_list(path, true)
for _, name in ipairs(mods) do
if name:sub(1, 1) ~= "." then
- local prefix = path .. DIR_DELIM .. name
+ local mod_path = path .. DIR_DELIM .. name
+ local mod_virtual_path = virtual_path .. "/" .. name
local toadd = {
dir_name = name,
parent_dir = path,
@@ -114,18 +115,18 @@ function get_mods(path,retval,modpack)
-- Get config file
local mod_conf
- local modpack_conf = io.open(prefix .. DIR_DELIM .. "modpack.conf")
+ local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf")
if modpack_conf then
toadd.is_modpack = true
modpack_conf:close()
- mod_conf = Settings(prefix .. DIR_DELIM .. "modpack.conf"):to_table()
+ mod_conf = Settings(mod_path .. DIR_DELIM .. "modpack.conf"):to_table()
if mod_conf.name then
name = mod_conf.name
toadd.is_name_explicit = true
end
else
- mod_conf = Settings(prefix .. DIR_DELIM .. "mod.conf"):to_table()
+ mod_conf = Settings(mod_path .. DIR_DELIM .. "mod.conf"):to_table()
if mod_conf.name then
name = mod_conf.name
toadd.is_name_explicit = true
@@ -136,12 +137,13 @@ function get_mods(path,retval,modpack)
toadd.name = name
toadd.author = mod_conf.author
toadd.release = tonumber(mod_conf.release) or 0
- toadd.path = prefix
+ toadd.path = mod_path
+ toadd.virtual_path = mod_virtual_path
toadd.type = "mod"
-- Check modpack.txt
-- Note: modpack.conf is already checked above
- local modpackfile = io.open(prefix .. DIR_DELIM .. "modpack.txt")
+ local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
if modpackfile then
modpackfile:close()
toadd.is_modpack = true
@@ -153,7 +155,7 @@ function get_mods(path,retval,modpack)
elseif toadd.is_modpack then
toadd.type = "modpack"
toadd.is_modpack = true
- get_mods(prefix, retval, name)
+ get_mods(mod_path, mod_virtual_path, retval, name)
end
end
end
@@ -397,6 +399,14 @@ function pkgmgr.is_modpack_entirely_enabled(data, name)
return true
end
+local function disable_all_by_name(list, name, except)
+ for i=1, #list do
+ if list[i].name == name and list[i] ~= except then
+ list[i].enabled = false
+ end
+ end
+end
+
---------- toggles or en/disables a mod or modpack and its dependencies --------
local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
if not mod.is_modpack then
@@ -404,6 +414,9 @@ local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mo
if toset == nil then
toset = not mod.enabled
end
+ if toset then
+ disable_all_by_name(list, mod.name, mod)
+ end
if mod.enabled ~= toset then
mod.enabled = toset
toggled_mods[#toggled_mods+1] = mod.name
@@ -648,8 +661,8 @@ function pkgmgr.preparemodlist(data)
--read global mods
local modpaths = core.get_modpaths()
- for _, modpath in ipairs(modpaths) do
- get_mods(modpath, global_mods)
+ for key, modpath in pairs(modpaths) do
+ get_mods(modpath, key, global_mods)
end
for i=1,#global_mods,1 do
@@ -688,22 +701,37 @@ function pkgmgr.preparemodlist(data)
DIR_DELIM .. "world.mt"
local worldfile = Settings(filename)
-
- for key,value in pairs(worldfile:to_table()) do
+ for key, value in pairs(worldfile:to_table()) do
if key:sub(1, 9) == "load_mod_" then
key = key:sub(10)
- local element = nil
- for i=1,#retval,1 do
+ local mod_found = false
+
+ local fallback_found = false
+ local fallback_mod = nil
+
+ for i=1, #retval do
if retval[i].name == key and
- not retval[i].is_modpack then
- element = retval[i]
- break
+ not retval[i].is_modpack then
+ if core.is_yes(value) or retval[i].virtual_path == value then
+ retval[i].enabled = true
+ mod_found = true
+ break
+ elseif fallback_found then
+ -- Only allow fallback if only one mod matches
+ fallback_mod = nil
+ else
+ fallback_found = true
+ fallback_mod = retval[i]
+ end
end
end
- if element ~= nil then
- element.enabled = value ~= "false" and value ~= "nil" and value
- else
- core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
+
+ if not mod_found then
+ if fallback_mod and value:find("/") then
+ fallback_mod.enabled = true
+ else
+ core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
+ end
end
end
end
@@ -797,7 +825,7 @@ function pkgmgr.get_game_mods(gamespec, retval)
if gamespec ~= nil and
gamespec.gamemods_path ~= nil and
gamespec.gamemods_path ~= "" then
- get_mods(gamespec.gamemods_path, retval)
+ get_mods(gamespec.gamemods_path, ("games/%s/mods"):format(gamespec.id), retval)
end
end
diff --git a/doc/menu_lua_api.txt b/doc/menu_lua_api.txt
index a8928441e..c2931af31 100644
--- a/doc/menu_lua_api.txt
+++ b/doc/menu_lua_api.txt
@@ -221,13 +221,24 @@ Package - content which is downloadable from the content db, may or may not be i
* returns path to global user data,
the directory that contains user-provided mods, worlds, games, and texture packs.
* core.get_modpath() (possible in async calls)
- * returns path to global modpath, where mods can be installed
+ * returns path to global modpath in the user path, where mods can be installed
* core.get_modpaths() (possible in async calls)
- * returns list of paths to global modpaths, where mods have been installed
-
+ * returns table of virtual path to global modpaths, where mods have been installed
The difference with "core.get_modpath" is that no mods should be installed in these
directories by Minetest -- they might be read-only.
+ Ex:
+
+ ```
+ {
+ mods = "/home/user/.minetest/mods",
+ share = "/usr/share/minetest/mods",
+
+ -- Custom dirs can be specified by the MINETEST_MOD_DIR env variable
+ ["/path/to/custom/dir"] = "/path/to/custom/dir",
+ }
+ ```
+
* core.get_clientmodpath() (possible in async calls)
* returns path to global client-side modpath
* core.get_gamepath() (possible in async calls)
diff --git a/doc/world_format.txt b/doc/world_format.txt
index eb1d7f728..98c9d2009 100644
--- a/doc/world_format.txt
+++ b/doc/world_format.txt
@@ -133,6 +133,19 @@ Example content (added indentation and - explanations):
load_mod_<mod> = false - whether <mod> is to be loaded in this world
auth_backend = files - which DB backend to use for authentication data
+For load_mod_<mod>, the possible values are:
+
+* `false` - Do not load the mod.
+* `true` - Load the mod from wherever it is found (may cause conflicts if the same mod appears also in some other place).
+* `mods/modpack/moddir` - Relative path to the mod
+ * Must be one of the following:
+ * `mods/`: mods in the user path's mods folder (ex `/home/user/.minetest/mods`)
+ * `share/`: mods in the share's mods folder (ex: `/usr/share/minetest/mods`)
+ * `/path/to/env`: you can use absolute paths to mods inside folders specified with the `MINETEST_MOD_PATH` env variable.
+ * Other locations and absolute paths are not supported
+ * Note that `moddir` is the directory name, not the mod name specified in mod.conf.
+
+
Player File Format
===================
diff --git a/src/content/mods.cpp b/src/content/mods.cpp
index 455506967..f75119bbb 100644
--- a/src/content/mods.cpp
+++ b/src/content/mods.cpp
@@ -89,7 +89,7 @@ void parseModContents(ModSpec &spec)
modpack2_is.close();
spec.is_modpack = true;
- spec.modpack_content = getModsInPath(spec.path, true);
+ spec.modpack_content = getModsInPath(spec.path, spec.virtual_path, true);
} else {
Settings info;
@@ -167,13 +167,14 @@ void parseModContents(ModSpec &spec)
}
std::map<std::string, ModSpec> getModsInPath(
- const std::string &path, bool part_of_modpack)
+ const std::string &path, const std::string &virtual_path, bool part_of_modpack)
{
// NOTE: this function works in mutual recursion with parseModContents
std::map<std::string, ModSpec> result;
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(path);
- std::string modpath;
+ std::string mod_path;
+ std::string mod_virtual_path;
for (const fs::DirListNode &dln : dirlist) {
if (!dln.dir)
@@ -185,10 +186,14 @@ std::map<std::string, ModSpec> getModsInPath(
if (modname[0] == '.')
continue;
- modpath.clear();
- modpath.append(path).append(DIR_DELIM).append(modname);
+ mod_path.clear();
+ mod_path.append(path).append(DIR_DELIM).append(modname);
- ModSpec spec(modname, modpath, part_of_modpack);
+ mod_virtual_path.clear();
+ // Intentionally uses / to keep paths same on different platforms
+ mod_virtual_path.append(virtual_path).append("/").append(modname);
+
+ ModSpec spec(modname, mod_path, part_of_modpack, mod_virtual_path);
parseModContents(spec);
result.insert(std::make_pair(modname, spec));
}
@@ -228,9 +233,9 @@ void ModConfiguration::printUnsatisfiedModsError() const
}
}
-void ModConfiguration::addModsInPath(const std::string &path)
+void ModConfiguration::addModsInPath(const std::string &path, const std::string &virtual_path)
{
- addMods(flattenMods(getModsInPath(path)));
+ addMods(flattenMods(getModsInPath(path, virtual_path)));
}
void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
@@ -294,29 +299,39 @@ void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
}
void ModConfiguration::addModsFromConfig(
- const std::string &settings_path, const std::set<std::string> &mods)
+ const std::string &settings_path,
+ const std::unordered_map<std::string, std::string> &modPaths)
{
Settings conf;
- std::set<std::string> load_mod_names;
+ 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) {
- if (name.compare(0, 9, "load_mod_") == 0 && conf.get(name) != "false" &&
- conf.get(name) != "nil")
- load_mod_names.insert(name.substr(9));
+ 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;
- for (const std::string &i : mods) {
- std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(i));
+ 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;
- if (load_mod_names.count(mod.name) != 0)
- addon_mods.push_back(mod);
- else
+ 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());
@@ -335,9 +350,22 @@ void ModConfiguration::addModsFromConfig(
if (!load_mod_names.empty()) {
errorstream << "The following mods could not be found:";
- for (const std::string &mod : load_mod_names)
- errorstream << " \"" << mod << "\"";
+ 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;
+ }
+ }
+ }
}
}
@@ -413,10 +441,12 @@ void ModConfiguration::resolveDependencies()
ClientModConfiguration::ClientModConfiguration(const std::string &path) :
ModConfiguration(path)
{
- std::set<std::string> paths;
+ std::unordered_map<std::string, std::string> paths;
std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
- paths.insert(path);
- paths.insert(path_user);
+ 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);
diff --git a/src/content/mods.h b/src/content/mods.h
index dd3b6e0e6..ab0a9300e 100644
--- a/src/content/mods.h
+++ b/src/content/mods.h
@@ -51,17 +51,36 @@ struct ModSpec
bool part_of_modpack = false;
bool is_modpack = false;
+ /**
+ * A constructed canonical path to represent this mod's location.
+ * This intended to be used as an identifier for a modpath that tolerates file movement,
+ * and cannot be used to read the mod files.
+ *
+ * Note that `mymod` is the directory name, not the mod name specified in mod.conf.
+ *
+ * Ex:
+ *
+ * - mods/mymod
+ * - mods/mymod (1)
+ * (^ this would have name=mymod in mod.conf)
+ * - mods/modpack1/mymod
+ * - games/mygame/mods/mymod
+ * - worldmods/mymod
+ */
+ std::string virtual_path;
+
// For logging purposes
std::vector<const char *> deprecation_msgs;
// if modpack:
std::map<std::string, ModSpec> modpack_content;
- ModSpec(const std::string &name = "", const std::string &path = "") :
- name(name), path(path)
+
+ ModSpec()
{
}
- ModSpec(const std::string &name, const std::string &path, bool part_of_modpack) :
- name(name), path(path), part_of_modpack(part_of_modpack)
+
+ ModSpec(const std::string &name, const std::string &path, bool part_of_modpack, const std::string &virtual_path) :
+ name(name), path(path), part_of_modpack(part_of_modpack), virtual_path(virtual_path)
{
}
@@ -71,8 +90,16 @@ struct ModSpec
// Retrieves depends, optdepends, is_modpack and modpack_content
void parseModContents(ModSpec &mod);
-std::map<std::string, ModSpec> getModsInPath(
- const std::string &path, bool part_of_modpack = false);
+/**
+ * Gets a list of all mods and modpacks in path
+ *
+ * @param Path to search, should be absolute
+ * @param part_of_modpack Is this searching within a modpack?
+ * @param virtual_path Virtual path for this directory, see comment in ModSpec
+ * @returns map of mods
+ */
+std::map<std::string, ModSpec> getModsInPath(const std::string &path,
+ const std::string &virtual_path, bool part_of_modpack = false);
// replaces modpack Modspecs with their content
std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods);
@@ -97,15 +124,25 @@ public:
protected:
ModConfiguration(const std::string &worldpath);
- // adds all mods in the given path. used for games, modpacks
- // and world-specific mods (worldmods-folders)
- void addModsInPath(const std::string &path);
+
+ /**
+ * 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::set<std::string> &mods);
+ const std::unordered_map<std::string, std::string> &modPaths);
void checkConflictsAndDeps();
diff --git a/src/content/subgames.cpp b/src/content/subgames.cpp
index 62e82e0e4..23355990e 100644
--- a/src/content/subgames.cpp
+++ b/src/content/subgames.cpp
@@ -107,14 +107,13 @@ SubgameSpec findSubgame(const std::string &id)
std::string gamemod_path = game_path + DIR_DELIM + "mods";
// Find mod directories
- std::set<std::string> mods_paths;
- if (!user_game)
- mods_paths.insert(share + DIR_DELIM + "mods");
- if (user != share || user_game)
- mods_paths.insert(user + DIR_DELIM + "mods");
+ std::unordered_map<std::string, std::string> mods_paths;
+ mods_paths["mods"] = user + DIR_DELIM + "mods";
+ if (!user_game && user != share)
+ mods_paths["share"] = share + DIR_DELIM + "mods";
for (const std::string &mod_path : getEnvModPaths()) {
- mods_paths.insert(mod_path);
+ mods_paths[fs::AbsolutePath(mod_path)] = mod_path;
}
// Get meta
diff --git a/src/content/subgames.h b/src/content/subgames.h
index 4a50803e8..d36b4952f 100644
--- a/src/content/subgames.h
+++ b/src/content/subgames.h
@@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <string>
#include <set>
+#include <unordered_map>
#include <vector>
class Settings;
@@ -33,13 +34,16 @@ struct SubgameSpec
int release;
std::string path;
std::string gamemods_path;
- std::set<std::string> addon_mods_paths;
+
+ /**
+ * Map from virtual path to mods path
+ */
+ std::unordered_map<std::string, std::string> addon_mods_paths;
std::string menuicon_path;
SubgameSpec(const std::string &id = "", const std::string &path = "",
const std::string &gamemods_path = "",
- const std::set<std::string> &addon_mods_paths =
- std::set<std::string>(),
+ const std::unordered_map<std::string, std::string> &addon_mods_paths = {},
const std::string &name = "",
const std::string &menuicon_path = "",
const std::string &author = "", int release = 0) :
diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp
index 736ad022f..db031dde5 100644
--- a/src/script/lua_api/l_mainmenu.cpp
+++ b/src/script/lua_api/l_mainmenu.cpp
@@ -323,9 +323,9 @@ int ModApiMainMenu::l_get_games(lua_State *L)
lua_newtable(L);
int table2 = lua_gettop(L);
int internal_index = 1;
- for (const std::string &addon_mods_path : game.addon_mods_paths) {
+ for (const auto &addon_mods_path : game.addon_mods_paths) {
lua_pushnumber(L, internal_index);
- lua_pushstring(L, addon_mods_path.c_str());
+ lua_pushstring(L, addon_mods_path.second.c_str());
lua_settable(L, table2);
internal_index++;
}
@@ -533,14 +533,14 @@ int ModApiMainMenu::l_get_modpath(lua_State *L)
/******************************************************************************/
int ModApiMainMenu::l_get_modpaths(lua_State *L)
{
- int index = 1;
lua_newtable(L);
+
ModApiMainMenu::l_get_modpath(L);
- lua_rawseti(L, -2, index);
+ lua_setfield(L, -2, "mods");
+
for (const std::string &component : getEnvModPaths()) {
- index++;
lua_pushstring(L, component.c_str());
- lua_rawseti(L, -2, index);
+ lua_setfield(L, -2, fs::AbsolutePath(component).c_str());
}
return 1;
}
diff --git a/src/server/mods.cpp b/src/server/mods.cpp
index 609d8c346..ba76d4746 100644
--- a/src/server/mods.cpp
+++ b/src/server/mods.cpp
@@ -41,8 +41,10 @@ ServerModManager::ServerModManager(const std::string &worldpath) :
SubgameSpec gamespec = findWorldSubgame(worldpath);
// Add all game mods and all world mods
- addModsInPath(gamespec.gamemods_path);
- addModsInPath(worldpath + DIR_DELIM + "worldmods");
+ 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");
// Load normal mods
std::string worldmt = worldpath + DIR_DELIM + "world.mt";