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
95 lines
3.3 KiB
Python
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))
|