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
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""#48 solo test: does moving a coolant VALVE after the coolant BARS have
|
|
repainted leave a stuck XOR block on the Heat panel?
|
|
|
|
Hypothesis (from operator_1.log): VertNormalSlider paints its indicator with an
|
|
XOR fill and "erases" by XORing the same spot. Firing drops CoolantMass, which
|
|
repaints the coolant vertBars over the valve track; a later valve move then XORs
|
|
pixels that are no longer the indicator -> a permanently stuck block.
|
|
|
|
Sequence: autofire (coolant falls) + periodic 'C' valve presses, capturing the
|
|
composite each cycle, then diff the Heat panel region for pixels that turn ON and
|
|
STAY on.
|
|
|
|
Kills only the PID it spawns.
|
|
"""
|
|
import ctypes
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
KEYUP = 0x0002
|
|
VK_C = ord('C')
|
|
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_valve.png")
|
|
LOG = os.path.join(REPO, "scratchpad_valve.log")
|
|
OUT = os.path.join(REPO, "scratchpad", "valve")
|
|
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 the panels
|
|
"BT_AUTOFIRE": "1", # hold the trigger: coolant mass falls, bars repaint
|
|
"BT_SHOT": SHOT,
|
|
"BT_LOG": LOG,
|
|
})
|
|
proc = subprocess.Popen([EXE, "-egg", "LAST.EGG"], cwd=CONTENT, env=env)
|
|
print("pid", proc.pid)
|
|
|
|
def tap(vk, hold=0.12):
|
|
user32.keybd_event(vk, 0, 0, 0)
|
|
time.sleep(hold)
|
|
user32.keybd_event(vk, 0, KEYUP, 0)
|
|
|
|
frames = []
|
|
try:
|
|
# wait for the mission (the shot file only appears once rendering starts)
|
|
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)
|
|
|
|
for cycle in range(6):
|
|
# let autofire burn coolant so the bars repaint
|
|
time.sleep(7)
|
|
dst = os.path.join(OUT, "f%d_before_valve.png" % cycle)
|
|
try:
|
|
shutil.copy(SHOT, dst); frames.append(dst)
|
|
except Exception as e:
|
|
print(" copy failed:", e)
|
|
# move a coolant valve -> XOR erase at the old row
|
|
tap(VK_C)
|
|
print(" cycle %d: valve tap sent" % cycle)
|
|
time.sleep(4)
|
|
dst = os.path.join(OUT, "f%d_after_valve.png" % cycle)
|
|
try:
|
|
shutil.copy(SHOT, dst); frames.append(dst)
|
|
except Exception as e:
|
|
print(" copy failed:", e)
|
|
finally:
|
|
proc.terminate()
|
|
time.sleep(1)
|
|
|
|
print("\ncaptured %d frames in %s" % (len(frames), OUT))
|
|
txt = open(LOG, errors="replace").read() if os.path.exists(LOG) else ""
|
|
import re
|
|
print("[valve] events :", len(re.findall(r"^\[valve\]", txt, re.M)))
|
|
for l in re.findall(r"^\[valve\].*$", txt, re.M)[:8]:
|
|
print(" ", l)
|
|
print("[emitter] FIRED:", len(re.findall(r"\[emitter\] FIRED", txt)))
|