diff --git a/builtin/mainmenu/common.lua b/builtin/mainmenu/common.lua index 81e28f2bb..471360581 100644 --- a/builtin/mainmenu/common.lua +++ b/builtin/mainmenu/common.lua @@ -45,6 +45,27 @@ local function configure_selected_world_params(idx) end end +-- retrieved from https://wondernetwork.com/pings with (hopefully) representative cities +-- Amsterdam, Auckland, Brasilia, Denver, Lagos, Singapore +local latency_matrix = { + ["AF"] = { ["AS"]=258, ["EU"]=100, ["NA"]=218, ["OC"]=432, ["SA"]=308 }, + ["AS"] = { ["EU"]=168, ["NA"]=215, ["OC"]=125, ["SA"]=366 }, + ["EU"] = { ["NA"]=120, ["OC"]=298, ["SA"]=221 }, + ["NA"] = { ["OC"]=202, ["SA"]=168 }, + ["OC"] = { ["SA"]=411 }, + ["SA"] = {} +} +function estimate_continent_latency(own, spec) + local there = spec.geo_continent + if not own or not there then + return nil + end + if own == there then + return 0 + end + return latency_matrix[there][own] or latency_matrix[own][there] +end + function render_serverlist_row(spec) local text = "" if spec.name then diff --git a/builtin/mainmenu/serverlistmgr.lua b/builtin/mainmenu/serverlistmgr.lua index 964d0c584..06dc15777 100644 --- a/builtin/mainmenu/serverlistmgr.lua +++ b/builtin/mainmenu/serverlistmgr.lua @@ -15,28 +15,101 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -serverlistmgr = {} +serverlistmgr = { + -- continent code we detected for ourselves + my_continent = core.get_once("continent"), + + -- list of locally favorites servers + favorites = nil, + + -- list of servers fetched from public list + servers = nil, +} -------------------------------------------------------------------------------- +-- Efficient data structure for normalizing arbitrary scores attached to objects +-- e.g. {{"a", 3.14}, {"b", 3.14}, {"c", 20}, {"d", 0}} +-- -> {["d"] = 0, ["a"] = 0.5, ["b"] = 0.5, ["c"] = 1} +local Normalizer = {} + +function Normalizer:new() + local t = { + map = {} + } + setmetatable(t, self) + self.__index = self + return t +end + +function Normalizer:push(obj, score) + if not self.map[score] then + self.map[score] = {} + end + local t = self.map[score] + t[#t + 1] = obj +end + +function Normalizer:calc() + local list = {} + for k, _ in pairs(self.map) do + list[#list + 1] = k + end + table.sort(list) + local ret = {} + for i, k in ipairs(list) do + local score = #list == 1 and 1 or ( (i - 1) / (#list - 1) ) + for _, obj in ipairs(self.map[k]) do + ret[obj] = score + end + end + return ret +end + +-------------------------------------------------------------------------------- +-- how much the pre-sorted server list contributes to the final ranking +local WEIGHT_SORT = 2 +-- how much the estimated latency contributes to the final ranking +local WEIGHT_LATENCY = 1 + local function order_server_list(list) - local res = {} - --orders the favorite list after support + -- calculate the scores + local s1 = Normalizer:new() + local s2 = Normalizer:new() + for i, fav in ipairs(list) do + -- first: the original position + s1:push(fav, #list - i) + -- second: estimated latency + local ping = (fav.ping or 0) * 1000 + if ping < 400 then + -- If ping is over 400ms, assume the server has latency issues + -- anyway and don't estimate + ping = estimate_continent_latency(serverlistmgr.my_continent, fav) or ping + end + s2:push(fav, -ping) + end + s1 = s1:calc() + s2 = s2:calc() + + -- make a shallow copy and pre-calculate ordering + local res, order = {}, {} for i = 1, #list do local fav = list[i] - if is_server_protocol_compat(fav.proto_min, fav.proto_max) then - res[#res + 1] = fav - end - end - for i = 1, #list do - local fav = list[i] - if not is_server_protocol_compat(fav.proto_min, fav.proto_max) then - res[#res + 1] = fav - end + res[i] = fav + + local n = s1[fav] * WEIGHT_SORT + s2[fav] * WEIGHT_LATENCY + order[fav] = n end + + -- now sort the list + table.sort(res, function(fav1, fav2) + return order[fav1] > order[fav2] + end) + return res end local public_downloading = false +local geoip_downloading = false -------------------------------------------------------------------------------- function serverlistmgr.sync() @@ -56,6 +129,36 @@ function serverlistmgr.sync() return end + -- only fetched once per MT instance + if not serverlistmgr.my_continent and not geoip_downloading then + geoip_downloading = true + core.handle_async( + function(param) + local http = core.get_http_api() + local url = core.settings:get("serverlist_url") .. "/geoip" + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return + end + + local retval = core.parse_json(response.data) + return retval and type(retval.continent) == "string" and retval.continent + end, + nil, + function(result) + geoip_downloading = false + serverlistmgr.my_continent = result + core.set_once("continent", result) + -- reorder list if we already have it + if serverlistmgr.servers then + serverlistmgr.servers = order_server_list(serverlistmgr.servers) + core.event_handler("Refresh") + end + end + ) + end + if public_downloading then return end @@ -79,7 +182,7 @@ function serverlistmgr.sync() end, nil, function(result) - public_downloading = nil + public_downloading = false local favs = order_server_list(result) if favs[1] then serverlistmgr.servers = favs diff --git a/builtin/mainmenu/tests/serverlistmgr_spec.lua b/builtin/mainmenu/tests/serverlistmgr_spec.lua index ab7a6c60c..013bd0a28 100644 --- a/builtin/mainmenu/tests/serverlistmgr_spec.lua +++ b/builtin/mainmenu/tests/serverlistmgr_spec.lua @@ -1,4 +1,4 @@ -_G.core = {} +_G.core = {get_once = function(_) end} _G.vector = {metatable = {}} _G.unpack = table.unpack _G.serverlistmgr = {} diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index 789096d23..9c828430c 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -39,6 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client/renderingengine.h" #include "network/networkprotocol.h" #include "content/mod_configuration.h" +#include "threading/mutex_auto_lock.h" /******************************************************************************/ std::string ModApiMainMenu::getTextData(lua_State *L, std::string name) @@ -1007,6 +1008,44 @@ int ModApiMainMenu::l_do_async_callback(lua_State *L) return 1; } +/******************************************************************************/ +// this is intentionally a global and not part of MainMenuScripting or such +namespace { + std::unordered_map once_values; + std::mutex once_mutex; +} + +int ModApiMainMenu::l_set_once(lua_State *L) +{ + std::string key = readParam(L, 1); + if (lua_isnil(L, 2)) + return 0; + std::string value = readParam(L, 2); + + { + MutexAutoLock lock(once_mutex); + once_values[key] = value; + } + + return 0; +} + +int ModApiMainMenu::l_get_once(lua_State *L) +{ + std::string key = readParam(L, 1); + + { + MutexAutoLock lock(once_mutex); + auto it = once_values.find(key); + if (it == once_values.end()) + lua_pushnil(L); + else + lua_pushstring(L, it->second.c_str()); + } + + return 1; +} + /******************************************************************************/ void ModApiMainMenu::Initialize(lua_State *L, int top) { @@ -1054,6 +1093,8 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(open_dir); API_FCT(share_file); API_FCT(do_async_callback); + API_FCT(set_once); + API_FCT(get_once); } /******************************************************************************/ diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index 9dc40c7f4..f2c2aed74 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -156,6 +156,9 @@ private: static int l_share_file(lua_State *L); + static int l_set_once(lua_State *L); + + static int l_get_once(lua_State *L); // async static int l_do_async_callback(lua_State *L);