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 = 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 dofile(minetest.get_builtin_path().."common/settings/settingtypes.lua") ---[[ 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]\ ") 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 = [[ enable_clouds = true
@if:@selected_world:fi2 @set:list_width:@WIDTH * 0.3 @else:fi2 @set:list_width:@WIDTH * 0.8 @endif:fi2 image[${@WIDTH * 0.1 +-0.1},${@HEIGHT * 0.1 +-0.1};${@WIDTH * 0.8 + 0.2},${@HEIGHT * 0.8 + 0.2};$DEFAULT_ASSET_PATH/bg_translucent.png;8,8] scroll_container[${@WIDTH * 0.1},${@HEIGHT * 0.1};${@list_width},${@HEIGHT * 0.8};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 +-1) * 0.5};${@list_width},0.5;.select_world_${@name};${@name}] @endforeach:worlds scroll_container_end[] @if:@selected_world:fi box[${@WIDTH * 0.4 +-0.05},${@HEIGHT * 0.1};0.1,${@HEIGHT * 0.8};#292d2fff] scroll_container[${@WIDTH * 0.4},${@HEIGHT * 0.1};${@WIDTH * 0.8},${@HEIGHT * 0.8};worldmodsscroll;vertical;;0,0] @foreach:@WORLDMODS:wm label[0,${@i};${@name}] @endforeach:wm scroll_container_end[] scrollbar[-800,6;0,2;vertical;worldmodsscroll;] @else:fi @endif:fi 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 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 -- 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 out[#out +1] = x end 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 function get_mods_for_world(world) local config = minetest.check_mod_configuration(world) for _, x in ipairs(config.unsatisfied_mods) do x.unsatisfied = true config.satisfied_mods[#config.satisfied_mods +1] = x 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" 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 --[[ 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. Note that to prevent dialog definitions from conflicting with the contents of hypertext[] elements, they must not have leading whitespace before the opening delimiter. 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). - @MODS: The list of installed mods that are not incompatible with this game. Exposes @name, @title, @description, @author, @path, @depends, and @optional_depends. - @WORLDMODS: The list of mods installed on the current world. No-op if no world is 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: ``` Notes: - The only uniqueness requirements for the name of a block is that it not be the name of a statment of the same type contained in the body of that block. This is so that the parser knows which `end` belongs to which block without having to manage state. 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, depth) 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 unfound = 0 local a, b, pattern, name, content = fs:find("@foreach:([^:]+):(%w*)\n(.-)\n%s-@endforeach:%2", prev) -- print(string.rep("-", 20)..(depth or 0)) if a then -- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")") dialog[#dialog +1] = build_template_dialog(fs:sub(prev, a -1), (depth or 0) +1) dialog[#dialog +1] = { foreach = pattern:trim(), name = name, content = build_template_dialog(content:trim(), (depth or 0) +1) } prev = b i = i +1 else unfound = unfound +1 end local a, b, expr, name, content, else_content = fs:find("@if:([^:]+):(%w*)\n(.-)\n%s-@else:%2\n(.-\n?)@endif:%2", prev) if a then -- print("if "..expr.." ("..name..")\n"..content.."\nend if ("..name..")") dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, a -1), (depth or 0) +1) dialog[#dialog +1] = { condition = expr:trim(), name = name, content = build_template_dialog(content:trim(), (depth or 0) +1), else_content = build_template_dialog(else_content:trim(), (depth or 0) +1) } prev = b i = i +1 else unfound = unfound +1 end if unfound > 1 then break end end if prev > 1 then dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1)) 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 = "\n"..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("%f[^\n]<(%l+)>(.-)%f[^\n]") do if name == "meta" then menu[name] = parse_conf_text(fs) else menu[name] = build_template_dialog(fs) end end -- print(dump(menu, " ")) 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 -- Condense sub-expressions. local offset = 1 while offset < 100000 do local a, b, se = expr:find("(%b())", offset) if not a then break end se = se:gsub("^%((.*)%)$", "%1") local result = evaluate_template_expression(se, vars, depth +1) 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:([%a_]+):([^\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:sub(1, 1) == "@" then local var = loop.foreach:sub(2) if var == "WORLDS" then list = get_worlds_for_game(state.current_game.id) elseif var == "MODS" then list = get_mods_for_game(state.current_game.id) elseif var == "WORLDMODS" then list = state.current_world and get_mods_for_world(state.current_world) or {} else list = vars[var] end 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 -- Process the syntax tree for a game dialog and output the resulting string. function evaluate_game_dialog(dialog, vars) local out = "" if not dialog then return out end if type(dialog) == "string" then return evaluate_template_block(dialog, vars) elseif dialog.condition then out = out..evaluate_template_conditional(dialog, vars) elseif dialog.foreach then out = out..evaluate_template_foreach(dialog, vars) else for _, c in ipairs(dialog) do out = out..evaluate_game_dialog(c, vars) 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 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 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 -- print(fs) 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 -- If only `continue` existed... 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;;"..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;;Loading...]\ " end if i < 1 then fs = fs.."\ hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;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).."]\ " 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))).."]\ " 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 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 fs = fs.."\ hypertext[0.1,2.25;"..(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.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 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 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;;"..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 "username" or "address")..";true]\ " end if state.joining_server then fs = fs.."\ 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)).."...]\ " offset = offset +1 else fs = fs.."\ set_focus[server_address;true]\ hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;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 -- Shows 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 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 local icon = pkg.type == "game" and pkg.path.."/menu/icon.png" or pkg.path.."/icon.png" if not file_exists(icon) then icon = assets.."menu_content.png" 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(icon)..";view_package_"..fe(pkg.path)..";]\ tooltip[view_package_"..fe(pkg.path)..";"..fe(title)..";#444;#aaa]\ hypertext["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2) +2)..";2,0.5;nobg;"..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;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 if state.current_package then end minetest.update_formspec(content_header..fs.."\ button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\ ") 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 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() 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() end end 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:sub(1, string.len(".select_world_")) == ".select_world_" then state.menu_vars.selected_world = k:sub(string.len(".select_world_>")) 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] = value == "" and "0" or value show_game_menu() 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) -- 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 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 --]]