diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 5227a58..ec2016d 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -134,6 +134,29 @@ copied from them. `game/btl4main.cpp` off its real `btl4App`/`hWnd` pointers (never the globals directly). See [[reconstruction-gotchas]] §6 duplicate-GLOBAL corollary. +- **Glass per-display panel button click-crash (Gitea #18): FIXED 2026-07-20 [T2].** Clicking a + per-display Engineering panel button (0x21 reported) on an **Eng page** hard-crashed — cdb: a + wild call through an uninitialised pointer (`eip=cdcdcdcd` debug / `0x01048748` release) with + **zero btl4 frames above `Receiver::Receive`**. Root cause was NOT a `/FORCE` unresolved + external (the glass link log carried only the known-benign `mech3.obj` `CreateStreamedSubsystem` + set) but a **dense message-handler-table GAP slot** ([[reconstruction-gotchas]] §11): the + streamed Eng-page `.CTL` dispatches subsystem msg **id 3 / 0xb** to whatever subsystem is shown, + and a **weapon (Emitter)** registers only PoweredSubsystem 4-8 + MechWeapon 9-10 — so id 3 is a + gap below `entryCount`, and `Receiver::MessageHandlerSet::Build`'s `new HandlerEntry[]` left it + as heap garbage → `(this->*garbage)(msg)`. The 1995 binary has the identical non-zeroing + `new[]` and survived only on fresh-heap-zero luck (id 3 on a weapon was always meant to be + IGNORED — it's a Condenser/Reservoir action in a different subsystem branch; the uniform + Eng-page button template makes buttons for unimplemented actions authentically inert). The new + per-display windows just made the id reachable live. **Fix (engine, class-wide, faithful):** + `Build` null-inits every slot (`entryID=0/entryName=""/entryHandler=NullHandler`) before + copying inherited / placing supplied, so a gap is deterministically NullHandler → `Receive` + drops it — the authentic "Receiver ignores unhandled messages", not a glass-path guard. Fixes + the entire dead-button class (#14) at once. Verified: click-soaked every MFD bank + (Engineering/L+R Weapons/Heat/Comm, ~130 clicks through Quad↔Eng page cycles) under cdb — ZERO + AVs, and the reconstructed mapper preset selects still fire (`[mode] preset (1,1)/(2,1)…`); pod + build + solo un-regressed. The `[glasswin] CLICK` forensics log (names the button before + dispatch) landed with this. Detail: `engine/MUNGA/RECEIVER.cpp` Build. + ## Key Relationships - Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes · Feeds: [[multiplayer]] (miniconsole hosting, Steam transport) · Build: [[build-and-run]] diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 6124db4..f9170d6 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -220,6 +220,35 @@ as `AttributeIndexSet::Find` crashing in `WeaponCluster::WeaponCluster("PercentD (mechweap.cpp now static_asserts its pad base against `PoweredSubsystem::NextAttributeID`). [T2] +**FIXED STRUCTURALLY 2026-07-20 — the MESSAGE-handler twin (Gitea #18, the glass dead-button +crash).** The identical hazard exists for `Receiver::MessageHandlerSet` (RECEIVER.cpp/.h): +`Find(id)` returns `messageHandlers[id-1].entryHandler` for ANY `id <= entryCount` with **no +populated-check**, and `Receiver::Receive` calls it when `!= NullHandler`. A message id that no +class in the receiver's chain registers but that sits BELOW `entryCount` lands on a gap slot; +`Build` did `new HandlerEntry[entryCount]` and left gaps **uninitialised**, so `Receive` did +`(this->*garbage)(msg)` → wild-jump AV with **zero btl4 frames above `Receiver::Receive`**. +Trigger: the streamed Eng-page `.CTL` dispatches subsystem msg **id 3 / 0xb** to whatever +subsystem is shown on that MFD page; a **weapon (Emitter)** registers only PoweredSubsystem 4-8 + +MechWeapon 9-10, so id 3 (slot[2], < entryCount 10) is a gap. Clicking the glass Engineering +panel button 0x21 on an Eng page reached it live (`eip=cdcdcdcd` debug / `0x01048748` release). +**The 1995 binary has the IDENTICAL non-zeroing `new[]`** (part_002.c Build, `FUN_004022b0` = +operator new) and only survived on fresh-OS-heap-zero luck (a zero slot == NullHandler == +ignored) — a weapon receiving id 3 was *always meant to ignore it* (id 3 = a Condenser/Reservoir +action in a different subsystem branch; the Eng-page button template is uniform, so buttons for +actions a shown subsystem doesn't implement are authentically inert). **Fix:** `MessageHandlerSet:: +Build` now null-inits every slot up front (`entryID=0; entryName=""; entryHandler=NullHandler`) +before copying inherited / placing supplied — a gap is deterministically NullHandler → `Receive` +drops it (the authentic "Receiver ignores unhandled messages"), and the empty (never-NULL) name +keeps the name-based `Find()` strcmp from dereferencing a gap. This is the correct dispatch +contract, not a glass-path guard, so it protects the WHOLE aux-bank / dead-button class at once +(one build; the reported button + every MFD bank now click-soak clean under cdb). **Lesson (the +"/FORCE-garbage-via-new-dispatch-path" the report expected):** a NEW input surface (the +per-display glass windows) that makes a previously-unreachable message id *reachable* can fire a +latent heap-luck gap that the pod/legacy panel never exercised — the smoking gun was NOT a +`/FORCE` unresolved external (the link log carried only the known-benign mech3 set) but an +uninitialised dense-table slot; when a garbage-call AV shows zero symbols above `Receiver:: +Receive`, suspect a gap slot, not a missing symbol. [T2] + ## 12. Frame-pacing trap — the binary assumes a LOCKED 60 fps (task #11) The 1995 pod ran frame-locked; reconstructed per-frame logic can carry HIDDEN frame-rate diff --git a/engine/MUNGA/RECEIVER.cpp b/engine/MUNGA/RECEIVER.cpp index 6966179..009a3d6 100644 --- a/engine/MUNGA/RECEIVER.cpp +++ b/engine/MUNGA/RECEIVER.cpp @@ -454,6 +454,36 @@ Check_Table: messageHandlers = new Receiver::HandlerEntry[entryCount]; Check_Pointer(messageHandlers); Register_Pointer(messageHandlers); + + // + //----------------------------------------------------------------------- + // FAITHFUL FIX (Gitea #18 / reconstruction-gotchas #11 -- the glass + // dead-button crash). Find(id) returns messageHandlers[id-1].entryHandler + // for ANY id <= entryCount with no populated-check (RECEIVER.h Find), and + // Receive() calls it whenever it is != NullHandler. A message id that NO + // class in the receiver's chain registers but that is < entryCount lands on + // a GAP slot -- e.g. the streamed Eng-page .CTL dispatches subsystem msg id + // 3 / 0xb to a shown weapon (Emitter) whose handler chain only registers + // PoweredSubsystem 4-8 + MechWeapon 9-10, so slot[2] (id 3) is a gap below + // entryCount 10. operator new[] leaves these POD slots UNINITIALISED, so + // the gap held heap garbage and Receive did (this->*garbage)(msg) -> AV + // (debug CRT fill 0xCDCDCDCD; a recycled release block 0x01048748). The + // 1995 binary has the IDENTICAL non-zeroing new[] (part_002.c Build) and + // only survived on fresh-OS-heap-zero luck (a zero slot == NullHandler == + // ignored). Make that intended "Receiver ignores unhandled messages" + // DETERMINISTIC: null every slot up front so a gap is NullHandler (Receive + // drops it) with an empty -- never NULL -- name so the name-based Find() + // strcmp cannot dereference a gap. Inherited/supplied handlers overwrite + // their slots below; only the true gaps stay null. + //----------------------------------------------------------------------- + // + for (i=0; i= 0) { + // crash forensics (always-on, flushed BEFORE dispatch): if a + // click kills the process, the last log line names the button. + DEBUG_STREAM << "[glasswin] CLICK '" << (w->title ? w->title : "?") + << "' addr=0x" << std::hex << a << std::dec + << " at(" << (int)(short)LOWORD(lparam) << "," + << (int)(short)HIWORD(lparam) << ")" + << (latched[a & 0x7F] ? " unlatch" : " press") + << "\n" << std::flush; if (latched[a & 0x7F]) { latched[a & 0x7F] = 0; @@ -706,6 +714,9 @@ static LRESULT CALLBACK int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam)); if (a >= 0) { + DEBUG_STREAM << "[glasswin] CLICK '" << (w->title ? w->title : "?") + << "' addr=0x" << std::hex << a << std::dec + << " latch-toggle\n" << std::flush; latched[a & 0x7F] = latched[a & 0x7F] ? 0 : 1; PadRIO::SetScreenButton(a, latched[a & 0x7F]); InvalidateRect(window, NULL, FALSE);