]> git.lizzy.rs Git - minetest.git/commitdiff
Add dependency resolution to ContentDB (#9997)
authorrubenwardy <rw@rubenwardy.com>
Wed, 23 Dec 2020 14:42:18 +0000 (14:42 +0000)
committerGitHub <noreply@github.com>
Wed, 23 Dec 2020 14:42:18 +0000 (14:42 +0000)
builtin/mainmenu/dlg_contentstore.lua
builtin/mainmenu/dlg_create_world.lua
builtin/mainmenu/init.lua
builtin/settingtypes.txt

index 7a96df2a59a9843f60342b0f62658f7a1661e421..548bae67e7ba17917cd27b8c9fbe898a189c9f2f 100644 (file)
@@ -159,6 +159,290 @@ local function queue_download(package)
        end
 end
 
+local function get_raw_dependencies(package)
+       if package.raw_deps then
+               return package.raw_deps
+       end
+
+       local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
+       local version = core.get_version()
+       local base_url = core.settings:get("contentdb_url")
+       local url = base_url .. url_fmt:format(package.id, core.get_max_supp_proto(), version.string)
+
+       local response = http.fetch_sync({ url = url })
+       if not response.succeeded then
+               return
+       end
+
+       local data = core.parse_json(response.data) or {}
+
+       local content_lookup = {}
+       for _, pkg in pairs(store.packages_full) do
+               content_lookup[pkg.id] = pkg
+       end
+
+       for id, raw_deps in pairs(data) do
+               local package2 = content_lookup[id:lower()]
+               if package2 and not package2.raw_deps then
+                       package2.raw_deps = raw_deps
+
+                       for _, dep in pairs(raw_deps) do
+                               local packages = {}
+                               for i=1, #dep.packages do
+                                       packages[#packages + 1] = content_lookup[dep.packages[i]:lower()]
+                               end
+                               dep.packages = packages
+                       end
+               end
+       end
+
+       return package.raw_deps
+end
+
+local function has_hard_deps(raw_deps)
+       for i=1, #raw_deps do
+               if not raw_deps[i].is_optional then
+                       return true
+               end
+       end
+
+       return false
+end
+
+-- Recursively resolve dependencies, given the installed mods
+local function resolve_dependencies_2(raw_deps, installed_mods, out)
+       local function resolve_dep(dep)
+               -- Check whether it's already installed
+               if installed_mods[dep.name] then
+                       return {
+                               is_optional = dep.is_optional,
+                               name = dep.name,
+                               installed = true,
+                       }
+               end
+
+               -- Find exact name matches
+               local fallback
+               for _, package in pairs(dep.packages) do
+                       if package.type ~= "game" then
+                               if package.name == dep.name then
+                                       return {
+                                               is_optional = dep.is_optional,
+                                               name = dep.name,
+                                               installed = false,
+                                               package = package,
+                                       }
+                               elseif not fallback then
+                                       fallback = package
+                               end
+                       end
+               end
+
+               -- Otherwise, find the first mod that fulfils it
+               if fallback then
+                       return {
+                               is_optional = dep.is_optional,
+                               name = dep.name,
+                               installed = false,
+                               package = fallback,
+                       }
+               end
+
+               return {
+                       is_optional = dep.is_optional,
+                       name = dep.name,
+                       installed = false,
+               }
+       end
+
+       for _, dep in pairs(raw_deps) do
+               if not dep.is_optional and not out[dep.name] then
+                       local result  = resolve_dep(dep)
+                       out[dep.name] = result
+                       if result and result.package and not result.installed then
+                               local raw_deps2 = get_raw_dependencies(result.package)
+                               if raw_deps2 then
+                                       resolve_dependencies_2(raw_deps2, installed_mods, out)
+                               end
+                       end
+               end
+       end
+
+       return true
+end
+
+-- Resolve dependencies for a package, calls the recursive version.
+local function resolve_dependencies(raw_deps, game)
+       assert(game)
+
+       local installed_mods = {}
+
+       local mods = {}
+       pkgmgr.get_game_mods(game, mods)
+       for _, mod in pairs(mods) do
+               installed_mods[mod.name] = true
+       end
+
+       for _, mod in pairs(pkgmgr.global_mods:get_list()) do
+               installed_mods[mod.name] = true
+       end
+
+       local out = {}
+       if not resolve_dependencies_2(raw_deps, installed_mods, out) then
+               return nil
+       end
+
+       local retval = {}
+       for _, dep in pairs(out) do
+               retval[#retval + 1] = dep
+       end
+
+       table.sort(retval, function(a, b)
+               return a.name < b.name
+       end)
+
+       return retval
+end
+
+local install_dialog = {}
+function install_dialog.get_formspec()
+       local package = install_dialog.package
+       local raw_deps = install_dialog.raw_deps
+       local will_install_deps = install_dialog.will_install_deps
+
+       local selected_game_idx = 1
+       local selected_gameid = core.settings:get("menu_last_game")
+       local games = table.copy(pkgmgr.games)
+       for i=1, #games do
+               if selected_gameid and games[i].id == selected_gameid then
+                       selected_game_idx = i
+               end
+
+               games[i] = minetest.formspec_escape(games[i].name)
+       end
+
+       local selected_game = pkgmgr.games[selected_game_idx]
+       local deps_to_install = 0
+       local deps_not_found = 0
+
+       install_dialog.dependencies = resolve_dependencies(raw_deps, selected_game)
+       local formatted_deps = {}
+       for _, dep in pairs(install_dialog.dependencies) do
+               formatted_deps[#formatted_deps + 1] = "#fff"
+               formatted_deps[#formatted_deps + 1] = minetest.formspec_escape(dep.name)
+               if dep.installed then
+                       formatted_deps[#formatted_deps + 1] = "#ccf"
+                       formatted_deps[#formatted_deps + 1] = fgettext("Already installed")
+               elseif dep.package then
+                       formatted_deps[#formatted_deps + 1] = "#cfc"
+                       formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author)
+                       deps_to_install = deps_to_install + 1
+               else
+                       formatted_deps[#formatted_deps + 1] = "#f00"
+                       formatted_deps[#formatted_deps + 1] = fgettext("Not found")
+                       deps_not_found = deps_not_found + 1
+               end
+       end
+
+       local message_bg = "#3333"
+       local message
+       if will_install_deps then
+               message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install)
+       else
+               message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install)
+       end
+       if deps_not_found > 0 then
+               message = fgettext("$1 required dependencies could not be found.", deps_not_found) ..
+                               " " .. fgettext("Please check that the base game is correct.", deps_not_found) ..
+                               "\n" .. message
+               message_bg = mt_color_orange
+       end
+
+       local formspec = {
+               "formspec_version[3]",
+               "size[7,7.85]",
+               "style[title;border=false]",
+               "box[0,0;7,0.5;#3333]",
+               "button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]",
+
+               "container[0.375,0.70]",
+
+               "label[0,0.25;", fgettext("Base Game:"), "]",
+               "dropdown[2,0;4.25,0.5;gameid;", table.concat(games, ","), ";", selected_game_idx, "]",
+
+               "label[0,0.8;", fgettext("Dependencies:"), "]",
+
+               "tablecolumns[color;text;color;text]",
+               "table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]",
+
+               "container_end[]",
+
+               "checkbox[0.375,5.1;will_install_deps;",
+                       fgettext("Install missing dependencies"), ";",
+                       will_install_deps and "true" or "false", "]",
+
+               "box[0,5.4;7,1.2;", message_bg, "]",
+               "textarea[0.375,5.5;6.25,1;;;", message, "]",
+
+               "container[1.375,6.85]",
+               "button[0,0;2,0.8;install_all;", fgettext("Install"), "]",
+               "button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]",
+               "container_end[]",
+       }
+
+       return table.concat(formspec, "")
+end
+
+function install_dialog.handle_submit(this, fields)
+       if fields.cancel then
+               this:delete()
+               return true
+       end
+
+       if fields.will_install_deps ~= nil then
+               install_dialog.will_install_deps = minetest.is_yes(fields.will_install_deps)
+               return true
+       end
+
+       if fields.install_all then
+               queue_download(install_dialog.package)
+
+               if install_dialog.will_install_deps then
+                       for _, dep in pairs(install_dialog.dependencies) do
+                               if not dep.is_optional and not dep.installed and dep.package then
+                                       queue_download(dep.package)
+                               end
+                       end
+               end
+
+               this:delete()
+               return true
+       end
+
+       if fields.gameid then
+               for _, game in pairs(pkgmgr.games) do
+                       if game.name == fields.gameid then
+                               core.settings:set("menu_last_game", game.id)
+                               break
+                       end
+               end
+               return true
+       end
+
+       return false
+end
+
+function install_dialog.create(package, raw_deps)
+       install_dialog.dependencies = nil
+       install_dialog.package = package
+       install_dialog.raw_deps = raw_deps
+       install_dialog.will_install_deps = true
+       return dialog_create("package_view",
+                       install_dialog.get_formspec,
+                       install_dialog.handle_submit,
+                       nil)
+end
+
 local function get_file_extension(path)
        local parts = path:split(".")
        return parts[#parts]
@@ -570,15 +854,24 @@ function store.handle_submit(this, fields)
                assert(package)
 
                if fields["install_" .. i] then
-                       queue_download(package)
+                       local deps = get_raw_dependencies(package)
+                       if deps and has_hard_deps(deps) then
+                               local dlg = install_dialog.create(package, deps)
+                               dlg:set_parent(this)
+                               this:hide()
+                               dlg:show()
+                       else
+                               queue_download(package)
+                       end
+
                        return true
                end
 
                if fields["uninstall_" .. i] then
-                       local dlg_delmod = create_delete_content_dlg(package)
-                       dlg_delmod:set_parent(this)
+                       local dlg = create_delete_content_dlg(package)
+                       dlg:set_parent(this)
                        this:hide()
-                       dlg_delmod:show()
+                       dlg:show()
                        return true
                end
 
index 7566d2409ebd5f70243c9b3f7a15f9486492e206..5931496c1ee78063ce12f9ce08aa004738985b66 100644 (file)
@@ -98,7 +98,7 @@ local function create_world_formspec(dialogdata)
        -- Error out when no games found
        if #pkgmgr.games == 0 then
                return "size[12.25,3,true]" ..
-                       "box[0,0;12,2;#ff8800]" ..
+                       "box[0,0;12,2;" .. mt_color_orange .. "]" ..
                        "textarea[0.3,0;11.7,2;;;"..
                        fgettext("You have no games installed.") .. "\n" ..
                        fgettext("Download one from minetest.net") .. "]" ..
index 96d02d06c661ee2ab4151bdcf4e163918d2ee20c..2fc0ae64c0922c0a0a2b7c14ac8739fd80f69963 100644 (file)
@@ -19,6 +19,7 @@ mt_color_grey  = "#AAAAAA"
 mt_color_blue  = "#6389FF"
 mt_color_green = "#72FF63"
 mt_color_dark_green = "#25C191"
+mt_color_orange  = "#FF8800"
 
 local menupath = core.get_mainmenu_path()
 local basepath = core.get_builtin_path()
index 7060a0b6e9ab60cfd62ef45427aa07f744739300..7e23b5641f8ab88d6a2bc67bb36da68758433d72 100644 (file)
@@ -2205,4 +2205,5 @@ contentdb_url (ContentDB URL) string https://content.minetest.net
 contentdb_flag_blacklist (ContentDB Flag Blacklist) string nonfree, desktop_default
 
 #    Maximum number of concurrent downloads. Downloads exceeding this limit will be queued.
+#    This should be lower than curl_parallel_limit.
 contentdb_max_concurrent_downloads (ContentDB Max Concurrent Downloads) int 3