@@ -159,6 +159,290 @@ local function queue_download(package)
159
159
end
160
160
end
161
161
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
+
162
446
local function get_file_extension (path )
163
447
local parts = path :split (" ." )
164
448
return parts [# parts ]
@@ -570,15 +854,24 @@ function store.handle_submit(this, fields)
570
854
assert (package )
571
855
572
856
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
+
574
867
return true
575
868
end
576
869
577
870
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 )
580
873
this :hide ()
581
- dlg_delmod :show ()
874
+ dlg :show ()
582
875
return true
583
876
end
584
877
0 commit comments