1
0
mirror of https://github.com/luanti-org/luanti.git synced 2025-11-20 16:35:25 +01:00

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
This commit is contained in:
SmallJoker
2025-10-29 22:56:30 +01:00
committed by sfan5
parent bf01cdf823
commit 392c054be9
9 changed files with 106 additions and 106 deletions

View File

@@ -94,77 +94,25 @@ pkgmgr = {}
-- @param path Absolute directory path to scan recursively -- @param path Absolute directory path to scan recursively
-- @param virtual_path Prettified unique path (e.g. "mods", "mods/mt_modpack") -- @param virtual_path Prettified unique path (e.g. "mods", "mods/mt_modpack")
-- @param listing Input. Flat array to insert located mods and modpacks -- @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)
function pkgmgr.get_mods(path, virtual_path, listing, modpack) local mods = core.get_mod_list(path, virtual_path)
local mods = core.get_dir_list(path, true) local parent = {}
local added = {} for i, toadd in ipairs(mods) do
for _, name in ipairs(mods) do listing[#listing + 1] = toadd
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
-- Get config file if toadd.is_modpack then
local mod_conf parent[toadd.modpack_depth + 1] = toadd
local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf") elseif parent[toadd.modpack_depth] then
if modpack_conf then toadd.modpack = parent[toadd.modpack_depth].name
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
end 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 end
pkgmgr.update_translations(added) pkgmgr.update_translations(mods)
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
end end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -346,11 +294,8 @@ function pkgmgr.render_packagelist(render_list, use_technical_names, with_icon)
end end
retval[#retval + 1] = color retval[#retval + 1] = color
if v.modpack ~= nil or v.loc == "game" then -- `v.modpack_depth` is `nil` for the selected game (treated as level 0)
retval[#retval + 1] = "1" retval[#retval + 1] = (v.modpack_depth or 0) + (v.loc == "game" and 1 or 0)
else
retval[#retval + 1] = "0"
end
if with_icon then if with_icon then
retval[#retval + 1] = icon retval[#retval + 1] = icon

View File

@@ -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 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)` * `core.check_mod_configuration(world_path, mod_paths)`
* Checks whether configuration is valid. * Checks whether configuration is valid.
* `world_path`: path to the world * `world_path`: path to the world

View File

@@ -60,7 +60,7 @@ void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
std::set<std::string> seen_this_iteration; std::set<std::string> seen_this_iteration;
for (const ModSpec &mod : new_mods) { for (const ModSpec &mod : new_mods) {
if (mod.part_of_modpack != want_from_modpack) if ((mod.modpack_depth > 0) != want_from_modpack)
continue; continue;
// unrelated to this code, but we want to assert it somewhere // unrelated to this code, but we want to assert it somewhere

View File

@@ -61,24 +61,39 @@ bool parseModContents(ModSpec &spec)
spec.is_modpack = false; spec.is_modpack = false;
spec.modpack_content.clear(); spec.modpack_content.clear();
std::string conf_filename;
// Handle modpacks (defined by containing modpack.txt) // Handle modpacks (defined by containing modpack.txt)
if (fs::IsFile(spec.path + DIR_DELIM + "modpack.txt") || if (fs::IsFile(spec.path + DIR_DELIM + "modpack.conf")) {
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.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")) { } else if (!fs::IsFile(spec.path + DIR_DELIM + "init.lua")) {
return false; 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; 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"); 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."); 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")) if (info.exists("author"))
spec.author = info.get("author"); spec.author = info.get("author");
@@ -86,6 +101,12 @@ bool parseModContents(ModSpec &spec)
if (info.exists("release")) if (info.exists("release"))
spec.release = info.getS32("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 // Attempt to load dependencies from mod.conf
bool mod_conf_has_depends = false; bool mod_conf_has_depends = false;
if (info.exists("depends")) { 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; return true;
} }
std::map<std::string, ModSpec> getModsInPath( std::map<std::string, ModSpec> 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 // NOTE: this function works in mutual recursion with parseModContents
@@ -171,7 +187,7 @@ std::map<std::string, ModSpec> getModsInPath(
// Intentionally uses / to keep paths same on different platforms // Intentionally uses / to keep paths same on different platforms
mod_virtual_path.append(virtual_path).append("/").append(modname); 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)) { if (parseModContents(spec)) {
result[modname] = std::move(spec); result[modname] = std::move(spec);
} }
@@ -179,19 +195,19 @@ std::map<std::string, ModSpec> getModsInPath(
return result; return result;
} }
std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods) std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods,
bool discard_modpacks)
{ {
std::vector<ModSpec> result; std::vector<ModSpec> result;
for (const auto &it : mods) { for (const auto &it : mods) {
const ModSpec &mod = it.second; const ModSpec &mod = it.second;
if (!mod.is_modpack || !discard_modpacks) {
result.push_back(mod);
}
if (mod.is_modpack) { if (mod.is_modpack) {
std::vector<ModSpec> content = flattenMods(mod.modpack_content); std::vector<ModSpec> content = flattenMods(mod.modpack_content, discard_modpacks);
result.reserve(result.size() + content.size()); result.reserve(result.size() + content.size());
result.insert(result.end(), content.begin(), content.end()); result.insert(result.end(), content.begin(), content.end());
} else // not a modpack
{
result.push_back(mod);
} }
} }
return result; return result;

View File

@@ -16,6 +16,7 @@ class ModStorageDatabase;
struct ModSpec struct ModSpec
{ {
bool is_name_explicit = false; //< 'Specified in a .conf file?'
std::string name; std::string name;
std::string author; std::string author;
std::string path; // absolute path on disk std::string path; // absolute path on disk
@@ -27,7 +28,7 @@ struct ModSpec
std::unordered_set<std::string> optdepends; std::unordered_set<std::string> optdepends;
std::unordered_set<std::string> unsatisfied_depends; std::unordered_set<std::string> unsatisfied_depends;
bool part_of_modpack = false; int modpack_depth = 0; //< Modpack depth, 0 = no parent modpack
bool is_modpack = false; 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) : ModSpec(const std::string &name, const std::string &path, int modpack_depth, const std::string &virtual_path) :
name(name), path(path), part_of_modpack(part_of_modpack), virtual_path(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 * Gets a list of all mods and modpacks in path
* *
* @param Path to search, should be absolute * @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 * @param virtual_path Virtual path for this directory, see comment in ModSpec
* @returns map of mods * @returns map of mods
*/ */
std::map<std::string, ModSpec> getModsInPath(const std::string &path, std::map<std::string, ModSpec> 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 // replaces modpack Modspecs with their content
std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods); std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods,
bool discard_modpacks = true);
class ModStorage : public IMetadata class ModStorage : public IMetadata

View File

@@ -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::string findLocaleFileInMods(const std::string &path, const std::string &filename_no_ext)
{ {
std::vector<ModSpec> mods = flattenMods(getModsInPath(path, "root", true)); std::vector<ModSpec> mods = flattenMods(getModsInPath(path, "root", 0));
for (const auto &mod : mods) { for (const auto &mod : mods) {
std::string ret = findLocaleFileWithExtension( std::string ret = findLocaleFileWithExtension(

View File

@@ -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_pushstring(L, spec.virtual_path.c_str());
lua_setfield(L, -2, "virtual_path"); lua_setfield(L, -2, "virtual_path");
lua_newtable(L); if (include_unsatisfied) {
int i = 1; lua_newtable(L);
for (const auto &dep : spec.unsatisfied_depends) { int i = 1;
lua_pushstring(L, dep.c_str()); for (const auto &dep : spec.unsatisfied_depends) {
lua_rawseti(L, -2, i++); lua_pushstring(L, dep.c_str());
lua_rawseti(L, -2, i++);
}
lua_setfield(L, -2, "unsatisfied_depends");
} }
lua_setfield(L, -2, "unsatisfied_depends");
} }

View File

@@ -421,6 +421,34 @@ int ModApiMainMenu::l_get_content_info(lua_State *L)
return 1; 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<ModSpec> 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) 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_worlds);
API_FCT(get_games); API_FCT(get_games);
API_FCT(get_content_info); API_FCT(get_content_info);
API_FCT(get_mod_list);
API_FCT(check_mod_configuration); API_FCT(check_mod_configuration);
API_FCT(get_content_translation); API_FCT(get_content_translation);
API_FCT(start); API_FCT(start);

View File

@@ -58,6 +58,8 @@ private:
static int l_get_content_info(lua_State *L); 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_check_mod_configuration(lua_State *L);
static int l_get_content_translation(lua_State *L); static int l_get_content_translation(lua_State *L);