]> git.lizzy.rs Git - dragonfireclient.git/commitdiff
Use virtual paths to specify exact mod to enable (#11784)
authorrubenwardy <rw@rubenwardy.com>
Sun, 30 Jan 2022 22:40:53 +0000 (22:40 +0000)
committerGitHub <noreply@github.com>
Sun, 30 Jan 2022 22:40:53 +0000 (22:40 +0000)
builtin/mainmenu/dlg_config_world.lua
builtin/mainmenu/dlg_settings_advanced.lua
builtin/mainmenu/pkgmgr.lua
doc/menu_lua_api.txt
doc/world_format.txt
src/content/mods.cpp
src/content/mods.h
src/content/subgames.cpp
src/content/subgames.h
src/script/lua_api/l_mainmenu.cpp
src/server/mods.cpp

index 9bdf92a74fec2977e94067949d158816a6e4b459..510d9f804247b45f91758ceaa3545e4aa27e2bc5 100644 (file)
@@ -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
index 06fd32d840e2dc8cb1acb2a5baf743f1fe2bbd7d..772509670ec394577b2c7199a38bbe7a1d123ff1 100644 (file)
@@ -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")
index 6de6715296a4b42f2085a802806ceb70cc381ebb..eeeb5641ba2d84e8c4c5a90da963c022734e8147 100644 (file)
@@ -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
 
index a8928441edef28e676e008424a7f995f68aaf73c..c2931af310f6a45aec6d43de75577aaf7a214432 100644 (file)
@@ -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)
index eb1d7f7282b3a5fccaf88dd535d5a660aa82a27a..98c9d200905837bd2018fc958e7a6155e71cd337 100644 (file)
@@ -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
 ===================
 
index 455506967d6d1f22a0dfb5d0f3b7b351758a06d8..f75119bbbb1e3d400c13d95da0d1003eadc56171 100644 (file)
@@ -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);
index dd3b6e0e68fd34d996a370ba349b0917564df938..ab0a9300e89b8aee0e66d90a4a5173b416d3d849 100644 (file)
@@ -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 @@ class ModConfiguration
 
 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();
 
index 62e82e0e4a29235a255460213c5ff25dee3821f9..23355990ec442671acd4e7fe8bf73853cd63645a 100644 (file)
@@ -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
index 4a50803e8305a4cc82ab4e9c41b66c09ae44f2cc..d36b4952fde076fd490ed6dbb17ce4f066efea30 100644 (file)
@@ -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) :
index 736ad022fdfd6c41d84bf88093e4776426c2ca79..db031dde5d49b36c8c1a55b7920d8fce5c4fa959 100644 (file)
@@ -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;
 }
index 609d8c3462dcf028592a83a6035f794787f36ec1..ba76d47464f1ccf1ff00cd950909085b54308dd0 100644 (file)
@@ -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";