The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/ methodDescription/registration) -> the config line was parse-SKIPPED and never built. Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c, methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute reads the linked mech's world position and shows two 100-unit sector numerics: numericA = Round(-localOrigin.z * 0.01) + 500 numericB = Round( localOrigin.x * 0.01) + 500 (rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong "truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm). Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating), Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked (static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling; the config image name is copied (nameCopy) since Execute reads it per-frame. Registered in BTL4MethodDescription[]. VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1; Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504 -- authentic 100-unit sectors from live mech position). Gauge composite renders full, no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard). Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each unregistered gauge primitive the dev-parse skips -- the tool that pinned this down (earlier "not built" runs were killed before the lazy gauge-renderer init). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
332 lines
21 KiB
Markdown
332 lines
21 KiB
Markdown
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<int> 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). |