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>
63 lines
2.3 KiB
Python
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")
|