Fix glass panel dead-button crash: null-init MessageHandlerSet gap slots (Gitea #18)
Clicking a per-display glass Engineering panel button (0x21 on an Eng page) hard-crashed via a wild call through an uninitialised pointer (eip=cdcdcdcd debug / 0x01048748 release), zero btl4 frames above Receiver::Receive. Root cause is the dense message-handler-table gap hazard (reconstruction- gotchas #11), the message-side twin of the attribute-table one -- NOT a /FORCE unresolved external (the glass link log carried only the known-benign mech3.obj CreateStreamedSubsystem set). Receiver::MessageHandlerSet::Find(id) returns messageHandlers[id-1].entryHandler for any id <= entryCount with no populated- check, and Receive() calls it when != NullHandler. The streamed Eng-page .CTL dispatches subsystem msg id 3/0xb to whatever subsystem is shown; a weapon (Emitter) registers only PoweredSubsystem 4-8 + MechWeapon 9-10, so id 3 is a gap below entryCount 10. Build did new HandlerEntry[entryCount] and left gaps uninitialised -> (this->*garbage)(msg). The 1995 binary has the identical non- zeroing new[] (part_002.c Build) and survived only on fresh-heap-zero luck: a weapon receiving id 3 was always meant to IGNORE it (id 3 = 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 now 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"), with an empty never-NULL name so the name-based Find() strcmp can't deref a gap. Correct dispatch contract, not a glass-path guard -> protects the whole dead-button class (#14). Also lands the [glasswin] CLICK crash-forensics log (names the button before dispatch). Verified under cdb: click-soaked every MFD bank (Engineering/L+R Weapons/Heat/ Comm, ~130 clicks through Quad<->Eng page cycles) -> zero AVs, reconstructed mapper preset selects still fire ([mode] preset (1,1)/(2,1)...); pod build + solo mission un-regressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bc5ac3a0db
commit
5ba5697a08
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<entryCount; ++i)
|
||||
{
|
||||
messageHandlers[i].entryID = 0;
|
||||
messageHandlers[i].entryName = "";
|
||||
messageHandlers[i].entryHandler = Receiver::NullHandler;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
if (inheritance)
|
||||
{
|
||||
|
||||
@@ -674,6 +674,14 @@ static LRESULT CALLBACK
|
||||
int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam));
|
||||
if (a >= 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);
|
||||
|
||||
Reference in New Issue
Block a user