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>
184 lines
6.5 KiB
Python
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)
|