The client now boots cleanly on the bench machine (the CAC spawn-key
crash was fixed in 8e8483d). Add the harness used to drive bench scenes
and capture frametimes, the bench-world generation workflow, and a
self-contained handoff brief. All files live under analysis/ (packwizignored),
so nothing player-facing changes.
- harness/rcon.py: minimal RCON client for the temp worldgen server
- harness/run_scene.py: apply variant -> launch Prism under MangoHud ->
xdotool scene commands -> p50/p95/p99/1%-low from the CSV
- HANDOFF.md: state, gotchas, next steps
- build.py: disable DH autoUpdater for bench variants (suppress modal)
77 lines
2.4 KiB
Python
77 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Minimal Minecraft RCON client (stdlib only).
|
|
|
|
Usage: python3 rcon.py <host> <port> <password> <command> [command ...]
|
|
Exit non-zero if the server reports a failure.
|
|
"""
|
|
import socket
|
|
import struct
|
|
import sys
|
|
|
|
|
|
class Rcon:
|
|
def __init__(self, host: str, port: int, password: str, timeout: float = 10.0):
|
|
self.host, self.port, self.password = host, port, password
|
|
self.timeout = timeout
|
|
self._id = 0
|
|
|
|
def _send(self, sock: socket.socket, ptype: int, body: str) -> int:
|
|
self._id += 1
|
|
rid = self._id
|
|
payload = struct.pack("<ii", rid, ptype) + body.encode("utf-8") + b"\x00\x00"
|
|
sock.sendall(struct.pack("<i", len(payload)) + payload)
|
|
return rid
|
|
|
|
def _recv(self, sock: socket.socket):
|
|
raw = b""
|
|
while len(raw) < 4:
|
|
chunk = sock.recv(4 - len(raw))
|
|
if not chunk:
|
|
raise ConnectionError("RCON connection closed")
|
|
raw += chunk
|
|
(length,) = struct.unpack("<i", raw)
|
|
body = b""
|
|
while len(body) < length:
|
|
chunk = sock.recv(length - len(body))
|
|
if not chunk:
|
|
raise ConnectionError("RCON connection closed")
|
|
body += chunk
|
|
rid, ptype = struct.unpack("<ii", body[:8])
|
|
return rid, ptype, body[8:-2].decode("utf-8", "replace")
|
|
|
|
def command(self, cmd: str) -> str:
|
|
with socket.create_connection((self.host, self.port), timeout=self.timeout) as sock:
|
|
sock.settimeout(self.timeout)
|
|
rid = self._send(sock, 3, self.password)
|
|
rid2, _, _ = self._recv(sock)
|
|
if rid2 == -1:
|
|
raise PermissionError("RCON auth failed")
|
|
self._send(sock, 2, cmd)
|
|
_, _, text = self._recv(sock)
|
|
return text
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) < 5:
|
|
print(__doc__)
|
|
return 2
|
|
host, port, password = argv[1], int(argv[2]), argv[3]
|
|
rc = 0
|
|
for cmd in argv[4:]:
|
|
try:
|
|
out = Rcon(host, port, password).command(cmd)
|
|
except Exception as exc: # noqa: BLE001 - CLI surface
|
|
print(f"! {cmd}: {exc}", file=sys.stderr)
|
|
rc = 1
|
|
continue
|
|
if out.strip():
|
|
print(out.strip())
|
|
lowered = out.lower()
|
|
if "unknown" in lowered or "failed" in lowered or "error" in lowered:
|
|
rc = 1
|
|
return rc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|