]> git.lizzy.rs Git - minetest.git/blob - builtin/mainmenu/pkgmgr.lua
Tune shadow perspective distortion (#12146)
[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 name = item
82
83                         local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
84                         if path == current_texture_path then
85                                 name = fgettext("$1 (Enabled)", name)
86                         end
87
88                         local conf = Settings(path .. "texture_pack.conf")
89
90                         retval[#retval + 1] = {
91                                 name = item,
92                                 author = conf:get("author"),
93                                 release = tonumber(conf:get("release")) or 0,
94                                 list_name = name,
95                                 type = "txp",
96                                 path = path,
97                                 enabled = path == current_texture_path,
98                         }
99                 end
100         end
101 end
102
103 function get_mods(path, virtual_path, retval, modpack)
104         local mods = core.get_dir_list(path, true)
105
106         for _, name in ipairs(mods) do
107                 if name:sub(1, 1) ~= "." then
108                         local mod_path = path .. DIR_DELIM .. name
109                         local mod_virtual_path = virtual_path .. "/" .. name
110                         local toadd = {
111                                 dir_name = name,
112                                 parent_dir = path,
113                         }
114                         retval[#retval + 1] = toadd
115
116                         -- Get config file
117                         local mod_conf
118                         local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf")
119                         if modpack_conf then
120                                 toadd.is_modpack = true
121                                 modpack_conf:close()
122
123                                 mod_conf = Settings(mod_path .. DIR_DELIM .. "modpack.conf"):to_table()
124                                 if mod_conf.name then
125                                         name = mod_conf.name
126                                         toadd.is_name_explicit = true
127                                 end
128                         else
129                                 mod_conf = Settings(mod_path .. DIR_DELIM .. "mod.conf"):to_table()
130                                 if mod_conf.name then
131                                         name = mod_conf.name
132                                         toadd.is_name_explicit = true
133                                 end
134                         end
135
136                         -- Read from config
137                         toadd.name = name
138                         toadd.author = mod_conf.author
139                         toadd.release = tonumber(mod_conf.release) or 0
140                         toadd.path = mod_path
141                         toadd.virtual_path = mod_virtual_path
142                         toadd.type = "mod"
143
144                         -- Check modpack.txt
145                         -- Note: modpack.conf is already checked above
146                         local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
147                         if modpackfile then
148                                 modpackfile:close()
149                                 toadd.is_modpack = true
150                         end
151
152                         -- Deal with modpack contents
153                         if modpack and modpack ~= "" then
154                                 toadd.modpack = modpack
155                         elseif toadd.is_modpack then
156                                 toadd.type = "modpack"
157                                 toadd.is_modpack = true
158                                 get_mods(mod_path, mod_virtual_path, retval, name)
159                         end
160                 end
161         end
162 end
163
164 --modmanager implementation
165 pkgmgr = {}
166
167 function pkgmgr.get_texture_packs()
168         local txtpath = core.get_texturepath()
169         local txtpath_system = core.get_texturepath_share()
170         local retval = {}
171
172         load_texture_packs(txtpath, retval)
173         -- on portable versions these two paths coincide. It avoids loading the path twice
174         if txtpath ~= txtpath_system then
175                 load_texture_packs(txtpath_system, retval)
176         end
177
178         table.sort(retval, function(a, b)
179                 return a.name > b.name
180         end)
181
182         return retval
183 end
184
185 --------------------------------------------------------------------------------
186 function pkgmgr.get_folder_type(path)
187         local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
188         if testfile ~= nil then
189                 testfile:close()
190                 return { type = "mod", path = path }
191         end
192
193         testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r")
194         if testfile ~= nil then
195                 testfile:close()
196                 return { type = "modpack", path = path }
197         end
198
199         testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
200         if testfile ~= nil then
201                 testfile:close()
202                 return { type = "modpack", path = path }
203         end
204
205         testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
206         if testfile ~= nil then
207                 testfile:close()
208                 return { type = "game", path = path }
209         end
210
211         testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
212         if testfile ~= nil then
213                 testfile:close()
214                 return { type = "txp", path = path }
215         end
216
217         return nil
218 end
219
220 -------------------------------------------------------------------------------
221 function pkgmgr.get_base_folder(temppath)
222         if temppath == nil then
223                 return { type = "invalid", path = "" }
224         end
225
226         local ret = pkgmgr.get_folder_type(temppath)
227         if ret then
228                 return ret
229         end
230
231         local subdirs = core.get_dir_list(temppath, true)
232         if #subdirs == 1 then
233                 ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
234                 if ret then
235                         return ret
236                 else
237                         return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
238                 end
239         end
240
241         return nil
242 end
243
244 --------------------------------------------------------------------------------
245 function pkgmgr.isValidModname(modpath)
246         if modpath:find("-") ~= nil then
247                 return false
248         end
249
250         return true
251 end
252
253 --------------------------------------------------------------------------------
254 function pkgmgr.parse_register_line(line)
255         local pos1 = line:find("\"")
256         local pos2 = nil
257         if pos1 ~= nil then
258                 pos2 = line:find("\"",pos1+1)
259         end
260
261         if pos1 ~= nil and pos2 ~= nil then
262                 local item = line:sub(pos1+1,pos2-1)
263
264                 if item ~= nil and
265                         item ~= "" then
266                         local pos3 = item:find(":")
267
268                         if pos3 ~= nil then
269                                 local retval = item:sub(1,pos3-1)
270                                 if retval ~= nil and
271                                         retval ~= "" then
272                                         return retval
273                                 end
274                         end
275                 end
276         end
277         return nil
278 end
279
280 --------------------------------------------------------------------------------
281 function pkgmgr.parse_dofile_line(modpath,line)
282         local pos1 = line:find("\"")
283         local pos2 = nil
284         if pos1 ~= nil then
285                 pos2 = line:find("\"",pos1+1)
286         end
287
288         if pos1 ~= nil and pos2 ~= nil then
289                 local filename = line:sub(pos1+1,pos2-1)
290
291                 if filename ~= nil and
292                         filename ~= "" and
293                         filename:find(".lua") then
294                         return pkgmgr.identify_modname(modpath,filename)
295                 end
296         end
297         return nil
298 end
299
300 --------------------------------------------------------------------------------
301 function pkgmgr.identify_modname(modpath,filename)
302         local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
303         if testfile ~= nil then
304                 local line = testfile:read()
305
306                 while line~= nil do
307                         local modname = nil
308
309                         if line:find("minetest.register_tool") then
310                                 modname = pkgmgr.parse_register_line(line)
311                         end
312
313                         if line:find("minetest.register_craftitem") then
314                                 modname = pkgmgr.parse_register_line(line)
315                         end
316
317
318                         if line:find("minetest.register_node") then
319                                 modname = pkgmgr.parse_register_line(line)
320                         end
321
322                         if line:find("dofile") then
323                                 modname = pkgmgr.parse_dofile_line(modpath,line)
324                         end
325
326                         if modname ~= nil then
327                                 testfile:close()
328                                 return modname
329                         end
330
331                         line = testfile:read()
332                 end
333                 testfile:close()
334         end
335
336         return nil
337 end
338 --------------------------------------------------------------------------------
339 function pkgmgr.render_packagelist(render_list)
340         if not render_list then
341                 if not pkgmgr.global_mods then
342                         pkgmgr.refresh_globals()
343                 end
344                 render_list = pkgmgr.global_mods
345         end
346
347         local list = render_list:get_list()
348         local retval = {}
349         for i, v in ipairs(list) do
350                 local color = ""
351                 if v.is_modpack then
352                         local rawlist = render_list:get_raw_list()
353                         color = mt_color_dark_green
354
355                         for j = 1, #rawlist, 1 do
356                                 if rawlist[j].modpack == list[i].name and
357                                                 not rawlist[j].enabled then
358                                         -- Modpack not entirely enabled so showing as grey
359                                         color = mt_color_grey
360                                         break
361                                 end
362                         end
363                 elseif v.is_game_content or v.type == "game" then
364                         color = mt_color_blue
365                 elseif v.enabled or v.type == "txp" then
366                         color = mt_color_green
367                 end
368
369                 retval[#retval + 1] = color
370                 if v.modpack ~= nil or v.loc == "game" then
371                         retval[#retval + 1] = "1"
372                 else
373                         retval[#retval + 1] = "0"
374                 end
375                 retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
376         end
377
378         return table.concat(retval, ",")
379 end
380
381 --------------------------------------------------------------------------------
382 function pkgmgr.get_dependencies(path)
383         if path == nil then
384                 return {}, {}
385         end
386
387         local info = core.get_content_info(path)
388         return info.depends or {}, info.optional_depends or {}
389 end
390
391 ----------- tests whether all of the mods in the modpack are enabled -----------
392 function pkgmgr.is_modpack_entirely_enabled(data, name)
393         local rawlist = data.list:get_raw_list()
394         for j = 1, #rawlist do
395                 if rawlist[j].modpack == name and not rawlist[j].enabled then
396                         return false
397                 end
398         end
399         return true
400 end
401
402 local function disable_all_by_name(list, name, except)
403         for i=1, #list do
404                 if list[i].name == name and list[i] ~= except then
405                         list[i].enabled = false
406                 end
407         end
408 end
409
410 ---------- toggles or en/disables a mod or modpack and its dependencies --------
411 local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
412         if not mod.is_modpack then
413                 -- Toggle or en/disable the mod
414                 if toset == nil then
415                         toset = not mod.enabled
416                 end
417                 if mod.enabled ~= toset then
418                         toggled_mods[#toggled_mods+1] = mod.name
419                 end
420                 if toset then
421                         -- Mark this mod for recursive dependency traversal
422                         enabled_mods[mod.name] = true
423
424                         -- Disable other mods with the same name
425                         disable_all_by_name(list, mod.name, mod)
426                 end
427                 mod.enabled = toset
428         else
429                 -- Toggle or en/disable every mod in the modpack,
430                 -- interleaved unsupported
431                 for i = 1, #list do
432                         if list[i].modpack == mod.name then
433                                 toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, list[i])
434                         end
435                 end
436         end
437 end
438
439 function pkgmgr.enable_mod(this, toset)
440         local list = this.data.list:get_list()
441         local mod = list[this.data.selected_mod]
442
443         -- Game mods can't be enabled or disabled
444         if mod.is_game_content then
445                 return
446         end
447
448         local toggled_mods = {}
449         local enabled_mods = {}
450         toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
451         toset = mod.enabled -- Update if toggled
452
453         if not toset then
454                 -- Mod(s) were disabled, so no dependencies need to be enabled
455                 table.sort(toggled_mods)
456                 core.log("info", "Following mods were disabled: " ..
457                         table.concat(toggled_mods, ", "))
458                 return
459         end
460
461         -- Enable mods' depends after activation
462
463         -- Make a list of mod ids indexed by their names
464         local mod_ids = {}
465         for id, mod2 in pairs(list) do
466                 if mod2.type == "mod" and not mod2.is_modpack then
467                         mod_ids[mod2.name] = id
468                 end
469         end
470
471         -- to_enable is used as a DFS stack with sp as stack pointer
472         local to_enable = {}
473         local sp = 0
474         for name in pairs(enabled_mods) do
475                 local depends = pkgmgr.get_dependencies(list[mod_ids[name]].path)
476                 for i = 1, #depends do
477                         local dependency_name = depends[i]
478                         if not enabled_mods[dependency_name] then
479                                 sp = sp+1
480                                 to_enable[sp] = dependency_name
481                         end
482                 end
483         end
484
485         -- If sp is 0, every dependency is already activated
486         while sp > 0 do
487                 local name = to_enable[sp]
488                 sp = sp-1
489
490                 if not enabled_mods[name] then
491                         enabled_mods[name] = true
492                         local mod_to_enable = list[mod_ids[name]]
493                         if not mod_to_enable then
494                                 core.log("warning", "Mod dependency \"" .. name ..
495                                         "\" not found!")
496                         else
497                                 if not mod_to_enable.enabled then
498                                         mod_to_enable.enabled = true
499                                         toggled_mods[#toggled_mods+1] = mod_to_enable.name
500                                 end
501                                 -- Push the dependencies of the dependency onto the stack
502                                 local depends = pkgmgr.get_dependencies(mod_to_enable.path)
503                                 for i = 1, #depends do
504                                         if not enabled_mods[name] then
505                                                 sp = sp+1
506                                                 to_enable[sp] = depends[i]
507                                         end
508                                 end
509                         end
510                 end
511         end
512
513         -- Log the list of enabled mods
514         table.sort(toggled_mods)
515         core.log("info", "Following mods were enabled: " ..
516                 table.concat(toggled_mods, ", "))
517 end
518
519 --------------------------------------------------------------------------------
520 function pkgmgr.get_worldconfig(worldpath)
521         local filename = worldpath ..
522                                 DIR_DELIM .. "world.mt"
523
524         local worldfile = Settings(filename)
525
526         local worldconfig = {}
527         worldconfig.global_mods = {}
528         worldconfig.game_mods = {}
529
530         for key,value in pairs(worldfile:to_table()) do
531                 if key == "gameid" then
532                         worldconfig.id = value
533                 elseif key:sub(0, 9) == "load_mod_" then
534                         -- Compatibility: Check against "nil" which was erroneously used
535                         -- as value for fresh configured worlds
536                         worldconfig.global_mods[key] = value ~= "false" and value ~= "nil"
537                                 and value
538                 else
539                         worldconfig[key] = value
540                 end
541         end
542
543         --read gamemods
544         local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
545         pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
546
547         return worldconfig
548 end
549
550 --------------------------------------------------------------------------------
551 function pkgmgr.install_dir(type, path, basename, targetpath)
552         local basefolder = pkgmgr.get_base_folder(path)
553
554         -- There's no good way to detect a texture pack, so let's just assume
555         -- it's correct for now.
556         if type == "txp" then
557                 if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
558                         return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
559                 end
560
561                 local from = basefolder and basefolder.path or path
562                 if targetpath then
563                         core.delete_dir(targetpath)
564                 else
565                         targetpath = core.get_texturepath() .. DIR_DELIM .. basename
566                 end
567                 if not core.copy_dir(from, targetpath, false) then
568                         return nil,
569                                 fgettext("Failed to install $1 to $2", basename, targetpath)
570                 end
571                 return targetpath, nil
572
573         elseif not basefolder then
574                 return nil, fgettext("Unable to find a valid mod or modpack")
575         end
576
577         --
578         -- Get destination
579         --
580         if basefolder.type == "modpack" then
581                 if type ~= "mod" then
582                         return nil, fgettext("Unable to install a modpack as a $1", type)
583                 end
584
585                 -- Get destination name for modpack
586                 if targetpath then
587                         core.delete_dir(targetpath)
588                 else
589                         local clean_path = nil
590                         if basename ~= nil then
591                                 clean_path = basename
592                         end
593                         if not clean_path then
594                                 clean_path = get_last_folder(cleanup_path(basefolder.path))
595                         end
596                         if clean_path then
597                                 targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
598                         else
599                                 return nil,
600                                         fgettext("Install Mod: Unable to find suitable folder name for modpack $1",
601                                         path)
602                         end
603                 end
604         elseif basefolder.type == "mod" then
605                 if type ~= "mod" then
606                         return nil, fgettext("Unable to install a mod as a $1", type)
607                 end
608
609                 if targetpath then
610                         core.delete_dir(targetpath)
611                 else
612                         local targetfolder = basename
613                         if targetfolder == nil then
614                                 targetfolder = pkgmgr.identify_modname(basefolder.path, "init.lua")
615                         end
616
617                         -- If heuristic failed try to use current foldername
618                         if targetfolder == nil then
619                                 targetfolder = get_last_folder(basefolder.path)
620                         end
621
622                         if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
623                                 targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
624                         else
625                                 return nil, fgettext("Install Mod: Unable to find real mod name for: $1", path)
626                         end
627                 end
628
629         elseif basefolder.type == "game" then
630                 if type ~= "game" then
631                         return nil, fgettext("Unable to install a game as a $1", type)
632                 end
633
634                 if targetpath then
635                         core.delete_dir(targetpath)
636                 else
637                         targetpath = core.get_gamepath() .. DIR_DELIM .. basename
638                 end
639         end
640
641         -- Copy it
642         if not core.copy_dir(basefolder.path, targetpath, false) then
643                 return nil,
644                         fgettext("Failed to install $1 to $2", basename, targetpath)
645         end
646
647         if basefolder.type == "game" then
648                 pkgmgr.update_gamelist()
649         else
650                 pkgmgr.refresh_globals()
651         end
652
653         return targetpath, nil
654 end
655
656 --------------------------------------------------------------------------------
657 function pkgmgr.preparemodlist(data)
658         local retval = {}
659
660         local global_mods = {}
661         local game_mods = {}
662
663         --read global mods
664         local modpaths = core.get_modpaths()
665         for key, modpath in pairs(modpaths) do
666                 get_mods(modpath, key, global_mods)
667         end
668
669         for i=1,#global_mods,1 do
670                 global_mods[i].type = "mod"
671                 global_mods[i].loc = "global"
672                 global_mods[i].enabled = false
673                 retval[#retval + 1] = global_mods[i]
674         end
675
676         --read game mods
677         local gamespec = pkgmgr.find_by_gameid(data.gameid)
678         pkgmgr.get_game_mods(gamespec, game_mods)
679
680         if #game_mods > 0 then
681                 -- Add title
682                 retval[#retval + 1] = {
683                         type = "game",
684                         is_game_content = true,
685                         name = fgettext("$1 mods", gamespec.name),
686                         path = gamespec.path
687                 }
688         end
689
690         for i=1,#game_mods,1 do
691                 game_mods[i].type = "mod"
692                 game_mods[i].loc = "game"
693                 game_mods[i].is_game_content = true
694                 retval[#retval + 1] = game_mods[i]
695         end
696
697         if data.worldpath == nil then
698                 return retval
699         end
700
701         --read world mod configuration
702         local filename = data.worldpath ..
703                                 DIR_DELIM .. "world.mt"
704
705         local worldfile = Settings(filename)
706         for key, value in pairs(worldfile:to_table()) do
707                 if key:sub(1, 9) == "load_mod_" then
708                         key = key:sub(10)
709                         local mod_found = false
710
711                         local fallback_found = false
712                         local fallback_mod = nil
713
714                         for i=1, #retval do
715                                 if retval[i].name == key and
716                                                 not retval[i].is_modpack then
717                                         if core.is_yes(value) or retval[i].virtual_path == value then
718                                                 retval[i].enabled = true
719                                                 mod_found = true
720                                                 break
721                                         elseif fallback_found then
722                                                 -- Only allow fallback if only one mod matches
723                                                 fallback_mod = nil
724                                         else
725                                                 fallback_found = true
726                                                 fallback_mod = retval[i]
727                                         end
728                                 end
729                         end
730
731                         if not mod_found then
732                                 if fallback_mod and value:find("/") then
733                                         fallback_mod.enabled = true
734                                 else
735                                         core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
736                                 end
737                         end
738                 end
739         end
740
741         return retval
742 end
743
744 function pkgmgr.compare_package(a, b)
745         return a and b and a.name == b.name and a.path == b.path
746 end
747
748 --------------------------------------------------------------------------------
749 function pkgmgr.comparemod(elem1,elem2)
750         if elem1 == nil or elem2 == nil then
751                 return false
752         end
753         if elem1.name ~= elem2.name then
754                 return false
755         end
756         if elem1.is_modpack ~= elem2.is_modpack then
757                 return false
758         end
759         if elem1.type ~= elem2.type then
760                 return false
761         end
762         if elem1.modpack ~= elem2.modpack then
763                 return false
764         end
765
766         if elem1.path ~= elem2.path then
767                 return false
768         end
769
770         return true
771 end
772
773 --------------------------------------------------------------------------------
774 function pkgmgr.mod_exists(basename)
775
776         if pkgmgr.global_mods == nil then
777                 pkgmgr.refresh_globals()
778         end
779
780         if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then
781                 return true
782         end
783
784         return false
785 end
786
787 --------------------------------------------------------------------------------
788 function pkgmgr.get_global_mod(idx)
789
790         if pkgmgr.global_mods == nil then
791                 return nil
792         end
793
794         if idx == nil or idx < 1 or
795                 idx > pkgmgr.global_mods:size() then
796                 return nil
797         end
798
799         return pkgmgr.global_mods:get_list()[idx]
800 end
801
802 --------------------------------------------------------------------------------
803 function pkgmgr.refresh_globals()
804         local function is_equal(element,uid) --uid match
805                 if element.name == uid then
806                         return true
807                 end
808         end
809         pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
810                         pkgmgr.comparemod, is_equal, nil, {})
811         pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
812         pkgmgr.global_mods:set_sortmode("alphabetic")
813 end
814
815 --------------------------------------------------------------------------------
816 function pkgmgr.find_by_gameid(gameid)
817         for i=1,#pkgmgr.games,1 do
818                 if pkgmgr.games[i].id == gameid then
819                         return pkgmgr.games[i], i
820                 end
821         end
822         return nil, nil
823 end
824
825 --------------------------------------------------------------------------------
826 function pkgmgr.get_game_mods(gamespec, retval)
827         if gamespec ~= nil and
828                 gamespec.gamemods_path ~= nil and
829                 gamespec.gamemods_path ~= "" then
830                 get_mods(gamespec.gamemods_path, ("games/%s/mods"):format(gamespec.id), retval)
831         end
832 end
833
834 --------------------------------------------------------------------------------
835 function pkgmgr.get_game_modlist(gamespec)
836         local retval = ""
837         local game_mods = {}
838         pkgmgr.get_game_mods(gamespec, game_mods)
839         for i=1,#game_mods,1 do
840                 if retval ~= "" then
841                         retval = retval..","
842                 end
843                 retval = retval .. game_mods[i].name
844         end
845         return retval
846 end
847
848 --------------------------------------------------------------------------------
849 function pkgmgr.get_game(index)
850         if index > 0 and index <= #pkgmgr.games then
851                 return pkgmgr.games[index]
852         end
853
854         return nil
855 end
856
857 --------------------------------------------------------------------------------
858 function pkgmgr.update_gamelist()
859         pkgmgr.games = core.get_games()
860 end
861
862 --------------------------------------------------------------------------------
863 function pkgmgr.gamelist()
864         local retval = ""
865         if #pkgmgr.games > 0 then
866                 retval = retval .. core.formspec_escape(pkgmgr.games[1].name)
867
868                 for i=2,#pkgmgr.games,1 do
869                         retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].name)
870                 end
871         end
872         return retval
873 end
874
875 --------------------------------------------------------------------------------
876 -- read initial data
877 --------------------------------------------------------------------------------
878 pkgmgr.update_gamelist()