Add online content repository

Replaces mods and texture pack tabs with a single content tab
This commit is contained in:
rubenwardy 2018-04-17 14:54:50 +01:00 committed by luk3yx
parent fd38beb656
commit ebfd6a61d3
45 changed files with 1928 additions and 787 deletions

View File

@ -126,6 +126,9 @@ LOCAL_SRC_FILES := \
jni/src/content_mapnode.cpp \
jni/src/content_nodemeta.cpp \
jni/src/content_sao.cpp \
jni/src/content/contentdb.cpp \
jni/src/content/mods.cpp \
jni/src/content/subgames.cpp \
jni/src/convert_json.cpp \
jni/src/craftdef.cpp \
jni/src/database-dummy.cpp \
@ -190,7 +193,6 @@ LOCAL_SRC_FILES := \
jni/src/mg_ore.cpp \
jni/src/mg_schematic.cpp \
jni/src/minimap.cpp \
jni/src/mods.cpp \
jni/src/nameidmapping.cpp \
jni/src/nodedef.cpp \
jni/src/nodemetadata.cpp \
@ -221,7 +223,6 @@ LOCAL_SRC_FILES := \
jni/src/sound.cpp \
jni/src/sound_openal.cpp \
jni/src/staticobject.cpp \
jni/src/subgame.cpp \
jni/src/tileanimation.cpp \
jni/src/tool.cpp \
jni/src/treegen.cpp \

View File

