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+)>(.-)") 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 --]]