Add formspec escaping to subgame list in create world dialog (#5808)
[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         for i, v in ipairs(list) do
242                 local color = ""
243                 if v.is_modpack then
244                         local rawlist = render_list:get_raw_list()
245                         color = mt_color_dark_green
246
247                         for j = 1, #rawlist, 1 do
248                                 if rawlist[j].modpack == list[i].name and
249                                                 rawlist[j].enabled ~= true then
250                                         -- Modpack not entirely enabled so showing as grey
251                                         color = mt_color_grey
252                                         break
253                                 end
254                         end
255                 elseif v.is_game_content then
256                         color = mt_color_blue
257                 elseif v.enabled then
258                         color = mt_color_green
259                 end
260
261                 retval[#retval + 1] = color
262                 if v.modpack ~= nil or v.typ == "game_mod" then
263                         retval[#retval + 1] = "1"
264                 else
265                         retval[#retval + 1] = "0"
266                 end
267                 retval[#retval + 1] = core.formspec_escape(v.name)
268         end
269
270         return table.concat(retval, ",")
271 end
272
273 --------------------------------------------------------------------------------
274 function modmgr.get_dependencies(modfolder)
275         local toadd_hard = ""
276         local toadd_soft = ""
277         if modfolder ~= nil then
278                 local filename = modfolder ..
279                                         DIR_DELIM .. "depends.txt"
280
281                 local hard_dependencies = {}
282                 local soft_dependencies = {}
283                 local dependencyfile = io.open(filename,"r")
284                 if dependencyfile then
285                         local dependency = dependencyfile:read("*l")
286                         while dependency do
287                                 dependency = dependency:gsub("\r", "")
288                                 if string.sub(dependency, -1, -1) == "?" then
289                                         table.insert(soft_dependencies, string.sub(dependency, 1, -2))
290                                 else
291                                         table.insert(hard_dependencies, dependency)
292                                 end
293                                 dependency = dependencyfile:read()
294                         end
295                         dependencyfile:close()
296                 end
297                 toadd_hard = table.concat(hard_dependencies, ",")
298                 toadd_soft = table.concat(soft_dependencies, ",")
299         end
300
301         return toadd_hard, toadd_soft
302 end
303
304 --------------------------------------------------------------------------------
305 function modmgr.get_worldconfig(worldpath)
306         local filename = worldpath ..
307                                 DIR_DELIM .. "world.mt"
308
309         local worldfile = Settings(filename)
310
311         local worldconfig = {}
312         worldconfig.global_mods = {}
313         worldconfig.game_mods = {}
314
315         for key,value in pairs(worldfile:to_table()) do
316                 if key == "gameid" then
317                         worldconfig.id = value
318                 elseif key:sub(0, 9) == "load_mod_" then
319                         worldconfig.global_mods[key] = core.is_yes(value)
320                 else
321                         worldconfig[key] = value
322                 end
323         end
324
325         --read gamemods
326         local gamespec = gamemgr.find_by_gameid(worldconfig.id)
327         gamemgr.get_game_mods(gamespec, worldconfig.game_mods)
328
329         return worldconfig
330 end
331
332 --------------------------------------------------------------------------------
333 function modmgr.installmod(modfilename,basename)
334         local modfile = modmgr.identify_filetype(modfilename)
335         local modpath = modmgr.extract(modfile)
336
337         if modpath == nil then
338                 gamedata.errormessage = fgettext("Install Mod: file: \"$1\"", modfile.name) ..
339                         fgettext("\nInstall Mod: unsupported filetype \"$1\" or broken archive", modfile.type)
340                 return
341         end
342
343         local basefolder = modmgr.getbasefolder(modpath)
344
345         if basefolder.type == "modpack" then
346                 local clean_path = nil
347
348                 if basename ~= nil then
349                         clean_path = "mp_" .. basename
350                 end
351
352                 if clean_path == nil then
353                         clean_path = get_last_folder(cleanup_path(basefolder.path))
354                 end
355
356                 if clean_path ~= nil then
357                         local targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
358                         if not core.copy_dir(basefolder.path,targetpath) then
359                                 gamedata.errormessage = fgettext("Failed to install $1 to $2", basename, targetpath)
360                         end
361                 else
362                         gamedata.errormessage = fgettext("Install Mod: unable to find suitable foldername for modpack $1", modfilename)
363                 end
364         end
365
366         if basefolder.type == "mod" then
367                 local targetfolder = basename
368
369                 if targetfolder == nil then
370                         targetfolder = modmgr.identify_modname(basefolder.path,"init.lua")
371                 end
372
373                 --if heuristic failed try to use current foldername
374                 if targetfolder == nil then
375                         targetfolder = get_last_folder(basefolder.path)
376                 end
377
378                 if targetfolder ~= nil and modmgr.isValidModname(targetfolder) then
379                         local targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
380                         core.copy_dir(basefolder.path,targetpath)
381                 else
382                         gamedata.errormessage = fgettext("Install Mod: unable to find real modname for: $1", modfilename)
383                 end
384         end
385
386         core.delete_dir(modpath)
387
388         modmgr.refresh_globals()
389
390 end
391
392 --------------------------------------------------------------------------------
393 function modmgr.preparemodlist(data)
394         local retval = {}
395
396         local global_mods = {}
397         local game_mods = {}
398
399         --read global mods
400         local modpath = core.get_modpath()
401
402         if modpath ~= nil and
403                 modpath ~= "" then
404                 get_mods(modpath,global_mods)
405         end
406
407         for i=1,#global_mods,1 do
408                 global_mods[i].typ = "global_mod"
409                 retval[#retval + 1] = global_mods[i]
410         end
411
412         --read game mods
413         local gamespec = gamemgr.find_by_gameid(data.gameid)
414         gamemgr.get_game_mods(gamespec, game_mods)
415
416         if #game_mods > 0 then
417                 -- Add title
418                 retval[#retval + 1] = {
419                         typ = "game",
420                         is_game_content = true,
421                         name = fgettext("Subgame Mods")
422                 }
423         end
424
425         for i=1,#game_mods,1 do
426                 game_mods[i].typ = "game_mod"
427                 game_mods[i].is_game_content = true
428                 retval[#retval + 1] = game_mods[i]
429         end
430
431         if data.worldpath == nil then
432                 return retval
433         end
434
435         --read world mod configuration
436         local filename = data.worldpath ..
437                                 DIR_DELIM .. "world.mt"
438
439         local worldfile = Settings(filename)
440
441         for key,value in pairs(worldfile:to_table()) do
442                 if key:sub(1, 9) == "load_mod_" then
443                         key = key:sub(10)
444                         local element = nil
445                         for i=1,#retval,1 do
446                                 if retval[i].name == key and
447                                         not retval[i].is_modpack then
448                                         element = retval[i]
449                                         break
450                                 end
451                         end
452                         if element ~= nil then
453                                 element.enabled = core.is_yes(value)
454                         else
455                                 core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
456                         end
457                 end
458         end
459
460         return retval
461 end
462
463 --------------------------------------------------------------------------------
464 function modmgr.comparemod(elem1,elem2)
465         if elem1 == nil or elem2 == nil then
466                 return false
467         end
468         if elem1.name ~= elem2.name then
469                 return false
470         end
471         if elem1.is_modpack ~= elem2.is_modpack then
472                 return false
473         end
474         if elem1.typ ~= elem2.typ then
475                 return false
476         end
477         if elem1.modpack ~= elem2.modpack then
478                 return false
479         end
480
481         if elem1.path ~= elem2.path then
482                 return false
483         end
484
485         return true
486 end
487
488 --------------------------------------------------------------------------------
489 function modmgr.mod_exists(basename)
490
491         if modmgr.global_mods == nil then
492                 modmgr.refresh_globals()
493         end
494
495         if modmgr.global_mods:raw_index_by_uid(basename) > 0 then
496                 return true
497         end
498
499         return false
500 end
501
502 --------------------------------------------------------------------------------
503 function modmgr.get_global_mod(idx)
504
505         if modmgr.global_mods == nil then
506                 return nil
507         end
508
509         if idx == nil or idx < 1 or
510                 idx > modmgr.global_mods:size() then
511                 return nil
512         end
513
514         return modmgr.global_mods:get_list()[idx]
515 end
516
517 --------------------------------------------------------------------------------
518 function modmgr.refresh_globals()
519         modmgr.global_mods = filterlist.create(
520                                         modmgr.preparemodlist, --refresh
521                                         modmgr.comparemod, --compare
522                                         function(element,uid) --uid match
523                                                 if element.name == uid then
524                                                         return true
525                                                 end
526                                         end,
527                                         nil, --filter
528                                         {}
529                                         )
530         modmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
531         modmgr.global_mods:set_sortmode("alphabetic")
532 end
533
534 --------------------------------------------------------------------------------
535 function modmgr.identify_filetype(name)
536
537         if name:sub(-3):lower() == "zip" then
538                 return {
539                                 name = name,
540                                 type = "zip"
541                                 }
542         end
543
544         if name:sub(-6):lower() == "tar.gz" or
545                 name:sub(-3):lower() == "tgz"then
546                 return {
547                                 name = name,
548                                 type = "tgz"
549                                 }
550         end
551
552         if name:sub(-6):lower() == "tar.bz2" then
553                 return {
554                                 name = name,
555                                 type = "tbz"
556                                 }
557         end
558
559         if name:sub(-2):lower() == "7z" then
560                 return {
561                                 name = name,
562                                 type = "7z"
563                                 }
564         end
565
566         return {
567                 name = name,
568                 type = "ukn"
569         }
570 end