From 392c054be9d19974c873336a2d62c29f9c894096 Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Wed, 29 Oct 2025 22:56:30 +0100 Subject: [PATCH] Main menu: Fix mod detection within nested modpacks * Re-use the C++ implementation of mod detection * Correctly show up nested modpacks in the main menu --- builtin/mainmenu/content/pkgmgr.lua | 89 ++++++----------------------- doc/menu_lua_api.md | 4 ++ src/content/mod_configuration.cpp | 2 +- src/content/mods.cpp | 56 +++++++++++------- src/content/mods.h | 14 +++-- src/gui/guiEngine.cpp | 2 +- src/script/common/c_content.cpp | 14 +++-- src/script/lua_api/l_mainmenu.cpp | 29 ++++++++++ src/script/lua_api/l_mainmenu.h | 2 + 9 files changed, 106 insertions(+), 106 deletions(-) diff --git a/builtin/mainmenu/content/pkgmgr.lua b/builtin/mainmenu/content/pkgmgr.lua index 2863e2a5f..63c181d26 100644 --- a/builtin/mainmenu/content/pkgmgr.lua +++ b/builtin/mainmenu/content/pkgmgr.lua @@ -94,77 +94,25 @@ pkgmgr = {} -- @param path Absolute directory path to scan recursively -- @param virtual_path Prettified unique path (e.g. "mods", "mods/mt_modpack") -- @param listing Input. Flat array to insert located mods and modpacks --- @param modpack Currently processing modpack or nil/"" if none (recursion) -function pkgmgr.get_mods(path, virtual_path, listing, modpack) - local mods = core.get_dir_list(path, true) - local added = {} - for _, name in ipairs(mods) do - if name:sub(1, 1) ~= "." then - local mod_path = path .. DIR_DELIM .. name - local mod_virtual_path = virtual_path .. "/" .. name - local toadd = { - dir_name = name, - parent_dir = path, - } - listing[#listing + 1] = toadd - added[#added + 1] = toadd +function pkgmgr.get_mods(path, virtual_path, listing) + local mods = core.get_mod_list(path, virtual_path) + local parent = {} + for i, toadd in ipairs(mods) do + listing[#listing + 1] = toadd - -- Get config file - local mod_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(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(mod_path .. DIR_DELIM .. "mod.conf"):to_table() - if mod_conf.name then - name = mod_conf.name - toadd.is_name_explicit = true - end - end - - -- Read from config - toadd.name = name - toadd.title = mod_conf.title - toadd.author = mod_conf.author - toadd.release = tonumber(mod_conf.release) or 0 - 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(mod_path .. DIR_DELIM .. "modpack.txt") - if modpackfile then - modpackfile:close() - toadd.is_modpack = true - end - - -- Deal with modpack contents - if modpack and modpack ~= "" then - toadd.modpack = modpack - elseif toadd.is_modpack then - toadd.type = "modpack" - toadd.is_modpack = true - pkgmgr.get_mods(mod_path, mod_virtual_path, listing, name) - end + if toadd.is_modpack then + parent[toadd.modpack_depth + 1] = toadd + elseif parent[toadd.modpack_depth] then + toadd.modpack = parent[toadd.modpack_depth].name end + + local parent_dir, dir_name = toadd.path:match("^(.+)[/\\]([^/\\]+)$") + toadd.dir_name = dir_name + toadd.parent_dir = parent_dir + toadd.type = toadd.is_modpack and "modpack" or "mod" end - pkgmgr.update_translations(added) - - if not modpack then - -- Sort all when the recursion is done - table.sort(listing, function(a, b) - return a.virtual_path:lower() < b.virtual_path:lower() - end) - end + pkgmgr.update_translations(mods) end -------------------------------------------------------------------------------- @@ -346,11 +294,8 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_icon) end retval[#retval + 1] = color - if v.modpack ~= nil or v.loc == "game" then - retval[#retval + 1] = "1" - else - retval[#retval + 1] = "0" - end + -- `v.modpack_depth` is `nil` for the selected game (treated as level 0) + retval[#retval + 1] = (v.modpack_depth or 0) + (v.loc == "game" and 1 or 0) if with_icon then retval[#retval + 1] = icon diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 8a1a35332..e4d9efa2f 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -342,6 +342,10 @@ Package - content which is downloadable from the content db, may or may not be i optional_depends = {"mod", "names"}, -- mods only } ``` +* `core.get_mod_list(path, virtual_path)` + * Returns a flat list of mod and modpack information found within the specified path. + * Each entry consists of the fields `name`, `author`, `release`, `description`, + `path`, `virtual_path`, `is_name_explicit`, `is_modpack`, `modpack_depth`. * `core.check_mod_configuration(world_path, mod_paths)` * Checks whether configuration is valid. * `world_path`: path to the world diff --git a/src/content/mod_configuration.cpp b/src/content/mod_configuration.cpp index a2731d0f3..8a33497ce 100644 --- a/src/content/mod_configuration.cpp +++ b/src/content/mod_configuration.cpp @@ -60,7 +60,7 @@ void ModConfiguration::addMods(const std::vector &new_mods) std::set seen_this_iteration; for (const ModSpec &mod : new_mods) { - if (mod.part_of_modpack != want_from_modpack) + if ((mod.modpack_depth > 0) != want_from_modpack) continue; // unrelated to this code, but we want to assert it somewhere diff --git a/src/content/mods.cpp b/src/content/mods.cpp index 2fb92d6c8..841f0b9e7 100644 --- a/src/content/mods.cpp +++ b/src/content/mods.cpp @@ -61,24 +61,39 @@ bool parseModContents(ModSpec &spec) spec.is_modpack = false; spec.modpack_content.clear(); + std::string conf_filename; + // Handle modpacks (defined by containing modpack.txt) - if (fs::IsFile(spec.path + DIR_DELIM + "modpack.txt") || - fs::IsFile(spec.path + DIR_DELIM + "modpack.conf")) { + if (fs::IsFile(spec.path + DIR_DELIM + "modpack.conf")) { + spec.is_modpack = true; + conf_filename = "modpack.conf"; + } else if (fs::IsFile(spec.path + DIR_DELIM + "modpack.txt")) { spec.is_modpack = true; - spec.modpack_content = getModsInPath(spec.path, spec.virtual_path, true); - return true; } else if (!fs::IsFile(spec.path + DIR_DELIM + "init.lua")) { return false; + } else { + // Is a mod + conf_filename = "mod.conf"; } + if (spec.is_modpack) + spec.modpack_content = getModsInPath(spec.path, spec.virtual_path, spec.modpack_depth + 1); Settings info; - info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str()); + if (!conf_filename.empty()) + info.readConfigFile((spec.path + DIR_DELIM + conf_filename).c_str()); - if (info.exists("name")) + if (info.exists("name")) { spec.name = info.get("name"); - else + spec.is_name_explicit = true; + } else if (!spec.is_modpack) { spec.deprecation_msgs.push_back("Mods not having a mod.conf file with the name is deprecated."); + } + + if (info.exists("description")) + spec.desc = info.get("description"); + else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc)) + spec.deprecation_msgs.push_back("description.txt is deprecated, please use mod[pack].conf instead."); if (info.exists("author")) spec.author = info.get("author"); @@ -86,6 +101,12 @@ bool parseModContents(ModSpec &spec) if (info.exists("release")) spec.release = info.getS32("release"); + + // The subsequent fields are not available for modpacks + if (spec.is_modpack) + return true; + + // Attempt to load dependencies from mod.conf bool mod_conf_has_depends = false; if (info.exists("depends")) { @@ -135,16 +156,11 @@ bool parseModContents(ModSpec &spec) } } - if (info.exists("description")) - spec.desc = info.get("description"); - else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc)) - spec.deprecation_msgs.push_back("description.txt is deprecated, please use mod.conf instead."); - return true; } std::map getModsInPath( - const std::string &path, const std::string &virtual_path, bool part_of_modpack) + const std::string &path, const std::string &virtual_path, int modpack_depth) { // NOTE: this function works in mutual recursion with parseModContents @@ -171,7 +187,7 @@ std::map getModsInPath( // 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); + ModSpec spec(modname, mod_path, modpack_depth, mod_virtual_path); if (parseModContents(spec)) { result[modname] = std::move(spec); } @@ -179,19 +195,19 @@ std::map getModsInPath( return result; } -std::vector flattenMods(const std::map &mods) +std::vector flattenMods(const std::map &mods, + bool discard_modpacks) { std::vector result; for (const auto &it : mods) { const ModSpec &mod = it.second; + if (!mod.is_modpack || !discard_modpacks) { + result.push_back(mod); + } if (mod.is_modpack) { - std::vector content = flattenMods(mod.modpack_content); + std::vector content = flattenMods(mod.modpack_content, discard_modpacks); result.reserve(result.size() + content.size()); result.insert(result.end(), content.begin(), content.end()); - - } else // not a modpack - { - result.push_back(mod); } } return result; diff --git a/src/content/mods.h b/src/content/mods.h index 2e7c6621a..5ee86c5eb 100644 --- a/src/content/mods.h +++ b/src/content/mods.h @@ -16,6 +16,7 @@ class ModStorageDatabase; struct ModSpec { + bool is_name_explicit = false; //< 'Specified in a .conf file?' std::string name; std::string author; std::string path; // absolute path on disk @@ -27,7 +28,7 @@ struct ModSpec std::unordered_set optdepends; std::unordered_set unsatisfied_depends; - bool part_of_modpack = false; + int modpack_depth = 0; //< Modpack depth, 0 = no parent modpack bool is_modpack = false; /** @@ -58,8 +59,8 @@ struct ModSpec { } - 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) + ModSpec(const std::string &name, const std::string &path, int modpack_depth, const std::string &virtual_path) : + name(name), path(path), modpack_depth(modpack_depth), virtual_path(virtual_path) { } @@ -77,15 +78,16 @@ struct ModSpec * 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 modpack_depth If > 0: Is this searching within a modpack * @param virtual_path Virtual path for this directory, see comment in ModSpec * @returns map of mods */ std::map getModsInPath(const std::string &path, - const std::string &virtual_path, bool part_of_modpack = false); + const std::string &virtual_path, int modpack_depth = 0); // replaces modpack Modspecs with their content -std::vector flattenMods(const std::map &mods); +std::vector flattenMods(const std::map &mods, + bool discard_modpacks = true); class ModStorage : public IMetadata diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index c8129ec0a..77cc91e55 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -211,7 +211,7 @@ std::string findLocaleFileWithExtension(const std::string &path) /******************************************************************************/ std::string findLocaleFileInMods(const std::string &path, const std::string &filename_no_ext) { - std::vector mods = flattenMods(getModsInPath(path, "root", true)); + std::vector mods = flattenMods(getModsInPath(path, "root", 0)); for (const auto &mod : mods) { std::string ret = findLocaleFileWithExtension( diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 0a41ffcd8..ab3eb9687 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -2654,11 +2654,13 @@ void push_mod_spec(lua_State *L, const ModSpec &spec, bool include_unsatisfied) lua_pushstring(L, spec.virtual_path.c_str()); lua_setfield(L, -2, "virtual_path"); - lua_newtable(L); - int i = 1; - for (const auto &dep : spec.unsatisfied_depends) { - lua_pushstring(L, dep.c_str()); - lua_rawseti(L, -2, i++); + if (include_unsatisfied) { + lua_newtable(L); + int i = 1; + for (const auto &dep : spec.unsatisfied_depends) { + lua_pushstring(L, dep.c_str()); + lua_rawseti(L, -2, i++); + } + lua_setfield(L, -2, "unsatisfied_depends"); } - lua_setfield(L, -2, "unsatisfied_depends"); } diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index 87155ed22..1ffc31978 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -421,6 +421,34 @@ int ModApiMainMenu::l_get_content_info(lua_State *L) return 1; } +/******************************************************************************/ +int ModApiMainMenu::l_get_mod_list(lua_State *L) +{ + std::string path = luaL_checkstring(L, 1); + std::string virtual_path = luaL_checkstring(L, 2); + + CHECK_SECURE_PATH(L, path.c_str(), false) + + std::vector mods_flat = flattenMods(getModsInPath(path, virtual_path), false); + int i = 0; + lua_createtable(L, mods_flat.size(), 0); + for (const ModSpec &spec : mods_flat) { + push_mod_spec(L, spec, false); + + lua_pushboolean(L, spec.is_name_explicit); + lua_setfield(L, -2, "is_name_explicit"); + + lua_pushboolean(L, spec.is_modpack); + lua_setfield(L, -2, "is_modpack"); + + lua_pushinteger(L, spec.modpack_depth); + lua_setfield(L, -2, "modpack_depth"); + + lua_rawseti(L, -2, ++i); // assign to return table + } + return 1; +} + /******************************************************************************/ int ModApiMainMenu::l_check_mod_configuration(lua_State *L) { @@ -1045,6 +1073,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(get_worlds); API_FCT(get_games); API_FCT(get_content_info); + API_FCT(get_mod_list); API_FCT(check_mod_configuration); API_FCT(get_content_translation); API_FCT(start); diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 13c74fcd6..848bf2dea 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -58,6 +58,8 @@ private: static int l_get_content_info(lua_State *L); + static int l_get_mod_list(lua_State *L); + static int l_check_mod_configuration(lua_State *L); static int l_get_content_translation(lua_State *L);