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