39 Commits

Author SHA1 Message Date
ShadowNinja
ce10e802bc Add points to servers with a domain name
This also adds options for banned domain suffixes and domain suffixes
that shouldn't get the extra points.
2021-04-17 15:21:41 -04:00
sfan5
5d5f31d295 Only apply uptime penalty on repeated restarts 2021-03-15 12:35:02 +01:00
sfan5
a9ecf55b38 Avoid unnecesary disk writes 2021-03-15 11:38:49 +01:00
sfan5
9f144f3e3c Adjust server ranking
specificially, penalize servers that support both v4 and v5
Reasoning: We, as a project, have no interest in promoting servers
that intentionally restrict themselves to the feature set of an old,
potentially buggy version.
2021-03-15 11:27:38 +01:00
sfan5
e37149a834 Improve table column sizing, template, CSS 2021-02-21 14:08:00 +01:00
luk3yx
a5bc675a6e Replace "master server" with "serverlist" in README.md (#43) 2020-12-30 16:27:11 +01:00
Tyler Schwend
578a7bc987 doc: Support use of Apache (#40) 2020-12-28 20:06:19 +01:00
sfan5
e99ecd6582 Improve behaviour of protocol filtering 2020-03-08 12:20:19 +01:00
sfan5
ddcd98a457 Highlight even table rows better 2020-02-20 20:04:40 +01:00
sfan5
56ece3ba3d Clean up template a little 2019-08-13 13:44:47 +02:00
sfan5
ba0077a4f5 Add dropdown that filters by protocol version 2019-08-13 13:21:04 +02:00
sfan5
04810a094c Improve lookup failure logging 2019-07-17 12:53:11 +02:00
sfan5
85c3048cd4 Fix handling of GeoIP lookup failure 2019-07-17 12:46:57 +02:00
Auke Kok
772fc29cb8 Detect geo of server.
Using geolite2, we attempt to detect the continent of the server
and store the 'continent code' in the serverlist. This is reasonably
broad enough to help players in the client find truly "nearby" server.

The client will have to be changed to select a continent to change
ordering or filtering.
2019-07-17 12:33:48 +02:00
sfan5
ac66259801 Less confusing status codes 2019-07-17 12:23:35 +02:00
sfan5
794807c9ff Raise JSON length limit
it's not hard to hit 5000 if you just throw in a lot of mods
2019-05-07 17:30:15 +02:00
sfan5
8aa2efd5eb Move some styles back into index.html
These should only apply on servers.minetest.net, not when
the server list is embedded into another page.
2019-02-13 13:28:32 +01:00
luk3yx
8d0c99b5d0 Add nicer CSS
• Add more whitespace around everything.
• Change the fonts.
• Change the colours/colors.
• Add a shadow around the mods/flags/etc lists.
2019-02-13 13:24:06 +01:00
sfan5
6f51e2f00f Tune server ranking (again) 2018-11-11 23:16:43 +01:00
sfan5
e7c4d2c20a Tune server ranking 2018-10-27 22:46:12 +02:00
sfan5
67d8515fd8 Move misplaced <script> tag in index.html 2018-10-27 22:16:58 +02:00
sfan5
57fb13cbb8 Use most compact JSON representation 2018-06-25 13:19:49 +02:00
sfan5
0a3d05baf5 Use portable os.replace instead of relying on POSIX behaviour 2018-06-25 12:48:23 +02:00
nOOb3167
a2b47ff52b Document serverUp protocol
serverUp uses hex spaghetti to send and receive a short sequence of packets, to see whether a server is online.
The packet format is now annotated for easier cross-reference with minetest source.
2018-06-04 16:27:08 +02:00
nOOb3167
e49da8f1b9 Compatibility fix (string field conversion) 2018-05-18 10:09:36 +02:00
nOOb3167
48020105af Schedule purging using plain python, obviating the need for APScheduler 2018-05-18 10:05:57 +02:00
nOOb3167
f43f201af5 Fix potential use-before-defined of serverList variable 2018-05-18 10:05:57 +02:00
nOOb3167
23d45c0a15 Improve use of os.path.join 2018-03-18 14:07:59 +01:00
nOOb3167
f5bddaaef5 Fix locking
Calls to save() in purgeOld() and update() were in race condition.
Race condition was eliminated by extending the lock scope within purgeOld().
Calls to load() were in race condition with themselves.
While current use of load() (called only during construction of class ServerList()) does not manifest the bug, any future change introducing concurrent use of load() would.
Race condition potential was eliminated by extending the lock scope within load().
2018-03-18 14:07:59 +01:00
nOOb3167
78abbee771 Fix server purging 2018-03-18 14:07:59 +01:00
sfan5
2f87286475 Make hostname blacklist case insensitive 2017-12-24 20:42:47 +01:00
sfan5
0d93321f6d Restrict protocol of "server_url" values to HTTP(S) 2017-11-06 19:45:28 +01:00
sfan5
da9f297346 Refine "guest" name regex
Avoid detecting names such as "Bob123" (<4 chars) or "Angel060" (leading zero)
2017-08-08 23:30:35 +02:00
sfan5
afd7b16e5b README formatting fixes 2017-06-14 13:37:45 +02:00
sfan5
828a1fda7e Allow banning by server hostname 2017-05-14 15:29:46 +02:00
sfan5
705ea6e1a0 Re-add banlist features
This effectively reverts commit b366290118.
2017-05-14 15:03:05 +02:00
sfan5
5de6082f57 Design changes on the server list page 2017-02-18 20:34:53 +01:00
sfan5
5e12cb5022 Update detection of guest clients 2017-02-18 11:31:24 +01:00
ShadowNinja
58f03d0395 Try to fix list corruption issue
Sometimes the list was send in a corrupted form.  This
appeared to be the result of overwriting the file as it
was being read by the server.  This commit tries to fix
this by saving to a temporary file and then moving it over
the served file.
2016-02-04 16:49:25 -05:00
8 changed files with 350 additions and 137 deletions

View File

@@ -12,20 +12,17 @@ First install node.js, e.g.:
# apt-get install nodejs
# # OR:
# pacman -S nodejs
# # OR:
# emerge nodejs
Then install doT.js and its dependencies:
$ cd ~
$ npm install dot commander mkdirp
And finally compile the template:
$ cd static
$ ~/node_modules/dot/bin/dot-packer -s . -d .
$ ../node_modules/dot/bin/dot-packer -s .
You can now serve the webpage by copying the files in static/ to your web root, or by [starting the master server](#setting-up-the-server).
You can now serve the webpage by copying the files in static/ to your web root, or by [starting the server list](#setting-up-the-server).
Embedding the server list in a page
@@ -58,48 +55,78 @@ Setting up the server
1. Install Python 3 and pip:
# pacman -S python python-pip
# # OR:
# apt-get install python3 python3-pip
pacman -S python python-pip
# OR:
apt-get install python3 python3-pip
2. Install required Python packages:
# # You might have to use pip3 if your system defaults to Python 2
# pip install -r requirements.txt
# You might have to use pip3 if your system defaults to Python 2
pip install -r requirements.txt
3. If using in production, install uwsgi and it's python plugin:
# pacman -S uwsgi uwsgi-plugin-python
# # OR:
# apt-get install uwsgi uwsgi-plugin-python
# # OR:
# pip install uwsgi
pacman -S uwsgi uwsgi-plugin-python
# OR:
apt-get install uwsgi uwsgi-plugin-python
# OR:
pip install uwsgi
4. Configure the server by adding options to `config.py`.
See `config-example.py` for defaults.
5. Start the server:
$ ./server.py
$ # Or for production:
$ uwsgi -s /tmp/minetest-master.sock --plugin python -w server:app --enable-threads
$ # Then configure according to http://flask.pocoo.org/docs/deploying/uwsgi/
$ ./server.py
$ # Or for production:
$ uwsgi -s /tmp/minetest-master.sock --plugin python -w server:app --enable-threads
$ # Then configure according to http://flask.pocoo.org/docs/deploying/uwsgi/
7. (optional) Configure the proxy server, if any. You should make the server
load static files directly from the static directory. Also, `/list`
should be served from `list.json`. Example for nginx:
root /path/to/server/static;
rewrite ^/list$ /list.json;
try_files $uri @uwsgi;
location @uwsgi {
uwsgi_pass ...;
}
root /path/to/server/static;
rewrite ^/list$ /list.json;
try_files $uri @uwsgi;
location @uwsgi {
uwsgi_pass ...;
}
Setting up the server (Apache version)
---------------------
If you wish to use Apache to host the server list, do steps 1-2, 4, above. Additionally install/enable mod_wsgi and an Apache site config like the following:
# This config assumes you have the server list at DocumentRoot.
# Visitors to the server list in this config would visit http://local.server/ and
# apache would serve up the output from server.py. Static resources would be served
# from http://local.server/static.
# Where are the minetest-server files located?
DocumentRoot /var/games/minetest/serverlist
# Serve up server.py at the root of the URL.
WSGIScriptAlias / /var/games/minetest/serverlist/server.py
# The name of the function that we call when we invoke server.py
WSGICallableObject app
# These options are necessary to enable Daemon mode. Without this, you'll have strange behavior
# with servers dropping off your list! You can tweak threads as needed. See mod_wsgi documentation.
WSGIProcessGroup minetest-serverlist
WSGIDaemonProcess minetest-serverlist threads=2
<Directory /var/games/minetest/serverlist>
Require all granted
</Directory>
</VirtualHost>
License
-------
The Minetest master server is licensed under the GNU Lesser General Public
The Minetest server list code is licensed under the GNU Lesser General Public
License version 2.1 or later (LGPLv2.1+). A LICENSE.txt file should have been
supplied with your copy of this software containing a copy of the license.

View File

@@ -1,4 +1,3 @@
# Enables detailed tracebacks and an interactive Python console on errors.
# Never use in production!
DEBUG = False
@@ -8,13 +7,27 @@ HOST = "127.0.0.1"
# Port for development server to listen on
PORT = 5000
# Amount of time, is seconds, after which servers are removed from the list
# Amount of time, in seconds, after which servers are removed from the list
# if they haven't updated their listings. Note: By default Minetest servers
# only announce once every 5 minutes, so this should be more than 300.
PURGE_TIME = 350
# List of banned IP addresses for announce
# e.g. ['2620:101::44']
BANNED_IPS = []
# List of banned servers as host/port pairs, domains must be lowercase
# e.g. ['1.2.3.4/30000', 'server.example.net', 'server.example.net/30001']
BANNED_SERVERS = []
# List of banned domain suffixes, must be lowercase
# e.g. ['.example.net', 'server.example.com']
BANNED_DOMAINS = []
# List of domain suffixes that should not get a point bonus (e.g. free domains), must be lowercase
IRREPUTABLE_DOMAINS = ['.cf', '.ga', '.gq', '.ml', '.tk']
# Creates server entries if a server sends an 'update' and there is no entry yet.
# This should only be used to populate the server list after list.json was deleted.
# This WILL cause problems such as mapgen, mods and privilege information missing from the list
ALLOW_UPDATE_WITHOUT_OLD = False

View File

@@ -1,3 +1,2 @@
APScheduler>=3
Flask>=0.10
maxminddb-geolite2>=2018.703

210
server.py
View File

@@ -1,15 +1,11 @@
#!/usr/bin/env python3
import os, sys, json, time, socket
import os, re, sys, json, time, socket, ipaddress
from threading import Thread, RLock
from geolite2 import geolite2
from apscheduler.schedulers.background import BackgroundScheduler
from flask import Flask, request, send_from_directory
# Set up scheduler
sched = BackgroundScheduler(timezone="UTC")
sched.start()
app = Flask(__name__, static_url_path = "")
# Load configuration
@@ -39,9 +35,12 @@ def announce():
if ip.startswith("::ffff:"):
ip = ip[7:]
if ip in app.config["BANNED_IPS"]:
return "Banned (IP).", 403
data = request.values["json"]
if len(data) > 5000:
if len(data) > 8192:
return "JSON data is too big.", 413
try:
@@ -72,11 +71,23 @@ def announce():
server["port"] = int(server["port"])
#### End compatability code ####
if "%s/%d" % (server["ip"], server["port"]) in app.config["BANNED_SERVERS"]:
return "Banned (Server).", 403
elif "address" in server:
# Normalize address for ban checks
server["address"] = server["address"].lower().rstrip(".")
if f'{server["address"]}/{server["port"]}' in app.config["BANNED_SERVERS"] or \
server["address"] in app.config["BANNED_SERVERS"]:
return "Banned (Server).", 403
for domain in app.config["BANNED_DOMAINS"]:
if server["address"].endswith(domain):
return "Banned (Domain).", 403
old = serverList.get(ip, server["port"])
if action == "delete":
if not old:
return "Server not found.", 500
return "Server not found."
serverList.remove(old)
serverList.save()
return "Removed from server list."
@@ -85,23 +96,28 @@ def announce():
if action == "update" and not old:
if app.config["ALLOW_UPDATE_WITHOUT_OLD"]:
old = server
old["start"] = time.time()
old["clients_top"] = 0
old["updates"] = 0
old["total_clients"] = 0
action = "start"
else:
return "Server to update not found.", 500
return "Server to update not found."
server["update_time"] = time.time()
server["update_time"] = int(time.time())
server["start"] = time.time() if action == "start" else old["start"]
if action == "start":
server["start"] = int(time.time())
tracker.push("%s:%d" % (server["ip"], server["port"]), server["start"])
else:
server["start"] = old["start"]
if "clients_list" in server:
server["clients"] = len(server["clients_list"])
server["clients_top"] = max(server["clients"], old["clients_top"]) if old else server["clients"]
if "url" in server:
url = server["url"]
if not any(url.startswith(p) for p in ["http://", "https://", "//"]):
del server["url"]
# Make sure that startup options are saved
if action == "update":
for field in ("dedicated", "rollback", "mapgen", "privs",
@@ -122,10 +138,7 @@ def announce():
finishRequestAsync(server)
return "Thanks, your request has been filed.", 202
sched.add_job(lambda: serverList.purgeOld(), "interval",
seconds=60, coalesce=True, max_instances=1)
return "Request has been filed.", 202
# Utilities
@@ -135,14 +148,37 @@ def serverUp(info):
sock = socket.socket(info[0], info[1], info[2])
sock.settimeout(3)
sock.connect(info[4])
# send packet of type ORIGINAL, with no data
# this should prompt the server to assign us a peer id
# [0] u32 protocol_id (PROTOCOL_ID)
# [4] session_t sender_peer_id (PEER_ID_INEXISTENT)
# [6] u8 channel
# [7] u8 type (PACKET_TYPE_ORIGINAL)
buf = b"\x4f\x45\x74\x03\x00\x00\x00\x01"
sock.send(buf)
start = time.time()
# receive reliable packet of type CONTROL, subtype SET_PEER_ID,
# with our assigned peer id as data
# [0] u32 protocol_id (PROTOCOL_ID)
# [4] session_t sender_peer_id
# [6] u8 channel
# [7] u8 type (PACKET_TYPE_RELIABLE)
# [8] u16 seqnum
# [10] u8 type (PACKET_TYPE_CONTROL)
# [11] u8 controltype (CONTROLTYPE_SET_PEER_ID)
# [12] session_t peer_id_new
data = sock.recv(1024)
end = time.time()
if not data:
return False
peer_id = data[12:14]
# send packet of type CONTROL, subtype DISCO,
# to cleanly close our server connection
# [0] u32 protocol_id (PROTOCOL_ID)
# [4] session_t sender_peer_id
# [6] u8 channel
# [7] u8 type (PACKET_TYPE_CONTROL)
# [8] u8 controltype (CONTROLTYPE_DISCO)
buf = b"\x4f\x45\x74\x03" + peer_id + b"\x00\x00\x03"
sock.send(buf)
sock.close()
@@ -170,8 +206,8 @@ fields = {
"mods": (False, "list", "str"),
"version": (True, "str"),
"proto_min": (False, "int"),
"proto_max": (False, "int"),
"proto_min": (True, "int"),
"proto_max": (True, "int"),
"gameid": (True, "str"),
"mapgen": (False, "str"),
@@ -202,8 +238,9 @@ def checkRequest(server):
if data[1] == "bool" and type(server[name]).__name__ == "str":
server[name] = True if server[name].lower() in ("true", "1") else False
continue
# clients_max was sent as a string instead of an integer
if name == "clients_max" and type(server[name]).__name__ == "str":
# Accept strings in integer fields but convert it to an
# integer, for interoperability with e.g. minetest.write_json.
if data[1] == "int" and type(server[name]).__name__ == "str":
server[name] = int(server[name])
continue
#### End compatibility code ####
@@ -246,6 +283,18 @@ def asyncFinishThread(server):
% (server["ip"], server["address"], addresses))
return
reader = geolite2.reader()
try:
geo = reader.get(server["ip"])
except geoip2.errors.GeoIP2Error:
app.logger.warning("GeoIP lookup failure for %s." % (server["ip"],))
if geo and "continent" in geo:
server["geo_continent"] = geo["continent"]["code"]
else:
app.logger.warning("Unable to get GeoIP Continent data for %s."
% (server["ip"],))
server["ping"] = serverUp(info[0])
if not server["ping"]:
app.logger.warning("Server %s:%d has no ping."
@@ -257,6 +306,29 @@ def asyncFinishThread(server):
serverList.update(server)
class UptimeTracker:
def __init__(self):
self.d = {}
self.cleanTime = 0
self.lock = RLock()
def push(self, id, ts):
with self.lock:
if time.time() >= self.cleanTime: # clear once in a while
self.d.clear()
self.cleanTime = time.time() + 48*60*60
if id in self.d:
self.d[id] = self.d[id][-1:] + [ts]
else:
self.d[id] = [0, ts]
# returns the before-last start time, in bulk
def getStartTimes(self):
ret = {}
with self.lock:
for k, v in self.d.items():
ret[k] = v[0]
return ret
class ServerList:
def __init__(self):
self.list = []
@@ -285,28 +357,39 @@ class ServerList:
pass
def sort(self):
start_times = tracker.getStartTimes()
def server_points(server):
points = 0
# 1 per client, but only 1/8 per client with a guest
# or all-numeric name.
# 1 per client, but only 1/8 per "guest" client
if "clients_list" in server:
for name in server["clients_list"]:
if name.startswith("Guest") or \
name.isdigit():
if re.match(r"[A-Z][a-z]{3,}[1-9][0-9]{2,3}", name):
points += 1/8
else:
points += 1
else:
# Old server
# Old server (1/4 per client)
points = server["clients"] / 4
# Penalize highly loaded servers to improve player distribution.
# Note: This doesn't just make more than 16 players stop
# Note: This doesn't just make more than 80% of max players stop
# increasing your points, it can actually reduce your points
# if you have guests/all-numerics.
if server["clients"] > 16:
points -= server["clients"] - 16
# if you have guests.
cap = int(server["clients_max"] * 0.80)
if server["clients"] > cap:
points -= server["clients"] - cap
# 8 for servers with a reputable domain name
try:
ipaddress.ip_address(server["address"])
except ValueError:
for domain in app.config["IRREPUTABLE_DOMAINS"]:
if server["address"].endswith(domain):
break
else:
points += 8
# 1 per month of age, limited to 8
points += min(8, server["game_time"] / (60*60*24*30))
@@ -315,7 +398,7 @@ class ServerList:
points += min(4, server["pop_v"] / 2)
# -8 for unrealistic max_clients
if server["clients_max"] >= 128:
if server["clients_max"] > 200:
points -= 8
# -8 per second of ping over 0.4s
@@ -323,10 +406,17 @@ class ServerList:
points -= (server["ping"] - 0.4) * 8
# Up to -8 for less than an hour of uptime (penalty linearly decreasing)
# only if the server has restarted before within the last 2 hours
HOUR_SECS = 60 * 60
uptime = server["uptime"]
if uptime < HOUR_SECS:
points -= ((HOUR_SECS - uptime) / HOUR_SECS) * 8
start_time = start_times.get("%s:%d" % (server["ip"], server["port"]), 0)
if start_time >= time.time() - 2 * HOUR_SECS:
points -= ((HOUR_SECS - uptime) / HOUR_SECS) * 8
# reduction to 40% for servers that support both legacy (v4) and v5 clients
if server["proto_min"] <= 32 and server["proto_max"] > 36:
points *= 0.4
return points
@@ -334,23 +424,24 @@ class ServerList:
self.list.sort(key=server_points, reverse=True)
def purgeOld(self):
cutoff = int(time.time()) - app.config["PURGE_TIME"]
with self.lock:
for server in self.list:
if server["update_time"] < time.time() - app.config["PURGE_TIME"]:
self.list.remove(server)
self.save()
count = len(self.list)
self.list = [server for server in self.list if cutoff <= server["update_time"]]
if len(self.list) < count:
self.save()
def load(self):
try:
with open(os.path.join("static", "list.json"), "r") as fd:
data = json.load(fd)
except FileNotFoundError:
return
if not data:
return
with self.lock:
try:
with open(os.path.join(app.static_folder, "list.json"), "r") as fd:
data = json.load(fd)
except FileNotFoundError:
return
if not data:
return
self.list = data["list"]
self.maxServers = data["total_max"]["servers"]
self.maxClients = data["total_max"]["clients"]
@@ -365,15 +456,18 @@ class ServerList:
self.maxServers = max(servers, self.maxServers)
self.maxClients = max(clients, self.maxClients)
with open(os.path.join("static", "list.json"), "w") as fd:
list_path = os.path.join(app.static_folder, "list.json")
with open(list_path + "~", "w") as fd:
json.dump({
"total": {"servers": servers, "clients": clients},
"total_max": {"servers": self.maxServers, "clients": self.maxClients},
"list": self.list
},
fd,
indent = "\t" if app.config["DEBUG"] else None
indent = "\t" if app.config["DEBUG"] else None,
separators = (', ', ': ') if app.config["DEBUG"] else (',', ':')
)
os.replace(list_path + "~", list_path)
def update(self, server):
with self.lock:
@@ -386,8 +480,22 @@ class ServerList:
self.sort()
self.save()
class PurgeThread(Thread):
def __init__(self):
Thread.__init__(self)
self.daemon = True
def run(self):
while True:
time.sleep(60)
serverList.purgeOld()
# Globals / Startup
tracker = UptimeTracker()
serverList = ServerList()
PurgeThread().start()
if __name__ == "__main__":
app.run(host = app.config["HOST"], port = app.config["PORT"])

View File

@@ -3,9 +3,28 @@
<head>
<meta charset="utf-8">
<title>Minetest server list</title>
<style>
body {
font-family: Roboto, Ubuntu, "Segoe UI", sans;
}
a {
color: #336B87;
}
a:visited {
color: #336BA1;
}
@media only screen and (max-width: 1024px) {
#server_list table .version, #server_list table .flags, #server_list table .uptime {
display: none;
}
}
</style>
<script>
var master = {show_proto_select: true};
</script>
</head>
<body>
<div id="server_list"></div>
<script src="list.js"></script>
</body>
</html>
<script src="list.js"></script>

View File

@@ -5,6 +5,9 @@ if (!master.output) master.output = '#server_list';
if (!master.list) master.list = "list";
if (!master.list_root) master.list_root = master.root;
if (!master.list_url) master.list_url = master.list_root + master.list;
master.cached_json = null;
// Utility functions used by the templating code
function humanTime(seconds) {
if (typeof(seconds) != "number") return '?';
@@ -13,10 +16,10 @@ function humanTime(seconds) {
d: 86400,
h: 3600,
m: 60
}
};
for (var i in conv) {
if (seconds >= conv[i]) {
return (seconds / conv[i]).toFixed(1) + i;
return (seconds / conv[i]).toFixed(i=='y'?1:0) + i;
}
}
return seconds + 's';
@@ -37,28 +40,22 @@ function addressString(server) {
var str = '<span'
if (shortStr.length > 25) {
shortStr = shortStr.substr(0, 23) + "&hellip;";
str += ' class="mts_tooltip" title="' + addrStr + '"'
str += ' title="' + addrStr + '"'
}
if (server.port != 30000)
shortStr += ':' + server.port;
return str + '>' + shortStr + '</span>';
}
function tooltipString(str, maxLen) {
function tooltipString(str) {
str = escapeHTML(str);
var shortStr = str;
var ret = '<span';
if (shortStr.length > maxLen) {
shortStr = shortStr.substr(0, maxLen - 2) + "&hellip;";
ret += ' class="mts_tooltip" title="' + str + '"';
}
return ret + '>' + shortStr + '</span>';
return '<span title="' + str + '">' + str + '</div>';
}
function hoverList(name, list) {
if (!list || list.length == 0) return '';
var str = '<div class="mts_hover_list">'
str += name + ' (' + list.length + ')<br />';
str += '<b>' + name + '</b> (' + list.length + ')<br />';
for (var i in list) {
str += escapeHTML(list[i]) + '<br />';
}
@@ -67,19 +64,35 @@ function hoverList(name, list) {
function hoverString(name, string) {
if (!string) return '';
return '<div class="mts_hover_list">'
+ name + ':<br />'
return '<div class="mts_hover_list">'
+ '<b>' + name + '</b>:<br />'
+ escapeHTML(string) + '<br />'
+ '</div>';
}
function constantWidth(str, width) {
return '<span class="mts_cwidth" style="width:' + width + 'em;">' + str + '</span>';
}
// Code that fetches & displays the actual list
function draw(json) {
if (json == null)
return;
var html = window.render.servers(json);
jQuery(master.output).html(html);
jQuery('.proto_select', master.output).on('change', function(e) {
master.proto_range = e.target.value;
draw(master.cached_json); // re-render
});
}
function get() {
jQuery.getJSON(master.list_url, draw);
jQuery.getJSON(master.list_url, function(json) {
master.cached_json = json;
draw(json);
});
}
function loaded(){
@@ -89,6 +102,7 @@ function loaded(){
get();
}
// https://github.com/pyrsmk/toast
this.toast=function(){var e=document,t=e.getElementsByTagName("head")[0],n=this.setTimeout,r="createElement",i="appendChild",s="addEventListener",o="onreadystatechange",u="styleSheet",a=10,f=0,l=function(){--f},c,h=function(e,r,i,s){if(!t)n(function(){h(e)},a);else if(e.length){c=-1;while(i=e[++c]){if((s=typeof i)=="function"){r=function(){return i(),!0};break}if(s=="string")p(i);else if(i.pop){p(i[0]),r=i[1];break}}d(r,Array.prototype.slice.call(e,c+1))}},p=function(n,s){++f,/\.css$/.test(n)?(s=e[r]("link"),s.rel=u,s.href=n,t[i](s),v(s)):(s=e[r]("script"),s.src=n,t[i](s),s[o]===null?s[o]=m:s.onload=l)},d=function(e,t){if(!f)if(!e||e()){h(t);return}n(function(){d(e,t)},a)},v=function(e){if(e.sheet||e[u]){l();return}n(function(){v(e)},a)},m=function(){/ded|co/.test(this.readyState)&&l()};h(arguments)};
@@ -96,6 +110,6 @@ toast(master.root + 'style.css', master.root + 'servers.js', function() {
if (typeof(jQuery) != 'undefined')
return loaded();
else
toast('//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js', loaded);
toast('//ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js', loaded);
});

View File

@@ -1,59 +1,68 @@
{{? !master.no_total}}
<div class="total">
Players: {{=it.total.clients}}/{{=it.total_max.clients}}&nbsp;
Servers: {{=it.total.servers}}/{{=it.total_max.servers}}
<div>
<span class="header_total">
Players: {{=it.total.clients}}/{{=it.total_max.clients}}&nbsp;
Servers: {{=it.total.servers}}/{{=it.total_max.servers}}
</span>
{{? master.show_proto_select}}
, Protocol: <select class="proto_select">
<option value="">All</option>
<option value="[11,32]" {{? master.proto_range=='[11,32]'}}selected{{?}}>11-32 (Minetest 0.4)</option>
<option value="[37,99]" {{? master.proto_range=='[37,99]'}}selected{{?}}>37 (Minetest 5.0)</option>
</select>{{?}}
</div>
{{?}}
<table>
<thead><tr>
{{? !master.no_address}}<th>Address[:Port]</th>{{?}}
{{? !master.no_clients}}<th>Players / Max{{? !master.no_avgtop}}<br/>Average / Top{{?}}</th>{{?}}
{{? !master.no_version}}<th>Version, Subgame, Mapgenerator</th>{{?}}
{{? !master.no_version}}<th class="version">Version, Subgame[, Mapgen]</th>{{?}}
{{? !master.no_name}}<th>Name</th>{{?}}
{{? !master.no_description}}<th>Description</th>{{?}}
{{? !master.no_flags}}<th>Flags</th>{{?}}
{{? !master.no_uptime}}<th>Uptime, Age</th>{{?}}
{{? !master.no_flags}}<th class="flags">Flags</th>{{?}}
{{? !master.no_uptime}}<th class="uptime">Uptime, Age</th>{{?}}
{{? !master.no_ping}}<th>Ping, Lag</th>{{?}}
</tr></thead>
<tbody>
{{ var tmp = master.proto_range ? JSON.parse(master.proto_range) : null;}}
{{~it.list :server:index}}
{{ if (master.limit && index + 1 > master.limit) break;}}
{{ if (master.min_clients && server.clients < master.min_clients) continue;}}
{{ if (tmp && (tmp[0] > server.proto_max || tmp[1] < server.proto_min)) continue;}}
<tr>
{{? !master.no_address}}
<td class ="address">
<td class="address">
{{=addressString(server)}}
</td>{{?}}
{{? !master.no_clients}}
<td class="clients{{? server.clients_list && server.clients_list.length > 0}} mts_hover_list_text{{?}}">
{{=server.clients}}/{{=server.clients_max}}{{? !master.no_avgtop}} &nbsp;&nbsp;{{=Math.floor(server.pop_v)}}/{{=server.clients_top}}{{?}}
{{=constantWidth(server.clients + '/' + server.clients_max, 3.4)}}
{{? !master.no_avgtop}} {{=constantWidth(Math.floor(server.pop_v) + '/' + server.clients_top, 3.4)}}{{?}}
{{=hoverList("Clients", server.clients_list)}}
</td>{{?}}
{{? !master.no_version}}
<td class="version{{? server.mods && server.mods.length > 0}} mts_hover_list_text{{?}}">
{{=escapeHTML(server.version)}}, {{=escapeHTML(server.gameid)}},&nbsp;
{{=escapeHTML(server.mapgen || '?')}}
{{!server.version}}, {{!server.gameid}}
{{? server.mapgen}}, {{!server.mapgen}}{{?}}
{{=hoverList("Mods", server.mods)}}
</td>{{?}}
{{? !master.no_name}}
<td class="name">
{{? server.url}}
<a href="{{=escapeHTML(server.url)}}">{{=tooltipString(server.name, 25)}}</a>
<a href="{{!server.url}}">{{=tooltipString(server.name)}}</a>
{{??}}
{{=tooltipString(server.name, 25)}}
{{=tooltipString(server.name)}}
{{?}}
</td>{{?}}
{{? !master.no_description}}
<td class="description">
{{=tooltipString(server.description, 50)}}
{{=tooltipString(server.description)}}
</td>{{?}}
{{? !master.no_flags}}
<td class="flags {{? server.privs}} mts_hover_list_text{{?}}">
{{=hoverString("Privs", server.privs)}}
{{=hoverString("Default privileges", server.privs)}}
{{=server.creative ? 'Cre ' : ''}}
{{=server.dedicated ? 'Ded ' : ''}}
{{=server.damage ? 'Dmg ' : ''}}
{{=server.liquid_finite ? 'Liq ' : ''}}
{{=server.pvp ? 'PvP ' : ''}}
{{=server.password ? 'Pwd ' : ''}}
{{=server.rollback ? 'Rol ' : ''}}
@@ -61,16 +70,16 @@
</td>{{?}}
{{? !master.no_uptime}}
<td class="uptime">
{{=humanTime(server.uptime)}}, {{=humanTime(server.game_time)}}
{{=constantWidth(humanTime(server.uptime), 3.2)}} / {{=constantWidth(humanTime(server.game_time), 3.2)}}
</td>{{?}}
{{? !master.no_ping}}
<td class="ping">
{{=Math.floor(server.ping * 1000)}}{{? server.lag}}, {{= Math.floor(server.lag * 1000)}}{{?}}
{{=constantWidth(Math.floor(server.ping * 1000), 1.8)}}{{? server.lag}} / {{=constantWidth(Math.floor(server.lag * 1000), 1.8)}}{{?}}
</td>{{?}}
</tr>
{{~}}
</tbody>
</table>
{{? master.min_clients || master.limit}}
<a class="clickable" onclick="delete master.min_clients; delete master.limit; get();">More...</a>
<a href="javascript:delete master.min_clients; delete master.limit; get();">Show more...</a>
{{?}}

View File

@@ -1,4 +1,4 @@
#server_list .total {
#server_list .header_total {
font-weight: bold;
}
@@ -12,42 +12,66 @@
}
#server_list td, #server_list th {
border: 1px solid gray;
border: 1px solid #2A3132;
padding: 5px;
color: inherit;
}
#server_list thead {
background-color: #FFA;
background-color: #2A3132;
border-bottom: 5px solid #336B87;
color: white;
font-size: 1.1em;
}
#server_list tbody tr:nth-child(even) {
background-color: #EEE;
background-color: #E0E0E0;
}
#server_list tbody tr:hover {
background-color: #CCC;
#server_list td.clients, #server_list td.uptime, #server_list td.ping {
text-align: center;
}
#server_list td.version, #server_list td.name, #server_list td.description {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/* Note: the column widths here are not exact as auto-layout is left enabled */
#server_list td.address {
max-width: 24ch;
}
#server_list td.version {
max-width: 16ch;
}
#server_list td.name {
max-width: 32ch;
}
#server_list td.description {
max-width: 64ch;
}
.mts_hover_list {
display: none;
border: 1px solid #88F;
border-radius: 4px;
background-color: white;
border: 1px solid #336B87;
border-radius: 10px;
background-color: #FFF;
position: absolute;
z-index: 100;
padding: 0.5em;
box-shadow: 1px 1px 5px 3px rgba(0, 0, 0, 0.25);
}
.mts_hover_list b {
font-size: 1.1em;
}
td:hover .mts_hover_list {
display: block;
}
.mts_hover_list_text, .mts_tooltip {
text-decoration: underline;
text-decoration-style: dashed;
.mts_cwidth {
display: inline-block;
}
.clickable {
text-decoration: underline;
cursor: pointer;
}