]> git.lizzy.rs Git - minetest.git/blob - builtin/mainmenu/pkgmgr.lua
Fix list sorting behaviour with missing geoip
[minetest.git] / builtin / mainmenu / pkgmgr.lua
1 --Minetest
2 --Copyright (C) 2013 sapier
3 --
4 --This program is free software; you can redistribute it and/or modify
5 --it under the terms of the GNU Lesser General Public License as published by
6 --the Free Software Foundation; either version 2.1 of the License, or
7 --(at your option) any later version.
8 --
9 --This program is distributed in the hope that it will be useful,
10 --but WITHOUT ANY WARRANTY; without even the implied warranty of
11 --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 --GNU Lesser General Public License for more details.
13 --
14 --You should have received a copy of the GNU Lesser General Public License along
15 --with this program; if not, write to the Free Software Foundation, Inc.,
16 --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 --------------------------------------------------------------------------------
19 local function get_last_folder(text,count)
20         local parts = text:split(DIR_DELIM)
21
22         if count == nil then
23                 return parts[#parts]
24         end
25
26         local retval = ""
27         for i=1,count,1 do
28                 retval = retval .. parts[#parts - (count-i)] .. DIR_DELIM
29         end
30
31         return retval
32 end
33
34 local function cleanup_path(temppath)
35
36         local parts = temppath:split("-")
37         temppath = ""
38         for i=1,#parts,1 do
39                 if temppath ~= "" then
40                         temppath = temppath .. "_"
41                 end
42                 temppath = temppath .. parts[i]
43         end
44
45         parts = temppath:split(".")
46         temppath = ""
47         for i=1,#parts,1 do
48                 if temppath ~= "" then
49                         temppath = temppath .. "_"
50                 end
51                 temppath = temppath .. parts[i]
52         end
53
54         parts = temppath:split("'")
55         temppath = ""
56         for i=1,#parts,1 do
57                 if temppath ~= "" then
58                         temppath = temppath .. ""
59                 end
60                 temppath = temppath .. parts[i]
61         end
62
63         parts = temppath:split(" ")
64         temppath = ""
65         for i=1,#parts,1 do
66                 if temppath ~= "" then
67                         temppath = temppath
68                 end
69                 temppath = temppath .. parts[i]
70         end
71
72         return temppath
73 end
74
75 local function load_texture_packs(txtpath, retval)
76         local list = core.get_dir_list(txtpath, true)
77         local current_texture_path = core.settings:get("texture_path")
78
79         for _, item in ipairs(list) do
80                 if item ~= "base" then
81                         local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
82                         local conf = Settings(path .. "texture_pack.conf")
83                         local enabled = path == current_texture_path
84
85                         local title = conf:get("title") or item
86
87                         -- list_* is only used if non-nil, else the regular versions are used.
88                         retval[#retval + 1] = {
89                                 name = item,
90                                 title = title,
91                                 list_name = enabled and fgettext("$1 (Enabled)", item) or nil,
92                                 list_title = enabled and fgettext("$1 (Enabled)", title) or nil,
93                                 author = conf:get("author"),
94                                 release = tonumber(conf:get("release")) or 0,
95                                 type = "txp",
96                                 path = path,
97                                 enabled = enabled,
98                         }
99                 end
100         end
101 end
102
103 --modmanager implementation
104 pkgmgr = {}
105
106 --- Scans a directory recursively for mods and adds them to `listing`
107 -- @param path         Absolute directory path to scan recursively
108 -- @param virtual_path Prettified unique path (e.g. "mods", "mods/mt_modpack")
109 -- @param listing      Input. Flat array to insert located mods and modpacks
110 -- @param modpack      Currently processing modpack or nil/"" if none (recursion)
111 function pkgmgr.get_mods(path, virtual_path, listing, modpack)
112         local mods = core.get_dir_list(path, true)
113
114         for _, name in ipairs(mods) do
115                 if name:sub(1, 1) ~= "." then
116                         local mod_path = path .. DIR_DELIM .. name
117                         local mod_virtual_path = virtual_path .. "/" .. name
118                         local toadd = {
119                                 dir_name = name,
120                                 parent_dir = path,
121                         }
122                         listing[#listing + 1] = toadd
123
124                         -- Get config file
125                         local mod_conf
126                         local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf")
127                         if modpack_conf then
128                                 toadd.is_modpack = true
129                                 modpack_conf:close()
130
131                                 mod_conf = Settings(mod_path .. DIR_DELIM .. "modpack.conf"):to_table()
132                                 if mod_conf.name then
133                                         name = mod_conf.name
134                                         toadd.is_name_explicit = true
135                                 end
136                         else
137                                 mod_conf = Settings(mod_path .. DIR_DELIM .. "mod.conf"):to_table()
138                                 if mod_conf.name then
139                                         name = mod_conf.name
140                                         toadd.is_name_explicit = true
141                                 end
142                         end
143
144                         -- Read from config
145                         toadd.name = name
146                         toadd.title = mod_conf.title
147                         toadd.author = mod_conf.author
148                         toadd.release = tonumber(mod_conf.release) or 0
149                         toadd.path = mod_path
150                         toadd.virtual_path = mod_virtual_path
151                         toadd.type = "mod"
152
153                         -- Check modpack.txt
154                         -- Note: modpack.conf is already checked above
155                         local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
156                         if modpackfile then
157                                 modpackfile:close()
158                                 toadd.is_modpack = true
159                         end
160
161                         -- Deal with modpack contents
162                         if modpack and modpack ~= "" then
163                                 toadd.modpack = modpack
164                         elseif toadd.is_modpack then
165                                 toadd.type = "modpack"
166                                 toadd.is_modpack = true
167                                 pkgmgr.get_mods(mod_path, mod_virtual_path, listing, name)
168                         end
169                 end
170         end
171
172         if not modpack then
173                 -- Sort all when the recursion is done
174                 table.sort(listing, function(a, b)
175                         return a.virtual_path:lower() < b.virtual_path:lower()
176                 end)
177         end
178 end
179
180 function pkgmgr.get_texture_packs()
181         local txtpath = core.get_texturepath()
182         local txtpath_system = core.get_texturepath_share()
183         local retval = {}
184
185         load_texture_packs(txtpath, retval)
186         -- on portable versions these two paths coincide. It avoids loading the path twice
187         if txtpath ~= txtpath_system then
188                 load_texture_packs(txtpath_system, retval)
189         end
190
191         table.sort(retval, function(a, b)
192                 return a.title:lower() < b.title:lower()
193         end)
194
195         return retval
196 end
197
198 --------------------------------------------------------------------------------
199 function pkgmgr.get_folder_type(path)
200         local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
201         if testfile ~= nil then
202                 testfile:close()
203                 return { type = "mod", path = path }
204         end
205
206         testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r")
207         if testfile ~= nil then
208                 testfile:close()
209                 return { type = "modpack", path = path }
210         end
211
212         testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
213         if testfile ~= nil then
214                 testfile:close()
215                 return { type = "modpack", path = path }
216         end
217
218         testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
219         if testfile ~= nil then
220                 testfile:close()
221                 return { type = "game", path = path }
222         end
223
224         testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
225         if testfile ~= nil then
226                 testfile:close()
227                 return { type = "txp", path = path }
228         end
229
230         return nil
231 end
232
233 -------------------------------------------------------------------------------
234 function pkgmgr.get_base_folder(temppath)
235         if temppath == nil then
236                 return { type = "invalid", path = "" }
237         end
238
239         local ret = pkgmgr.get_folder_type(temppath)
240         if ret then
241                 return ret
242         end
243
244         local subdirs = core.get_dir_list(temppath, true)
245         if #subdirs == 1 then
246                 ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
247                 if ret then
248                         return ret
249                 else
250                         return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
251                 end
252         end
253
254         return nil
255 end
256
257 --------------------------------------------------------------------------------
258 function pkgmgr.is_valid_modname(modpath)
259         return modpath:match("[^a-z0-9_]") == nil
260 end
261
262 --------------------------------------------------------------------------------
263 function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
264         if not render_list then
265                 if not pkgmgr.global_mods then
266                         pkgmgr.refresh_globals()
267                 end
268                 render_list = pkgmgr.global_mods
269         end
270
271         local list = render_list:get_list()
272         local retval = {}
273         for i, v in ipairs(list) do
274                 local color = ""
275                 local icon = 0
276                 local error = with_error and with_error[v.virtual_path]
277                 local function update_error(val)
278                         if val and (not error or (error.type == "warning" and val.type == "error")) then
279                                 error = val
280                         end
281                 end
282
283                 if v.is_modpack then
284                         local rawlist = render_list:get_raw_list()
285                         color = mt_color_dark_green
286
287                         for j = 1, #rawlist do
288                                 if rawlist[j].modpack == list[i].name then
289                                         if with_error then
290                                                 update_error(with_error[rawlist[j].virtual_path])
291                                         end
292
293                                         if rawlist[j].enabled then
294                                                 icon = 1
295                                         else
296                                                 -- Modpack not entirely enabled so showing as grey
297                                                 color = mt_color_grey
298                                         end
299                                 end
300                         end
301                 elseif v.is_game_content or v.type == "game" then
302                         icon = 1
303                         color = mt_color_blue
304
305                         local rawlist = render_list:get_raw_list()
306                         if v.type == "game" and with_error then
307                                 for j = 1, #rawlist do
308                                         if rawlist[j].is_game_content then
309                                                 update_error(with_error[rawlist[j].virtual_path])
310                                         end
311                                 end
312                         end
313                 elseif v.enabled or v.type == "txp" then
314                         icon = 1
315                         color = mt_color_green
316                 end
317
318                 if error then
319                         if error.type == "warning" then
320                                 color = mt_color_orange
321                                 icon = 2
322                         else
323                                 color = mt_color_red
324                                 icon = 3
325                         end
326                 end
327
328                 retval[#retval + 1] = color
329                 if v.modpack ~= nil or v.loc == "game" then
330                         retval[#retval + 1] = "1"
331                 else
332                         retval[#retval + 1] = "0"
333                 end
334
335                 if with_error then
336                         retval[#retval + 1] = icon
337                 end
338
339                 if use_technical_names then
340                         retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
341                 else
342                         retval[#retval + 1] = core.formspec_escape(v.list_title or v.list_name or v.title or v.name)
343                 end
344         end
345
346         return table.concat(retval, ",")
347 end
348
349 --------------------------------------------------------------------------------
350 function pkgmgr.get_dependencies(path)
351         if path == nil then
352                 return {}, {}
353         end
354
355         local info = core.get_content_info(path)
356         return info.depends or {}, info.optional_depends or {}
357 end
358
359 ----------- tests whether all of the mods in the modpack are enabled -----------
360 function pkgmgr.is_modpack_entirely_enabled(data, name)
361         local rawlist = data.list:get_raw_list()
362         for j = 1, #rawlist do
363                 if rawlist[j].modpack == name and not rawlist[j].enabled then
364                         return false
365                 end
366         end
367         return true
368 end
369
370 local function disable_all_by_name(list, name, except)
371         for i=1, #list do
372                 if list[i].name == name and list[i] ~= except then
373                         list[i].enabled = false
374                 end
375         end
376 end
377
378 ---------- toggles or en/disables a mod or modpack and its dependencies --------
379 local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
380         if not mod.is_modpack then
381                 -- Toggle or en/disable the mod
382                 if toset == nil then
383                         toset = not mod.enabled
384                 end
385                 if mod.enabled ~= toset then
386                         toggled_mods[#toggled_mods+1] = mod.name
387                 end
388                 if toset then
389                         -- Mark this mod for recursive dependency traversal
390                         enabled_mods[mod.name] = true
391
392                         -- Disable other mods with the same name
393                         disable_all_by_name(list, mod.name, mod)
394                 end
395                 mod.enabled = toset
396         else
397                 -- Toggle or en/disable every mod in the modpack,
398                 -- interleaved unsupported
399                 for i = 1, #list do
400                         if list[i].modpack == mod.name then
401                                 toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, list[i])
402                         end
403                 end
404         end
405 end
406
407 function pkgmgr.enable_mod(this, toset)
408         local list = this.data.list:get_list()
409         local mod = list[this.data.selected_mod]
410
411         -- Game mods can't be enabled or disabled
412         if mod.is_game_content then
413                 return
414         end
415
416         local toggled_mods = {}
417         local enabled_mods = {}
418         toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
419
420         if next(enabled_mods) == nil then
421                 -- Mod(s) were disabled, so no dependencies need to be enabled
422                 table.sort(toggled_mods)
423                 core.log("info", "Following mods were disabled: " ..
424                         table.concat(toggled_mods, ", "))
425                 return
426         end
427
428         -- Enable mods' depends after activation
429
430         -- Make a list of mod ids indexed by their names. Among mods with the
431         -- same name, enabled mods take precedence, after which game mods take
432         -- precedence, being last in the mod list.
433         local mod_ids = {}
434         for id, mod2 in pairs(list) do
435                 if mod2.type == "mod" and not mod2.is_modpack then
436                         local prev_id = mod_ids[mod2.name]
437                         if not prev_id or not list[prev_id].enabled then
438                                 mod_ids[mod2.name] = id
439                         end
440                 end
441         end
442
443         -- to_enable is used as a DFS stack with sp as stack pointer
444         local to_enable = {}
445         local sp = 0
446         for name in pairs(enabled_mods) do
447                 local depends = pkgmgr.get_dependencies(list[mod_ids[name]].path)
448                 for i = 1, #depends do
449                         local dependency_name = depends[i]
450                         if not enabled_mods[dependency_name] then
451                                 sp = sp+1
452                                 to_enable[sp] = dependency_name
453                         end
454                 end
455         end
456
457         -- If sp is 0, every dependency is already activated
458         while sp > 0 do
459                 local name = to_enable[sp]
460                 sp = sp-1
461
462                 if not enabled_mods[name] then
463                         enabled_mods[name] = true
464                         local mod_to_enable = list[mod_ids[name]]
465                         if not mod_to_enable then
466                                 core.log("warning", "Mod dependency \"" .. name ..
467                                         "\" not found!")
468                         elseif not mod_to_enable.is_game_content then
469                                 if not mod_to_enable.enabled then
470                                         mod_to_enable.enabled = true
471                                         toggled_mods[#toggled_mods+1] = mod_to_enable.name
472                                 end
473                                 -- Push the dependencies of the dependency onto the stack
474                                 local depends = pkgmgr.get_dependencies(mod_to_enable.path)
475                                 for i = 1, #depends do
476                                         if not enabled_mods[depends[i]] then
477                                                 sp = sp+1
478                                                 to_enable[sp] = depends[i]
479                                         end
480                                 end
481                         end
482                 end
483         end
484
485         -- Log the list of enabled mods
486         table.sort(toggled_mods)
487         core.log("info", "Following mods were enabled: " ..
488                 table.concat(toggled_mods, ", "))
489 end
490
491 --------------------------------------------------------------------------------
492 function pkgmgr.get_worldconfig(worldpath)
493         local filename = worldpath ..
494                                 DIR_DELIM .. "world.mt"
495
496         local worldfile = Settings(filename)
497
498         local worldconfig = {}
499         worldconfig.global_mods = {}
500         worldconfig.game_mods = {}
501
502         for key,value in pairs(worldfile:to_table()) do
503                 if key == "gameid" then
504                         worldconfig.id = value
505                 elseif key:sub(0, 9) == "load_mod_" then
506                         -- Compatibility: Check against "nil" which was erroneously used
507                         -- as value for fresh configured worlds
508                         worldconfig.global_mods[key] = value ~= "false" and value ~= "nil"
509                                 and value
510                 else
511                         worldconfig[key] = value
512                 end
513         end
514
515         --read gamemods
516         local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
517         pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
518
519         return worldconfig
520 end
521
522 --------------------------------------------------------------------------------
523 function pkgmgr.install_dir(expected_type, path, basename, targetpath)
524         assert(type(expected_type) == "string")
525         assert(type(path) == "string")
526         assert(basename == nil or type(basename) == "string")
527         assert(targetpath == nil or type(targetpath) == "string")
528
529         local basefolder = pkgmgr.get_base_folder(path)
530
531         if expected_type == "txp" then
532                 assert(basename)
533
534                 -- There's no good way to detect a texture pack, so let's just assume
535                 -- it's correct for now.
536                 if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
537                         return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
538                 end
539
540                 local from = basefolder and basefolder.path or path
541                 if not targetpath then
542                         targetpath = core.get_texturepath() .. DIR_DELIM .. basename
543                 end
544                 core.delete_dir(targetpath)
545                 if not core.copy_dir(from, targetpath, false) then
546                         return nil,
547                                 fgettext("Failed to install $1 to $2", basename, targetpath)
548                 end
549                 return targetpath, nil
550
551         elseif not basefolder then
552                 return nil, fgettext("Unable to find a valid mod, modpack, or game")
553         end
554
555         -- Check type
556         if basefolder.type ~= expected_type and (basefolder.type ~= "modpack" or expected_type ~= "mod") then
557                 return nil, fgettext("Unable to install a $1 as a $2", basefolder.type, expected_type)
558         end
559
560         -- Set targetpath if not predetermined
561         if not targetpath then
562                 local content_path
563                 if basefolder.type == "modpack" or basefolder.type == "mod" then
564                         if not basename then
565                                 basename = get_last_folder(cleanup_path(basefolder.path))
566                         end
567                         content_path = core.get_modpath()
568                 elseif basefolder.type == "game" then
569                         content_path = core.get_gamepath()
570                 else
571                         error("Unknown content type")
572                 end
573
574                 if basename and (basefolder.type ~= "mod" or pkgmgr.is_valid_modname(basename)) then
575                         targetpath = content_path .. DIR_DELIM .. basename
576                 else
577                         return nil,
578                                 fgettext("Install: Unable to find suitable folder name for $1", path)
579                 end
580         end
581
582         -- Copy it
583         core.delete_dir(targetpath)
584         if not core.copy_dir(basefolder.path, targetpath, false) then
585                 return nil,
586                         fgettext("Failed to install $1 to $2", basename, targetpath)
587         end
588
589         if basefolder.type == "game" then
590                 pkgmgr.update_gamelist()
591         else
592                 pkgmgr.refresh_globals()
593         end
594
595         return targetpath, nil
596 end
597
598 --------------------------------------------------------------------------------
599 function pkgmgr.preparemodlist(data)
600         local retval = {}
601
602         local global_mods = {}
603         local game_mods = {}
604
605         --read global mods
606         local modpaths = core.get_modpaths()
607         for key, modpath in pairs(modpaths) do
608                 pkgmgr.get_mods(modpath, key, global_mods)
609         end
610
611         for i=1,#global_mods,1 do
612                 global_mods[i].type = "mod"
613                 global_mods[i].loc = "global"
614                 global_mods[i].enabled = false
615                 retval[#retval + 1] = global_mods[i]
616         end
617
618         --read game mods
619         local gamespec = pkgmgr.find_by_gameid(data.gameid)
620         pkgmgr.get_game_mods(gamespec, game_mods)
621
622         if #game_mods > 0 then
623                 -- Add title
624                 retval[#retval + 1] = {
625                         type = "game",
626                         is_game_content = true,
627                         name = fgettext("$1 mods", gamespec.title),
628                         path = gamespec.path
629                 }
630         end
631
632         for i=1,#game_mods,1 do
633                 game_mods[i].type = "mod"
634                 game_mods[i].loc = "game"
635                 game_mods[i].is_game_content = true
636                 retval[#retval + 1] = game_mods[i]
637         end
638
639         if data.worldpath == nil then
640                 return retval
641         end
642
643         --read world mod configuration
644         local filename = data.worldpath ..
645                                 DIR_DELIM .. "world.mt"
646
647         local worldfile = Settings(filename)
648         for key, value in pairs(worldfile:to_table()) do
649                 if key:sub(1, 9) == "load_mod_" then
650                         key = key:sub(10)
651                         local mod_found = false
652
653                         local fallback_found = false
654                         local fallback_mod = nil
655
656                         for i=1, #retval do
657                                 if retval[i].name == key and
658                                                 not retval[i].is_modpack then
659                                         if core.is_yes(value) or retval[i].virtual_path == value then
660                                                 retval[i].enabled = true
661                                                 mod_found = true
662                                                 break
663                                         elseif fallback_found then
664                                                 -- Only allow fallback if only one mod matches
665                                                 fallback_mod = nil
666                                         else
667                                                 fallback_found = true
668                                                 fallback_mod = retval[i]
669                                         end
670                                 end
671                         end
672
673                         if not mod_found then
674                                 if fallback_mod and value:find("/") then
675                                         fallback_mod.enabled = true
676                                 else
677                                         core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
678                                 end
679                         end
680                 end
681         end
682
683         return retval
684 end
685
686 function pkgmgr.compare_package(a, b)
687         return a and b and a.name == b.name and a.path == b.path
688 end
689
690 --------------------------------------------------------------------------------
691 function pkgmgr.comparemod(elem1,elem2)
692         if elem1 == nil or elem2 == nil then
693                 return false
694         end
695         if elem1.name ~= elem2.name then
696                 return false
697         end
698         if elem1.is_modpack ~= elem2.is_modpack then
699                 return false
700         end
701         if elem1.type ~= elem2.type then
702                 return false
703         end
704         if elem1.modpack ~= elem2.modpack then
705                 return false
706         end
707
708         if elem1.path ~= elem2.path then
709                 return false
710         end
711
712         return true
713 end
714
715 --------------------------------------------------------------------------------
716 function pkgmgr.refresh_globals()
717         local function is_equal(element,uid) --uid match
718                 if element.name == uid then
719                         return true
720                 end
721         end
722         pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
723                         pkgmgr.comparemod, is_equal, nil, {})
724         pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
725         pkgmgr.global_mods:set_sortmode("alphabetic")
726 end
727
728 --------------------------------------------------------------------------------
729 function pkgmgr.find_by_gameid(gameid)
730         for i, game in ipairs(pkgmgr.games) do
731                 if game.id == gameid then
732                         return game, i
733                 end
734         end
735         return nil, nil
736 end
737
738 --------------------------------------------------------------------------------
739 function pkgmgr.get_game_mods(gamespec, retval)
740         if gamespec ~= nil and
741                 gamespec.gamemods_path ~= nil and
742                 gamespec.gamemods_path ~= "" then
743                 pkgmgr.get_mods(gamespec.gamemods_path, ("games/%s/mods"):format(gamespec.id), retval)
744         end
745 end
746
747 --------------------------------------------------------------------------------
748 function pkgmgr.update_gamelist()
749         pkgmgr.games = core.get_games()
750         table.sort(pkgmgr.games, function(a, b)
751                 return a.title:lower() < b.title:lower()
752         end)
753 end
754
755 --------------------------------------------------------------------------------
756 -- read initial data
757 --------------------------------------------------------------------------------
758 pkgmgr.update_gamelist()