diff --git a/builtin/mainmenu/dlg_config_world.lua b/builtin/mainmenu/dlg_config_world.lua index f73256612..e76e10ef7 100644 --- a/builtin/mainmenu/dlg_config_world.lua +++ b/builtin/mainmenu/dlg_config_world.lua @@ -61,12 +61,68 @@ local function init_data(data) data.list:set_sortmode("alphabetic") end + +-- Returns errors errors and a list of all enabled mods (inc. game and world mods) +-- +-- `with_errors` is a table from mod virtual path to `{ type = "error" | "warning" }`. +-- `enabled_mods_by_name` is a table from mod virtual path to `true`. +-- +-- @param world_path Path to the world +-- @param all_mods List of mods, with `enabled` property. +-- @returns with_errors, enabled_mods_by_name +local function check_mod_configuration(world_path, all_mods) + -- Build up lookup tables for enabled mods and all mods by vpath + local enabled_mod_paths = {} + local all_mods_by_vpath = {} + for _, mod in ipairs(all_mods) do + if mod.type == "mod" then + all_mods_by_vpath[mod.virtual_path] = mod + end + if mod.enabled then + enabled_mod_paths[mod.virtual_path] = mod.path + end + end + + -- Use the engine's mod configuration code to resolve dependencies and return any errors + local config_status = core.check_mod_configuration(world_path, enabled_mod_paths) + + -- Build the list of enabled mod virtual paths + local enabled_mods_by_name = {} + for _, mod in ipairs(config_status.satisfied_mods) do + assert(mod.virtual_path ~= "") + enabled_mods_by_name[mod.name] = all_mods_by_vpath[mod.virtual_path] or mod + end + for _, mod in ipairs(config_status.unsatisfied_mods) do + assert(mod.virtual_path ~= "") + enabled_mods_by_name[mod.name] = all_mods_by_vpath[mod.virtual_path] or mod + end + + -- Build the table of errors + local with_error = {} + for _, mod in ipairs(config_status.unsatisfied_mods) do + local error = { type = "warning" } + with_error[mod.virtual_path] = error + + for _, depname in ipairs(mod.unsatisfied_depends) do + if not enabled_mods_by_name[depname] then + error.type = "error" + break + end + end + end + + return with_error, enabled_mods_by_name +end + local function get_formspec(data) if not data.list then init_data(data) end - local mod = data.list:get_list()[data.selected_mod] or {name = ""} + local all_mods = data.list:get_list() + local with_error, enabled_mods_by_name = check_mod_configuration(data.worldspec.path, all_mods) + + local mod = all_mods[data.selected_mod] or {name = ""} local retval = "size[11.5,7.5,true]" .. @@ -87,6 +143,29 @@ local function get_formspec(data) "textarea[0.25,0.7;5.75,7.2;;" .. info .. ";]" else local hard_deps, soft_deps = pkgmgr.get_dependencies(mod.path) + + -- Add error messages to dep lists + if mod.enabled or mod.is_game_content then + for i, dep_name in ipairs(hard_deps) do + local dep = enabled_mods_by_name[dep_name] + if not dep then + hard_deps[i] = mt_color_red .. dep_name .. " " .. fgettext("(Unsatisfied)") + elseif with_error[dep.virtual_path] then + hard_deps[i] = mt_color_orange .. dep_name .. " " .. fgettext("(Enabled, has error)") + else + hard_deps[i] = mt_color_green .. dep_name + end + end + for i, dep_name in ipairs(soft_deps) do + local dep = enabled_mods_by_name[dep_name] + if dep and with_error[dep.virtual_path] then + soft_deps[i] = mt_color_orange .. dep_name .. " " .. fgettext("(Enabled, has error)") + elseif dep then + soft_deps[i] = mt_color_green .. dep_name + end + end + end + local hard_deps_str = table.concat(hard_deps, ",") local soft_deps_str = table.concat(soft_deps, ",") @@ -138,7 +217,6 @@ local function get_formspec(data) if mod.name ~= "" and not mod.is_game_content then if mod.is_modpack then - if pkgmgr.is_modpack_entirely_enabled(data, mod.name) then retval = retval .. "button[5.5,0.125;3,0.5;btn_mp_disable;" .. @@ -167,9 +245,12 @@ local function get_formspec(data) local use_technical_names = core.settings:get_bool("show_technical_names") return retval .. - "tablecolumns[color;tree;text]" .. + "tablecolumns[color;tree;image,align=inline,width=1.5,0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. + ",1=" .. core.formspec_escape(defaulttexturedir .. "checkbox_16_white.png") .. + ",2=" .. core.formspec_escape(defaulttexturedir .. "error_icon_orange.png") .. + ",3=" .. core.formspec_escape(defaulttexturedir .. "error_icon_red.png") .. ";text]" .. "table[5.5,0.75;5.75,6;world_config_modlist;" .. - pkgmgr.render_packagelist(data.list, use_technical_names) .. ";" .. data.selected_mod .."]" + pkgmgr.render_packagelist(data.list, use_technical_names, with_error) .. ";" .. data.selected_mod .."]" end local function handle_buttons(this, fields) diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index d073f5a57..f890765fa 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -21,6 +21,7 @@ mt_color_lightblue = "#99CCFF" mt_color_green = "#72FF63" mt_color_dark_green = "#25C191" mt_color_orange = "#FF8800" +mt_color_red = "#FF3300" local menupath = core.get_mainmenu_path() local basepath = core.get_builtin_path() diff --git a/builtin/mainmenu/pkgmgr.lua b/builtin/mainmenu/pkgmgr.lua index b2f3243c4..32a65fd08 100644 --- a/builtin/mainmenu/pkgmgr.lua +++ b/builtin/mainmenu/pkgmgr.lua @@ -337,7 +337,7 @@ function pkgmgr.identify_modname(modpath,filename) return nil end -------------------------------------------------------------------------------- -function pkgmgr.render_packagelist(render_list, use_technical_names) +function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) if not render_list then if not pkgmgr.global_mods then pkgmgr.refresh_globals() @@ -349,24 +349,59 @@ function pkgmgr.render_packagelist(render_list, use_technical_names) local retval = {} for i, v in ipairs(list) do local color = "" + local icon = 0 + local error = with_error and with_error[v.virtual_path] + local function update_error(val) + if val and (not error or (error.type == "warning" and val.type == "error")) then + error = val + end + end + if v.is_modpack then local rawlist = render_list:get_raw_list() color = mt_color_dark_green - for j = 1, #rawlist, 1 do - if rawlist[j].modpack == list[i].name and - not rawlist[j].enabled then - -- Modpack not entirely enabled so showing as grey - color = mt_color_grey - break + for j = 1, #rawlist do + if rawlist[j].modpack == list[i].name then + if with_error then + update_error(with_error[rawlist[j].virtual_path]) + end + + if rawlist[j].enabled then + icon = 1 + else + -- Modpack not entirely enabled so showing as grey + color = mt_color_grey + end end end elseif v.is_game_content or v.type == "game" then + icon = 1 color = mt_color_blue + + local rawlist = render_list:get_raw_list() + if v.type == "game" and with_error then + for j = 1, #rawlist do + if rawlist[j].is_game_content then + update_error(with_error[rawlist[j].virtual_path]) + end + end + end elseif v.enabled or v.type == "txp" then + icon = 1 color = mt_color_green end + if error then + if error.type == "warning" then + color = mt_color_orange + icon = 2 + else + color = mt_color_red + icon = 3 + end + end + retval[#retval + 1] = color if v.modpack ~= nil or v.loc == "game" then retval[#retval + 1] = "1" @@ -374,6 +409,10 @@ function pkgmgr.render_packagelist(render_list, use_technical_names) retval[#retval + 1] = "0" end + if with_error then + retval[#retval + 1] = icon + end + if use_technical_names then retval[#retval + 1] = core.formspec_escape(v.list_name or v.name) else @@ -503,7 +542,7 @@ function pkgmgr.enable_mod(this, toset) if not mod_to_enable then core.log("warning", "Mod dependency \"" .. name .. "\" not found!") - else + elseif not mod_to_enable.is_game_content then if not mod_to_enable.enabled then mod_to_enable.enabled = true toggled_mods[#toggled_mods+1] = mod_to_enable.name diff --git a/doc/menu_lua_api.txt b/doc/menu_lua_api.txt index 63e229135..4d495f21e 100644 --- a/doc/menu_lua_api.txt +++ b/doc/menu_lua_api.txt @@ -276,7 +276,18 @@ Package - content which is downloadable from the content db, may or may not be i depends = {"mod", "names"}, -- mods only optional_depends = {"mod", "names"}, -- mods only } +* core.check_mod_configuration(world_path, mod_paths) + * Checks whether configuration is valid. + * `world_path`: path to the world + * `mod_paths`: list of enabled mod paths + * returns: + { + is_consistent = true, -- true is consistent, false otherwise + unsatisfied_mods = {}, -- list of mod specs + satisfied_mods = {}, -- list of mod specs + error_message = "", -- message or nil + } Logging ------- diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 10670a60a..166980025 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -2143,3 +2143,35 @@ void push_collision_move_result(lua_State *L, const collisionMoveResult &res) lua_setfield(L, -2, "collisions"); /**/ } + + +void push_mod_spec(lua_State *L, const ModSpec &spec, bool include_unsatisfied) +{ + lua_newtable(L); + + lua_pushstring(L, spec.name.c_str()); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, spec.author.c_str()); + lua_setfield(L, -2, "author"); + + lua_pushinteger(L, spec.release); + lua_setfield(L, -2, "release"); + + lua_pushstring(L, spec.desc.c_str()); + lua_setfield(L, -2, "description"); + + lua_pushstring(L, spec.path.c_str()); + lua_setfield(L, -2, "path"); + + 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++); + } + lua_setfield(L, -2, "unsatisfied_depends"); +} diff --git a/src/script/common/c_content.h b/src/script/common/c_content.h index 06f80328a..ade3e4c1e 100644 --- a/src/script/common/c_content.h +++ b/src/script/common/c_content.h @@ -42,6 +42,7 @@ extern "C" { // We do a explicit path include because by default c_content.h include src/client/hud.h // prior to the src/hud.h, which is not good on server only build #include "../../hud.h" +#include "content/mods.h" namespace Json { class Value; } @@ -204,3 +205,5 @@ void push_hud_element (lua_State *L, HudElement *elem); bool read_hud_change (lua_State *L, HudElementStat &stat, HudElement *elem, void **value); void push_collision_move_result(lua_State *L, const collisionMoveResult &res); + +void push_mod_spec(lua_State *L, const ModSpec &spec, bool include_unsatisfied); diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index 4a847ed6d..cf4a057e1 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/client.h" #include "client/renderingengine.h" #include "network/networkprotocol.h" - +#include "content/mod_configuration.h" /******************************************************************************/ std::string ModApiMainMenu::getTextData(lua_State *L, std::string name) @@ -409,6 +409,100 @@ int ModApiMainMenu::l_get_content_info(lua_State *L) return 1; } +/******************************************************************************/ +int ModApiMainMenu::l_check_mod_configuration(lua_State *L) +{ + std::string worldpath = luaL_checkstring(L, 1); + + ModConfiguration modmgr; + + // Add all game mods + SubgameSpec gamespec = findWorldSubgame(worldpath); + modmgr.addGameMods(gamespec); + modmgr.addModsInPath(worldpath + DIR_DELIM + "worldmods", "worldmods"); + + // Add user-configured mods + std::vector modSpecs; + + luaL_checktype(L, 2, LUA_TTABLE); + + lua_pushnil(L); + while (lua_next(L, 2)) { + // Ignore non-string keys + if (lua_type(L, -2) != LUA_TSTRING) { + throw LuaError( + "Unexpected non-string key in table passed to " + "core.check_mod_configuration"); + } + + std::string modpath = luaL_checkstring(L, -1); + lua_pop(L, 1); + std::string virtual_path = lua_tostring(L, -1); + + modSpecs.emplace_back(); + ModSpec &spec = modSpecs.back(); + spec.name = fs::GetFilenameFromPath(modpath.c_str()); + spec.path = modpath; + spec.virtual_path = virtual_path; + if (!parseModContents(spec)) { + throw LuaError("Not a mod!"); + } + } + + modmgr.addMods(modSpecs); + try { + modmgr.checkConflictsAndDeps(); + } catch (const ModError &err) { + errorstream << err.what() << std::endl; + + lua_newtable(L); + + lua_pushboolean(L, false); + lua_setfield(L, -2, "is_consistent"); + + lua_newtable(L); + lua_setfield(L, -2, "unsatisfied_mods"); + + lua_newtable(L); + lua_setfield(L, -2, "satisfied_mods"); + + lua_pushstring(L, err.what()); + lua_setfield(L, -2, "error_message"); + return 1; + } + + + lua_newtable(L); + + lua_pushboolean(L, modmgr.isConsistent()); + lua_setfield(L, -2, "is_consistent"); + + lua_newtable(L); + int top = lua_gettop(L); + unsigned int index = 1; + for (const auto &spec : modmgr.getUnsatisfiedMods()) { + lua_pushnumber(L, index); + push_mod_spec(L, spec, true); + lua_settable(L, top); + index++; + } + + lua_setfield(L, -2, "unsatisfied_mods"); + + lua_newtable(L); + top = lua_gettop(L); + index = 1; + for (const auto &spec : modmgr.getMods()) { + lua_pushnumber(L, index); + push_mod_spec(L, spec, false); + lua_settable(L, top); + index++; + } + lua_setfield(L, -2, "satisfied_mods"); + + return 1; +} + /******************************************************************************/ int ModApiMainMenu::l_show_keys_menu(lua_State *L) { @@ -921,6 +1015,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(get_worlds); API_FCT(get_games); API_FCT(get_content_info); + API_FCT(check_mod_configuration); API_FCT(start); API_FCT(close); API_FCT(show_keys_menu); diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 6ceff6dd7..9dc40c7f4 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -82,6 +82,8 @@ private: static int l_get_content_info(lua_State *L); + static int l_check_mod_configuration(lua_State *L); + //gui static int l_show_keys_menu(lua_State *L); diff --git a/textures/base/pack/checkbox_16_white.png b/textures/base/pack/checkbox_16_white.png new file mode 100644 index 000000000..0cf0f3e65 Binary files /dev/null and b/textures/base/pack/checkbox_16_white.png differ diff --git a/textures/base/pack/error_icon_orange.png b/textures/base/pack/error_icon_orange.png new file mode 100644 index 000000000..1f1586f21 Binary files /dev/null and b/textures/base/pack/error_icon_orange.png differ diff --git a/textures/base/pack/error_icon_red.png b/textures/base/pack/error_icon_red.png new file mode 100644 index 000000000..1f5bafbf4 Binary files /dev/null and b/textures/base/pack/error_icon_red.png differ