Files
BT411/scratchpad/test_mixed_version.py
T
arcattackandClaude Opus 5 d3e724c254 cut 4.11.584 for tonight, and PROVE it still plays with the 554 everyone has
The merge brought Cyd's cockpit refit into the build testers will run tonight,
so the question that decides the evening is whether a 554 client can still join.
Rather than reason about it, tested it: extracted the real BT411_4.11.554.zip
and ran ONE 554 pod and ONE 584 pod against the same relay through a real
launch.

  WIRE-COMPATIBLE.  Both staged, both REGISTERED, the relay launched the mixed
  round, and 30s in both were still registered AND still on the UDP fast path
  (registered [2, 3] udp-known [2, 3]), zero drops, both processes alive.
  => NOBODY HAS TO UPDATE TONIGHT.  The new zip is an upgrade, not a
     flag day (unlike 554, which changed the scoreboard wire format).

Supporting evidence for why that held: the merge did NOT change the attribute
table's shape -- 36 ATTRIBUTE_ENTRY(Mech,...) rows before and after.  Cyd's
crouch work POPULATED an existing pad slot (0x37 DuckState, previously
attrPad), so no index shifted.

The zip itself (dist/BT411_4.11.584.zip, 47.0 MB):
  * built clean at HEAD 2b0506d, 0 compile errors, the 40 /FORCE-tolerated
    externs byte-identical to every prior build;
  * 34-check document audit passes, now including "the README does NOT
    advertise the RGB keylight" since that feature was unshipped today;
  * CLEAN-EXTRACT BOOT from the packaged zip: 0 faults, GLASS profile with the
    cockpit surround, environ.ini written, 74-key board loaded, and
    content\bindings.txt generated carrying the "# bindings-board 2" marker --
    i.e. a fresh install lands on the new board with the migration armed;
  * the zip ships NEITHER bindings.txt NOR operator_secret.txt (verified) --
    player files and a live credential both stay out;
  * screenshot from the extracted build confirms the refit renders: under-glass
    buttons, corrected hat labels, both weapon columns, radar centred.

One harness bug fixed here: the drop check matched a bare "dropped", which also
matches the stats line's own counter -- "(dropped 0, tcp-fallback 0)" -- and the
operator's control socket closing normally.  It reported a compatibility FAILURE
on a perfectly healthy mixed round.  Now matches real pod drops only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:10:24 -05:00

184 lines
6.5 KiB
Python

