minetest-menu/init.lua
2025-10-23 19:21:31 -04:00

2049 lines
81 KiB
Lua

gamedata = {
}
local http = minetest.get_http_api()
-- Used for brevity when making fullscreen things
window = minetest.get_window_info()
size = window.max_formspec_size
dpi = {
x = window.size.x /size.x,
y = window.size.y /size.y
}
-- Default textures
default_textures = minetest.get_texturepath_share().."/base/pack/"
-- FIXME: Assets
assets = os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"
local version = minetest.get_version()
-- This is where all view-specific state information goes.
state = {}
local fe = minetest.formspec_escape
local hte = minetest.hypertext_escape
function include(file)
-- FIXME: Use get_builtin_path()
dofile(os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"..file)
end
dofile(minetest.get_builtin_path().."common/settings/settingtypes.lua")
-- Theme
theme = {
text_color = "#aaa",
muted_text_color = "#777",
code_color = "#da8",
link_color = "#8ad",
mod_label_color = "#888",
game_label_color = "#888",
texture_pack_label_color = "#888",
bg = "",
modal_bg = "#0008",
syntax_keyword = "#d8d",
syntax_keyword_value = "#79e",
syntax_identifier = "#adf",
syntax_operator = "#f66",
syntax_string = "#da8",
syntax_number = "#cdb",
syntax_comment = "#777",
syntax_placeholder = "#ccc",
}
--End theme
---[[
minetest.set_formspec_prepend("\
style[*;textcolor=#aaa]\
style_type[field;border=false]\
style_type[pwdfield;border=false]\
style_type[image_button;border=false]\
style_type[button;border=false;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
style_type[button:hovered;border=false;bgimg="..assets.."btn_bg_2_hover.png;bgimg_middle=8,8]\
style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0;content_offset=0,0]\
")
local meta_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
bgcolor[#000;true;#151618]\
style_type[box;bordercolors=#124722;borderwidths=-5;colors=#151618]\
image[0,"..(size.y *0.08)..");"..size.x..","..(size.x *(72/672))..";"..default_textures.."menu_header.png]\
"
-- box[0,0;"..size.x..","..size.y..";]\
local game_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
bgcolor[#0000;true;#0000]\
"
local servers_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
bgcolor[#0000;true;#151618]\
"
local content_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
bgcolor[#0000;true;#151618]\
"
local default_game_menu = [[
{% macro field(x, y, w, h, name, value) %}
image[{{ x }},{{ y }};{{ w }},{{ h }};{{ DEFAULT_ASSETS }}btn_bg_2_light.png;8,8]
field[{{ x + 0.1 }},{{ y }};{{ w - 0.2 }},{{ h }};{{ name }};;{{ value }}]
{% endmacro %}
{% view main %}
{% set worlds = get_worlds() %}
{# Special layouts are applied when there are less than four worlds for a given game. #}
{% if len(worlds) == 1 %}
image[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - 1.5 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
button[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[1].name }}]
{% elseif len(worlds) == 2 %}
image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[1].name }}]
image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[2].name }}]
{% elseif len(worlds) == 3 %}
image[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - 1.5 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
button[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[1].name }}]
image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[2].name }}]
image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[3]) }};]
button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[3].name }}]
{% elseif len(worlds) == 4 %}
image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[1].name }}]
image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[2].name }}]
image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[3]) }};]
button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[3].name }}]
image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[4]) }};]
button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[4].name }}]
{% else %}
scroll_container[0,0;{{ WIDTH }},{{ HEIGHT }};worldscroll;vertical;;1,0]
{% set cols = floor((WIDTH - 2) / 5) %}
{% set trailing_cols = len(worlds) % cols %}
{% set rows = ceil(len(worlds) / cols) %}
{% set col = 0 %}
{% set row = 0 %}
{% set rx = cols * 2.5 %}
{% set ry = min(rows * 2.5, (HEIGHT - 2) / 2) %}
{% for world in worlds %}
image[{{ WIDTH * 0.5 - rx + col * 5 }},{{ HEIGHT * 0.5 - ry + row * 5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
image_button[{{ WIDTH * 0.5 - rx + col * 5 + 0.5 }},{{ HEIGHT * 0.5 - ry + row * 5 + 0.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', world) }};]
button[{{ WIDTH * 0.5 - rx + col * 5 }},{{ HEIGHT * 0.5 - ry + row * 5 + 3 }};4,0.75;nobg;{{ fe(world.name) }}]
{% set col = col + 1 %}
{% if col > cols - 1 %}
{% set col = 0 %}
{% set row = row + 1 %}
{% if row == rows - 1 and trailing_cols > 0 %}
{% set rx = min(trailing_cols, cols) * 2.5 %}
{% endif %}
{% endif %}
{% endfor %}
scroll_container_end[]
scrollbar[-800,0;0,0;vertical;worldscroll;]
{% endif %}
{% if selected_world %}
box[0,0;{{ WIDTH }},{{ HEIGHT }};#0003]
image[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.9 }},{{ HEIGHT * 0.9 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
label[1,1;Selected world: {{ fe(selected_world.name) }}]
button[1,1;3,1;{{ action('set', 'selected_world', nil) }};Back]
{% endif %}
{#
image[{{ WIDTH * 0.1 - 0.1 }},{{ HEIGHT * 0.1 - 0.1}};{{ WIDTH * 0.8 + 0.2 }},{{ HEIGHT * 0.8 + 0.2 }};{{ DEFAULT_ASSETS }}bg_translucent.png;8,8]
scroll_container[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.1 }};{{ list_width }},{{ HEIGHT * 0.8 }};worldscroll;vertical;;0,0]
{% for world in get_worlds() %}
style[.select_world_{{ world.path }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
style[.select_world_{{ world.path }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
style[.select_world_{{ world.path }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
button[0,{{ loop.index0 * 0.5}};{{ list_width }},0.5;.select_world_{{ world.path }};{{ world.name }}]
{% endfor %}
scroll_container_end[]
{% if selected_world %}
box[{{ WIDTH * 0.4 - 0.05 }},{{ HEIGHT * 0.1 - 0.1 }};0.1,{{ HEIGHT * 0.8 + 0.2 }};#292d2fff]
scroll_container[{{ WIDTH * 0.4 + 0.05 }},{{ HEIGHT * 0.1 }};{{ WIDTH * 0.8 - 0.05 }},{{ HEIGHT * 0.8 }};worlconfigscroll;vertical;;0,0]
{% set j = 0 %}
{% if setting_damage %}
{% if damage_enabled %}
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_damage_enabled_to_false;]
{% else %}
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_damage_enabled_to_true;]
{% endif %}
label[0.5,{{ j + 0.25 }};Damage]
{% set j = j + 0.5 %}
{% endif %}
{% if setting_creative %}
{% if creative_enabled %}
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_creative_enabled_to_false;]
{% else %}
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_creative_enabled_to_true;]
{% endif %}
label[0.5,{{ j + 0.25 }};Creative]
{% set j = j + 0.5 %}
{% endif %}
{% set play_str = "Play" %}
{% if setting_server %}
{% if server_enabled %}
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_server_enabled_to_false;]
{% set play_str = "Host server" %}
{% else %}
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_server_enabled_to_true;]
{% set play_str = "Play" %}
{% endif %}
label[0.5,{{ j + 0.25 }};Server]
{% set j = j + 0.5 %}
{% endif %}
{% set j = j + 1 %}
button[{{ WIDTH * 0.05 }},{{ j }};{{ WIDTH * 0.4 }},0.75;.overlay_view_modconfig;Configure mods]
button[{{ WIDTH * 0.05 }},{{ j + 1 }};{{ WIDTH * 0.4 }},0.75;.play;{{ play_str }}]
scroll_container_end[]
scrollbar[-800,6;0,2;vertical;worldconfigscroll;]
{% endif %}
#}
scrollbaroptions[arrows=hide]
scrollbar[-800,6;0,2;vertical;worldscroll;]
{% endview %}
{% view modconfig %}
image[{{ WIDTH * 0.05 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
hypertext[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;<global valign=middle halign=center><b>Available mods</b>]
scroll_container[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};modsscroll;vertical;;0,0]
{% for mod in get_mods() %}
style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}]
{% endfor %}
scroll_container_end[]
scrollbar[-800,6;0,2;vertical;modsscroll;]
image[{{ WIDTH * 0.55 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
hypertext[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;<global valign=middle halign=center><b>Enabled mods</b>]
scroll_container[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};worldmodsscroll;vertical;;0,0]
{% for mod in get_world_mods() %}
{% if not mod.game_provided %}
style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}]
{% endif %}
{% endfor %}
scroll_container_end[]
scrollbar[-800,6;0,2;vertical;worldmodsscroll;]
image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 - WIDTH * 0.1 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_right.png;.add_mod_to_world;]
image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_left.png;.remove_mod_from_world;]
tooltip[.add_mod_to_world;Add mod to world;#444;#aaa]
tooltip[.remove_mod_from_world;Remove mod from world;#444;#aaa]
button[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Cancel]
button[{{ WIDTH * 0.5 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Confirm]
{% endview %}
{% view addworld %}
{% endview %}
]]
function core.on_before_close()
--minetest.settings:write()
end
minetest.async_jobs = {}
local function handle_job(jobid, serialized_retval)
local retval = minetest.deserialize(serialized_retval)
assert(type(minetest.async_jobs[jobid]) == "function")
minetest.async_jobs[jobid](retval)
minetest.async_jobs[jobid] = nil
end
minetest.async_event_handler = handle_job
function minetest.handle_async(func, parameter, callback)
-- Serialize parameters
local serialized_param = minetest.serialize(parameter)
if serialized_param == nil then
return false
end
local jobid = minetest.do_async_callback(func, serialized_param)
minetest.async_jobs[jobid] = callback
return true
end
-- MARK: - Helpers
local function readable_content_type(type)
if type == "game" then
return "Game"
elseif type == "mod" then
return "Mod"
elseif type == "modpack" then
return "Mod collection"
elseif type == "txp" then
return "Texture pack"
else
return "Unknown"
end
end
-- Returns the contents of the file, or nil in case of failure.
local function read_file(path)
local f = io.open(path)
if f then
local data = f:read("a")
f:close()
if data == "" then return end
return data
end
end
-- Check if a file exists.
local function file_exists(path)
local f = io.open(path)
if f then
f:close()
return true
end
return false
end
-- Returns the get_games() entry corresponding to a given game ID.
local function get_game_info(game)
for _, x in ipairs(minetest.get_games()) do
if x.id == game then
return x
end
end
end
local function get_world_index(path)
for i, x in ipairs(minetest.get_worlds()) do
if x.path == path then
return i
end
end
return -1
end
function get_worlds_for_game(id)
local out = {}
for _, x in ipairs(minetest.get_worlds()) do
if x.gameid == id then
out[#out +1] = x
end
end
table.sort(out, function(a, b)
return a.name < b.name
end)
return out
end
function get_mods_for_game(id)
local out = {}
for _, x in ipairs(get_all_mods()) do
local conf = Settings(x.path.."mod.conf")
local unsupported_games = conf:get("unsupported_games")
if unsupported_games then
if table.indexof(unsupported_games:trim():split(","), id) == -1 then
out[#out +1] = x
end
else
local supported_games = conf:get("supported_games")
if supported_games then
if table.indexof(supported_games:trim():split(","), id) ~= -1 then
out[#out +1] = x
end
else
out[#out +1] = x
end
end
end
return out
end
local function get_game_mods(id)
local out = {}
local base = get_game_info(id).path.."/mods/"
for _, x in pairs(minetest.get_dir_list(base, true)) do
out[#out +1] = {
name = x,
path = base..x
}
end
return out
end
function get_mods_for_world(world)
local conf = Settings(world.."/world.mt")
local mods = {}
local game
local game_mods = {}
for k, v in pairs(conf:to_table()) do
if k == "gameid" then
game = v
elseif k:sub(1, 9) == "load_mod_" then
if v ~= "false" and v ~= "nil" and v then
mods[k:sub(10)] = minetest.get_user_path().."/"..v
end
end
end
if game then
for _, x in ipairs(get_game_mods(game)) do
game_mods[x.name] = x.path
end
end
local config = minetest.check_mod_configuration(world, mods)
-- print(dump(config))
for _, x in ipairs(config.unsatisfied_mods) do
x.unsatisfied = true
config.satisfied_mods[#config.satisfied_mods +1] = x
end
if game then
for _, x in ipairs(config.satisfied_mods) do
if game_mods[x.name] then
x.game_provided = true
end
end
end
return config.satisfied_mods
end
-- Returns a list of content available for download.
function get_available_content()
local version = minetest.get_version()
local cdb = minetest.settings:get("contentdb_url")
local url = cdb.."/api/packages/?type=mod&type=game&type=txp&protocol_version="..minetest.get_max_supp_proto().."&engine_version="..minetest.urlencode(version.string)
for _, x in pairs(minetest.settings:get("contentdb_flag_blacklist"):split(",")) do
x = x:trim()
if x ~= "" then
url = url.."&hide="..minetest.urlencode(x)
end
end
local languages
local lang = minetest.get_language()
if lang ~= "" then
languages = { lang, "en;q=0.8" }
else
languages = { "en" }
end
local http = minetest.get_http_api()
local response = http.fetch_sync({
url = url,
extra_headers = {
"Accept-Language: "..table.concat(languages, ", ")
},
})
if not response.succeeded then
return
end
local items = minetest.parse_json(response.data)
local out = {}
for _, x in pairs(items) do
out[#out +1] = x
end
return out
end
-- Returns a list of all the installed mods.
function get_all_mods(dir)
local all = false
if not dir then
if state.all_mods then return state.all_mods end
dir = minetest.get_modpath()
all = true
end
local out = {}
for _, x in ipairs(minetest.get_dir_list(dir, true)) do
local info = minetest.get_content_info(dir.."/"..x)
if info.type == "mod" or info.type == "modpack" then
out[#out +1] = info
end
end
if all then
state.all_mods = out
end
return out
end
-- Returns a list of all the installed games.
function get_all_games(dir)
local all = false
if not dir then
if state.all_games then return state.all_games end
dir = minetest.get_gamepath()
all = true
end
local out = {}
for _, x in ipairs(minetest.get_dir_list(dir, true)) do
local info = minetest.get_content_info(dir.."/"..x)
if info.type == "game" then
out[#out +1] = info
end
end
if all then
state.all_games = out
end
return out
end
-- Returns a list of all the installed texture packs.
function get_all_texture_packs(dir)
local all = false
if not dir then
if state.all_texture_packs then return state.all_texture_packs end
dir = minetest.get_texturepath()
all = true
end
local out = {}
for _, x in ipairs(minetest.get_dir_list(dir, true)) do
local info = minetest.get_content_info(dir.."/"..x)
if info.type == "txp" then
out[#out +1] = info
end
end
if all then
state.all_texture_packs = out
end
return out
end
-- Return a list of all content, as a conglomeration of the lists of mods, games, and texture packs.
function get_all_content()
if not state.all_content then
state.all_content = {}
table.insert_all(state.all_content, get_all_mods())
table.insert_all(state.all_content, get_all_games())
table.insert_all(state.all_content, get_all_texture_packs())
table.sort(state.all_content, function(a, b)
if not a then
return b ~= nil
elseif not b then
return a == nil
end
return a.name < b.name
end)
end
return state.all_content
end
-- Parses text as .conf format.
function parse_conf_text(txt)
local out = {}
local multiline = false
local key = ""
local value = ""
for line in (txt.."\n"):gmatch("(.*)\n") do
if multiline then
if line:find("\"\"\"") then
line = line:gsub("\"\"\"", ""):trim()
multiline = false
out[key] = value.."\n"..line
key = ""
value = ""
else
value = value.."\n"..line:trim()
end
else
local k, v = line:match("(.-)=(.*)")
if not k then break end
if v:find("\"\"\"") then
key = k:trim()
value = v:gsub("\"\"\"", ""):trim()
multiline = true
else
out[k:trim()] = v:trim()
end
end
end
return out
end
local function markdown_to_hypertext_format(md)
return md:gsub("\n%s*####%s*([^\n]-)\n", "\n<b>%1</b>\n")
:gsub("\n%s*###%s*([^\n]-)\n", "\n<big>%1</big>\n")
:gsub("([^\n]-)\n%-+\n", "\n<big>%1</big>\n")
:gsub("\n%s*##%s*([^\n]-)\n", "\n<bigger>%1</bigger>\n")
:gsub("\n%s*#%s*([^\n]-)\n", "\n<style size=48>%1</style>\n")
:gsub("%*%*([^`\n]-)%*%*", "<b>%1</b>")
:gsub("%*([^`\n]-)%*", "<i>%1</i>")
:gsub("__([^`\n]-)__", "<b>%1</b>")
:gsub("\n_([^`\n]-)_", "<i>%1</i>")
:gsub("%*%s+(.-)\n", "• %1\n")
-- :gsub("```%a-\n(.-)```", "<mono><style color="..theme.code_color..">%1</style></mono>")
-- :gsub("`([^`]-)`", "<mono><style color="..theme.code_color..">%1</style></mono>")
-- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them.
:gsub("%[?!%[[^%]]-%]%([^\n]+%)", "")
end
local keywords = {
"and", "break", "do", "else", "elseif", "end", --[["false",]] "for", "function",
"goto", "if", "in", "local", --[["nil",]] "not", "or", "repeat", "return",
"then", --[["true",]] "until", "while"
}
local keyword_set = {}
for _, kw in ipairs(keywords) do
keyword_set[kw] = true
end
local keyword_value_set = {
["true"] = true,
["false"] = true,
["nil"] = true,
["math.huge"] = true
}
local function colorize(str, color)
return "<style color="..(theme["syntax_"..color] or "#fff")..">"..str.."</style>"
end
local function syntax_highlight(code)
local out = ""
local pos = 1
while pos <= #code do
local matched = false
-- Whitespace
local ws_start, ws_end = code:find("^%s+", pos)
if ws_start then
out = out..colorize(code:sub(ws_start, ws_end), "whitespace")
pos = ws_end + 1
matched = true
end
-- Single-line comment
if not matched then
local com_start, com_end = code:find("^%-%-[^\n]*", pos)
if com_start then
out = out..colorize(code:sub(com_start, com_end), "comment")
pos = com_end + 1
matched = true
end
end
-- String (double-quoted, simple escape handling)
if not matched then
local str_start, str_end = code:find('^"[^"]*"', pos)
if str_start then
out = out..colorize(code:sub(str_start, str_end), "string")
pos = str_end + 1
matched = true
end
end
-- String (single-quoted, similar)
if not matched then
local str_start, str_end = code:find("^'[^']*", pos)
if str_start then
out = out..colorize(code:sub(str_start, str_end), "string")
pos = str_end + 1
matched = true
end
end
-- Documentation placeholders. These aren't in Lua, but that means I
-- can highlight them without worrying about confusing alternative meanings.
if not matched then
local str_start, str_end = code:find('^\\?<[^>]*>', pos)
if str_start then
out = out..colorize(code:sub(str_start, str_end), "placeholder")
pos = str_end + 1
matched = true
end
end
-- Number (basic integer/float)
if not matched then
local num_start, num_end = code:find("^%d+%.?%d*[eE]?[+-]?%d*", pos)
if num_start then
out = out..colorize(code:sub(num_start, num_end), "number")
pos = num_end + 1
matched = true
end
end
-- Identifier/Keyword
if not matched then
local id_start, id_end, id_text = code:find("^([%a_][%w_]*)", pos)
if id_start then
if keyword_set[id_text] then
out = out..colorize(id_text, "keyword")
elseif keyword_value_set[id_text] then
out = out..colorize(id_text, "keyword_value")
else
out = out..colorize(id_text, "identifier")
end
pos = id_end + 1
matched = true
end
end
-- Operators/Punctuation (catch-all for non-alphanumeric)
if not matched then
local op_start, op_end = code:find("^[^%s%a%d_\"'{}(),%][]", pos)
if op_start then
local op = code:sub(op_start, op_end)
out = out..colorize(op, op == "-" and code:sub(op_end +1, op_end +2):find "^%d" and "number" or "operator")
pos = op_end + 1
matched = true
end
end
-- If nothing matched, advance to avoid infinite loop (e.g., invalid char)
if not matched then
out = out..colorize(code:sub(pos, pos), "generic")
pos = pos + 1
end
end
return out
end
local function markdown_to_hypertext(md)
md = ("\n"..hte(md or "").."\n")
local out = ""
-- The following is necessary because code blocks should not have formatting applied to their contents.
local offset = 1
while true do
local block_start, block_end, lang, block = md:find("```(%a*)\n(.-)```", offset)
local code_start, code_end, code = md:find("[^`]`([^`\n]+)`", offset)
if not block_start and not code_start then break end
if block_start and block_start < (code_start or math.huge) then
-- Don't apply syntax highlighting to blocks annotated as a language other than Lua.
-- However, assume that blocks with no annotation are probably Lua.
if lang == "lua" then
block = syntax_highlight(block)
end
-- The block:gsub hack is used so that leading whitespace (indentation) is not trimmed.
out = out..markdown_to_hypertext_format(md:sub(offset, block_start -1)).."<mono><style color="..theme.code_color..">"..block:gsub("\n", "\n\u{a0}").."</style></mono>"
offset = block_end +1
end
if code_start and code_start < (block_start or math.huge) then
out = out..markdown_to_hypertext_format(md:sub(offset, code_start)).."<mono><style color="..theme.code_color..">"..code.."</style></mono>"
offset = code_end +1
end
end
out = out..markdown_to_hypertext_format(md:sub(offset))
-- md = md
-- :gsub("\n%s*####%s*([^\n]-)\n", "\n<b>%1</b>\n")
-- :gsub("\n%s*###%s*([^\n]-)\n", "\n<big>%1</big>\n")
-- :gsub("([^\n]-)\n%-+\n", "\n<big>%1</big>\n")
-- :gsub("\n%s*##%s*([^\n]-)\n", "\n<bigger>%1</bigger>\n")
-- :gsub("\n%s*#%s*([^\n]-)\n", "\n<style size=48>%1</style>\n")
-- :gsub("%*%*([^`\n]-)%*%*", "<b>%1</b>")
-- :gsub("%*([^`\n]-)%*", "<i>%1</i>")
-- :gsub("__([^`\n]-)__", "<b>%1</b>")
-- :gsub("\n_([^`\n]-)_", "<i>%1</i>")
-- :gsub("%*%s+(.-)\n", "• %1\n")
-- -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them.
-- :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "")
-- print(md)
return fe(out)
end
include "templates.lua"
--[[ MARK: - Stylesheet Parser
The question of custom themes being what it is, and in order to avoid an
unreasonably convoluted settings page, I've opted to allow users to declare
themes simply by editing a stylesheet.
Stylesheets are defined in basic toml format, like so:
```
[button]
color = #faa
[label]
color = #444
[.table_even]
bgcolor = #111
```
Each section begins with a header (denoted by brackets), which specifies the type of element to apply the style to. Prefixing a header name with a dot will cause that header to match elements which are given a certain class.
Rules take the form of `<property> = <value>`. Rules specified prior to any header will apply to all elements.
--]]
local function build_stylesheet_rules(rules)
local out = {}
local offset = 1
while true do
local a, b, key, value = rules:find("(%w+)%s*=%s*([%w%p]+)", offset)
if not a then break end
out[key] = value
offset = b
end
return out
end
function build_stylesheet(sheet)
-- Strip commetns
sheet = sheet:gsub("%-%-.*\n", "")
local out = {}
local last_header
local offset = 1
while true do
local a, b, header = sheet:find("%[%s*([%a._-]+)%s*%]", offset)
if not a then break end
out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset, a -1))
last_header = header
offset = b +1
end
out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset))
return out
end
print(dump(build_stylesheet([[
[button]
color = #faa
[label]
color = #444
]])))
function evaluate_stylesheet()
end
-- MARK: - Views
-- The meta menu, for choosing which game menu to enter or entering the servers/content views.
local last_game = minetest.settings:get("menu_last_game")
function show_meta_menu(v)
state.loc = "games"
local games = minetest.get_games()
if #games < 1 then
minetest.update_formspec(meta_header.."\
hypertext[0,0;"..size.x..","..size.y..";ht;<global valign=middle halign=center><tag name=action color=#222 hovercolor=#444>\
<style size=72 color=#222><b>No games installed.</b></style>\
<style size=36 color=#222><action name=content>Click here to install a game.</action></style>]\
")
return
end
local idx = 1
for i, x in ipairs(games) do
if x.id == last_game then idx = i end
end
if v then idx = v else v = idx end
local fs = ""
fs = fs.."\
scroll_container[0,0;"..size.x..","..size.y..";carousel;horizontal;;]\
box[0,0;"..(#games *5)..",1;#0000]\
"
local center = (size.x /2) +(v /10)
for i, x in ipairs(games) do
fs = fs.."\
image_button["..(center +(i -(#games /2)) *0.25)..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == idx and "circle_light" or "circle")..".png;jump"..i..";]\
"
local dist = i -idx
local scale = 4 /(math.abs(dist) +1)
if scale > 0.1 then
--This looks neat, but is useless for UI purposes: center +10^(1 /math.abs(dist)) *math.sign(dist)
local lc = center +math.abs(dist *10)^0.55 *math.sign(dist)
if x.menuicon_path == "" then
x.menuicon_path = assets.."games.png"
end
--TODO: Do these need some kind of background?
fs = fs.."\
image_button["..(lc -(scale /2))..","..(size.y /2 -(scale /2) +(4 -scale) /2 +2)..";"..scale..","..scale..";"..fe(x.menuicon_path)..";game"..fe(x.id)..";]\
tooltip[game"..fe(x.id)..";"..fe(x.title)..";#444;#aaa]\
"
if dist == 0 then
fs = fs.."\
button["..(lc -(scale /2))..","..(size.y /2 +4)..";4,0.5;nobg;"..fe(x.title).."]\
"
end
end
end
fs = fs.."\
scroll_container_end[]\
scrollbaroptions[min=1;max="..#games..";smallstep=1]\
scrollbar[0,-800;"..size.x..",0;horizontal;carousel;"..(v or "").."]\
"
minetest.update_formspec(meta_header..fs.."\
style[show_servers_view;border=false;bgimg="..fe(assets).."menu_servers.png;bgimg_middle=0]\
style[show_servers_view:hovered;border=false;bgimg="..fe(assets).."menu_servers_hovered.png;bgimg_middle=0]\
button["..(size.x *0.05)..","..(size.y *0.3)..";4,4;show_servers_view;]\
button["..(size.x *0.05)..","..(size.y *0.3 +4)..";4,0.5;nobg;Servers]\
style[show_content_view;border=false;bgimg="..fe(assets).."menu_content.png;bgimg_middle=0]\
style[show_content_view:hovered;border=false;bgimg="..fe(assets).."menu_content_hovered.png;bgimg_middle=0]\
button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3)..";4,4;show_content_view;]\
button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3 +4)..";4,0.5;nobg;Content]\
style[show_settings_view,show_about_view;bgimg="..assets.."menu_tab_bg.png]\
style[show_settings_view:hovered,show_about_view:hovered;bgimg="..assets.."menu_tab_bg.png;bgcolor=#fffd]\
button[1,0;2,0.5;show_about_view;About]\
button["..(size.x -3)..",0;2,0.5;show_settings_view;Settings]\
")
end
-- Shows games' custom menus.
function show_game_menu(args)
if not state.current_game then
minetest.log("warning", "Main menu attempted to show a menu for a nonexistent game; aborting.")
return
end
if not args then args = {} end
local game = state.current_game
if not game.menu then
local file = read_file(game.path.."/menu/mainmenu.txt") or default_game_menu
game.disabled_settings = {}
local settings = Settings(game.path.."/game.conf")
local disabled_settings = string.split(settings:get("disabled_settings") or "", ",")
for _, x in ipairs(disabled_settings) do
x = x:trim()
if x == "enable_damage" then
game.disabled_settings.damage = false
elseif x == "!enable_damage" then
game.disabled_settings.damage = true
elseif x == "creative_mode" then
game.disabled_settings.creative = false
elseif x == "!creative_mode" then
game.disabled_settings.creative = true
elseif x == "enable_server" then
game.disabled_settings.server = false
end
end
local overlays = {}
local backgrounds = {}
local headers = {}
local footers = {}
local music = {}
for _, x in ipairs(minetest.get_dir_list(game.path.."/menu", false)) do
if x:sub(1, string.len("background")) == "background" then
local a, b, id = x:find("background.(%d+).png")
if a or x == "background.png" then
backgrounds[(tonumber(id) or 0) +1] = x
end
elseif x:sub(1, string.len("overlay")) == "overlay" then
local a, b, id = x:find("overlay.(%d+).png")
if a or x == "overlay.png" then
overlays[(tonumber(id) or 0) +1] = x
end
elseif x:sub(1, string.len("header")) == "header" then
local a, b, id = x:find("header.(%d+).png")
if a or x == "header.png" then
headers[(tonumber(id) or 0) +1] = x
end
elseif x:sub(1, string.len("footer")) == "footer" then
local a, b, id = x:find("footer.(%d+).png")
if a or x == "footer.png" then
footers[(tonumber(id) or 0) +1] = x
end
elseif x:sub(1, string.len("theme")) == "theme" then
local a, b, id = x:find("theme.(%d+).ogg")
if a or x == "theme.ogg" then
music[(tonumber(id) or 0) +1] = x
end
end
end
if #backgrounds > 0 then
local path = game.path.."/menu/"..backgrounds[math.random(#backgrounds)]
if minetest.set_background("background", path) then
game.background = path
end
elseif #overlays > 0 then
local path = game.path.."/menu/"..overlays[math.random(#overlays)]
-- I don't know why, but using "overlay" here simply does nothing. Whatever
-- the difference is, it isn't visual, so this effectively 'fixes' it.
if minetest.set_background("background", path) then
game.overlay = path
end
else
minetest.set_background("background", "")
end
if #headers > 0 then
local path = game.path.."/menu/"..headers[math.random(#headers)]
if minetest.set_background("header", path) then
game.header = path
end
end
if #music > 0 then
local path = game.path.."/menu/"..music[math.random(#music)]
if minetest.set_background("header", path) then
game.music = path
end
end
game.menu = template.build(file)
state.menu_vars = {
get_worlds = function() return get_worlds_for_game(game.id) end,
get_mods = function() return get_mods_for_game(game.id) end,
get_world_mods = function(world)
world = world or state.menu_vars.selected_world
return world and get_mods_for_world(state.menu_vars.selected_world) or {}
end
}
state.menu_vars.setting_damage = game.disabled_settings.damage == nil
state.menu_vars.setting_creative = game.disabled_settings.creative == nil
state.menu_vars.setting_server = game.disabled_settings.server == nil
end
if args.show_dialog then
state.menu_current = {args.show_dialog}
elseif args.overlay_dialog then
state.menu_current[#state.menu_current +1] = args.overlay_dialog
elseif args.unoverlay_dialog and #state.menu_current > 1 then
state.menu_current[#state.menu_current] = nil
end
state.menu_vars.WIDTH = size.x
state.menu_vars.HEIGHT = size.y
local fs = ""
if not game.background and not game.overlay then
fs = "\
bgcolor[#000;true;#151618]\
"
end
for i, x in ipairs(state.menu_current) do
if i > 1 then
fs = fs.."\
box[0,0;"..size.x..","..size.y..";#0009]\
"
end
fs = fs..template.evaluate(game.menu[x], state.menu_vars)
end
minetest.update_formspec(game_header..fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
")
end
function add_favorite_server(address, port)
state.favorite_servers[#state.favorite_servers +1] = {address = address, port = port}
local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w")
f:write(minetest.write_json(state.favorite_servers))
f:flush()
f:close()
end
function remove_favorite_server(address, port)
local idx
for i, x in pairs(state.favorite_servers) do
if x.address == address and x.port == port then
idx = i
break
end
end
if idx then
table.remove(state.favorite_servers, idx)
local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w")
f:write(minetest.write_json(state.favorite_servers))
f:flush()
f:close()
end
end
function refresh_server_list()
minetest.handle_async(
function(param)
local http = minetest.get_http_api()
local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format(
"https://servers.luanti.org" or minetest.settings:get("serverlist_url"),
minetest.get_min_supp_proto(),
minetest.get_max_supp_proto()
)
local response = http.fetch_sync{url = url}
if not response.succeeded then
return {}
end
local retval = minetest.parse_json(response.data)
return retval and retval.list or {}
end,
nil,
function(result)
local list = table.copy(state.favorite_servers)
table.insert_all(list, result)
state.serverlist = list
if state.serverlist_filtered then
state.serverlist_filtered = search_server_list(state.servers_filter)
end
if state.loc == "servers" then
show_servers_menu()
end
end
)
end
function search_server_list(input)
if input:trim() == "" then
return nil
end
local search_str = ""
local words = {}
local mods = {}
local games = {}
local players = {}
input = input:lower()
for x in input:gmatch("%S+") do
if x:sub(1, 4) == "mod:" then
mods[#mods +1] = x:sub(5)
elseif x:sub(1, 7) == "player:" then
players[#players +1] = x:sub(8)
elseif x:sub(1, 5) == "game:" then
games[#games +1] = x:sub(6)
else
words[#words +1] = x
end
end
-- print(dump({words = words, mods= mods, players =players, games = games}, " "))
local out = {}
for _, x in ipairs(state.serverlist) do
local passed = true
for _, a in ipairs(words) do
if not (x.description and x.description:lower():find(a, 1, true) or x.name and x.name:lower():find(a, 1, true)) then
passed = false
end
end
-- PUC Lua doesn't have `continue`, hence the indentation.
if passed then
if #games > 0 then
passed = false
end
for _, a in ipairs(games) do
if a == x.gameid then
passed = true
end
end
if passed then
if x.mods and #mods > 0 then
local found = 0
for _, a in ipairs(x.mods) do
for _, b in ipairs(mods) do
if a == b then
found = found +1
end
end
end
passed = found == #mods
else
passed = not not x.mods
end
if passed then
if x.clients_list and #players > 0 then
local found = 0
for _, a in ipairs(x.clients_list) do
for _, b in ipairs(players) do
if a:lower() == b then
found = found +1
end
end
end
passed = found == #players
else
passed = not not x.clients_list
end
if passed then
out[#out +1] = x
end
end
end
end
end
return out
end
-- Shows the server list.
function show_servers_menu()
local fs = ""
local loading
if not state.serverlist then
loading = true
refresh_server_list()
local favorite_servers = minetest.parse_json(read_file(minetest.get_user_path().."/client/serverlist/favoriteservers.json") or "{}")
for _, x in ipairs(favorite_servers) do
x.favorite = true
end
state.favorite_servers = favorite_servers
state.serverlist = favorite_servers
end
-- Overlay dialogs don't occlude area tooltips, so such tooltips are disabled in that case.
local showing_dialog = state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players
local current_server
local joining_server
fs = "\
style[servers_filter;border=false;textcolor=#aaa]\
image["..(size.x *0.095)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.65)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
field["..(size.x *0.095 +0.1)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.45)..",0.75;servers_filter;;"..(state.menu_vars.servers_filter or "").."]\
style[servers_search,servers_refresh,servers_cancel;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
image_button["..(size.x *0.91 -5.65)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."search.png;servers_search;]\
tooltip[servers_search;Search server list;#444;#aaa]\
image_button["..(size.x *0.91 -4.8)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."cancel.png;servers_cancel;]\
tooltip[servers_cancel;Cancel search;#444;#aaa]\
image_button["..(size.x *0.91 -3.95)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."refresh.png;servers_refresh;]\
tooltip[servers_refresh;Refresh server list;#444;#aaa]\
button["..(size.x *0.91 -3.1)..","..(size.y *0.1 -0.85)..";3,0.75;direct_connection;Direct Connection...]\
image["..(size.x *0.095)..","..(size.y *0.1)..";"..(size.x *0.81)..","..(size.y *0.8)..";"..assets.."btn_bg.png;8,8]\
scroll_container[0,"..(size.y *0.1 +0.1)..";"..size.x..","..(size.y *0.8 -0.2)..";serverscroll;vertical;;0,0]\
"
local listx = size.x *0.1
local infox = state.current_server and size.x *0.6 or size.x *0.9
local max = size.x *0.9
local i = 0
for idx, x in ipairs(state.serverlist_filtered or state.serverlist) do
-- Skip incompatible servers. You can't join them, so showing them is rather pointless.
if (x.proto_max or version.proto_min) >= version.proto_min then
local ping_lvl = 0
local lag = (x.lag or 0) * 1000 + (x.ping or 0) * 250
if lag <= 125 then
ping_lvl = 4
elseif lag <= 175 then
ping_lvl = 3
elseif lag <= 250 then
ping_lvl = 2
elseif lag <= 400 then
ping_lvl = 1
end
local name = x.name and x.name:trim() or ""
if name == "" then
name = minetest.colorize("#888", x.address..":"..x.port)
end
local label_offset = 0.1
fs = fs.."\
style[serverinfo"..idx..";border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#"..(i %2 == 1 and "373530" or "403e39").."ff]\
style[serverinfo"..idx..":hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0]\
button["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;serverinfo"..idx..";]\
"
if x.favorite then
fs = fs.."\
image["..(listx)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_favorite.png]\
"..(showing_dialog and "" or "tooltip["..(listx)..","..(i *0.5)..";0.5,0.5;Favorite server;#444;#aaa]\
")
label_offset = 0.6
end
local clients = ""
local color = "#aaa"
local icons_offset = (infox -0.6)
if x.clients then
icons_offset = icons_offset -1
clients = x.clients..(x.clients_max and "/"..x.clients_max or "")
if x.clients > 0 and x.clients_max then
local percent = x.clients /x.clients_max
if percent < 0.75 then
color = "#638b67"
elseif percent < 1 then
color = "#a69174"
else
color = "#9d5b5b"
end
end
fs = fs.."\
hypertext["..icons_offset..","..(i *0.5)..";1,0.5;;<global valign=middle halign=center color="..color..">"..fe(clients).."]\
"
end
if x.pvp == false then
icons_offset = icons_offset -0.5
fs = fs.."\
image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_peaceful.png]\
"..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Peaceful;#444;#aaa]\
")
end
if x.creative then
icons_offset = icons_offset -0.5
fs = fs.."\
image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_creative.png]\
"..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Creative;#444;#aaa]\
")
end
fs = fs.."\
label["..(listx +label_offset)..","..(i *0.5 +0.25)..";"..fe(name).."]\
"..((x.ping or x.lag) and "image["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_icon_ping_"..ping_lvl..".png]\
"..(showing_dialog and "" or "tooltip["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;Ping: "..math.floor(lag)..";#444;#aaa]\
") or "")
i = i +1
end
end
if loading then
fs = fs.."\
hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;<global valign=middle halign=center color=#777>Loading...]\
"
end
if i < 1 then
fs = fs.."\
hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;<global valign=middle halign=center color=#777>No servers found.]\
"
end
fs = fs.."\
scroll_container_end[]\
scrollbar[-800,0;1,1;vertical;serverscroll;"..(state.menu_vars.serverscroll and tonumber(state.menu_vars.serverscroll:sub(5)) or 0).."]\
"
-- Server detail view.
if state.current_server then
fs = fs.."box["..(infox -0.05)..","..(size.y *0.1 +0.05)..";0.1,"..(size.y *0.8 -0.1)..";#292d2fff]\
container["..infox..","..(size.y *0.1).."]\
hypertext[0,0;"..(max -infox)..",0.75;;<global valign=middle halign=center color=#aaa><b>"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."</b>]\
image[0.1,0.75;"..(max -infox -0.2)..",0.5;"..assets.."btn_bg_2_dark.png;8,8]\
box["..((max -infox) *0.7 -0.05)..",0.75;0.1,0.5;#292d2fff]\
hypertext[0.2,0.8;"..((max -infox) *0.7 -0.2)..",0.5;server_address;<global valign=middle color=#aaa>"..fe(hte(state.current_server.address)).."]\
hypertext["..((max -infox) *0.7 +0.1)..",0.8;"..((max -infox) *0.3 -0.2)..",0.5;server_port;<global valign=middle color=#aaa>"..fe(hte(tostring(state.current_server.port))).."]\
"
local buttons_offset = 0.1
if state.current_server.mods then
fs = fs.."\
image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_mods.png;show_server_mods;]\
tooltip[show_server_mods;Show mod list;#444;#aaa]\
"
buttons_offset = buttons_offset +0.5
end
if state.current_server.clients_list then
fs = fs.."\
image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_players.png;show_server_players;]\
tooltip[show_server_players;Show player list;#444;#aaa]\
"
buttons_offset = buttons_offset +0.5
end
if state.current_server.favorite then
fs = fs.."\
image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_unfavorite.png;unfavorite_server;]\
tooltip[unfavorite_server;Remove from favorites;#444;#aaa]\
"
buttons_offset = buttons_offset +0.5
end
local y = buttons_offset > 0.1 and 2.25 or 1.75
fs = fs.."\
hypertext[0.1,"..y..";"..(max -infox -0.2)..","..((size.y *0.8 -1.5 -1.05))..";server_desc;<global color=#aaa>"..fe(hte(state.current_server.description or "")).."]\
button[0.1,"..(size.y *0.8 -1)..";"..(max -infox -0.2)..",0.75;join_server;Join Server]\
container_end[]\
"
end
fs = fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
"
-- Mod list dialog.
if state.showing_server_mods then
fs = fs.."\
box[0,0;"..size.x..","..size.y..";#0008]\
style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\
image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\
scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\
"
for i, x in ipairs(state.current_server.mods) do
fs = fs.."\
button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\
label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\
"
end
fs = fs.."\
scroll_container_end[]\
scrollbar[-800,0;1,1;vertical;modsscroll;]\
button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\
"
end
-- Player list dialog.
if state.showing_server_players then
fs = fs.."\
box[0,0;"..size.x..","..size.y..";#0008]\
style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\
image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\
scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\
"
for i, x in ipairs(state.current_server.clients_list) do
fs = fs.."\
button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\
label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\
"
end
fs = fs.."\
scroll_container_end[]\
scrollbar[-800,0;1,1;vertical;modsscroll;]\
button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\
"
end
-- Connection dialog.
if state.joining_server or state.connecting_to_server then
local offset = 0
fs = fs.."\
box[0,0;"..size.x..","..size.y..";#0008]\
image["..(size.x *0.25 -0.2)..","..(size.y *0.25 -0.1)..";"..(size.x *0.5 +0.4)..","..(size.y *0.5 +0.2)..";"..assets.."btn_bg.png;8,8]\
scroll_container["..(size.x *0.25)..","..(size.y *0.25)..";"..(size.x *0.5)..","..(size.y *0.5 +0.2)..";serverconfscroll;vertical;;0,0]\
"
if state.server_connection_error then
offset = offset +0.75
fs = fs.."\
hypertext[0,0;"..(size.x *0.5)..",0.75;;<global valign=middle color=#9d5b5b>"..fe(hte(state.server_connection_error.msg)).."]\
set_focus["..fe(state.server_connection_error.element)..";true]\
"
else
fs = fs.."\
set_focus[server_"..(state.joining_server and (minetest.settings:get("name") and "password" or "username") or "address")..";true]\
"
end
-- Make the address and port immutable if joining from the serverlist.
if state.joining_server then
fs = fs.."\
hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;<global valign=middle halign=center color=#aaa>Connecting to <b>"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."</b>...]\
"
offset = offset +1
else
fs = fs.."\
set_focus[server_address;true]\
hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;<global valign=middle halign=center color=#aaa>Connecting to server...]\
"
offset = offset +1
fs = fs.."\
image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
label[0.2,"..(0.1 +offset)..";Address]\
field[0.2,"..(0.25 +offset)..";"..(size.x *0.4 -0.4)..",0.75;server_address;;"..(state.menu_vars.server_address or "").."]\
box["..(size.x *0.4 -0.25)..","..(0.25 +offset)..";0.1,0.75;#292d2fff]\
label["..(size.x *0.4 -0.2)..","..(0.1 +offset)..";Port]\
field["..(size.x *0.4)..","..(0.25 +offset)..";"..(size.x *0.1 -0.2)..",0.75;server_port;;"..(state.menu_vars.server_port or "").."]\
"
offset = offset +1
end
fs = fs.."\
image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
label[0.2,"..(0.1 +offset)..";Username]\
field[0.2,"..(0.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_username;;"..minetest.settings:get("name").."]\
image[0,"..(1.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
style[server_password;border=false;textcolor=#aaa;bgimg="..assets.."btn_bg.png]\
label[0.2,"..(1.1 +offset)..";Password]\
pwdfield[0.2,"..(1.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_password;]\
button[0,"..(2.25 +offset)..";"..(size.x *0.5)..",0.75;cancel_join_server;Cancel]\
button[0,"..(3.25 +offset)..";"..(size.x *0.5)..",0.75;confirm_join_server;Join]\
scroll_container_end[]\
scrollbar[-800,0;1,1;vertical;serverconfscroll;"..(state.menu_vars.serverconfscroll and tonumber(state.menu_vars.serverconfscroll:sub(5)) or 0).."]\
"
end
minetest.update_formspec(servers_header..fs)
end
-- Show the content manager.
function show_content_menu()
local fs = ""
if not state.all_content then
get_all_content()
end
local w = size.x
local h = size.y -1.1
if not state.current_package then
fs = "\
scroll_container[0,1.1;"..w..","..h..";contentscroll;horizontal;;]\
"
local cols = math.floor(w /2.5)
local spacing_x = w %2.5 /cols +0.5
local rows = math.floor(h /3)
local spacing_y = h %3 /rows +0.5
local pages = math.ceil(#state.all_content /(rows *cols))
local page = state.content_page or 1
local offset = page /10
local x = 0
local y = 0
local i = (page -1) *(rows *cols) +1
while true do
local pkg = state.all_content[i]
if not pkg then break end
if not pkg.icon then
pkg.icon = pkg.type == "game" and pkg.path.."/menu/icon.png" or pkg.path.."/icon.png"
if not file_exists(pkg.icon) then
pkg.icon = assets.."menu_content.png"
end
end
local title = pkg.title:trim()
if title == "" then
title = pkg.name
end
fs = fs.."\
image_button["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2))..";2,2;"..fe(pkg.icon)..";view_package_"..i..";]\
tooltip[view_package_"..i..";"..fe(title)..";#444;#aaa]\
hypertext["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2) +2)..";2,0.75;nobg;<global color=#aaa valign=middle halign=center>"..fe(hte(title)).."]\
"
i = i +1
x = x +1
if x > cols -1 then
x = 0
y = y +1
if y > rows -1 then break end
end
end
fs = fs.."\
box[0,0;"..(pages *w)..",1;#0000]\
scroll_container_end[]\
scrollbaroptions[min=1;max="..pages..";smallstep=1]\
scrollbar[-800,0;1,1;horizontal;contentscroll;"..page.."]\
"
fs = fs.."\
box[0,0;"..size.x..",1;#403e39ff]\
box[0,1;"..size.x..",0.1;#292d2fff]\
style[content_search,content_cancel,go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\
image_button[0.125,0.125;0.75,0.75;"..assets.."search.png;content_search;]\
tooltip[content_search;Search;#444;#aaa]\
image_button[1,0.125;0.75,0.75;"..assets.."cancel.png;content_cancel;]\
tooltip[content_cancel;Cancel;#444;#aaa]\
style[test;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
style[test:hovered;bgimg="..assets.."white.png;bgimg_middle=0]\
image[2,0.125;"..(size.x -9)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
field[2.1,0.125;"..(size.x -9.2)..",0.75;content_filter;;]\
button["..(w -6)..",0;4,1;test;Browse Online Content...]\
image_button["..(w -1.5)..",0.125;0.75,0.75;"..assets.."new_package.png;new_package;]\
tooltip[new_package;New Package...;#444;#aaa]\
"
local start = w /2 -(pages *0.125)
for i = 1, pages do
fs = fs.."\
image_button["..(start +(i *0.25))..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == page and "circle_light.png" or "circle.png")..";page"..i..";]\
"
end
else
local pkg = state.all_content[state.current_package]
local desc = ""
if file_exists(pkg.path.."/README.md") then
desc = read_file(pkg.path.."/README.md")
end
local pkg_type = pkg.type
if pkg_type == "mod" then
pkg_type = "<style color="..theme.mod_label_color..">\\[Mod\\]</style>"
elseif pkg_type == "modpack" then
pkg_type = "<style color="..theme.mod_label_color..">\\[Mod Collection\\]</style>"
elseif pkg_type == "game" then
pkg_type = "<style color="..theme.game_label_color..">\\[Game\\]</style>"
elseif pkg_type == "txp" then
pkg_type = "<style color="..theme.texture_pack_label_color..">\\[Texture Pack\\]</style>"
end
fs = fs.."\
style[go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\
image_button["..(size.x -1)..",0.5;0.5,0.5;"..assets.."cancel.png;go_back;]\
imdage["..(size.x *0.1 -0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.6)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\
hypertext["..(size.x *0.1)..","..(size.y *0.1)..";"..(size.x *0.6 -0.2)..","..(size.y *0.8)..";aa;\
<global halign=center color="..theme.text_color..">\
"..markdown_to_hypertext(desc).."\
]\
image["..(size.x *0.7 +0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.2 +0.2)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\
hypertext["..(size.x *0.7 +0.2)..","..(size.y *0.1)..";"..(size.x *0.2)..","..(size.y *0.8)..";bb;\
<global halign=center color="..theme.text_color..">\
<img name='"..fe(hte(pkg.icon)).."' width="..(size.x *0.1 *dpi.x).." height="..(size.x *0.1 *dpi.y)..">\
<center><big>"..fe(hte(pkg.title or pkg.name)).."</big></center>\
"..pkg_type.." <style color="..theme.muted_text_color..">"..fe(hte(pkg.name)).."</style>\
Creator: <b>"..fe(hte(pkg.author)).."</b>\
Location: "..(pkg.path:sub(2):gsub("/", "<style size=12> \\> </style>")).."\
"..(pkg.type == "modpack" and "Modules: "..#minetest.get_dir_list(pkg.path, true) or "").."\
]\
button["..(size.x *0.7 +0.2)..","..(size.y *0.9 -0.75)..";"..(size.x *0.2)..",0.75;browse_source;Browse Source]\
"
end
fs = fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
"
minetest.update_formspec(content_header..fs)
end
function show_about_menu()
if not state.lua_api then
state.lua_api = {}
state.lua_api_sections = {}
-- TODO: Make this portable
local doc = read_file(minetest.get_builtin_path().."/../luanti/lua_api.md")
local offset = 1
while true do
local a, b, heading = doc:find("([^\n]+)\n=+\n", offset)
if not a then break end
if offset > 1 then
state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset, a -1)
end
state.lua_api_sections[#state.lua_api_sections +1] = heading
offset = b
end
state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset)
end
local fs = "\
style[docs_section;bgimg="..assets.."white.png;bgcolor=#fff0]\
style[docs_section:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
box[0,0;"..(size.x *0.25 -0.55)..","..size.y..";#403e39ff]\
box["..(size.x *0.25 -0.55)..",0;0.1,"..size.y..";#292d2fff]\
scroll_container[0,0;"..(size.x *0.25 -0.55)..","..size.y..";sidebarscroll;vertical;;0,0]\
"
for i, x in ipairs(state.lua_api_sections) do
fs = fs.."\
box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\
button[0,"..((i -1) *0.8)..";"..(size.x *0.25 -0.55)..",0.75;docs_section;"..fe(x).."]\
"
end
fs = fs.."\
scroll_container_end[]\
scrollbar[-800,-800;0,0;vertical;sidebarscroll;"..(state.menu_vars.sidebarscroll and state.menu_vars.sidebarscroll:sub(5) or "").."]\
hypertext["..(size.x *0.25)..",0;"..size.x..","..size.y..";aa;<global color="..theme.text_color..">\
"..markdown_to_hypertext(state.lua_api[state.menu_vars.docs_section or state.lua_api_sections[1]]).."\
]"
fs = fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
"
minetest.update_formspec(content_header..fs)
end
function show_settings_menu()
if not state.settings then
state.settings = {}
state.settings_categories = {}
local settings = settingtypes.parse_config_file(minetest.get_builtin_path().."/settingtypes.txt")
for _, x in ipairs(settings) do
if x.type == "category" then
if x.level == 1 and not x.name:find "Hide:" then
state.settings_categories[#state.settings_categories +1] = x.name
state.settings[x.name] = {}
end
else
local s = state.settings[state.settings_categories[#state.settings_categories]]
s[#s +1] = x
end
end
end
local fs = "\
box[0,0;"..(size.x *0.25 -0.75)..","..size.y..";#403e39ff]\
box["..(size.x *0.25 -0.75)..",0;0.1,"..size.y..";#292d2fff]\
scroll_container[0,0;"..(size.x *0.25 -0.75)..","..size.y..";sidebarscroll;vertical;;0,0]\
style[show_theme;bgimg="..assets.."white.png;bgcolor=#fff0]\
style[show_theme:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
button[0,0;"..(size.x *0.25 -0.55)..",0.75;show_theme;Theme]\
"
for i, category in ipairs(state.settings_categories) do
fs = fs.."\
style[show_category;bgimg="..assets.."white.png;bgcolor=#fff0]\
style[show_category:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\
button[0,"..(i *0.8)..";"..(size.x *0.25 -0.55)..",0.75;show_category;"..fe(category).."]\
"
end
fs = fs.."scroll_container_end[]\
scrollbar[-800,-800;0,0;vertical;sidebarscroll;]\
scroll_container["..(size.x *0.25)..",0;"..(size.x *0.75)..","..size.y..";settingsscroll;vertical;;0,0]\
"
if state.settings_category then
local page = state.settings[state.settings_category]
local y = 0.25
for i, x in ipairs(page) do
if x.type == "string" then
fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\
image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\
button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\
"
y = y +1.2
elseif x.type == "bool" then
local state = minetest.settings:get_bool(x.name, x.default)
fs = fs.."image_button[0,"..y..";0.75,0.75;"..assets..(state and "checkbox_filled" or "checkbox_empty")..".png;toggle_setting_"..fe(x.name)..";]\
label[1,"..(y +0.375)..";"..fe(x.readable_name).."]\
"
y = y +1
elseif x.type == "int" or x.type == "float" then
fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\
image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\
button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\
"
y = y +1.2
end
end
end
fs = fs.."\
scroll_container_end[]\
scrollbar[-800,-800;0,0;vertical;settingsscroll;]\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
"
minetest.update_formspec(content_header..fs)
end
function minetest.button_handler(data)
if not state.menu_vars then state.menu_vars = {} end
setmetatable(state.menu_vars, {__index = data})
if state.loc == "games" then
if data.content or data.ht == "action:content" then
if state.loc ~= "content" then show_content() end
elseif data.games then
if state.loc ~= "games" then show_meta_menu() end
elseif data.carousel and data.carousel:sub(1, 4) == "CHG:" then
local v = tonumber(data.carousel:sub(5))
show_meta_menu(v)
else
for k, v in pairs(data) do
if k:sub(1, 4) == "game" then
local id = k:sub(5)
state.current_game = get_game_info(id)
state.menu_current = {"main"}
state.loc = "game"
state.menu_vars = {}
show_game_menu()
minetest.settings:set("menu_last_game", id)
last_game = id
elseif k:sub(1, 4) == "jump" then
local v = tonumber(k:sub(5))
show_meta_menu(v)
end
end
end
elseif state.loc == "servers" then
if data.servers_refresh then
refresh_server_list()
elseif data.servers_search or data.key_enter_field == "servers_filter" then
state.servers_filter = data.servers_filter
state.serverlist_filtered = search_server_list(data.servers_filter)
show_servers_menu()
elseif data.servers_cancel then
state.serverlist_filtered = nil
state.servers_filter = nil
data.servers_filter = ""
show_servers_menu()
elseif data.unfavorite_server then
state.current_server.favorite = nil
remove_favorite_server(state.current_server.address, state.current_server.port)
show_servers_menu()
elseif data.show_server_mods then
state.showing_server_mods = true
show_servers_menu()
elseif data.show_server_players then
state.showing_server_players = true
show_servers_menu()
elseif data.close_dialog then
state.showing_server_players = nil
state.showing_server_mods = nil
show_servers_menu()
elseif data.direct_connection then
state.connecting_to_server = true
show_servers_menu()
elseif data.join_server then
state.joining_server = true
show_servers_menu()
elseif data.cancel_join_server then
state.joining_server = nil
state.connecting_to_server = nil
show_servers_menu()
elseif data.confirm_join_server or data.key_enter_field == "server_username" or data.key_enter_field == "server_password" or data.key_enter_field == "server_address" then
minetest.settings:set("name", data.server_username)
local address
local port
if state.connecting_to_server then
if not tonumber(data.server_port) then
state.server_connection_error = {
msg = "Invalid port.",
element = "server_port"
}
show_servers_menu()
state.server_connection_error = nil
return
end
address = data.server_address:lower()
port = data.server_port:lower()
else
address = state.current_server.address:lower()
port = state.current_server.port
end
local is_favorite = false
for _, x in ipairs(state.favorite_servers) do
if x.address == address and x.port == port then
is_favorite = true
end
end
if not is_favorite then
add_favorite_server(address, port)
end
minetest.settings:set("address", address)
minetest.settings:set("remote_port", port)
gamedata = {
playername = data.server_username,
password = data.server_password,
address = address,
port = port,
selected_world = 0,
singleplayer = false
}
minetest.start()
else
for k, v in pairs(data) do
if k:sub(1, string.len("serverinfo")) == "serverinfo" then
local idx = k:sub(string.len("serverinfo>"))
if idx:sub(1, 1) == "f" then
state.current_server = state.favorite_servers[tonumber(idx:sub(2))]
else
state.current_server = (state.serverlist_filtered or state.serverlist)[tonumber(idx)]
end
show_servers_menu()
end
end
end
elseif state.loc == "content" then
if data.contentscroll and data.contentscroll:sub(1, 4) == "CHG:" then
state.content_page = tonumber(data.contentscroll:sub(5))
show_content_menu()
elseif data.go_back then
state.current_package = nil
show_content_menu()
else
for k, v in pairs(data) do
if k:sub(1, 4) == "page" then
state.content_page = tonumber(k:sub(5))
show_content_menu()
elseif k:sub(1, string.len("view_package_")) == "view_package_" then
state.current_package = tonumber(k:sub(string.len("view_package_>")))
show_content_menu()
end
end
end
elseif state.loc == "game" then
for k, v in pairs(data) do
if k == ".play" then
gamedata = {
playername = "singleplayer",
password = "",
address = nil,
port = nil,
selected_world = get_world_index(state.menu_vars.selected_world),
singleplayer = true
}
minetest.start()
elseif k:sub(1, string.len(".show_view_")) == ".show_view_" then
show_game_menu {
show_dialog = k:sub(string.len(".show_view_>"))
}
elseif k:sub(1, string.len(".overlay_view_")) == ".overlay_view_" then
show_game_menu {
overlay_dialog = k:sub(string.len(".overlay_view_>"))
}
elseif k:sub(1, string.len(".select_world_")) == ".select_world_" then
state.menu_vars.selected_world = k:sub(string.len(".select_world_>"))
state.menu_vars.selected_world_name = k:match "/([^/]+)$" or "<unknown world>"
local conf = Settings(state.menu_vars.selected_world.."/world.mt")
local de = conf:get("enable_damage")
if de == "true" then
state.menu_vars.damage_enabled = true
elseif de == "false" then
state.menu_vars.damage_enabled = false
else
state.menu_vars.creative_damage = not state.current_game.disabled_settings.damage
end
local cm = conf:get("creative_mode")
if cm == "true" then
state.menu_vars.creative_enabled = true
elseif cm == "false" then
state.menu_vars.creative_enabled = false
else
state.menu_vars.creative_enabled = not not state.current_game.disabled_settings.creative
end
show_game_menu()
elseif k == ".unoverlay_dialog" then
show_game_menu {
unoverlay_dialog = true
}
elseif k:sub(1, string.len(".set_")) == ".set_" then
local name, value = k:match "%.set_|(.+)|_to_(.*)"
state.menu_vars[name] = minetest.parse_json(value:gsub("\\([];,$\\[])", "%1"))
show_game_menu()
end
end
elseif state.loc == "about" then
if data.docs_section then
show_about_menu()
end
elseif state.loc == "settings" then
if data.show_category then
state.settings_category = data.show_category
show_settings_menu()
else
for k, v in pairs(data) do
end
end
end
if data.to_meta_menu then
state.current_game = nil
state.loc = "games"
show_meta_menu()
elseif data.show_servers_view then
state.current_game = nil
state.loc = "servers"
show_servers_menu()
elseif data.show_content_view then
state.current_game = nil
state.loc = "content"
show_content_menu()
elseif data.show_about_view then
state.current_game = nil
state.loc = "about"
show_about_menu()
elseif data.show_settings_view then
state.current_game = nil
state.loc = "settings"
show_settings_menu()
end
end
function minetest.event_handler(ev)
-- When Esc is pressed, close the current dialog, or the game if we are on the meta menu.
if ev == "MenuQuit" then
if state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players then
state.joining_server = nil
state.connecting_to_server = nil
state.showing_server_mods = nil
state.showing_server_players = nil
show_servers_menu()
elseif state.loc ~= "games" then
show_meta_menu()
else
minetest.close()
end
end
end
state.theme = build_stylesheet("")
minetest.set_clouds(false)
if last_game then
state.current_game = get_game_info(last_game)
if state.current_game then
state.menu_current = {"main"}
state.loc = "game"
show_game_menu()
else
show_meta_menu()
end
else
show_meta_menu()
end
--]]