Skip to content

Commit d2bbf13

Browse files
authoredDec 23, 2020
Add dependency resolution to ContentDB (#9997)
1 parent 535557c commit d2bbf13

File tree

4 files changed

+300
-5
lines changed

4 files changed

+300
-5
lines changed
 

‎builtin/mainmenu/dlg_contentstore.lua

+297-4
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,290 @@ local function queue_download(package)
159159
end
160160
end
161161

162+
local function get_raw_dependencies(package)
163+
if package.raw_deps then
164+
return package.raw_deps
165+
end
166+
167+
local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
168+
local version = core.get_version()
169+
local base_url = core.settings:get("contentdb_url")
170+
local url = base_url .. url_fmt:format(package.id, core.get_max_supp_proto(), version.string)
171+
172+
local response = http.fetch_sync({ url = url })
173+
if not response.succeeded then
174+
return
175+
end
176+
177+
local data = core.parse_json(response.data) or {}
178+
179+
local content_lookup = {}
180+
for _, pkg in pairs(store.packages_full) do
181+
content_lookup[pkg.id] = pkg
182+
end
183+
184+
for id, raw_deps in pairs(data) do
185+
local package2 = content_lookup[id:lower()]
186+
if package2 and not package2.raw_deps then
187+
package2.raw_deps = raw_deps
188+
189+
for _, dep in pairs(raw_deps) do
190+
local packages = {}
191+
for i=1, #dep.packages do
192+
packages[#packages + 1] = content_lookup[dep.packages[i]:lower()]
193+
end
194+
dep.packages = packages
195+
end
196+
end
197+
end
198+
199+
return package.raw_deps
200+
end
201+
202+
local function has_hard_deps(raw_deps)
203+
for i=1, #raw_deps do
204+
if not raw_deps[i].is_optional then
205+
return true
206+
end
207+
end
208+
209+
return false
210+
end
211+
212+
-- Recursively resolve dependencies, given the installed mods
213+
local function resolve_dependencies_2(raw_deps, installed_mods, out)
214+
local function resolve_dep(dep)
215+
-- Check whether it's already installed
216+
if installed_mods[dep.name] then
217+
return {
218+
is_optional = dep.is_optional,
219+
name = dep.name,
220+
installed = true,
221+
}
222+
end
223+
224+
-- Find exact name matches
225+
local fallback
226+
for _, package in pairs(dep.packages) do
227+
if package.type ~= "game" then
228+
if package.name == dep.name then
229+
return {
230+
is_optional = dep.is_optional,
231+
name = dep.name,
232+
installed = false,
233+
package = package,
234+
}
235+
elseif not fallback then
236+
fallback = package
237+
end
238+
end
239+
end
240+
241+
-- Otherwise, find the first mod that fulfils it
242+
if fallback then
243+
return {
244+
is_optional = dep.is_optional,
245+
name = dep.name,
246+
installed = false,
247+
package = fallback,
248+
}
249+
end
250+
251+
return {
252+
is_optional = dep.is_optional,
253+
name = dep.name,
254+
installed = false,
255+
}
256+
end
257+
258+
for _, dep in pairs(raw_deps) do
259+
if not dep.is_optional and not out[dep.name] then
260+
local result = resolve_dep(dep)
261+
out[dep.name] = result
262+
if result and result.package and not result.installed then
263+
local raw_deps2 = get_raw_dependencies(result.package)
264+
if raw_deps2 then
265+
resolve_dependencies_2(raw_deps2, installed_mods, out)
266+
end
267+
end
268+
end
269+
end
270+
271+
return true
272+
end
273+
274+
-- Resolve dependencies for a package, calls the recursive version.
275+
local function resolve_dependencies(raw_deps, game)
276+
assert(game)
277+
278+
local installed_mods = {}
279+
280+
local mods = {}
281+
pkgmgr.get_game_mods(game, mods)
282+
for _, mod in pairs(mods) do
283+
installed_mods[mod.name] = true
284+
end
285+
286+
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
287+
installed_mods[mod.name] = true
288+
end
289+
290+
local out = {}
291+
if not resolve_dependencies_2(raw_deps, installed_mods, out) then
292+
return nil
293+
end
294+
295+
local retval = {}
296+
for _, dep in pairs(out) do
297+
retval[#retval + 1] = dep
298+
end
299+
300+
table.sort(retval, function(a, b)
301+
return a.name < b.name
302+
end)
303+
304+
return retval
305+
end
306+
307+
local install_dialog = {}
308+
function install_dialog.get_formspec()
309+
local package = install_dialog.package
310+
local raw_deps = install_dialog.raw_deps
311+
local will_install_deps = install_dialog.will_install_deps
312+
313+
local selected_game_idx = 1
314+
local selected_gameid = core.settings:get("menu_last_game")
315+
local games = table.copy(pkgmgr.games)
316+
for i=1, #games do
317+
if selected_gameid and games[i].id == selected_gameid then
318+
selected_game_idx = i
319+
end
320+
321+
games[i] = minetest.formspec_escape(games[i].name)
322+
end
323+
324+
local selected_game = pkgmgr.games[selected_game_idx]
325+
local deps_to_install = 0
326+
local deps_not_found = 0
327+
328+
install_dialog.dependencies = resolve_dependencies(raw_deps, selected_game)
329+
local formatted_deps = {}
330+
for _, dep in pairs(install_dialog.dependencies) do
331+
formatted_deps[#formatted_deps + 1] = "#fff"
332+
formatted_deps[#formatted_deps + 1] = minetest.formspec_escape(dep.name)
333+
if dep.installed then
334+
formatted_deps[#formatted_deps + 1] = "#ccf"
335+
formatted_deps[#formatted_deps + 1] = fgettext("Already installed")
336+
elseif dep.package then
337+
formatted_deps[#formatted_deps + 1] = "#cfc"
338+
formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author)
339+
deps_to_install = deps_to_install + 1
340+
else
341+
formatted_deps[#formatted_deps + 1] = "#f00"
342+
formatted_deps[#formatted_deps + 1] = fgettext("Not found")
343+
deps_not_found = deps_not_found + 1
344+
end
345+
end
346+
347+
local message_bg = "#3333"
348+
local message
349+
if will_install_deps then
350+
message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install)
351+
else
352+
message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install)
353+
end
354+
if deps_not_found > 0 then
355+
message = fgettext("$1 required dependencies could not be found.", deps_not_found) ..
356+
" " .. fgettext("Please check that the base game is correct.", deps_not_found) ..
357+
"\n" .. message
358+
message_bg = mt_color_orange
359+
end
360+
361+
local formspec = {
362+
"formspec_version[3]",
363+
"size[7,7.85]",
364+
"style[title;border=false]",
365+
"box[0,0;7,0.5;#3333]",
366+
"button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]",
367+
368+
"container[0.375,0.70]",
369+
370+
"label[0,0.25;", fgettext("Base Game:"), "]",
371+
"dropdown[2,0;4.25,0.5;gameid;", table.concat(games, ","), ";", selected_game_idx, "]",
372+
373+
"label[0,0.8;", fgettext("Dependencies:"), "]",
374+
375+
"tablecolumns[color;text;color;text]",
376+
"table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]",
377+
378+
"container_end[]",
379+
380+
"checkbox[0.375,5.1;will_install_deps;",
381+
fgettext("Install missing dependencies"), ";",
382+
will_install_deps and "true" or "false", "]",
383+
384+
"box[0,5.4;7,1.2;", message_bg, "]",
385+
"textarea[0.375,5.5;6.25,1;;;", message, "]",
386+
387+
"container[1.375,6.85]",
388+
"button[0,0;2,0.8;install_all;", fgettext("Install"), "]",
389+
"button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]",
390+
"container_end[]",
391+
}
392+
393+
return table.concat(formspec, "")
394+
end
395+
396+
function install_dialog.handle_submit(this, fields)
397+
if fields.cancel then
398+
this:delete()
399+
return true
400+
end
401+
402+
if fields.will_install_deps ~= nil then
403+
install_dialog.will_install_deps = minetest.is_yes(fields.will_install_deps)
404+
return true
405+
end
406+
407+
if fields.install_all then
408+
queue_download(install_dialog.package)
409+
410+
if install_dialog.will_install_deps then
411+
for _, dep in pairs(install_dialog.dependencies) do
412+
if not dep.is_optional and not dep.installed and dep.package then
413+
queue_download(dep.package)
414+
end
415+
end
416+
end
417+
418+
this:delete()
419+
return true
420+
end
421+
422+
if fields.gameid then
423+
for _, game in pairs(pkgmgr.games) do
424+
if game.name == fields.gameid then
425+
core.settings:set("menu_last_game", game.id)
426+
break
427+
end
428+
end
429+
return true
430+
end
431+
432+
return false
433+
end
434+
435+
function install_dialog.create(package, raw_deps)
436+
install_dialog.dependencies = nil
437+
install_dialog.package = package
438+
install_dialog.raw_deps = raw_deps
439+
install_dialog.will_install_deps = true
440+
return dialog_create("package_view",
441+
install_dialog.get_formspec,
442+
install_dialog.handle_submit,
443+
nil)
444+
end
445+
162446
local function get_file_extension(path)
163447
local parts = path:split(".")
164448
return parts[#parts]
@@ -570,15 +854,24 @@ function store.handle_submit(this, fields)
570854
assert(package)
571855

572856
if fields["install_" .. i] then
573-
queue_download(package)
857+
local deps = get_raw_dependencies(package)
858+
if deps and has_hard_deps(deps) then
859+
local dlg = install_dialog.create(package, deps)
860+
dlg:set_parent(this)
861+
this:hide()
862+
dlg:show()
863+
else
864+
queue_download(package)
865+
end
866+
574867
return true
575868
end
576869

577870
if fields["uninstall_" .. i] then
578-
local dlg_delmod = create_delete_content_dlg(package)
579-
dlg_delmod:set_parent(this)
871+
local dlg = create_delete_content_dlg(package)
872+
dlg:set_parent(this)
580873
this:hide()
581-
dlg_delmod:show()
874+
dlg:show()
582875
return true
583876
end
584877

‎builtin/mainmenu/dlg_create_world.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ local function create_world_formspec(dialogdata)
9898
-- Error out when no games found
9999
if #pkgmgr.games == 0 then
100100
return "size[12.25,3,true]" ..
101-
"box[0,0;12,2;#ff8800]" ..
101+
"box[0,0;12,2;" .. mt_color_orange .. "]" ..
102102
"textarea[0.3,0;11.7,2;;;"..
103103
fgettext("You have no games installed.") .. "\n" ..
104104
fgettext("Download one from minetest.net") .. "]" ..

‎builtin/mainmenu/init.lua

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mt_color_grey = "#AAAAAA"
1919
mt_color_blue = "#6389FF"
2020
mt_color_green = "#72FF63"
2121
mt_color_dark_green = "#25C191"
22+
mt_color_orange = "#FF8800"
2223

2324
local menupath = core.get_mainmenu_path()
2425
local basepath = core.get_builtin_path()

‎builtin/settingtypes.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2205,4 +2205,5 @@ contentdb_url (ContentDB URL) string https://content.minetest.net
22052205
contentdb_flag_blacklist (ContentDB Flag Blacklist) string nonfree, desktop_default
22062206

22072207
# Maximum number of concurrent downloads. Downloads exceeding this limit will be queued.
2208+
# This should be lower than curl_parallel_limit.
22082209
contentdb_max_concurrent_downloads (ContentDB Max Concurrent Downloads) int 3

0 commit comments

Comments
 (0)
Please sign in to comment.