code:
--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.

--------------------------------------------------------------------------------

local core_developers = {
	"Perttu Ahola (celeron55) <celeron55@gmail.com>",
	"sfan5 <sfan5@live.de>",
	"ShadowNinja <shadowninja@minetest.net>",
	"Nathanaël Courant (Nore/Ekdohibs) <nore@mesecons.net>",
	"Loic Blot (nerzhul/nrz) <loic.blot@unix-experience.fr>",
	"paramat",
	"Craig Robbins (Zeno) <craig.d.robbins@gmail.com>",
	"Auke Kok (sofar) <sofar@foo-projects.org>",
	"rubenwardy <rw@rubenwardy.com>",
	"Krock/SmallJoker <mk939@ymail.com>",
}

local active_contributors = {
	"red-001 <red-001@outlook.ie> [CSM & Menu fixes]",
	"Dániel Juhász (juhdanad) <juhdanad@gmail.com> [Audiovisuals: lighting]",
	"numberZero [Audiovisuals: meshgen]",
	"Lars Hofhansl <larsh@apache.org> [Occulusion culling, fixes]",
	"Jean-Patrick G (kilbith) <jeanpatrick.guerrero@gmail.com> [Audiovisuals]",
	"Vincent Glize (Dumbeldor) <vincent.glize@live.fr> [CSM]",
	"bigfoot547 [CSM]",
	"Rogier <rogier777@gmail.com> [Fixes]",
	"Wuzzy [Audiovisuals]",
	"Shara/Ezhh [Settings]",
}

local previous_core_developers = {
	"BlockMen",
	"Maciej Kasatkin (RealBadAngel) [RIP]",
	"Lisa Milne (darkrose) <lisa@ltmnet.com>",
	"proller",
	"Ilya Zhuravlev (xyz) <xyz@minetest.net>",
	"PilzAdam <pilzadam@minetest.net>",
	"est31 <MTest31@outlook.com>",
	"kahrl <kahrl@gmx.net>",
	"Ryan Kwolek (kwolekr) <kwolekr@minetest.net>",
	"sapier",
}

local previous_contributors = {
	"Gregory Currie (gregorycu) [optimisation]",
	"Diego Martínez (kaeza) <kaeza@users.sf.net>",
	"T4im [Profiler]",
	"TeTpaAka [Hand overriding, nametag colors]",
	"HybridDog [Fixes]",
	"Duane Robertson <duane@duanerobertson.com> [MGValleys]",
	"neoascetic [OS X Fixes]",
	"TriBlade9 <triblade9@mail.com> [Audiovisuals]",
	"Jurgen Doser (doserj) <jurgen.doser@gmail.com> [Fixes]",
	"MirceaKitsune <mirceakitsune@gmail.com> [Audiovisuals]",
	"Guiseppe Bilotta (Oblomov) <guiseppe.bilotta@gmail.com> [Fixes]",
	"matttpt <matttpt@gmail.com> [Fixes]",
	"Nils Dagsson Moskopp (erlehmann) <nils@dieweltistgarnichtso.net> [Minetest Logo]",
	"Jeija <jeija@mesecons.net> [HTTP, particles]",
}

local function buildCreditList(source)
	local ret = {}
	for i = 1, #source do
		ret[i] = core.formspec_escape(source[i])
	end
	return table.concat(ret, ",,")
end

return {
	name = "credits",
	caption = fgettext("Credits"),
	cbf_formspec = function(tabview, name, tabdata)
		local logofile = defaulttexturedir .. "logo.png"
		local version = core.get_version()
		return "image[0.5,1;" .. core.formspec_escape(logofile) .. "]" ..
			"label[0.5,3.2;" .. version.project .. " " .. version.string .. "]" ..
			"label[0.5,3.5;http://minetest.net]" ..
			"tablecolumns[color;text]" ..
			"tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
			"table[3.5,-0.25;8.5,6.05;list_credits;" ..
			"#FFFF00," .. fgettext("Core Developers") .. ",," ..
			buildCreditList(core_developers) .. ",,," ..
			"#FFFF00," .. fgettext("Active Contributors") .. ",," ..
			buildCreditList(active_contributors) .. ",,," ..
			"#FFFF00," .. fgettext("Previous Core Developers") ..",," ..
			buildCreditList(previous_core_developers) .. ",,," ..
			"#FFFF00," .. fgettext("Previous Contributors") .. ",," ..
			buildCreditList(previous_contributors) .. "," ..
			";1]"
	end
}


code:
--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 current_game()
	local last_game_id = core.settings:get("menu_last_game")
	local game, index = gamemgr.find_by_gameid(last_game_id)

	return game
end

local function singleplayer_refresh_gamebar()

	local old_bar = ui.find_by_name("game_button_bar")

	if old_bar ~= nil then
		old_bar:delete()
	end

	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)
					local index = filterlist.get_current_index(menudata.worldlist,
						tonumber(core.settings:get("mainmenu_last_selected_world")))
					if not index or index < 1 then
						local selected = core.get_textlist_index("sp_worlds")
						if selected ~= nil and selected < #menudata.worldlist:get_list() then
							index = selected
						else
							index = #menudata.worldlist:get_list()
						end
					end
					menu_worldmt_legacy(index)
					return true
				end
			end
		end
	end

	local btnbar = buttonbar_create("game_button_bar",
		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

		local image = nil
		local text = nil
		local tooltip = core.formspec_escape(gamemgr.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)
		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)

			text = part1 .. "\n" .. part2
			if part3 ~= nil and
				part3 ~= "" then
				text = text .. "\n" .. part3
			end
		end
		btnbar:add_button(btn_name, text, image, tooltip)
	end
end

local function get_formspec(tabview, name, tabdata)
	local retval = ""

	local index = filterlist.get_current_index(menudata.worldlist,
				tonumber(core.settings:get("mainmenu_last_selected_world"))
				)

	retval = retval ..
			"button[4,4.15;2.6,0.5;world_delete;".. fgettext("Delete") .. "]" ..
			"button[6.5,4.15;2.8,0.5;world_create;".. fgettext("New") .. "]" ..
			"button[9.2,4.15;2.55,0.5;world_configure;".. fgettext("Configure") .. "]" ..
			"label[4,-0.25;".. fgettext("Select World:") .. "]"..
			"checkbox[0.25,0.25;cb_creative_mode;".. fgettext("Creative Mode") .. ";" ..
			dump(core.settings:get_bool("creative_mode")) .. "]"..
			"checkbox[0.25,0.7;cb_enable_damage;".. fgettext("Enable Damage") .. ";" ..
			dump(core.settings:get_bool("enable_damage")) .. "]"..
			"checkbox[0.25,1.15;cb_server;".. fgettext("Host Server") ..";" ..
			dump(core.settings:get_bool("enable_server")) .. "]" ..
			"textlist[4,0.25;7.5,3.7;sp_worlds;" ..
			menu_render_worldlist() ..
			";" .. index .. "]"

	if core.settings:get_bool("enable_server") then
		retval = retval ..
				"button[8.5,5;3.25,0.5;play;".. fgettext("Host Game") .. "]" ..
				"checkbox[0.25,1.6;cb_server_announce;" .. fgettext("Announce Server") .. ";" ..
				dump(core.settings:get_bool("server_announce")) .. "]" ..
				"label[0.25,2.2;" .. fgettext("Name/Password") .. "]" ..
				"field[0.55,3.2;3.5,0.5;te_playername;;" ..
				core.formspec_escape(core.settings:get("name")) .. "]" ..
				"pwdfield[0.55,4;3.5,0.5;te_passwd;]"

		local bind_addr = core.settings:get("bind_address")
		if bind_addr ~= nil and bind_addr ~= "" then
			retval = retval ..
				"field[0.55,5.2;2.25,0.5;te_serveraddr;" .. fgettext("Bind Address") .. ";" ..
				core.formspec_escape(core.settings:get("bind_address")) .. "]" ..
				"field[2.8,5.2;1.25,0.5;te_serverport;" .. fgettext("Port") .. ";" ..
				core.formspec_escape(core.settings:get("port")) .. "]"
		else
			retval = retval ..
				"field[0.55,5.2;3.5,0.5;te_serverport;" .. fgettext("Server Port") .. ";" ..
				core.formspec_escape(core.settings:get("port")) .. "]"
		end
	else
		retval = retval ..
				"button[8.5,5;3.25,0.5;play;".. fgettext("Play Game") .. "]"
	end

	return retval
end

