Files
BT411/scratchpad/commstest.py
T
arcattackandClaude Opus 5 2dd4ee3910 ROOT CAUSE: target designation wrote a Mech* into deathPending, disabling respawn (#57/#48)
MEASURED our compiled BTPlayer layout (new one-shot [layout] ctor diagnostic):
  sizeof=652 (0x28c)  killCount@0x270  pad_0x280@0x274
  objectiveMech@0x278  deathPending@0x284

The three target-designation sites wrote RAW at pilotArray[0]+0x284 intending the
BINARY's objectiveMech.  On our object 0x284 is deathPending -- the death-cycle
latch.  So EVERY target designation (the Comm bank 0x30-0x37, or the typed
t/y/u/i/o keys) stored a Mech pointer into deathPending, and a non-zero
deathPending makes VehicleDeadMessageHandler take its dedup early-return, which
skips the warp, ++deathCount, the PLAYER_DEAD record AND the drop-zone hunt.

=> designating a target silently disabled your respawn for the rest of the
   process.  That is #57's missing first cause: it explains David's very first
   death being swallowed with no prior death, and the 3-of-3 swallowed deaths in
   the final round.  It also explains the operator's persistent observation that
   the trouble began when the comms panel went live -- before the pilot list
   populated, there was nothing to designate.
   And the designation never worked either: the value never reached
   objectiveMech (input-audit finding #1).

FIX: all three sites now use named-member bridges in the complete-BTPlayer TU --
BTPilotSetObjectiveMech / BTPilotObjectiveMech / BTPilotVehicle / BTPilotPosition
/ BTVehicleDestroyed.  Also retires the raw +0x1fc (playerVehicle) reads and the
raw +0x100 position reads in ChooseNearestPilot, and routes its destroyed-check
through the real Mech predicate instead of the Is_Destroyed stub.

LAYOUT LOCKS: static_asserts on objectiveMech@0x278, deathPending@0x284,
killCount@0x270 and sizeof==0x28c, in the friend fn BTPlayerLayoutSelfCheck, so
a member shift fails the BUILD instead of silently clobbering the latch again.

Rigs: scratchpad/commstest.py (drives designation + captures panels),
valvetest.py, lampgallery.py (428-bitmap gallery for #48 visual search).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-25 08:33:22 -05:00

93 lines
3.1 KiB
Python

"""#48 solo reproduction: drive the COMMS panel's target-designation path.
The one comms action never exercised in any rig. Target designation (typed
t/y/u/i/o, and the Comm bank buttons 0x30-0x37) performs three RAW writes at
`pilotArray[0] + 0x284` (mechmppr.cpp:1344/:1364/:1420) intending the binary's
`objectiveMech`. Our compiled layout differs from the binary's, so that write
lands on whatever member really sits at byte 0x284 -- the ctor now prints the
real offsets, and this run hammers the designation keys while capturing frames.
Also toggles the pilot list's SELECT-TARGET highlight, which is the branch that
makes PilotList::DrawMechIcon repaint (filled box / inverted blit).
Kills only the PID it spawns.
"""
import ctypes
import os
import re
import shutil
import subprocess
import sys
import time
KEYUP = 0x0002
user32 = ctypes.windll.user32
REPO = r"C:\git\bt411"
CONTENT = os.path.join(REPO, "content")
EXE = os.path.join(REPO, "build", "Release", "btl4.exe")
SHOT = os.path.join(REPO, "scratchpad_comms.png")
LOG = os.path.join(REPO, "scratchpad_comms.log")
OUT = os.path.join(REPO, "scratchpad", "comms")
os.makedirs(OUT, exist_ok=True)
for f in (SHOT, LOG):
if os.path.exists(f):
os.remove(f)
env = dict(os.environ)
env.update({
"BT_START_INSIDE": "1",
"BT_KEY_NOFOCUS": "1",
"BT_DEV_GAUGES": "1", # cockpit surround -> BT_SHOT captures all panels
"BT_SPAWN_ENEMY": "1", # something to designate
"BT_SCORE_LOG": "1", # pilot roster + score diagnostics
"BT_SHOT": SHOT,
"BT_LOG": LOG,
})
proc = subprocess.Popen([EXE, "-egg", "LAST.EGG"], cwd=CONTENT, env=env)
print("pid", proc.pid)
def tap(ch, hold=0.10):
vk = ord(ch.upper())
user32.keybd_event(vk, 0, 0, 0)
time.sleep(hold)
user32.keybd_event(vk, 0, KEYUP, 0)
time.sleep(0.12)
saved = []
try:
deadline = time.time() + 220
while time.time() < deadline:
if os.path.exists(SHOT) and os.path.getsize(SHOT) > 20000:
break
time.sleep(2)
else:
print("FAIL: never rendered"); sys.exit(2)
time.sleep(6)
shutil.copy(SHOT, os.path.join(OUT, "00_baseline.png")); saved.append("00_baseline")
# hammer the designation keys -- each press runs the raw +0x284 write path
for rnd in range(6):
for ch in "tyuio":
tap(ch)
time.sleep(4)
n = "%02d_after_%d_designations" % (rnd + 1, (rnd + 1) * 5)
try:
shutil.copy(SHOT, os.path.join(OUT, n + ".png")); saved.append(n)
except Exception as e:
print(" copy failed:", e)
print(" round %d: 5 designations sent" % (rnd + 1))
finally:
proc.terminate()
time.sleep(1)
print("\nsaved %d frames to %s" % (len(saved), OUT))
txt = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
for pat, label in ((r"^\[layout\].*$", "LAYOUT"),
(r"^\[score\] pilot roster built.*$", "roster"),
(r"^\[target\].*$", "target")):
hits = re.findall(pat, txt, re.M)
print("--- %s (%d):" % (label, len(hits)))
for h in hits[:3]:
print(" ", h[:150])