local 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 local 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 local function search_server_list(state, input) if input:trim() == "" then return nil end local search_str = "" local words = {} local mods = {} local game 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 game = x:sub(6) else words[#words + 1] = x end end local out = {} for _, x in ipairs(state.servers) 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 game and x.gameid and game ~= x.gameid:lower():trim() then passed = false 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():trim() == 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 local function refresh_server_list(state) core.handle_async( function(param) local http = core.get_http_api() local url = string.format("%s/list?proto_version_min=%d&proto_version_max=%d", "https://servers.luanti.org" or core.settings:get("serverlist_url"), core.get_min_supp_proto(), core.get_max_supp_proto() ) local response = http.fetch_sync({url = url}) if not response.succeeded then return {} end local retval = core.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.servers = list if state.search ~= "" then state.servers_filtered = search_server_list(state, state.search) list = state.servers_filtered end state.serverlist(list) end ) end local function join_server(state) core.settings:set("name", state.username()) local address local port if state.joining() == true then if not tonumber(state.port()) then state.join_error("Invalid port.") return end address = state.address():lower() port = state.port():lower() else address = state.joining().address:lower() port = state.joining().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 core.settings:set("address", address) core.settings:set("remote_port", port) gamedata = { playername = state.username(), password = state.password(), address = address, port = port, selected_world = 0, singleplayer = false } core.start() end return function(state) imfs.begin(size.x, size.y) :padding(0, 0) :bgcolor(theme.styles.bg_color, true) local loading if not state.serverlist() then loading = true local favorite_servers = core.parse_json(read_file(core.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._val = favorite_servers refresh_server_list(state) end local enable_tooltips = not (state.joining() or state.detail_view()) local list = state.serverlist() imfs.row("10%", size.y * 0.1 - 0.85, "80%", 0.75) :gap(0.1) theme.field(0, 0, "1x", "100%", state.search) :onchange(function(value) state.search = value end) :onenter(function(value) state.search = value state.servers_filtered = search_server_list(state, state.search) state.serverlist(state.servers_filtered) state.scroll_pos(1) end) imfs.button(0, 0, 0.75, 0.75) :image(theme.icon("search")) :style({ bgimg = theme.get_background_image("button"), bgimg_middle = theme.styles.button.border_width, }) :tooltip("Search server list") :onclick(function() state.servers_filtered = search_server_list(state, state.search) state.serverlist(state.servers_filtered) state.scroll_pos(1) end) imfs.button(0, 0, 0.75, 0.75) :image(theme.icon("cancel")) :style({ bgimg = theme.get_background_image("button"), bgimg_middle = theme.styles.button.border_width, }) :tooltip("Clear search") :onclick(function() state.search = "" state.servers_filtered = nil state.serverlist(state.servers) state.scroll_pos(1) end) imfs.button(0, 0, 0.75, 0.75) :image(theme.icon("refresh")) :style({ bgimg = theme.get_background_image("button"), bgimg_middle = theme.styles.button.border_width, }) :tooltip("Refresh server list") :onclick(function() refresh_server_list(state) end) imfs.button(0, 0, 3, 0.75, "Direct Connection...") :onclick(function() state.joining(true) end) imfs.row_end() local border_width = pixel_to_formspec(theme.styles.container.border_width) imfs.image("10%", "10%", "80%", "80%", theme.get_background_image("container")) imfs.group("10% + "..border_width, "10% + "..border_width, "80% - "..(border_width * 2), "80% - "..(border_width * 2)) imfs.scroll_container(0, 0, state.detail() and "65% - 0.05" or "100%", "100%", "vertical", 0, "") :scrollbar(function() imfs.scrollbar(state.detail() and "65% - 0.55" or "100% - 0.5", 0, 0.5, "100%", "vertical", state.scroll_pos) :options({ min = 1, max = #list, smallstep = 1, thumbsize = 0, }) :onchange(function(action, value) if action == "CHG" then state.scroll_pos(value) end end) end) imfs.scope("servers") local height = size.y * 0.8 local i = 0 local idx = state.scroll_pos() while true do -- We must use a while loop here because we skip incompatible servers, -- and we have no way of knowing how many there will be until we iterate. if i > math.ceil(height / 0.5) then break end local x = list[idx] -- End early if we've run out of list. if not x then break end -- 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 = core.colorize("#888", x.address..":"..x.port) end imfs.button(0, i * 0.5, "100%", 0.5) :style({ border = false, bgimg = "[fill:1x1:0,0:#fff", bgimg_middle = 0, bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt, }) :style("hovered", { border = false, bgimg = "[fill:1x1:0,0:#fff", bgimg_middle = 0, }) :style("pressed", { border = false, bgimg = "[fill:1x1:0,0:#fff", bgimg_middle = 0, }) :onclick(function() state.detail(x) end) imfs.row(0.1, i * 0.5, "100% - 0.65", 0.5) if x.favorite then imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_favorite")) :tooltip("Favorite server", enable_tooltips) end local name = x.name and x.name:trim() or "" if name == "" then name = core.colorize("#888", x.address..":"..x.port) end -- We use an arealabel to ensure that the name will be clipped -- if it is inordinately long, rather than overflowing onto -- other thigs. imfs.arealabel(0, 0.125, "1x", 0.375, name) if x.pvp == false then imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_peaceful")) :tooltip("Peaceful", enable_tooltips) end if x.creative then imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_creative")) :tooltip("Creative", enable_tooltips) end if x.clients then local clients = x.clients..( x.clients_max and "/"..x.clients_max or "" ) local color = "#aaa" 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 imfs.hypertext(0, 0, 1, 0.5, ""..core.hypertext_escape(clients)) end if x.ping or x.lag then imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_icon_ping_"..ping_lvl)) :tooltip("Ping: "..math.floor(lag), enable_tooltips) end imfs.row_end() i = i + 1 end idx = idx + 1 end imfs.scope_end() if loading then imfs.hypertext(0, i * 0.5, "100%", 0.5, "Loading...") end imfs.scroll_container_end() local detail = state.detail() if detail then imfs.box("65% - 0.05", 0, 0.1, "100%", theme.styles.container.border_color) imfs.column("65% + 0.1", border_width, "35% - "..(border_width + 0.1), "100% - "..(border_width * 2)) :gap(0.1) imfs.hypertext(0, 0, "100%", 0.75, ""..core.hypertext_escape(detail.name or detail.address..":"..detail.port).."") if detail.mods or detail.clients_list or detail.favorites then imfs.row(0.1, 0, "100% - 0.2", 0.5) if detail.mods then imfs.button(0, 0, 0.5, 0.5) :image(theme.icon("menu_servers_mods")) :tooltip("Show mod list") :onclick(function() state.detail_view("mod_list") end) end if detail.clients_list then imfs.button(0, 0, 0.5, 0.5) :image(theme.icon("menu_servers_players")) :tooltip("Show player list") :onclick(function() state.detail_view("player_list") end) end if detail.favorite then imfs.button(0, 0, 0.5, 0.5) :image(theme.icon("menu_servers_unfavorite")) :tooltip("Remove from favorites") :onclick(function() detail.favorite = nil remove_favorite_server(detail.address, detail.port) -- Bump the serverlist to notify that we've changed it. state.serverlist(state.serverlist) end) end imfs.row_end() end imfs.hypertext(0.1, 0, "100% - 0.2", "1x", ""..core.hypertext_escape(detail.description or "")) imfs.button(0, 0, "100%", 0.75, "Join server") :onclick(function() state.joining(detail) end) imfs.column_end() end imfs.group_end() imfs.button("100% - 1", 0.25, 0.75, 0.75, "<") :onclick(function() show_meta_menu() end) local joining = state.joining() if joining then imfs.box(0, 0, "100%", "100%", "#0008") imfs.button(0, 0, "100%", "100%") :styles(styles_no_background) :onclick(function() state.joining(false) end) imfs.group("20%", "20%", "60%", "60%") imfs.image(0, 0, "100%", "100%", theme.get_background_image("container")) imfs.scroll_container(border_width + 0.1, border_width + 0.1, "100% - "..(border_width * 2 + 0.2), "100% - "..(border_width * 2 + 0.2)) imfs.column(0, 0, "100%", 100) :gap(0.1) if state.join_error() then imfs.hypertext(0, 0, "100%", 0.75, ""..core.hypertext_escape(state.server_connection_error.msg)) end if joining == true then imfs.hypertext(0, 0, "100%", 0.75, "Connecting to server...") imfs.group(0, 0, "100%", 1.05) imfs.image(0, 0.3, "100%", "100% - 0.3", theme.get_background_image("field")) imfs.label(0.2, 0.125, "Address") imfs.field(0.1, 0.3, "70% - 0.2", "100% - 0.3") :onchange(function(value) state.address(value) end) :onenter(function() join_server(state) end) imfs.box("70% - 0.05", 0.3, 0.1, "100% - 0.3", theme.styles.field.border_color) imfs.label("70% + 0.2", 0.125, "Port") imfs.field("70% + 0.1", 0.3, "30% - 0.2", "100% - 0.3") :onchange(function(value) state.port(value) end) :onenter(function() join_server(state) end) imfs.group_end() else imfs.hypertext(0, 0, "100%", 0.75, "Connecting to "..core.hypertext_escape(joining.name or joining.address..":"..joining.port).."...") end theme.field(0, 0, "100%", 0.75, "Username", state.username) :onchange(function(value) state.username(value) end) :onenter(function() join_server(state) end) theme.field(0, 0, "100%", 0.75, "Password", "") :password() :onchange(function(value) state.password(value) end) :onenter(function() join_server(state) end) imfs.row(0, 0, "100%", 0.75) :gap(0.1) imfs.button(0, 0, "1x", "100%", "Cancel") :onclick(function() state.joining(false) end) imfs.button(0, 0, "1x", "100%", "Join") :onclick(function() join_server(state) end) imfs.row_end() imfs.column_end() imfs.scroll_container_end() imfs.group_end() elseif state.detail_view() then imfs.box(0, 0, "100%", "100%", "#0008") imfs.button(0, 0, "100%", "100%") :styles(styles_no_background) :onclick(function() state.detail_view(false) end) imfs.image("20%", "20%", "60%", "60%", theme.get_background_image("container")) imfs.group("20% + "..border_width, "20% + "..border_width, "60% - "..(border_width * 2), "60% - "..(border_width * 2)) imfs.scroll_container(0, 0, "100%", "100% - 0.8") :scrollbar("100% - 0.5", 0, 0.5, "100% - 0.8", "vertical") local list = state.detail_view() == "mod_list" and detail.mods or detail.clients_list for i = 0, #list - 1 do imfs.button(0, i * 0.5, "100%", 0.5) :style({ border = false, bgimg = "[fill:1x1:0,0:#fff", bgimg_middle = 0, bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt, }) :style("hovered", style_borderless_hovered) :style("pressed", style_borderless_hovered) -- This isn't just the button label because we want left-alignment. imfs.arealabel(0.1, i * 0.5 + 0.125, "100%", 0.375, list[i + 1]) end imfs.scroll_container_end() imfs.button(0, "100% - 0.75", "100%", 0.75, "Close") :onclick(function() state.detail_view(false) end) imfs.group_end() end return imfs.end_() end