Files
BT412/scratchpad/wf_0_MessageBoard_decode.md
T
arcattackandClaude Opus 4.8 4a4ec6855c gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
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>
2026-07-07 21:25:48 -05:00

21 KiB
Raw Blame History

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 StatusMessagePoola 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]:

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 enabledEntity *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):

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 — 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 (0x900xA3).
  • Phantom fields: none — last member previousMessageId@0xA0 (4 bytes) ends at 0xA4 == sizeof == Make alloc.
  • Field mislabels (fix in header): enabledtrackedMech (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).