I have fully traced the widget. Here is the complete implementation spec. --- # MessageBoard — Cockpit Gauge Widget Decode & Reconstruction Spec **Verdict up front:** `MessageBoard` is *already fully declared* in `game/reconstructed/btl4gau3.hpp` and *documented in prose* in `btl4gau3.cpp`, but has **no method bodies, no `methodDescription`, and no registration** → the CFG keyword `messageBoard` is currently **parse-skipped**. This spec supplies the byte-exact bodies + registration, **corrects three field mislabels** in the existing header, and **confirms the data is DEFERRED (empty board)** — with a correction to the task hint about *which* subsystem is the blocker. --- ## 1. Identity | Property | Value | Evidence | |---|---|---| | Class name | `MessageBoard` | id string `s_MessageBoard_0051bb61` (`part_014.c:3835`); already in `btl4gau3.hpp:260` | | Base class | `GraphicGauge` (→ `Gauge`) | base ctor `FUN_00444818` (`part_014.c:3857`); `engine/MUNGA/GAUGE.h:400` | | Config keyword | `"messageBoard"` | `content/GAUGE/L4GAUGE.CFG:4913` (1 use, in `Secondary1`) | | **Make** | `@004cb678` = `FUN_004cb678` | `part_014.c:3825-3843` | | **ctor** | `@004cb704` = `FUN_004cb704` | `part_014.c:3849-3866` | | **dtor** | `@004cb788` = `FUN_004cb788` | `part_014.c:3872-3886` | | **TestInstance** | `@004cb7e4` (→ `FUN_004448ac` = `GraphicGauge::TestInstance`) | `part_014.c:3892-3897` | | ShowInstance (empty no-op vtbl slot) | `@004cb7f4` (bare `return`) | `part_014.c:3903-3907` | | **BecameActive** | `@004cb7fc` = `FUN_004cb7fc` | `part_014.c:3913-3919` | | **SetSource** (setter; **not** `SetEnable`) | `@004cb818` = `FUN_004cb818` | `part_014.c:3925-3930` | | **Execute** | `@004cb82c` = `FUN_004cb82c` | `part_014.c:3936-4013` | | vtable | `PTR_FUN_0051bddc` | ctor `part_014.c:3858`, dtor `:3876` | | **sizeof** | **`0xA4`** (== Make alloc `FUN_00402298(0xa4)`) | `part_014.c:3832` | | GUID | none | — | There is **no `methodDescription` in the binary** for this widget (the binary's Make read raw `DAT_0051b43c..0051b508`); like `PlayerStatus`/`PilotList`/`VertTwoPartBar`, the reconstruction supplies a `methodDescription` and rewires Make to read `methodDescription.parameterList[]`. --- ## 2. What it DISPLAYS + the data binding (DEFERRED — blocker confirmed) **What it draws:** the secondary MFD's **comm/status message ticker**. Two 128×32 cells at the port origin (`SetOrigin(x,y)`, `offset=(113,607)`): - **Message cell** (view-rel `(0,0)`): blits one cell of the strip bitmap `btsmsgs.pcx`. Message id → grid cell `col = id&3` (×128 px), `row = id>>2` (×32 px): `srcRect = [cellX, cellY, cellX+0x7f, cellY+0x1f]` (`part_014.c:3969-3979`). `id == -1` → clear the cell to `color` (`:3963-3967`). - **Name cell** (view-rel `(0x80,0)`): blits the *sender's* name bitmap from the App name/palette cache (`part_014.c:3983-4008`). **The data source it reads (decoded from `FUN_004cb82c`):** ``` source = this->trackedMech // this[0x24] @0x90 (set by SetSource @004cb818) player = *(source + 0x190) // mech -> owning BTPlayer (:3949 == MECH_OWNING_PLAYER) msg = *(player + 0x1dc) // BTPlayer's current status-message record (:3951) msg == 0 -> messageId = -1, nameEntity = 0 (:3952-3955) else -> nameEntity = *(msg+0x10), messageId = *(msg+0xc) (:3957-3958) nameEntity -> +0x1e0 = palette/name key -> App+0xc8 name-bitmap cache lookup (:3992-3994, 4002-4004) ``` `mech+0x190 = owningPlayer (BTPlayer*)` is confirmed by `btplayer.cpp:68` and the `MECH_OWNING_PLAYER` macro; `TeamStatusDisplay` uses the same `mech+0x190` (`btl4gau3.cpp:167`). **Is the data LIVE now? NO — DEFERRED. Two independent blockers:** 1. **The source is never bound.** `SetSource` (`FUN_004cb818`, the *only* writer of `this[0x24]` besides the ctor's `=0`) has **zero recovered callers** (grep across `reference/decomp/all` returns only its own definition). The Make **ignores** its `entity` param (`FUN_004cb678` never touches `param_4`), so nothing binds a mech at construction either. → `trackedMech` stays `NULL` → the whole Execute body is gated `if (source != 0 …)` (`part_014.c:3949`) → **no-op**. 2. **The message-record feed is a NULL stub.** The record at `BTPlayer+0x1dc` is populated only by `AddStatusMessage(Player__StatusMessage*)`, and that is gated on `StatusMessagePool` — **a NULL bring-up stub** (`btplayer.cpp:135` `extern MemoryBlock *StatusMessagePool;`, guarded at `:508` `if (sender_owner && StatusMessagePool)`). No status messages are ever created, so even a bound mech would read `msg == 0` → messageId `-1` → empty. **⚠ Correction to the task hint:** the source is **NOT** the `SubsystemMessageManager` (0xBD3). That class *is* reconstructed (`game/reconstructed/messmgr.cpp`) but it is the **per-mech DAMAGE/explosion hub** (`ConsolidateAndSendDamage`, `weaponExplosions`, `commonDamageInformation`) — its fields are damage records, not `{msgId, nameEntity}`, and the MessageBoard never references it. The real source is the **`BTPlayer` status-message queue** (`Player__StatusMessage`, `btplayer.cpp:513`) gated by `StatusMessagePool`. **That pool is the blocker to name.** --- ## 3. Class layout (byte-exact) + locks `GraphicGauge` base ends at `0x90` (`Gauge` 0x48 + `GraphicsView localView` 0x48; confirmed by every sibling: `VertTwoPartBar` first field `tileImage@0x90`, `btl4gaug.cpp:1138`). MessageBoard adds `this[0x24..0x28]`: ```cpp class MessageBoard : public GraphicGauge { ... protected: Entity *trackedMech; // @0x90 this[0x24] source mech (SetSource); ctor=0 ⟵ WAS "int enabled" (MISLABEL) char *stripImage; // @0x94 this[0x25] interned "btsmsgs.pcx" (held bitmap ref) int color; // @0x98 this[0x26] param_10 (CFG 4th arg = 0) int previousNameId; // @0x9C this[0x27] BecameActive=-1 ⟵ WAS "previousMessageId" (SWAPPED) int previousMessageId; // @0xA0 this[0x28] BecameActive=-2 ⟵ WAS "previousNameId" (SWAPPED) friend struct MessageBoardLayoutCheck; }; ``` **Three header corrections** vs the current `btl4gau3.hpp:273-279`: - `int enabled` → **`Entity *trackedMech`** (it's a pointer dereferenced `*(src+0x190)` at `:3949`, not a bool; the setter is `SetSource`, not `SetEnable`). - `previousMessageId`/`previousNameId` are **swapped**. Proof: BecameActive writes `+0x9c=-1`, `+0xa0=-2` (`:3916-3917`); Execute's *message-id* compare is against `+0xa0` (`:3960`) and the *name-entity* compare against `+0x9c` (`:3983`). So `+0x9c=previousNameId(-1)`, `+0xa0=previousMessageId(-2)`. (The prose at `btl4gau3.cpp:784` already states "previousNameId=-1; previousMessageId=-2" — the struct labels contradict the prose.) **Locks (add to `btl4gau3.cpp`; protected members ⇒ `friend struct`):** ```cpp struct MessageBoardLayoutCheck { static_assert(sizeof(GraphicGauge) == 0x90, "GraphicGauge base size"); static_assert(offsetof(MessageBoard, trackedMech) == 0x90, "trackedMech"); static_assert(offsetof(MessageBoard, stripImage) == 0x94, "stripImage"); static_assert(offsetof(MessageBoard, color) == 0x98, "color"); static_assert(offsetof(MessageBoard, previousNameId) == 0x9C, "previousNameId"); static_assert(offsetof(MessageBoard, previousMessageId)== 0xA0, "previousMessageId"); static_assert(sizeof(MessageBoard) == 0xA4, "MessageBoard sizeof == Make alloc 0xa4"); }; ``` --- ## 4. Full method bodies to write ### 4.0 CFG call shape → `methodDescription` `content/GAUGE/L4GAUGE.CFG:4912-4917`: ``` offset = (113,607); messageBoard( E, # p[0] rate ModeAlwaysActive, # p[1] mode mask btsmsgs.pcx, # p[2] strip bitmap 0 # p[3] draw/erase color ); ``` `offset` supplies `position` (x=113, y=607) to Make (the ctor's `SetOrigin`). 4 in-paren params: ```cpp MethodDescription MessageBoard::methodDescription = { "messageBoard", MessageBoard::Make, { { ParameterDescription::typeRate, NULL }, // p[0] rate (E) { ParameterDescription::typeModeMask, NULL }, // p[1] mode (ModeAlwaysActive) { ParameterDescription::typeString, NULL }, // p[2] strip bitmap (btsmsgs.pcx) { ParameterDescription::typeColor, NULL }, // p[3] draw/erase color (0) PARAMETER_DESCRIPTION_END } }; ``` ### 4.1 Deferred data bridge (declare near top of `btl4gau3.cpp`) ```cpp // Resolve the viewpoint player's current HUD status message for `tracked_mech`. // Returns True if the mech has a bound owning-player record; fills: // *messageId -- btsmsgs.pcx strip cell index (-1 = no active message) // *nameBitmap -- sender's name bitmap (NULL = none / App name-cache not wired) // DEFERRED: StatusMessagePool (btplayer.cpp) is a NULL stub + BTPlayer+0x1dc is // never populated, so this returns False today -> the board stays empty. Reads // the raw BTPlayer/StatusMessage offsets in a COMPLETE-BTPlayer TU (never here), // dodging the databinding trap. Implement in btplayer.cpp / btl4mppr.cpp. extern Logical BTResolveMessageBoard(Entity *tracked_mech, int *messageId, BitMap **nameBitmap); ``` (Provide a stub `Logical BTResolveMessageBoard(Entity*, int *id, BitMap **bmp){ *id=-1; *bmp=NULL; return False; }` in `btplayer.cpp` until the feed lands — mirrors `LookupPlayerNameBitmap`/`BTResolveRosterPilot`.) ### 4.2 Make — `@004cb678` Alloc `0xa4`, construct, then presence-check the strip bitmap (`FUN_00442aec` Get / `FUN_00442c12` Release) and return whether it exists. `param_5`=renderer, `param_5+0x4c+4`=`bitMapBin`. ```cpp Logical MessageBoard::Make( int display_port_index, Vector2DOf position, Entity * /*entity -- ignored by the binary (FUN_004cb678)*/, GaugeRenderer *gauge_renderer ) { ParameterDescription *p = methodDescription.parameterList; MessageBoard *gauge = (MessageBoard *)operator new(0xa4); // FUN_00402298(0xa4) if (gauge != NULL) { new (gauge) MessageBoard( p[0].data.rate, p[1].data.modeMask, (L4GaugeRenderer *)gauge_renderer, display_port_index, // graphics_port_number position.x, position.y, // x, y (offset=(113,607)) p[2].data.string, // strip image (btsmsgs.pcx) p[3].data.color, // color (0) "MessageBoard"); } // Presence-check the strip bitmap (binary returns this as the Make result). L4Warehouse *wh = (L4Warehouse *)gauge_renderer->warehousePointer; // renderer+0x4c BitMap *strip = wh->bitMapBin.Get(p[2].data.string); // FUN_00442aec if (strip == NULL) return False; wh->bitMapBin.Release(p[2].data.string); // FUN_00442c12 return True; } ``` ### 4.3 ctor — `@004cb704` ```cpp MessageBoard::MessageBoard( GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int graphics_port_number, int x, int y, const char *strip_image, int color_in, const char *identification_string) : GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner 0 folded in) graphics_port_number, identification_string) { // vtable PTR_FUN_0051bddc set by the base->derived chain. stripImage = new char[strlen(strip_image) + 1]; // FUN_004700ac (intern a private copy) strcpy(stripImage, strip_image); L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer; wh->bitMapBin.Get(stripImage); // FUN_00442aec (hold a ref for life) localView.SetOrigin(x, y); // this+0x48 vtbl+0x10 color = color_in; // this[0x26] trackedMech = NULL; // this[0x24] (param_1[0x24]=0, :3864) // previousNameId / previousMessageId set by BecameActive (byte-faithful). } ``` ### 4.4 dtor — `@004cb788` Release the strip ref (keyed on the name, **before** the free), free the name; **do not** write the base-dtor call (compiler emits `FUN_00444870`). ```cpp MessageBoard::~MessageBoard() { L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer; wh->bitMapBin.Release(stripImage); // FUN_00442c12 delete[] stripImage; // FUN_004022e8 stripImage = NULL; } ``` ### 4.5 TestInstance — `@004cb7e4` ```cpp Logical MessageBoard::TestInstance() const { return GraphicGauge::TestInstance(); } // FUN_004448ac ``` (`@004cb7f4` is an empty no-op virtual slot — `ShowInstance`; omit it and inherit, as `PlayerStatus`/`PilotList` do.) ### 4.6 SetSource — `@004cb818` (retained; no recovered caller ⇒ deferred) ```cpp void MessageBoard::SetSource(Entity *mech) { trackedMech = mech; } // this[0x24] = param_2 ``` ### 4.7 BecameActive — `@004cb7fc` ```cpp void MessageBoard::BecameActive() { previousNameId = -1; // this[0x27] @0x9c (0xffffffff, :3916) previousMessageId = -2; // this[0x28] @0xa0 (0xfffffffe, :3917) } ``` ### 4.8 Execute — `@004cb82c` (safe reconstruction; faithful draw path via the bridge) ```cpp void MessageBoard::Execute() { // FAITHFUL LITERAL: the board only draws while a source mech is bound. The // binder (SetSource @004cb818) has no recovered caller AND the per-player // status feed (StatusMessagePool, btplayer.cpp) is a NULL stub, so trackedMech // stays NULL and Execute is a safe no-op == the DEFERRED / empty board. if (trackedMech == NULL) // binary gate :3949 return; // Message record via the safe bridge (NOT raw *(mech+0x190)/+0x1dc offsets: // the databinding trap -- our compiled BTPlayer/StatusMessage layout != 1995). int messageId = -1; BitMap *nameBitmap = NULL; BTResolveMessageBoard(trackedMech, &messageId, &nameBitmap); // --- message strip cell (:3960-3981) ------------------------------------- if (messageId != previousMessageId) { previousMessageId = messageId; localView.MoveToAbsolute(0, 0); // vtbl+0x24 :3962 if (messageId == -1) // no message -> clear { localView.SetColor(color); // vtbl+0x18 :3964 localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c :3966 } else { int cellX = (messageId & 3) << 7; // :3969 int cellY = (messageId >> 2) << 5; // :3970 localView.SetColor(7); // FUN_004c2f88()==7 :3971 L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer; BitMap *strip = wh->bitMapBin.Get(stripImage); // FUN_00442aec :3974 if (strip != NULL) localView.DrawBitMapOpaque(color, 0, strip, // vtbl+0x58 :3976 cellX, cellY, cellX + 0x7f, cellY + 0x1f); wh->bitMapBin.Release(stripImage); // FUN_00442c12 :3980 } } // --- sender name cell (:3983-4009) --------------------------------------- // Binary tracks the name ENTITY pointer + reads (+0x1e0)->App+0xc8 cache; the // safe bridge form returns the resolved bitmap directly (App name cache is not // wired -> NULL -> the "clear" branch, same deferral as PilotList). int nameState = (nameBitmap != NULL) ? 1 : 0; if (nameState != previousNameId) { localView.MoveToAbsolute(0x80, 0); // vtbl+0x24 (name cell x=128) :3984-3985 if (nameBitmap == NULL) { localView.SetColor(color); // vtbl+0x18 :3987 localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c :3989 } else { localView.SetColor(7); // :4000 localView.DrawBitMapOpaque(color, 0, nameBitmap, 0, 0, // vtbl+0x58 :4005 nameBitmap->Data.Size.x - 1, nameBitmap->Data.Size.y - 1); } previousNameId = nameState; // :4009 } } ``` GraphicsView vtable slots confirmed from `engine/MUNGA/GRAPH2D.h`: `SetOrigin@0x10:557`, `SetColor@0x18:568`, `MoveToAbsolute@0x24:581`, `DrawFilledRectangleToAbsolute@0x44:621`, `DrawFilledRectangleToRelative@0x4c:624`, `DrawBitMap@0x54:636`, `DrawBitMapOpaque@0x58:656`, `DrawPixelMap8@0x5c:679`. The binary's name-*erase*-previous path uses `DrawBitMap` (vtbl+0x54, `part_014.c:3995`) — collapsed into the clear branch above since no previous bitmap is tracked in the safe bridge form. ### 4.9 Registration (`btl4grnd.cpp`, before `&BTL4ChainToPrevious` at line 150) ```cpp &PilotList::methodDescription, // "pilotList" &GeneratorCluster::methodDescription, // "GeneratorCluster" &MessageBoard::methodDescription, // "messageBoard" -- sec-MFD comm/status ticker (DEFERRED source) &BTL4ChainToPrevious ``` Add `static MethodDescription methodDescription;` to the `MessageBoard` class in `btl4gau3.hpp`. --- ## 5. Systemic-check verdicts - **Container-Execute rule:** N/A — `MessageBoard` is a **leaf** `GraphicGauge`, not a container (no child gauges, no `Execute=Fail` base risk). It **overrides `Execute`** (`FUN_004cb82c`) and **overrides `BecameActive`** (`FUN_004cb7fc`) with a **non-inactivating** body (sets sentinels only) → satisfies the rule inherently. ✅ - **Shadow fields:** none. Derived fields start at `0x90` (`GraphicGauge` ends there); `renderer`/`localView` are inherited base members, accessed by name — no re-declaration. ✅ - **Alias fields:** none. `trackedMech`/`stripImage`/`color`/`previousNameId`/`previousMessageId` are all genuine new slots (`0x90`–`0xA3`). ✅ - **Phantom fields:** none — last member `previousMessageId@0xA0` (4 bytes) ends at `0xA4 == sizeof == Make alloc`. ✅ - **Field mislabels (fix in header):** `enabled`→`trackedMech` (pointer, not bool; setter is `SetSource` not `SetEnable`); `previousMessageId`/`previousNameId` **swapped**. ✅ (§3) - **Databinding NULL risks:** the raw `*(mech+0x190)`, `*(player+0x1dc)`, `nameEntity+0x1e0`, `App+0xc8` reads are the **databinding trap** — kept **out** of this TU via `BTResolveMessageBoard` (complete-BTPlayer TU). The `bitMapBin.Get` result is NULL-checked (`:3975`) before the strip blit. ✅ - **Every-mech crash risk:** none. With `trackedMech==NULL` (always, until wired) Execute early-returns — zero draws, zero visual change to the sec-MFD composite, byte-identical to the current parse-skipped behavior. Make/ctor allocate + hold one bitmap ref (released in dtor, per the corrected base-dtor-glue rule — no explicit base call). ✅ - **`/FORCE` stub risk:** `BTResolveMessageBoard` **must** have a real (stub) definition, or the `/FORCE` link turns the call into a runtime AV (CLAUDE.md gotcha). Provide the returns-`False` stub in `btplayer.cpp`. ⚠ --- ## 6. Confidence + unknowns **Confidence: HIGH** for Make/ctor/dtor/BecameActive/TestInstance/SetSource/layout/registration — all fully recovered, sizeof/vtable/offsets cross-checked against the `VertTwoPartBar`/`PlayerStatus`/`PilotList` templates and the GraphicsView vtable header. The widget will **build, register (stop parse-skipping), construct, and run as a safe no-op**. **Confidence: HIGH** on the blocker: the board is **DEFERRED/empty** because (1) `SetSource` is never called (no recovered caller) and (2) `StatusMessagePool` is a NULL stub (`btplayer.cpp:135/508`). Both confirmed by grep + source. **Unknowns / follow-ups (do not block the landing):** 1. **Who calls `SetSource`.** No caller in the recovered window — likely in the gauge renderer's viewpoint-bind step (`GAUGREND`/`btl4grnd`, not in `part_014.c`). To bring the board **alive** without it, self-resolve in Execute: `trackedMech = (Entity*)application->GetViewpointEntity();` (`APP.h:373`) — the accepted approach `PlayerStatus`/`PilotList` already use. Left as the safe no-op by default to guarantee zero regression on the dev-gauge composite. 2. **The `Player__StatusMessage` record layout** (`+0xc` messageId, `+0x10` nameEntity) and the App+0xc8 name-bitmap cache — needed inside `BTResolveMessageBoard` once `StatusMessagePool` is wired. These live in `btplayer.cpp`'s domain (`Player__StatusMessage` ctor at `:513`); decode `FUN_0042e580`=`AddStatusMessage` and the message record ctor to expose `messageId`/`nameEntity` as compiled accessors. 3. **`btsmsgs.pcx` cell grid** (4 cols × N rows of 128×32) is derived from the `(id&3)<<7,(id>>2)<<5` math (`:3969-3970`), not from the file — verify against the asset when the feed is live. 4. Cosmetic: the empty-state `color==0` clear boxes are never painted in the default landing (guarded off); if self-resolve is enabled later, confirm palette index 0 on the sec MFD is background (else the message region paints black — faithful, but verify visually).