local function main_button_handler(this, fields, name, tabdata)

	assert(name == "local")

	local world_doubleclick = false

	if fields["sp_worlds"] ~= nil then
		local event = core.explode_textlist_event(fields["sp_worlds"])
		local selected = core.get_textlist_index("sp_worlds")

		menu_worldmt_legacy(selected)

		if event.type == "DCL" then
			world_doubleclick = true
		end

		if event.type == "CHG" and selected ~= nil then
			core.settings:set("mainmenu_last_selected_world",
				menudata.worldlist:get_raw_index(selected))
			return true
		end
	end

	if menu_handle_key_up_down(fields,"sp_worlds","mainmenu_last_selected_world") then
		return true
	end

	if fields["cb_creative_mode"] then
		core.settings:set("creative_mode", fields["cb_creative_mode"])
		local selected = core.get_textlist_index("sp_worlds")
		menu_worldmt(selected, "creative_mode", fields["cb_creative_mode"])

		return true
	end

	if fields["cb_enable_damage"] then
		core.settings:set("enable_damage", fields["cb_enable_damage"])
		local selected = core.get_textlist_index("sp_worlds")
		menu_worldmt(selected, "enable_damage", fields["cb_enable_damage"])

		return true
	end

	if fields["cb_server"] then
		core.settings:set("enable_server", fields["cb_server"])

		return true
	end

	if fields["cb_server_announce"] then
		core.settings:set("server_announce", fields["cb_server_announce"])
		local selected = core.get_textlist_index("srv_worlds")
		menu_worldmt(selected, "server_announce", fields["cb_server_announce"])

		return true
	end

	if fields["play"] ~= nil or world_doubleclick or fields["key_enter"] then
		local selected = core.get_textlist_index("sp_worlds")
		gamedata.selected_world = menudata.worldlist:get_raw_index(selected)

		if core.settings:get_bool("enable_server") then
			if selected ~= nil and gamedata.selected_world ~= 0 then
				gamedata.playername     = fields["te_playername"]
				gamedata.password       = fields["te_passwd"]
				gamedata.port           = fields["te_serverport"]
				gamedata.address        = ""

				core.settings:set("port",gamedata.port)
				if fields["te_serveraddr"] ~= nil then
					core.settings:set("bind_address",fields["te_serveraddr"])
				end

				--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)
					core.settings:set("menu_last_game", game.id)
				end

				core.start()
			else
				gamedata.errormessage =
					fgettext("No world created or selected!")
			end
		else
			if selected ~= nil and gamedata.selected_world ~= 0 then
				gamedata.singleplayer = true
				core.start()
			else
				gamedata.errormessage =
					fgettext("No world created or selected!")
			end
			return true
		end
	end

	if fields["world_create"] ~= nil then
		local create_world_dlg = create_create_world_dlg(true)
		create_world_dlg:set_parent(this)
		this:hide()
		create_world_dlg:show()
		mm_texture.update("singleplayer",current_game())
		return true
	end

	if fields["world_delete"] ~= nil then
		local selected = core.get_textlist_index("sp_worlds")
		if selected ~= nil and
			selected <= menudata.worldlist:size() then
			local world = menudata.worldlist:get_list()[selected]
			if world ~= nil and
				world.name ~= nil and
				world.name ~= "" then
				local index = menudata.worldlist:get_raw_index(selected)
				local delete_world_dlg = create_delete_world_dlg(world.name,index)
				delete_world_dlg:set_parent(this)
				this:hide()
				delete_world_dlg:show()
				mm_texture.update("singleplayer",current_game())
			end
		end

		return true
	end

	if fields["world_configure"] ~= nil then
		local selected = core.get_textlist_index("sp_worlds")
		if selected ~= nil then
			local configdialog =
				create_configure_world_dlg(
						menudata.worldlist:get_raw_index(selected))

			if (configdialog ~= nil) then
				configdialog:set_parent(this)
				this:hide()
				configdialog:show()
				mm_texture.update("singleplayer",current_game())
			end
		end

		return true
	end
end

local function on_change(type, old_tab, new_tab)
	local buttonbar = ui.find_by_name("game_button_bar")

	if ( buttonbar == nil ) then
		singleplayer_refresh_gamebar()
		buttonbar = ui.find_by_name("game_button_bar")
	end

	if (type == "ENTER") then
		local game = current_game()

		if game then
			menudata.worldlist:set_filtercriteria(game.id)
			core.set_topleft_text(game.name)
			mm_texture.update("singleplayer",game)
		end
		buttonbar:show()
	else
		menudata.worldlist:set_filtercriteria(nil)
		buttonbar:hide()
		core.set_topleft_text("")
		mm_texture.update(new_tab,nil)
	end
end

--------------------------------------------------------------------------------
return {
	name = "local",
	caption = fgettext("Local Game"),
	cbf_formspec = get_formspec,
	cbf_button_handler = main_button_handler,
	on_change = on_change
}

repo_name: pgimeno/minetest

license: unknown


code:
--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 .. "]"

	retval = retval ..
--		"label[0.8,4.2;" .. fgettext("Add mod:") .. "]" ..
--		TODO Disabled due to upcoming release 0.4.8 and irrlicht messing up localization
--		"button[0.75,4.85;1.8,0.5;btn_mod_mgr_install_local;".. fgettext("Local install") .. "]" ..

--		TODO Disabled due to service being offline, and not likely to come online again, in this form
--		"button[0,4.85;5.25,0.5;btn_modstore;".. fgettext("Online mod repository") .. "]"
		""

	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
		local modscreenshot = nil

		--check for screenshot beeing available
		local screenshotfilename = selected_mod.path .. DIR_DELIM .. "screenshot.png"
		local error = nil
		local screenshotfile,error = io.open(screenshotfilename,"r")
		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 .. "]"

		local descriptionlines = nil
		error = nil
		local descriptionfilename = selected_mod.path .. "description.txt"
		local descriptionfile,error = io.open(descriptionfilename,"r")
		if error == nil then
			local descriptiontext = descriptionfile:read("*all")

			descriptionlines = core.wrap_text(descriptiontext, 42, true)
			descriptionfile:close()
		else
			descriptionlines = {}
			descriptionlines[#descriptionlines + 1] = fgettext("No mod description available")
		end

		retval = retval ..
			"label[5.5,1.7;".. fgettext("Mod information:") .. "]" ..
			"textlist[5.5,2.2;6.2,2.4;description;"

		for i=1,#descriptionlines,1 do
			retval = retval .. core.formspec_escape(descriptionlines[i]) .. ","
		end

		if selected_mod.is_modpack then
			retval = retval .. ";0]" ..
				"button[10,4.85;2,0.5;btn_mod_mgr_rename_modpack;" ..
				fgettext("Rename") .. "]"
			retval = retval .. "button[5.5,4.85;4.5,0.5;btn_mod_mgr_delete_mod;"
				.. fgettext("Uninstall selected modpack") .. "]"
		else
			--show dependencies
			local toadd_hard, toadd_soft = modmgr.get_dependencies(selected_mod.path)
			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.85;4.5,0.5;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_modstore"] ~= nil then
		local modstore_ui = ui.find_by_name("modstore")
		if modstore_ui ~= nil then
			tabview:hide()
			modstore.update_modlist()
			modstore_ui:show()
		else
			print("modstore ui element not found")
		end
		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
}


