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
93 lines
3.1 KiB
Python
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])
|