#!/usr/bin/env python3 """Quake Live server checks. With sv_master 0 (not browser-listed) QL ignores getstatus/getinfo/A2S, so liveness uses the connect handshake ('getchallenge' -> 'No Steam auth token.') and player info comes from RCON 'status' (only on the server machine, through ../rcon). python tools/qlq.py alive check from anywhere (exit 0 = answered, 1 = no answer) python3 tools/qlq.py --humans (server) number of human players (bots have address 'bot') python3 tools/qlq.py --status (server) alive + map, factory, players """ import os, re, socket, subprocess, sys H = b"\xff\xff\xff\xff" RCON = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "rcon") # num score ping name lastmsg address qport rate steamid (ping may be CNCT/ZMBI while connecting) ROW = re.compile(r"^\s*\d+\s+(-?\d+)\s+(\d+|CNCT|ZMBI)\s+(.*?)\s+\d+\s+(\S+)\s+\d+\s+\d+\s+\d+\s*$") def is_alive_reply(data): return data.startswith(H) and len(data) > 4 def alive(host, port, timeout=3, tries=3): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(timeout) for _ in range(tries): try: s.sendto(H + b"getchallenge\n", (host, port)) if is_alive_reply(s.recv(65535)): return True except OSError: continue return False def parse_rcon_status(text): """-> (mapname, [(score, ping, name, address)]) or None if this is not a status reply.""" m = re.search(r"^map: (\S+)", text, re.M) if not m: return None players = [] for l in text.splitlines(): r = ROW.match(l) if r: players.append((int(r.group(1)), r.group(2), r.group(3), r.group(4))) return m.group(1), players def humans(players): return sum(1 for p in players if p[3] != "bot") def rcon(cmd): return subprocess.run([RCON, cmd], capture_output=True, text=True, timeout=20).stdout def main(argv): host, port = argv[1], int(argv[2]) if "--humans" in argv: st = parse_rcon_status(rcon("status")) if st is None: print("?"); return 1 print(humans(st[1])); return 0 if not alive(host, port): print(f"no answer from {host}:{port}"); return 1 if "--status" in argv: st = parse_rcon_status(rcon("status")) fac = re.search(r'"g_factory" is:"([^"]*)"', rcon("g_factory")) if st is None: print(f"alive {host}:{port} | rcon: no status"); return 0 print(f"alive {host}:{port} | map {st[0]} | factory {fac.group(1) if fac else '?'}" f" | players {len(st[1])} (humans {humans(st[1])})") for p in st[1]: print(f" {p[2]} score {p[0]} ping {p[1]} {'bot' if p[3] == 'bot' else 'human'}") return 0 print(f"alive {host}:{port}") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))