562 lines
22 KiB
Lua
562 lines
22 KiB
Lua
|
|
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, "<global valign=middle halign=center color="..color..">"..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, "<global valign=middle halign=center color=#777>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, "<global valign=middle halign=center color=#aaa><b>"..core.hypertext_escape(detail.name or detail.address..":"..detail.port).."</b>")
|
|
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", "<global color=#aaa>"..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, "<global valign=middle color=#9d5b5b>"..core.hypertext_escape(state.server_connection_error.msg))
|
|
end
|
|
|
|
if joining == true then
|
|
imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa>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, "<global valign=middle halign=center color=#aaa>Connecting to <b>"..core.hypertext_escape(joining.name or joining.address..":"..joining.port).."</b>...")
|
|
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
|