summaryrefslogtreecommitdiff
path: root/builtin
diff options
context:
space:
mode:
authorrubenwardy <rw@rubenwardy.com>2020-12-23 14:42:18 +0000
committerGitHub <noreply@github.com>2020-12-23 14:42:18 +0000
commitd2bbf13dfe61c48c1051ab5bd8b2b0e97ad7599e (patch)
tree241060daede4b9c8f1f935b470594e78b3a1c749 /builtin
parent535557cc2e972c555bc5a294b95f2c2974b81eea (diff)
downloadminetest-d2bbf13dfe61c48c1051ab5bd8b2b0e97ad7599e.tar.gz
minetest-d2bbf13dfe61c48c1051ab5bd8b2b0e97ad7599e.tar.bz2
minetest-d2bbf13dfe61c48c1051ab5bd8b2b0e97ad7599e.zip
Add dependency resolution to ContentDB (#9997)
Diffstat (limited to 'builtin')
-rw-r--r--builtin/mainmenu/dlg_contentstore.lua301
-rw-r--r--builtin/mainmenu/dlg_create_world.lua2
-rw-r--r--builtin/mainmenu/init.lua1
-rw-r--r--builtin/settingtypes.txt1
4 files changed, 300 insertions, 5 deletions
diff --git a/builtin/mainmenu/dlg_contentstore.lua b/builtin/mainmenu/dlg_contentstore.lua
index 7a96df2a5..548bae67e 100644
--- a/builtin/mainmenu/dlg_contentstore.lua
+++ b/builtin/mainmenu/dlg_contentstore.lua
@@ -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
diff --git a/builtin/mainmenu/dlg_create_world.lua b/builtin/mainmenu/dlg_create_world.lua
index 7566d2409..5931496c1 100644
--- a/builtin/mainmenu/dlg_create_world.lua
+++ b/builtin/mainmenu/dlg_create_world.lua
@@ -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") .. "]" ..
diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua
index 96d02d06c..2fc0ae64c 100644
--- a/builtin/mainmenu/init.lua
+++ b/builtin/mainmenu/init.lua
@@ -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()
diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt
index 7060a0b6e..7e23b5641 100644
--- a/builtin/settingtypes.txt
+++ b/builtin/settingtypes.txt
@@ -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