2 --Copyright (C) 2013 sapier
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.
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.
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.
18 --------------------------------------------------------------------------------
19 local function get_last_folder(text,count)
20 local parts = text:split(DIR_DELIM)
28 retval = retval .. parts[#parts - (count-i)] .. DIR_DELIM
34 local function cleanup_path(temppath)
36 local parts = temppath:split("-")
39 if temppath ~= "" then
40 temppath = temppath .. "_"
42 temppath = temppath .. parts[i]
45 parts = temppath:split(".")
48 if temppath ~= "" then
49 temppath = temppath .. "_"
51 temppath = temppath .. parts[i]
54 parts = temppath:split("'")
57 if temppath ~= "" then
58 temppath = temppath .. ""
60 temppath = temppath .. parts[i]
63 parts = temppath:split(" ")
66 if temppath ~= "" then
69 temppath = temppath .. parts[i]
75 function get_mods(path,retval,modpack)
76 local mods = core.get_dir_list(path, true)
78 for _, name in ipairs(mods) do
79 if name:sub(1, 1) ~= "." then
80 local prefix = path .. DIR_DELIM .. name
85 retval[#retval + 1] = toadd
89 local modpack_conf = io.open(prefix .. DIR_DELIM .. "modpack.conf")
91 toadd.is_modpack = true
94 mod_conf = Settings(prefix .. DIR_DELIM .. "modpack.conf"):to_table()
97 toadd.is_name_explicit = true
100 mod_conf = Settings(prefix .. DIR_DELIM .. "mod.conf"):to_table()
101 if mod_conf.name then
103 toadd.is_name_explicit = true
109 toadd.author = mod_conf.author
110 toadd.release = tonumber(mod_conf.release or "0")
115 -- Note: modpack.conf is already checked above
116 local modpackfile = io.open(prefix .. DIR_DELIM .. "modpack.txt")
119 toadd.is_modpack = true
122 -- Deal with modpack contents
123 if modpack and modpack ~= "" then
124 toadd.modpack = modpack
125 elseif toadd.is_modpack then
126 toadd.type = "modpack"
127 toadd.is_modpack = true
128 get_mods(prefix, retval, name)
134 --modmanager implementation
137 function pkgmgr.get_texture_packs()
138 local txtpath = core.get_texturepath()
139 local list = core.get_dir_list(txtpath, true)
142 local current_texture_path = core.settings:get("texture_path")
144 for _, item in ipairs(list) do
145 if item ~= "base" then
148 local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
149 if path == current_texture_path then
150 name = fgettext("$1 (Enabled)", name)
153 local conf = Settings(path .. "texture_pack.conf")
155 retval[#retval + 1] = {
157 author = conf:get("author"),
158 release = tonumber(conf:get("release") or "0"),
162 enabled = path == current_texture_path,
167 table.sort(retval, function(a, b)
168 return a.name > b.name
174 --------------------------------------------------------------------------------
175 function pkgmgr.extract(modfile)
176 if modfile.type == "zip" then
177 local tempfolder = os.tempfolder()
179 if tempfolder ~= nil and
180 tempfolder ~= "" then
181 core.create_dir(tempfolder)
182 if core.extract_zip(modfile.name,tempfolder) then
190 function pkgmgr.get_folder_type(path)
191 local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
192 if testfile ~= nil then
194 return { type = "mod", path = path }
197 testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r")
198 if testfile ~= nil then
200 return { type = "modpack", path = path }
203 testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
204 if testfile ~= nil then
206 return { type = "modpack", path = path }
209 testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
210 if testfile ~= nil then
212 return { type = "game", path = path }
215 testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
216 if testfile ~= nil then
218 return { type = "txp", path = path }
224 -------------------------------------------------------------------------------
225 function pkgmgr.get_base_folder(temppath)
226 if temppath == nil then
227 return { type = "invalid", path = "" }
230 local ret = pkgmgr.get_folder_type(temppath)
235 local subdirs = core.get_dir_list(temppath, true)
236 if #subdirs == 1 then
237 ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
241 return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
248 --------------------------------------------------------------------------------
249 function pkgmgr.isValidModname(modpath)
250 if modpath:find("-") ~= nil then
257 --------------------------------------------------------------------------------
258 function pkgmgr.parse_register_line(line)
259 local pos1 = line:find("\"")
262 pos2 = line:find("\"",pos1+1)
265 if pos1 ~= nil and pos2 ~= nil then
266 local item = line:sub(pos1+1,pos2-1)
270 local pos3 = item:find(":")
273 local retval = item:sub(1,pos3-1)
284 --------------------------------------------------------------------------------
285 function pkgmgr.parse_dofile_line(modpath,line)
286 local pos1 = line:find("\"")
289 pos2 = line:find("\"",pos1+1)
292 if pos1 ~= nil and pos2 ~= nil then
293 local filename = line:sub(pos1+1,pos2-1)
295 if filename ~= nil and
297 filename:find(".lua") then
298 return pkgmgr.identify_modname(modpath,filename)
304 --------------------------------------------------------------------------------
305 function pkgmgr.identify_modname(modpath,filename)
306 local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
307 if testfile ~= nil then
308 local line = testfile:read()
313 if line:find("minetest.register_tool") then
314 modname = pkgmgr.parse_register_line(line)
317 if line:find("minetest.register_craftitem") then
318 modname = pkgmgr.parse_register_line(line)
322 if line:find("minetest.register_node") then
323 modname = pkgmgr.parse_register_line(line)
326 if line:find("dofile") then
327 modname = pkgmgr.parse_dofile_line(modpath,line)
330 if modname ~= nil then
335 line = testfile:read()
342 --------------------------------------------------------------------------------
343 function pkgmgr.render_packagelist(render_list)
344 if not render_list then
345 if not pkgmgr.global_mods then
346 pkgmgr.refresh_globals()
348 render_list = pkgmgr.global_mods
351 local list = render_list:get_list()
353 for i, v in ipairs(list) do
356 local rawlist = render_list:get_raw_list()
357 color = mt_color_dark_green
359 for j = 1, #rawlist, 1 do
360 if rawlist[j].modpack == list[i].name and
361 not rawlist[j].enabled then
362 -- Modpack not entirely enabled so showing as grey
363 color = mt_color_grey
367 elseif v.is_game_content or v.type == "game" then
368 color = mt_color_blue
369 elseif v.enabled or v.type == "txp" then
370 color = mt_color_green
373 retval[#retval + 1] = color
374 if v.modpack ~= nil or v.loc == "game" then
375 retval[#retval + 1] = "1"
377 retval[#retval + 1] = "0"
379 retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
382 return table.concat(retval, ",")
385 --------------------------------------------------------------------------------
386 function pkgmgr.get_dependencies(path)
391 local info = core.get_content_info(path)
392 return info.depends or {}, info.optional_depends or {}
395 ----------- tests whether all of the mods in the modpack are enabled -----------
396 function pkgmgr.is_modpack_entirely_enabled(data, name)
397 local rawlist = data.list:get_raw_list()
398 for j = 1, #rawlist do
399 if rawlist[j].modpack == name and not rawlist[j].enabled then
406 ---------- toggles or en/disables a mod or modpack and its dependencies --------
407 function pkgmgr.enable_mod(this, toset)
408 local list = this.data.list:get_list()
409 local mod = list[this.data.selected_mod]
411 -- Game mods can't be enabled or disabled
412 if mod.is_game_content then
416 local toggled_mods = {}
418 local enabled_mods = {}
419 if not mod.is_modpack then
420 -- Toggle or en/disable the mod
422 toset = not mod.enabled
424 if mod.enabled ~= toset then
426 toggled_mods[#toggled_mods+1] = mod.name
429 -- Mark this mod for recursive dependency traversal
430 enabled_mods[mod.name] = true
433 -- Toggle or en/disable every mod in the modpack,
434 -- interleaved unsupported
436 if list[i].modpack == mod.name then
438 toset = not list[i].enabled
440 if list[i].enabled ~= toset then
441 list[i].enabled = toset
442 toggled_mods[#toggled_mods+1] = list[i].name
445 enabled_mods[list[i].name] = true
451 -- Mod(s) were disabled, so no dependencies need to be enabled
452 table.sort(toggled_mods)
453 minetest.log("info", "Following mods were disabled: " ..
454 table.concat(toggled_mods, ", "))
458 -- Enable mods' depends after activation
460 -- Make a list of mod ids indexed by their names
462 for id, mod2 in pairs(list) do
463 if mod2.type == "mod" and not mod2.is_modpack then
464 mod_ids[mod2.name] = id
468 -- to_enable is used as a DFS stack with sp as stack pointer
471 for name in pairs(enabled_mods) do
472 local depends = pkgmgr.get_dependencies(list[mod_ids[name]].path)
473 for i = 1, #depends do
474 local dependency_name = depends[i]
475 if not enabled_mods[dependency_name] then
477 to_enable[sp] = dependency_name
481 -- If sp is 0, every dependency is already activated
483 local name = to_enable[sp]
486 if not enabled_mods[name] then
487 enabled_mods[name] = true
488 local mod_to_enable = list[mod_ids[name]]
489 if not mod_to_enable then
490 minetest.log("warning", "Mod dependency \"" .. name ..
493 if mod_to_enable.enabled == false then
494 mod_to_enable.enabled = true
495 toggled_mods[#toggled_mods+1] = mod_to_enable.name
497 -- Push the dependencies of the dependency onto the stack
498 local depends = pkgmgr.get_dependencies(mod_to_enable.path)
499 for i = 1, #depends do
500 if not enabled_mods[name] then
502 to_enable[sp] = depends[i]
509 -- Log the list of enabled mods
510 table.sort(toggled_mods)
511 minetest.log("info", "Following mods were enabled: " ..
512 table.concat(toggled_mods, ", "))
515 --------------------------------------------------------------------------------
516 function pkgmgr.get_worldconfig(worldpath)
517 local filename = worldpath ..
518 DIR_DELIM .. "world.mt"
520 local worldfile = Settings(filename)
522 local worldconfig = {}
523 worldconfig.global_mods = {}
524 worldconfig.game_mods = {}
526 for key,value in pairs(worldfile:to_table()) do
527 if key == "gameid" then
528 worldconfig.id = value
529 elseif key:sub(0, 9) == "load_mod_" then
530 -- Compatibility: Check against "nil" which was erroneously used
531 -- as value for fresh configured worlds
532 worldconfig.global_mods[key] = value ~= "false" and value ~= "nil"
535 worldconfig[key] = value
540 local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
541 pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
546 --------------------------------------------------------------------------------
547 function pkgmgr.install_dir(type, path, basename, targetpath)
548 local basefolder = pkgmgr.get_base_folder(path)
550 -- There's no good way to detect a texture pack, so let's just assume
551 -- it's correct for now.
552 if type == "txp" then
553 if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
554 return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
557 local from = basefolder and basefolder.path or path
559 core.delete_dir(targetpath)
560 core.create_dir(targetpath)
562 targetpath = core.get_texturepath() .. DIR_DELIM .. basename
564 if not core.copy_dir(from, targetpath) then
566 fgettext("Failed to install $1 to $2", basename, targetpath)
568 return targetpath, nil
570 elseif not basefolder then
571 return nil, fgettext("Unable to find a valid mod or modpack")
577 if basefolder.type == "modpack" then
578 if type ~= "mod" then
579 return nil, fgettext("Unable to install a modpack as a $1", type)
582 -- Get destination name for modpack
584 core.delete_dir(targetpath)
585 core.create_dir(targetpath)
587 local clean_path = nil
588 if basename ~= nil then
589 clean_path = basename
591 if not clean_path then
592 clean_path = get_last_folder(cleanup_path(basefolder.path))
595 targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
598 fgettext("Install Mod: Unable to find suitable folder name for modpack $1",
602 elseif basefolder.type == "mod" then
603 if type ~= "mod" then
604 return nil, fgettext("Unable to install a mod as a $1", type)
608 core.delete_dir(targetpath)
609 core.create_dir(targetpath)
611 local targetfolder = basename
612 if targetfolder == nil then
613 targetfolder = pkgmgr.identify_modname(basefolder.path, "init.lua")
616 -- If heuristic failed try to use current foldername
617 if targetfolder == nil then
618 targetfolder = get_last_folder(basefolder.path)
621 if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
622 targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
624 return nil, fgettext("Install Mod: Unable to find real mod name for: $1", path)
628 elseif basefolder.type == "game" then
629 if type ~= "game" then
630 return nil, fgettext("Unable to install a game as a $1", type)
634 core.delete_dir(targetpath)
635 core.create_dir(targetpath)
637 targetpath = core.get_gamepath() .. DIR_DELIM .. basename
642 if not core.copy_dir(basefolder.path, targetpath) then
644 fgettext("Failed to install $1 to $2", basename, targetpath)
647 if basefolder.type == "game" then
648 pkgmgr.update_gamelist()
650 pkgmgr.refresh_globals()
653 return targetpath, nil
656 --------------------------------------------------------------------------------
657 function pkgmgr.install(type, modfilename, basename, dest)
658 local archive_info = pkgmgr.identify_filetype(modfilename)
659 local path = pkgmgr.extract(archive_info)
663 fgettext("Install: file: \"$1\"", archive_info.name) .. "\n" ..
664 fgettext("Install: Unsupported file type \"$1\" or broken archive",
668 local targetpath, msg = pkgmgr.install_dir(type, path, basename, dest)
669 core.delete_dir(path)
670 return targetpath, msg
673 --------------------------------------------------------------------------------
674 function pkgmgr.preparemodlist(data)
677 local global_mods = {}
681 local modpath = core.get_modpath()
683 if modpath ~= nil and
685 get_mods(modpath,global_mods)
688 for i=1,#global_mods,1 do
689 global_mods[i].type = "mod"
690 global_mods[i].loc = "global"
691 retval[#retval + 1] = global_mods[i]
695 local gamespec = pkgmgr.find_by_gameid(data.gameid)
696 pkgmgr.get_game_mods(gamespec, game_mods)
698 if #game_mods > 0 then
700 retval[#retval + 1] = {
702 is_game_content = true,
703 name = fgettext("$1 mods", gamespec.name),
708 for i=1,#game_mods,1 do
709 game_mods[i].type = "mod"
710 game_mods[i].loc = "game"
711 game_mods[i].is_game_content = true
712 retval[#retval + 1] = game_mods[i]
715 if data.worldpath == nil then
719 --read world mod configuration
720 local filename = data.worldpath ..
721 DIR_DELIM .. "world.mt"
723 local worldfile = Settings(filename)
725 for key,value in pairs(worldfile:to_table()) do
726 if key:sub(1, 9) == "load_mod_" then
730 if retval[i].name == key and
731 not retval[i].is_modpack then
736 if element ~= nil then
737 element.enabled = value ~= "false" and value ~= "nil" and value
739 core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
747 function pkgmgr.compare_package(a, b)
748 return a and b and a.name == b.name and a.path == b.path
751 --------------------------------------------------------------------------------
752 function pkgmgr.comparemod(elem1,elem2)
753 if elem1 == nil or elem2 == nil then
756 if elem1.name ~= elem2.name then
759 if elem1.is_modpack ~= elem2.is_modpack then
762 if elem1.type ~= elem2.type then
765 if elem1.modpack ~= elem2.modpack then
769 if elem1.path ~= elem2.path then
776 --------------------------------------------------------------------------------
777 function pkgmgr.mod_exists(basename)
779 if pkgmgr.global_mods == nil then
780 pkgmgr.refresh_globals()
783 if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then
790 --------------------------------------------------------------------------------
791 function pkgmgr.get_global_mod(idx)
793 if pkgmgr.global_mods == nil then
797 if idx == nil or idx < 1 or
798 idx > pkgmgr.global_mods:size() then
802 return pkgmgr.global_mods:get_list()[idx]
805 --------------------------------------------------------------------------------
806 function pkgmgr.refresh_globals()
807 local function is_equal(element,uid) --uid match
808 if element.name == uid then
812 pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
813 pkgmgr.comparemod, is_equal, nil, {})
814 pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
815 pkgmgr.global_mods:set_sortmode("alphabetic")
818 --------------------------------------------------------------------------------
819 function pkgmgr.identify_filetype(name)
821 if name:sub(-3):lower() == "zip" then
828 if name:sub(-6):lower() == "tar.gz" or
829 name:sub(-3):lower() == "tgz"then
836 if name:sub(-6):lower() == "tar.bz2" then
843 if name:sub(-2):lower() == "7z" then
857 --------------------------------------------------------------------------------
858 function pkgmgr.find_by_gameid(gameid)
859 for i=1,#pkgmgr.games,1 do
860 if pkgmgr.games[i].id == gameid then
861 return pkgmgr.games[i], i
867 --------------------------------------------------------------------------------
868 function pkgmgr.get_game_mods(gamespec, retval)
869 if gamespec ~= nil and
870 gamespec.gamemods_path ~= nil and
871 gamespec.gamemods_path ~= "" then
872 get_mods(gamespec.gamemods_path, retval)
876 --------------------------------------------------------------------------------
877 function pkgmgr.get_game_modlist(gamespec)
880 pkgmgr.get_game_mods(gamespec, game_mods)
881 for i=1,#game_mods,1 do
885 retval = retval .. game_mods[i].name
890 --------------------------------------------------------------------------------
891 function pkgmgr.get_game(index)
892 if index > 0 and index <= #pkgmgr.games then
893 return pkgmgr.games[index]
899 --------------------------------------------------------------------------------
900 function pkgmgr.update_gamelist()
901 pkgmgr.games = core.get_games()
904 --------------------------------------------------------------------------------
905 function pkgmgr.gamelist()
907 if #pkgmgr.games > 0 then
908 retval = retval .. core.formspec_escape(pkgmgr.games[1].name)
910 for i=2,#pkgmgr.games,1 do
911 retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].name)
917 --------------------------------------------------------------------------------
919 --------------------------------------------------------------------------------
920 pkgmgr.update_gamelist()