From 56f15b569ac1fc73d07857419d6e1e15e8e5547c Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sat, 1 Aug 2026 12:26:47 -0500 Subject: [PATCH] #98: prove the lamp fix -- table pinned to the image, three condensers verified live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scratchpad/night8/lamptable_check.py | 73 ++++++++++++++++++++++++++++ scratchpad/night8/leaklamp_pixel.py | 62 +++++++++++++++++++++++ scratchpad/night8/leaklamp_pixel.sh | 45 +++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 scratchpad/night8/lamptable_check.py create mode 100644 scratchpad/night8/leaklamp_pixel.py create mode 100644 scratchpad/night8/leaklamp_pixel.sh diff --git a/scratchpad/night8/lamptable_check.py b/scratchpad/night8/lamptable_check.py new file mode 100644 index 0000000..8934e9e --- /dev/null +++ b/scratchpad/night8/lamptable_check.py @@ -0,0 +1,73 @@ +"""Assert the reconstructed lamp tables byte-for-byte against BTL4OPT.EXE. + +#98: the per-condenser lamp table was "corrected" on a consistency argument and +was wrong. This pins every entry to the image so the next person does not have +to trust a comment -- run it and it either matches or it does not. +""" +import re, struct, sys + +EXE = r"C:\git\bt411\content\BTL4OPT.EXE" +SRC = r"C:\git\bt411\game\reconstructed\btl4galm.cpp" + +d = open(EXE, "rb").read() +e = struct.unpack_from("= 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") diff --git a/scratchpad/night8/leaklamp_pixel.sh b/scratchpad/night8/leaklamp_pixel.sh new file mode 100644 index 0000000..8939249 --- /dev/null +++ b/scratchpad/night8/leaklamp_pixel.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# #98 PIXEL PROOF: does a leaking condenser's cooling-loop button visibly FLASH? +# +# The log proves the lamp RESOLVES and SetAlertState(True) is called. It does +# not prove a pilot sees anything. A flash is a lamp alternating between two +# appearances, so it shows up as TEMPORAL VARIANCE in that button's pixels -- +# and nowhere else on a static cockpit. +# +# Two legs, identical except for the damage: +# leak -- condenser 6 leaking (dz_rarm) +# ctl -- no self-damage at all +# Frames are captured from the cockpit view (the player's actual view) and +# compared by per-pixel variance across frames in leaklamp_pixel.py. +# +# PASS = a variance hotspot in the coolant panel on the leak leg that is absent +# on the control leg. +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > LEAK.EGG + +LEG="${1:-leak}" +rm -f lamp_${LEG}_*.png "leakpix_${LEG}.log" + +# (bt_launch is a shell FUNCTION -- export into the environment, do not `env`.) +if [ "$LEG" = "leak" ]; then + export BT_SELF_DAMAGE=4 BT_SELF_DAMAGE_ZONE=dz_rarm +else + unset BT_SELF_DAMAGE BT_SELF_DAMAGE_ZONE +fi +# Stand still and do nothing but leak: no autofire, no enemy, no motion, so the +# ONLY thing changing in the cockpit is the lamp we are testing. +export BT_LAMP_LOG=1 +export BT_SHOT_EVERY=12 BT_SHOT_PREFIX=lamp_${LEG} +bt_launch "leakpix_${LEG}.log" LEAK.EGG 0x03 + +sleep 100 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +echo "=== lamp resolution ===" +grep -E "^\[galarm\] condition 2" "leakpix_${LEG}.log" | sort -u +echo "=== frames ===" +ls lamp_${LEG}_*.png 2>/dev/null | wc -l