Move chat command handling code from C++ to Lua (#5528)
[oweals/minetest.git] / builtin / mainmenu / modmgr.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 .. DIR_DELIM
25                         local toadd = {}
26                         retval[#retval + 1] = toadd
27
28                         local mod_conf = Settings(prefix .. "mod.conf"):to_table()
29                         if mod_conf.name then
30                                 name = mod_conf.name
31                         end
32
33                         toadd.name = name
34                         toadd.path = prefix
35
36                         if modpack ~= nil and modpack ~= "" then
37                                 toadd.modpack = modpack
38                         else
39                                 local modpackfile = io.open(prefix .. "modpack.txt")
40                                 if modpackfile then
41                                         modpackfile:close()
42                                         toadd.is_modpack = true
43                                         get_mods(prefix, retval, name)
44                                 end
45                         end
46                 end
47         end
48 end
49
50 --modmanager implementation
51 modmgr = {}
52
53 --------------------------------------------------------------------------------
54 function modmgr.extract(modfile)
55         if modfile.type == "zip" then
56                 local tempfolder = os.tempfolder()
57
58                 if tempfolder ~= nil and
59                         tempfolder ~= "" then
60                         core.create_dir(tempfolder)
61                         if core.extract_zip(modfile.name,tempfolder) then
62                                 return tempfolder
63                         end
64                 end
65         end
66         return nil
67 end
68
69 -------------------------------------------------------------------------------
70 function modmgr.getbasefolder(temppath)
71
72         if temppath == nil then
73                 return {
74                 type = "invalid",
75                 path = ""
76                 }
77         end
78
79         local testfile = io.open(temppath .. DIR_DELIM .. "init.lua","r")
80         if testfile ~= nil then
81                 testfile:close()
82                 return {
83                                 type="mod",
84                                 path=temppath
85                                 }
86         end
87
88         testfile = io.open(temppath .. DIR_DELIM .. "modpack.txt","r")
89         if testfile ~= nil then
90                 testfile:close()
91                 return {
92                                 type="modpack",
93                                 path=temppath
94                                 }
95         end
96
97         local subdirs = core.get_dir_list(temppath, true)
98
99         --only single mod or modpack allowed
100         if #subdirs ~= 1 then
101                 return {
102                         type = "invalid",
103                         path = ""
104                         }
105         end
106
107         testfile =
108         io.open(temppath .. DIR_DELIM .. subdirs[1] ..DIR_DELIM .."init.lua","r")
109         if testfile ~= nil then
110                 testfile:close()
111                 return {
112                         type="mod",
113                         path= temppath .. DIR_DELIM .. subdirs[1]
114                         }
115         end
116
117         testfile =
118         io.open(temppath .. DIR_DELIM .. subdirs[1] ..DIR_DELIM .."modpack.txt","r")
119         if testfile ~= nil then
120                 testfile:close()
121                 return {
122                         type="modpack",
123                         path=temppath ..  DIR_DELIM .. subdirs[1]
124                         }
125         end
126
127         return {
128                 type = "invalid",
129                 path = ""
130                 }
131 end
132
133 --------------------------------------------------------------------------------
134 function modmgr.isValidModname(modpath)
135         if modpath:find("-") ~= nil then
136                 return false
137         end
138
139         return true
140 end
141
142 --------------------------------------------------------------------------------
143 function modmgr.parse_register_line(line)
144         local pos1 = line:find("\"")
145         local pos2 = nil
146         if pos1 ~= nil then
147                 pos2 = line:find("\"",pos1+1)
148         end
149
150         if pos1 ~= nil and pos2 ~= nil then
151                 local item = line:sub(pos1+1,pos2-1)
152
153                 if item ~= nil and
154                         item ~= "" then
155                         local pos3 = item:find(":")
156
157                         if pos3 ~= nil then
158                                 local retval = item:sub(1,pos3-1)
159                                 if retval ~= nil and
160                                         retval ~= "" then
161                                         return retval
162                                 end
163                         end
164                 end
165         end
166         return nil
167 end
168
169 --------------------------------------------------------------------------------
170 function modmgr.parse_dofile_line(modpath,line)
171         local pos1 = line:find("\"")
172         local pos2 = nil
173         if pos1 ~= nil then
174                 pos2 = line:find("\"",pos1+1)
175         end
176
177         if pos1 ~= nil and pos2 ~= nil then
178                 local filename = line:sub(pos1+1,pos2-1)
179
180                 if filename ~= nil and
181                         filename ~= "" and
182                         filename:find(".lua") then
183                         return modmgr.identify_modname(modpath,filename)
184                 end
185         end
186         return nil
187 end
188
189 --------------------------------------------------------------------------------
190 function modmgr.identify_modname(modpath,filename)
191         local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
192         if testfile ~= nil then
193                 local line = testfile:read()
194
195                 while line~= nil do
196                         local modname = nil
197
198                         if line:find("minetest.register_tool") then
199                                 modname = modmgr.parse_register_line(line)
200                         end
201
202                         if line:find("minetest.register_craftitem") then
203                                 modname = modmgr.parse_register_line(line)
204                         end
205
206
207                         if line:find("minetest.register_node") then
208                                 modname = modmgr.parse_register_line(line)
209                         end
210
211                         if line:find("dofile") then
212                                 modname = modmgr.parse_dofile_line(modpath,line)
213                         end
214
215                         if modname ~= nil then
216                                 testfile:close()
217                                 return modname
218                         end
219
220                         line = testfile:read()
221                 end
222                 testfile:close()
223         end
224
225         return nil
226 end
227 --------------------------------------------------------------------------------
228 function modmgr.render_modlist(render_list)
229         local retval = ""
230
231         if render_list == nil then
232                 if modmgr.global_mods == nil then
233                         modmgr.refresh_globals()
234                 end
235                 render_list = modmgr.global_mods
236         end
237
238         local list = render_list:get_list()
239         local last_modpack = nil
240         local retval = {}
241         local in_game_mods = false
242         for i, v in ipairs(list) do
243                 if v.typ == "game_mod" and not in_game_mods then
244                         in_game_mods = true
245                         retval[#retval + 1] = mt_color_blue
246                         retval[#retval + 1] = "0"
247                         retval[#retval + 1] = fgettext("Subgame Mods")
248                 end
249
250                 local color = ""
251                 if v.is_modpack then
252                         local rawlist = render_list:get_raw_list()
253                         color = mt_color_dark_green
254
255                         for j = 1, #rawlist, 1 do
256                                 if rawlist[j].modpack == list[i].name and
257                                                 rawlist[j].enabled ~= true then
258                                         -- Modpack not entirely enabled so showing as grey
259                                         color = mt_color_grey
260                                         break
261                                 end
262                         end
263                 elseif v.typ == "game_mod" then
264                         color = mt_color_blue
265                 elseif v.enabled then
266                         color = mt_color_green
267                 end
268
269                 retval[#retval + 1] = color
270                 if v.modpack ~= nil or v.typ == "game_mod" then
271                         retval[#retval + 1] = "1"
272                 else
273                         retval[#retval + 1] = "0"
274                 end
275                 retval[#retval + 1] = core.formspec_escape(v.name)
276         end
277
278         return table.concat(retval, ",")
279 end
280
281 --------------------------------------------------------------------------------
282 function modmgr.get_dependencies(modfolder)
283         local toadd_hard = ""
284         local toadd_soft = ""
285         if modfolder ~= nil then
286                 local filename = modfolder ..
287                                         DIR_DELIM .. "depends.txt"
288
289                 local hard_dependencies = {}
290                 local soft_dependencies = {}
291                 local dependencyfile = io.open(filename,"r")
292                 if dependencyfile then
293                         local dependency = dependencyfile:read("*l")
294                         while dependency do
295                                 dependency = dependency:gsub("\r", "")
296                                 if string.sub(dependency, -1, -1) == "?" then
297                                         table.insert(soft_dependencies, string.sub(dependency, 1, -2))
298                                 else
299                                         table.insert(hard_dependencies, dependency)
300                                 end
301                                 dependency = dependencyfile:read()
302                         end
303                         dependencyfile:close()
304                 end
305                 toadd_hard = table.concat(hard_dependencies, ",")
306                 toadd_soft = table.concat(soft_dependencies, ",")
307         end
308
309         return toadd_hard, toadd_soft
310 end
311
312 --------------------------------------------------------------------------------
313 function modmgr.get_worldconfig(worldpath)
314         local filename = worldpath ..
315                                 DIR_DELIM .. "world.mt"
316
317         local worldfile = Settings(filename)
318
319         local worldconfig = {}
320         worldconfig.global_mods = {}
321         worldconfig.game_mods = {}
322
323         for key,value in pairs(worldfile:to_table()) do
324                 if key == "gameid" then
325                         worldconfig.id = value
326                 elseif key:sub(0, 9) == "load_mod_" then
327                         worldconfig.global_mods[key] = core.is_yes(value)
328                 else
329                         worldconfig[key] = value
330                 end
331         end
332
333         --read gamemods
334         local gamespec = gamemgr.find_by_gameid(worldconfig.id)
335         gamemgr.get_game_mods(gamespec, worldconfig.game_mods)
336
337         return worldconfig
338 end
339
340 --------------------------------------------------------------------------------
341 function modmgr.installmod(modfilename,basename)
342         local modfile = modmgr.identify_filetype(modfilename)
343         local modpath = modmgr.extract(modfile)
344
345         if modpath == nil then
346                 gamedata.errormessage = fgettext("Install Mod: file: \"$1\"", modfile.name) ..
347                         fgettext("\nInstall Mod: unsupported filetype \"$1\" or broken archive", modfile.type)
348                 return
349         end
350
351         local basefolder = modmgr.getbasefolder(modpath)
352
353         if basefolder.type == "modpack" then
354                 local clean_path = nil
355
356                 if basename ~= nil then
357                         clean_path = "mp_" .. basename
358                 end
359
360                 if clean_path == nil then
361                         clean_path = get_last_folder(cleanup_path(basefolder.path))
362                 end
363
364                 if clean_path ~= nil then
365                         local targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
366                         if not core.copy_dir(basefolder.path,targetpath) then
367                                 gamedata.errormessage = fgettext("Failed to install $1 to $2", basename, targetpath)
368                         end
369                 else
370                         gamedata.errormessage = fgettext("Install Mod: unable to find suitable foldername for modpack $1", modfilename)
371                 end
372         end
373
374         if basefolder.type == "mod" then
375                 local targetfolder = basename
376
377                 if targetfolder == nil then
378                         targetfolder = modmgr.identify_modname(basefolder.path,"init.lua")
379                 end
380
381                 --if heuristic failed try to use current foldername
382                 if targetfolder == nil then
383                         targetfolder = get_last_folder(basefolder.path)
384                 end
385
386                 if targetfolder ~= nil and modmgr.isValidModname(targetfolder) then
387                         local targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
388                         core.copy_dir(basefolder.path,targetpath)
389                 else
390                         gamedata.errormessage = fgettext("Install Mod: unable to find real modname for: $1", modfilename)
391                 end
392         end
393
394         core.delete_dir(modpath)
395
396         modmgr.refresh_globals()
397
398 end
399
400 --------------------------------------------------------------------------------
401 function modmgr.preparemodlist(data)
402         local retval = {}
403
404         local global_mods = {}
405         local game_mods = {}
406
407         --read global mods
408         local modpath = core.get_modpath()
409
410         if modpath ~= nil and
411                 modpath ~= "" then
412                 get_mods(modpath,global_mods)
413         end
414
415         for i=1,#global_mods,1 do
416                 global_mods[i].typ = "global_mod"
417                 retval[#retval + 1] = global_mods[i]
418         end
419
420         --read game mods
421         local gamespec = gamemgr.find_by_gameid(data.gameid)
422         gamemgr.get_game_mods(gamespec, game_mods)
423
424         for i=1,#game_mods,1 do
425                 game_mods[i].typ = "game_mod"
426                 retval[#retval + 1] = game_mods[i]
427         end
428
429         if data.worldpath == nil then
430                 return retval
431         end
432
433         --read world mod configuration
434         local filename = data.worldpath ..
435                                 DIR_DELIM .. "world.mt"
436
437         local worldfile = Settings(filename)
438
439         for key,value in pairs(worldfile:to_table()) do
440                 if key:sub(1, 9) == "load_mod_" then
441                         key = key:sub(10)
442                         local element = nil
443                         for i=1,#retval,1 do
444                                 if retval[i].name == key and
445                                         not retval[i].is_modpack then
446                                         element = retval[i]
447                                         break
448                                 end
449                         end
450                         if element ~= nil then
451                                 element.enabled = core.is_yes(value)
452                         else
453                                 core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
454                         end
455                 end
456         end
457
458         return retval
459 end
460
461 --------------------------------------------------------------------------------
462 function modmgr.comparemod(elem1,elem2)
463         if elem1 == nil or elem2 == nil then
464                 return false
465         end
466         if elem1.name ~= elem2.name then
467                 return false
468         end
469         if elem1.is_modpack ~= elem2.is_modpack then
470                 return false
471         end
472         if elem1.typ ~= elem2.typ then
473                 return false
474         end
475         if elem1.modpack ~= elem2.modpack then
476                 return false
477         end
478
479         if elem1.path ~= elem2.path then
480                 return false
481         end
482
483         return true
484 end
485
486 --------------------------------------------------------------------------------
487 function modmgr.mod_exists(basename)
488
489         if modmgr.global_mods == nil then
490                 modmgr.refresh_globals()
491         end
492
493         if modmgr.global_mods:raw_index_by_uid(basename) > 0 then
494                 return true
495         end
496
497         return false
498 end
499
500 --------------------------------------------------------------------------------
501 function modmgr.get_global_mod(idx)
502
503         if modmgr.global_mods == nil then
504                 return nil
505         end
506
507         if idx == nil or idx < 1 or
508                 idx > modmgr.global_mods:size() then
509                 return nil
510         end
511
512         return modmgr.global_mods:get_list()[idx]
513 end
514
515 --------------------------------------------------------------------------------
516 function modmgr.refresh_globals()
517         modmgr.global_mods = filterlist.create(
518                                         modmgr.preparemodlist, --refresh
519                                         modmgr.comparemod, --compare
520                                         function(element,uid) --uid match
521                                                 if element.name == uid then
522                                                         return true
523                                                 end
524                                         end,
525                                         nil, --filter
526                                         {}
527                                         )
528         modmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
529         modmgr.global_mods:set_sortmode("alphabetic")
530 end
531
532 --------------------------------------------------------------------------------
533 function modmgr.identify_filetype(name)
534
535         if name:sub(-3):lower() == "zip" then
536                 return {
537                                 name = name,
538                                 type = "zip"
539                                 }
540         end
541
542         if name:sub(-6):lower() == "tar.gz" or
543                 name:sub(-3):lower() == "tgz"then
544                 return {
545                                 name = name,
546                                 type = "tgz"
547                                 }
548         end
549
550         if name:sub(-6):lower() == "tar.bz2" then
551                 return {
552                                 name = name,
553                                 type = "tbz"
554                                 }
555         end
556
557         if name:sub(-2):lower() == "7z" then
558                 return {
559                                 name = name,
560                                 type = "7z"
561                                 }
562         end
563
564         return {
565                 name = name,
566                 type = "ukn"
567         }
568 end