#!/usr/bin/env python3 """Mehr-Runden-Loopback mit zwei bzw. drei Godot-Prozessen (Host, Gast, wiederkehrender Gast). Ablauf: Der Host spielt --rounds Runden. Gast A spielt mit und trennt die Verbindung im Kampf der Runde --drop-round. Danach startet Gast B mit demselben Anzeigenamen und muss denselben Sitz zurückerhalten. Nach jeder Abrechnung vergleicht das Skript die Prüfsumme des öffentlichen Zustands von Host und Gast. Das ist ein lokaler Test über 127.0.0.1, keine Internetpartie auf sechs Rechnern. Aufruf: python3 tools/luneraria/net_rounds.py [--rounds 5] [--drop-round 2] [--godot godot] """ import argparse import json import re import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[2] LOG_DIR = ROOT / "docs" / "luneraria" / "logs" / "m4" ROUND_RE = re.compile(r"NET_ROUND role=(\w+) round=(\d+) phase=(\d+) sum=(\S+)") COMBAT_RE = re.compile(r"NET_CLIENT_COMBAT round=(\d+) frames=(\d+) local_steps=(-?\d+)") SETUP_RE = re.compile(r"NET_SETUP seat=(\d+)") def launch(godot, port, *extra): cmd = [godot, "--headless", "--path", str(ROOT), "res://tools/luneraria/net_pair.tscn", "--", f"--port={port}", *extra] return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) def main(): parser = argparse.ArgumentParser() parser.add_argument("--godot", default="godot") parser.add_argument("--rounds", type=int, default=5) parser.add_argument("--drop-round", type=int, default=2) parser.add_argument("--port", type=int, default=24581) args = parser.parse_args() LOG_DIR.mkdir(parents=True, exist_ok=True) host = launch(args.godot, args.port, "--role=host", f"--rounds={args.rounds}") time.sleep(2.0) guest_a = launch(args.godot, args.port, "--role=client", "--name=Gast", f"--rounds={args.rounds}", f"--drop-round={args.drop_round}") out_a, _ = guest_a.communicate(timeout=300) guest_b = launch(args.godot, args.port, "--role=client", "--name=Gast", f"--rounds={args.rounds}") out_b, _ = guest_b.communicate(timeout=300) out_host, _ = host.communicate(timeout=300) for name, text in (("host", out_host), ("gast-a", out_a), ("gast-b", out_b)): (LOG_DIR / f"net-rounds-{name}.log").write_text(text) def sums(text, role): return {int(m.group(2)): m.group(4) for m in ROUND_RE.finditer(text) if m.group(1) == role} host_sums = sums(out_host, "host") guest_sums = {**sums(out_a, "client"), **sums(out_b, "client")} compared = sorted(set(host_sums) & set(guest_sums)) mismatches = [r for r in compared if host_sums[r] != guest_sums[r]] combats = [{"round": int(m.group(1)), "frames": int(m.group(2)), "local_steps": int(m.group(3))} for text in (out_a, out_b) for m in COMBAT_RE.finditer(text)] seats_a = [int(s) for s in SETUP_RE.findall(out_a)] seats_b = [int(s) for s in SETUP_RE.findall(out_b)] checks = { "host_ok": host.returncode == 0, "guest_a_ok": guest_a.returncode == 0 and "NET_CLIENT_DROP" in out_a, "guest_b_ok": guest_b.returncode == 0 and "NET_CLIENT_DONE" in out_b, "spoof_rejected": "ACCEPTED-BAD" not in out_a + out_b and "NET_SPOOF REJECTED" in out_a, "rounds_compared": len(compared) >= args.rounds - 1, "checksums_equal": not mismatches and bool(compared), "client_never_simulates": bool(combats) and all(c["local_steps"] == 0 for c in combats), "client_receives_frames": bool(combats) and all(c["frames"] > 0 for c in combats), "rejoin_same_seat": bool(seats_a) and bool(seats_b) and seats_b[0] == seats_a[0], "bot_took_over": "Slot wird vom Bot übernommen" in out_host, "rejoin_announced": "kehrt zurück auf" in out_host, "no_script_errors": all("SCRIPT ERROR" not in t for t in (out_host, out_a, out_b)), } report = {"rounds": args.rounds, "drop_round": args.drop_round, "compared_rounds": compared, "mismatches": mismatches, "host_sums": host_sums, "guest_sums": guest_sums, "client_combats": combats, "seats": {"gast_a": seats_a, "gast_b": seats_b}, "checks": checks} (LOG_DIR / "net-rounds.json").write_text(json.dumps(report, indent=2, ensure_ascii=False)) failed = [k for k, v in checks.items() if not v] print(json.dumps(checks, indent=2)) print("NET_ROUNDS_%s: %d/%d" % ("PASS" if not failed else "FAIL", len(checks) - len(failed), len(checks))) return 0 if not failed else 1 if __name__ == "__main__": sys.exit(main())