Files
BT411/scratchpad/night8/leaklamp_pixel.py
T
Joe DiPrimaandClaude Opus 5 56f15b569a #98: prove the lamp fix -- table pinned to the image, three condensers verified live
Follows the request to prove the whole fix rather than the one case I had.

1. TABLE, all indices: scratchpad/night8/lamptable_check.py asserts the
   reconstructed arrays against BTL4OPT.EXE and exits nonzero on drift.
       kBTCondenserLamp @0051d058  MATCH  07 2F 2E 2D 2B 2A 29
       kBTPlacementLamp @0051d070  MATCH  29 1A 1B 1C 1D
       kFixed (FUN_004cc148 switch) MATCH  2F 2E 2D 2C 2B 2A
   This covers condensers 2/3/5 without hunting zones for them: there is no
   per-index code path, only the array contents, and those are now pinned.

2. LOOKUP PATH, live: condensers 1, 4 and 6 resolve 0x2F, 0x2B and 0x29 --
   exactly the table.  Condenser 4 is the meaningful control: my reverted "fix"
   would have given 0x2C.  Myomers (eng-page path, 0x25/0x21) and SRM6_1 (quad
   button, 0xd) still annunciate, so other subsystem classes are unregressed.

3. GUARD: condenserNumber is parsed from the name's trailing digit
   (NameTrailingNumber, mirroring the binary's atoi), and every condenser in
   BTL4.RES is Condenser1..Condenser6 -- no Condenser0, no bare name.  So the
   1..6 guard covers every shipped case and slot 0 (0x7) is unreachable.  This
   was the real risk in changing `n >= 0` to `n >= 1`; it is closed.

4. PIXELS: leaklamp_pixel.{sh,py} capture a leaking run and an undamaged control
   run from the cockpit and difference their per-pixel temporal variance.  The
   leak is visibly real -- the COOLANT reservoir drains on screen (S 331->330
   while the control sits at 329).

⚠ WHAT THE PIXELS DID NOT SETTLE, and it is not a testing gap.  Lamp 0x29 is
ALSO kBTPlacementLamp[0]: DAT_0051d058[6] and DAT_0051d070[0] are the SAME int32
-- the two tables abut.  So condenser 6's lamp may not be its own loop button at
all, and slot 6 may be an overrun in the BINARY too (its read is unchecked).
Reproducing it is the faithful choice either way, and we now do exactly what the
binary computes -- but whether a pilot sees loop 6's own button light is a
question only someone who played the original can answer.  Asked on the issue.

CORRECTION: 0b98370 claimed @0051d058 "holds gauge-type name strings, so the
provenance is questionable".  That was MY bug -- a PE section-header field-order
mistake (unpacking VirtualSize/VirtualAddress/SizeOfRawData/PointerToRawData
then destructuring in a different order).  The original reconstruction's
provenance was accurate.  The checker in (1) uses the corrected reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 12:26:47 -05:00

63 lines
2.3 KiB
Python

"""#98 pixel proof: locate the FLASHING lamp by temporal variance.
A flashing lamp alternates between two appearances, so across frames of an
otherwise-static cockpit it is the pixels that CHANGE. Comparing a leaking run
against an undamaged control run localises the flash without anyone having to
guess panel coordinates first.
"""
import glob, sys
import numpy as np
from PIL import Image
# The 2D cockpit panels only -- excludes the 3D viewport so world/particle
# motion cannot be mistaken for a lamp. (Viewport is ~x 280-1170, y 225-660.)
PANEL_LEFT = (slice(0, 300), slice(0, 330)) # (rows, cols) -- the coolant panel
def frames(prefix, take=40):
# the last frame is often truncated (the process is killed mid-write), and a
# corrupt frame must not be mistaken for change -- skip anything unreadable
fs = sorted(glob.glob("lamp_%s_*.png" % prefix))
out = []
for f in reversed(fs):
if len(out) >= take:
break
try:
out.append(np.asarray(Image.open(f).convert("L"), dtype=np.float32))
except Exception:
continue
if not out:
raise SystemExit("no readable frames for %s" % prefix)
return np.stack(out)
def report(prefix):
a = frames(prefix)
print("%-6s %d frames, %s" % (prefix, a.shape[0], a.shape[1:]))
sd = a.std(axis=0) # per-pixel temporal std-dev
panel = sd[PANEL_LEFT]
print(" coolant-panel std: mean=%.3f max=%.1f pixels>10: %d"
% (panel.mean(), panel.max(), int((panel > 10).sum())))
if panel.max() > 10:
ys, xs = np.where(panel > 10)
print(" hotspot bbox rows %d-%d, cols %d-%d (centroid %d,%d)"
% (ys.min(), ys.max(), xs.min(), xs.max(), int(ys.mean()), int(xs.mean())))
return sd
print("=" * 64)
leak = report("leak")
print()
ctl = report("ctl")
print("\n" + "=" * 64)
pl, pc = leak[PANEL_LEFT], ctl[PANEL_LEFT]
print("VERDICT")
print(" leaking run : %d panel pixels flashing (std>10)" % int((pl > 10).sum()))
print(" control run : %d panel pixels flashing (std>10)" % int((pc > 10).sum()))
if (pl > 10).sum() > 0 and (pc > 10).sum() == 0:
print(" -> a lamp flashes ONLY when a condenser is leaking. PASS")
elif (pl > 10).sum() == 0:
print(" -> nothing flashes on the leaking run. FAIL")
else:
print(" -> both runs show panel motion; inspect the bboxes above")