2 --Copyright (C) 2020 rubenwardy
4 --This program is free software; you can redistribute it and/or modify
5 --it under the terms of the GNU Lesser General Public License as published by
6 --the Free Software Foundation; either version 2.1 of the License, or
7 --(at your option) any later version.
9 --This program is distributed in the hope that it will be useful,
10 --but WITHOUT ANY WARRANTY; without even the implied warranty of
11 --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 --GNU Lesser General Public License for more details.
14 --You should have received a copy of the GNU Lesser General Public License along
15 --with this program; if not, write to the Free Software Foundation, Inc.,
16 --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 -- continent code we detected for ourselves
22 -- list of locally favorites servers
25 -- list of servers fetched from public list
30 if check_cache_age("geoip_last_checked", 3600) then
31 local tmp
= cache_settings
:get("geoip") or ""
32 if tmp
:match("^[A-Z][A-Z]$") then
33 serverlistmgr
.my_continent
= tmp
38 --------------------------------------------------------------------------------
39 -- Efficient data structure for normalizing arbitrary scores attached to objects
40 -- e.g. {{"a", 3.14}, {"b", 3.14}, {"c", 20}, {"d", 0}}
41 -- -> {["d"] = 0, ["a"] = 0.5, ["b"] = 0.5, ["c"] = 1}
44 function Normalizer
:new()
53 function Normalizer
:push(obj
, score
)
54 if not self
.map
[score
] then
57 local t
= self
.map
[score
]
61 function Normalizer
:calc()
63 for k
, _
in pairs(self
.map
) do
68 for i
, k
in ipairs(list
) do
69 local score
= #list
== 1 and 1 or ( (i
- 1) / (#list
- 1) )
70 for _
, obj
in ipairs(self
.map
[k
]) do
77 --------------------------------------------------------------------------------
78 -- how much the pre-sorted server list contributes to the final ranking
80 -- how much the estimated latency contributes to the final ranking
81 local WEIGHT_LATENCY
= 1
83 local function order_server_list(list
)
84 -- calculate the scores
85 local s1
= Normalizer
:new()
86 local s2
= Normalizer
:new()
87 for i
, fav
in ipairs(list
) do
88 -- first: the original position
89 s1
:push(fav
, #list
- i
)
90 -- second: estimated latency
91 local ping
= (fav
.ping
or 0) * 1000
93 -- If ping is under 400ms replace it with our own estimate,
94 -- we assume the server has latency issues anyway otherwise
95 ping
= estimate_continent_latency(serverlistmgr
.my_continent
, fav
) or 0
102 -- make a shallow copy and pre-calculate ordering
103 local res
, order
= {}, {}
108 local n
= s1
[fav
] * WEIGHT_SORT
+ s2
[fav
] * WEIGHT_LATENCY
113 table.sort(res
, function(fav1
, fav2
)
114 return order
[fav1
] > order
[fav2
]
120 local public_downloading
= false
121 local geoip_downloading
= false
123 --------------------------------------------------------------------------------
124 local function fetch_geoip()
125 local http
= core
.get_http_api()
126 local url
= core
.settings
:get("serverlist_url") .. "/geoip"
128 local response
= http
.fetch_sync({ url
= url
})
129 if not response
.succeeded
then
133 local retval
= core
.parse_json(response
.data
)
134 if type(retval
) ~= "table" then
137 return type(retval
.continent
) == "string" and retval
.continent
140 function serverlistmgr
.sync()
141 if not serverlistmgr
.servers
then
142 serverlistmgr
.servers
= {{
143 name
= fgettext("Loading..."),
144 description
= fgettext_ne("Try reenabling public serverlist and check your internet connection.")
148 local serverlist_url
= core
.settings
:get("serverlist_url") or ""
149 if not core
.get_http_api
or serverlist_url
== "" then
150 serverlistmgr
.servers
= {{
151 name
= fgettext("Public server list is disabled"),
157 if not serverlistmgr
.my_continent
and not geoip_downloading
then
158 geoip_downloading
= true
159 core
.handle_async(fetch_geoip
, nil, function(result
)
160 geoip_downloading
= false
164 serverlistmgr
.my_continent
= result
165 cache_settings
:set("geoip", result
)
166 cache_settings
:set("geoip_last_checked", tostring(os
.time()))
168 -- re-sort list if applicable
169 if serverlistmgr
.servers
then
170 serverlistmgr
.servers
= order_server_list(serverlistmgr
.servers
)
171 core
.event_handler("Refresh")
176 if public_downloading
then
179 public_downloading
= true
181 -- note: this isn't cached because it's way too dynamic
184 local http
= core
.get_http_api()
185 local url
= ("%s/list?proto_version_min=%d&proto_version_max=%d"):format(
186 core
.settings
:get("serverlist_url"),
187 core
.get_min_supp_proto(),
188 core
.get_max_supp_proto())
190 local response
= http
.fetch_sync({ url
= url
})
191 if not response
.succeeded
then
195 local retval
= core
.parse_json(response
.data
)
196 return retval
and retval
.list
or {}
200 public_downloading
= false
201 local favs
= order_server_list(result
)
203 serverlistmgr
.servers
= favs
205 core
.event_handler("Refresh")
210 --------------------------------------------------------------------------------
211 local function get_favorites_path(folder
)
212 local base
= core
.get_user_path() .. DIR_DELIM
.. "client" .. DIR_DELIM
.. "serverlist" .. DIR_DELIM
216 return base
.. core
.settings
:get("serverlist_file")
219 --------------------------------------------------------------------------------
220 local function save_favorites(favorites
)
221 local filename
= core
.settings
:get("serverlist_file")
222 -- If setting specifies legacy format change the filename to the new one
223 if filename
:sub(#filename
- 3):lower() == ".txt" then
224 core
.settings
:set("serverlist_file", filename
:sub(1, #filename
- 4) .. ".json")
227 assert(core
.create_dir(get_favorites_path(true)))
228 core
.safe_file_write(get_favorites_path(), core
.write_json(favorites
))
231 --------------------------------------------------------------------------------
232 function serverlistmgr
.read_legacy_favorites(path
)
233 local file
= io
.open(path
, "r")
239 for line
in file
:lines() do
240 lines
[#lines
+ 1] = line
249 local line
= lines
[i
]
251 return line
and line
:trim()
254 if pop():lower() == "[server]" then
256 local address
= pop()
257 local port
= tonumber(pop())
258 local description
= pop()
264 if description
== "" then
268 if not address
or #address
< 3 then
269 core
.log("warning", "Malformed favorites file, missing address at line " .. i
)
270 elseif not port
or port
< 1 or port
> 65535 then
271 core
.log("warning", "Malformed favorites file, missing port at line " .. i
)
272 elseif (name
and name
:upper() == "[SERVER]") or
273 (address
and address
:upper() == "[SERVER]") or
274 (description
and description
:upper() == "[SERVER]") then
275 core
.log("warning", "Potentially malformed favorites file, overran at line " .. i
)
277 favorites
[#favorites
+ 1] = {
281 description
= description
290 --------------------------------------------------------------------------------
291 local function read_favorites()
292 local path
= get_favorites_path()
294 -- If new format configured fall back to reading the legacy file
295 if path
:sub(#path
- 4):lower() == ".json" then
296 local file
= io
.open(path
, "r")
298 local json
= file
:read("*all")
300 return core
.parse_json(json
)
303 path
= path
:sub(1, #path
- 5) .. ".txt"
306 local favs
= serverlistmgr
.read_legacy_favorites(path
)
314 --------------------------------------------------------------------------------
315 local function delete_favorite(favorites
, del_favorite
)
316 for i
=1, #favorites
do
317 local fav
= favorites
[i
]
319 if fav
.address
== del_favorite
.address
and fav
.port
== del_favorite
.port
then
320 table.remove(favorites
, i
)
326 --------------------------------------------------------------------------------
327 function serverlistmgr
.get_favorites()
328 if serverlistmgr
.favorites
then
329 return serverlistmgr
.favorites
332 serverlistmgr
.favorites
= {}
334 -- Add favorites, removing duplicates
336 for _
, fav
in ipairs(read_favorites() or {}) do
337 local key
= ("%s:%d"):format(fav
.address
:lower(), fav
.port
)
338 if not seen
[key
] then
340 serverlistmgr
.favorites
[#serverlistmgr
.favorites
+ 1] = fav
344 return serverlistmgr
.favorites
347 --------------------------------------------------------------------------------
348 function serverlistmgr
.add_favorite(new_favorite
)
349 assert(type(new_favorite
.port
) == "number")
351 -- Whitelist favorite keys
353 name
= new_favorite
.name
,
354 address
= new_favorite
.address
,
355 port
= new_favorite
.port
,
356 description
= new_favorite
.description
,
359 local favorites
= serverlistmgr
.get_favorites()
360 delete_favorite(favorites
, new_favorite
)
361 table.insert(favorites
, 1, new_favorite
)
362 save_favorites(favorites
)
365 --------------------------------------------------------------------------------
366 function serverlistmgr
.delete_favorite(del_favorite
)
367 local favorites
= serverlistmgr
.get_favorites()
368 delete_favorite(favorites
, del_favorite
)
369 save_favorites(favorites
)