code:
--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)
	-- Update the cached supported proto info,
	-- it may have changed after a change by the settings menu.
	common_update_cached_supp_proto()
	local fav_selected = nil
	if menudata.search_result then
		fav_selected = menudata.search_result[tabdata.fav_selected]
	else
		fav_selected = menudata.favorites[tabdata.fav_selected]
	end

	if not tabdata.search_for then
		tabdata.search_for = ""
	end

	local retval =
		-- Search
		"field[0.15,0.35;6.05,0.27;te_search;;"..core.formspec_escape(tabdata.search_for).."]"..
		"button[5.8,0.1;2,0.1;btn_mp_search;" .. fgettext("Search") .. "]" ..

		-- Address / Port
		"label[7.75,-0.25;" .. fgettext("Address / Port") .. "]" ..
		"field[8,0.65;3.25,0.5;te_address;;" ..
			core.formspec_escape(core.settings:get("address")) .. "]" ..
		"field[11.1,0.65;1.4,0.5;te_port;;" ..
			core.formspec_escape(core.settings:get("remote_port")) .. "]" ..

		-- Name / Password
		"label[7.75,0.95;" .. fgettext("Name / Password") .. "]" ..
		"field[8,1.85;2.9,0.5;te_name;;" ..
			core.formspec_escape(core.settings:get("name")) .. "]" ..
		"pwdfield[10.73,1.85;1.77,0.5;te_pwd;]" ..

		-- Description Background
		"box[7.73,2.25;4.25,2.6;#999999]"..

		-- Connect
		"button[10.1,5.15;2,0.5;btn_mp_connect;" .. fgettext("Connect") .. "]"

	if tabdata.fav_selected and fav_selected then
		if gamedata.fav then
			retval = retval .. "button[7.75,5.15;2.3,0.5;btn_delete_favorite;" ..
				fgettext("Del. Favorite") .. "]"
		end
		if fav_selected.description then
			retval = retval .. "textarea[8.1,2.3;4.23,2.9;;" ..
				core.formspec_escape((gamedata.serverdescription or ""), true) .. ";]"
		end
	end

	--favourites
	retval = retval .. "tablecolumns[" ..
		image_column(fgettext("Favorite"), "favorite") .. ";" ..
		image_column(fgettext("Ping")) .. ",padding=0.25;" ..
		"color,span=3;" ..
		"text,align=right;" ..                -- clients
		"text,align=center,padding=0.25;" ..  -- "/"
		"text,align=right,padding=0.25;" ..   -- clients_max
		image_column(fgettext("Creative mode"), "creative") .. ",padding=1;" ..
		image_column(fgettext("Damage enabled"), "damage") .. ",padding=0.25;" ..
		image_column(fgettext("PvP enabled"), "pvp") .. ",padding=0.25;" ..
		"color,span=1;" ..
		"text,padding=1]" ..
		"table[-0.15,0.6;7.75,5.15;favourites;"

	if menudata.search_result then
		for i = 1, #menudata.search_result do
			local favs = core.get_favorites("local")
			local server = menudata.search_result[i]

			for fav_id = 1, #favs do
				if server.address == favs[fav_id].address and
						server.port == favs[fav_id].port then
					server.is_favorite = true
				end
			end

			if i ~= 1 then
				retval = retval .. ","
			end

			retval = retval .. render_serverlist_row(server, server.is_favorite)
		end
	elseif #menudata.favorites > 0 then
		local favs = core.get_favorites("local")
		if #favs > 0 then
			for i = 1, #favs do
			for j = 1, #menudata.favorites do
				if menudata.favorites[j].address == favs[i].address and
						menudata.favorites[j].port == favs[i].port then
					table.insert(menudata.favorites, i, table.remove(menudata.favorites, j))
				end
			end
				if favs[i].address ~= menudata.favorites[i].address then
					table.insert(menudata.favorites, i, favs[i])
				end
			end
		end
		retval = retval .. render_serverlist_row(menudata.favorites[1], (#favs > 0))
		for i = 2, #menudata.favorites do
			retval = retval .. "," .. render_serverlist_row(menudata.favorites[i], (i <= #favs))
		end
	end

	if tabdata.fav_selected then
		retval = retval .. ";" .. tabdata.fav_selected .. "]"
	else
		retval = retval .. ";0]"
	end

	return retval
end

--------------------------------------------------------------------------------
local function main_button_handler(tabview, fields, name, tabdata)
	local serverlist = menudata.search_result or menudata.favorites

	if fields.te_name then
		gamedata.playername = fields.te_name
		core.settings:set("name", fields.te_name)
	end

	if fields.favourites then
		local event = core.explode_table_event(fields.favourites)
		local fav = serverlist[event.row]

		if event.type == "DCL" then
			if event.row <= #serverlist then
				if menudata.favorites_is_public and
						not is_server_protocol_compat_or_error(
							fav.proto_min, fav.proto_max) then
					return true
				end

				gamedata.address    = fav.address
				gamedata.port       = fav.port
				gamedata.playername = fields.te_name
				gamedata.selected_world = 0

				if fields.te_pwd then
					gamedata.password = fields.te_pwd
				end

				gamedata.servername        = fav.name
				gamedata.serverdescription = fav.description

				if gamedata.address and gamedata.port then
					core.settings:set("address", gamedata.address)
					core.settings:set("remote_port", gamedata.port)
					core.start()
				end
			end
			return true
		end

		if event.type == "CHG" then
			if event.row <= #serverlist then
				gamedata.fav = false
				local favs = core.get_favorites("local")
				local address = fav.address
				local port    = fav.port
				gamedata.serverdescription = fav.description

				for i = 1, #favs do
					if fav.address == favs[i].address and
							fav.port == favs[i].port then
						gamedata.fav = true
					end
				end

				if address and port then
					core.settings:set("address", address)
					core.settings:set("remote_port", port)
				end
				tabdata.fav_selected = event.row
			end
			return true
		end
	end

	if fields.key_up or fields.key_down then
		local fav_idx = core.get_table_index("favourites")
		local fav = serverlist[fav_idx]

		if fav_idx then
			if fields.key_up and fav_idx > 1 then
				fav_idx = fav_idx - 1
			elseif fields.key_down and fav_idx < #menudata.favorites then
				fav_idx = fav_idx + 1
			end
		else
			fav_idx = 1
		end

		if not menudata.favorites or not fav then
			tabdata.fav_selected = 0
			return true
		end

		local address = fav.address
		local port    = fav.port
		gamedata.serverdescription = fav.description
		if address and port then
			core.settings:set("address", address)
			core.settings:set("remote_port", port)
		end

		tabdata.fav_selected = fav_idx
		return true
	end

	if fields.btn_delete_favorite then
		local current_favourite = core.get_table_index("favourites")
		if not current_favourite then return end

		core.delete_favorite(current_favourite)
		asyncOnlineFavourites()
		tabdata.fav_selected = nil

		core.settings:set("address", "")
		core.settings:set("remote_port", "30000")
		return true
	end

	if fields.btn_mp_search or fields.key_enter_field == "te_search" then
		tabdata.fav_selected = 1
		local input = fields.te_search:lower()
		tabdata.search_for = fields.te_search

		if #menudata.favorites < 2 then
			return true
		end

		menudata.search_result = {}

		-- setup the keyword list
		local keywords = {}
		for word in input:gmatch("%S+") do
			table.insert(keywords, word)
		end

		if #keywords == 0 then
			menudata.search_result = nil
			return true
		end

		-- Search the serverlist
		local search_result = {}
		for i = 1, #menudata.favorites do
			local server = menudata.favorites[i]
			local found = 0
			for k = 1, #keywords do
				local keyword = keywords[k]
				if server.name then
					local name = server.name:lower()
					local _, count = name:gsub(keyword, keyword)
					found = found + count * 4
				end

				if server.description then
					local desc = server.description:lower()
					local _, count = desc:gsub(keyword, keyword)
					found = found + count * 2
				end
			end
			if found > 0 then
				local points = (#menudata.favorites - i) / 5 + found
				server.points = points
				table.insert(search_result, server)
			end
		end
		if #search_result > 0 then
			table.sort(search_result, function(a, b)
				return a.points > b.points
			end)
			menudata.search_result = search_result
			local first_server = search_result[1]
			core.settings:set("address",     first_server.address)
			core.settings:set("remote_port", first_server.port)
		end
		return true
	end

	if (fields.btn_mp_connect or fields.key_enter)
			and fields.te_address ~= "" and fields.te_port then
		gamedata.playername = fields.te_name
		gamedata.password   = fields.te_pwd
		gamedata.address    = fields.te_address
		gamedata.port       = fields.te_port
		gamedata.selected_world = 0
		local fav_idx = core.get_table_index("favourites")
		local fav = serverlist[fav_idx]

		if fav_idx and fav_idx <= #serverlist and
				fav.address == fields.te_address and
				fav.port    == fields.te_port then

			gamedata.servername        = fav.name
			gamedata.serverdescription = fav.description

			if menudata.favorites_is_public and
					not is_server_protocol_compat_or_error(
						fav.proto_min, fav.proto_max) then
				return true
			end
		else
			gamedata.servername        = ""
			gamedata.serverdescription = ""
		end

		core.settings:set("address",     fields.te_address)
		core.settings:set("remote_port", fields.te_port)

		core.start()
		return true
	end
	return false
end

local function on_change(type, old_tab, new_tab)
	if type == "LEAVE" then return end
	asyncOnlineFavourites()
end

--------------------------------------------------------------------------------
return {
	name = "online",
	caption = fgettext("Play Online"),
	cbf_formspec = get_formspec,
	cbf_button_handler = main_button_handler,
	on_change = on_change
}


code:
--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.

--------------------------------------------------------------------------------

local labels = {
	leaves = {
		fgettext("Opaque Leaves"),
		fgettext("Simple Leaves"),
		fgettext("Fancy Leaves")
	},
	node_highlighting = {
		fgettext("Node Outlining"),
		fgettext("Node Highlighting"),
		fgettext("None")
	},
	filters = {
		fgettext("No Filter"),
		fgettext("Bilinear Filter"),
		fgettext("Trilinear Filter")
	},
	mipmap = {
		fgettext("No Mipmap"),
		fgettext("Mipmap"),
		fgettext("Mipmap + Aniso. Filter")
	},
	antialiasing = {
		fgettext("None"),
		fgettext("2x"),
		fgettext("4x"),
		fgettext("8x")
	}
}

local dd_options = {
	leaves = {
		table.concat(labels.leaves, ","),
		{"opaque", "simple", "fancy"}
	},
	node_highlighting = {
		table.concat(labels.node_highlighting, ","),
		{"box", "halo", "none"}
	},
	filters = {
		table.concat(labels.filters, ","),
		{"", "bilinear_filter", "trilinear_filter"}
	},
	mipmap = {
		table.concat(labels.mipmap, ","),
		{"", "mip_map", "anisotropic_filter"}
	},
	antialiasing = {
		table.concat(labels.antialiasing, ","),
		{"0", "2", "4", "8"}
	}
}

local getSettingIndex = {
	Leaves = function()
		local style = core.settings:get("leaves_style")
		for idx, name in pairs(dd_options.leaves[2]) do
			if style == name then return idx end
		end
		return 1
	end,
	NodeHighlighting = function()
		local style = core.settings:get("node_highlighting")
		for idx, name in pairs(dd_options.node_highlighting[2]) do
			if style == name then return idx end
		end
		return 1
	end,
	Filter = function()
		if core.settings:get(dd_options.filters[2][3]) == "true" then
			return 3
		elseif core.settings:get(dd_options.filters[2][3]) == "false" and
				core.settings:get(dd_options.filters[2][2]) == "true" then
			return 2
		end
		return 1
	end,
	Mipmap = function()
		if core.settings:get(dd_options.mipmap[2][3]) == "true" then
			return 3
		elseif core.settings:get(dd_options.mipmap[2][3]) == "false" and
				core.settings:get(dd_options.mipmap[2][2]) == "true" then
			return 2
		end
		return 1
	end,
	Antialiasing = function()
		local antialiasing_setting = core.settings:get("fsaa")
		for i = 1, #dd_options.antialiasing[2] do
			if antialiasing_setting == dd_options.antialiasing[2][i] then
				return i
			end
		end
		return 1
	end
}

local function antialiasing_fname_to_name(fname)
	for i = 1, #labels.antialiasing do
		if fname == labels.antialiasing[i] then
			return dd_options.antialiasing[2][i]
		end
	end
	return 0
end

local function dlg_confirm_reset_formspec(data)
	return  "size[8,3]" ..
		"label[1,1;" .. fgettext("Are you sure to reset your singleplayer world?") .. "]" ..
		"button[1,2;2.6,0.5;dlg_reset_singleplayer_confirm;" .. fgettext("Yes") .. "]" ..
		"button[4,2;2.8,0.5;dlg_reset_singleplayer_cancel;" .. fgettext("No") .. "]"
end

local function dlg_confirm_reset_btnhandler(this, fields, dialogdata)

	if fields["dlg_reset_singleplayer_confirm"] ~= nil then
		local worldlist = core.get_worlds()
		local found_singleplayerworld = false

		for i = 1, #worldlist do
			if worldlist[i].name == "singleplayerworld" then
				found_singleplayerworld = true
				gamedata.worldindex = i
			end
		end

		if found_singleplayerworld then
			core.delete_world(gamedata.worldindex)
		end

		core.create_world("singleplayerworld", 1)
		worldlist = core.get_worlds()
		found_singleplayerworld = false

		for i = 1, #worldlist do
			if worldlist[i].name == "singleplayerworld" then
				found_singleplayerworld = true
				gamedata.worldindex = i
			end
		end
	end

	this.parent:show()
	this:hide()
	this:delete()
	return true
end

local function showconfirm_reset(tabview)
	local new_dlg = dialog_create("reset_spworld",
		dlg_confirm_reset_formspec,
		dlg_confirm_reset_btnhandler,
		nil)
	new_dlg:set_parent(tabview)
	tabview:hide()
	new_dlg:show()
end

local function formspec(tabview, name, tabdata)
	local tab_string =
		"box[0,0;3.75,4.5;#999999]" ..
		"checkbox[0.25,0;cb_smooth_lighting;" .. fgettext("Smooth Lighting") .. ";"
				.. dump(core.settings:get_bool("smooth_lighting")) .. "]" ..
		"checkbox[0.25,0.5;cb_particles;" .. fgettext("Particles") .. ";"
				.. dump(core.settings:get_bool("enable_particles")) .. "]" ..
		"checkbox[0.25,1;cb_3d_clouds;" .. fgettext("3D Clouds") .. ";"
				.. dump(core.settings:get_bool("enable_3d_clouds")) .. "]" ..
		"checkbox[0.25,1.5;cb_opaque_water;" .. fgettext("Opaque Water") .. ";"
				.. dump(core.settings:get_bool("opaque_water")) .. "]" ..
		"checkbox[0.25,2.0;cb_connected_glass;" .. fgettext("Connected Glass") .. ";"
				.. dump(core.settings:get_bool("connected_glass")) .. "]" ..
		"dropdown[0.25,2.8;3.5;dd_node_highlighting;" .. dd_options.node_highlighting[1] .. ";"
				.. getSettingIndex.NodeHighlighting() .. "]" ..
		"dropdown[0.25,3.6;3.5;dd_leaves_style;" .. dd_options.leaves[1] .. ";"
				.. getSettingIndex.Leaves() .. "]" ..
		"box[4,0;3.75,4.5;#999999]" ..
		"label[4.25,0.1;" .. fgettext("Texturing:") .. "]" ..
		"dropdown[4.25,0.55;3.5;dd_filters;" .. dd_options.filters[1] .. ";"
				.. getSettingIndex.Filter() .. "]" ..
		"dropdown[4.25,1.35;3.5;dd_mipmap;" .. dd_options.mipmap[1] .. ";"
				.. getSettingIndex.Mipmap() .. "]" ..
		"label[4.25,2.15;" .. fgettext("Antialiasing:") .. "]" ..
		"dropdown[4.25,2.6;3.5;dd_antialiasing;" .. dd_options.antialiasing[1] .. ";"
				.. getSettingIndex.Antialiasing() .. "]" ..
		"label[4.25,3.45;" .. fgettext("Screen:") .. "]" ..
		"checkbox[4.25,3.6;cb_autosave_screensize;" .. fgettext("Autosave screen size") .. ";"
				.. dump(core.settings:get_bool("autosave_screensize")) .. "]" ..
		"box[8,0;3.75,4.5;#999999]" ..
		"checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders") .. ";"
				.. dump(core.settings:get_bool("enable_shaders")) .. "]"

	if PLATFORM == "Android" then
		tab_string = tab_string ..
			"button[8,4.75;4.1,1;btn_reset_singleplayer;"
			.. fgettext("Reset singleplayer world") .. "]"
	else
		tab_string = tab_string ..
			"button[8,4.75;4,1;btn_change_keys;"
			.. fgettext("Change keys") .. "]"
	end

	tab_string = tab_string ..
		"button[0,4.75;4,1;btn_advanced_settings;"
		.. fgettext("Advanced Settings") .. "]"

	if core.settings:get("touchscreen_threshold") ~= nil then
		tab_string = tab_string ..
			"label[4.3,4.1;" .. fgettext("Touchthreshold (px)") .. "]" ..
			"dropdown[3.85,4.55;3.85;dd_touchthreshold;0,10,20,30,40,50;" ..
			((tonumber(core.settings:get("touchscreen_threshold")) / 10) + 1) .. "]"
	end

	if core.settings:get_bool("enable_shaders") then
		tab_string = tab_string ..
			"checkbox[8.25,0.5;cb_bumpmapping;" .. fgettext("Bump Mapping") .. ";"
					.. dump(core.settings:get_bool("enable_bumpmapping")) .. "]" ..
			"checkbox[8.25,1;cb_tonemapping;" .. fgettext("Tone Mapping") .. ";"
					.. dump(core.settings:get_bool("tone_mapping")) .. "]" ..
			"checkbox[8.25,1.5;cb_generate_normalmaps;" .. fgettext("Normal Mapping") .. ";"
					.. dump(core.settings:get_bool("generate_normalmaps")) .. "]" ..
			"checkbox[8.25,2;cb_parallax;" .. fgettext("Parallax Occlusion") .. ";"
					.. dump(core.settings:get_bool("enable_parallax_occlusion")) .. "]" ..
			"checkbox[8.25,2.5;cb_waving_water;" .. fgettext("Waving Water") .. ";"
					.. dump(core.settings:get_bool("enable_waving_water")) .. "]" ..
			"checkbox[8.25,3;cb_waving_leaves;" .. fgettext("Waving Leaves") .. ";"
					.. dump(core.settings:get_bool("enable_waving_leaves")) .. "]" ..
			"checkbox[8.25,3.5;cb_waving_plants;" .. fgettext("Waving Plants") .. ";"
					.. dump(core.settings:get_bool("enable_waving_plants")) .. "]"
	else
		tab_string = tab_string ..
			"tablecolumns[color;text]" ..
			"tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
			"table[8.33,0.7;3.5,4;shaders;" ..
				"#888888," .. fgettext("Bump Mapping") .. "," ..
				"#888888," .. fgettext("Tone Mapping") .. "," ..
				"#888888," .. fgettext("Normal Mapping") .. "," ..
				"#888888," .. fgettext("Parallax Occlusion") .. "," ..
				"#888888," .. fgettext("Waving Water") .. "," ..
				"#888888," .. fgettext("Waving Leaves") .. "," ..
				"#888888," .. fgettext("Waving Plants") .. "," ..
				";1]"
	end

	return tab_string
end

--------------------------------------------------------------------------------
local function handle_settings_buttons(this, fields, tabname, tabdata)

	if fields["btn_advanced_settings"] ~= nil then
		local adv_settings_dlg = create_adv_settings_dlg()
		adv_settings_dlg:set_parent(this)
		this:hide()
		adv_settings_dlg:show()
		--mm_texture.update("singleplayer", current_game())
		return true
	end
	if fields["cb_smooth_lighting"] then
		core.settings:set("smooth_lighting", fields["cb_smooth_lighting"])
		return true
	end
	if fields["cb_particles"] then
		core.settings:set("enable_particles", fields["cb_particles"])
		return true
	end
	if fields["cb_3d_clouds"] then
		core.settings:set("enable_3d_clouds", fields["cb_3d_clouds"])
		return true
	end
	if fields["cb_opaque_water"] then
		core.settings:set("opaque_water", fields["cb_opaque_water"])
		return true
	end
	if fields["cb_connected_glass"] then
		core.settings:set("connected_glass", fields["cb_connected_glass"])
		return true
	end
	if fields["cb_autosave_screensize"] then
		core.settings:set("autosave_screensize", fields["cb_autosave_screensize"])
		return true
	end
	if fields["cb_shaders"] then
		if (core.settings:get("video_driver") == "direct3d8" or
				core.settings:get("video_driver") == "direct3d9") then
			core.settings:set("enable_shaders", "false")
			gamedata.errormessage = fgettext("To enable shaders the OpenGL driver needs to be used.")
		else
			core.settings:set("enable_shaders", fields["cb_shaders"])
		end
		return true
	end
	if fields["cb_bumpmapping"] then
		core.settings:set("enable_bumpmapping", fields["cb_bumpmapping"])
		return true
	end
	if fields["cb_tonemapping"] then
		core.settings:set("tone_mapping", fields["cb_tonemapping"])
		return true
	end
	if fields["cb_generate_normalmaps"] then
		core.settings:set("generate_normalmaps", fields["cb_generate_normalmaps"])
		return true
	end
	if fields["cb_parallax"] then
		core.settings:set("enable_parallax_occlusion", fields["cb_parallax"])
		return true
	end
	if fields["cb_waving_water"] then
		core.settings:set("enable_waving_water", fields["cb_waving_water"])
		return true
	end
	if fields["cb_waving_leaves"] then
		core.settings:set("enable_waving_leaves", fields["cb_waving_leaves"])
	end
	if fields["cb_waving_plants"] then
		core.settings:set("enable_waving_plants", fields["cb_waving_plants"])
		return true
	end
	if fields["btn_change_keys"] then
		core.show_keys_menu()
		return true
	end
	if fields["cb_touchscreen_target"] then
		core.settings:set("touchtarget", fields["cb_touchscreen_target"])
		return true
	end
	if fields["btn_reset_singleplayer"] then
		showconfirm_reset(this)
		return true
	end

	--Note dropdowns have to be handled LAST!
	local ddhandled = false

	for i = 1, #labels.leaves do
		if fields["dd_leaves_style"] == labels.leaves[i] then
			core.settings:set("leaves_style", dd_options.leaves[2][i])
			ddhandled = true
		end
	end
	for i = 1, #labels.node_highlighting do
		if fields["dd_node_highlighting"] == labels.node_highlighting[i] then
			core.settings:set("node_highlighting", dd_options.node_highlighting[2][i])
			ddhandled = true
		end
	end
	if fields["dd_filters"] == labels.filters[1] then
		core.settings:set("bilinear_filter", "false")
		core.settings:set("trilinear_filter", "false")
		ddhandled = true
	elseif fields["dd_filters"] == labels.filters[2] then
		core.settings:set("bilinear_filter", "true")
		core.settings:set("trilinear_filter", "false")
		ddhandled = true
	elseif fields["dd_filters"] == labels.filters[3] then
		core.settings:set("bilinear_filter", "false")
		core.settings:set("trilinear_filter", "true")
		ddhandled = true
	end
	if fields["dd_mipmap"] == labels.mipmap[1] then
		core.settings:set("mip_map", "false")
		core.settings:set("anisotropic_filter", "false")
		ddhandled = true
	elseif fields["dd_mipmap"] == labels.mipmap[2] then
		core.settings:set("mip_map", "true")
		core.settings:set("anisotropic_filter", "false")
		ddhandled = true
	elseif fields["dd_mipmap"] == labels.mipmap[3] then
		core.settings:set("mip_map", "true")
		core.settings:set("anisotropic_filter", "true")
		ddhandled = true
	end
	if fields["dd_antialiasing"] then
		core.settings:set("fsaa",
			antialiasing_fname_to_name(fields["dd_antialiasing"]))
		ddhandled = true
	end
	if fields["dd_touchthreshold"] then
		core.settings:set("touchscreen_threshold", fields["dd_touchthreshold"])
		ddhandled = true
	end

	return ddhandled
end

return {
	name = "settings",
	caption = fgettext("Settings"),
	cbf_formspec = formspec,
	cbf_button_handler = handle_settings_buttons
}


code:
--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.

--------------------------------------------------------------------------------
local function get_formspec(tabview, name, tabdata)
	-- Update the cached supported proto info,
	-- it may have changed after a change by the settings menu.
	common_update_cached_supp_proto()
	local fav_selected = menudata.favorites[tabdata.fav_selected]

	local retval =
		"label[9.5,0;".. fgettext("Name / Password") .. "]" ..
		"field[0.25,3.35;5.5,0.5;te_address;;" ..
			core.formspec_escape(core.settings:get("address")) .."]" ..
		"field[5.75,3.35;2.25,0.5;te_port;;" ..
			core.formspec_escape(core.settings:get("remote_port")) .."]" ..
		"button[10,2.6;2,1.5;btn_mp_connect;".. fgettext("Connect") .. "]" ..
		"field[9.8,1;2.6,0.5;te_name;;" ..
			core.formspec_escape(core.settings:get("name")) .."]" ..
		"pwdfield[9.8,2;2.6,0.5;te_pwd;]"

	if tabdata.fav_selected and fav_selected then
		if gamedata.fav then
			retval = retval .. "button[7.7,2.6;2.3,1.5;btn_delete_favorite;" ..
				fgettext("Del. Favorite") .. "]"
		end
	end

	retval = retval .. "tablecolumns[" ..
		image_column(fgettext("Favorite"), "favorite") .. ";" ..
		image_column(fgettext("Ping"), "") .. ",padding=0.25;" ..
		"color,span=3;" ..
		"text,align=right;" ..                -- clients
		"text,align=center,padding=0.25;" ..  -- "/"
		"text,align=right,padding=0.25;" ..   -- clients_max
		image_column(fgettext("Creative mode"), "creative") .. ",padding=1;" ..
		image_column(fgettext("Damage enabled"), "damage") .. ",padding=0.25;" ..
		image_column(fgettext("PvP enabled"), "pvp") .. ",padding=0.25;" ..
		"color,span=1;" ..
		"text,padding=1]" ..                  -- name
		"table[-0.05,0;9.2,2.75;favourites;"

	if #menudata.favorites > 0 then
		local favs = core.get_favorites("local")
		if #favs > 0 then
			for i = 1, #favs do
				for j = 1, #menudata.favorites do
					if menudata.favorites[j].address == favs[i].address and
							menudata.favorites[j].port == favs[i].port then
						table.insert(menudata.favorites, i,
							table.remove(menudata.favorites, j))
					end
				end
				if favs[i].address ~= menudata.favorites[i].address then
					table.insert(menudata.favorites, i, favs[i])
				end
			end
		end
		retval = retval .. render_serverlist_row(menudata.favorites[1], (#favs > 0))
		for i = 2, #menudata.favorites do
			retval = retval .. "," .. render_serverlist_row(menudata.favorites[i], (i <= #favs))
		end
	end

	if tabdata.fav_selected then
		retval = retval .. ";" .. tabdata.fav_selected .. "]"
	else
		retval = retval .. ";0]"
	end

	-- separator
	retval = retval .. "box[-0.28,3.75;12.4,0.1;#FFFFFF]"

	-- checkboxes
	retval = retval ..
		"checkbox[8.0,3.9;cb_creative;".. fgettext("Creative Mode") .. ";" ..
			dump(core.settings:get_bool("creative_mode")) .. "]"..
		"checkbox[8.0,4.4;cb_damage;".. fgettext("Enable Damage") .. ";" ..
			dump(core.settings:get_bool("enable_damage")) .. "]"
	-- buttons
	retval = retval ..
		"button[0,3.7;8,1.5;btn_start_singleplayer;" .. fgettext("Start Singleplayer") .. "]" ..
		"button[0,4.5;8,1.5;btn_config_sp_world;" .. fgettext("Config mods") .. "]"

	return retval
end

--------------------------------------------------------------------------------
local function main_button_handler(tabview, fields, name, tabdata)
	if fields.btn_start_singleplayer then
		gamedata.selected_world	= gamedata.worldindex
		gamedata.singleplayer	= true
		core.start()
		return true
	end

	if fields.favourites then
		local event = core.explode_table_event(fields.favourites)
		if event.type == "CHG" then
			if event.row <= #menudata.favorites then
				gamedata.fav = false
				local favs = core.get_favorites("local")
				local fav = menudata.favorites[event.row]
				local address = fav.address
				local port    = fav.port
				gamedata.serverdescription = fav.description

				for i = 1, #favs do
					if fav.address == favs[i].address and
							fav.port == favs[i].port then
						gamedata.fav = true
					end
				end

				if address and port then
					core.settings:set("address", address)
					core.settings:set("remote_port", port)
				end
				tabdata.fav_selected = event.row
			end
			return true
		end
	end

	if fields.btn_delete_favorite then
		local current_favourite = core.get_table_index("favourites")
		if not current_favourite then return end

		core.delete_favorite(current_favourite)
		asyncOnlineFavourites()
		tabdata.fav_selected = nil

		core.settings:set("address", "")
		core.settings:set("remote_port", "30000")
		return true
	end

	if fields.cb_creative then
		core.settings:set("creative_mode", fields.cb_creative)
		return true
	end

	if fields.cb_damage then
		core.settings:set("enable_damage", fields.cb_damage)
		return true
	end

	if fields.btn_mp_connect or fields.key_enter then
		gamedata.playername = fields.te_name
		gamedata.password   = fields.te_pwd
		gamedata.address    = fields.te_address
		gamedata.port	    = fields.te_port
		local fav_idx = core.get_textlist_index("favourites")

		if fav_idx and fav_idx <= #menudata.favorites and
				menudata.favorites[fav_idx].address == fields.te_address and
				menudata.favorites[fav_idx].port    == fields.te_port then
			local fav = menudata.favorites[fav_idx]
			gamedata.servername        = fav.name
			gamedata.serverdescription = fav.description

			if menudata.favorites_is_public and
					not is_server_protocol_compat_or_error(
						fav.proto_min, fav.proto_max) then
				return true
			end
		else
			gamedata.servername	   = ""
			gamedata.serverdescription = ""
		end

		gamedata.selected_world = 0

		core.settings:set("address", fields.te_address)
		core.settings:set("remote_port", fields.te_port)

 		core.start()
 		return true
 	end

	if fields.btn_config_sp_world then
		local configdialog = create_configure_world_dlg(1)
		if configdialog then
			configdialog:set_parent(tabview)
			tabview:hide()
			configdialog:show()
		end
		return true
	end
end

--------------------------------------------------------------------------------
local function on_activate(type,old_tab,new_tab)
	if type == "LEAVE" then return end
	asyncOnlineFavourites()
end

--------------------------------------------------------------------------------
return {
	name = "main",
	caption = fgettext("Main"),
	cbf_formspec = get_formspec,
	cbf_button_handler = main_button_handler,
	on_change = on_activate
}


code:
--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 filter_texture_pack_list(list)
	local retval = {}

	for _, item in ipairs(list) do
		if item ~= "base" then
			retval[#retval + 1] = item
		end
	end

	table.sort(retval)
	table.insert(retval, 1, fgettext("None"))

	return retval
end

--------------------------------------------------------------------------------
local function render_texture_pack_list(list)
	local retval = ""

	for i, v in ipairs(list) do
		if v:sub(1, 1) ~= "." then
			if retval ~= "" then
				retval = retval .. ","
			end

			retval = retval .. core.formspec_escape(v)
		end
	end

	return retval
end

--------------------------------------------------------------------------------
local function get_formspec(tabview, name, tabdata)

	local retval = "label[4,-0.25;" .. fgettext("Select texture pack:") .. "]" ..
			"textlist[4,0.25;7.5,5.0;TPs;"

	local current_texture_path = core.settings:get("texture_path")
	local list = filter_texture_pack_list(core.get_dir_list(core.get_texturepath(), true))
	local index = tonumber(core.settings:get("mainmenu_last_selected_TP"))

	if not index then index = 1 end

	if current_texture_path == "" then
		retval = retval ..
			render_texture_pack_list(list) ..
			";" .. index .. "]"
		return retval
	end

	local infofile = current_texture_path .. DIR_DELIM .. "description.txt"
	-- This adds backwards compatibility for old texture pack description files named
	-- "info.txt", and should be removed once all such texture packs have been updated
	if not file_exists(infofile) then
		infofile = current_texture_path .. DIR_DELIM .. "info.txt"
		if file_exists(infofile) then
			core.log("deprecated", "info.txt is deprecated. description.txt should be used instead.")
		end
	end

	local infotext = ""
	local f = io.open(infofile, "r")
	if not f then
		infotext = fgettext("No information available")
	else
		infotext = f:read("*all")
		f:close()
	end

	local screenfile = current_texture_path .. DIR_DELIM .. "screenshot.png"
	local no_screenshot
	if not file_exists(screenfile) then
		screenfile = nil
		no_screenshot = defaulttexturedir .. "no_screenshot.png"
	end

	return	retval ..
			render_texture_pack_list(list) ..
			";" .. index .. "]" ..
			"image[0.25,0.25;4.05,2.7;" .. core.formspec_escape(screenfile or no_screenshot) .. "]" ..
			"textarea[0.6,2.85;3.7,1.5;;" .. core.formspec_escape(infotext or "") .. ";]"
end

--------------------------------------------------------------------------------
local function main_button_handler(tabview, fields, name, tabdata)
	if fields["TPs"] then
		local event = core.explode_textlist_event(fields["TPs"])
		if event.type == "CHG" or event.type == "DCL" then
			local index = core.get_textlist_index("TPs")
			core.settings:set("mainmenu_last_selected_TP", index)
			local list = filter_texture_pack_list(core.get_dir_list(core.get_texturepath(), true))
			local current_index = core.get_textlist_index("TPs")
			if current_index and #list >= current_index then
				local new_path = core.get_texturepath() .. DIR_DELIM .. list[current_index]
				if list[current_index] == fgettext("None") then
					new_path = ""
				end
				core.settings:set("texture_path", new_path)
			end
		end
		return true
	end
	return false
end

--------------------------------------------------------------------------------
return {
	name = "texturepacks",
	caption = fgettext("Texturepacks"),
	cbf_formspec = get_formspec,
	cbf_button_handler = main_button_handler,
	on_change = nil
}


code:
--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.

mm_texture = {}

--------------------------------------------------------------------------------
function mm_texture.init()
	mm_texture.defaulttexturedir = core.get_texturepath() .. DIR_DELIM .. "base" ..
						DIR_DELIM .. "pack" .. DIR_DELIM
	mm_texture.basetexturedir = mm_texture.defaulttexturedir

	mm_texture.texturepack = core.settings:get("texture_path")

	mm_texture.gameid = nil
end

--------------------------------------------------------------------------------
function mm_texture.update(tab,gamedetails)
	if tab ~= "singleplayer" then
		mm_texture.reset()
		return
	end

	if gamedetails == nil then
		return
	end

	mm_texture.update_game(gamedetails)
end

--------------------------------------------------------------------------------
function mm_texture.reset()
	mm_texture.gameid = nil
	local have_bg      = false
	local have_overlay = mm_texture.set_generic("overlay")

	if not have_overlay then
		have_bg = mm_texture.set_generic("background")
	end

	mm_texture.clear("header")
	mm_texture.clear("footer")
	core.set_clouds(false)

	mm_texture.set_generic("footer")
	mm_texture.set_generic("header")

	if not have_bg then
		if core.settings:get_bool("menu_clouds") then
			core.set_clouds(true)
		else
			mm_texture.set_dirt_bg()
		end
	end
end

--------------------------------------------------------------------------------
function mm_texture.update_game(gamedetails)
	if mm_texture.gameid == gamedetails.id then
		return
	end

	local have_bg      = false
	local have_overlay = mm_texture.set_game("overlay",gamedetails)

	if not have_overlay then
		have_bg = mm_texture.set_game("background",gamedetails)
	end

	mm_texture.clear("header")
	mm_texture.clear("footer")
	core.set_clouds(false)

	if not have_bg then

		if core.settings:get_bool("menu_clouds") then
			core.set_clouds(true)
		else
			mm_texture.set_dirt_bg()
		end
	end

	mm_texture.set_game("footer",gamedetails)
	mm_texture.set_game("header",gamedetails)

	mm_texture.gameid = gamedetails.id
end

--------------------------------------------------------------------------------
function mm_texture.clear(identifier)
	core.set_background(identifier,"")
end

--------------------------------------------------------------------------------
function mm_texture.set_generic(identifier)
	--try texture pack first
	if mm_texture.texturepack ~= nil then
		local path = mm_texture.texturepack .. DIR_DELIM .."menu_" ..
										identifier .. ".png"
		if core.set_background(identifier,path) then
			return true
		end
	end

	if mm_texture.defaulttexturedir ~= nil then
		local path = mm_texture.defaulttexturedir .. DIR_DELIM .."menu_" ..
										identifier .. ".png"
		if core.set_background(identifier,path) then
			return true
		end
	end

	return false
end

--------------------------------------------------------------------------------
function mm_texture.set_game(identifier, gamedetails)

	if gamedetails == nil then
		return false
	end

	if mm_texture.texturepack ~= nil then
		local path = mm_texture.texturepack .. DIR_DELIM ..
			gamedetails.id .. "_menu_" .. identifier .. ".png"
		if core.set_background(identifier, path) then
			return true
		end
	end

	-- Find out how many randomized textures the subgame provides
	local n = 0
	local filename
	local menu_files = core.get_dir_list(gamedetails.path .. DIR_DELIM .. "menu", false)
	for i = 1, #menu_files do
		filename = identifier .. "." .. i .. ".png"
		if table.indexof(menu_files, filename) == -1 then
			n = i - 1
			break
		end
	end
	-- Select random texture, 0 means standard texture
	n = math.random(0, n)
	if n == 0 then
		filename = identifier .. ".png"
	else
		filename = identifier .. "." .. n .. ".png"
	end

	local path = gamedetails.path .. DIR_DELIM .. "menu" ..
		DIR_DELIM .. filename
	if core.set_background(identifier, path) then
		return true
	end

	return false
end

function mm_texture.set_dirt_bg()
	if mm_texture.texturepack ~= nil then
		local path = mm_texture.texturepack .. DIR_DELIM .."default_dirt.png"
		if core.set_background("background", path, true, 128) then
			return true
		end
	end

	-- Use universal fallback texture in textures/base/pack
	local minimalpath = defaulttexturedir .. "menu_bg.png"
	core.set_background("background", minimalpath, true, 128)
end


code:
--Minetest
--Copyright (C) 2016 T4im
--
--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_bool_default(name, default)
	local val = core.settings:get_bool(name)
	if val == nil then
		return default
	end
	return val
end

local profiler_path = core.get_builtin_path()..DIR_DELIM.."profiler"..DIR_DELIM
local profiler = {}
local sampler = assert(loadfile(profiler_path .. "sampling.lua"))(profiler)
local instrumentation  = assert(loadfile(profiler_path .. "instrumentation.lua"))(profiler, sampler, get_bool_default)
local reporter = dofile(profiler_path .. "reporter.lua")
profiler.instrument = instrumentation.instrument

---
-- Delayed registration of the /profiler chat command
-- Is called later, after `core.register_chatcommand` was set up.
--
function profiler.init_chatcommand()
	local instrument_profiler = get_bool_default("instrument.profiler", false)
	if instrument_profiler then
		instrumentation.init_chatcommand()
	end

	local param_usage = "print [filter] | dump [filter] | save [format [filter]] | reset"
	core.register_chatcommand("profiler", {
		description = "handle the profiler and profiling data",
		params = param_usage,
		privs = { server=true },
		func = function(name, param)
			local command, arg0 = string.match(param, "([^ ]+) ?(.*)")
			local args = arg0 and string.split(arg0, " ")

			if command == "dump" then
				core.log("action", reporter.print(sampler.profile, arg0))
				return true, "Statistics written to action log"
			elseif command == "print" then
				return true, reporter.print(sampler.profile, arg0)
			elseif command == "save" then
				return reporter.save(sampler.profile, args[1] or "txt", args[2])
			elseif command == "reset" then
				sampler.reset()
				return true, "Statistics were reset"
			end

			return false, string.format(
				"Usage: %s\n" ..
				"Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).",
				param_usage
			)
		end
	})

	if not instrument_profiler then
		instrumentation.init_chatcommand()
	end
end

sampler.init()
instrumentation.init()

return profiler


code:
--Minetest
--Copyright (C) 2016 T4im
--
--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 format, pairs, type = string.format, pairs, type
local core, get_current_modname = core, core.get_current_modname
local profiler, sampler, get_bool_default = ...

local instrument_builtin = get_bool_default("instrument.builtin", false)

local register_functions = {
	register_globalstep = 0,
	register_playerevent = 0,
	register_on_placenode = 0,
	register_on_dignode = 0,
	register_on_punchnode = 0,
	register_on_generated = 0,
	register_on_newplayer = 0,
	register_on_dieplayer = 0,
	register_on_respawnplayer = 0,
	register_on_prejoinplayer = 0,
	register_on_joinplayer = 0,
	register_on_leaveplayer = 0,
	register_on_cheat = 0,
	register_on_chat_message = 0,
	register_on_player_receive_fields = 0,
	register_on_craft = 0,
	register_craft_predict = 0,
	register_on_protection_violation = 0,
	register_on_item_eat = 0,
	register_on_punchplayer = 0,
	register_on_player_hpchange = 0,
}

---
-- Create an unique instrument name.
-- Generate a missing label with a running index number.
--
local counts = {}
local function generate_name(def)
	local class, label, func_name = def.class, def.label, def.func_name
	if label then
		if class or func_name then
			return format("%s '%s' %s", class or "", label, func_name or ""):trim()
		end
		return format("%s", label):trim()
	elseif label == false then
		return format("%s", class or func_name):trim()
	end

	local index_id = def.mod .. (class or func_name)
	local index = counts[index_id] or 1
	counts[index_id] = index + 1
	return format("%s[%d] %s", class or func_name, index, class and func_name or ""):trim()
end

---
-- Keep `measure` and the closure in `instrument` lean, as these, and their
-- directly called functions are the overhead that is caused by instrumentation.
--
local time, log = core.get_us_time, sampler.log
local function measure(modname, instrument_name, start, ...)
	log(modname, instrument_name, time() - start)
	return ...
end
--- Automatically instrument a function to measure and log to the sampler.
-- def = {
-- 		mod = "",
-- 		class = "",
-- 		func_name = "",
-- 		-- if nil, will create a label based on registration order
-- 		label = "" | false,
-- }
local function instrument(def)
	if not def or not def.func then
		return
	end
	def.mod = def.mod or get_current_modname()
	local modname = def.mod
	local instrument_name = generate_name(def)
	local func = def.func

	if not instrument_builtin and modname == "*builtin*" then
		return func
	end

	return function(...)
		-- This tail-call allows passing all return values of `func`
		-- also called https://en.wikipedia.org/wiki/Continuation_passing_style
		-- Compared to table creation and unpacking it won't lose `nil` returns
		-- and is expected to be faster
		-- `measure` will be executed after time() and func(...)
		return measure(modname, instrument_name, time(), func(...))
	end
end

local function can_be_called(func)
	-- It has to be a function or callable table
	return type(func) == "function" or
		((type(func) == "table" or type(func) == "userdata") and
		getmetatable(func) and getmetatable(func).__call)
end

local function assert_can_be_called(func, func_name, level)
	if not can_be_called(func) then
		-- Then throw an *helpful* error, by pointing on our caller instead of us.
		error(format("Invalid argument to %s. Expected function-like type instead of '%s'.", func_name, type(func)), level + 1)
	end
end

---
-- Wraps a registration function `func` in such a way,
-- that it will automatically instrument any callback function passed as first argument.
--
local function instrument_register(func, func_name)
	local register_name = func_name:gsub("^register_", "", 1)
	return function(callback, ...)
		assert_can_be_called(callback, func_name, 2)
		register_functions[func_name] = register_functions[func_name] + 1
		return func(instrument {
			func = callback,
			func_name = register_name
		}, ...)
	end
end

local function init_chatcommand()
	if get_bool_default("instrument.chatcommand", true) then
		local orig_register_chatcommand = core.register_chatcommand
		core.register_chatcommand = function(cmd, def)
			def.func = instrument {
				func = def.func,
				label = "/" .. cmd,
			}
			orig_register_chatcommand(cmd, def)
		end
	end
end

---
-- Start instrumenting selected functions
--
local function init()
	if get_bool_default("instrument.entity", true) then
		-- Explicitly declare entity api-methods.
		-- Simple iteration would ignore lookup via __index.
		local entity_instrumentation = {
			"on_activate",
			"on_step",
			"on_punch",
			"rightclick",
			"get_staticdata",
		}
		-- Wrap register_entity() to instrument them on registration.
		local orig_register_entity = core.register_entity
		core.register_entity = function(name, prototype)
			local modname = get_current_modname()
			for _, func_name in pairs(entity_instrumentation) do
				prototype[func_name] = instrument {
					func = prototype[func_name],
					mod = modname,
					func_name = func_name,
					label = prototype.label,
				}
			end
			orig_register_entity(name,prototype)
		end
	end

	if get_bool_default("instrument.abm", true) then
		-- Wrap register_abm() to automatically instrument abms.
		local orig_register_abm = core.register_abm
		core.register_abm = function(spec)
			spec.action = instrument {
				func = spec.action,
				class = "ABM",
				label = spec.label,
			}
			orig_register_abm(spec)
		end
	end

	if get_bool_default("instrument.lbm", true) then
		-- Wrap register_lbm() to automatically instrument lbms.
		local orig_register_lbm = core.register_lbm
		core.register_lbm = function(spec)
			spec.action = instrument {
				func = spec.action,
				class = "LBM",
				label = spec.label or spec.name,
			}
			orig_register_lbm(spec)
		end
	end

	if get_bool_default("instrument.global_callback", true) then
		for func_name, _ in pairs(register_functions) do
			core[func_name] = instrument_register(core[func_name], func_name)
		end
	end

	if get_bool_default("instrument.profiler", false) then
		-- Measure overhead of instrumentation, but keep it down for functions
		-- So keep the `return` for better optimization.
		profiler.empty_instrument = instrument {
			func = function() return end,
			mod = "*profiler*",
			class = "Instrumentation overhead",
			label = false,
		}
	end
end

return {
	register_functions = register_functions,
	instrument = instrument,
	init = init,
	init_chatcommand = init_chatcommand,
}


code:
--Minetest
--Copyright (C) 2016 T4im
--
--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 DIR_DELIM, LINE_DELIM = DIR_DELIM, "\n"
local table, unpack, string, pairs, io, os = table, unpack, string, pairs, io, os
local rep, sprintf, tonumber = string.rep, string.format, tonumber
local core, settings = core, core.settings
local reporter = {}

---
-- Shorten a string. End on an ellipsis if shortened.
--
local function shorten(str, length)
	if str and str:len() > length then
		return "..." .. str:sub(-(length-3))
	end
	return str
end

local function filter_matches(filter, text)
	return not filter or string.match(text, filter)
end

local function format_number(number, fmt)
	number = tonumber(number)
	if not number then
		return "N/A"
	end
	return sprintf(fmt or "%d", number)
end

local Formatter = {
	new = function(self, object)
		object = object or {}
		object.out = {} -- output buffer
		self.__index = self
		return setmetatable(object, self)
	end,
	__tostring = function (self)
		return table.concat(self.out, LINE_DELIM)
	end,
	print = function(self, text, ...)
		if (...) then
			text = sprintf(text, ...)
		end

		if text then
			-- Avoid format unicode issues.
			text = text:gsub("Ms", "µs")
		end

		table.insert(self.out, text or LINE_DELIM)
	end,
	flush = function(self)
		table.insert(self.out, LINE_DELIM)
		local text = table.concat(self.out, LINE_DELIM)
		self.out = {}
		return text
	end
}

local widths = { 55, 9, 9, 9, 5, 5, 5 }
local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths))

local HR = {}
for i=1, #widths do
	HR[i]= rep("-", widths[i])
end
-- ' | ' should break less with github than '-+-', when people are pasting there
HR = sprintf("-%s-", table.concat(HR, " | "))

local TxtFormatter = Formatter:new {
	format_row = function(self, modname, instrument_name, statistics)
		local label
		if instrument_name then
			label = shorten(instrument_name, widths[1] - 5)
			label = sprintf(" - %s %s", label, rep(".", widths[1] - 5 - label:len()))
		else -- Print mod_stats
			label = shorten(modname, widths[1] - 2) .. ":"
		end

		self:print(txt_row_format, label,
			format_number(statistics.time_min),
			format_number(statistics.time_max),
			format_number(statistics:get_time_avg()),
			format_number(statistics.part_min, "%.1f"),
			format_number(statistics.part_max, "%.1f"),
			format_number(statistics:get_part_avg(), "%.1f")
		)
	end,
	format = function(self, filter)
		local profile = self.profile
		self:print("Values below show absolute/relative times spend per server step by the instrumented function.")
		self:print("A total of %d samples were taken", profile.stats_total.samples)

		if filter then
			self:print("The output is limited to '%s'", filter)
		end

		self:print()
		self:print(
			txt_row_format,
			"instrumentation", "min Ms", "max Ms", "avg Ms", "min %", "max %", "avg %"
		)
		self:print(HR)
		for modname,mod_stats in pairs(profile.stats) do
			if filter_matches(filter, modname) then
				self:format_row(modname, nil, mod_stats)

				if mod_stats.instruments ~= nil then
					for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
						self:format_row(nil, instrument_name, instrument_stats)
					end
				end
			end
		end
		self:print(HR)
		if not filter then
			self:format_row("total", nil, profile.stats_total)
		end
	end
}

local CsvFormatter = Formatter:new {
	format_row = function(self, modname, instrument_name, statistics)
		self:print(
			"%q,%q,%d,%d,%d,%d,%d,%f,%f,%f",
			modname, instrument_name,
			statistics.samples,
			statistics.time_min,
			statistics.time_max,
			statistics:get_time_avg(),
			statistics.time_all,
			statistics.part_min,
			statistics.part_max,
			statistics:get_part_avg()
		)
	end,
	format = function(self, filter)
		self:print(
			"%q,%q,%q,%q,%q,%q,%q,%q,%q,%q",
			"modname", "instrumentation",
			"samples",
			"time min µs",
			"time max µs",
			"time avg µs",
			"time all µs",
			"part min %",
			"part max %",
			"part avg %"
		)
		for modname, mod_stats in pairs(self.profile.stats) do
			if filter_matches(filter, modname) then
				self:format_row(modname, "*", mod_stats)

				if mod_stats.instruments ~= nil then
					for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
						self:format_row(modname, instrument_name, instrument_stats)
					end
				end
			end
		end
	end
}

local function format_statistics(profile, format, filter)
	local formatter
	if format == "csv" then
		formatter = CsvFormatter:new {
			profile = profile
		}
	else
		formatter = TxtFormatter:new {
			profile = profile
		}
	end
	formatter:format(filter)
	return formatter:flush()
end

---
-- Format the profile ready for display and
-- @return string to be printed to the console
--
function reporter.print(profile, filter)
	if filter == "" then filter = nil end
	return format_statistics(profile, "txt", filter)
end

---
-- Serialize the profile data and
-- @return serialized data to be saved to a file
--
local function serialize_profile(profile, format, filter)
	if format == "lua" or format == "json" or format == "json_pretty" then
		local stats = filter and {} or profile.stats
		if filter then
			for modname, mod_stats in pairs(profile.stats) do
				if filter_matches(filter, modname) then
					stats[modname] = mod_stats
				end
			end
		end
		if format == "lua" then
			return core.serialize(stats)
		elseif format == "json" then
			return core.write_json(stats)
		elseif format == "json_pretty" then
			return core.write_json(stats, true)
		end
	end
	-- Fall back to textual formats.
	return format_statistics(profile, format, filter)
end

local worldpath = core.get_worldpath()
local function get_save_path(format, filter)
	local report_path = settings:get("profiler.report_path") or ""
	if report_path ~= "" then
		core.mkdir(sprintf("%s%s%s", worldpath, DIR_DELIM, report_path))
	end
	return (sprintf(
		"%s/%s/profile-%s%s.%s",
		worldpath,
		report_path,
		os.date("%Y%m%dT%H%M%S"),
		filter and ("-" .. filter) or "",
		format
	):gsub("[/\\]+", DIR_DELIM))-- Clean up delims
end

---
-- Save the profile to the world path.
-- @return success, log message
--
function reporter.save(profile, format, filter)
	if not format or format == "" then
		format = settings:get("profiler.default_report_format") or "txt"
	end
	if filter == "" then
		filter = nil
	end

	local path = get_save_path(format, filter)

	local output, io_err = io.open(path, "w")
	if not output then
		return false, "Saving of profile failed with: " .. io_err
	end
	local content, err = serialize_profile(profile, format, filter)
	if not content then
		output:close()
		return false, "Saving of profile failed with: " .. err
	end
	output:write(content)
	output:close()

	local logmessage = "Profile saved to " .. path
	core.log("action", logmessage)
	return true, logmessage
end

return reporter


code:
--Minetest
--Copyright (C) 2016 T4im
--
--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 setmetatable = setmetatable
local pairs, format = pairs, string.format
local min, max, huge = math.min, math.max, math.huge
local core = core

local profiler = ...
-- Split sampler and profile up, to possibly allow for rotation later.
local sampler = {}
local profile
local stats_total
local logged_time, logged_data

local _stat_mt = {
	get_time_avg = function(self)
		return self.time_all/self.samples
	end,
	get_part_avg = function(self)
		if not self.part_all then
			return 100 -- Extra handling for "total"
		end
		return self.part_all/self.samples
	end,
}
_stat_mt.__index = _stat_mt

function sampler.reset()
	-- Accumulated logged time since last sample.
	-- This helps determining, the relative time a mod used up.
	logged_time = 0
	-- The measurements taken through instrumentation since last sample.
	logged_data = {}

	profile = {
		-- Current mod statistics (max/min over the entire mod lifespan)
		-- Mod specific instrumentation statistics are nested within.
		stats = {},
		-- Current stats over all mods.
		stats_total = setmetatable({
			samples = 0,
			time_min = huge,
			time_max = 0,
			time_all = 0,
			part_min = 100,
			part_max = 100
		}, _stat_mt)
	}
	stats_total = profile.stats_total

	-- Provide access to the most recent profile.
	sampler.profile = profile
end

---
-- Log a measurement for the sampler to pick up later.
-- Keep `log` and its often called functions lean.
-- It will directly add to the instrumentation overhead.
--
function sampler.log(modname, instrument_name, time_diff)
	if time_diff <= 0 then
		if time_diff < 0 then
			-- This **might** have happened on a semi-regular basis with huge mods,
			-- resulting in negative statistics (perhaps midnight time jumps or ntp corrections?).
			core.log("warning", format(
					"Time travel of %s::%s by %dµs.",
					modname, instrument_name, time_diff
			))
		end
		-- Throwing these away is better, than having them mess with the overall result.
		return
	end

	local mod_data = logged_data[modname]
	if mod_data == nil then
		mod_data = {}
		logged_data[modname] = mod_data
	end

	mod_data[instrument_name] = (mod_data[instrument_name] or 0) + time_diff
	-- Update logged time since last sample.
	logged_time = logged_time + time_diff
end

---
-- Return a requested statistic.
-- Initialize if necessary.
--
local function get_statistic(stats_table, name)
	local statistic = stats_table[name]
	if statistic == nil then
		statistic = setmetatable({
			samples = 0,
			time_min = huge,
			time_max = 0,
			time_all = 0,
			part_min = 100,
			part_max = 0,
			part_all = 0,
		}, _stat_mt)
		stats_table[name] = statistic
	end
	return statistic
end

---
-- Update a statistic table
--
local function update_statistic(stats_table, time)
	stats_table.samples = stats_table.samples + 1

	-- Update absolute time (µs) spend by the subject
	stats_table.time_min = min(stats_table.time_min, time)
	stats_table.time_max = max(stats_table.time_max, time)
	stats_table.time_all = stats_table.time_all + time

	-- Update relative time (%) of this sample spend by the subject
	local current_part = (time/logged_time) * 100
	stats_table.part_min = min(stats_table.part_min, current_part)
	stats_table.part_max = max(stats_table.part_max, current_part)
	stats_table.part_all = stats_table.part_all + current_part
end

---
-- Sample all logged measurements each server step.
-- Like any globalstep function, this should not be too heavy,
-- but does not add to the instrumentation overhead.
--
local function sample(dtime)
	-- Rare, but happens and is currently of no informational value.
	if logged_time == 0 then
		return
	end

	for modname, instruments in pairs(logged_data) do
		local mod_stats = get_statistic(profile.stats, modname)
		if mod_stats.instruments == nil then
			-- Current statistics for each instrumentation component
			mod_stats.instruments = {}
		end

		local mod_time = 0
		for instrument_name, time in pairs(instruments) do
			if time > 0 then
				mod_time = mod_time + time
				local instrument_stats = get_statistic(mod_stats.instruments, instrument_name)

				-- Update time of this sample spend by the instrumented function.
				update_statistic(instrument_stats, time)
				-- Reset logged data for the next sample.
				instruments[instrument_name] = 0
			end
		end

		-- Update time of this sample spend by this mod.
		update_statistic(mod_stats, mod_time)
	end

	-- Update the total time spend over all mods.
	stats_total.time_min = min(stats_total.time_min, logged_time)
	stats_total.time_max = max(stats_total.time_max, logged_time)
	stats_total.time_all = stats_total.time_all + logged_time

	stats_total.samples = stats_total.samples + 1
	logged_time = 0
end

---
-- Setup empty profile and register the sampling function
--
function sampler.init()
	sampler.reset()

	if core.settings:get_bool("instrument.profiler") then
		core.register_globalstep(function()
			if logged_time == 0 then
				return
			end
			return profiler.empty_instrument()
		end)
		core.register_globalstep(profiler.instrument {
			func = sample,
			mod = "*profiler*",
			class = "Sampler (update stats)",
			label = false,
		})
	else
		core.register_globalstep(sample)
	end
end

return sampler