add etmasters script + etrelay section in README
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
*.tar.gz
|
||||||
|
*.json
|
||||||
@@ -12,18 +12,18 @@ One to launch a Jaymod server:
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Wolfenstein ET is built on a 32 bits arch. So, 32 bits librairies must exist to run the game.
|
Wolfenstein ET is built on a 32 bits arch. So, 32 bits libraries must exist to run the game.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# add the i386 architecture
|
# add the i386 architecture
|
||||||
sudo dpkg --add-architecture i386
|
sudo dpkg --add-architecture i386
|
||||||
|
|
||||||
# do update to fetch i386 librairies
|
# do update to fetch i386 libraries
|
||||||
sudo apt update
|
sudo apt update
|
||||||
|
|
||||||
# install the 32 bits compatible librairies
|
# install the 32 bits compatible libraries
|
||||||
sudo apt install ia32-libs (mint)
|
sudo apt install ia32-libs # Mint
|
||||||
sudo apt install lib32z1 (ubuntu)
|
sudo apt install lib32z1 # Ubuntu
|
||||||
|
|
||||||
# after the installation the following lib must be present
|
# after the installation the following lib must be present
|
||||||
sudo apt install libsdl1.2debian:i386
|
sudo apt install libsdl1.2debian:i386
|
||||||
@@ -65,11 +65,11 @@ cd /usr/local/games/enemy-territory
|
|||||||
|
|
||||||
Enjoy !
|
Enjoy !
|
||||||
|
|
||||||
**NOTE** : Some librairies may be missing, you need to install it one by one (test to launch ./et.x86 and see missing libs).
|
**NOTE** : Some libraries may be missing, you need to install it one by one (test to launch ./et.x86 and see missing libs).
|
||||||
|
|
||||||
## Create a systemd service (server creation only)
|
## Create a systemd service (server creation only)
|
||||||
|
|
||||||
In order to launch the server properly, a systemd service is a right solution instead of the **serverctl** sh script. Todo so :
|
In order to launch the server properly, a systemd service is a right solution instead of the **serverctl** sh script. To do so:
|
||||||
|
|
||||||
* put the **[launch_server.bash](./service/launch_server.bash)** wherever you want (ex: /opt/games/enemy-territory/server/linux).
|
* put the **[launch_server.bash](./service/launch_server.bash)** wherever you want (ex: /opt/games/enemy-territory/server/linux).
|
||||||
* put the **[wolfenstein-et.service](./service/wolfenstein-et.service)** file in **/etc/systemd/system** directory (must be **root**).
|
* put the **[wolfenstein-et.service](./service/wolfenstein-et.service)** file in **/etc/systemd/system** directory (must be **root**).
|
||||||
@@ -87,9 +87,33 @@ sudo service wolfenstein-et start
|
|||||||
sudo service wolfenstein-et status
|
sudo service wolfenstein-et status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Public server behind a tunnel
|
||||||
|
|
||||||
|
When the server runs at home and is only reachable through a front server (WireGuard), its own heartbeats leave through the home box: masters challenge the wrong IP and the server never shows up in the in-game browser.
|
||||||
|
|
||||||
|
* **[etrelay](https://gitea.sonak.fr/rmanach/etrelay)**: a small Rust UDP relay running on the front. It proxies players and sends the master heartbeats from the front public address. Build, usage and how it works (with diagrams): see its repository.
|
||||||
|
* Disable the server's own heartbeats in `server.cfg`, the relay sends them:
|
||||||
|
```cfg
|
||||||
|
set sv_master1 ""
|
||||||
|
set sv_master2 ""
|
||||||
|
set sv_master3 ""
|
||||||
|
set sv_master4 ""
|
||||||
|
set sv_master5 ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### List the servers known by the masters
|
||||||
|
|
||||||
|
**[etmasters.py](./etmasters.py)** (Python 3, standard library only) asks the masters (`etmaster.idsoftware.com`, `etmaster.net`) for their server list, then every server for its info: name, map, players, mod. Handy to check the server is listed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./etmasters.py # table sorted by players
|
||||||
|
./etmasters.py --json servers.json # JSON file instead
|
||||||
|
./etmasters.py --master etmaster.net:27950 --timeout 5
|
||||||
|
```
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
More useful informations : https://wiki.archlinux.org/title/Wolfenstein:_Enemy_Territory.
|
More useful information: https://wiki.archlinux.org/title/Wolfenstein:_Enemy_Territory.
|
||||||
|
|
||||||
### Set screen size in **1920*1080**
|
### Set screen size in **1920*1080**
|
||||||
* Open the player configuration file:
|
* Open the player configuration file:
|
||||||
@@ -126,7 +150,7 @@ You must install the corresponding 32 bits library:
|
|||||||
sudo apt install libasound2-plugins:i386
|
sudo apt install libasound2-plugins:i386
|
||||||
```
|
```
|
||||||
|
|
||||||
### Keyboard inputs does not work
|
### Keyboard input does not work
|
||||||
|
|
||||||
Try moving to `Xorg` instead of `Wayland`... To do so, on Ubuntu, logout, click on the **Settings** icon and select **Xorg**.
|
Try moving to `Xorg` instead of `Wayland`... To do so, on Ubuntu, logout, click on the **Settings** icon and select **Xorg**.
|
||||||
|
|
||||||
|
|||||||
Executable
+161
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""List Wolfenstein: Enemy Territory servers known by the master servers.
|
||||||
|
|
||||||
|
Asks each master for its server list (getservers), then every server for its info
|
||||||
|
(getinfo): name, map, players, mod. Prints a table, or writes JSON with --json.
|
||||||
|
|
||||||
|
./etmasters.py
|
||||||
|
./etmasters.py --json servers.json
|
||||||
|
./etmasters.py --master etmaster.net:27950
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
OOB = b"\xff\xff\xff\xff"
|
||||||
|
# 84: ET 2.60b / ET: Legacy protocol
|
||||||
|
GETSERVERS = OOB + b"getservers 84 empty full"
|
||||||
|
GETINFO = OOB + b"getinfo etmasters"
|
||||||
|
DEFAULT_MASTERS = ["etmaster.idsoftware.com:27950", "etmaster.net:27950"]
|
||||||
|
# ^1red ^7white... color codes in server names
|
||||||
|
COLOR_CODE = re.compile(r"\^.")
|
||||||
|
|
||||||
|
|
||||||
|
def query_master(master, timeout):
|
||||||
|
"""Server addresses listed by one master, None if it doesn't answer."""
|
||||||
|
host, port = master.rsplit(":", 1)
|
||||||
|
try:
|
||||||
|
ip = socket.gethostbyname(host)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
sock.sendto(GETSERVERS, (ip, int(port)))
|
||||||
|
|
||||||
|
servers, answered = set(), False
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data, _ = sock.recvfrom(65535)
|
||||||
|
if b"getserversResponse" not in data:
|
||||||
|
continue
|
||||||
|
answered = True
|
||||||
|
body = data.split(b"getserversResponse", 1)[1]
|
||||||
|
# \<4 bytes IP><2 bytes port>\...\EOT
|
||||||
|
for entry in body.split(b"\\")[1:]:
|
||||||
|
if len(entry) >= 6 and not entry.startswith(b"EOT"):
|
||||||
|
addr = socket.inet_ntoa(entry[:4])
|
||||||
|
servers.add((addr, int.from_bytes(entry[4:6], "big")))
|
||||||
|
# Every packet ends with EOT, the list spans several: read until timeout.
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
return servers if answered else None
|
||||||
|
|
||||||
|
|
||||||
|
def query_infos(addresses, timeout):
|
||||||
|
"""getinfo to every server at once, {address: info dict} for those answering."""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setblocking(False)
|
||||||
|
for addr in addresses:
|
||||||
|
try:
|
||||||
|
sock.sendto(GETINFO, addr)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
infos, deadline = {}, time.monotonic() + timeout
|
||||||
|
while len(infos) < len(addresses):
|
||||||
|
left = deadline - time.monotonic()
|
||||||
|
if left <= 0 or not select.select([sock], [], [], left)[0]:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
data, addr = sock.recvfrom(65535)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if data.startswith(OOB + b"infoResponse"):
|
||||||
|
infos[addr] = parse_info(data)
|
||||||
|
|
||||||
|
sock.close()
|
||||||
|
return infos
|
||||||
|
|
||||||
|
|
||||||
|
def parse_info(data):
|
||||||
|
"""infoResponse\n\\key\\value\\... -> dict."""
|
||||||
|
body = data.split(b"\n", 1)[1].decode("latin-1").strip("\\\n\x00")
|
||||||
|
parts = body.split("\\")
|
||||||
|
return dict(zip(parts[::2], parts[1::2]))
|
||||||
|
|
||||||
|
|
||||||
|
def server_entry(addr, masters, info):
|
||||||
|
entry = {"address": f"{addr[0]}:{addr[1]}", "masters": sorted(masters), "online": info is not None}
|
||||||
|
if info:
|
||||||
|
entry.update(
|
||||||
|
name=COLOR_CODE.sub("", info.get("hostname", "")).strip(),
|
||||||
|
map=info.get("mapname", ""),
|
||||||
|
players=int(info.get("clients", 0) or 0),
|
||||||
|
humans=int(info["humans"]) if info.get("humans", "").isdigit() else None,
|
||||||
|
max_players=int(info.get("sv_maxclients", 0) or 0),
|
||||||
|
# game: mod folder (nq, etpro, legacy...), gamename is always "et"
|
||||||
|
mod=info.get("game") or info.get("gamename", ""),
|
||||||
|
password=info.get("needpass") == "1",
|
||||||
|
)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def print_table(entries, masters_status):
|
||||||
|
for master, count in masters_status.items():
|
||||||
|
state = "no answer" if count is None else f"{count} servers"
|
||||||
|
print(f"{master:32} {state}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
online = [e for e in entries if e["online"]]
|
||||||
|
online.sort(key=lambda e: (-e["players"], e["name"].lower()))
|
||||||
|
|
||||||
|
header = f"{'ADDRESS':21} {'PLAYERS':>7} {'MAP':16} {'MOD':10} NAME"
|
||||||
|
print(header)
|
||||||
|
print("-" * len(header))
|
||||||
|
for e in online:
|
||||||
|
players = f"{e['players']}/{e['max_players']}"
|
||||||
|
lock = " [pw]" if e["password"] else ""
|
||||||
|
print(f"{e['address']:21} {players:>7} {e['map'][:16]:16} {e['mod'][:10]:10} {e['name'][:60]}{lock}")
|
||||||
|
|
||||||
|
total = sum(e["players"] for e in online)
|
||||||
|
print(f"\n{len(online)} servers online ({len(entries) - len(online)} not answering), {total} players")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="List ET servers known by the master servers.")
|
||||||
|
parser.add_argument("--master", action="append", metavar="HOST:PORT",
|
||||||
|
help=f"master to query, repeatable (default: {', '.join(DEFAULT_MASTERS)})")
|
||||||
|
parser.add_argument("--json", metavar="PATH", help="write servers as JSON to PATH instead of printing a table")
|
||||||
|
parser.add_argument("--timeout", type=float, default=3.0, help="seconds to wait for answers (default: 3)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
masters = args.master or DEFAULT_MASTERS
|
||||||
|
|
||||||
|
masters_status, listed_by = {}, {}
|
||||||
|
for master in masters:
|
||||||
|
servers = query_master(master, args.timeout)
|
||||||
|
masters_status[master] = None if servers is None else len(servers)
|
||||||
|
for addr in servers or ():
|
||||||
|
listed_by.setdefault(addr, set()).add(master)
|
||||||
|
|
||||||
|
infos = query_infos(list(listed_by), args.timeout)
|
||||||
|
entries = [server_entry(addr, ms, infos.get(addr)) for addr, ms in listed_by.items()]
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
with open(args.json, "w") as f:
|
||||||
|
json.dump({"masters": masters_status, "servers": entries}, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f"{len(entries)} servers written to {args.json}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print_table(entries, masters_status)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user