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>
21 KiB
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 bitmapbtsmsgs.pcx. Message id → grid cellcol = 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 tocolor(: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:
-
The source is never bound.
SetSource(FUN_004cb818, the only writer ofthis[0x24]besides the ctor's=0) has zero recovered callers (grep acrossreference/decomp/allreturns only its own definition). The Make ignores itsentityparam (FUN_004cb678never touchesparam_4), so nothing binds a mech at construction either. →trackedMechstaysNULL→ the whole Execute body is gatedif (source != 0 …)(part_014.c:3949) → no-op. -
The message-record feed is a NULL stub. The record at
BTPlayer+0x1dcis populated only byAddStatusMessage(Player__StatusMessage*), and that is gated onStatusMessagePool— a NULL bring-up stub (btplayer.cpp:135extern MemoryBlock *StatusMessagePool;, guarded at:508if (sender_owner && StatusMessagePool)). No status messages are ever created, so even a bound mech would readmsg == 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]:
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 isSetSource, notSetEnable).previousMessageId/previousNameIdare 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 atbtl4gau3.cpp:784already states "previousNameId=-1; previousMessageId=-2" — the struct labels contradict the prose.)
Locks (add to btl4gau3.cpp; protected members ⇒ friend struct):
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:
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)
// 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.
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
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).
MessageBoard::~MessageBoard()
{
L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer;
wh->bitMapBin.Release(stripImage); // FUN_00442c12
delete[] stripImage; // FUN_004022e8
stripImage = NULL;
}
4.5 TestInstance — @004cb7e4
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)
void MessageBoard::SetSource(Entity *mech) { trackedMech = mech; } // this[0x24] = param_2
4.7 BecameActive — @004cb7fc
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)
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)
&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 —
MessageBoardis a leafGraphicGauge, not a container (no child gauges, noExecute=Failbase risk). It overridesExecute(FUN_004cb82c) and overridesBecameActive(FUN_004cb7fc) with a non-inactivating body (sets sentinels only) → satisfies the rule inherently. ✅ - Shadow fields: none. Derived fields start at
0x90(GraphicGaugeends there);renderer/localVieware inherited base members, accessed by name — no re-declaration. ✅ - Alias fields: none.
trackedMech/stripImage/color/previousNameId/previousMessageIdare all genuine new slots (0x90–0xA3). ✅ - Phantom fields: none — last member
previousMessageId@0xA0(4 bytes) ends at0xA4 == sizeof == Make alloc. ✅ - Field mislabels (fix in header):
enabled→trackedMech(pointer, not bool; setter isSetSourcenotSetEnable);previousMessageId/previousNameIdswapped. ✅ (§3) - Databinding NULL risks: the raw
*(mech+0x190),*(player+0x1dc),nameEntity+0x1e0,App+0xc8reads are the databinding trap — kept out of this TU viaBTResolveMessageBoard(complete-BTPlayer TU). ThebitMapBin.Getresult 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). ✅ /FORCEstub risk:BTResolveMessageBoardmust have a real (stub) definition, or the/FORCElink turns the call into a runtime AV (CLAUDE.md gotcha). Provide the returns-Falsestub inbtplayer.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):
- Who calls
SetSource. No caller in the recovered window — likely in the gauge renderer's viewpoint-bind step (GAUGREND/btl4grnd, not inpart_014.c). To bring the board alive without it, self-resolve in Execute:trackedMech = (Entity*)application->GetViewpointEntity();(APP.h:373) — the accepted approachPlayerStatus/PilotListalready use. Left as the safe no-op by default to guarantee zero regression on the dev-gauge composite. - The
Player__StatusMessagerecord layout (+0xcmessageId,+0x10nameEntity) and the App+0xc8 name-bitmap cache — needed insideBTResolveMessageBoardonceStatusMessagePoolis wired. These live inbtplayer.cpp's domain (Player__StatusMessagector at:513); decodeFUN_0042e580=AddStatusMessageand the message record ctor to exposemessageId/nameEntityas compiled accessors. btsmsgs.pcxcell grid (4 cols × N rows of 128×32) is derived from the(id&3)<<7,(id>>2)<<5math (:3969-3970), not from the file — verify against the asset when the feed is live.- Cosmetic: the empty-state
color==0clear 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).