@ -288,11 +288,11 @@ function sort_mod_list(self)
table.sort(self.m_processed_list, function(a, b)
-- Show game mods at bottom
if a.typ ~= b.typ then
if b.typ == "game" then
return a.typ ~= "game_mod"
if a.type ~= b.type or a.loc ~= b.loc then
if b.type == "game" then
return a.loc ~= "game"
end
return b.typ == "game_mod"
return b.loc == "game"
end
-- If in same or no modpack, sort by name
if a.modpack == b.modpack then

View File

@ -545,6 +545,16 @@ function table.copy(t, seen)
end
return n
end
function table.insert_all(t, other)
for i=1, #other do
t[#t + 1] = other[i]
end
return t
end
--------------------------------------------------------------------------------
-- mainmenu only functions
--------------------------------------------------------------------------------

View File

@ -41,7 +41,7 @@ local function render_client_count(n)
end
local function configure_selected_world_params(idx)
local worldconfig = modmgr.get_worldconfig(menudata.worldlist:get_list()[idx].path)
local worldconfig = pkgmgr.get_worldconfig(menudata.worldlist:get_list()[idx].path)
if worldconfig.creative_mode then
core.settings:set("creative_mode", worldconfig.creative_mode)
end

View File

@ -35,7 +35,7 @@ local function get_formspec(data)
mod = {name=""}
end
local hard_deps, soft_deps = modmgr.get_dependencies(mod.path)
local hard_deps, soft_deps = pkgmgr.get_dependencies(mod.path)
retval = retval ..
"label[0,0.7;" .. fgettext("Mod:") .. "]" ..
@ -88,7 +88,7 @@ local function get_formspec(data)
retval = retval ..
"tablecolumns[color;tree;text]" ..
"table[5.5,0.75;5.75,6;world_config_modlist;"
retval = retval .. modmgr.render_modlist(data.list)
retval = retval .. pkgmgr.render_packagelist(data.list)
retval = retval .. ";" .. data.selected_mod .."]"
return retval
@ -239,7 +239,7 @@ function create_configure_world_dlg(worldidx)
dlg.data.worldspec = core.get_worlds()[worldidx]
if dlg.data.worldspec == nil then dlg:delete() return nil end
dlg.data.worldconfig = modmgr.get_worldconfig(dlg.data.worldspec.path)
dlg.data.worldconfig = pkgmgr.get_worldconfig(dlg.data.worldspec.path)
if dlg.data.worldconfig == nil or dlg.data.worldconfig.id == nil or
dlg.data.worldconfig.id == "" then
@ -249,8 +249,8 @@ function create_configure_world_dlg(worldidx)
end
dlg.data.list = filterlist.create(
modmgr.preparemodlist, --refresh
modmgr.comparemod, --compare
pkgmgr.preparemodlist, --refresh
pkgmgr.comparemod, --compare
function(element,uid) --uid match
if element.name == uid then
return true

View File

@ -0,0 +1,451 @@
--Minetest
--Copyright (C) 2018 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local function download_package(param)
if core.download_file(param.package.url, param.filename) then
return {
package = param.package,
filename = param.filename,
successful = true,
}
else
core.log("error", "downloading " .. dump(param.package.url) .. " failed")
return {
package = param.package,
successful = false,
}
end
end
local function start_install(calling_dialog, package)
local params = {
package = package,
filename = os.tempfolder() .. "_MODNAME_" .. package.name .. ".zip",
}
local function callback(result)
if result.successful then
local path, msg = pkgmgr.install(result.package.type, result.filename, result.package.name)
if not path then
gamedata.errormessage = msg
else
local conf_path
local name_is_title = false
if result.package.type == "mod" then
conf_path = path .. DIR_DELIM .. "mod.conf"
elseif result.package.type == "game" then
conf_path = path .. DIR_DELIM .. "game.conf"
name_is_title = true
elseif result.package.type == "txp" then
conf_path = path .. DIR_DELIM .. "texture_pack.conf"
end
if conf_path then
local conf = Settings(conf_path)
local function set_def(key, value)
if conf:get(key) == nil then
conf:set(key, value)
end
end
if name_is_title then
set_def("name", result.package.title)
else
set_def("title", result.package.title)
set_def("name", result.package.name)
end
set_def("description", result.package.short_description)
set_def("author", result.package.author)
conf:write()
end
end
os.remove(result.filename)
else
gamedata.errormessage = fgettext("Failed to download $1", package.name)
end
if gamedata.errormessage == nil then
core.button_handler({btn_hidden_close_download=result})
else
core.button_handler({btn_hidden_close_download={successful=false}})
end
end
if not core.handle_async(download_package, params, callback) then
minetest.log("error", "ERROR: async event failed")
gamedata.errormessage = fgettext("Failed to download $1", package.name)
end
local new_dlg = dialog_create("store_downloading",
function(data)
return "size[7,2]label[0.25,0.75;" ..
fgettext("Downloading and installing $1, please wait...", data.title) .. "]"
end,
function(this,fields)
if fields["btn_hidden_close_download"] ~= nil then
this:delete()
return true
end
return false
end,
nil)
new_dlg:set_parent(calling_dialog)
new_dlg.data.title = package.title
calling_dialog:hide()
new_dlg:show()
end
local package_dialog = {}
function package_dialog.get_formspec()
local package = package_dialog.package
local formspec = {
"size[8,4;true]",
"label[2.5,0.2;", core.formspec_escape(package.title), "]",
"label[0,1;", core.formspec_escape(package.short_description), "]",
"button[0,0;2,1;back;", fgettext("Back"), "]",
"button[6,0;2,1;install;", fgettext("Install"), "]",
}
-- TODO: screenshots
return table.concat(formspec, "")
end
function package_dialog.handle_submit(this, fields, tabname, tabdata)
if fields.back then
this:delete()
return true
end
if fields.install then
start_install(package_dialog.package)
return true
end
return false
end
function package_dialog.create(package)
package_dialog.package = package
return dialog_create("package_view",
package_dialog.get_formspec,
package_dialog.handle_submit,
nil)
end
local store = {}
local search_string = ""
local cur_page = 1
local num_per_page = 5
local filter_type = 1
local filter_types_titles = {
fgettext("All packages"),
fgettext("Games"),
fgettext("Mods"),
fgettext("Texture packs"),
}
local filter_types_type = {
nil,
"game",
"mod",
"txp",
}
function store.load()
store.packages_full = core.get_package_list()
store.packages = store.packages_full
store.loaded = true
end
function store.update_paths()
local mod_hash = {}
pkgmgr.refresh_globals()
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
mod_hash[mod.name] = mod
end
local game_hash = {}
pkgmgr.update_gamelist()
for _, game in pairs(pkgmgr.games) do
game_hash[game.id] = game
end
local txp_hash = {}
for _, txp in pairs(pkgmgr.get_texture_packs()) do
txp_hash[txp.name] = txp
end
for _, package in pairs(store.packages_full) do
local content
if package.type == "mod" then
content = mod_hash[package.name]
elseif package.type == "game" then
content = game_hash[package.name]
elseif package.type == "txp" then
content = txp_hash[package.name]
end
if content and content.author == package.author then
package.path = content.path
else
package.path = nil
end
end
end
function store.filter_packages(query)
if query == "" and filter_type == 1 then
store.packages = store.packages_full
return
end
local keywords = {}
for word in query:lower():gmatch("%S+") do
table.insert(keywords, word)
end
local function matches_keywords(package, keywords)
for k = 1, #keywords do
local keyword = keywords[k]
if string.find(package.name:lower(), keyword, 1, true) or
string.find(package.title:lower(), keyword, 1, true) or
string.find(package.author:lower(), keyword, 1, true) or
string.find(package.short_description:lower(), keyword, 1, true) then
return true
end
end
return false
end
store.packages = {}
for _, package in pairs(store.packages_full) do
if (query == "" or matches_keywords(package, keywords)) and
(filter_type == 1 or package.type == filter_types_type[filter_type]) then
store.packages[#store.packages + 1] = package
end
end
end
function store.get_formspec()
assert(store.loaded)
store.update_paths()
local pages = math.ceil(#store.packages / num_per_page)
if cur_page > pages then
cur_page = 1
end
local formspec = {
"size[12,6.5;true]",
"field[0.3,0.1;10.2,1;search_string;;", core.formspec_escape(search_string), "]",
"field_close_on_enter[search_string;false]",
"button[10.2,-0.2;2,1;search;", fgettext("Search"), "]",
"dropdown[0,1;2.4;type;",
table.concat(filter_types_titles, ","),
";",
filter_type,
"]",
-- "textlist[0,1;2.4,5.6;a;",
-- table.concat(taglist, ","),
-- "]"
}
local start_idx = (cur_page - 1) * num_per_page + 1
for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
local package = store.packages[i]
formspec[#formspec + 1] = "container[3,"
formspec[#formspec + 1] = i - start_idx + 1
formspec[#formspec + 1] = "]"
-- image
formspec[#formspec + 1] = "image[-0.4,0;1.5,1;"
formspec[#formspec + 1] = defaulttexturedir
formspec[#formspec + 1] = "no_screenshot.png"
formspec[#formspec + 1] = "]"
-- title
formspec[#formspec + 1] = "label[1,0;"
formspec[#formspec + 1] = core.formspec_escape(package.title ..
" by " .. package.author)
formspec[#formspec + 1] = "]"
-- description
local short = package.short_description
if #short > 60 then
short = short:sub(1, 59) .. ""
end
formspec[#formspec + 1] = "label[1,0.3;"
formspec[#formspec + 1] = core.formspec_escape(short)
formspec[#formspec + 1] = "]"
-- buttons
if package.path then
formspec[#formspec + 1] = "button[6,0;1.5,1;uninstall_"
formspec[#formspec + 1] = tostring(i)
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = fgettext("Uninstall")
formspec[#formspec + 1] = "]"
else
formspec[#formspec + 1] = "button[6,0;1.5,1;install_"
formspec[#formspec + 1] = tostring(i)
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = fgettext("Install")
formspec[#formspec + 1] = "]"
end
formspec[#formspec + 1] = "button[7.5,0;1.5,1;view_"
formspec[#formspec + 1] = tostring(i)
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = fgettext("View")
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "container_end[]"
end
formspec[#formspec + 1] = "container[0,"
formspec[#formspec + 1] = num_per_page + 1
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "button[2.6,0;3,1;back;"
formspec[#formspec + 1] = fgettext("Back to Main Menu")
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "button[7,0;1,1;pstart;<<]"
formspec[#formspec + 1] = "button[8,0;1,1;pback;<]"
formspec[#formspec + 1] = "label[9.2,0.2;"
formspec[#formspec + 1] = tonumber(cur_page)
formspec[#formspec + 1] = " / "
formspec[#formspec + 1] = tonumber(pages)
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "button[10,0;1,1;pnext;>]"
formspec[#formspec + 1] = "button[11,0;1,1;pend;>>]"
formspec[#formspec + 1] = "container_end[]"
formspec[#formspec + 1] = "]"
return table.concat(formspec, "")
end
function store.handle_submit(this, fields, tabname, tabdata)
if fields.search or fields.key_enter_field == "search_string" then
search_string = fields.search_string:trim()
cur_page = 1
store.filter_packages(search_string)
core.update_formspec(store.get_formspec())
return true
end
if fields.back then
this:delete()
return true
end
if fields.pstart then
cur_page = 1
core.update_formspec(store.get_formspec())
return true
end
if fields.pend then
cur_page = math.ceil(#store.packages / num_per_page)
core.update_formspec(store.get_formspec())
return true
end
if fields.pnext then
cur_page = cur_page + 1
local pages = math.ceil(#store.packages / num_per_page)
if cur_page > pages then
cur_page = 1
end
core.update_formspec(store.get_formspec())
return true
end
if fields.pback then
if cur_page == 1 then
local pages = math.ceil(#store.packages / num_per_page)
cur_page = pages
else
cur_page = cur_page - 1
end
core.update_formspec(store.get_formspec())
return true
end
if fields.type then
local new_type = table.indexof(filter_types_titles, fields.type)
if new_type ~= filter_type then
filter_type = new_type
store.filter_packages(search_string)
return true
end
end
local start_idx = (cur_page - 1) * num_per_page + 1
assert(start_idx ~= nil)
for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do
local package = store.packages[i]
assert(package)
if fields["install_" .. i] then
start_install(this, package)
return true
end
if fields["uninstall_" .. i] then
local dlg_delmod = create_delete_content_dlg(package)
dlg_delmod:set_parent(this)
this:hide()
dlg_delmod:show()
return true
end
if fields["view_" .. i] then
local dlg = package_dialog.create(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
end
return false
end
function create_store_dlg(type)
if not store.loaded then
store.load()
end
search_string = ""
cur_page = 1
store.filter_packages(search_string)
return dialog_create("store",
store.get_formspec,
store.handle_submit,
nil)
end

View File

@ -20,6 +20,35 @@ local function create_world_formspec(dialogdata)
local current_seed = core.settings:get("fixed_map_seed") or ""
local current_mg = core.settings:get("mg_name")
local gameid = core.settings:get("menu_last_game")
local game, gameidx = nil , 0
if gameid ~= nil then
game, gameidx = pkgmgr.find_by_gameid(gameid)
if gameidx == nil then
gameidx = 0
end
end
local game_by_gameidx = core.get_game(gameidx)
if game_by_gameidx ~= nil then
local gamepath = game_by_gameidx.path
local gameconfig = Settings(gamepath.."/game.conf")
local disallowed_mapgens = (gameconfig:get("disallowed_mapgens") or ""):split()
for key, value in pairs(disallowed_mapgens) do
disallowed_mapgens[key] = value:trim()
end
if disallowed_mapgens then
for i = #mapgens, 1, -1 do
if table.indexof(disallowed_mapgens, mapgens[i]) > 0 then
table.remove(mapgens, i)
end
end
end
end
local mglist = ""
local selindex = 1
@ -57,17 +86,17 @@ local function create_world_formspec(dialogdata)
"dropdown[4.2,2;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]" ..
"label[2,3;" .. fgettext("Game") .. "]"..
"textlist[4.2,3;5.8,2.3;games;" .. gamemgr.gamelist() ..
"textlist[4.2,3;5.8,2.3;games;" .. pkgmgr.gamelist() ..
";" .. gameidx .. ";true]" ..
"button[3.25,6;2.5,0.5;world_create_confirm;" .. fgettext("Create") .. "]" ..
"button[5.75,6;2.5,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]"
if #gamemgr.games == 0 then
if #pkgmgr.games == 0 then
retval = retval .. "box[2,4;8,1;#ff8800]label[2.25,4;" ..
fgettext("You have no subgames installed.") .. "]label[2.25,4.4;" ..
fgettext("Download one from minetest.net") .. "]"
elseif #gamemgr.games == 1 and gamemgr.games[1].id == "minimal" then
elseif #pkgmgr.games == 1 and pkgmgr.games[1].id == "minimal" then
retval = retval .. "box[1.75,4;8.7,1;#ff8800]label[2,4;" ..
fgettext("Warning: The minimal development test is meant for developers.") .. "]label[2,4.4;" ..
fgettext("Download a subgame, such as minetest_game, from minetest.net") .. "]"
@ -102,10 +131,10 @@ local function create_world_buttonhandler(this, fields)
if message ~= nil then
gamedata.errormessage = message
else
core.settings:set("menu_last_game",gamemgr.games[gameindex].id)
core.settings:set("menu_last_game",pkgmgr.games[gameindex].id)
if this.data.update_worldlist_filter then
menudata.worldlist:set_filtercriteria(gamemgr.games[gameindex].id)
mm_texture.update("singleplayer", gamemgr.games[gameindex].id)
menudata.worldlist:set_filtercriteria(pkgmgr.games[gameindex].id)
mm_texture.update("singleplayer", pkgmgr.games[gameindex].id)
end
menudata.worldlist:refresh()
core.settings:set("mainmenu_last_selected_world",
@ -120,6 +149,8 @@ local function create_world_buttonhandler(this, fields)
end
if fields["games"] then
local gameindex = core.get_textlist_index("games")
core.settings:set("menu_last_game", pkgmgr.games[gameindex].id)
return true
end

View File

@ -17,39 +17,38 @@
--------------------------------------------------------------------------------
local function delete_mod_formspec(dialogdata)
dialogdata.mod = modmgr.global_mods:get_list()[dialogdata.selected]
local function delete_content_formspec(dialogdata)
local retval =
"size[11.5,4.5,true]" ..
"label[2,2;" ..
fgettext("Are you sure you want to delete \"$1\"?", dialogdata.mod.name) .. "]"..
"button[3.25,3.5;2.5,0.5;dlg_delete_mod_confirm;" .. fgettext("Delete") .. "]" ..
"button[5.75,3.5;2.5,0.5;dlg_delete_mod_cancel;" .. fgettext("Cancel") .. "]"
fgettext("Are you sure you want to delete \"$1\"?", dialogdata.content.name) .. "]"..
"button[3.25,3.5;2.5,0.5;dlg_delete_content_confirm;" .. fgettext("Delete") .. "]" ..
"button[5.75,3.5;2.5,0.5;dlg_delete_content_cancel;" .. fgettext("Cancel") .. "]"
return retval
end
--------------------------------------------------------------------------------
local function delete_mod_buttonhandler(this, fields)
if fields["dlg_delete_mod_confirm"] ~= nil then
local function delete_content_buttonhandler(this, fields)
if fields["dlg_delete_content_confirm"] ~= nil then
if this.data.mod.path ~= nil and
this.data.mod.path ~= "" and
this.data.mod.path ~= core.get_modpath() then
if not core.delete_dir(this.data.mod.path) then
gamedata.errormessage = fgettext("Modmgr: failed to delete \"$1\"", this.data.mod.path)
if this.data.content.path ~= nil and
this.data.content.path ~= "" and
this.data.content.path ~= core.get_modpath() and
this.data.content.path ~= core.get_gamepath() and
this.data.content.path ~= core.get_texturepath() then
if not core.delete_dir(this.data.content.path) then
gamedata.errormessage = fgettext("pkgmgr: failed to delete \"$1\"", this.data.content.path)
end
modmgr.refresh_globals()
pkgmgr.refresh_globals()
else
gamedata.errormessage = fgettext("Modmgr: invalid modpath \"$1\"", this.data.mod.path)
gamedata.errormessage = fgettext("pkgmgr: invalid path \"$1\"", this.data.content.path)
end
this:delete()
return true
end
if fields["dlg_delete_mod_cancel"] then
if fields["dlg_delete_content_cancel"] then
this:delete()
return true
end
@ -58,12 +57,13 @@ local function delete_mod_buttonhandler(this, fields)
end
--------------------------------------------------------------------------------
function create_delete_mod_dlg(selected_index)
function create_delete_content_dlg(content)
assert(content.name)
local retval = dialog_create("dlg_delete_mod",
delete_mod_formspec,
delete_mod_buttonhandler,
local retval = dialog_create("dlg_delete_content",
delete_content_formspec,
delete_content_buttonhandler,
nil)
retval.data.selected = selected_index
retval.data.content = content
return retval
end

View File

@ -19,7 +19,7 @@
local function rename_modpack_formspec(dialogdata)
dialogdata.mod = modmgr.global_mods:get_list()[dialogdata.selected]
dialogdata.mod = pkgmgr.global_mods:get_list()[dialogdata.selected]
local retval =
"size[11.5,4.5,true]" ..
@ -39,9 +39,9 @@ local function rename_modpack_buttonhandler(this, fields)
local oldpath = core.get_modpath() .. DIR_DELIM .. this.data.mod.name
local targetpath = core.get_modpath() .. DIR_DELIM .. fields["te_modpack_name"]
core.copy_dir(oldpath,targetpath,false)
modmgr.refresh_globals()
modmgr.selected_mod = modmgr.global_mods:get_current_index(
modmgr.global_mods:raw_index_by_uid(fields["te_modpack_name"]))
pkgmgr.refresh_globals()
pkgmgr.selected_mod = pkgmgr.global_mods:get_current_index(
pkgmgr.global_mods:raw_index_by_uid(fields["te_modpack_name"]))
this:delete()
return true

View File

@ -280,7 +280,7 @@ local function parse_config_file(read_all, parse_mods)
-- Parse games
local games_category_initialized = false
local index = 1
local game = gamemgr.get_game(index)
local game = pkgmgr.get_game(index)
while game do
local path = game.path .. DIR_DELIM .. FILENAME
local file = io.open(path, "r")
@ -307,7 +307,7 @@ local function parse_config_file(read_all, parse_mods)
end
index = index + 1
game = gamemgr.get_game(index)
game = pkgmgr.get_game(index)
end
-- Parse mods

View File

@ -1,83 +0,0 @@
--Minetest
--Copyright (C) 2013 sapier
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
gamemgr = {}
--------------------------------------------------------------------------------
function gamemgr.find_by_gameid(gameid)
for i=1,#gamemgr.games,1 do
if gamemgr.games[i].id == gameid then
return gamemgr.games[i], i
end
end
return nil, nil
end
--------------------------------------------------------------------------------
function gamemgr.get_game_mods(gamespec, retval)
if gamespec ~= nil and
gamespec.gamemods_path ~= nil and
gamespec.gamemods_path ~= "" then
get_mods(gamespec.gamemods_path, retval)
end
end
--------------------------------------------------------------------------------
function gamemgr.get_game_modlist(gamespec)
local retval = ""
local game_mods = {}
gamemgr.get_game_mods(gamespec, game_mods)
for i=1,#game_mods,1 do
if retval ~= "" then
retval = retval..","
end
retval = retval .. game_mods[i].name
end
return retval
end
--------------------------------------------------------------------------------
function gamemgr.get_game(index)
if index > 0 and index <= #gamemgr.games then
return gamemgr.games[index]
end
return nil
end
--------------------------------------------------------------------------------
function gamemgr.update_gamelist()
gamemgr.games = core.get_games()
end
--------------------------------------------------------------------------------
function gamemgr.gamelist()
local retval = ""
if #gamemgr.games > 0 then
retval = retval .. core.formspec_escape(gamemgr.games[1].name)
for i=2,#gamemgr.games,1 do
retval = retval .. "," .. core.formspec_escape(gamemgr.games[i].name)
end
end
return retval
end
--------------------------------------------------------------------------------
-- read initial data
--------------------------------------------------------------------------------
gamemgr.update_gamelist()

View File

@ -34,15 +34,15 @@ dofile(basepath .. DIR_DELIM .. "fstk" .. DIR_DELIM .. "dialog.lua")
dofile(basepath .. DIR_DELIM .. "fstk" .. DIR_DELIM .. "tabview.lua")
dofile(basepath .. DIR_DELIM .. "fstk" .. DIR_DELIM .. "ui.lua")
dofile(menupath .. DIR_DELIM .. "common.lua")
dofile(menupath .. DIR_DELIM .. "gamemgr.lua")
dofile(menupath .. DIR_DELIM .. "modmgr.lua")
dofile(menupath .. DIR_DELIM .. "pkgmgr.lua")
dofile(menupath .. DIR_DELIM .. "textures.lua")
dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua")
dofile(menupath .. DIR_DELIM .. "dlg_settings_advanced.lua")
dofile(menupath .. DIR_DELIM .. "dlg_contentstore.lua")
if PLATFORM ~= "Android" then
dofile(menupath .. DIR_DELIM .. "dlg_create_world.lua")
dofile(menupath .. DIR_DELIM .. "dlg_delete_mod.lua")
dofile(menupath .. DIR_DELIM .. "dlg_delete_content.lua")
dofile(menupath .. DIR_DELIM .. "dlg_delete_world.lua")
dofile(menupath .. DIR_DELIM .. "dlg_rename_modpack.lua")
end
@ -50,14 +50,13 @@ end
local tabs = {}
tabs.settings = dofile(menupath .. DIR_DELIM .. "tab_settings.lua")
tabs.mods = dofile(menupath .. DIR_DELIM .. "tab_mods.lua")
tabs.credits = dofile(menupath .. DIR_DELIM .. "tab_credits.lua")
tabs.content = dofile(menupath .. DIR_DELIM .. "tab_content.lua")
tabs.credits = dofile(menupath .. DIR_DELIM .. "tab_credits.lua")
if PLATFORM == "Android" then
tabs.simple_main = dofile(menupath .. DIR_DELIM .. "tab_simple_main.lua")
else
tabs.local_game = dofile(menupath .. DIR_DELIM .. "tab_local.lua")
tabs.play_online = dofile(menupath .. DIR_DELIM .. "tab_online.lua")
tabs.texturepacks = dofile(menupath .. DIR_DELIM .. "tab_texturepacks.lua")
end
--------------------------------------------------------------------------------
@ -130,16 +129,14 @@ local function init_globals()
if PLATFORM == "Android" then
tv_main:add(tabs.simple_main)
tv_main:add(tabs.settings)
else
tv_main:set_autosave_tab(true)
tv_main:add(tabs.local_game)
tv_main:add(tabs.play_online)
tv_main:add(tabs.settings)
tv_main:add(tabs.texturepacks)
end
tv_main:add(tabs.mods)
tv_main:add(tabs.content)
tv_main:add(tabs.settings)
tv_main:add(tabs.credits)
tv_main:set_global_event_handler(main_event_handler)

View File

@ -31,7 +31,9 @@ function get_mods(path,retval,modpack)
end
toadd.name = name
toadd.author = mod_conf.author
toadd.path = prefix
toadd.type = "mod"
if modpack ~= nil and modpack ~= "" then
toadd.modpack = modpack
@ -39,6 +41,7 @@ function get_mods(path,retval,modpack)
local modpackfile = io.open(prefix .. "modpack.txt")
if modpackfile then
modpackfile:close()
toadd.type = "modpack"
toadd.is_modpack = true
get_mods(prefix, retval, name)
end
@ -48,10 +51,46 @@ function get_mods(path,retval,modpack)
end
--modmanager implementation
modmgr = {}
pkgmgr = {}
function pkgmgr.get_texture_packs()
local txtpath = core.get_texturepath()
local list = core.get_dir_list(txtpath, true)
local retval = {}
local current_texture_path = core.settings:get("texture_path")
for _, item in ipairs(list) do
if item ~= "base" then
local name = item
local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
if path == current_texture_path then
name = fgettext("$1 (Enabled)", name)
end
local conf = Settings(path .. "texture_pack.conf")
retval[#retval + 1] = {
name = item,
author = conf:get("author"),
list_name = name,
type = "txp",
path = path,
enabled = path == current_texture_path,
}
end
end
table.sort(retval, function(a, b)
return a.name > b.name
end)
return retval
end
--------------------------------------------------------------------------------
function modmgr.extract(modfile)
function pkgmgr.extract(modfile)
if modfile.type == "zip" then
local tempfolder = os.tempfolder()
@ -66,72 +105,60 @@ function modmgr.extract(modfile)
return nil
end
function pkgmgr.get_folder_type(path)
local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
if testfile ~= nil then
testfile:close()
return { type = "mod", path = path }
end
testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
if testfile ~= nil then
testfile:close()
return { type = "modpack", path = path }
end
testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
if testfile ~= nil then
testfile:close()
return { type = "game", path = path }
end
testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
if testfile ~= nil then
testfile:close()
return { type = "txp", path = path }
end
return nil
end
-------------------------------------------------------------------------------
function modmgr.getbasefolder(temppath)
function pkgmgr.get_base_folder(temppath)
if temppath == nil then
return {
type = "invalid",
path = ""
}
return { type = "invalid", path = "" }
end
local testfile = io.open(temppath .. DIR_DELIM .. "init.lua","r")
if testfile ~= nil then
testfile:close()
return {
type="mod",
path=temppath
}
end
testfile = io.open(temppath .. DIR_DELIM .. "modpack.txt","r")
if testfile ~= nil then
testfile:close()
return {
type="modpack",
path=temppath
}
local ret = pkgmgr.get_folder_type(temppath)
if ret then
return ret
end
local subdirs = core.get_dir_list(temppath, true)
--only single mod or modpack allowed
if #subdirs ~= 1 then
return {
type = "invalid",
path = ""
}
if #subdirs == 1 then
ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
if ret then
return ret
else
return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
end
end
testfile =
io.open(temppath .. DIR_DELIM .. subdirs[1] ..DIR_DELIM .."init.lua","r")
if testfile ~= nil then
testfile:close()
return {
type="mod",
path= temppath .. DIR_DELIM .. subdirs[1]
}
end
testfile =
io.open(temppath .. DIR_DELIM .. subdirs[1] ..DIR_DELIM .."modpack.txt","r")
if testfile ~= nil then
testfile:close()
return {
type="modpack",
path=temppath .. DIR_DELIM .. subdirs[1]
}
end
return {
type = "invalid",
path = ""
}
return nil
end
--------------------------------------------------------------------------------
function modmgr.isValidModname(modpath)
function pkgmgr.isValidModname(modpath)
if modpath:find("-") ~= nil then
return false
end
@ -140,7 +167,7 @@ function modmgr.isValidModname(modpath)
end
--------------------------------------------------------------------------------
function modmgr.parse_register_line(line)
function pkgmgr.parse_register_line(line)
local pos1 = line:find("\"")
local pos2 = nil
if pos1 ~= nil then
@ -167,7 +194,7 @@ function modmgr.parse_register_line(line)
end
--------------------------------------------------------------------------------
function modmgr.parse_dofile_line(modpath,line)
function pkgmgr.parse_dofile_line(modpath,line)
local pos1 = line:find("\"")
local pos2 = nil
if pos1 ~= nil then
@ -180,14 +207,14 @@ function modmgr.parse_dofile_line(modpath,line)
if filename ~= nil and
filename ~= "" and
filename:find(".lua") then
return modmgr.identify_modname(modpath,filename)
return pkgmgr.identify_modname(modpath,filename)
end
end
return nil
end
--------------------------------------------------------------------------------
function modmgr.identify_modname(modpath,filename)
function pkgmgr.identify_modname(modpath,filename)
local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
if testfile ~= nil then
local line = testfile:read()
@ -196,20 +223,20 @@ function modmgr.identify_modname(modpath,filename)
local modname = nil
if line:find("minetest.register_tool") then
modname = modmgr.parse_register_line(line)
modname = pkgmgr.parse_register_line(line)
end
if line:find("minetest.register_craftitem") then
modname = modmgr.parse_register_line(line)
modname = pkgmgr.parse_register_line(line)
end
if line:find("minetest.register_node") then
modname = modmgr.parse_register_line(line)
modname = pkgmgr.parse_register_line(line)
end
if line:find("dofile") then
modname = modmgr.parse_dofile_line(modpath,line)
modname = pkgmgr.parse_dofile_line(modpath,line)
end
if modname ~= nil then
@ -225,14 +252,14 @@ function modmgr.identify_modname(modpath,filename)
return nil
end
--------------------------------------------------------------------------------
function modmgr.render_modlist(render_list)
function pkgmgr.render_packagelist(render_list)
local retval = ""
if render_list == nil then
if modmgr.global_mods == nil then
modmgr.refresh_globals()
if pkgmgr.global_mods == nil then
pkgmgr.refresh_globals()
end
render_list = modmgr.global_mods
render_list = pkgmgr.global_mods
end
local list = render_list:get_list()
@ -252,36 +279,36 @@ function modmgr.render_modlist(render_list)
break
end
end
elseif v.is_game_content then
elseif v.is_game_content or v.type == "game" then
color = mt_color_blue
elseif v.enabled then
elseif v.enabled or v.type == "txp" then
color = mt_color_green
end
retval[#retval + 1] = color
if v.modpack ~= nil or v.typ == "game_mod" then
if v.modpack ~= nil or v.loc == "game" then
retval[#retval + 1] = "1"
else
retval[#retval + 1] = "0"
end
retval[#retval + 1] = core.formspec_escape(v.name)
retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
end
return table.concat(retval, ",")
end
--------------------------------------------------------------------------------
function modmgr.get_dependencies(path)
function pkgmgr.get_dependencies(path)
if path == nil then
return "", ""
end
local info = core.get_mod_info(path)
return table.concat(info.depends, ","), table.concat(info.optional_depends, ",")
local info = core.get_content_info(path)
return table.concat(info.depends or {}, ","), table.concat(info.optional_depends or {}, ",")
end
--------------------------------------------------------------------------------
function modmgr.get_worldconfig(worldpath)
function pkgmgr.get_worldconfig(worldpath)
local filename = worldpath ..
DIR_DELIM .. "world.mt"
@ -302,26 +329,35 @@ function modmgr.get_worldconfig(worldpath)
end
--read gamemods
local gamespec = gamemgr.find_by_gameid(worldconfig.id)
gamemgr.get_game_mods(gamespec, worldconfig.game_mods)
local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
return worldconfig
end
--------------------------------------------------------------------------------
function modmgr.installmod(modfilename,basename)
local modfile = modmgr.identify_filetype(modfilename)
local modpath = modmgr.extract(modfile)
function pkgmgr.install_dir(type, path, basename)
local basefolder = pkgmgr.get_base_folder(path)
if modpath == nil then
gamedata.errormessage = fgettext("Install Mod: file: \"$1\"", modfile.name) ..
fgettext("\nInstall Mod: unsupported filetype \"$1\" or broken archive", modfile.type)
return
local targetpath
if type == "txp" then
if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
end
local from = basefolder and basefolder.path or path
targetpath = core.get_texturepath() .. DIR_DELIM .. basename
core.copy_dir(from, targetpath)
return targetpath, nil
elseif not basefolder then
return nil, fgettext("Unable to find a valid mod or modpack")
end
local basefolder = modmgr.getbasefolder(modpath)
if basefolder.type == "modpack" then
if type ~= "mod" then
return nil, fgettext("Unable to install a modpack as a $1", type)
end
local clean_path = nil
if basename ~= nil then
@ -333,20 +369,27 @@ function modmgr.installmod(modfilename,basename)
end
if clean_path ~= nil then
local targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
if not core.copy_dir(basefolder.path,targetpath) then
gamedata.errormessage = fgettext("Failed to install $1 to $2", basename, targetpath)
return nil,
fgettext("Failed to install $1 to $2", basename, targetpath)
end
else
gamedata.errormessage = fgettext("Install Mod: unable to find suitable foldername for modpack $1", modfilename)
return nil,
fgettext("Install Mod: unable to find suitable foldername for modpack $1",
modfilename)
end
end
if basefolder.type == "mod" then
pkgmgr.refresh_globals()
elseif basefolder.type == "mod" then
if type ~= "mod" then
return nil, fgettext("Unable to install a mod as a $1", type)
end
local targetfolder = basename
if targetfolder == nil then
targetfolder = modmgr.identify_modname(basefolder.path,"init.lua")
targetfolder = pkgmgr.identify_modname(basefolder.path,"init.lua")
end
--if heuristic failed try to use current foldername
@ -354,22 +397,46 @@ function modmgr.installmod(modfilename,basename)
targetfolder = get_last_folder(basefolder.path)
end
if targetfolder ~= nil and modmgr.isValidModname(targetfolder) then
local targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
core.copy_dir(basefolder.path,targetpath)
if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
core.copy_dir(basefolder.path, targetpath)
else
gamedata.errormessage = fgettext("Install Mod: unable to find real modname for: $1", modfilename)
return nil, fgettext("Install Mod: unable to find real modname for: $1", modfilename)
end
pkgmgr.refresh_globals()
elseif basefolder.type == "game" then
if type ~= "game" then
return nil, fgettext("Unable to install a game as a $1", type)
end
targetpath = core.get_gamepath() .. DIR_DELIM .. basename
core.copy_dir(basefolder.path, targetpath)
end
core.delete_dir(modpath)
modmgr.refresh_globals()
return targetpath, nil
end
--------------------------------------------------------------------------------
function modmgr.preparemodlist(data)
function pkgmgr.install(type, modfilename, basename)
local archive_info = pkgmgr.identify_filetype(modfilename)
local path = pkgmgr.extract(archive_info)
if path == nil then
return nil,
fgettext("Install: file: \"$1\"", archive_info.name) .. "\n" ..
fgettext("Install: unsupported filetype \"$1\" or broken archive",
archive_info.type)
end
local targetpath, msg = pkgmgr.install_dir(type, path, basename)
core.delete_dir(path)
return targetpath, msg
end
--------------------------------------------------------------------------------
function pkgmgr.preparemodlist(data)
local retval = {}
local global_mods = {}
@ -384,25 +451,27 @@ function modmgr.preparemodlist(data)
end
for i=1,#global_mods,1 do
global_mods[i].typ = "global_mod"
global_mods[i].type = "mod"
global_mods[i].loc = "global"
retval[#retval + 1] = global_mods[i]
end
--read game mods
local gamespec = gamemgr.find_by_gameid(data.gameid)
gamemgr.get_game_mods(gamespec, game_mods)
local gamespec = pkgmgr.find_by_gameid(data.gameid)
pkgmgr.get_game_mods(gamespec, game_mods)
if #game_mods > 0 then
-- Add title
retval[#retval + 1] = {
typ = "game",
type = "game",
is_game_content = true,
name = fgettext("Subgame Mods")
}
end
for i=1,#game_mods,1 do
game_mods[i].typ = "game_mod"
game_mods[i].type = "mod"
game_mods[i].loc = "game"
game_mods[i].is_game_content = true
retval[#retval + 1] = game_mods[i]
end
@ -439,8 +508,12 @@ function modmgr.preparemodlist(data)
return retval
end
function pkgmgr.compare_package(a, b)
return a and b and a.name == b.name and a.path == b.path
end
--------------------------------------------------------------------------------
function modmgr.comparemod(elem1,elem2)
function pkgmgr.comparemod(elem1,elem2)
if elem1 == nil or elem2 == nil then
return false
end
@ -450,7 +523,7 @@ function modmgr.comparemod(elem1,elem2)
if elem1.is_modpack ~= elem2.is_modpack then
return false
end
if elem1.typ ~= elem2.typ then
if elem1.type ~= elem2.type then
return false
end
if elem1.modpack ~= elem2.modpack then
@ -465,13 +538,13 @@ function modmgr.comparemod(elem1,elem2)
end
--------------------------------------------------------------------------------
function modmgr.mod_exists(basename)
function pkgmgr.mod_exists(basename)
if modmgr.global_mods == nil then
modmgr.refresh_globals()
if pkgmgr.global_mods == nil then
pkgmgr.refresh_globals()
end
if modmgr.global_mods:raw_index_by_uid(basename) > 0 then
if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then
return true
end
@ -479,39 +552,35 @@ function modmgr.mod_exists(basename)
end
--------------------------------------------------------------------------------
function modmgr.get_global_mod(idx)
function pkgmgr.get_global_mod(idx)
if modmgr.global_mods == nil then
if pkgmgr.global_mods == nil then
return nil
end
if idx == nil or idx < 1 or
idx > modmgr.global_mods:size() then
idx > pkgmgr.global_mods:size() then
return nil
end
return modmgr.global_mods:get_list()[idx]
return pkgmgr.global_mods:get_list()[idx]
end
--------------------------------------------------------------------------------
function modmgr.refresh_globals()
modmgr.global_mods = filterlist.create(
modmgr.preparemodlist, --refresh
modmgr.comparemod, --compare
function(element,uid) --uid match
if element.name == uid then
return true
end
end,
nil, --filter
{}
)
modmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
modmgr.global_mods:set_sortmode("alphabetic")
function pkgmgr.refresh_globals()
local function is_equal(element,uid) --uid match
if element.name == uid then
return true
end
end
pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
pkgmgr.comparemod, is_equal, nil, {})
pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
pkgmgr.global_mods:set_sortmode("alphabetic")
end
--------------------------------------------------------------------------------
function modmgr.identify_filetype(name)
function pkgmgr.identify_filetype(name)
if name:sub(-3):lower() == "zip" then
return {
@ -547,3 +616,69 @@ function modmgr.identify_filetype(name)
type = "ukn"
}
end
--------------------------------------------------------------------------------
function pkgmgr.find_by_gameid(gameid)
for i=1,#pkgmgr.games,1 do
if pkgmgr.games[i].id == gameid then
return pkgmgr.games[i], i
end
end
return nil, nil
end
--------------------------------------------------------------------------------
function pkgmgr.get_game_mods(gamespec, retval)
if gamespec ~= nil and
gamespec.gamemods_path ~= nil and
gamespec.gamemods_path ~= "" then
get_mods(gamespec.gamemods_path, retval)
end
end
--------------------------------------------------------------------------------
function pkgmgr.get_game_modlist(gamespec)
local retval = ""
local game_mods = {}
pkgmgr.get_game_mods(gamespec, game_mods)
for i=1,#game_mods,1 do
if retval ~= "" then
retval = retval..","
end
retval = retval .. game_mods[i].name
end
return retval
end
--------------------------------------------------------------------------------
function pkgmgr.get_game(index)
if index > 0 and index <= #pkgmgr.games then
return pkgmgr.games[index]
end
return nil
end
--------------------------------------------------------------------------------
function pkgmgr.update_gamelist()
pkgmgr.games = core.get_games()
end
--------------------------------------------------------------------------------
function pkgmgr.gamelist()
local retval = ""
if #pkgmgr.games > 0 then
retval = retval .. core.formspec_escape(pkgmgr.games[1].name)
for i=2,#pkgmgr.games,1 do
retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].name)
end
end
return retval
end
--------------------------------------------------------------------------------
-- read initial data
--------------------------------------------------------------------------------
pkgmgr.update_gamelist()

View File

@ -0,0 +1,217 @@
--Minetest
--Copyright (C) 2014 sapier
--Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local packages_raw
local packages
--------------------------------------------------------------------------------
local function get_formspec(tabview, name, tabdata)
if pkgmgr.global_mods == nil then
pkgmgr.refresh_globals()
end
if pkgmgr.games == nil then
pkgmgr.update_gamelist()
end
if packages == nil then
packages_raw = {}
table.insert_all(packages_raw, pkgmgr.games)
table.insert_all(packages_raw, pkgmgr.get_texture_packs())
table.insert_all(packages_raw, pkgmgr.global_mods:get_list())
local function get_data()
return packages_raw
end
local function is_equal(element, uid) --uid match
return (element.type == "game" and element.id == uid) or
element.name == uid
end
packages = filterlist.create(get_data, pkgmgr.compare_package,
is_equal, nil, {})
end
if tabdata.selected_pkg == nil then
tabdata.selected_pkg = 1
end
local retval =
"label[0.05,-0.25;".. fgettext("Installed Packages:") .. "]" ..
"tablecolumns[color;tree;text]" ..
"table[0,0.25;5.1,4.3;pkglist;" ..
pkgmgr.render_packagelist(packages) ..
";" .. tabdata.selected_pkg .. "]" ..
"button[0,4.85;5.25,0.5;btn_contentdb;".. fgettext("Browse online content") .. "]"
local selected_pkg
if filterlist.size(packages) >= tabdata.selected_pkg then
selected_pkg = packages:get_list()[tabdata.selected_pkg]
end
if selected_pkg ~= nil then
--check for screenshot beeing available
local screenshotfilename = selected_pkg.path .. DIR_DELIM .. "screenshot.png"
local screenshotfile, error = io.open(screenshotfilename, "r")
local modscreenshot
if error == nil then
screenshotfile:close()
modscreenshot = screenshotfilename
end
if modscreenshot == nil then
modscreenshot = defaulttexturedir .. "no_screenshot.png"
end
retval = retval ..
"image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" ..
"label[8.25,0.6;" .. core.formspec_escape(selected_pkg.name) .. "]" ..
"label[5.5,1.7;".. fgettext("Information:") .. "]" ..
"textlist[5.5,2.2;6.2,2.4;description;"
local info = core.get_content_info(selected_pkg.path)
local desc = info.description or fgettext("No package description available")
local descriptionlines = core.wrap_text(desc, 42, true)
for i = 1, #descriptionlines do
retval = retval .. core.formspec_escape(descriptionlines[i]) .. ","
end
if selected_pkg.type == "mod" then
if selected_pkg.is_modpack then
retval = retval .. ";0]" ..
"button[8.9,4.65;3,1;btn_mod_mgr_rename_modpack;" ..
fgettext("Rename") .. "]"
else
--show dependencies
local toadd_hard = table.concat(info.depends or {}, ",")
local toadd_soft = table.concat(info.optional_depends or {}, ",")
if toadd_hard == "" and toadd_soft == "" then
retval = retval .. "," .. fgettext("No dependencies.")
else
if toadd_hard ~= "" then
retval = retval .. "," .. fgettext("Dependencies:") .. ","
retval = retval .. toadd_hard
end
if toadd_soft ~= "" then
if toadd_hard ~= "" then
retval = retval .. ","
end
retval = retval .. "," .. fgettext("Optional dependencies:") .. ","
retval = retval .. toadd_soft
end
end
retval = retval .. ";0]"
end
else
retval = retval .. ";0]"
if selected_pkg.type == "txp" then
if selected_pkg.enabled then
retval = retval ..
"button[8.9,4.65;3,1;btn_mod_mgr_disable_txp;" ..
fgettext("Disable Texture Pack") .. "]"
else
retval = retval ..
"button[8.9,4.65;3,1;btn_mod_mgr_use_txp;" ..
fgettext("Use Texture Pack") .. "]"
end
end
end
retval = retval .. "button[5.5,4.65;3,1;btn_mod_mgr_delete_mod;"
.. fgettext("Uninstall Package") .. "]"
end
return retval
end
--------------------------------------------------------------------------------
local function handle_buttons(tabview, fields, tabname, tabdata)
if fields["pkglist"] ~= nil then
local event = core.explode_table_event(fields["pkglist"])
tabdata.selected_pkg = event.row
return true
end
if fields["btn_mod_mgr_install_local"] ~= nil then
core.show_file_open_dialog("mod_mgt_open_dlg", fgettext("Select Package File:"))
return true
end
if fields["btn_contentdb"] ~= nil then
local dlg = create_store_dlg()
dlg:set_parent(tabview)
tabview:hide()
dlg:show()
packages = nil
return true
end
if fields["btn_mod_mgr_rename_modpack"] ~= nil then
local dlg_renamemp = create_rename_modpack_dlg(tabdata.selected_pkg)
dlg_renamemp:set_parent(tabview)
tabview:hide()
dlg_renamemp:show()
return true
end
if fields["btn_mod_mgr_delete_mod"] ~= nil then
local mod = packages:get_list()[tabdata.selected_pkg]
local dlg_delmod = create_delete_content_dlg(mod)
dlg_delmod:set_parent(tabview)
tabview:hide()
dlg_delmod:show()
packages = nil
return true
end
if fields.btn_mod_mgr_use_txp then
local txp = packages:get_list()[tabdata.selected_pkg]
core.settings:set("texture_path", txp.path)
packages = nil
return true
end
if fields.btn_mod_mgr_disable_txp then
core.settings:set("texture_path", "")
packages = nil
return true
end
if fields["mod_mgt_open_dlg_accepted"] and
fields["mod_mgt_open_dlg_accepted"] ~= "" then
pkgmgr.install_mod(fields["mod_mgt_open_dlg_accepted"],nil)
return true
end
return false
end
--------------------------------------------------------------------------------
return {
name = "content",
caption = fgettext("Content"),
cbf_formspec = get_formspec,
cbf_button_handler = handle_buttons,
on_change = pkgmgr.update_gamelist
}

View File

@ -17,7 +17,7 @@
local function current_game()
local last_game_id = core.settings:get("menu_last_game")
local game, index = gamemgr.find_by_gameid(last_game_id)
local game, index = pkgmgr.find_by_gameid(last_game_id)
return game
end
@ -32,12 +32,12 @@ local function singleplayer_refresh_gamebar()
local function game_buttonbar_button_handler(fields)
for key,value in pairs(fields) do
for j=1,#gamemgr.games,1 do
if ("game_btnbar_" .. gamemgr.games[j].id == key) then
mm_texture.update("singleplayer", gamemgr.games[j])
core.set_topleft_text(gamemgr.games[j].name)
core.settings:set("menu_last_game",gamemgr.games[j].id)
menudata.worldlist:set_filtercriteria(gamemgr.games[j].id)
for j=1,#pkgmgr.games,1 do
if ("game_btnbar_" .. pkgmgr.games[j].id == key) then
mm_texture.update("singleplayer", pkgmgr.games[j])
core.set_topleft_text(pkgmgr.games[j].name)
core.settings:set("menu_last_game",pkgmgr.games[j].id)
menudata.worldlist:set_filtercriteria(pkgmgr.games[j].id)
local index = filterlist.get_current_index(menudata.worldlist,
tonumber(core.settings:get("mainmenu_last_selected_world")))
if not index or index < 1 then
@ -59,21 +59,21 @@ local function singleplayer_refresh_gamebar()
game_buttonbar_button_handler,
{x=-0.3,y=5.9}, "horizontal", {x=12.4,y=1.15})
for i=1,#gamemgr.games,1 do
local btn_name = "game_btnbar_" .. gamemgr.games[i].id
for i=1,#pkgmgr.games,1 do
local btn_name = "game_btnbar_" .. pkgmgr.games[i].id
local image = nil
local text = nil
local tooltip = core.formspec_escape(gamemgr.games[i].name)
local tooltip = core.formspec_escape(pkgmgr.games[i].name)
if gamemgr.games[i].menuicon_path ~= nil and
gamemgr.games[i].menuicon_path ~= "" then
image = core.formspec_escape(gamemgr.games[i].menuicon_path)
if pkgmgr.games[i].menuicon_path ~= nil and
pkgmgr.games[i].menuicon_path ~= "" then
image = core.formspec_escape(pkgmgr.games[i].menuicon_path)
else
local part1 = gamemgr.games[i].id:sub(1,5)
local part2 = gamemgr.games[i].id:sub(6,10)
local part3 = gamemgr.games[i].id:sub(11)
local part1 = pkgmgr.games[i].id:sub(1,5)
local part2 = pkgmgr.games[i].id:sub(6,10)
local part3 = pkgmgr.games[i].id:sub(11)
text = part1 .. "\n" .. part2
if part3 ~= nil and
@ -213,7 +213,7 @@ local function main_button_handler(this, fields, name, tabdata)
--update last game
local world = menudata.worldlist:get_raw_element(gamedata.selected_world)
if world then
local game, index = gamemgr.find_by_gameid(world.gameid)
local game, index = pkgmgr.find_by_gameid(world.gameid)
core.settings:set("menu_last_game", game.id)
end

View File

@ -1,151 +0,0 @@
--Minetest
--Copyright (C) 2014 sapier
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
--------------------------------------------------------------------------------
local function get_formspec(tabview, name, tabdata)
if modmgr.global_mods == nil then
modmgr.refresh_globals()
end
if tabdata.selected_mod == nil then
tabdata.selected_mod = 1
end
local retval =
"label[0.05,-0.25;".. fgettext("Installed Mods:") .. "]" ..
"tablecolumns[color;tree;text]" ..
"table[0,0.25;5.1,5;modlist;" ..
modmgr.render_modlist(modmgr.global_mods) ..
";" .. tabdata.selected_mod .. "]"
local selected_mod = nil
if filterlist.size(modmgr.global_mods) >= tabdata.selected_mod then
selected_mod = modmgr.global_mods:get_list()[tabdata.selected_mod]
end
if selected_mod ~= nil then
--check for screenshot beeing available
local screenshotfilename = selected_mod.path .. DIR_DELIM .. "screenshot.png"
local screenshotfile, error = io.open(screenshotfilename,"r")
local modscreenshot
if error == nil then
screenshotfile:close()
modscreenshot = screenshotfilename
end
if modscreenshot == nil then
modscreenshot = defaulttexturedir .. "no_screenshot.png"
end
retval = retval ..
"image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" ..
"label[8.25,0.6;" .. selected_mod.name .. "]" ..
"label[5.5,1.7;".. fgettext("Mod Information:") .. "]" ..
"textlist[5.5,2.2;6.2,2.4;description;"
local info = core.get_mod_info(selected_mod.path)
local desc = info.description or fgettext("No mod description available")
local descriptionlines = core.wrap_text(desc, 42, true)
for i = 1, #descriptionlines do
retval = retval .. core.formspec_escape(descriptionlines[i]) .. ","
end
if selected_mod.is_modpack then
retval = retval .. ";0]" ..
"button[9.9,4.65;2,1;btn_mod_mgr_rename_modpack;" ..
fgettext("Rename") .. "]"
retval = retval .. "button[5.5,4.65;4.5,1;btn_mod_mgr_delete_mod;"
.. fgettext("Uninstall Selected Modpack") .. "]"
else
--show dependencies
local toadd_hard = table.concat(info.depends, ",")
local toadd_soft = table.concat(info.optional_depends, ",")
if toadd_hard == "" and toadd_soft == "" then
retval = retval .. "," .. fgettext("No dependencies.")
else
if toadd_hard ~= "" then
retval = retval .. "," .. fgettext("Dependencies:") .. ","
retval = retval .. toadd_hard
end
if toadd_soft ~= "" then
if toadd_hard ~= "" then
retval = retval .. ","
end
retval = retval .. "," .. fgettext("Optional dependencies:") .. ","
retval = retval .. toadd_soft
end
end
retval = retval .. ";0]"
retval = retval .. "button[5.5,4.65;4.5,1;btn_mod_mgr_delete_mod;"
.. fgettext("Uninstall Selected Mod") .. "]"
end
end
return retval
end
--------------------------------------------------------------------------------
local function handle_buttons(tabview, fields, tabname, tabdata)
if fields["modlist"] ~= nil then
local event = core.explode_table_event(fields["modlist"])
tabdata.selected_mod = event.row
return true
end
if fields["btn_mod_mgr_install_local"] ~= nil then
core.show_file_open_dialog("mod_mgt_open_dlg",fgettext("Select Mod File:"))
return true
end
if fields["btn_mod_mgr_rename_modpack"] ~= nil then
local dlg_renamemp = create_rename_modpack_dlg(tabdata.selected_mod)
dlg_renamemp:set_parent(tabview)
tabview:hide()
dlg_renamemp:show()
return true
end
if fields["btn_mod_mgr_delete_mod"] ~= nil then
local dlg_delmod = create_delete_mod_dlg(tabdata.selected_mod)
dlg_delmod:set_parent(tabview)
tabview:hide()
dlg_delmod:show()
return true
end
if fields["mod_mgt_open_dlg_accepted"] ~= nil and
fields["mod_mgt_open_dlg_accepted"] ~= "" then
modmgr.installmod(fields["mod_mgt_open_dlg_accepted"],nil)
return true
end
return false
end
--------------------------------------------------------------------------------
return {
name = "mods",
caption = fgettext("Mods"),
cbf_formspec = get_formspec,
cbf_button_handler = handle_buttons,
on_change = gamemgr.update_gamelist
}

View File

@ -52,21 +52,25 @@ Games are looked up from:
* `$path_share/games/gameid/`
* `$path_user/games/gameid/`
where `gameid` is unique to each game.
Where `gameid` is unique to each game.
The game directory contains the file `game.conf`, which contains these fields:
The game directory can contain the following files:
name = <Human-readable full name of the game>
e.g.
name = Minetest
The game directory can contain the file minetest.conf, which will be used
to set default settings when running the particular game.
It can also contain a settingtypes.txt in the same format as the one in builtin.
This settingtypes.txt will be parsed by the menu and the settings will be displayed
in the "Games" category in the settings tab.
* `game.conf`, with the following keys:
* `name` - required, human readable name e.g. `name = Minetest`
* `description` - Short description to be shown in the content tab
* `disallowed_mapgens = <comma-separated mapgens>`
e.g. `disallowed_mapgens = v5,v6,flat`
These mapgens are removed from the list of mapgens for the game.
* `minetest.conf`:
Used to set default settings when running this game.
* `settingtypes.txt`:
In the same format as the one in builtin.
This settingtypes.txt will be parsed by the menu and the settings will be
displayed in the "Games" category in the advanced settings tab.
* If the game contains a folder called `textures` the server will load it as a
texturepack, overriding mod textures.
Any server texturepack will override mod textures and the game texturepack.
### Menu images
@ -2246,7 +2250,11 @@ Helper functions
* returns time with microsecond precision. May not return wall time.
* `table.copy(table)`: returns a table
* returns a deep copy of `table`
* `minetest.pointed_thing_to_face_pos(placer, pointed_thing)`: returns a position
* `table.insert_all(table, other_table)`:
* Appends all values in `other_table` to `table` - uses `#table + 1` to
find new indices.
* `minetest.pointed_thing_to_face_pos(placer, pointed_thing)`: returns a
position.
* returns the exact position on the surface of a pointed node
`minetest` namespace reference

View File

@ -33,14 +33,6 @@ core.close()
Filesystem:
core.get_builtin_path()
^ returns path to builtin root
core.get_modpath() (possible in async calls)
^ returns path to global modpath
core.get_clientmodpath() (possible in async calls)
^ returns path to global client-side modpath
core.get_gamepath() (possible in async calls)
^ returns path to global gamepath
core.get_texturepath() (possible in async calls)
^ returns path to default textures
core.create_dir(absolute_path) (possible in async calls)
^ absolute_path to directory to create (needs to be absolute)
^ returns true/false
@ -71,6 +63,8 @@ core.get_video_drivers()
^ returns list of available video drivers' settings name and 'friendly' display name
^ e.g. { {name="opengl", friendly_name="OpenGL"}, {name="software", friendly_name="Software Renderer"} }
^ first element of returned list is guaranteed to be the NULL driver
core.get_mapgen_names([include_hidden=false]) -> table of map generator algorithms
registered in the core (possible in async calls)
Formspec:
core.update_formspec(formspec)
@ -111,29 +105,58 @@ core.get_screen_info()
window_height = <current window height>
}
Packages:
core.get_game(index)
^ returns {
id = <id>,
path = <full path to game>,
gamemods_path = <path>,
name = <name of game>,
menuicon_path = <full path to menuicon>,
DEPRECATED:
addon_mods_paths = {[1] = <path>,},
}
core.get_games() -> table of all games in upper format (possible in async calls)
core.get_mapgen_names([include_hidden=false]) -> table of map generator algorithms
registered in the core (possible in async calls)
core.get_mod_info(path)
^ returns {
name = "name of mod",
type = "mod" or "modpack",
description = "description",
path = "path/to/mod",
depends = {"mod", "names"},
optional_depends = {"mod", "names"},
}
### Content and Packages
Content - an installed mod, modpack, game, or texture pack (txt)
Package - content which is downloadable from the content db, may or may not be installed.
* core.get_modpath() (possible in async calls)
* returns path to global modpath
* core.get_clientmodpath() (possible in async calls)
* returns path to global client-side modpath
* core.get_gamepath() (possible in async calls)
* returns path to global gamepath
* core.get_texturepath() (possible in async calls)
* returns path to default textures
* core.get_game(index)
* returns:
{
id = <id>,
path = <full path to game>,
gamemods_path = <path>,
name = <name of game>,
menuicon_path = <full path to menuicon>,
author = "author",
DEPRECATED:
addon_mods_paths = {[1] = <path>,},
}
* core.get_games() -> table of all games in upper format (possible in async calls)
* core.get_content_info(path)
* returns
{
name = "name of content",
type = "mod" or "modpack" or "game" or "txp",
description = "description",
author = "author",
path = "path/to/content",
depends = {"mod", "names"}, -- mods only
optional_depends = {"mod", "names"}, -- mods only
}
* core.get_package_list() -> downloads package list from content db
* returns a list of:
{
name = "basename",
title = "human readable title",
author = "username",
type = "", -- mod, game, txp
short_description = "description",
url = "",
}
Favorites:
core.get_favorites(location) -> list of favorites (possible in async calls)

View File

@ -9,6 +9,7 @@ Texture pack directory structure
textures
|-- Texture Pack
| |-- texture_pack.conf
| |-- screenshot.png
| |-- description.txt
| |-- override.txt
@ -21,9 +22,17 @@ This is a directory containing the entire contents of a single texture pack.
It can be chosen more or less freely and will also become the name of the
texture pack. The name must not be “base”.
### `texture_pack.conf`
A key-value config file with the following keys:
* `title` - human readable title
* `description` - short description, shown in the content tab
### `description.txt`
**Deprecated**, you should use texture_pack.conf instead.
A file containing a short description of the texture pack to be shown in the
texture packs tab.
content tab.
### `screenshot.png`
A preview image showing an in-game screenshot of this texture pack; it will be

View File

@ -360,6 +360,7 @@ add_custom_target(GenerateVersion
add_subdirectory(threading)
add_subdirectory(content)
add_subdirectory(network)
add_subdirectory(script)
add_subdirectory(unittest)
@ -419,7 +420,6 @@ set(common_SRCS
mg_decoration.cpp
mg_ore.cpp
mg_schematic.cpp
mods.cpp
nameidmapping.cpp
nodedef.cpp
nodemetadata.cpp
@ -446,7 +446,6 @@ set(common_SRCS
socket.cpp
sound.cpp
staticobject.cpp
subgame.cpp
terminal_chat_console.cpp
tileanimation.cpp
tool.cpp
@ -455,6 +454,7 @@ set(common_SRCS
voxel.cpp
voxelalgorithms.cpp
${common_network_SRCS}
${content_SRCS}
${JTHREAD_SRCS}
${common_SCRIPT_SRCS}
${UTIL_SRCS}

View File

@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "mapblock_mesh.h"
#include "mapblock.h"
#include "minimap.h"
#include "mods.h"
#include "content/mods.h"
#include "profiler.h"
#include "gettext.h"
#include "clientmap.h"

View File

@ -0,0 +1,7 @@
set(content_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/content.cpp
${CMAKE_CURRENT_SOURCE_DIR}/packages.cpp
${CMAKE_CURRENT_SOURCE_DIR}/mods.cpp
${CMAKE_CURRENT_SOURCE_DIR}/subgames.cpp
PARENT_SCOPE
)

108
src/content/content.cpp Normal file
View File

@ -0,0 +1,108 @@
/*
Minetest
Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <fstream>
#include "content/content.h"
#include "content/subgames.h"
#include "content/mods.h"
#include "filesys.h"
#include "settings.h"
enum ContentType
{
ECT_UNKNOWN,
ECT_MOD,
ECT_MODPACK,
ECT_GAME,
ECT_TXP
};
ContentType getContentType(const ContentSpec &spec)
{
std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
if (modpack_is.good()) {
modpack_is.close();
return ECT_MODPACK;
}
std::ifstream init_is((spec.path + DIR_DELIM + "init.lua").c_str());
if (init_is.good()) {
init_is.close();
return ECT_MOD;
}
std::ifstream game_is((spec.path + DIR_DELIM + "game.conf").c_str());
if (game_is.good()) {
game_is.close();
return ECT_GAME;
}
std::ifstream txp_is((spec.path + DIR_DELIM + "texture_pack.conf").c_str());
if (txp_is.good()) {
txp_is.close();
return ECT_TXP;
}
return ECT_UNKNOWN;
}
void parseContentInfo(ContentSpec &spec)
{
std::string conf_path;
switch (getContentType(spec)) {
case ECT_MOD:
spec.type = "mod";
conf_path = spec.path + DIR_DELIM + "mod.conf";
break;
case ECT_MODPACK:
spec.type = "modpack";
conf_path = spec.path + DIR_DELIM + "mod.conf";
break;
case ECT_GAME:
spec.type = "game";
conf_path = spec.path + DIR_DELIM + "game.conf";
break;
case ECT_TXP:
spec.type = "txp";
conf_path = spec.path + DIR_DELIM + "texture_pack.conf";
break;
default:
spec.type = "unknown";
break;
}
Settings conf;
if (!conf_path.empty() && conf.readConfigFile(conf_path.c_str())) {
if (conf.exists("name"))
spec.name = conf.get("name");
if (conf.exists("description"))
spec.desc = conf.get("description");
if (conf.exists("author"))
spec.author = conf.get("author");
}
if (spec.desc.empty()) {
std::ifstream is((spec.path + DIR_DELIM + "description.txt").c_str());
spec.desc = std::string((std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>());
}
}

33
src/content/content.h Normal file
View File

@ -0,0 +1,33 @@
/*
Minetest
Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "config.h"
#include "convert_json.h"
struct ContentSpec
{
std::string type;
std::string author;
std::string name;
std::string desc;
std::string path;
};
void parseContentInfo(ContentSpec &spec);

View File

@ -21,24 +21,24 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <fstream>
#include <json/json.h>
#include <algorithm>
#include "mods.h"
#include "content/mods.h"
#include "filesys.h"
#include "log.h"
#include "subgame.h"
#include "content/subgames.h"
#include "settings.h"
#include "convert_json.h"
#include "exceptions.h"
#include "porting.h"
bool parseDependsString(std::string &dep,
std::unordered_set<char> &symbols)
bool parseDependsString(std::string &dep, std::unordered_set<char> &symbols)
{
dep = trim(dep);
symbols.clear();
size_t pos = dep.size();
while (pos > 0 && !string_allowed(dep.substr(pos-1, 1), MODNAME_ALLOWED_CHARS)) {
while (pos > 0 &&
!string_allowed(dep.substr(pos - 1, 1), MODNAME_ALLOWED_CHARS)) {
// last character is a symbol, not part of the modname
symbols.insert(dep[pos-1]);
symbols.insert(dep[pos - 1]);
--pos;
}
dep = trim(dep.substr(0, pos));
@ -49,19 +49,22 @@ void parseModContents(ModSpec &spec)
{
// NOTE: this function works in mutual recursion with getModsInPath
Settings info;
info.readConfigFile((spec.path+DIR_DELIM+"mod.conf").c_str());
info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str());
if (info.exists("name"))
spec.name = info.get("name");
if (info.exists("author"))
spec.author = info.get("author");
spec.depends.clear();
spec.optdepends.clear();
spec.is_modpack = false;
spec.modpack_content.clear();
// Handle modpacks (defined by containing modpack.txt)
std::ifstream modpack_is((spec.path+DIR_DELIM+"modpack.txt").c_str());
if (modpack_is.good()) { // a modpack, recursively get the mods in it
std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
if (modpack_is.good()) { // a modpack, recursively get the mods in it
modpack_is.close(); // We don't actually need the file
spec.is_modpack = true;
spec.modpack_content = getModsInPath(spec.path, true);
@ -74,8 +77,10 @@ void parseModContents(ModSpec &spec)
if (info.exists("depends")) {
mod_conf_has_depends = true;
std::string dep = info.get("depends");
// clang-format off
dep.erase(std::remove_if(dep.begin(), dep.end(),
static_cast<int(*)(int)>(&std::isspace)), dep.end());
static_cast<int (*)(int)>(&std::isspace)), dep.end());
// clang-format on
for (const auto &dependency : str_split(dep, ',')) {
spec.depends.insert(dependency);
}
@ -84,8 +89,10 @@ void parseModContents(ModSpec &spec)
if (info.exists("optional_depends")) {
mod_conf_has_depends = true;
std::string dep = info.get("optional_depends");
// clang-format off
dep.erase(std::remove_if(dep.begin(), dep.end(),
static_cast<int(*)(int)>(&std::isspace)), dep.end());
static_cast<int (*)(int)>(&std::isspace)), dep.end());
// clang-format on
for (const auto &dependency : str_split(dep, ',')) {
spec.optdepends.insert(dependency);
}
@ -117,14 +124,16 @@ void parseModContents(ModSpec &spec)
if (info.exists("description")) {
spec.desc = info.get("description");
} else {
std::ifstream is((spec.path + DIR_DELIM + "description.txt").c_str());
std::ifstream is((spec.path + DIR_DELIM + "description.txt")
.c_str());
spec.desc = std::string((std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>());
}
}
}
std::map<std::string, ModSpec> getModsInPath(std::string path, bool part_of_modpack)
std::map<std::string, ModSpec> getModsInPath(
const std::string &path, bool part_of_modpack)
{
// NOTE: this function works in mutual recursion with parseModContents
@ -140,8 +149,10 @@ std::map<std::string, ModSpec> getModsInPath(std::string path, bool part_of_modp
continue;
std::string modpath = path + DIR_DELIM + modname;
ModSpec spec(modname, modpath);
spec.part_of_modpack = part_of_modpack;
modpath.clear();
modpath.append(path).append(DIR_DELIM).append(modname);
ModSpec spec(modname, modpath, part_of_modpack);
parseModContents(spec);
result.insert(std::make_pair(modname, spec));
}
@ -159,10 +170,9 @@ std::vector<ModSpec> flattenMods(std::map<std::string, ModSpec> mods)
{
std::vector<ModSpec> content = flattenMods(mod.modpack_content);
result.reserve(result.size() + content.size());
result.insert(result.end(),content.begin(),content.end());
result.insert(result.end(), content.begin(), content.end());
}
else //not a modpack
} else // not a modpack
{
result.push_back(mod);
}
@ -179,13 +189,11 @@ ModConfiguration::ModConfiguration(const std::string &worldpath):
void ModConfiguration::printUnsatisfiedModsError() const
{
for (std::vector<ModSpec>::const_iterator it = m_unsatisfied_mods.begin();
it != m_unsatisfied_mods.end(); ++it) {
ModSpec mod = *it;
errorstream << "mod \"" << mod.name << "\" has unsatisfied dependencies: ";
for (UNORDERED_SET<std::string>::iterator dep_it = mod.unsatisfied_depends.begin();
dep_it != mod.unsatisfied_depends.end(); ++dep_it)
errorstream << " \"" << *dep_it << "\"";
for (const ModSpec &mod : m_unsatisfied_mods) {
errorstream << "mod \"" << mod.name
<< "\" has unsatisfied dependencies: ";
for (const std::string &unsatisfied_depend : mod.unsatisfied_depends)
errorstream << " \"" << unsatisfied_depend << "\"";
errorstream << std::endl;
}
}
@ -200,12 +208,12 @@ void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
// Maintain a map of all existing m_unsatisfied_mods.
// Keys are mod names and values are indices into m_unsatisfied_mods.
std::map<std::string, u32> existing_mods;
for(u32 i = 0; i < m_unsatisfied_mods.size(); ++i){
for (u32 i = 0; i < m_unsatisfied_mods.size(); ++i) {
existing_mods[m_unsatisfied_mods[i].name] = i;
}
// Add new mods
for(int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack){
for (int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack) {
// First iteration:
// Add all the mods that come from modpacks
// Second iteration:
@ -222,15 +230,16 @@ void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
// GOOD CASE: completely new mod.
m_unsatisfied_mods.push_back(mod);
existing_mods[mod.name] = m_unsatisfied_mods.size() - 1;
}
else if(seen_this_iteration.count(mod.name) == 0){
} else if (seen_this_iteration.count(mod.name) == 0) {
// BAD CASE: name conflict in different levels.
u32 oldindex = existing_mods[mod.name];
const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
warningstream<<"Mod name conflict detected: \""
<<mod.name<<"\""<<std::endl
<<"Will not load: "<<oldmod.path<<std::endl
<<"Overridden by: "<<mod.path<<std::endl;
warningstream << "Mod name conflict detected: \""
<< mod.name << "\"" << std::endl
<< "Will not load: " << oldmod.path
<< std::endl
<< "Overridden by: " << mod.path
<< std::endl;
m_unsatisfied_mods[oldindex] = mod;
// If there was a "VERY BAD CASE" name conflict
@ -241,10 +250,12 @@ void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
// VERY BAD CASE: name conflict in the same level.
u32 oldindex = existing_mods[mod.name];
const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
warningstream<<"Mod name conflict detected: \""
<<mod.name<<"\""<<std::endl
<<"Will not load: "<<oldmod.path<<std::endl
<<"Will not load: "<<mod.path<<std::endl;
warningstream << "Mod name conflict detected: \""
<< mod.name << "\"" << std::endl
<< "Will not load: " << oldmod.path
<< std::endl
<< "Will not load: " << mod.path
<< std::endl;
m_unsatisfied_mods[oldindex] = mod;
m_name_conflicts.insert(mod.name);
}
@ -253,17 +264,16 @@ void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
}
}
void ModConfiguration::addModsFormConfig(const std::string &settings_path, const std::set<std::string> &mods)
void ModConfiguration::addModsFromConfig(
const std::string &settings_path, const std::set<std::string> &mods)
{
Settings conf;
std::set<std::string> load_mod_names;
conf.readConfigFile(settings_path.c_str());
std::vector<std::string> names = conf.getNames();
for (std::vector<std::string>::iterator it = names.begin();
it != names.end(); ++it) {
std::string name = *it;
if (name.compare(0,9,"load_mod_")==0 && conf.getBool(name))
for (const std::string &name : names) {
if (name.compare(0, 9, "load_mod_") == 0 && conf.getBool(name))
load_mod_names.insert(name.substr(9));
}
@ -273,7 +283,7 @@ void ModConfiguration::addModsFormConfig(const std::string &settings_path, const
std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(*i));
for (std::vector<ModSpec>::const_iterator it = addon_mods_in_path.begin();
it != addon_mods_in_path.end(); ++it) {
const ModSpec& mod = *it;
const ModSpec &mod = *it;
if (load_mod_names.count(mod.name) != 0)
addon_mods.push_back(mod);
else
@ -307,9 +317,11 @@ void ModConfiguration::checkConflictsAndDeps()
// report on name conflicts
if (!m_name_conflicts.empty()) {
std::string s = "Unresolved name conflicts for mods ";
for (UNORDERED_SET<std::string>::const_iterator it = m_name_conflicts.begin();
it != m_name_conflicts.end(); ++it) {
if (it != m_name_conflicts.begin()) s += ", ";
for (std::unordered_set<std::string>::const_iterator it =
m_name_conflicts.begin();
it != m_name_conflicts.end(); ++it) {
if (it != m_name_conflicts.begin())
s += ", ";
s += std::string("\"") + (*it) + "\"";
}
s += ".";
@ -353,13 +365,12 @@ void ModConfiguration::resolveDependencies()
// Step 3: mods without unmet dependencies can be appended to
// the sorted list.
while(!satisfied.empty()){
while (!satisfied.empty()) {
ModSpec mod = satisfied.back();
m_sorted_mods.push_back(mod);
satisfied.pop_back();
for(std::list<ModSpec>::iterator it = unsatisfied.begin();
it != unsatisfied.end(); ){
ModSpec& mod2 = *it;
for (auto it = unsatisfied.begin(); it != unsatisfied.end();) {
ModSpec &mod2 = *it;
mod2.unsatisfied_depends.erase(mod.name);
if(mod2.unsatisfied_depends.empty()){
satisfied.push_back(mod2);
@ -386,12 +397,12 @@ ServerModConfiguration::ServerModConfiguration(const std::string &worldpath):
// Load normal mods
std::string worldmt = worldpath + DIR_DELIM + "world.mt";
addModsFormConfig(worldmt, gamespec.addon_mods_paths);
addModsFromConfig(worldmt, gamespec.addon_mods_paths);
}
#ifndef SERVER
ClientModConfiguration::ClientModConfiguration(const std::string &path):
ModConfiguration(path)
ClientModConfiguration::ClientModConfiguration(const std::string &path) :
ModConfiguration(path)
{
std::set<std::string> paths;
std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
@ -399,13 +410,11 @@ ClientModConfiguration::ClientModConfiguration(const std::string &path):
paths.insert(path_user);
std::string settings_path = path_user + DIR_DELIM + "mods.conf";
addModsFormConfig(settings_path, paths);
addModsFromConfig(settings_path, paths);
}
#endif
ModMetadata::ModMetadata(const std::string &mod_name):
m_mod_name(mod_name),
m_modified(false)
ModMetadata::ModMetadata(const std::string &mod_name) : m_mod_name(mod_name)
{
m_stringvars.clear();
}
@ -426,23 +435,25 @@ bool ModMetadata::save(const std::string &root_path)
if (!fs::PathExists(root_path)) {
if (!fs::CreateAllDirs(root_path)) {
errorstream << "ModMetadata[" << m_mod_name << "]: Unable to save. '"
<< root_path << "' tree cannot be created." << std::endl;
errorstream << "ModMetadata[" << m_mod_name
<< "]: Unable to save. '" << root_path
<< "' tree cannot be created." << std::endl;
return false;
}
} else if (!fs::IsDir(root_path)) {
errorstream << "ModMetadata[" << m_mod_name << "]: Unable to save. '"
<< root_path << "' is not a directory." << std::endl;
<< root_path << "' is not a directory." << std::endl;
return false;
}
bool w_ok = fs::safeWriteToFile(root_path + DIR_DELIM + m_mod_name,
Json::FastWriter().write(json));
bool w_ok = fs::safeWriteToFile(
root_path + DIR_DELIM + m_mod_name, Json::FastWriter().write(json));
if (w_ok) {
m_modified = false;
} else {
errorstream << "ModMetadata[" << m_mod_name << "]: failed write file." << std::endl;
errorstream << "ModMetadata[" << m_mod_name << "]: failed write file."
<< std::endl;
}
return w_ok;
}
@ -451,16 +462,23 @@ bool ModMetadata::load(const std::string &root_path)
{
m_stringvars.clear();
std::ifstream is((root_path + DIR_DELIM + m_mod_name).c_str(), std::ios_base::binary);
std::ifstream is((root_path + DIR_DELIM + m_mod_name).c_str(),
std::ios_base::binary);
if (!is.good()) {
return false;
}
Json::Reader reader;
Json::Value root;
if (!reader.parse(is, root)) {
errorstream << "ModMetadata[" << m_mod_name << "]: failed read data "
"(Json decoding failure)." << std::endl;
Json::CharReaderBuilder builder;
builder.settings_["collectComments"] = false;
std::string errs;
if (!Json::parseFromStream(builder, is, &root, &errs)) {
errorstream << "ModMetadata[" << m_mod_name
<< "]: failed read data "
"(Json decoding failure). Message: "
<< errs << std::endl;
return false;
}

View File

@ -36,38 +36,38 @@ with this program; if not, write to the Free Software Foundation, Inc.,
struct ModSpec
{
std::string name;
std::string author;
std::string path;
std::string desc;
//if normal mod:
UNORDERED_SET<std::string> depends;
UNORDERED_SET<std::string> optdepends;
UNORDERED_SET<std::string> unsatisfied_depends;
// if normal mod:
std::unordered_set<std::string> depends;
std::unordered_set<std::string> optdepends;
std::unordered_set<std::string> unsatisfied_depends;
bool part_of_modpack = false;
bool is_modpack = false;
// if modpack:
std::map<std::string,ModSpec> modpack_content;
ModSpec(const std::string &name_="", const std::string &path_=""):
name(name_),
path(path_),
depends(),
optdepends(),
unsatisfied_depends(),
part_of_modpack(false),
is_modpack(false),
modpack_content()
{}
std::map<std::string, ModSpec> modpack_content;
ModSpec(const std::string &name = "", const std::string &path = "") :
name(name), path(path)
{
}
ModSpec(const std::string &name, const std::string &path, bool part_of_modpack) :
name(name), path(path), part_of_modpack(part_of_modpack)
{
}
};
// Retrieves depends, optdepends, is_modpack and modpack_content
void parseModContents(ModSpec &mod);
std::map<std::string,ModSpec> getModsInPath(std::string path, bool part_of_modpack = false);
std::map<std::string, ModSpec> getModsInPath(
const std::string &path, bool part_of_modpack = false);
// replaces modpack Modspecs with their content
std::vector<ModSpec> flattenMods(std::map<std::string,ModSpec> mods);
std::vector<ModSpec> flattenMods(std::map<std::string, ModSpec> mods);
// a ModConfiguration is a subset of installed mods, expected to have
// all dependencies fullfilled, so it can be used as a list of mods to
@ -76,15 +76,9 @@ class ModConfiguration
{
public:
// checks if all dependencies are fullfilled.
bool isConsistent() const
{
return m_unsatisfied_mods.empty();
}
bool isConsistent() const { return m_unsatisfied_mods.empty(); }
std::vector<ModSpec> getMods()
{
return m_sorted_mods;
}
const std::vector<ModSpec> &getMods() const { return m_sorted_mods; }
const std::vector<ModSpec> &getUnsatisfiedMods() const
{
@ -102,9 +96,11 @@ protected:
// adds all mods in the set.
void addMods(const std::vector<ModSpec> &new_mods);
void addModsFormConfig(const std::string &settings_path, const std::set<std::string> &mods);
void addModsFromConfig(const std::string &settings_path,
const std::set<std::string> &mods);
void checkConflictsAndDeps();
private:
// move mods from m_unsatisfied_mods to m_sorted_mods
// in an order that satisfies dependencies
@ -131,37 +127,24 @@ private:
UNORDERED_SET<std::string> m_name_conflicts;
// Deleted default constructor
ModConfiguration() {}
ModConfiguration() = default;
};
class ServerModConfiguration: public ModConfiguration
class ServerModConfiguration : public ModConfiguration
{
public:
ServerModConfiguration(const std::string &worldpath);
ServerModConfiguration(const std::string &path);
};
#ifndef SERVER
class ClientModConfiguration: public ModConfiguration
class ClientModConfiguration : public ModConfiguration
{
public:
ClientModConfiguration(const std::string &path);
};
#endif
struct ModLicenseInfo {
int id;
std::string shortinfo;
std::string url;
};
struct ModAuthorInfo {
int id;
std::string username;
};
class ModMetadata: public Metadata
class ModMetadata : public Metadata
{
public:
ModMetadata(const std::string &mod_name);
@ -176,6 +159,7 @@ public:
const std::string &getModName() const { return m_mod_name; }
virtual bool setString(const std::string &name, const std::string &var);
private:
std::string m_mod_name;
bool m_modified;

68
src/content/packages.cpp Normal file
View File

@ -0,0 +1,68 @@
/*
Minetest
Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "content/packages.h"
#include "log.h"
#include "filesys.h"
#include "porting.h"
#include "settings.h"
#include "content/mods.h"
#include "content/subgames.h"
#if USE_CURL
std::vector<Package> getPackagesFromURL(const std::string &url)
{
std::vector<std::string> extra_headers;
extra_headers.emplace_back("Accept: application/json");
Json::Value json = fetchJsonValue(url, &extra_headers);
if (!json.isArray()) {
errorstream << "Invalid JSON download " << std::endl;
return std::vector<Package>();
}
std::vector<Package> packages;
// Note: `unsigned int` is required to index JSON
for (unsigned int i = 0; i < json.size(); ++i) {
Package package;
package.name = json[i]["name"].asString();
package.title = json[i]["title"].asString();
package.author = json[i]["author"].asString();
package.type = json[i]["type"].asString();
package.shortDesc = json[i]["shortDesc"].asString();
package.url = json[i]["url"].asString();
Json::Value jScreenshots = json[i]["screenshots"];
for (unsigned int j = 0; j < jScreenshots.size(); ++j) {
package.screenshots.push_back(jScreenshots[j].asString());
}
if (package.valid()) {
packages.push_back(package);
} else {
errorstream << "Invalid package at " << i << std::endl;
}
}
return packages;
}
#endif

49
src/content/packages.h Normal file
View File

@ -0,0 +1,49 @@
/*
Minetest
Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "config.h"
#include "convert_json.h"
struct Package
{
std::string name; // Technical name
std::string title;
std::string author;
std::string type; // One of "mod", "game", or "txp"
std::string shortDesc;
std::string url; // download URL
std::vector<std::string> screenshots;
bool valid()
{
return !(name.empty() || title.empty() || author.empty() ||
type.empty() || url.empty());
}
};
#if USE_CURL
std::vector<Package> getPackagesFromURL(const std::string &url);
#else
inline std::vector<Package> getPackagesFromURL(const std::string &url)
{
return std::vector<Package>();
}
#endif

View File

@ -17,18 +17,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "subgame.h"
#include "content/subgames.h"
#include "porting.h"
#include "filesys.h"
#include "settings.h"
#include "log.h"
#include "util/strfnd.h"
#include "defaultsettings.h" // for override_default_settings
#include "mapgen.h" // for MapgenParams
#include "mapgen.h" // for MapgenParams
#include "util/string.h"
#ifndef SERVER
#include "client/tile.h" // getImagePath
#include "client/tile.h" // getImagePath
#endif
bool getGameMinetestConfig(const std::string &game_path, Settings &conf)
@ -37,30 +37,14 @@ bool getGameMinetestConfig(const std::string &game_path, Settings &conf)
return conf.readConfigFile(conf_path.c_str());
}
bool getGameConfig(const std::string &game_path, Settings &conf)
{
std::string conf_path = game_path + DIR_DELIM + "game.conf";
return conf.readConfigFile(conf_path.c_str());
}
std::string getGameName(const std::string &game_path)
{
Settings conf;
if(!getGameConfig(game_path, conf))
return "";
if(!conf.exists("name"))
return "";
return conf.get("name");
}
struct GameFindPath
{
std::string path;
bool user_specific;
GameFindPath(const std::string &path, bool user_specific):
path(path),
user_specific(user_specific)
{}
GameFindPath(const std::string &path, bool user_specific) :
path(path), user_specific(user_specific)
{
}
};
std::string getSubgamePathEnv()
@ -75,10 +59,12 @@ SubgameSpec findSubgame(const std::string &id)
return SubgameSpec();
std::string share = porting::path_share;
std::string user = porting::path_user;
std::vector<GameFindPath> find_paths;
// Get games install locations
Strfnd search_paths(getSubgamePathEnv());
// Get all possible paths fo game
std::vector<GameFindPath> find_paths;
while (!search_paths.at_end()) {
std::string path = search_paths.next(PATH_DELIM);
find_paths.push_back(GameFindPath(
@ -86,15 +72,13 @@ SubgameSpec findSubgame(const std::string &id)
find_paths.push_back(GameFindPath(
path + DIR_DELIM + id + "_game", false));
}
find_paths.emplace_back(
user + DIR_DELIM + "games" + DIR_DELIM + id + "_game", true);
find_paths.emplace_back(user + DIR_DELIM + "games" + DIR_DELIM + id, true);
find_paths.emplace_back(
share + DIR_DELIM + "games" + DIR_DELIM + id + "_game", false);
find_paths.emplace_back(share + DIR_DELIM + "games" + DIR_DELIM + id, false);
find_paths.push_back(GameFindPath(
user + DIR_DELIM + "games" + DIR_DELIM + id + "_game", true));
find_paths.push_back(GameFindPath(
user + DIR_DELIM + "games" + DIR_DELIM + id, true));
find_paths.push_back(GameFindPath(
share + DIR_DELIM + "games" + DIR_DELIM + id + "_game", false));
find_paths.push_back(GameFindPath(
share + DIR_DELIM + "games" + DIR_DELIM + id, false));
// Find game directory
std::string game_path;
bool user_game = true; // Game is in user's directory
@ -106,24 +90,41 @@ SubgameSpec findSubgame(const std::string &id)
break;
}
}
if(game_path == "")
if (game_path.empty())
return SubgameSpec();
std::string gamemod_path = game_path + DIR_DELIM + "mods";
// Find mod directories
std::set<std::string> mods_paths;
if(!user_game)
if (!user_game)
mods_paths.insert(share + DIR_DELIM + "mods");
if(user != share || user_game)
if (user != share || user_game)
mods_paths.insert(user + DIR_DELIM + "mods");
std::string game_name = getGameName(game_path);
if(game_name == "")
// Get meta
std::string conf_path = game_path + DIR_DELIM + "game.conf";
Settings conf;
conf.readConfigFile(conf_path.c_str());
std::string game_name;
if (conf.exists("name"))
game_name = conf.get("name");
else
game_name = id;
std::string game_author;
if (conf.exists("author"))
game_author = conf.get("author");
std::string menuicon_path;
#ifndef SERVER
menuicon_path = getImagePath(game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png");
menuicon_path = getImagePath(
game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png");
#endif
return SubgameSpec(id, game_path, gamemod_path, mods_paths, game_name,
menuicon_path);
menuicon_path, game_author);
}
SubgameSpec findWorldSubgame(const std::string &world_path)
@ -131,14 +132,21 @@ SubgameSpec findWorldSubgame(const std::string &world_path)
std::string world_gameid = getWorldGameId(world_path, true);
// See if world contains an embedded game; if so, use it.
std::string world_gamepath = world_path + DIR_DELIM + "game";
if(fs::PathExists(world_gamepath)){
if (fs::PathExists(world_gamepath)) {
SubgameSpec gamespec;
gamespec.id = world_gameid;
gamespec.path = world_gamepath;
gamespec.gamemods_path= world_gamepath + DIR_DELIM + "mods";
gamespec.name = getGameName(world_gamepath);
if(gamespec.name == "")
gamespec.name = "unknown";
gamespec.gamemods_path = world_gamepath + DIR_DELIM + "mods";
Settings conf;
std::string conf_path = world_gamepath + DIR_DELIM + "game.conf";
conf.readConfigFile(conf_path.c_str());
if (conf.exists("name"))
gamespec.name = conf.get("name");
else
gamespec.name = world_gameid;
return gamespec;
}
return findSubgame(world_gameid);
@ -156,23 +164,26 @@ std::set<std::string> getAvailableGameIds()
while (!search_paths.at_end())
gamespaths.insert(search_paths.next(PATH_DELIM));
for (std::set<std::string>::const_iterator i = gamespaths.begin();
i != gamespaths.end(); ++i){
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(*i);
for(u32 j=0; j<dirlist.size(); j++){
if(!dirlist[j].dir)
for (const std::string &gamespath : gamespaths) {
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(gamespath);
for (const fs::DirListNode &dln : dirlist) {
if (!dln.dir)
continue;
// If configuration file is not found or broken, ignore game
Settings conf;
if(!getGameConfig(*i + DIR_DELIM + dirlist[j].name, conf))
std::string conf_path = gamespath + DIR_DELIM + dln.name +
DIR_DELIM + "game.conf";
if (!conf.readConfigFile(conf_path.c_str()))
continue;
// Add it to result
const char *ends[] = {"_game", NULL};
std::string shorter = removeStringEnd(dirlist[j].name, ends);
if(shorter != "")
std::string shorter = removeStringEnd(dln.name, ends);
if (!shorter.empty())
gameids.insert(shorter);
else
gameids.insert(dirlist[j].name);
gameids.insert(dln.name);
}
}
return gameids;
@ -201,18 +212,18 @@ std::string getWorldGameId(const std::string &world_path, bool can_be_legacy)
std::string conf_path = world_path + DIR_DELIM + "world.mt";
Settings conf;
bool succeeded = conf.readConfigFile(conf_path.c_str());
if(!succeeded){
if(can_be_legacy){
if (!succeeded) {
if (can_be_legacy) {
// If map_meta.txt exists, it is probably an old minetest world
if(fs::PathExists(world_path + DIR_DELIM + "map_meta.txt"))
if (fs::PathExists(world_path + DIR_DELIM + "map_meta.txt"))
return LEGACY_GAMEID;
}
return "";
}
if(!conf.exists("gameid"))
if (!conf.exists("gameid"))
return "";
// The "mesetint" gameid has been discarded
if(conf.get("gameid") == "mesetint")
if (conf.get("gameid") == "mesetint")
return "minetest";
return conf.get("gameid");
}
@ -235,40 +246,39 @@ std::vector<WorldSpec> getAvailableWorlds()
worldspaths.insert(porting::path_user + DIR_DELIM + "worlds");
infostream << "Searching worlds..." << std::endl;
for (std::set<std::string>::const_iterator i = worldspaths.begin();
i != worldspaths.end(); ++i) {
infostream << " In " << (*i) << ": " <<std::endl;
std::vector<fs::DirListNode> dirvector = fs::GetDirListing(*i);
for(u32 j=0; j<dirvector.size(); j++){
if(!dirvector[j].dir)
for (const std::string &worldspath : worldspaths) {
infostream << " In " << worldspath << ": " << std::endl;
std::vector<fs::DirListNode> dirvector = fs::GetDirListing(worldspath);
for (const fs::DirListNode &dln : dirvector) {
if (!dln.dir)
continue;
std::string fullpath = *i + DIR_DELIM + dirvector[j].name;
std::string name = dirvector[j].name;
std::string fullpath = worldspath + DIR_DELIM + dln.name;
std::string name = dln.name;
// Just allow filling in the gameid always for now
bool can_be_legacy = true;
std::string gameid = getWorldGameId(fullpath, can_be_legacy);
WorldSpec spec(fullpath, name, gameid);
if(!spec.isValid()){
infostream<<"(invalid: "<<name<<") ";
if (!spec.isValid()) {
infostream << "(invalid: " << name << ") ";
} else {
infostream<<name<<" ";
infostream << name << " ";
worlds.push_back(spec);
}
}
infostream<<std::endl;
infostream << std::endl;
}
// Check old world location
do{
do {
std::string fullpath = porting::path_user + DIR_DELIM + "world";
if(!fs::PathExists(fullpath))
if (!fs::PathExists(fullpath))
break;
std::string name = "Old World";
std::string gameid = getWorldGameId(fullpath, true);
WorldSpec spec(fullpath, name, gameid);
infostream<<"Old world found."<<std::endl;
infostream << "Old world found." << std::endl;
worlds.push_back(spec);
}while(0);
infostream<<worlds.size()<<" found."<<std::endl;
} while (false);
infostream << worlds.size() << " found." << std::endl;
return worlds;
}
@ -305,8 +315,9 @@ bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamesp
// Create map_meta.txt if does not already exist
std::string map_meta_path = path + DIR_DELIM + "map_meta.txt";
if (!fs::PathExists(map_meta_path)){
verbosestream << "Creating map_meta.txt (" << map_meta_path << ")" << std::endl;
if (!fs::PathExists(map_meta_path)) {
verbosestream << "Creating map_meta.txt (" << map_meta_path << ")"
<< std::endl;
fs::CreateAllDirs(path);
std::ostringstream oss(std::ios_base::binary);
@ -322,4 +333,3 @@ bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamesp
}
return true;
}

View File

@ -30,39 +30,33 @@ class Settings;
struct SubgameSpec
{
std::string id; // "" = game does not exist
std::string path; // path to game
std::string gamemods_path; //path to mods of the game
std::set<std::string> addon_mods_paths; //paths to addon mods for this game
std::string id;
std::string name;
std::string author;
std::string path;
std::string gamemods_path;
std::set<std::string> addon_mods_paths;
std::string menuicon_path;
SubgameSpec(const std::string &id_="",
const std::string &path_="",
const std::string &gamemods_path_="",
const std::set<std::string> &addon_mods_paths_=std::set<std::string>(),
const std::string &name_="",
const std::string &menuicon_path_=""):
id(id_),
path(path_),
gamemods_path(gamemods_path_),
addon_mods_paths(addon_mods_paths_),
name(name_),
menuicon_path(menuicon_path_)
{}
bool isValid() const
SubgameSpec(const std::string &id = "", const std::string &path = "",
const std::string &gamemods_path = "",
const std::set<std::string> &addon_mods_paths =
std::set<std::string>(),
const std::string &name = "",
const std::string &menuicon_path = "",
const std::string &author = "") :
id(id),
name(name), author(author), path(path),
gamemods_path(gamemods_path), addon_mods_paths(addon_mods_paths),
menuicon_path(menuicon_path)
{
return (id != "" && path != "");
}
bool isValid() const { return (!id.empty() && !path.empty()); }
};
// minetest.conf
bool getGameMinetestConfig(const std::string &game_path, Settings &conf);
// game.conf
bool getGameConfig(const std::string &game_path, Settings &conf);
std::string getGameName(const std::string &game_path);
SubgameSpec findSubgame(const std::string &id);
SubgameSpec findWorldSubgame(const std::string &world_path);
@ -71,8 +65,7 @@ std::set<std::string> getAvailableGameIds();
std::vector<SubgameSpec> getAvailableGames();
bool getWorldExists(const std::string &world_path);
std::string getWorldGameId(const std::string &world_path,
bool can_be_legacy=false);
std::string getWorldGameId(const std::string &world_path, bool can_be_legacy = false);
struct WorldSpec
{
@ -80,15 +73,12 @@ struct WorldSpec
std::string name;
std::string gameid;
WorldSpec(
const std::string &path_="",
const std::string &name_="",
const std::string &gameid_=""
):
path(path_),
name(name_),
gameid(gameid_)
{}
WorldSpec(const std::string &path = "", const std::string &name = "",
const std::string &gameid = "") :
path(path),
name(name), gameid(gameid)
{
}
bool isValid() const
{
@ -103,4 +93,3 @@ std::vector<WorldSpec> getAvailableWorlds();
bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamespec);
#endif

View File

@ -22,7 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <sstream>
#include "convert_json.h"
#include "mods.h"
#include "content/mods.h"
#include "config.h"
#include "log.h"
#include "settings.h"

View File

@ -254,6 +254,7 @@ void set_default_settings(Settings *settings)
#endif
settings->setDefault("font_size", font_size_str);
settings->setDefault("mono_font_size", font_size_str);
settings->setDefault("contentdb_url", "https://contentdb.rubenwardy.com");
// Server

View File

@ -247,7 +247,7 @@ HTTPFetchOngoing::HTTPFetchOngoing(const HTTPFetchRequest &request_,
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 1);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3);
curl_easy_setopt(curl, CURLOPT_ENCODING, "gzip");
std::string bind_address = g_settings->get("bind_address");

View File

@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "common/c_converter.h"
#include "serverobject.h"
#include "filesys.h"
#include "mods.h"
#include "content/mods.h"
#include "porting.h"
#include "util/string.h"
#include "server.h"

View File

@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "lua_api/l_base.h"
#include "lua_api/l_internal.h"
#include "cpp_api/s_base.h"
#include <mods.h>
#include "content/mods.h"
#include <server.h>
ScriptApiBase *ModApiBase::getScriptApiBase(lua_State *L)

View File

@ -25,11 +25,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "guiMainMenu.h"
#include "guiKeyChangeMenu.h"
#include "guiFileSelectMenu.h"
#include "subgame.h"
#include "version.h"
#include "porting.h"
#include "filesys.h"
#include "convert_json.h"
#include "content/packages.h"
#include "content/content.h"
#include "content/subgames.h"
#include "serverlist.h"
#include "mapgen.h"
#include "settings.h"
@ -451,6 +453,10 @@ int ModApiMainMenu::l_get_games(lua_State *L)
lua_pushstring(L, game.path.c_str());
lua_settable(L, top_lvl2);
lua_pushstring(L, "type");
lua_pushstring(L, "game");
lua_settable(L, top_lvl2);
lua_pushstring(L, "gamemods_path");
lua_pushstring(L, game.gamemods_path.c_str());
lua_settable(L, top_lvl2);
@ -459,6 +465,10 @@ int ModApiMainMenu::l_get_games(lua_State *L)
lua_pushstring(L, game.name.c_str());
lua_settable(L, top_lvl2);
lua_pushstring(L, "author");
lua_pushstring(L, game.author.c_str());
lua_settable(L, top_lvl2);
lua_pushstring(L, "menuicon_path");
lua_pushstring(L, game.menuicon_path.c_str());
lua_settable(L, top_lvl2);
@ -481,47 +491,56 @@ int ModApiMainMenu::l_get_games(lua_State *L)
}
/******************************************************************************/
int ModApiMainMenu::l_get_mod_info(lua_State *L)
int ModApiMainMenu::l_get_content_info(lua_State *L)
{
std::string path = luaL_checkstring(L, 1);
ModSpec spec;
ContentSpec spec;
spec.path = path;
parseModContents(spec);
parseContentInfo(spec);
lua_newtable(L);
lua_pushstring(L, spec.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, spec.is_modpack ? "modpack" : "mod");
lua_pushstring(L, spec.type.c_str());
lua_setfield(L, -2, "type");
lua_pushstring(L, spec.author.c_str());
lua_setfield(L, -2, "author");
lua_pushstring(L, spec.desc.c_str());
lua_setfield(L, -2, "description");
lua_pushstring(L, spec.path.c_str());
lua_setfield(L, -2, "path");
// Dependencies
lua_newtable(L);
int i = 1;
for (const auto &dep : spec.depends) {
lua_pushstring(L, dep.c_str());
lua_rawseti(L, -2, i);
i++;
}
lua_setfield(L, -2, "depends");
if (spec.type == "mod") {
ModSpec spec;
spec.path = path;
parseModContents(spec);
// Optional Dependencies
lua_newtable(L);
i = 1;
for (const auto &dep : spec.optdepends) {
lua_pushstring(L, dep.c_str());
lua_rawseti(L, -2, i);
i++;
// Dependencies
lua_newtable(L);
int i = 1;
for (const auto &dep : spec.depends) {
lua_pushstring(L, dep.c_str());
lua_rawseti(L, -2, i);
i++;
}
lua_setfield(L, -2, "depends");
// Optional Dependencies
lua_newtable(L);
i = 1;
for (const auto &dep : spec.optdepends) {
lua_pushstring(L, dep.c_str());
lua_rawseti(L, -2, i);
i++;
}
lua_setfield(L, -2, "optional_depends");
}
lua_setfield(L, -2, "optional_depends");
return 1;
}
@ -836,6 +855,10 @@ bool ModApiMainMenu::isMinetestPath(std::string path)
if (fs::PathStartsWith(path,fs::RemoveRelativePathComponents(porting::path_user + DIR_DELIM + "mods")))
return true;
/* mods */
if (fs::PathStartsWith(path,fs::RemoveRelativePathComponents(porting::path_user + DIR_DELIM + "textures")))
return true;
/* worlds */
if (fs::PathStartsWith(path,fs::RemoveRelativePathComponents(porting::path_user + DIR_DELIM + "worlds")))
return true;
@ -968,6 +991,54 @@ int ModApiMainMenu::l_get_screen_info(lua_State *L)
return 1;
}
int ModApiMainMenu::l_get_package_list(lua_State *L)
{
std::string url = g_settings->get("contentdb_url");
std::vector<Package> packages = getPackagesFromURL(url + "/packages/");
// Make table
lua_newtable(L);
int top = lua_gettop(L);
unsigned int index = 1;
// Fill table
for (const auto &package : packages) {
lua_pushnumber(L, index);
lua_newtable(L);
int top_lvl2 = lua_gettop(L);
lua_pushstring(L, "name");
lua_pushstring(L, package.name.c_str());
lua_settable (L, top_lvl2);
lua_pushstring(L, "title");
lua_pushstring(L, package.title.c_str());
lua_settable (L, top_lvl2);
lua_pushstring(L, "author");
lua_pushstring(L, package.author.c_str());
lua_settable (L, top_lvl2);
lua_pushstring(L, "type");
lua_pushstring(L, package.type.c_str());
lua_settable (L, top_lvl2);
lua_pushstring(L, "short_description");
lua_pushstring(L, package.shortDesc.c_str());
lua_settable (L, top_lvl2);
lua_pushstring(L, "url");
lua_pushstring(L, package.url.c_str());
lua_settable (L, top_lvl2);
lua_settable(L, top);
index++;
}
return 1;
}
/******************************************************************************/
int ModApiMainMenu::l_get_min_supp_proto(lua_State *L)
{
@ -1013,7 +1084,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
API_FCT(get_table_index);
API_FCT(get_worlds);
API_FCT(get_games);
API_FCT(get_mod_info);
API_FCT(get_content_info);
API_FCT(start);
API_FCT(close);
API_FCT(get_favorites);
@ -1039,6 +1110,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
API_FCT(get_video_drivers);
API_FCT(get_video_modes);
API_FCT(get_screen_info);
API_FCT(get_package_list);
API_FCT(get_min_supp_proto);
API_FCT(get_max_supp_proto);
API_FCT(do_async_callback);
@ -1047,7 +1119,6 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
/******************************************************************************/
void ModApiMainMenu::InitializeAsync(lua_State *L, int top)
{
API_FCT(get_worlds);
API_FCT(get_games);
API_FCT(get_favorites);
@ -1062,5 +1133,5 @@ void ModApiMainMenu::InitializeAsync(lua_State *L, int top)
//API_FCT(extract_zip); //TODO remove dependency to GuiEngine
API_FCT(download_file);
//API_FCT(gettext); (gettext lib isn't threadsafe)
API_FCT(get_package_list);
}

View File

@ -84,7 +84,7 @@ private:
static int l_get_games(lua_State *L);
static int l_get_mod_info(lua_State *L);
static int l_get_content_info(lua_State *L);
//gui
@ -132,6 +132,9 @@ private:
static int l_get_video_modes(lua_State *L);
//content store
static int l_get_package_list(lua_State *L);
//version compatibility
static int l_get_min_supp_proto(lua_State *L);

View File

@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "lua_api/l_storage.h"
#include "l_internal.h"
#include "mods.h"
#include "content/mods.h"
#include "server.h"
int ModApiStorage::l_get_mod_storage(lua_State *L)

View File

@ -18,7 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/
#include "scripting_mainmenu.h"
#include "mods.h"
#include "content/mods.h"
#include "cpp_api/s_internal.h"
#include "lua_api/l_base.h"
#include "lua_api/l_mainmenu.h"

View File

@ -49,7 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "content_nodemeta.h"
#include "content_abm.h"
#include "content_sao.h"
#include "mods.h"
#include "content/mods.h"
#include "event_manager.h"
#include "serverlist.h"
#include "util/string.h"

View File

@ -26,9 +26,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "hud.h"
#include "gamedef.h"
#include "serialization.h" // For SER_FMT_VER_INVALID
#include "mods.h"
#include "content/mods.h"
#include "inventorymanager.h"
#include "subgame.h"
#include "content/subgames.h"
#include "tileanimation.h" // struct TileAnimationParams
#include "util/numeric.h"
#include "util/thread.h"

107
src/server/mods.cpp Normal file
View File

@ -0,0 +1,107 @@
/*
Minetest
Copyright (C) 2018 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "mods.h"
#include "filesys.h"
#include "log.h"
#include "scripting_server.h"
#include "content/subgames.h"
/**
* Manage server mods
*
* All new calls to this class must be tested in test_servermodmanager.cpp
*/
/**
* Creates a ServerModManager which targets worldpath
* @param worldpath
*/
ServerModManager::ServerModManager(const std::string &worldpath) :
ModConfiguration(worldpath)
{
SubgameSpec gamespec = findWorldSubgame(worldpath);
// Add all game mods and all world mods
addModsInPath(gamespec.gamemods_path);
addModsInPath(worldpath + DIR_DELIM + "worldmods");
// Load normal mods
std::string worldmt = worldpath + DIR_DELIM + "world.mt";
addModsFromConfig(worldmt, gamespec.addon_mods_paths);
}
// clang-format off
// This function cannot be currenctly easily tested but it should be ASAP
void ServerModManager::loadMods(ServerScripting *script)
{
// Print mods
infostream << "Server: Loading mods: ";
for (const ModSpec &mod : m_sorted_mods) {
infostream << mod.name << " ";
}
infostream << std::endl;
// Load and run "mod" scripts
for (const ModSpec &mod : m_sorted_mods) {
if (!string_allowed(mod.name, MODNAME_ALLOWED_CHARS)) {
throw ModError("Error loading mod \"" + mod.name +
"\": Mod name does not follow naming "
"conventions: "
"Only characters [a-z0-9_] are allowed.");
}
std::string script_path = mod.path + DIR_DELIM + "init.lua";
infostream << " [" << padStringRight(mod.name, 12) << "] [\""
<< script_path << "\"]" << std::endl;
auto t = std::chrono::steady_clock::now();
script->loadMod(script_path, mod.name);
infostream << "Mod \"" << mod.name << "\" loaded after "
<< std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t).count() * 0.001f
<< " seconds" << std::endl;
}
}
// clang-format on
const ModSpec *ServerModManager::getModSpec(const std::string &modname) const
{
std::vector<ModSpec>::const_iterator it;
for (it = m_sorted_mods.begin(); it != m_sorted_mods.end(); ++it) {
const ModSpec &mod = *it;
if (mod.name == modname)
return &mod;
}
return NULL;
}
void ServerModManager::getModNames(std::vector<std::string> &modlist) const
{
for (const ModSpec &spec : m_sorted_mods)
modlist.push_back(spec.name);
}
void ServerModManager::getModsMediaPaths(std::vector<std::string> &paths) const
{
for (const ModSpec &spec : m_sorted_mods) {
paths.push_back(spec.path + DIR_DELIM + "textures");
paths.push_back(spec.path + DIR_DELIM + "sounds");
paths.push_back(spec.path + DIR_DELIM + "media");
paths.push_back(spec.path + DIR_DELIM + "models");
paths.push_back(spec.path + DIR_DELIM + "locale");
}
}

43
src/server/mods.h Normal file
View File

@ -0,0 +1,43 @@
/*
Minetest
Copyright (C) 2018 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "content/mods.h"
class ServerScripting;
/**
* Manage server mods
*
* All new calls to this class must be tested in test_servermodmanager.cpp
*/
class ServerModManager : public ModConfiguration
{
public:
/**
* Creates a ServerModManager which targets worldpath
* @param worldpath
*/
ServerModManager(const std::string &worldpath);
void loadMods(ServerScripting *script);
const ModSpec *getModSpec(const std::string &modname) const;
void getModNames(std::vector<std::string> &modlist) const;
void getModsMediaPaths(std::vector<std::string> &paths) const;
};

View File

@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <iostream>
#include "config.h"
#include "mods.h"
#include "content/mods.h"
#include <json/json.h>
#ifndef SERVERLIST_HEADER

View File

@ -23,6 +23,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "itemdef.h"
#include "gamedef.h"
#include "mods.h"
#include "content/mods.h"
#include "util/numeric.h"
content_t t_CONTENT_STONE;
content_t t_CONTENT_GRASS;