gauge-complete P4g: MessageBoard reconstructed + registered -> LAST parse-skip cleared (0 unregistered gauges)

The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker,
L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This
was the last unregistered gauge primitive.

Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive
@4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed
(decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at
+0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED
(BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c).
Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription
= 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[].

DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource
has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62)
is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe
no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real
/FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer
TU, dodging the databinding trap). The name-cell path is a marked structural simplification to
restore when the feed lands.

VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive
registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET
DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-07 21:39:47 -05:00
co-authored by Claude Opus 4.8
parent c0d2eb0aae
commit 7fc4acb89f
5 changed files with 202 additions and 24 deletions
+166 -13
View File
@@ -882,19 +882,172 @@ void
// MessageBoard @004cb678 Make / @004cb704 ctor
//###########################################################################
//
// @004cb678 -- Make: allocate (0xa4) and construct "MessageBoard" from
// DAT_0051b43c..0051b508; verify the strip image exists (BitMapCache).
// @004cb704 -- ctor (vtable PTR_FUN_0051bddc). GraphicGauge base; interns +
// AddRefs the message strip bitmap (this[0x25]); sets the port origin; color =
// param_10 (this[0x26]).
// @004cb788 -- dtor: release the strip, free the name, base dtor.
// @004cb7fc -- BecameActive: previousNameId = -1; previousMessageId = -2.
// @004cb82c -- Execute. Track the local player's mech (this[0x24]). Read its
// controller message ids (mech+0x190 controller, +0xc message id, +0x10 name
// id). When the message id changes, blit the corresponding 0x80x0x20 cell of
// the strip ((id&3)<<7, (id>>2)<<5) tinted by FUN_004c2f88 (=7); -1 clears.
// When the name id changes, blit the player's name bitmap (App+0xc8 palette
// cache lookup) similarly.
// Resolve the tracked mech's current HUD status message (msg strip cell + sender
// name bitmap). DEFERRED: StatusMessagePool (btstubs.cpp:62) is a NULL stub +
// BTPlayer+0x1dc is never populated, so this returns False today -> the board stays
// empty. Lives in btplayer.cpp (a complete-BTPlayer TU) to dodge the databinding
// trap; MUST have a real (stub) definition or the /FORCE link AVs the call.
extern Logical BTResolveMessageBoard(Entity *tracked_mech, int *messageId, BitMap **nameBitmap);
// Overflow lock only (like SectorDisplay/Sensor: the 2007 GraphicGauge base is not
// byte-identical to 1995, so exact-offset asserts would fail; every read here is via
// named members). Binary offsets: trackedMech@0x90 stripImage@0x94 color@0x98
// previousNameId@0x9C previousMessageId@0xA0, sizeof 0xA4 == Make alloc.
struct MessageBoardLayoutCheck {
static_assert(sizeof(MessageBoard) <= 0xA4, "MessageBoard must fit its 0xA4 Make alloc");
};
// CFG shape (L4GAUGE.CFG:4913, port=sec, offset=(113,607)):
// messageBoard( E, ModeAlwaysActive, btsmsgs.pcx, 0 )
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
}
};
//
// @004cb678 -- Make. Alloc 0xa4 + construct; presence-check the strip bitmap
// (binary returns whether it exists). The `entity` param is ignored by the binary.
//
Logical
MessageBoard::Make(
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- ignored by 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, // offset=(113,607) -> SetOrigin
p[2].data.string, // strip image (btsmsgs.pcx)
p[3].data.color, // color (0)
"MessageBoard");
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;
}
//
// @004cb704 -- ctor (vtable PTR_FUN_0051bddc). GraphicGauge base (owner 0); copy +
// hold the strip bitmap; SetOrigin; color = param. trackedMech starts NULL.
//
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)
graphics_port_number, identification_string)
{
stripImage = new char[strlen(strip_image) + 1]; // nameCopy (FUN_004700ac)
strcpy(stripImage, strip_image);
L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer;
wh->bitMapBin.Get(stripImage); // FUN_00442aec (hold for life)
localView.SetOrigin(x, y); // this+0x48 vtbl+0x10
color = color_in; // this[0x26]
trackedMech = NULL; // this[0x24] (:3864)
// previousNameId / previousMessageId set by BecameActive (byte-faithful).
}
//
// @004cb788 -- dtor. Release + free the strip; base dtor runs implicitly.
//
MessageBoard::~MessageBoard()
{
L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer;
wh->bitMapBin.Release(stripImage); // FUN_00442c12
delete[] stripImage; stripImage = NULL; // FUN_004022e8
}
// @004cb7e4 -- TestInstance: out-of-line forward (so it isn't /FORCE-stubbed).
Logical MessageBoard::TestInstance() const { return GraphicGauge::TestInstance(); }
// @004cb818 -- SetSource: bind the source mech. (No recovered caller -> deferred.)
void MessageBoard::SetSource(Entity *mech) { trackedMech = mech; }
// @004cb7fc -- BecameActive: reset the change-sentinels. NON-inactivating.
void
MessageBoard::BecameActive()
{
previousNameId = -1; // this[0x27] @0x9c (:3916)
previousMessageId = -2; // this[0x28] @0xa0 (:3917)
}
//
// @004cb82c -- Execute. Draw the message strip cell + sender name when they change.
// FAITHFUL: the board only draws while a source mech is bound; the binder (SetSource)
// has no recovered caller AND the per-player status feed (StatusMessagePool) is a NULL
// stub, so trackedMech stays NULL and Execute is a safe no-op == the DEFERRED empty board.
//
void
MessageBoard::Execute()
{
if (trackedMech == NULL) // binary gate :3949
return;
// Message record via the safe bridge (NOT raw *(mech+0x190)/+0x1dc offsets).
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
if (messageId == -1) {
localView.SetColor(color); // vtbl+0x18
localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c
} else {
int cellX = (messageId & 3) << 7; // :3969
int cellY = (messageId >> 2) << 5; // :3970
localView.SetColor(7); // FUN_004c2f88()==7
L4Warehouse *wh = (L4Warehouse *)renderer->warehousePointer;
BitMap *strip = wh->bitMapBin.Get(stripImage); // FUN_00442aec
if (strip != NULL)
localView.DrawBitMapOpaque(color, 0, strip, // vtbl+0x58
cellX, cellY, cellX + 0x7f, cellY + 0x1f);
wh->bitMapBin.Release(stripImage); // FUN_00442c12
}
}
// --- sender name cell (:3983-4009). STRUCTURAL SIMPLIFICATION (marked): the binary
// tracks the previous name ENTITY pointer at 0x9c and erases it via +0x1e0->App+0xc8;
// here previousNameId is a bool (0/1). Functionally identical while DEFERRED (name
// always NULL); RESTORE the entity-pointer tracking + erase branch when the feed goes live.
int nameState = (nameBitmap != NULL) ? 1 : 0;
if (nameState != previousNameId)
{
localView.MoveToAbsolute(0x80, 0); // vtbl+0x24 (name cell x=128)
if (nameBitmap == NULL) {
localView.SetColor(color); // vtbl+0x18
localView.DrawFilledRectangleToRelative(0x80, 0x20); // vtbl+0x4c
} else {
localView.SetColor(7);
localView.DrawBitMapOpaque(color, 0, nameBitmap, 0, 0, // vtbl+0x58
nameBitmap->Data.Size.x - 1, nameBitmap->Data.Size.y - 1);
}
previousNameId = nameState; // :4009
}
}
// === btl4grnd.cpp begins at @004cbea0 (BTL4GaugeRenderer ctor) -- not this TU.
+20 -11
View File
@@ -260,31 +260,40 @@
//#######################################################################
// MessageBoard -- the rank / message bitmap board. @004cb678 Make /
// @004cb704 ctor / @004cb788 dtor / @004cb7fc BecameActive / @004cb82c
// Execute. vtable PTR_FUN_0051bddc. Tracks the local player's mech
// (App+0x6c -> +0x190 controller +0xc/+0x10 message ids) and blits the
// message strip cell ((id&3)<<7, (id>>2)<<5) plus a player name bitmap.
// MessageBoard (config keyword "messageBoard") -- the secondary-MFD comm/status
// message ticker. @004cb678 Make / @004cb704 ctor / @004cb788 dtor /
// @004cb7fc BecameActive / @004cb82c Execute. vtable PTR_FUN_0051bddc.
// Blits one cell of the strip bitmap btsmsgs.pcx (message id -> cell (id&3)<<7,
// (id>>2)<<5) + the sender's name bitmap. Was PROSE-ONLY (no bodies/
// methodDescription) -> "messageBoard" was parse-skipped.
//
// DEFERRED / EMPTY (authentic for bring-up): the source mech is never bound
// (SetSource @004cb818 has no caller) AND the per-player status queue
// (StatusMessagePool, btstubs.cpp:62) is a NULL stub, so there are no messages
// -> Execute early-returns on the NULL source (a safe no-op == the empty board).
//#######################################################################
class MessageBoard :
public GraphicGauge
{
public:
static MethodDescription methodDescription; // "messageBoard"
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004cb678
MessageBoard( // @004cb704
GaugeRate, ModeMask, L4GaugeRenderer *, int,
GaugeRate, ModeMask, L4GaugeRenderer *, int graphics_port_number,
int x, int y, const char *strip_image, int color,
const char *identification_string);
~MessageBoard(); // @004cb788
Logical TestInstance() const;
void BecameActive(); // @004cb7fc
void SetSource(Entity *); // @004cb818 (binds trackedMech; no caller -> deferred)
void Execute(); // @004cb82c
protected:
int enabled; // @0x90 this[0x24]
char *stripImage; // @0x94 this[0x25]
int color; // @0x98 this[0x26]
int previousMessageId; // @0x9C this[0x27]
int previousNameId; // @0xA0 this[0x28]
Entity *trackedMech; // @0x90 this[0x24] source mech (SetSource; ctor=0) (FIX: was int enabled)
char *stripImage; // @0x94 this[0x25] interned btsmsgs.pcx (held ref)
int color; // @0x98 this[0x26] param (CFG 4th arg = 0)
int previousNameId; // @0x9C this[0x27] BecameActive=-1 (FIX: was previousMessageId)
int previousMessageId; // @0xA0 this[0x28] BecameActive=-2 (FIX: was previousNameId)
friend struct MessageBoardLayoutCheck;
};
+1
View File
@@ -149,6 +149,7 @@ MethodDescription
&GeneratorCluster::methodDescription, // "GeneratorCluster" -- the 4 generator engineering panels (buttons 9-12)
&SectorDisplay::methodDescription, // "sectorDisplay" -- radar SECTOR X/Z read-out (Secondary overlay)
&PrepEngrScreen::methodDescription, // "prepEngr" -- per-Eng-screen static label overlay (12x)
&MessageBoard::methodDescription, // "messageBoard" -- sec-MFD comm/status ticker (DEFERRED source -> empty)
&BTL4ChainToPrevious
};
+15
View File
@@ -1118,6 +1118,21 @@ int BTPilotIsSelected(void *pilot, void *local_player)
return ((void *)((BTPlayer *)pilot) == (void *)target->GetPlayerLink()) ? 1 : 0;
}
// MessageBoard data bridge (consumed by btl4gau3.cpp MessageBoard::Execute). DEFERRED:
// the per-player status queue (StatusMessagePool, btstubs.cpp:62) is a NULL stub and
// BTPlayer+0x1dc is never populated, so there are no messages -> return False (empty
// board). Real impl (when StatusMessagePool is wired): owningPlayer (mech+0x190) ->
// status record (+0x1dc) -> {messageId@+0xc, nameEntity@+0x10 -> name bitmap} via the
// compiled BTPlayer/Player__StatusMessage members (decode AddStatusMessage @0042e580).
// A real (stub) definition is REQUIRED or the /FORCE link AVs MessageBoard::Execute's call.
class BitMap;
Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap **nameBitmap)
{
*messageId = -1;
*nameBitmap = 0;
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed
// the scoreboard. The inflictor is always the viewpoint mech in bring-up, so the
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB