]> git.lizzy.rs Git - minetest.git/blob - builtin/mainmenu/pkgmgr.lua
Builtin: Fix grayed-out but enabled modpacks
[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 function get_mods(path,retval,modpack)
20         local mods = core.get_dir_list(path, true)
21
22         for _, name in ipairs(mods) do
23                 if name:sub(1, 1) ~= "." then
24                         local prefix = path .. DIR_DELIM .. name
25                         local toadd = {
26                                 dir_name = name,
27                                 parent_dir = path,
28                         }
29                         retval[#retval + 1] = toadd
30
31                         -- Get config file
32                         local mod_conf
33                         local modpack_conf = io.open(prefix .. DIR_DELIM .. "modpack.conf")
34                         if modpack_conf then
35                                 toadd.is_modpack = true
36                                 modpack_conf:close()
37
38                                 mod_conf = Settings(prefix .. DIR_DELIM .. "modpack.conf"):to_table()
39                                 if mod_conf.name then
40                                         name = mod_conf.name
41                                         toadd.is_name_explicit = true
42                                 end
43                         else
44                                 mod_conf = Settings(prefix .. DIR_DELIM .. "mod.conf"):to_table()
45                                 if mod_conf.name then
46                                         name = mod_conf.name
47                                         toadd.is_name_explicit = true
48                                 end
49                         end
50
51                         -- Read from config
52                         toadd.name = name
53                         toadd.author = mod_conf.author
54                         toadd.release = tonumber(mod_conf.release or "0")
55                         toadd.path = prefix
56                         toadd.type = "mod"
57
58                         -- Check modpack.txt
59                         --  Note: modpack.conf is already checked above
60                         local modpackfile = io.open(prefix .. DIR_DELIM .. "modpack.txt")
61                         if modpackfile then
62                                 modpackfile:close()
63                                 toadd.is_modpack = true
64                         end
65
66                         -- Deal with modpack contents
67                         if modpack and modpack ~= "" then
68                                 toadd.modpack = modpack
69                         elseif toadd.is_modpack then
70                                 toadd.type = "modpack"
71                                 toadd.is_modpack = true
72                                 get_mods(prefix, retval, name)
73                         end
74                 end
75         end
76 end
77
78 --modmanager implementation
79 pkgmgr = {}
80
81 function pkgmgr.get_texture_packs()
82         local txtpath = core.get_texturepath()
83         local list = core.get_dir_list(txtpath, true)
84         local retval = {}
85
86         local current_texture_path = core.settings:get("texture_path")
87
88         for _, item in ipairs(list) do
89                 if item ~= "base" then
90                         local name = item
91
92                         local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
93                         if path == current_texture_path then
94                                 name = fgettext("$1 (Enabled)", name)
95                         end
96
97                         local conf = Settings(path .. "texture_pack.conf")
98
99                         retval[#retval + 1] = {
100                                 name = item,
101                                 author = conf:get("author"),
102                                 release = tonumber(conf:get("release") or "0"),
103                                 list_name = name,
104                                 type = "txp",
105                                 path = path,
106                                 enabled = path == current_texture_path,
107                         }
108                 end
109         end
110
111         table.sort(retval, function(a, b)
112                 return a.name > b.name
113         end)
114
115         return retval
116 end
117
118 --------------------------------------------------------------------------------
119 function pkgmgr.extract(modfile)
120         if modfile.type == "zip" then
121                 local tempfolder = os.tempfolder()
122
123                 if tempfolder ~= nil and
124                         tempfolder ~= "" then
125                         core.create_dir(tempfolder)
126                         if core.extract_zip(modfile.name,tempfolder) then
127                                 return tempfolder
128                         end
129                 end
130         end
131         return nil
132 end
133
134 function pkgmgr.get_folder_type(path)
135         local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
136         if testfile ~= nil then
137                 testfile:close()
138                 return { type = "mod", path = path }
139         end
140
141         testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r")
142         if testfile ~= nil then
143                 testfile:close()
144                 return { type = "modpack", path = path }
145         end
146
147         testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
148         if testfile ~= nil then
149                 testfile:close()
150                 return { type = "modpack", path = path }
151         end
152
153         testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
154         if testfile ~= nil then
155                 testfile:close()
156                 return { type = "game", path = path }
157         end
158
159         testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
160         if testfile ~= nil then
161                 testfile:close()
162                 return { type = "txp", path = path }
163         end
164
165         return nil
166 end
167
168 -------------------------------------------------------------------------------
169 function pkgmgr.get_base_folder(temppath)
170         if temppath == nil then
171                 return { type = "invalid", path = "" }
172         end
173
174         local ret = pkgmgr.get_folder_type(temppath)
175         if ret then
176                 return ret
177         end
178
179         local subdirs = core.get_dir_list(temppath, true)
180         if #subdirs == 1 then
181                 ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
182                 if ret then
183                         return ret
184                 else
185                         return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
186                 end
187         end
188
189         return nil
190 end
191
192 --------------------------------------------------------------------------------
193 function pkgmgr.isValidModname(modpath)
194         if modpath:find("-") ~= nil then
195                 return false
196         end
197
198         return true
199 end
200
201 --------------------------------------------------------------------------------
202 function pkgmgr.parse_register_line(line)
203         local pos1 = line:find("\"")
204         local pos2 = nil
205         if pos1 ~= nil then
206                 pos2 = line:find("\"",pos1+1)
207         end
208
209         if pos1 ~= nil and pos2 ~= nil then
210                 local item = line:sub(pos1+1,pos2-1)
211
212                 if item ~= nil and
213                         item ~= "" then
214                         local pos3 = item:find(":")
215
216                         if pos3 ~= nil then
217                                 local retval = item:sub(1,pos3-1)
218                                 if retval ~= nil and
219                                         retval ~= "" then
220                                         return retval
221                                 end
222                         end
223                 end
224         end
225         return nil
226 end
227
228 --------------------------------------------------------------------------------
229 function pkgmgr.parse_dofile_line(modpath,line)
230         local pos1 = line:find("\"")
231         local pos2 = nil
232         if pos1 ~= nil then
233                 pos2 = line:find("\"",pos1+1)
234         end
235
236         if pos1 ~= nil and pos2 ~= nil then
237                 local filename = line:sub(pos1+1,pos2-1)
238
239                 if filename ~= nil and
240                         filename ~= "" and
241                         filename:find(".lua") then
242                         return pkgmgr.identify_modname(modpath,filename)
243                 end
244         end
245         return nil
246 end
247
248 --------------------------------------------------------------------------------
249 function pkgmgr.identify_modname(modpath,filename)
250         local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
251         if testfile ~= nil then
252                 local line = testfile:read()
253
254                 while line~= nil do
255                         local modname = nil
256
257                         if line:find("minetest.register_tool") then
258                                 modname = pkgmgr.parse_register_line(line)
259                         end
260
261                         if line:find("minetest.register_craftitem") then
262                                 modname = pkgmgr.parse_register_line(line)
263                         end
264
265
266                         if line:find("minetest.register_node") then
267                                 modname = pkgmgr.parse_register_line(line)
268                         end
269
270                         if line:find("dofile") then
271                                 modname = pkgmgr.parse_dofile_line(modpath,line)
272                         end
273
274                         if modname ~= nil then
275                                 testfile:close()
276                                 return modname
277                         end
278
279                         line = testfile:read()
280                 end
281                 testfile:close()
282         end
283
284         return nil
285 end
286 --------------------------------------------------------------------------------
287 function pkgmgr.render_packagelist(render_list)
288         local retval = ""
289
290         if render_list == nil then
291                 if pkgmgr.global_mods == nil then
292                         pkgmgr.refresh_globals()
293                 end
294                 render_list = pkgmgr.global_mods
295         end
296
297         local list = render_list:get_list()
298         local last_modpack = nil
299         local retval = {}
300         for i, v in ipairs(list) do
301                 local color = ""
302                 if v.is_modpack then
303                         local rawlist = render_list:get_raw_list()
304                         color = mt_color_dark_green
305
306                         for j = 1, #rawlist, 1 do
307                                 if rawlist[j].modpack == list[i].name and
308                                                 not rawlist[j].enabled then
309                                         -- Modpack not entirely enabled so showing as grey
310                                         color = mt_color_grey
311                                         break
312                                 end
313                         end
314                 elseif v.is_game_content or v.type == "game" then
315                         color = mt_color_blue
316                 elseif v.enabled or v.type == "txp" then
317                         color = mt_color_green
318                 end
319
320                 retval[#retval + 1] = color
321                 if v.modpack ~= nil or v.loc == "game" then
322                         retval[#retval + 1] = "1"
323                 else
324                         retval[#retval + 1] = "0"
325                 end
326                 retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
327         end
328
329         return table.concat(retval, ",")
330 end
331
332 --------------------------------------------------------------------------------
333 function pkgmgr.get_dependencies(path)
334         if path == nil then
335                 return "", ""
336         end
337
338         local info = core.get_content_info(path)
339         return table.concat(info.depends or {}, ","), table.concat(info.optional_depends or {}, ",")
340 end
341
342 ----------- tests whether all of the mods in the modpack are enabled -----------
343 function pkgmgr.is_modpack_entirely_enabled(data, name)
344         local rawlist = data.list:get_raw_list()
345         for j = 1, #rawlist do
346                 if rawlist[j].modpack == name and not rawlist[j].enabled then
347                         return false
348                 end
349         end
350         return true
351 end
352
353 ---------- toggles or en/disables a mod or modpack -----------------------------
354 function pkgmgr.enable_mod(this, toset)
355         local mod = this.data.list:get_list()[this.data.selected_mod]
356
357         -- game mods can't be enabled or disabled
358         if mod.is_game_content then
359                 return
360         end
361
362         -- toggle or en/disable the mod
363         if not mod.is_modpack then
364                 if toset == nil then
365                         mod.enabled = not mod.enabled
366                 else
367                         mod.enabled = toset
368                 end
369                 return
370         end
371
372         -- toggle or en/disable every mod in the modpack, interleaved unsupported
373         local list = this.data.list:get_raw_list()
374         for i = 1, #list do
375                 if list[i].modpack == mod.name then
376                         if toset == nil then
377                                 toset = not list[i].enabled
378                         end
379                         list[i].enabled = toset
380                 end
381         end
382 end
383
384 --------------------------------------------------------------------------------
385 function pkgmgr.get_worldconfig(worldpath)
386         local filename = worldpath ..
387                                 DIR_DELIM .. "world.mt"
388
389         local worldfile = Settings(filename)
390
391         local worldconfig = {}
392         worldconfig.global_mods = {}
393         worldconfig.game_mods = {}
394
395         for key,value in pairs(worldfile:to_table()) do
396                 if key == "gameid" then
397                         worldconfig.id = value
398                 elseif key:sub(0, 9) == "load_mod_" then
399                         -- Compatibility: Check against "nil" which was erroneously used
400                         -- as value for fresh configured worlds
401                         worldconfig.global_mods[key] = value ~= "false" and value ~= "nil"
402                                 and value
403                 else
404                         worldconfig[key] = value
405                 end
406         end
407
408         --read gamemods
409         local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
410         pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
411
412         return worldconfig
413 end
414
415 --------------------------------------------------------------------------------
416 function pkgmgr.install_dir(type, path, basename, targetpath)
417         local basefolder = pkgmgr.get_base_folder(path)
418
419         -- There's no good way to detect a texture pack, so let's just assume
420         -- it's correct for now.
421         if type == "txp" then
422                 if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
423                         return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
424                 end
425
426                 local from = basefolder and basefolder.path or path
427                 if targetpath then
428                         core.delete_dir(targetpath)
429                         core.create_dir(targetpath)
430                 else
431                         targetpath = core.get_texturepath() .. DIR_DELIM .. basename
432                 end
433                 if not core.copy_dir(from, targetpath) then
434                         return nil,
435                                 fgettext("Failed to install $1 to $2", basename, targetpath)
436                 end
437                 return targetpath, nil
438
439         elseif not basefolder then
440                 return nil, fgettext("Unable to find a valid mod or modpack")
441         end
442
443         --
444         -- Get destination
445         --
446         if basefolder.type == "modpack" then
447                 if type ~= "mod" then
448                         return nil, fgettext("Unable to install a modpack as a $1", type)
449                 end
450
451                 -- Get destination name for modpack
452                 if targetpath then
453                         core.delete_dir(targetpath)
454                         core.create_dir(targetpath)
455                 else
456                         local clean_path = nil
457                         if basename ~= nil then
458                                 clean_path = basename
459                         end
460                         if not clean_path then
461                                 clean_path = get_last_folder(cleanup_path(basefolder.path))
462                         end
463                         if clean_path then
464                                 targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
465                         else
466                                 return nil,
467                                         fgettext("Install Mod: Unable to find suitable folder name for modpack $1",
468                                         modfilename)
469                         end
470                 end
471         elseif basefolder.type == "mod" then
472                 if type ~= "mod" then
473                         return nil, fgettext("Unable to install a mod as a $1", type)
474                 end
475
476                 if targetpath then
477                         core.delete_dir(targetpath)
478                         core.create_dir(targetpath)
479                 else
480                         local targetfolder = basename
481                         if targetfolder == nil then
482                                 targetfolder = pkgmgr.identify_modname(basefolder.path, "init.lua")
483                         end
484
485                         -- If heuristic failed try to use current foldername
486                         if targetfolder == nil then
487                                 targetfolder = get_last_folder(basefolder.path)
488                         end
489
490                         if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
491                                 targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
492                         else
493                                 return nil, fgettext("Install Mod: Unable to find real mod name for: $1", modfilename)
494                         end
495                 end
496
497         elseif basefolder.type == "game" then
498                 if type ~= "game" then
499                         return nil, fgettext("Unable to install a game as a $1", type)
500                 end
501
502                 if targetpath then
503                         core.delete_dir(targetpath)
504                         core.create_dir(targetpath)
505                 else
506                         targetpath = core.get_gamepath() .. DIR_DELIM .. basename
507                 end
508         end
509
510         -- Copy it
511         if not core.copy_dir(basefolder.path, targetpath) then
512                 return nil,
513                         fgettext("Failed to install $1 to $2", basename, targetpath)
514         end
515
516         if basefolder.type == "game" then
517                 pkgmgr.update_gamelist()
518         else
519                 pkgmgr.refresh_globals()
520         end
521
522         return targetpath, nil
523 end
524
525 --------------------------------------------------------------------------------
526 function pkgmgr.install(type, modfilename, basename, dest)
527         local archive_info = pkgmgr.identify_filetype(modfilename)
528         local path = pkgmgr.extract(archive_info)
529
530         if path == nil then
531                 return nil,
532                         fgettext("Install: file: \"$1\"", archive_info.name) .. "\n" ..
533                         fgettext("Install: Unsupported file type \"$1\" or broken archive",
534                                 archive_info.type)
535         end
536
537         local targetpath, msg = pkgmgr.install_dir(type, path, basename, dest)
538         core.delete_dir(path)
539         return targetpath, msg
540 end
541
542 --------------------------------------------------------------------------------
543 function pkgmgr.preparemodlist(data)
544         local retval = {}
545
546         local global_mods = {}
547         local game_mods = {}
548
549         --read global mods
550         local modpath = core.get_modpath()
551
552         if modpath ~= nil and
553                 modpath ~= "" then
554                 get_mods(modpath,global_mods)
555         end
556
557         for i=1,#global_mods,1 do
558                 global_mods[i].type = "mod"
559                 global_mods[i].loc = "global"
560                 retval[#retval + 1] = global_mods[i]
561         end
562
563         --read game mods
564         local gamespec = pkgmgr.find_by_gameid(data.gameid)
565         pkgmgr.get_game_mods(gamespec, game_mods)
566
567         if #game_mods > 0 then
568                 -- Add title
569                 retval[#retval + 1] = {
570                         type = "game",
571                         is_game_content = true,
572                         name = fgettext("$1 mods", gamespec.name),
573                         path = gamespec.path
574                 }
575         end
576
577         for i=1,#game_mods,1 do
578                 game_mods[i].type = "mod"
579                 game_mods[i].loc = "game"
580                 game_mods[i].is_game_content = true
581                 retval[#retval + 1] = game_mods[i]
582         end
583
584         if data.worldpath == nil then
585                 return retval
586         end
587
588         --read world mod configuration
589         local filename = data.worldpath ..
590                                 DIR_DELIM .. "world.mt"
591
592         local worldfile = Settings(filename)
593
594         for key,value in pairs(worldfile:to_table()) do
595                 if key:sub(1, 9) == "load_mod_" then
596                         key = key:sub(10)
597                         local element = nil
598                         for i=1,#retval,1 do
599                                 if retval[i].name == key and
600                                         not retval[i].is_modpack then
601                                         element = retval[i]
602                                         break
603                                 end
604                         end
605                         if element ~= nil then
606                                 element.enabled = value ~= "false" and value ~= "nil" and value
607                         else
608                                 core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
609                         end
610                 end
611         end
612
613         return retval
614 end
615
616 function pkgmgr.compare_package(a, b)
617         return a and b and a.name == b.name and a.path == b.path
618 end
619
620 --------------------------------------------------------------------------------
621 function pkgmgr.comparemod(elem1,elem2)
622         if elem1 == nil or elem2 == nil then
623                 return false
624         end
625         if elem1.name ~= elem2.name then
626                 return false
627         end
628         if elem1.is_modpack ~= elem2.is_modpack then
629                 return false
630         end
631         if elem1.type ~= elem2.type then
632                 return false
633         end
634         if elem1.modpack ~= elem2.modpack then
635                 return false
636         end
637
638         if elem1.path ~= elem2.path then
639                 return false
640         end
641
642         return true
643 end
644
645 --------------------------------------------------------------------------------
646 function pkgmgr.mod_exists(basename)
647
648         if pkgmgr.global_mods == nil then
649                 pkgmgr.refresh_globals()
650         end
651
652         if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then
653                 return true
654         end
655
656         return false
657 end
658
659 --------------------------------------------------------------------------------
660 function pkgmgr.get_global_mod(idx)
661
662         if pkgmgr.global_mods == nil then
663                 return nil
664         end
665
666         if idx == nil or idx < 1 or
667                 idx > pkgmgr.global_mods:size() then
668                 return nil
669         end
670
671         return pkgmgr.global_mods:get_list()[idx]
672 end
673
674 --------------------------------------------------------------------------------
675 function pkgmgr.refresh_globals()
676         local function is_equal(element,uid) --uid match
677                 if element.name == uid then
678                         return true
679                 end
680         end
681         pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
682                         pkgmgr.comparemod, is_equal, nil, {})
683         pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
684         pkgmgr.global_mods:set_sortmode("alphabetic")
685 end
686
687 --------------------------------------------------------------------------------
688 function pkgmgr.identify_filetype(name)
689
690         if name:sub(-3):lower() == "zip" then
691                 return {
692                                 name = name,
693                                 type = "zip"
694                                 }
695         end
696
697         if name:sub(-6):lower() == "tar.gz" or
698                 name:sub(-3):lower() == "tgz"then
699                 return {
700                                 name = name,
701                                 type = "tgz"
702                                 }
703         end
704
705         if name:sub(-6):lower() == "tar.bz2" then
706                 return {
707                                 name = name,
708                                 type = "tbz"
709                                 }
710         end
711
712         if name:sub(-2):lower() == "7z" then
713                 return {
714                                 name = name,
715                                 type = "7z"
716                                 }
717         end
718
719         return {
720                 name = name,
721                 type = "ukn"
722         }
723 end
724
725
726 --------------------------------------------------------------------------------
727 function pkgmgr.find_by_gameid(gameid)
728         for i=1,#pkgmgr.games,1 do
729                 if pkgmgr.games[i].id == gameid then
730                         return pkgmgr.games[i], i
731                 end
732         end
733         return nil, nil
734 end
735
736 --------------------------------------------------------------------------------
737 function pkgmgr.get_game_mods(gamespec, retval)
738         if gamespec ~= nil and
739                 gamespec.gamemods_path ~= nil and
740                 gamespec.gamemods_path ~= "" then
741                 get_mods(gamespec.gamemods_path, retval)
742         end
743 end
744
745 --------------------------------------------------------------------------------
746 function pkgmgr.get_game_modlist(gamespec)
747         local retval = ""
748         local game_mods = {}
749         pkgmgr.get_game_mods(gamespec, game_mods)
750         for i=1,#game_mods,1 do
751                 if retval ~= "" then
752                         retval = retval..","
753                 end
754                 retval = retval .. game_mods[i].name
755         end
756         return retval
757 end
758
759 --------------------------------------------------------------------------------
760 function pkgmgr.get_game(index)
761         if index > 0 and index <= #pkgmgr.games then
762                 return pkgmgr.games[index]
763         end
764
765         return nil
766 end
767
768 --------------------------------------------------------------------------------
769 function pkgmgr.update_gamelist()
770         pkgmgr.games = core.get_games()
771 end
772
773 --------------------------------------------------------------------------------
774 function pkgmgr.gamelist()
775         local retval = ""
776         if #pkgmgr.games > 0 then
777                 retval = retval .. core.formspec_escape(pkgmgr.games[1].name)
778
779                 for i=2,#pkgmgr.games,1 do
780                         retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].name)
781                 end
782         end
783         return retval
784 end
785
786 --------------------------------------------------------------------------------
787 -- read initial data
788 --------------------------------------------------------------------------------
789 pkgmgr.update_gamelist()