]> git.lizzy.rs Git - minetest.git/commitdiff
Take geographic distance into account for server list ordering (#12790)
authorsfan5 <sfan5@live.de>
Mon, 17 Oct 2022 11:56:28 +0000 (13:56 +0200)
committerGitHub <noreply@github.com>
Mon, 17 Oct 2022 11:56:28 +0000 (07:56 -0400)
builtin/mainmenu/common.lua
builtin/mainmenu/serverlistmgr.lua
builtin/mainmenu/tests/serverlistmgr_spec.lua
src/script/lua_api/l_mainmenu.cpp
src/script/lua_api/l_mainmenu.h

index 81e28f2bb3b6cecbcc41048656307c8ced4d65fa..4713605812ea2e994459aa28d89ec6179e86bb2f 100644 (file)
@@ -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
index 964d0c584f2a4641ce2cd70f9519b44e20361c10..06dc15777d204202473fb34a62e725136c4b7ba3 100644 (file)
 --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
-       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
+       -- 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 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
index ab7a6c60c85e5a27b1890ca1aa9f4cf9dcb83ef1..013bd0a2844a9eb1e35aef3498f9ee621caee6fd 100644 (file)
@@ -1,4 +1,4 @@
-_G.core = {}
+_G.core = {get_once = function(_) end}
 _G.vector = {metatable = {}}
 _G.unpack = table.unpack
 _G.serverlistmgr = {}
index 789096d23128e46ea7ff2ed0fc0abb70a9659314..9c828430cabdfdedc5abfb420251ed0ae297e021 100644 (file)
@@ -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<std::string, std::string> once_values;
+       std::mutex once_mutex;
+}
+
+int ModApiMainMenu::l_set_once(lua_State *L)
+{
+       std::string key = readParam<std::string>(L, 1);
+       if (lua_isnil(L, 2))
+               return 0;
+       std::string value = readParam<std::string>(L, 2);
+
+       {
+               MutexAutoLock lock(once_mutex);
+               once_values[key] = value;
+       }
+
+       return 0;
+}
+
+int ModApiMainMenu::l_get_once(lua_State *L)
+{
+       std::string key = readParam<std::string>(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);
 }
 
 /******************************************************************************/
index 9dc40c7f48f8234cbf4c9059e50cfdb9c9466693..f2c2aed74460f5f12b93701fd3df285ffb675fd4 100644 (file)
@@ -156,6 +156,9 @@ class ModApiMainMenu: public ModApiBase
 
        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);