pkgmgr.lua 19 KB

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