From d3e724c2544f19d0e0255cfd61f1837244cd4ff8 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sun, 26 Jul 2026 20:10:24 -0500 Subject: [PATCH] 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) --- scratchpad/test_mixed_version.py | 183 +++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 scratchpad/test_mixed_version.py diff --git a/scratchpad/test_mixed_version.py b/scratchpad/test_mixed_version.py new file mode 100644 index 0000000..88272f2 --- /dev/null +++ b/scratchpad/test_mixed_version.py @@ -0,0 +1,183 @@ +"""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)