Files
BT411/scratchpad/lampgallery.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

95 lines
3.3 KiB
Python

"""#48: contact-sheet gallery of every gauge bitmap, so the artifact can be
identified by eye instead of by theory.
Page 0 = CANDIDATES: small, mostly-solid images (the artifacts are featureless
blocks/bars), sorted most-solid first -- the shapes worth checking.
Pages 1+= the complete set, name-sorted, for manual browsing.
Each tile is labelled with its filename and native pixel size. Mono-MFD art is
stored paletted and gets tinted green at draw time, so judge SHAPE and SIZE, not
colour.
"""
import os
from PIL import Image, ImageDraw
SRC = r"C:\git\bt411\content\GAUGE"
OUT = r"C:\git\bt411\scratchpad\gallery"
os.makedirs(OUT, exist_ok=True)
COLS, ROWS = 10, 8
IMGW, IMGH = 186, 126
LBL = 26
TILEW, TILEH = IMGW, IMGH + LBL
names = sorted(f for f in os.listdir(SRC) if f.upper().endswith((".PCC", ".PCX")))
loaded = []
for n in names:
try:
im = Image.open(os.path.join(SRC, n)).convert("RGB")
loaded.append((n, im))
except Exception:
pass
print("decoded %d of %d" % (len(loaded), len(names)))
def solidity(im):
"""fraction of pixels that are not background-black"""
small = im.resize((min(im.size[0], 64), min(im.size[1], 64)))
px = list(small.getdata())
nz = sum(1 for r, g, b in px if r + g + b > 40)
return nz / float(len(px))
def tile_for(name, im):
t = Image.new("RGB", (TILEW, TILEH), (18, 18, 18))
w, h = im.size
s = min(float(IMGW - 6) / w, float(IMGH - 6) / h)
if s > 6:
s = 6 # do not blow tiny art up beyond 6x
d = im.resize((max(1, int(w * s)), max(1, int(h * s))), Image.NEAREST)
t.paste(d, ((TILEW - d.size[0]) // 2, (IMGH - d.size[1]) // 2))
dr = ImageDraw.Draw(t)
dr.rectangle([0, 0, TILEW - 1, TILEH - 1], outline=(60, 60, 60))
dr.text((4, IMGH + 2), "%s" % name, fill=(210, 210, 210))
dr.text((4, IMGH + 13), "%dx%d" % (w, h), fill=(130, 170, 130))
return t
def make_page(items, path, title):
pw, ph = COLS * TILEW, ROWS * TILEH + 30
page = Image.new("RGB", (pw, ph), (8, 8, 8))
dr = ImageDraw.Draw(page)
dr.text((8, 9), title, fill=(255, 235, 120))
for i, (n, im) in enumerate(items):
r, c = divmod(i, COLS)
page.paste(tile_for(n, im), (c * TILEW, 30 + r * TILEH))
page.save(path)
return path
# ---- page 0: candidates (small + solid = featureless block/bar shapes) -------
cands = []
for n, im in loaded:
w, h = im.size
if w <= 80 and h <= 80: # the artifacts were ~12x54 / ~18x30
cands.append((solidity(im), n, im))
cands.sort(reverse=True, key=lambda t: t[0])
top = [(n, im) for _s, n, im in cands[:COLS * ROWS]]
pages = [make_page(top, os.path.join(OUT, "page0_CANDIDATES.png"),
"PAGE 0 -- BEST CANDIDATES: small (<=80px) and mostly SOLID, "
"most-solid first. Artifacts were ~12x54 and ~18x30 native.")]
# ---- pages 1+: everything, name-sorted --------------------------------------
per = COLS * ROWS
for p in range((len(loaded) + per - 1) // per):
chunk = loaded[p * per:(p + 1) * per]
pages.append(make_page(
chunk, os.path.join(OUT, "page%d.png" % (p + 1)),
"PAGE %d/%d -- all gauge art, name-sorted (%s .. %s)"
% (p + 1, (len(loaded) + per - 1) // per, chunk[0][0], chunk[-1][0])))
print("wrote %d pages to %s" % (len(pages), OUT))
for p in pages:
print(" " + os.path.basename(p))