"""Can a 554 client still play against tonight's build? (2026-07-26)
The operator's testers are on BT411_4.11.554. Tonight's build carries Cyd's
cockpit-refit merge, so before telling anyone "you don't need to update", prove
it: one pod from the OLD zip and one from the NEW build, on the same relay,
through a real launch.
Pass = both REGISTER, the relay launches, and NEITHER pod drops during the
round. Fail = a mismatch that would strand half the players tonight.
Usage: python scratchpad/test_mixed_version.py [path-to-old-554-root]
Spawns its own relay + pods; kills only that tree.
"""
import os
import re
import shutil
import subprocess
import sys
import time
REPO = r"C:\git\bt411"
CONTENT = os.path.join(REPO, "content")
SCRATCH = os.environ.get("TEMP", ".")
OLD_ROOT = (sys.argv[1] if len(sys.argv) > 1 else
os.path.join(SCRATCH, "claude", "C--git-bt411",
"89ab96e9-6cf0-4555-939d-05bf3708745a", "scratchpad",
"old554", "BT411_4.11.554"))
PORT = 1500
SRC_EGG = "FOGDAY.EGG"
EGG = "_mixed_test.EGG"
RELAY_LOG = os.path.join(SCRATCH, "mixed_relay.log")
fails = []
procs = []
def check(label, ok, extra=""):
print(" %-52s %s %s" % (label, "OK" if ok else "*** FAIL ***", extra))
if not ok:
fails.append(label)
def relay_text():
try:
with open(RELAY_LOG, encoding="utf-8", errors="replace") as f:
return f.read()
except OSError:
return ""
def wait_for(pattern, timeout):
end = time.time() + timeout
while time.time() < end:
if re.search(pattern, relay_text()):
return True
time.sleep(0.25)
return False
def kill_tree():
ours = {p.pid for p in procs}
try:
out = subprocess.run(["wmic", "process", "get",
"ProcessId,ParentProcessId"],
capture_output=True, text=True).stdout
pairs = []
for line in out.splitlines():
b = line.split()
if len(b) == 2 and b[0].isdigit() and b[1].isdigit():
pairs.append((int(b[0]), int(b[1])))
for _ in range(4):
for parent, pid in pairs:
if parent in ours:
ours.add(pid)
except Exception:
pass
for p in procs:
try:
if p.poll() is None:
p.kill()
except Exception:
pass
for pid in ours:
subprocess.run(["taskkill", "/PID", str(pid), "/F", "/T"],
capture_output=True)
try:
old_exe = os.path.join(OLD_ROOT, "build", "Release", "btl4.exe")
new_exe = os.path.join(REPO, "build", "Release", "btl4.exe")
old_content = os.path.join(OLD_ROOT, "content")
for p in (old_exe, new_exe, old_content):
if not os.path.exists(p):
sys.exit("missing: %s" % p)
if "btl4.exe" in subprocess.run(
["tasklist", "/FI", "IMAGENAME eq btl4.exe", "/NH"],
capture_output=True, text=True).stdout:
sys.exit("ABORT: btl4 already running")
print("OLD pod: %s" % old_exe)
print("NEW pod: %s" % new_exe)
# the SAME egg for both sides (copy into each content dir)
shutil.copyfile(os.path.join(CONTENT, SRC_EGG),
os.path.join(CONTENT, EGG))
shutil.copyfile(os.path.join(CONTENT, SRC_EGG),
os.path.join(old_content, EGG))
log = open(RELAY_LOG, "w", encoding="utf-8")
procs.append(subprocess.Popen(
[sys.executable, "-u", os.path.join(REPO, "tools", "btconsole.py"),
"--relay", str(PORT), os.path.join(CONTENT, EGG),
"--bind", "127.0.0.1", "--manual-launch"],
cwd=CONTENT, stdout=log, stderr=subprocess.STDOUT))
check("relay up", wait_for(r"console 127\.0\.0\.1:%d" % PORT, 20))
base = {"BT_START_INSIDE": "1", "BT_DEV_GAUGES": "1", "BT_MATCHLOG": "0",
"BT_RELAY": "127.0.0.1:%d" % PORT}
# seat 1 = the OLD 554 pod, seat 2 = tonight's build
for label, exe, cwd, netport in (
("554(old)", old_exe, old_content, 1502),
("584(new)", new_exe, CONTENT, 1602)):
e = dict(os.environ)
e.update(base)
e["BT_LOG"] = "mixed_%s.log" % netport
e["BT_SELF"] = "127.0.0.1:%d" % netport
procs.append(subprocess.Popen(
[exe, "-egg", EGG, "-net", str(netport)], cwd=cwd, env=e))
print(" started %s on :%d" % (label, netport))
time.sleep(3)
check("BOTH pods staged (old + new on one relay)",
wait_for(r"WAITING FOR OPERATOR", 120))
regs = len(re.findall(r"REGISTERED", relay_text()))
check("both REGISTERED", regs >= 2, "registered=%d" % regs)
# launch a real round
import socket
with open(os.path.join(CONTENT, "operator_secret.txt")) as f:
secret = f.read().strip()
s = socket.create_connection(("127.0.0.1", PORT + 7), timeout=10)
s.sendall(("AUTH %s\n" % secret).encode())
time.sleep(0.6)
s.sendall(b"launch\n")
time.sleep(1)
s.close()
check("relay launched the mixed round",
wait_for(r"RunMission #2 sent to 2 pod", 60))
# both must SURVIVE the round -- a wire mismatch shows up as a drop
print(" holding the round for 30s to watch for drops...")
time.sleep(30)
# Match REAL pod drops only. A bare "dropped" also matches the stats
# line's own counter -- "(dropped 0, tcp-fallback 0)" -- and the operator's
# control socket closing normally, which cost one false failure here.
txt = relay_text()
drops = re.findall(r"game\[[^\]]*\] dropped|PLAYER \d+ LEFT", txt)
check("neither pod dropped during the round", len(drops) == 0,
"drops=%r" % (drops[:3],))
last = txt.rstrip().splitlines()[-1] if txt.strip() else ""
m = re.search(r"registered \[([^\]]*)\] udp-known \[([^\]]*)\]", txt[::-1][:0] + txt)
reg = re.findall(r"registered \[([^\]]*)\] udp-known \[([^\]]*)\]", txt)
check("both hosts still registered AND on UDP at the end",
bool(reg) and reg[-1][0].count(",") == 1 and reg[-1][1].count(",") == 1,
"last stats: registered [%s] udp-known [%s]" % (reg[-1] if reg else ("?", "?")))
alive = sum(1 for p in procs[1:] if p.poll() is None)
check("both pod processes still alive", alive == 2, "alive=%d/2" % alive)
finally:
kill_tree()
for junk in (os.path.join(CONTENT, EGG), os.path.join(old_content, EGG)):
try:
os.remove(junk)
except OSError:
pass
print()
print("WIRE-COMPATIBLE with 554" if not fails else "FAILURES: %s" % fails)
sys.exit(1 if fails else 0)