gamedata = {
}
local http = minetest.get_http_api()
-- Used for brevity when making fullscreen things
window = minetest.get_window_info()
size = window.max_formspec_size
-- Default textures
default_textures = minetest.get_texturepath_share().."/base/pack/"
--FIXME: Assets
local assets = "/Users/iboettcher/eclipse-workspace/mods/mtmenu/"
local version = minetest.get_version()
-- Holds all the interface parts
state = {}
local fe = minetest.formspec_escape
local hte = minetest.hypertext_escape
---[[
-- Prepended to the meta menu's formspec.
local meta_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
style[*;textcolor=#aaa]\
style_type[image_button;border=false]\
style_type[button;border=false;bgimg="..assets.."btn_bg.png;bgimg_middle=8,8]\
bgcolor[#000;true;#151618]\
style_type[box;bordercolors=#124722;borderwidths=-5;colors=#151618]\
box[0,0;"..size.x..","..size.y..";]\
image[0,"..(size.y *0.08)..");"..size.x..","..(size.x *(72/672))..";"..default_textures.."menu_header.png]\
"
local meta_footer = "\
style[nobg;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0]\
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]\
"
-- Prepended to game menus' formspecs.
local game_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
style[*;textcolor=#aaa]\
style_type[image_button;border=false]\
style_type[button;border=false;bgimg="..assets.."btn_bg.png;bgimg_middle=8,8]\
bgcolor[#0000;true;#0000]\
"
local servers_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
style[*;textcolor=#aaa]\
style_type[image_button;border=false]\
style_type[field;border=false]\
style_type[pwdfield;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]\
bgcolor[#0000;true;#151618]\
"
local content_header = "formspec_version[8]\
size["..size.x..","..size.y.."]\
padding[0,0]\
style[*;textcolor=#aaa]\
style_type[image_button;border=false]\
style_type[button;border=false;bgimg="..assets.."btn_bg.png;bgimg_middle=8,8]\
bgcolor[#0000;true;#151618]\
"
-- The default main menu for games.
local default_game_menu = [[
enable_clouds = true
@set:test:Hello
label[2,2;Test]
scroll_container[1,6;4,2;worldscroll;vertical;;0,0]
@foreach:$WORLDS:worlds
@if:@i % 2:sel
style[.select_world_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
style[.select_world_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
style[.select_world_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
@else:sel
style[.select_world_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
style[.select_world_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
style[.select_world_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
@endif:sel
button[0,${@i * 0.5};4,0.5;.select_world_${@name};${@test}]
@endforeach:worlds
scroll_container_end[]
scrollbaroptions[arrows=hide]
scrollbar[-800,6;0,2;vertical;worldscroll;]
label[2,2;Add World, asset path is $ASSET_PATH]
button[2,3;3,1;.show_dialog_main;back]
@foreach:[hello,there,bob]:myloop
label[6,${@i + 3};${@item}]
@endforeach:myloop
]]
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 function
local serialized_func = string.dump(func)
assert(serialized_func ~= nil)
-- Serialize parameters
local serialized_param = minetest.serialize(parameter)
if serialized_param == nil then
return false
end
local jobid = minetest.do_async_callback(serialized_func, serialized_param)
minetest.async_jobs[jobid] = callback
return true
end
-- MARK: - Helpers
-- 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_worlds_for_game(id)
local out = {}
for _, x in ipairs(minetest.get_worlds()) do
if x.gameid == id then
if math.random() > 0.5 then x.selected = true end
out[#out +1] = x
end
end
return out
end
-- Returns a list of content available for download.
function get_all_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
-- 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
--[[ MARK: - Template Engine
Builds a formspec from the contents of a game-provided main menu file.
=================================================================================
Menu File Documentation
=================================================================================
Menu files are essentially just formspecs, but with special semantics and extended syntax.
First, menu files are made up of a number of dialog definitions. A dialog definition is
basically an HTML tag, where the tag name is the name of the dialog. For example this:
```
label[2,2;A label]
button[1,1;4,1;button;This is a button]
```
will define a dialog named 'main', with a label in it, and a dialog named 'other',
with a button in it. Every menu file must have a dialog named 'main', which serves
as the entry point of the game's menu.
Being able to define these other dialogs would be pretty pointless if they couldn't
be used for anything. Accordingly, you can use standard formspec actions, e.g. buttons,
to segue to a different dialog. To do this, set the action name to
'.show_dialog_', where is the name of the dialog you want to open.
You can also use '.overlay_dialog_' to draw the target dialog on top of the
current dialog, and '.unoverlay_dialog' to hide it again.
There are several other action patterns that invoke special behavior:
-
- '.play': Start the game.
- '.play_with_world_named_': Start the game on the specified world. If the
world in question does not exist, it will be created.
It is also possible to define metadata for the main menu. To do this, create a dialog
section named 'meta'. The contents of this section will then be parsed in the same way
as minetest.conf instead of being treated as a dialog.
Available metadata options are:
- 'enable_clouds': true/false (defaults to false)
Now, all this is good when we know exactly what content we want on the main menu.
But what if we want to render e.g. a list of worlds, or even a list of game modes?
This can be done using the @foreach construct. @foreach takes in a list of values,
the name of a JSON file, or a reference to a menu-exposed list.
The major advantage of a foreach loop, however, is interpolation. Inside a foreach
loop, the pattern '${}' will be replaces with the result of .
Inside an expression, you can reference variables as '@'. Which variables are
available depends on the iterated list. In the case of a list literal, the current
item is exposed as a variable named @item, while for a JSON file, each key in the
file's top-level object will correspond to a variable. (Note that variable names may
only contain letters or numbers, even if mapped from a JSON file.)
The menu-provided lists are:
- $WORLDS: The list of worlds for this game. Exposes @name (the world name),
@path (the world's full absolute path), @gameid (the ID of the current game),
and @selected (whether the world is currently selected).
Expressions also support rudimentary mathematical operations, namely addition (+),
subtraction (+-), multiplication (*), division (/), and exponentiation (^). Trying
to perform math on a non-tonumber()-able variable will treat the variable as 0.
Grouping is not supported (yet).
As an example, this:
```
label[1,1;Worlds:]
@foreach:$WORLDS:name
label[1,${@i + 1};World named ${@name}]
@endforeach:name
@foreach:[one,two,three]:name2
label[4,${@i + 1};Item ${@i} is named: ${@item}]
@endforeach:name2
```
will:
- Create a "Worlds" label at 1,1;
- Create a label for every world with that world's name and a Y coordinate that
corresponds to the world's position in the list, plus 1 so these labels start
below the existing label.
- Create labels for "one", "two", and "three" that display the item's value and
its position in the list, with similarly increasing Y-coordinates.
Besides foreach loops, menu files also support conditionals. Conditionals are
written as '@if:: ... @else: ... @endif:', where
is any expression. The conditional will be replaced with the contents
of its first block if the condition evaluates to non-zero, and the contents of
its second block otherwise.
Example:
```
@if: @name = test :
@else:
@endif:
```
Note: Because of the way the main menu works, image paths must be specified in full.
To make this non-painful, when referencing images use '$ASSET_PATH/'
instead of just the image name. $ASSET_PATH will be replaced with the actual path
to the game's menu/ directory, so images used by the main menu should be stored
there. You can use $ASSET_PATH in any context. Additionally, $DEFAULT_ASSET_PATH is
the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, in
case you need to stylistically unset a background image defined by a global style.
--]]
local function build_template_dialog(fs)
if fs:trim() == "" then return end
local dialog = {}
local i = 0
-- "(.-\n?)%s-@foreach:([^:]+):(%l*)\n(.-)\n%s-@endforeach:%3(\n?.*)"
-- Extract foreach loops
local prev = 1
while i < 1000 do
local a, b, pattern, name, content = fs:find("@foreach:([^:]+):(%w*)\n(.-)\n%s-@endforeach:%2", prev)
if not a then break end
-- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, a -1))
dialog[#dialog +1] = {
foreach = pattern:trim(),
name = name,
content = build_template_dialog(content:trim())
}
prev = b
i = i +1
end
if i > 0 then
dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1))
else
-- Extract conditionals
prev = 0
while i < 1000 do
local a, b, expr, name, content, else_content = fs:find("@if:([^:]+):(%w*)\n(.-)\n%s-@else:%2\n(.-\n?)@endif:%2", prev)
if not a then break end
-- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
dialog[#dialog +1] = fs:sub(prev +1, a -1)
dialog[#dialog +1] = {
condition = expr:trim(),
name = name,
content = build_template_dialog(content:trim()),
else_content = build_template_dialog(else_content:trim())
}
prev = b
i = i +1
end
if i > 0 then
dialog[#dialog +1] = fs:sub(prev +1)
end
end
if #dialog == 1 then dialog = dialog[1] end
if i == 0 then dialog = fs end
-- minetest.log(dump(dialog, " "))
return dialog
end
local function build_game_menu(input)
-- MARK: Environment variables
input = input
:gsub("%$ASSET_PATH", fe(state.current_game.path.."/menu"))
:gsub("%$DEFAULT_ASSET_PATH", fe(assets:sub(1, #assets -1)))
:gsub("%$NO_IMAGE", fe(default_textures.."blank.png"))
:gsub("/%*.-%*/", "")
local menu = {}
for name, fs in input:gmatch("<(%l+)>(.-)%1>") do
if name == "meta" then
menu[name] = parse_conf_text(fs)
else
menu[name] = build_template_dialog(fs)
end
end
return menu
end
-- Split a template expression into a binary-tree node.
local function split_template_expression(expr)
-- These are basically the operator definitions, listed from lowest
-- (evaluated last) to highest (evaluated first) precedence.
local a, b, op
-- Or
if not a then
a, b, op = expr:find("(|)")
end
-- And
if not a then
a, b, op = expr:find("(&)")
end
-- Greater than
if not a then
a, b, op = expr:find("(>)")
end
-- Greater than or equal to
if not a then
a, b, op = expr:find("(>=)")
end
-- Less than
if not a then
a, b, op = expr:find("(<)")
end
-- Less than or equal to
if not a then
a, b, op = expr:find("(<=)")
end
-- Equal to
if not a then
a, b, op = expr:find("(==)")
end
if not a then
a, b, op = expr:find("(=)")
end
-- Addition
if not a then
a, b, op = expr:find("(+)")
end
-- Multiplication
if not a then
a, b, op = expr:find("(*)")
end
-- Division
if not a then
a, b, op = expr:find("(/)")
end
-- Modulo
if not a then
a, b, op = expr:find("(%%)")
end
-- Exponent
if not a then
a, b, op = expr:find("(%^)")
end
if not a then
return {value = expr}
end
return {
op = op,
lhs = split_template_expression(expr:sub(1, a -1)),
rhs = split_template_expression(expr:sub(b +1))
}
end
-- Reduce a template expression from a binary-tree node into a single value.
local function reduce_template_expression(tree)
if tree.op == "|" then
return (reduce_template_expression(tree.lhs) ~= "0" or reduce_template_expression(tree.rhs) ~= "0") and "1" or "0"
elseif tree.op == "&" then
return (reduce_template_expression(tree.lhs) ~= "0" and reduce_template_expression(tree.rhs) ~= "0") and "1" or "0"
elseif tree.op == ">" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) > (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
elseif tree.op == ">=" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) >= (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
elseif tree.op == "<" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) < (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
elseif tree.op == "<=" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) <= (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
elseif tree.op == "=" or tree.op == "==" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) == (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
elseif tree.op == "+" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) + (tonumber(reduce_template_expression(tree.rhs)) or 0)
elseif tree.op == "*" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) * (tonumber(reduce_template_expression(tree.rhs)) or 0)
elseif tree.op == "/" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) / (tonumber(reduce_template_expression(tree.rhs)) or 0)
elseif tree.op == "%" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) % (tonumber(reduce_template_expression(tree.rhs)) or 0)
elseif tree.op == "^" then
return (tonumber(reduce_template_expression(tree.lhs)) or 0) ^ (tonumber(reduce_template_expression(tree.rhs)) or 0)
else
return tree.value
end
end
-- Evaluate an interpolation expression, with an optional table of variables.
local function evaluate_template_expression(expr, vars, depth)
if expr == "" then return "0" end
if not depth then depth = 0 end
-- This handles the case where vars is omitted, because then it ends up
-- just setting vars to an empty table.
if type(vars) ~= "table" then
vars = {item = vars}
end
-- Expand all variables so we can deal with a constexpr
local offset = 1
while offset < 100000 do
local a, b, name = expr:find("@([%a]+)", offset)
if not a then break end
-- If referencing an undefined variable, default to 0 because it's safest that way.
local result = minetest.formspec_escape(tostring(vars[name] or "0"))
expr = expr:sub(1, a -1)..result..expr:sub(b +1)
offset = a +#result
end
-- If there are no operators, this is a constant expression and we can just return.
if not expr:find("%p") then return expr end
-- Expression parsing
local tree = split_template_expression(expr)
return tostring(reduce_template_expression(tree))
end
-- Do variable interpolation for the given formspec using the provided variable table.
local function evaluate_template_block(fs, vars)
-- Assignment statements
local offset = 1
while offset < #fs do
local a, b, name, expr = fs:find("@set:(%w+):(.-)\n", offset)
if not a then break end
vars[name] = evaluate_template_expression(expr, vars)
fs = fs:sub(1, a -1)..fs:sub(b +1)
offset = a
end
-- Interpolations
offset = 1
while offset < #fs do
local a, b, expr = fs:find("%${([^}]-)}", offset)
if not a then break end
local result = evaluate_template_expression(expr, vars)
fs = fs:sub(1, a -1)..result..fs:sub(b +1)
offset = a +#result
end
return fs
end
-- Interpret the list expression of a foreach loop, then iterate.
local function evaluate_template_foreach(loop, vars)
local out = ""
local list = {}
if loop.foreach:sub(1,1) == "[" then
list = loop.foreach:gsub("^%[(.*)%]$", "%1"):split(",")
elseif loop.foreach == "$WORLDS" then
list = get_worlds_for_game(state.current_game.id)
end
for i, x in ipairs(list) do
local vars2 = {}
if type(x) ~= "table" then
vars2.item = x
else
for k, v in pairs(x) do
vars2[k] = v
end
end
vars.i = i
out = out..evaluate_game_dialog(loop.content, setmetatable(vars2, {__index = vars, __newindex = vars})).."\n"
end
return out
end
-- Determine which branch of an if statement should be evaluated.
local function evaluate_template_conditional(cond, vars)
local out = ""
local list = {}
local condition = evaluate_template_expression(cond.condition, vars)
if condition ~= "0" then
out = out..evaluate_game_dialog(cond.content, vars).."\n"
else
out = out..evaluate_game_dialog(cond.else_content, vars).."\n"
end
return out
end
-- Game dialogs might contain e.g. foreach loops, so build those if needed.
function evaluate_game_dialog(dialog, vars)
local out = ""
if type(dialog) == "string" then return evaluate_template_block(dialog, vars) end
for _, x in ipairs(dialog) do
if type(x) == "string" then
out = out..evaluate_template_block(x, vars)
elseif x.condition then
out = out..evaluate_template_conditional(x, vars)
elseif x.foreach then
out = out..evaluate_template_foreach(x, vars)
else
for _, c in ipairs(x) do
out = out..evaluate_game_dialog(c, vars)
end
end
end
return out
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;\
\
]\
")
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 cool, 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)
local test = io.open(x.menuicon_path)
if test then
test:close()
else
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.."\
style[_game_label;bgimg="..default_textures.."blank.png]\
button["..(lc -(scale /2))..","..(size.y /2 +4)..";4,0.5;_game_label;"..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..meta_footer)
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
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
game.menu = build_game_menu(file)
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
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
fs = fs..evaluate_game_dialog(game.menu[x], setmetatable({WIDTH = size.x, HEIGHT = size.y}, {__index = state.menu_vars}))
end
minetest.update_formspec(game_header..fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
")
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)
state.serverlist = result
-- If we need to load the server list, we need to read the list of favorite servers as well.
local favorite_servers = minetest.parse_json(read_file(minetest.get_user_path().."/client/serverlist/favoriteservers.json") or "{}")
if #favorite_servers > 0 then
for _, x in ipairs(favorite_servers) do
x.favorite = true
end
table.insert_all(favorite_servers, state.serverlist)
state.serverlist = favorite_servers
end
state.favorite_servers = favorite_servers
if state.loc == "servers" then
show_servers_menu()
end
end
)
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.serverlist = favorite_servers
-- fs = "\
-- hypertext[0,0;"..size.x..","..size.y..";ht;\
-- ]\
-- "
end
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 -2.55)..",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 -2.2)..",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 -0.85)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."refresh.png;servers_refresh;]\
image_button["..(size.x *0.91 -1.7)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."cancel.png;servers_cancel;]\
image_button["..(size.x *0.91 -2.55)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."search.png;servers_search;]\
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
-- if #state.favorite_servers > 0 then
-- fs = fs.."\
-- box["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;#"..(i %2 == 1 and "373530" or "403e39").."ff]\
-- box["..(listx -0.05)..","..(i *0.5 +0.2)..";"..(infox -listx +0.1)..",0.1;#292d2fff]\
-- box["..((infox -listx) /2 -1.5)..","..(i *0.5)..";3,0.5;#"..(i %2 == 1 and "373530" or "403e39").."ff]\
-- box["..((infox -listx) /2 -1.55)..","..(i *0.5 +0.125)..";0.1,0.25;#292d2fff]\
-- box["..((infox -listx) /2 +1.45)..","..(i *0.5 +0.125)..";0.1,0.25;#292d2fff]\
-- hypertext["..((infox -listx) /2 -1.5)..","..(i *0.5)..";3,0.5;;Favorite Servers]\
-- "
-- i = i +1
-- end
-- for idx, x in ipairs(state.favorite_servers) do
-- if x.address and x.port then
-- local address = minetest.encode_base64(x.address)
-- if address == state.current_server then
-- current_server = x
-- end
-- if address == state.joining_server then
-- joining_server = x
-- end
-- fs = fs.."\
-- style[serverinfof"..idx..";border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#"..(i %2 == 1 and "373530" or "403e39").."ff]\
-- style[serverinfof"..idx..":hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0]\
-- button["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;serverinfof"..idx..";]\
-- label["..(listx +0.1)..","..(i *0.5 +0.25)..";"..fe(x.address..":"..x.port).."]\
-- "
-- i = i +1
-- end
-- end
-- if i > 0 then
-- fs = fs.."\
-- box["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;#"..(i %2 == 1 and "373530" or "403e39").."ff]\
-- box["..(listx -0.05)..","..(i *0.5 +0.2)..";"..(infox -listx +0.1)..",0.1;#292d2fff]\
-- box["..((infox -listx) /2 -1.5)..","..(i *0.5)..";3,0.5;#"..(i %2 == 1 and "373530" or "403e39").."ff]\
-- box["..((infox -listx) /2 -1.55)..","..(i *0.5 +0.125)..";0.1,0.25;#292d2fff]\
-- box["..((infox -listx) /2 +1.45)..","..(i *0.5 +0.125)..";0.1,0.25;#292d2fff]\
-- hypertext["..((infox -listx) /2 -1.5)..","..(i *0.5)..";3,0.5;;Public Servers]\
-- "
-- i = i +1
-- end
for idx, x in ipairs(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
-- Otherwise, the colons in an IPv6 address would make the style element blow up.
--local address = minetest.encode_base64(x.address)
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
-- The overlay dialog doesn't occlude area tooltips, so they are disabled in that case.
fs = fs.."\
image["..(listx)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_favorite.png]\
"..(state.joining_server and "" or "tooltip["..(listx)..","..(i *0.5)..";0.5,0.5;Favorite server;#444;#aaa]\
")
label_offset = 0.6
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]\
"..(state.joining_server 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;;Loading...]\
"
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).."]\
"
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;;"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."]\
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;"..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;"..fe(hte(tostring(state.current_server.port))).."]\
hypertext[0.1,1.5;"..(max -infox -0.2)..","..((size.y *0.8 -1.5 -1.05))..";server_desc;"..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;<]\
"
if state.joining_server or state.connecting_to_server then
local offset = 0
if state.server_connection_error then
offset = offset +1
fs= fs.."\
hypertext[0,0;"..(size.x *0.5)..",1;;"..fe(hte(state.server_connection_error)).."]\
"
end
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]\
hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;Connecting to "..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."...]\
image[0,"..(1.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
label[0.2,"..(1.1 +offset)..";Username]\
field[0.2,"..(1.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_username;;"..minetest.settings:get("name").."]\
image[0,"..(2.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,"..(2.1 +offset)..";Password]\
pwdfield[0.2,"..(2.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_password;]\
button[0,"..(3.25 +offset)..";"..(size.x *0.5)..",0.75;cancel_join_server;Cancel]\
button[0,"..(4.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
-- Shows the content manager.
function show_content_menu()
local fs = ""
minetest.update_formspec(content_header..fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
")
end
function minetest.button_handler(data)
state.menu_vars = 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
show_servers_menu()
elseif data.servers_cancel then
state.servers_filter = nil
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
show_servers_menu()
elseif data.confirm_join_server or data.key_enter_field == "server_username" or data.key_enter_field == "server_password" then
minetest.settings:set("name", data.server_username)
minetest.settings:set("address", state.current_server.address)
minetest.settings:set("remote_port", state.current_server.port)
gamedata = {
playername = data.server_username,
password = data.server_password,
address = state.current_server.address,
port = state.current_server.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[tonumber(idx)]
end
show_servers_menu()
end
end
end
elseif state.loc == "content" then
for k, v in pairs(data) do
end
elseif state.loc == "game" then
for k, v in pairs(data) do
if k:sub(1, string.len(".show_dialog_")) == ".show_dialog_" then
show_game_menu {
show_dialog = k:sub(string.len(".show_dialog_>"))
}
elseif k:sub(1, string.len(".overlay_dialog_")) == ".overlay_dialog_" then
show_game_menu {
overlay_dialog = k:sub(string.len(".overlay_dialog_>"))
}
elseif k == ".unoverlay_dialog" then
show_game_menu {
unoverlay_dialog = true
}
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()
end
end
function minetest.event_handler(ev)
-- Quit on quit
if ev == "MenuQuit" then
if state.joining_server then
state.joining_server = nil
show_servers_menu()
elseif state.loc ~= "games" then
show_meta_menu()
else
minetest.close()
end
end
end
minetest.set_clouds(false)
if last_game then
state.current_game = get_game_info(last_game)
state.menu_current = {"main"}
state.loc = "game"
show_game_menu()
else
show_meta_menu()
end
--]]