From 0b983703a4c000818b7ca18075a0b8a29ded6fb7 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sat, 1 Aug 2026 11:23:24 -0500 Subject: [PATCH] #98: CORRECTION -- restore the byte-verified condenser lamp table; my previous mapping was a guess and was wrong 0254d9e re-mapped condensers onto the coolingLoop1..6 lamps on the reasoning that "six condensers <-> six loops" and that the table's missing 0x2C was a transcription slip. It was not. Read the decomp -- which was exported the whole time and which I should have read BEFORE touching the table: FUN_004cc264(sub) { return *(int *)(&DAT_0051d058 + sub[0x1d4] * 4); } indexed by condenserNumber, no bounds check. And the bytes at 0051d058 are: 07 00 00 00 2F 00 00 00 2E 00 00 00 2D 00 00 00 2B 00 00 00 2A 00 00 00 29 00 00 00 1A 00 00 00 So the ORIGINAL table was byte-accurate: the binary really does skip 0x2C. The condenser lamps (2F 2E 2D 2B 2A 29) are a DIFFERENT set from the coolingLoop lamps of FUN_004cc148 (2F 2E 2D 2C 2B 2A) -- overlapping but distinct. My "fix" therefore mis-mapped condensers 4, 5 AND 6, replacing a correct mapping with a plausible-looking wrong one. What IS a real port defect, and all that should have changed: the bounds check. `n >= 0 && n < 6` is a 0-based bound on a 1-based index (live: 'Condenser6' reports 6), so condenser 6 was rejected and annunciated NOTHING, where the binary resolves it to 0x29. The table is now 7 slots ending in 0x29 and the guard admits 1..6 -- kept rather than dropped, so a stray authored number cannot walk off the end into the adjacent per-placement table the way the binary's unchecked read would. Verified: Condenser6 leak -> [galarm] condition 2 -> lamp 0x29 FLASH, which is what the binary computes for it. Lesson, and it is the third time this week: I had a live-reproduced symptom and inferred the cause from internal consistency instead of reading the available decomp. The symptom was real; the explanation was invented. Gotcha 24 covers over-generalising a verified fact -- this is its sibling: inventing a fact to explain a verified symptom. Co-Authored-By: Claude Opus 5 (1M context) --- game/reconstructed/btl4galm.cpp | 40 ++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/game/reconstructed/btl4galm.cpp b/game/reconstructed/btl4galm.cpp index 501dedb..43861f3 100644 --- a/game/reconstructed/btl4galm.cpp +++ b/game/reconstructed/btl4galm.cpp @@ -163,8 +163,13 @@ static const int kBTQuadModeMask[12] = { 0x1,0x1,0x1,0x1, 0x20,0x20,0x20,0x20, 0 static const int kBTEngModeMask[12] = { 0x2,0x4,0x8,0x10, 0x40,0x80,0x100,0x200, 0x800,0x1000,0x2000,0x4000 }; static const int kBTQuadLamp[12] = { 0xF,0xD,0xB,0x9, 0x27,0x25,0x23,0x21, 0x7,0x5,0x3,0x1 }; static const int kBTEngBankTop[12] = { 0xF,0xF,0xF,0xF, 0x27,0x27,0x27,0x27, 0x7,0x7,0x7,0x7 }; -// (the per-condenser lamp table that used to live here is retired: condenser N -// resolves through BTFixedLampOf(N-1), the same six cooling-loop lamps. #98) +// @0051d058 per-condenser lamps, BYTE-VERIFIED from the image (int32 each): +// 07 2F 2E 2D 2B 2A 29 ... indexed 1-BASED by condenserNumber (+0x1D4), +// so slot 0 is unused and condensers 1..6 -> 2F 2E 2D 2B 2A 29. +// Deliberately NOT the coolingLoop set of FUN_004cc148 (which has 0x2C and +// no 0x29) -- overlapping but distinct lamps. Slot 6 (0x29) is also +// DAT_0051d070[0], the first per-placement lamp: the two tables abut. +static const int kBTCondenserLamp[7] = { 0x7, 0x2F,0x2E,0x2D,0x2B,0x2A, 0x29 }; static const int kBTPlacementLamp[5] = { 0x29, 0x1A,0x1B,0x1C,0x1D }; // [auxScreenPlacement] // @@ -292,21 +297,30 @@ void // #98 -- "I'm getting leaks but no indicators" / "the display // buttons aren't ALWAYS flashing on leaking components". // - // condenserNumber is 1-BASED (verified live: 'Condenser6' reports 6), - // which the old table's own comment said -- but the guard was - // `n >= 0 && n < 6`, a 0-based bound. Condenser 6 was therefore - // REJECTED outright and could never annunciate, and the table it - // indexed was missing 0x2C, so condensers 4 and 5 flashed the NEXT - // loop's button instead of their own. Three of six correct, one - // silent, two lying -- exactly the "sometimes" players reported. + // FUN_004cc264 is literally: + // return *(int *)(&DAT_0051d058 + sub[0x1d4] * 4); + // -- indexed by condenserNumber with NO bounds check. The table + // bytes at 0051d058 are, verified from the image: + // 07 2F 2E 2D 2B 2A 29 ... (int32 each) + // and condenserNumber is 1-BASED (live: 'Condenser6' reports 6), so + // slot 0 (0x7) is unused and condensers 1..6 map to + // 0x2F 0x2E 0x2D 0x2B 0x2A 0x29. + // NOTE these are NOT the coolingLoop1..6 lamps of FUN_004cc148 + // (0x2F 0x2E 0x2D 0x2C 0x2B 0x2A) -- the condenser set skips 0x2C + // and ends on 0x29. They overlap but are different lamps; do not + // "correct" one into the other (I did, and it mis-mapped 4, 5 and 6). // - // Condenser N drives cooling-loop N, whose six lamps are the same - // ones the coolingLoop1..6 codes resolve to, so use that verified - // map rather than a second, partly-wrong copy of it. + // THE PORT BUG was only the bounds check: `n >= 0 && n < 6` is a + // 0-based bound on a 1-based index, so condenser 6 was rejected and + // annunciated NOTHING, while the binary resolves it to 0x29. Six + // condensers, one permanently silent -- which is what players read as + // "sometimes". The guard now admits 1..6 (kept, unlike the binary's + // unchecked read, so a stray authored number cannot walk off the + // table into the adjacent per-placement one). // int n = BTCondenserNumber(the_subsystem); if (n >= 1 && n <= 6) - lamp_id = BTFixedLampOf(n - 1); // loop N -> its own button + lamp_id = kBTCondenserLamp[n]; } } else if (BTSubsystemIsGenerator(the_subsystem))