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>
This commit is contained in:
arcattack
2026-07-07 21:25:48 -05:00
co-authored by Claude Opus 4.8
parent d54e0009d7
commit 4a4ec6855c
20 changed files with 1468 additions and 70 deletions
+3
View File
@@ -1349,6 +1349,9 @@ void
//
if (getenv("BT_DEV_GAUGES") != NULL)
{
if (getenv("BT_GAUGE_SKIP_LOG"))
DEBUG_STREAM << "[gskip] unresolved primitive '"
<< local_name << "' -> skipped\n" << std::flush;
const char
*skip_token;
int
+155 -48
View File
@@ -59,18 +59,8 @@
# include <app.hpp>
#endif
//
// Unrecovered .data resource pools used by the two Make() factories below. The
// concrete bytes (rate/mode/colours/image names) were not recovered from the
// optimised image; declared here as typed placeholders so the construction
// paths compile. (BEST-EFFORT.) DebugStream is provided by the shared recon
// headers.
//
static int DAT_0051a2bc = 0; // SectorDisplay GaugeRate
static int DAT_0051a300 = 0; // SectorDisplay ModeMask
static char DAT_0051a344[] = ""; // SectorDisplay grid image name
// (PlayerStatus's DAT_ placeholder pool removed -- its rewired Make reads
// methodDescription.parameterList[] like the other registered widgets.)
// (SectorDisplay's + PlayerStatus's DAT_ placeholder pools removed -- their rewired
// Make()s read methodDescription.parameterList[] like the other registered widgets.)
//###########################################################################
@@ -92,61 +82,178 @@ static char DAT_0051a344[] = ""; // SectorDisplay grid image name
// SectorDisplay @004c9d44 Make / @004c9e10 ctor
//###########################################################################
// @0x51a2b0 -- registered so the interpreter builds the CFG line
// sectorDisplay( K, ModeAlwaysActive, helv15.pcc, 0, 3 ) (L4GAUGE.CFG:5146; the
// port index + offset=(125,579) arrive via Make's args, not config tokens).
MethodDescription
SectorDisplay::methodDescription =
{
"sectorDisplay",
SectorDisplay::Make,
{
{ ParameterDescription::typeRate, NULL }, // p[0] rate [row0 type=1]
{ ParameterDescription::typeModeMask, NULL }, // p[1] modeMask [row1 type=2]
{ ParameterDescription::typeString, NULL }, // p[2] image [row2 type=9]
{ ParameterDescription::typeColor, NULL }, // p[3] color [row3 type=4]
{ ParameterDescription::typeColor, NULL }, // p[4] okColor [row4 type=4]
PARAMETER_DESCRIPTION_END
}
};
//
// @004c9d44 -- Make. Allocate (0xc4) and construct "SectorDisplay" from the
// resource strings (DAT_0051a2bc..0051a3cc). Then verify the grid image exists
// (BitMapCache); if missing, warn "SectorDisplay: Missing image <name>".
// @004c9d44 -- Make. Alloc 0xc4 + construct; then peek the grid image and warn
// "SectorDisplay: Missing image <name>" if absent (binary-faithful: the base ctor
// self-registers the gauge regardless of the return value).
//
Logical
SectorDisplay::Make(
int /*display_port_index*/,
Vector2DOf<int> /*position*/,
Entity *entity,
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer
)
{
SectorDisplay *gauge = (SectorDisplay *)operator new(0xc4);
ParameterDescription *p = methodDescription.parameterList;
SectorDisplay *gauge = (SectorDisplay *)operator new(0xc4); // FUN_00402298(0xc4)
if (gauge != NULL)
{
//
// Construct with the recovered grid-image name; the remaining geometry /
// colour / font-image arguments were not recovered, so are typed
// placeholders.
//
new (gauge) SectorDisplay(
(GaugeRate)DAT_0051a2bc, // rate
(ModeMask)DAT_0051a300, // mode mask
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
0, // graphics port number
0, 0, // x, y
(char *)&DAT_0051a344, // grid image
(char *)0, // font image
0, // color
display_port_index, // graphics_port_number (FIX: was 0)
position.x, position.y, // SetOrigin (FIX: was 0,0)
p[2].data.string, // grid/font image (helv15.pcc)
p[3].data.color, // numericColor
p[4].data.color, // gridColor / numeric okColor
"SectorDisplay");
}
BitMap *grid = gauge_renderer->warehousePointer->bitMapBin.GetIfAlreadyExists(
(char *)&DAT_0051a344); // FUN_00442aec (peek, no AddRef)
p[2].data.string); // FUN_00442aec (peek, no AddRef)
if (grid == NULL)
{
DebugStream << "SectorDisplay: Missing image "
<< (char *)&DAT_0051a344 << "\n"; // FUN_004dbb24 x3
}
DebugStream << "SectorDisplay: Missing image " << p[2].data.string << "\n";
if (getenv("BT_SECTOR_LOG"))
DEBUG_STREAM << "[sector] Make port=" << display_port_index << " pos=("
<< position.x << "," << position.y << ") image=" << p[2].data.string
<< " gridCached=" << (int)(grid != NULL) << "\n" << std::flush;
return (grid != NULL);
}
//
// @004c9e10 -- ctor (vtable PTR_FUN_0051beec). GraphicGauge base, interns the
// grid image; computes cellWidth = imageWidth/14, gridLeft = cellWidth*12,
// gridRight = cellWidth*12 + cellWidth - 1, gridHeight = imageHeight; sets the
// port origin; builds two NumericDisplays (this[0x2F] at origin, this[0x30] at
// +cellWidth*4). baseLine = dirty = 500.
// @004c9f94 -- dtor: release grid, delete both numerics, GraphicGauge::~GraphicGauge.
// @004ca038 -- BecameActive: dirty=1; reset both numerics.
// @004ca068 -- SetEnable: this[0x24] = enable.
// @004ca07c -- Execute: when enabled, DrawAt the two numerics at the rounded
// cursor position (+offsets), and on the first pass blit the grid image.
// @004c9e10 -- ctor (vtable PTR_FUN_0051beec). GraphicGauge base (owner 0); copy
// the grid-image name; from the cached bitmap compute cellWidth=imageW/14,
// gridLeft=cellWidth*12, gridRight=cellWidth*13-1, gridHeight=imageH; SetOrigin;
// build two 3-digit NumericDisplays (@0 and @cellWidth*4).
//
SectorDisplay::SectorDisplay(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
int graphics_port_number, int x, int y,
const char *image, int color, int ok_color, const char *identification_string)
: GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner 0)
graphics_port_number, identification_string)
{
// nameCopy(image) -- Execute reads gridImage per-frame, so it must outlive the
// (transient) config parameterList string; store a durable copy (FUN_004700ac).
gridImage = NULL;
if (image != NULL) {
gridImage = new char[strlen(image) + 1];
strcpy(gridImage, image);
}
numericColor = color; // @0xA8
gridColor = ok_color; // @0xAC
subject = NULL; // @0x90
sectorBaseB = 500; // @0xB4 (0x1f4)
sectorBaseA = 500; // @0xB0
dirty = 1; // @0xB8 (binary leaves uninit; BecameActive sets it)
L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer; // renderer+0x4c
BitMap *img = wh->bitMapBin.GetIfAlreadyExists(gridImage); // FUN_00442aec (peek)
if (img == NULL) {
cellWidth = gridLeft = gridRight = gridHeight = 0;
} else {
cellWidth = img->Data.Size.x / 14; // img+0xc width
gridLeft = cellWidth * 12;
gridRight = cellWidth * 12 + cellWidth - 1;
gridHeight = img->Data.Size.y; // img+0x10 height
}
localView.SetOrigin(x, y); // this+0x48 vtbl+0x10
// NumericDisplay(warehouse, x, y, image, fieldWidth=3, format=unsignedFormat(0), color, okColor)
numericA = new NumericDisplay(wh, 0, 0, image, 3,
NumericDisplay::unsignedFormat, color, ok_color);
numericB = new NumericDisplay(wh, cellWidth * 4, 0, image, 3,
NumericDisplay::unsignedFormat, color, ok_color);
}
//
// @004c9f94 -- dtor. Free the copied name + both numerics; the base
// GraphicGauge::~GraphicGauge runs implicitly (do NOT call it).
//
SectorDisplay::~SectorDisplay()
{
delete[] gridImage; gridImage = NULL;
delete numericA; numericA = NULL; // FUN_0047018c
delete numericB; numericB = NULL;
}
// @004ca020 -- TestInstance: out-of-line forward to the base (so it isn't /FORCE-stubbed).
Logical
SectorDisplay::TestInstance() const { return GraphicGauge::TestInstance(); }
// @004ca068 -- LinkToEntity (slot 9): cache the renderer's linked entity as the subject.
void
SectorDisplay::LinkToEntity(Entity *entity) { subject = entity; }
// @004ca038 -- BecameActive (slot 3): mark dirty + reset both numerics. NON-inactivating
// (does NOT chain the base) -> satisfies the container-Execute rule.
void
SectorDisplay::BecameActive()
{
dirty = 1;
numericB->ForceUpdate(); // FUN_004703f4 (B then A, binary order)
numericA->ForceUpdate();
}
// @004ca07c -- Execute (slot 16): draw the two sector numerics from the linked mech's
// world X/Z (Round via the codebase +0.5 idiom == FUN_004dcd94), then on the first
// active frame blit the grid-cell background.
void
SectorDisplay::Execute()
{
Entity *s = subject; // this+0x90
// PORT accommodation (marked): the LinkToEntity broadcast may be unwired on
// WinTesla -> fall back to the viewpoint mech (same as btl4rdr ResolveOperatorEntity).
// The binary gates purely on `subject != 0` (@0x4ca08e).
if (s == NULL && application != NULL)
s = (Entity *)application->GetViewpointEntity();
if (s == NULL)
return;
const Point3D &pos = s->localOrigin.linearPosition; // s+0x100 (x@+0x100, z@+0x108)
Scalar za = -pos.z * 0.01f; // @0x4ca194 = 0.01 (100-unit sectors)
Scalar xb = pos.x * 0.01f;
int vA = (int)(za + (za < 0.0f ? -0.5f : 0.5f)) + sectorBaseA; // Round(-Z*0.01)+500
int vB = (int)(xb + (xb < 0.0f ? -0.5f : 0.5f)) + sectorBaseB; // Round( X*0.01)+500
numericA->Draw(&localView, (Scalar)vA); // FUN_00470430
numericB->Draw(&localView, (Scalar)vB);
if (getenv("BT_SECTOR_LOG")) {
static int s_n = 0;
if ((s_n++ % 120) == 0)
DEBUG_STREAM << "[sector] Execute -Z=" << (-pos.z) << " X=" << pos.x
<< " -> sectorA=" << vA << " sectorB=" << vB << "\n" << std::flush;
}
if (dirty) { // this+0xB8
dirty = 0;
BitMap *grid = ((L4Warehouse *)renderer->warehousePointer)
->bitMapBin.GetIfAlreadyExists(gridImage); // FUN_00442aec
if (grid != NULL) {
localView.MoveToAbsolute(cellWidth * 3, 0); // vtbl+0x24
localView.SetColor(gridColor); // vtbl+0x18 (this+0xAC)
localView.DrawBitMap(0, grid, gridLeft, 0, gridRight, gridHeight); // vtbl+0x54
}
}
}
//###########################################################################
+31 -22
View File
@@ -88,41 +88,50 @@
//#######################################################################
// SectorDisplay -- a 14-column bitmap status grid with two NumericDisplays.
// SectorDisplay (config keyword "sectorDisplay") -- the radar SECTOR X/Z
// read-out on the Secondary overlay: a one-shot grid-cell background blit +
// two 3-digit NumericDisplays showing the linked mech's world sector coords
// (Round(-Z*0.01)+500, Round(X*0.01)+500 -> 100-unit sectors).
// @004c9d44 Make / @004c9e10 ctor / @004c9f94 dtor / @004ca038 BecameActive /
// @004ca068 SetEnable / @004ca07c Execute. vtable PTR_FUN_0051beec.
// Make warns "SectorDisplay: Missing image <name>". The numeric pair shows
// a row/column read-out aligned to the grid bitmap (image width / 14).
// @004ca068 LinkToEntity (slot 9) / @004ca07c Execute (slot 16). vtable 0x51beec.
// Was PROSE-ONLY (placeholder Make, no ctor/methodDescription) -> the config
// "sectorDisplay" line was parse-skipped. Reconstructed byte-verified from the
// disassembly (ctor @4c9e10, Execute @4ca07c) + methodDescription PE-parse.
//#######################################################################
class SectorDisplay :
public GraphicGauge
{
public:
static MethodDescription methodDescription; // "sectorDisplay"
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004c9d44
SectorDisplay( // @004c9e10
GaugeRate, ModeMask, L4GaugeRenderer *, int,
int x, int y, const char *grid_image,
const char *font_image, int color,
SectorDisplay( // @004c9e10 (owner folded to 0, like the siblings)
GaugeRate, ModeMask, L4GaugeRenderer *, int graphics_port_number,
int x, int y, const char *image, int color, int ok_color,
const char *identification_string);
~SectorDisplay(); // @004c9f94
Logical TestInstance() const;
void SetEnable(Logical); // @004ca068 this[0x24]
void BecameActive(); // @004ca038
void Execute(); // @004ca07c
void LinkToEntity(Entity *); // @004ca068 slot 9 -- caches the subject mech
void BecameActive(); // @004ca038 slot 3 (non-inactivating)
void Execute(); // @004ca07c slot 16
protected:
Logical enabled; // @0x90 this[0x24]
char *gridImage; // @0x94 this[0x25]
int cellWidth; // @0x98 this[0x26] imageW/14
int gridLeft; // @0x9C this[0x27] cellWidth*12
int gridRight; // @0xA0 this[0x28]
int gridHeight; // @0xA4 this[0x29]
int color; // @0xAC this[0x2B]
int baseLine; // @0xB0 this[0x2C] =500
int dirty; // @0xB4 this[0x2D] =500
Entity *subject; // @0x90 LinkToEntity target (ctor=0)
char *gridImage; // @0x94 interned image name (copied; blit source)
int cellWidth; // @0x98 imageWidth / 14
int gridLeft; // @0x9C cellWidth*12
int gridRight; // @0xA0 cellWidth*13 - 1
int gridHeight; // @0xA4 imageHeight
int numericColor; // @0xA8 param_9 (both numerics' color)
int gridColor; // @0xAC param_10 (blit color + numeric okColor)
int sectorBaseA; // @0xB0 = 500 (added to the -Z numeric)
int sectorBaseB; // @0xB4 = 500 (added to the +X numeric)
int dirty; // @0xB8 redraw flag (BecameActive=1, Execute clears)
NumericDisplay
*numericA, // @0xBC this[0x2F]
*numericB; // @0xC0 this[0x30]
*numericA, // @0xBC -Z sector readout
*numericB; // @0xC0 X sector readout
// sizeof == 0xC4 (== Make alloc); overflow-lock only (2007 GraphicGauge base
// is not byte-identical to 1995, and every read here is via named members).
};
static_assert(sizeof(SectorDisplay) <= 0xC4, "SectorDisplay must fit its 0xC4 Make alloc");
//#######################################################################
+1
View File
@@ -147,6 +147,7 @@ MethodDescription
&VertNormalSlider::methodDescription, // "vertNormalSlider" -- condenser valve-setting slider (heat MFD)
&PilotList::methodDescription, // "pilotList" -- Comm KILLS/DEATHS pilot roster
&GeneratorCluster::methodDescription, // "GeneratorCluster" -- the 4 generator engineering panels (buttons 9-12)
&SectorDisplay::methodDescription, // "sectorDisplay" -- radar SECTOR X/Z read-out (Secondary overlay)
&BTL4ChainToPrevious
};
+23
View File
@@ -0,0 +1,23 @@
lines=open('GAUGE/L4GAUGE.CFG',encoding='latin-1').read().split('\n')
depth=0; start=None
for i,l in enumerate(lines[4900:5160],start=4901):
for ch in l:
if ch=='{': depth+=1
elif ch=='}': depth-=1
if i in (4901,4902,5127,5129,5146,5152) or depth==0 and i>4902 and start is None:
pass
# find Secondary1 block extent
depth=0; opened=False; close=None
for i,l in enumerate(lines,start=1):
if i<4901: continue
for ch in l:
if ch=='{': depth+=1; opened=True
elif ch=='}': depth-=1
if opened and depth==0:
close=i; break
print("Secondary1 block: 4901 ..", close, "| sectorDisplay@5146 inside:", 4901<=5146<=close)
# what ports are in the block
for i in range(4901,close+1):
l=lines[i-1].strip()
if l.startswith('port') or 'sectorDisplay' in l or l.endswith('overlay;'):
print(f" {i}: {l}")
+22
View File
@@ -0,0 +1,22 @@
lines=open('GAUGE/L4GAUGE.CFG',encoding='latin-1').read().split('\n')
# MechInit block extent from 4390
depth=0; opened=False; close=None
for i in range(4390,len(lines)+1):
l=lines[i-1]
for ch in l:
if ch=='{': depth+=1; opened=True
elif ch=='}': depth-=1
if opened and depth==0: close=i; break
print("MechInit block: 4390 ..", close, "| prepEngr@4595 inside:", 4390<=4595<=(close or 0))
# structure between 4407 and 4600
print("--- lines 4408-4430 & around 4590-4600 ---")
for i in list(range(4408,4416))+list(range(4590,4600)):
print(f" {i}: {lines[i-1].strip()[:70]}")
# any nested braces / sub-blocks between 4407 and 4595?
d=0
for i in range(4390,4600):
for ch in lines[i-1]:
if ch=='{': d+=1
elif ch=='}': d-=1
if lines[i-1].strip().endswith('{') or lines[i-1].strip()=='}':
print(f" brace@{i} depth->{d}: {lines[i-1].strip()[:50]}")
+6
View File
@@ -0,0 +1,6 @@
from PIL import Image
im=Image.open('scratchpad/sect_gauge.png')
# radar/secondary tile is bottom-center; sectorDisplay at secondary (125,579)=lower-left
c=im.crop((340,250,660,415)).resize((960,495))
c.save('scratchpad/sect_zoom.png')
print("saved", c.size)
+6
View File
@@ -0,0 +1,6 @@
from PIL import Image
im=Image.open('scratchpad/sect_gauge.png')
# bottom-left of the radar tile (the SECTOR label area)
c=im.crop((345,355,430,410)).resize((680,440))
c.save('scratchpad/sect_zoom2.png')
print("saved", c.size)
+16
View File
@@ -0,0 +1,16 @@
import json,sys,re
p=r'C:\Users\epilectrik\.claude\projects\C--git-bt411\09bf24bb-f92d-423a-b815-1eb662736f58\subagents\workflows\wf_35445603-8db\journal.jsonl'
out=[]
for line in open(p,encoding='utf-8',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('type')=='result':
out.append(str(d.get('result') or d.get('value') or ''))
for i,r in enumerate(out):
# guess widget by keyword frequency
w={'PrepEngr':r.count('PrepEngr'),'SectorDisplay':r.count('SectorDisplay'),'MessageBoard':r.count('MessageBoard')}
widget=max(w,key=w.get)
kind='verify' if ('VERDICT' in r or 'adversarial' in r[:400].lower() or 'CONFIRMED' in r[:400]) else 'decode'
fn=f'scratchpad/wf_{i}_{widget}_{kind}.md'
open(fn,'w',encoding='utf-8').write(r)
print(f'{i}: {widget:16} {kind:7} len={len(r):6} -> {fn}')
+16
View File
@@ -0,0 +1,16 @@
import struct,sys
va=int(sys.argv[1],16)
f=open('content/BTL4OPT.EXE','rb').read()
pe=struct.unpack_from('<I',f,0x3c)[0]
numsec=struct.unpack_from('<H',f,pe+6)[0]; optsz=struct.unpack_from('<H',f,pe+20)[0]
imgbase=struct.unpack_from('<I',f,pe+0x34)[0]; sectab=pe+24+optsz
for i in range(numsec):
o=sectab+i*40; vsz,va2,rsz,ra=struct.unpack_from('<IIII',f,o+8)
if imgbase+va2<=va<imgbase+va2+max(vsz,rsz):
fo=ra+(va-imgbase-va2); raw=f[fo:fo+10]
# 80-bit extended: 64-bit mantissa + 15-bit exp + sign
m=struct.unpack('<Q',raw[0:8])[0]; se=struct.unpack('<H',raw[8:10])[0]
sign=-1 if se&0x8000 else 1; exp=(se&0x7fff)-16383
val=sign*(m/2**63)*2.0**exp
print(f"VA {va:#x} tbyte={raw.hex()} = {val!r}")
break
+13
View File
@@ -0,0 +1,13 @@
import json,sys
sys.stdout.reconfigure(encoding='utf-8',errors='replace')
p=r'C:\Users\epilectrik\.claude\projects\C--git-bt411\09bf24bb-f92d-423a-b815-1eb662736f58\subagents\workflows\wf_35445603-8db\journal.jsonl'
out=[]
for line in open(p,encoding='utf-8',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('type')=='result':
r=d.get('result') or d.get('value') or ''
out.append((d.get('label',''),str(r)))
print("total results:",len(out))
for i,(lbl,r) in enumerate(out):
print(f"##### {i} label={lbl} len={len(r)} first80={r[:80]!r}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+332
View File
@@ -0,0 +1,332 @@
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).
+294
View File
@@ -0,0 +1,294 @@
I now have the complete picture, verified against disassembly, the PE method-description table, the CFG, and the sibling reconstructions. Here is the full implementation spec.
---
# SectorDisplay — Reconstruction Spec
Cockpit **radar SECTOR X/Z read-out** (the Secondary-overlay grid + two 3-digit coordinate numerics). Config keyword `sectorDisplay`, currently **prose-only** in `game/reconstructed/btl4gau3.cpp` (a placeholder `Make`, no ctor/`methodDescription`/registration) → the CFG line is **parse-skipped**. Everything below is decoded from `BTL4OPT.EXE` (Execute/ctor/vtable disassembled; methodDescription PE-parsed) and cross-checked against `part_014.c` and the already-reconstructed siblings.
---
## 1. Identity
| Field | Value | Evidence |
|---|---|---|
| Class | `SectorDisplay` | id string `s_SectorDisplay_0051bb34` `part_014.c:2635`; `"sectorDisplay"` keyword `@0x51bc82` |
| Base | `GraphicGauge` (→`GraphicGaugeBackground``GaugeBase`); base ctor `FUN_00444818`, base vtable `@0x4ee088` | ctor `@0x4c9e38` calls `0x444818`; `btl4gau3.hpp:97` |
| Config keyword | `sectorDisplay` (1 use, `L4GAUGE.CFG:5146`) | methodDescription name ptr `@0x51a2b0``@0x51bc82` |
| **Make** | `@0x4c9d44` (returns `Logical`; alloc `operator new(0xc4)`) | `part_014.c:2625`; disasm |
| ctor | `@0x4c9e10` (sets vtable `0x51beec`, sizeof `0xc4`) | `part_014.c:2654`; disasm |
| dtor | `@0x4c9f94` (slot 0) | `part_014.c:2712` |
| BecameActive | `@0x4ca038` (slot 3, +0x0c) | vtdump `0x51beec` |
| **LinkToEntity(Entity\*)** | `@0x4ca068` (slot 9, +0x24) — *the subject setter* | vtdump; base slot9 `0x444088` = empty `LinkToEntity` (`GAUGE.h:62`) |
| Execute | `@0x4ca07c` (slot 16, +0x40) | vtdump |
| TestInstance | `@0x4ca020``FUN_004448ac``FUN_004443a4` (base test); `@0x4ca030` = empty no-op (`TestClass`/dead) | `part_014.c:2736,2747` |
| **methodDescription** | `@0x51a2b0` → keyword `@0x51bc82`, Make `0x4c9d44`, 5 param rows | PE dump |
| vtable | `PTR_FUN_0051beec` (17 slots; overrides 0/3/9/16) | vtdump |
| sizeof | **0xC4** (== Make alloc `FUN_00402298(0xc4)`, `@0x4c9d54`) | disasm |
| GUID / RTTI | none | vtable has no RTTI slot |
**Overridden vtable slots** (vs base `0x4ee088`): slot0 dtor `0x4c9f94`; slot3 `BecameActive` `0x4ca038`; **slot9 `LinkToEntity` `0x4ca068`** (base `0x444088` is an empty `ret`); slot16 `Execute` `0x4ca07c`. All others inherited.
---
## 2. What it displays & the data binding — **DATA-LIVE**
Two `NumericDisplay`s show the linked mech's **world sector coordinates**, plus a one-shot grid-cell background blit.
The subject is an **`Entity*`** cached at `+0x90` by the overridden **`LinkToEntity(Entity*)`** (slot 9). The engine `GaugeRenderer::LinkToEntity` (`GAUGREND.cpp:3008`, == binary broadcast `FUN_00447b70` `part_006.c:5295` which calls slot9 on all three gauge lists) pushes the viewpoint entity to every gauge each frame.
Execute reads the subject's position (disasm `@0x4ca096``@0x4ca0b9`):
```
numericA value = (int)(-subject->localOrigin.linearPosition.z * 0.01f) + 500 // -Z/100 + 500
numericB value = (int)( subject->localOrigin.linearPosition.x * 0.01f) + 500 // X/100 + 500
```
- Scale const `@0x4ca194` is the **80-bit extended `0.01`** (= ÷100 → 100-unit sectors); decoded exact.
- `-Z` uses `fchs` `@0x4ca09c`; both truncate toward zero (`FUN_004dcd94` sets FPU RC=chop `or 0xc`, `fistp` — it is `(long)`, **not** round-to-nearest).
- `entity+0x100` == `Entity::localOrigin.linearPosition` is **already established** by the reconstructed radar (`btl4rdr.cpp:150,622` `FUN_00408440(this+0x310, owner+0x100)`); `.x@+0x100, .z@+0x108`. `Origin localOrigin` is a base-`Entity` member (`ENTITY.h:140`, `ORIGIN.h:15`), read via the **engine type** (compiled-consistent) — **not** a raw offset, so no databinding-trap garbage.
- Heading is **not** read (the task's "heading" hint is inaccurate — only X/Z position feed the two numerics).
**LIVE now**, with one caveat: the radar notes the renderer's linked-entity socket "may be unwired on WinTesla" (`btl4rdr.cpp:1029`). Mitigation (faithful to intent, same as `ResolveOperatorEntity`/`HeadingPointer::Execute`): when `subject==NULL`, fall back to `application->GetViewpointEntity()`. The grid background blits once (dirty-gated) from cell 12 of the `helv15.pcc` strip; if that image isn't cached the blit is skipped and Make warns `"SectorDisplay: Missing image …"` (`@0x4c9e91` peek).
---
## 3. Class layout (byte-exact) + locks
⚠ The **current `btl4gau3.hpp` layout is wrong** (mislabels `+0x90` as `enabled`, omits `+0xA8` and the `+0xB8` dirty flag, mislabels the two `500` offsets as `baseLine/dirty`). Corrected from ctor/Execute disasm:
```cpp
class SectorDisplay : public GraphicGauge
{
public:
static MethodDescription methodDescription; // register "sectorDisplay"
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @0x4c9d44
SectorDisplay(GaugeRate, ModeMask, L4GaugeRenderer *,
int graphics_port_number, int x, int y,
const char *image, int color, int ok_color,
const char *identification_string); // @0x4c9e10 (10 args; owner folded to 0)
~SectorDisplay(); // @0x4c9f94
Logical TestInstance() const; // @0x4ca020
void LinkToEntity(Entity *entity); // @0x4ca068 (slot 9 override)
void BecameActive(); // @0x4ca038 (slot 3)
void Execute(); // @0x4ca07c (slot 16)
protected:
Entity *subject; // @0x90 this[0x24] LinkToEntity target (linked mech); ctor=0
char *gridImage; // @0x94 this[0x25] interned image name (helv15.pcc)
int cellWidth; // @0x98 this[0x26] imageWidth / 14
int gridLeft; // @0x9C this[0x27] cellWidth*12
int gridRight; // @0xA0 this[0x28] cellWidth*13 - 1
int gridHeight; // @0xA4 this[0x29] imageHeight
int numericColor; // @0xA8 this[0x2A] param_10 (numeric color)
int gridColor; // @0xAC this[0x2B] param_11 (grid blit color + numeric okColor)
int offsetX; // @0xB0 this[0x2C] = 500 (added to numericA)
int offsetY; // @0xB4 this[0x2D] = 500 (added to numericB)
int dirty; // @0xB8 this[0x2E] redraw flag (BecameActive=1, Execute clears)
NumericDisplay *numericA; // @0xBC this[0x2F] -Z sector readout
NumericDisplay *numericB; // @0xC0 this[0x30] X sector readout
// sizeof == 0xC4
};
```
**Locks** — use the **overflow lock only** (matching the sibling widgets: the 2007 `GraphicGauge` base is not byte-identical to the 1995 base, so `subject@0x90`/`sizeof==0xC4` byte-exact asserts would fail; every read here is via named members / the engine `Entity` type, so byte-exactness is not required):
```cpp
// namespace scope (sizeof needs no friend); the class reads NO raw this-offsets, so this
// overflow lock against the Make alloc is the load-bearing guarantee (like Sensor/heat-leaf):
static_assert(sizeof(SectorDisplay) <= 0xC4, "SectorDisplay must fit its 0xC4 Make alloc");
```
(If a future reader wants the offset asserts anyway, they require a `friend struct SectorDisplayLayoutCheck` and will only pass if the engine `GraphicGauge` happens to end at 0x90 — do **not** gate the build on that.)
---
## 4. Full method bodies
```cpp
//======================= btl4gau3.cpp (replace the prose-only SectorDisplay block) =======================
// @0x51a2b0 -- registered so the interpreter builds "sectorDisplay(K,ModeAlwaysActive,helv15.pcc,0,3)".
// CFG shape (L4GAUGE.CFG:5146, port=overlay, offset=(125,579)):
// sectorDisplay( K, ModeAlwaysActive, helv15.pcc, 0, 3 )
// | | | | +-- p[4] color=3 -> gridColor / numeric okColor
// | | | +----- p[3] color=0 -> numericColor
// | | +----------------- p[2] string -> grid/font image
// | +----------------------------------- p[1] modeMask -> ModeAlwaysActive
// +-------------------------------------- p[0] rate -> 'K'
// (position (125,579) and the overlay port index arrive via Make's args, not config tokens.)
MethodDescription
SectorDisplay::methodDescription =
{
"sectorDisplay",
SectorDisplay::Make,
{
{ ParameterDescription::typeRate, NULL }, // p[0] rate [binary row0 type=1 @0x51a2b8]
{ ParameterDescription::typeModeMask, NULL }, // p[1] modeMask [row1 type=2 @0x51a2fc]
{ ParameterDescription::typeString, NULL }, // p[2] image [row2 type=9 @0x51a340]
{ ParameterDescription::typeColor, NULL }, // p[3] color [row3 type=4 @0x51a384]
{ ParameterDescription::typeColor, NULL }, // p[4] okColor [row4 type=4 @0x51a3c8]
PARAMETER_DESCRIPTION_END // [row5 type=0 @0x51a40c]
}
};
// @0x4c9d44 -- Make. Alloc 0xc4 + construct; then verify the grid image exists
// (warn "SectorDisplay: Missing image <name>"). Returns True iff the image is cached
// (binary-faithful: the object self-registers via the base ctor regardless of the return).
Logical
SectorDisplay::Make(
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer)
{
ParameterDescription *p = methodDescription.parameterList;
SectorDisplay *gauge = (SectorDisplay *)operator new(0xc4); // FUN_00402298(0xc4)
if (gauge != NULL)
new (gauge) SectorDisplay(
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
display_port_index, // graphics_port_number (FIX: was 0)
position.x, position.y, // SetOrigin (FIX: was 0,0)
p[2].data.string, // grid/font image (helv15.pcc)
p[3].data.color, // numericColor
p[4].data.color, // gridColor / numeric okColor
"SectorDisplay");
BitMap *grid = ((L4Warehouse *)gauge_renderer->warehousePointer) // renderer+0x4c
->bitMapBin.GetIfAlreadyExists(p[2].data.string); // FUN_00442aec (peek)
if (grid == NULL)
DebugStream << "SectorDisplay: Missing image " << p[2].data.string << "\n"; // FUN_004dbb24 x3
return (grid != NULL);
}
// @0x4c9e10 -- ctor (vtable 0x51beec). GraphicGauge base (owner=0); intern the image;
// cellWidth=imgW/14, gridLeft=cellWidth*12, gridRight=cellWidth*13-1, gridHeight=imgH;
// SetOrigin(x,y); build the two 3-digit NumericDisplays (unsignedFormat).
SectorDisplay::SectorDisplay(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
int graphics_port_number, int x, int y,
const char *image, int color, int ok_color, const char *identification_string)
: GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818, owner=0
graphics_port_number, identification_string)
{
gridImage = InternName(image); // FUN_004700ac (dtor FreeName's it)
numericColor = color; // this[0x2A] (param_10)
gridColor = ok_color; // this[0x2B] (param_11)
subject = NULL; // this[0x24]
offsetY = 500; // this[0x2D] (@0x4c9e6d 0x1f4)
offsetX = 500; // this[0x2C] (@0x4c9e77 0x1f4)
L4Warehouse *wh = (L4Warehouse *)renderer_in->warehousePointer; // renderer+0x4c
BitMap *img = wh->bitMapBin.Get(gridImage); // FUN_00442aec (held; dtor Releases)
if (img == NULL) {
cellWidth = gridLeft = gridRight = gridHeight = 0;
} else {
cellWidth = img->Data.Size.x / 14; // img+0xc = width (@0x4c9ec1)
gridLeft = cellWidth * 12; // @0x4c9ed4 (shl 2, *3)
gridRight = cellWidth * 12 + cellWidth - 1; // @0x4c9ee6
gridHeight = img->Data.Size.y; // img+0x10 = height (@0x4c9ef5)
}
localView.SetOrigin(x, y); // this+0x48 vtbl+0x10 (@0x4c9f0d)
// NumericDisplay(warehouse, x, y, font, fieldWidth=3, format=unsignedFormat(0), color, okColor)
numericA = new NumericDisplay(wh, 0, 0, image, 3,
NumericDisplay::unsignedFormat, color, ok_color); // this[0x2F]
numericB = new NumericDisplay(wh, cellWidth * 4, 0, image, 3, // @0x4c9f6f shl 2
NumericDisplay::unsignedFormat, color, ok_color); // this[0x30]
dirty = 1; // binary leaves this[0x2E] uninit (BecameActive sets it before Execute); init for safety
}
// @0x4c9f94 -- dtor. Release grid, free name, delete both numerics. The base
// GraphicGauge::~GraphicGauge (FUN_00444870) runs IMPLICITLY -- do NOT call it.
SectorDisplay::~SectorDisplay()
{
((L4Warehouse *)renderer->warehousePointer)->bitMapBin.Release(gridImage); // FUN_00442c12
FreeName(gridImage); gridImage = NULL; // FUN_004022e8
delete numericA; numericA = NULL; // FUN_0047018c
delete numericB; numericB = NULL;
}
// @0x4ca020 -- TestInstance: out-of-line forward to the base (so it isn't /FORCE-stubbed).
Logical
SectorDisplay::TestInstance() const { return GraphicGauge::TestInstance(); }
// @0x4ca068 -- LinkToEntity (slot 9): cache the renderer's linked entity as the subject.
void
SectorDisplay::LinkToEntity(Entity *entity) { subject = entity; }
// @0x4ca038 -- BecameActive (slot 3): mark dirty + reset both numerics. NON-inactivating
// (does NOT call the base) -> satisfies the container-Execute rule.
void
SectorDisplay::BecameActive()
{
dirty = 1; // this[0x2E]
numericB->ForceUpdate(); // FUN_004703f4 (order B then A, @0x4ca03e/0x4ca04f)
numericA->ForceUpdate();
}
// @0x4ca07c -- Execute (slot 16): draw the two sector numerics from the linked mech's
// world X/Z; on the first active frame blit the grid-cell background.
void
SectorDisplay::Execute()
{
Entity *s = subject;
// PORT accommodation (marked): the LinkToEntity broadcast may be unwired on WinTesla ->
// fall back to the viewpoint mech (same pattern as btl4rdr ResolveOperatorEntity).
// The binary gates purely on `subject != 0` (@0x4ca08e).
if (s == NULL && application != NULL)
s = (Entity *)application->GetViewpointEntity();
if (s == NULL)
return;
const Point3D &pos = s->localOrigin.linearPosition; // s+0x100 (x@+0x100, z@+0x108)
int vA = (int)(-pos.z * 0.01f) + offsetX; // @0x4ca096 ftol(chop)
int vB = (int)( pos.x * 0.01f) + offsetY; // @0x4ca0ad
numericA->Draw(&localView, (Scalar)vA); // FUN_00470430
numericB->Draw(&localView, (Scalar)vB);
if (dirty) { // @0x4ca10f
dirty = 0;
BitMap *grid = ((L4Warehouse *)renderer->warehousePointer)
->bitMapBin.GetIfAlreadyExists(gridImage); // FUN_00442aec
if (grid != NULL) {
localView.MoveToAbsolute(cellWidth * 3, 0); // vtbl+0x24 @0x4ca152
localView.SetColor(gridColor); // vtbl+0x18 @0x4ca163
localView.DrawBitMap(0 /*rotation*/, grid, // vtbl+0x54 @0x4ca187
gridLeft, 0, gridRight, gridHeight); // source rect
}
}
}
```
**Registration**`game/reconstructed/btl4grnd.cpp`, in `BTL4MethodDescription[]` immediately before `&BTL4ChainToPrevious` (`btl4grnd.cpp:149`):
```cpp
&SectorDisplay::methodDescription, // "sectorDisplay" -- radar SECTOR X/Z read-out (Secondary overlay)
```
`SectorDisplay` is already reachable there (`btl4grnd.cpp:71` includes `btl4gau3.hpp`).
---
## 5. Systemic-check verdicts
- **Container-Execute override:** ✅ satisfied. `Execute` (slot 16) and `BecameActive` (slot 3, non-inactivating) are both overridden, so the base `Gauge::Execute` `Fail``abort()` is never reached. The two `NumericDisplay`s are **private sub-objects drawn directly** (not registered gauges), so there's no child-`Execute`/`GuardedExecute` recursion.
- **Shadow fields:** none vs the engine base — `subject@0x90` is the first subclass field (base `GraphicGauge` ends before 0x90). The failures found were **internal layout bugs in the current header** (`enabled``subject`, missing `+0xA8`/`+0xB8`, `baseLine/dirty``offsetX/offsetY`), fixed in §3.
- **Alias / phantom:** none. `numericColor@0xA8` is a real stored field (consumed only by the child ctors, never re-read — faithful, not phantom); `offsetX/offsetY` are two independent `500`s; all members lie within `sizeof 0xC4`.
- **Databinding NULL risks:** all guarded — `subject==NULL` early-returns (+viewpoint fallback); missing grid image → `GetIfAlreadyExists` NULL → blit skipped + Make warns; `renderer->warehousePointer` always set by the base. Add a `numericA/numericB != NULL` guard before `Draw` if you keep the binary's `new`-can-fail branch (`@0x4c9f20`).
- **Every-mech crash risk:** ✅ none. This is a **single cockpit gauge** built once for the local pod; it reads the **linked/viewpoint** entity's `localOrigin.linearPosition` through the **engine `Entity` type** (compiled-consistent), never a raw `*(T*)(entity+0xNN)`. It does not iterate mechs and is not per-mech.
---
## 6. Confidence & unknowns
**Confidence: HIGH.** Full function set disassembled (Execute/ctor/dtor/BecameActive/LinkToEntity/vtable), `methodDescription` PE-parsed exact (keyword + 5 typed rows), the `0.01`/`÷100` const decoded, the CFG shape confirmed, and the position binding (`localOrigin.linearPosition`) is the exact mapping the reconstructed radar already uses.
**Unknowns / follow-ups (none blocking):**
1. Whether `GaugeRenderer::LinkToEntity` is broadcast with a real viewpoint in the port. The `application->GetViewpointEntity()` fallback makes the gauge LIVE either way; if you confirm the broadcast fires, the fallback is inert.
2. `InternName`/`FreeName` (`FUN_004700ac`/`FUN_004022e8`) exact engine symbols — if `NumericDisplay`'s ctor copies the font name internally (siblings pass `p[2].data.string` uncopied), you may store `p[2].data.string` directly and drop the intern/free pair. Cosmetic.
3. `bitMapBin.Get`(ctor, held) vs `GetIfAlreadyExists`(peek) ref-counting — both compile; the ctor `Get` is balanced by the dtor `Release` (binary-faithful). If `Get`/`Release` aren't symmetric on the WinTesla `L4Warehouse`, use `GetIfAlreadyExists` in the ctor and drop the dtor `Release`.
4. `FUN_004ca030` (empty `ret`) is an unreferenced `TestClass`/dead stub — not needed for function.
5. Visual semantics of the blitted cell-12 sub-rect of `helv15.pcc` (`gridLeft=cellWidth*12`) are replicated exactly but not eyeballed; verify against pod footage when the Secondary overlay is composited (`BT_DEV_GAUGES`).
+390
View File
@@ -0,0 +1,390 @@
I now have complete, definitive coverage. The `vtables.tsv` row `0051a040` shows PrepEngr's slots `…4c7d14 4173b8 417938 4c7e48…` — byte-identical in shape to BackgroundBitmap's `…471d7c 4173b8 417938 471dec…` (dtor, 2 inherited, **BecameActive**). This confirms `FUN_004c7e48` is the **BecameActive** slot and PrepEngrScreen is a `GraphicGaugeBackground`. Everything is traced. Here is the full spec.
---
# PrepEngrScreen — decode & reconstruction spec (config keyword `prepEngr`)
> **Bottom line up front:** the existing `btl4gau2.hpp` stub for `PrepEngrScreen` is **wrong in three ways** and there is **no `methodDescription`/`Make`**, so `prepEngr(...)` is parse-skipped (12 CFG calls never build). It is NOT an `Execute`-driven `Gauge` with a child chain — it is a **`GraphicGaugeBackground` painted on activation** (identical base/vtable shape to the engine's `BackgroundBitmap`). Corrections: (1) base = `GraphicGaugeBackground`, not `Gauge`; (2) the overridden slot is `BecameActive()`, not `Execute()`; (3) `screenNumber@0x6C` and `mech@0x70` are **swapped** in the stub.
---
## 1. Identity
| Field | Value | Citation |
|---|---|---|
| Class name | `PrepEngrScreen` | id-string `s_PrepEngr_00519a9d` = `"PrepEngr"`, part_014.c:1193 |
| Config keyword | `prepEngr` | L4GAUGE.CFG:4595…4759 (12×); it is the methodDescription's first string (distinct from the `"PrepEngr"` id-string) |
| Base class | **`GraphicGaugeBackground`** (`GaugeBase`→…→localView, **no** rate/Execute) | ctor calls base `FUN_004440d4` (part_014.c:1213); `FUN_004440d4` = GraphicGaugeBackground ctor `(mode,renderer,ownerID,port,idstr)` → localView(GraphicsViewRecord) at **+0x24 / index 9** (part_006.c @004440d4), identical to `BackgroundBitmap` (part_009.c @00471d00) |
| ctor | `@004c7bf0` (`FUN_004c7bf0`) | part_014.c:1204 |
| dtor | `@004c7d14` (`FUN_004c7d14`) | part_014.c:1246 |
| **BecameActive** (the paint) | `@004c7e48` (`FUN_004c7e48`) | part_014.c:1288 |
| Make (config factory) | `@004c7b30` (`FUN_004c7b30`) | part_014.c:1172 |
| methodDescription | in `.rdata` (**not exported** by the fn-only decompiler) — reconstruct from the CFG call shape | — |
| vtable | `PTR_FUN_0051a06c` | part_014.c:1214,1252 |
| sizeof | **0x90** (= `FUN_00402298(0x90)` in Make) | part_014.c:1189 |
| GUID / classID | none (GraphicGaugeBackground carries no distinct classID; it's not roster-registered) | — |
**Vtable-slot proof** (`reference/decomp/vtables.tsv`, row `0051a040`): `… 4c7d14 4173b8 417938 4c7e48 …` — the exact same shape as BackgroundBitmap (row `004fb918`): `… 471d7c 4173b8 417938 471dec …`. i.e. `[dtor][inherited][inherited][BecameActive]`. Only **dtor + BecameActive** are overridden; everything else is inherited (`444xxx` base slots). `TestInstance` is non-virtual (GAUGE.h:46) → not a vtable override.
---
## 2. What it displays & data binding
**`prepEngr` = "PREPARE the engineering screen":** a **static per-screen label overlay** for one of the 12 MFD engineering screens (`ModeMFD{1,2,3}Eng{1..4}` → screen numbers 1..12; CFG:4595-4769). It is **not** the live bars/dials — those come from `vehicleSubSystems`/`SubsystemCluster`. `PrepEngrScreen` paints, **once each time its screen becomes active**, the labels appropriate to the **one subsystem assigned to that screen**.
`BecameActive` (`@004c7e48`) walks the mech's subsystem roster, finds the **first** subsystem whose `auxScreenNumber == this->screenNumber`, and paints:
| Element | Source | Where drawn | Live now? |
|---|---|---|---|
| screen-number numeric (2 digits) | `this->screenNumber` (own member) | (0xE9,0x176) | **LIVE** (own field) |
| linked heat-sink numeric (1 digit) | `ResolveLink(AttributePointerOf(sub,"HeatSink")) + 0x1D4` | (0x15E,5) | **DEFERRED** — raw offset on the resolved sink (databinding risk; guard NULL) |
| subsystem label bitmap | `sub + 0x224` (embedded char[] = a `.pcc` name) | (0x125,0x172) | **DEFERRED** — raw subsystem offset, not a byte-exact field; use the `BTGetSubsystemAuxScreen` label bridge instead |
| type-specific label cells | `this->statusImage[1..6]` dispatched by `sub->GetClassID()` | fixed cells via `DrawStatusCells` | **LIVE** (own members + classID accessor) |
**ClassID → label dispatch** (`switch(sub+4)`, part_014.c:1330-1357; images are `this[0x1D..0x23]`, byte 0x74..0x8C):
| ClassID | Subsystem | `DrawStatusCells(topBox, A, B, C)` | +extra |
|---|---|---|---|
| 0xBC3 | Sensor | `(1, , , )` | draw `statusImage[6]`=`btesrnge.pcc` @(0xB8,0x8C) — part_014.c:1332-1334 |
| 0xBC6 | Myomers | `(0, , img5=espeed, img3=bteslev)` | part_014.c:1339 |
| 0xBC8 / 0xBD4 | Emitter / PPC | `(0, img1=escnd, img2=edmg, img3=bteslev)` | part_014.c:1344 |
| 0xBCD / 0xBD0 | ProjectileWeapon / MissileLauncher | `(1, , , img4=bteunjm)` | part_014.c:1349 |
| 0xBCE | GaussRifle | warn *"…Gauss rifle not yet supported"* + `(1, , , img3=bteslev)` | part_014.c:1353-1354 |
| default | — | nothing (return) | part_014.c:1336 |
**Databinding-safe roster access** (the trap: reconstructed `Mech` is 0x638 vs binary 0x854, so raw `mech+0x124/+0x128` read garbage). Use the same accessors the sibling `VehicleSubSystems::Make` uses (btl4gau2.cpp:1621-1636): `entity->GetSubsystemCount()`, `entity->GetSubsystem(i)`, and `BTGetSubsystemAuxScreen(sub,&auxScreen,&placement,label64)` (declared btl4gau2.cpp:151) — the bridge both applies the `FUN_0041a1a4(**(sub+0xc),0x50f4bc)` PoweredSubsystem type filter (part_014.c:1308) **and** returns `auxScreenNumber` (`sub+0x1DC`, part_014.c:1309), replacing the two raw reads.
**Content** (from all 12 CFG calls, identical param set, CFG:4595-4605): font `helv42.pcc`; labels `escnd.pcc`(sensor) `edmg.pcc`(damage) `bteslev.pcc`(seek/energy level) `bteunjm.pcc`(unjam) `espeed.pcc`(speed) `btesrnge.pcc`(range).
---
## 3. Class layout (byte-exact) + locks
Binary layout (from ctor `FUN_004c7bf0` int-index writes = byte N×4, and the `FUN_00402298(0x90)` alloc):
```
GraphicGaugeBackground base .......... 0x00 .. 0x23 (GaugeBase fields incl. renderer@0x1C)
localView (GraphicsView) ........... 0x24 .. 0x6B (GraphicsViewRecord, init'd by FUN_0044ad78)
screenNumber int ................. 0x6C this[0x1B] (param_7; part_014.c:1216) ← NOT mech
mech Entity* ............. 0x70 this[0x1C] (param_6; part_014.c:1215) ← NOT screenNumber
statusImage[0] char* (font helv42) . 0x74 this[0x1D] (part_014.c:1218)
statusImage[1] char* (escnd) ....... 0x78 this[0x1E] (:1220)
statusImage[2] char* (edmg) ........ 0x7C this[0x1F] (:1222)
statusImage[3] char* (bteslev) ..... 0x80 this[0x20] (:1224)
statusImage[4] char* (bteunjm) ..... 0x84 this[0x21] (:1226)
statusImage[5] char* (espeed) ...... 0x88 this[0x22] (:1228)
statusImage[6] char* (btesrnge) .... 0x8C this[0x23] (:1230)
sizeof = 0x90
```
> ⚠ The current `btl4gau2.hpp:318-320` stub has `mech@0x6C` / `screenNumber@0x70` **swapped** and base `Gauge`. Both must change.
Because the reconstruction's `GraphicGaugeBackground`/`GraphicsView` are **not** byte-exact to the 1995 layout, do **not** emit `offsetof` locks against the binary offsets for the own fields (they'd fail like `Sensor`/heat-leaf). The only load-bearing size fact is the Make alloc; assert only that the compiled object **fits** the binary alloc:
```cpp
static_assert(sizeof(PrepEngrScreen) <= 0x90, "PrepEngrScreen exceeds binary alloc 0x90");
```
(If you `new PrepEngrScreen` normally rather than `operator new(0x90)`, even this is optional — no external code reads PrepEngrScreen at raw offsets.)
---
## 4. Full bodies to write
### 4a. Header (replace the stub at `btl4gau2.hpp:304-322`)
```cpp
//#######################################################################
// PrepEngrScreen -- "prepEngr": the per-engineering-screen STATIC label
// overlay. @004c7b30 Make / @004c7bf0 ctor / @004c7d14 dtor /
// @004c7e48 BecameActive. vtable PTR_FUN_0051a06c.
//
// A GraphicGaugeBackground (paint-on-activation, NOT an Execute gauge --
// same base/shape as the engine's BackgroundBitmap, FUN_00471d00). Interns
// 7 status .pcc's (font + 6 labels) and, when its MFD-Eng screen activates,
// paints the labels for the ONE subsystem whose auxScreenNumber == this
// screen (1..12), dispatched by the subsystem's ClassID.
//#######################################################################
class PrepEngrScreen :
public GraphicGaugeBackground
{
public:
static MethodDescription methodDescription;
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004c7b30
PrepEngrScreen( // @004c7bf0
ModeMask, L4GaugeRenderer *, unsigned int owner_ID,
int graphics_port_number, Entity *mech, int screen_number,
const char *img0/*font*/, const char *img1, const char *img2,
const char *img3, const char *img4, const char *img5,
const char *img6, const char *identification_string = "PrepEngr");
~PrepEngrScreen(); // @004c7d14
void BecameActive(); // @004c7e48 (the paint slot -- NOT Execute)
protected:
int screenNumber; // @0x6C this[0x1B]
Entity *mech; // @0x70 this[0x1C]
char *statusImage[7]; // @0x74 this[0x1D..0x23] (interned .pcc names)
};
```
### 4b. Implementation (add to `btl4gau2.cpp`, replacing the prose block at 819-851)
```cpp
//###########################################################################
// PrepEngrScreen -- "prepEngr" @004c7b30 Make / @004c7bf0 ctor /
// @004c7d14 dtor / @004c7e48 BecameActive. A GraphicGaugeBackground.
//###########################################################################
// ---- FUN_004c79c8 : draw the 3 status-label cells (+ optional top erase box).
// Each cell either blits its label .pcc or blanks the cell. Coords are the
// binary's exact absolute cell rectangles (part_014.c:1127-1166).
static void
DrawStatusCells(GraphicsView *view, L4Warehouse *warehouse,
int topBox, const char *A, const char *B, const char *C)
{
if (topBox) // part_014.c:1131-1135
{
view->SetColor(0);
view->MoveToAbsolute(0x97, 0x80);
view->DrawFilledRectangleToAbsolute(0x17d, 0x13b);
}
if (A == NULL) { view->SetColor(0); view->MoveToAbsolute(0x15a,0x145); // :1136-1140
view->DrawFilledRectangleToAbsolute(0x17f,0x159); }
else { view->SetColor(0xff); DrawPortBackground(view,warehouse,0x15a,0x145,A); } // FUN_004c2ec4
if (B == NULL) { view->SetColor(0); view->MoveToAbsolute(0x85,0x58); // :1145-1149
view->DrawFilledRectangleToAbsolute(0x17f,0x7c); }
else { view->SetColor(0xff); DrawPortBackground(view,warehouse,0x85,0x58,B); }
if (C == NULL) { view->SetColor(0); view->MoveToAbsolute(0x2a,0x34); // :1154-1160
// NB: raw uses GraphicsView vtbl+0x4c(99,10) then MoveToAbsolute(10,0)
// then DrawFilledRectangleToAbsolute(0x9f,0x2d) -- a compound blank.
// vtbl+0x4c is an unresolved GraphicsView slot; a plain fill is close:
view->DrawFilledRectangleToAbsolute(0x9f,0x2d); }
else { view->SetColor(0xff); DrawPortBackground(view,warehouse,0x2a,0x34,C); }
}
// CFG shape (L4GAUGE.CFG:4595): mode, int screen(1..12), then 7 .pcc names
// (font + 6 labels). 9 params.
MethodDescription
PrepEngrScreen::methodDescription =
{
"prepEngr",
PrepEngrScreen::Make,
{
{ ParameterDescription::typeModeMask, NULL }, // p[0] mode (ModeMFD*Eng*)
{ ParameterDescription::typeInteger, NULL }, // p[1] screen number 1..12
{ ParameterDescription::typeString, NULL }, // p[2] font helv42.pcc -> statusImage[0]
{ ParameterDescription::typeString, NULL }, // p[3] escnd.pcc -> statusImage[1]
{ ParameterDescription::typeString, NULL }, // p[4] edmg.pcc -> statusImage[2]
{ ParameterDescription::typeString, NULL }, // p[5] bteslev.pcc -> statusImage[3]
{ ParameterDescription::typeString, NULL }, // p[6] bteunjm.pcc -> statusImage[4]
{ ParameterDescription::typeString, NULL }, // p[7] espeed.pcc -> statusImage[5]
{ ParameterDescription::typeString, NULL }, // p[8] btesrnge.pcc -> statusImage[6]
PARAMETER_DESCRIPTION_END
}
};
//
// @004c7b30 -- Make. Validate the screen number (p[1], must be 1..12) then
// construct. The GraphicGaugeBackground base ctor self-registers with the
// renderer (GAUGE.cpp GaugeBase::GaugeBase -> renderer->Add(this)), so the
// gauge's BecameActive fires whenever its Eng screen is selected.
//
Logical
PrepEngrScreen::Make(
int display_port_index,
Vector2DOf<int> /*position -- ignored; PrepEngr draws at absolute coords*/,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
int screen = p[1].data.integer;
if (screen < 1 || screen > 12) // part_014.c:1182
{
DebugStream << "PrepEngr: screen number out of range " << screen << "\n"; // :1183-1185
return False; // raw returns 0
}
// operator new(0x90) + placement-new to mirror FUN_00402298(0x90) (:1189).
PrepEngrScreen *g = (PrepEngrScreen *)operator new(0x90);
if (g != NULL)
new (g) PrepEngrScreen(
p[0].data.modeMask, // mode (ModeMFD*Eng*)
(L4GaugeRenderer *)gauge_renderer,
0, // owner_ID (raw hard-wires 0)
display_port_index, // graphics_port_number
entity, // mech (Make param_4)
screen, // screen number
p[2].data.string, p[3].data.string, p[4].data.string,
p[5].data.string, p[6].data.string, p[7].data.string,
p[8].data.string,
"PrepEngr");
return True; // raw returns 1
}
//
// @004c7bf0 -- ctor. GraphicGaugeBackground base (mode/renderer/0/port/"PrepEngr");
// store screen + mech; nameCopy the 7 images and pre-load (AddRef) each from the
// bitmap cache. (No SetOrigin -- localView keeps its default origin.)
//
PrepEngrScreen::PrepEngrScreen(
ModeMask mode_mask, L4GaugeRenderer *renderer_in, unsigned int owner_ID,
int graphics_port_number, Entity *mech_in, int screen_number,
const char *img0, const char *img1, const char *img2, const char *img3,
const char *img4, const char *img5, const char *img6,
const char *identification_string
):
GraphicGaugeBackground(mode_mask, renderer_in, owner_ID, // FUN_004440d4
graphics_port_number, identification_string)
{
screenNumber = screen_number; // @0x6C (param_7, :1216)
mech = mech_in; // @0x70 (param_6, :1215)
const char *src[7] = { img0,img1,img2,img3,img4,img5,img6 };
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; // renderer+0x4c
for (int i = 0; i < 7; i++)
{
statusImage[i] = nameCopy(src[i]); // FUN_004700ac (:1217-1230)
warehouse->bitMapBin.Get(statusImage[i]); // FUN_00442aec pre-load/AddRef (:1232-1238)
}
}
//
// @004c7d14 -- dtor. Release the 7 pre-loaded images + free their name copies.
// (The localView + base dtor run implicitly via the C++ epilogue -- do NOT emit
// explicit FUN_0044ae0c/FUN_00443f7c calls; they are compiler glue, per the
// reconstruction destructor rule.)
//
PrepEngrScreen::~PrepEngrScreen()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
for (int i = 0; i < 7; i++)
{
warehouse->bitMapBin.Release(statusImage[i]); // FUN_00442c12 (:1254-1260)
delete[] statusImage[i]; // FUN_004022e8 (:1261-1274)
statusImage[i] = NULL;
}
}
//
// @004c7e48 -- BecameActive. Find the subsystem on this screen and paint its
// labels. (Roster/aux-screen read via the databinding-safe accessors + the
// PoweredSubsystem bridge, exactly like VehicleSubSystems::Make.)
//
void
PrepEngrScreen::BecameActive()
{
if (mech == NULL) return;
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer; // this[0x1C]->+0x4c
const char *font = statusImage[0]; // helv42.pcc
int count = mech->GetSubsystemCount(); // mech+0x124
for (int i = 1; i < count; i++) // skip slot 0 (the controls mapper)
{
Subsystem *sub = mech->GetSubsystem(i); // mech+0x128[i]
if (sub == NULL) continue;
int auxScreen = 0, placement = -1; char label64[64];
if (!BTGetSubsystemAuxScreen(sub, &auxScreen, &placement, label64)) // FUN_0041a1a4 filter + sub+0x1dc
continue; // not a PoweredSubsystem-derived weapon
if (auxScreen != screenNumber) // part_014.c:1309
continue;
// ---- numeric #1 : the screen number, 2 digits @(0xE9,0x176) (:1310-1313)
{
NumericDisplay num(warehouse, 0xe9, 0x176, font, 2,
NumericDisplay::unsignedFormat, 0, 0xff); // FUN_004700bc
num.Draw(&localView, (Scalar)screenNumber); // FUN_00470430
} // ~NumericDisplay = FUN_0047018c
// ---- numeric #2 : linked heat-sink number, 1 digit @(0x15E,5) (:1315-1321)
int hsNumber = 0;
void *hsLink = AttributePointerOf(sub, "HeatSink"); // FUN_0041bfc0
Subsystem *hs = (Subsystem *)ResolveLink(hsLink); // FUN_00417ab4
if (hs != NULL)
hsNumber = *(int *)((char *)hs + 0x1d4); // BEST-EFFORT raw (see §6)
{
NumericDisplay num(warehouse, 0x15e, 5, font, 1,
NumericDisplay::unsignedFormat, 0, 0xff);
num.Draw(&localView, (Scalar)hsNumber);
}
// ---- subsystem label bitmap @(0x125,0x172) (:1322-1329)
// Raw draws the .pcc name at sub+0x224; use the aux-screen label bridge
// (label64) for a databinding-safe equivalent. (BEST-EFFORT; see §6.)
localView.SetColor(0xff);
if (label64[0] != '\0')
DrawPortBackground(&localView, warehouse, 0x125, 0x172, label64);
// ---- type-specific label cells (switch on ClassID, :1330-1357)
switch (sub->GetClassID()) // sub+4
{
case 0xBC3: // Sensor
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, NULL);
localView.SetColor(0xff);
DrawPortBackground(&localView, warehouse, 0xb8, 0x8c, statusImage[6]); // btesrnge
return;
case 0xBC6: // Myomers
DrawStatusCells(&localView, warehouse, 0, NULL, statusImage[5], statusImage[3]);
return;
case 0xBC8: // Emitter
case 0xBD4: // PPC
DrawStatusCells(&localView, warehouse, 0, statusImage[1], statusImage[2], statusImage[3]);
return;
case 0xBCD: // ProjectileWeapon
case 0xBD0: // MissileLauncher
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[4]);
return;
case 0xBCE: // GaussRifle -- not yet supported
DebugStream << "PrepEngr: Gauss rifle not yet supported\n"; // :1353
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[3]);
return;
default:
return;
}
}
}
```
> `DrawPortBackground(view, warehouse, x, y, tile)` (btl4gau2.cpp:923) **is** `FUN_004c2ec4`; `nameCopy`, `AttributePointerOf`, `ResolveLink`, `BTGetSubsystemAuxScreen`, `NumericDisplay`, `L4Warehouse` are already available in this TU. `DrawPortBackground` currently takes `(GraphicsView*, L4Warehouse*, int, int, const char*)` — matches the calls above.
### 4c. Registration (`btl4grnd.cpp`, in `BTL4MethodDescription[]`, before `&BTL4ChainToPrevious`)
```cpp
&GeneratorCluster::methodDescription, // (existing, line 149)
&PrepEngrScreen::methodDescription, // "prepEngr" -- per-Eng-screen static label overlay
&BTL4ChainToPrevious
```
(`#include <btl4gau2.hpp>` is already present, btl4grnd.cpp:69.)
---
## 5. Systemic-check verdicts
- **Container-Execute override needed?** **NO.** `PrepEngrScreen` is a `GraphicGaugeBackground`, which has **no `Execute` virtual** (`Execute` first appears on `Gauge`, GAUGE.h:366). The `Gauge::Execute → Fail→abort` hazard does not apply. The overridden slot is **`BecameActive()`** (confirmed vtable-identical to `BackgroundBitmap`). The override body is a real paint (does not chain the inactivating base) — exactly like `BackgroundBitmap::BecameActive` (L4GAUGE.cpp:2510), so the "default BecameActive inactivates" hazard is also handled.
- **Shadow fields?** None. Own fields (`screenNumber`/`mech`/`statusImage`) are new; the base `GraphicGaugeBackground` owns `renderer`/`localView`, which are **used** (not re-declared). Do NOT re-declare `localView` or `renderer`.
- **Alias fields?** None.
- **Phantom fields?** None — all own fields are inside the 0x90 object; nothing past `sizeof`.
- **The stub's two bugs to fix:** base `Gauge``GraphicGaugeBackground`; `screenNumber`/`mech` offsets swapped; `Execute()``BecameActive()`.
- **Databinding NULL / every-mech crash risk:**
- `mech==NULL` → guarded (early return).
- roster via `GetSubsystemCount/GetSubsystem` (safe accessors) + slot-0 skip → safe.
- `ResolveLink("HeatSink")` may be NULL → **guarded** before the `+0x1D4` read.
- `sub+0x1D4` and `sub+0x224` are **raw subsystem offsets** on not-byte-exact objects → **fail-soft** (wrong digit / `bitMapBin.Get` returns NULL → draws nothing), not a crash. §6 gives the safe substitutes (label bridge; a "HeatSinkNumber" attribute if one is published).
- `NumericDisplay`/`DrawStatusCells`/`DrawPortBackground` operate on the gauge's own `localView` + `warehouse` (both valid post-ctor) → safe. No `GuardedExecute` needed (background paints aren't SEH-wrapped, but nothing here dereferences unresolved game state without a guard).
- **Registration / lifetime:** `GaugeBase::GaugeBase` self-registers (`renderer->Add(this)`, GAUGE.cpp), so `Make` needs no explicit `Register_Object` (that macro is only debug memory-tracking, MEMREG.h:24). Unlike `BackgroundBitmap`, `prepEngr` is **never** `ModeAlwaysActive` (always `ModeMFD*Eng*`), so there is **no run-once-then-delete** branch — the gauge persists and repaints on each activation. Correct.
---
## 6. Confidence & unknowns
**Confidence: HIGH** for identity, base class, the `BecameActive` slot, the Make/ctor/dtor bodies, the methodDescription shape, the layout, and the ClassID→label dispatch. The base class, vtable slot, and lifetime are triple-confirmed (base-ctor `FUN_004440d4` signature match + `BackgroundBitmap` structural twin + `vtables.tsv` row `0051a040`). The CFG call shape (9 params) and image→slot mapping are pinned to the exact ctor stores and the switch.
**Unknowns / follow-ups (all cosmetic, none block registration):**
1. **`sub+0x224` (subsystem label .pcc name), part_014.c:1322** — a raw embedded `char[]` used as a bitmap name. It is **not** `Subsystem::subsystemName` (`GetName()`, at a much lower offset) and **not** the aux-screen label (`sub+0x1E4`, which `BTGetSubsystemAuxScreen` already returns). Spec substitutes the bridge's `label64`; confirming/bridging the true `+0x224` field (or verifying it equals the aux label) needs a `disas2.py`/field-map pass on the subsystem ctor. Low value (one label bitmap).
2. **`resolvedHeatSink+0x1D4`, part_014.c:1317** — a 1-digit readout (the linked sink's bank/index). Byte-exact `HeatSink` is only 0x1D0, so the resolved object must be a larger subclass for this to be in-bounds; raw read is best-effort. If a `HeatSinkNumber`/`BankNumber` attribute exists, prefer `AttributePointerOf`; otherwise keep the guarded raw read.
3. **`DrawStatusCells` empty-cell C-placeholder** (part_014.c:1156-1159) uses GraphicsView **vtbl+0x4c(99,10)** — an unresolved GraphicsView slot (between `DrawFilledRectangleToAbsolute@0x48` and `DrawBitMap@0x54`; likely a relative-size rectangle). The spec draws a plain fill at the same coords; resolving the exact call needs a GraphicsView vtable decode. Only affects the blanked-cell appearance when a label is NULL.
4. **The `FUN_0041a1a4(**(sub+0xc),0x50f4bc)` filter** (part_014.c:1308) is folded into `BTGetSubsystemAuxScreen` (same treatment as `VehicleSubSystems`); if that bridge's filter ever diverges from the exact `ClassDerivations` resolution it would change *which* subsystems qualify — but the ClassID `switch` is the effective filter anyway, so this is faithful.
**Net:** registering `&PrepEngrScreen::methodDescription` + the bodies above stops the 12 `prepEngr(...)` lines from parse-skipping and paints the engineering-screen labels; the two raw subsystem reads (#1,#2) are guarded fail-soft best-efforts flagged for a later attribute/bridge pass.
+42
View File
@@ -0,0 +1,42 @@
I have completed the adversarial verification against the raw decomp, the CFG, the sibling templates, and the data-binding source files. Every load-bearing claim in the spec was checked. Here is the corrected verdict.
---
# ADVERSARIAL VERIFY — MessageBoard reconstruction spec
**Bottom line:** the spec is **substantially CORRECT and ready-to-implement**. All addresses, `sizeof`, ctor offset writes, the base class, the methodDescription row set, the Make/dtor conventions, and the DEFERRED-data verdict are verified against the binary. The spec's **three header corrections are all genuinely correct**, and its **correction of the task hint** (source is BTPlayer status queue, *not* SubsystemMessageManager 0xBD3) is verified. I found **one wrong citation**, **one fidelity deviation to flag**, and **two small notes**. No blocking errors.
## What I verified as CORRECT (accept as-is)
- **Identity / addresses** (all confirmed `part_014.c`): Make `@004cb678:3825`, ctor `@004cb704:3849`, dtor `@004cb788:3872`, TestInstance `@004cb7e4:3892``FUN_004448ac`, no-op slot `@004cb7f4:3903`, BecameActive `@004cb7fc:3913`, SetSource `@004cb818:3925`, Execute `@004cb82c:3936`, vtable `PTR_FUN_0051bddc` (`:3858/:3876`). Base = `GraphicGauge` (`FUN_00444818`), ends at 0x90.
- **sizeof 0xA4 == Make alloc** `FUN_00402298(0xa4)` (`:3832`). ✓
- **ctor offset writes** (`:3859-3864`): `[0x25]`=stripImage(0x94), `[0x26]`=color(0x98), `[0x24]`=trackedMech(0x90)=0; `SetOrigin(x,y)` via `localView` vtbl+0x10. ✓
- **The three header fixes are all correct:**
- `enabled`**`Entity *trackedMech`**: 0x90 is a pointer dereferenced at `+0x190` (`iVar1 = *(*(param_1+0x90)+400)`, `:3950`) and written only by `SetSource` (`:3928`). It is not a bool. ✓
- **previousMessageId / previousNameId are SWAPPED** in the current header. Binary proof: BecameActive writes `0x9c=-1, 0xa0=-2` (`:3916-3917`); Execute compares **messageId vs 0xa0** (`:3960`) and **nameEntity(iVar5) vs 0x9c** (`:3983`). ⇒ `0x9c=previousNameId(-1)`, `0xa0=previousMessageId(-2)` — exactly the spec's relabel, and it matches the prose at `btl4gau3.cpp:784`. ✓
- **methodDescription rows** match the CFG call `messageBoard(E, ModeAlwaysActive, btsmsgs.pcx, 0)` at `L4GAUGE.CFG:4913-4917` under the `offset=(113,607)` at `:4912` → 4 rows `typeRate/typeModeMask/typeString/typeColor`; position threaded from the `offset` directive; **entity param ignored** (binary Make never touches `param_4`, `:3825-3843`). This is exactly the PilotList template (`btl4gau3.cpp:218-239`), except MessageBoard *does* use `position` (SetOrigin). ✓
- **Make presence-check + return** is byte-faithful *and* matches the codebase template `VertTwoPartBar::Make` (`btl4gaug.cpp:1099-1107`: `Get(string); if NULL return False; Release; return True`). `content/GAUGE/BTSMSGS.PCX` exists → returns True in practice. ✓
- **dtor** correctly omits the base-dtor call (`FUN_00444870` is compiler epilogue glue, `:3880`) — avoids the double-dtor bug. `FUN_004700ac` is the string-dup helper used across widgets; freeing via `delete[]`/`FUN_004022e8` is consistent. ✓
- **Data-binding blocker — CONFIRMED on both legs:**
- **SetSource (`FUN_004cb818`) has ZERO callers** anywhere in `reference/decomp/all/` (grep returns only its own definition) → `trackedMech` stays NULL. ✓
- **`StatusMessagePool = 0`** — the NULL stub is real (`btstubs.cpp:62`), and the only writer of `BTPlayer+0x1dc` is gated `if (sender_owner && StatusMessagePool)` (`btplayer.cpp:508`) → no messages ever created. ✓
- **Task-hint correction is right:** `SubsystemMessageManager` (`messmgr.cpp`) *is* reconstructed but is the per-mech damage/explosion hub (`ConsolidateAndSendDamage`/`weaponExplosions`), not a `{msgId,nameEntity}` source; MessageBoard never references it. `mech+0x190`=owningPlayer confirmed (`btplayer.cpp:68`, `MECH_OWNING_PLAYER`); `DAT_004efc94`==application (App+0xc8 name cache, `btplayer.cpp:53`). ✓
- **Systemic checks:** leaf `GraphicGauge` (not a container) that overrides both Execute and a non-inactivating BecameActive → Container-Execute/Fail rule satisfied. No shadow/alias/phantom fields (all new slots 0x900xA3, ends exactly at 0xA4). Raw `mech+0x190`/`player+0x1dc`/`+0x1e0`/App+0xc8 reads kept out of the widget TU via the `BTResolveMessageBoard` bridge (databinding trap avoided). Every-mech crash risk: none (early return on NULL). `/FORCE` stub for the bridge required. ✓
## CORRECTIONS (the list of what I changed)
1. **[Citation fix — §2, §5, §6] `StatusMessagePool`'s NULL definition is `btstubs.cpp:62`** (`MemoryBlock *StatusMessagePool = 0;`), **not** `btplayer.cpp:135/508`. Those btplayer lines are the `extern` decl and the guard site; the actual stub lives in `btstubs.cpp`. The conclusion (feed is dead) is unchanged — only name the correct file so the follow-up (wiring the pool) edits the right place.
2. **[Fidelity deviation — §4.8, must be flagged, not fixed] The Execute name-cell path is a STRUCTURAL simplification, not byte-faithful.** The binary stores the *previous name ENTITY POINTER* at 0x9c and, when the sender changes to "none", looks up that previous entity's name bitmap via its `+0x1e0` key and **erases** it with `DrawBitMap` (vtbl+0x54, `:3992-3996`). The spec collapses 0x9c to a boolean `nameState` (0/1) and drops the erase-previous path. **While DEFERRED this is functionally identical** (nameEntity always 0, previousNameId init 1 → first Execute takes the `iVar5==0 && prev==-1` clear branch, `:3986-3990`, matching the binary), so it is safe for the landing. **But it changes `previousNameId`'s semantics from pointer→int**, so when the status feed goes live the entity-pointer tracking + erase branch must be restored for correct name repainting. Keep the spec's body for the deferred landing; add a code comment marking this as the restore point. (The spec does acknowledge the deviation; I am raising it to an explicit action item because it silently redefines a member.)
3. **[Minor note — §1/§4.5] The no-op vtable slot `@004cb7f4` ("ShowInstance"):** the binary override is a bare `return`. The spec omits it and inherits (per the PilotList/PlayerStatus precedent). This is only byte-faithful **if** the `GraphicGauge` base slot is itself trivial for this call site; if strict fidelity is wanted, add an empty override. Low risk (leaf widget, slot rarely hit) — accept the omission but note it.
4. **[No change, confirmation] Make's `bool` return on a missing bitmap** is binary-faithful and template-matched; because `BTSMSGS.PCX` is present it returns True, so registration/parse is unaffected. No action.
Everything else in the spec — the layout table + `static_assert` locks, ctor/dtor/BecameActive/TestInstance/SetSource/Make bodies, the `BTResolveMessageBoard` bridge + returns-`False` stub in `btplayer.cpp`, the `methodDescription` literal, and the registration line inserted before `&BTL4ChainToPrevious` in `btl4grnd.cpp` (after `GeneratorCluster`) plus adding `static MethodDescription methodDescription;` to the class — is **verified and should be implemented as written**.
## FINAL CONFIDENCE: **HIGH**
- Layout/methods/registration/sizeof/offsets: **HIGH** — fully recovered and cross-checked against `part_014.c:3823-4013` and the `PilotList`/`VertTwoPartBar` templates.
- DEFERRED/empty verdict: **HIGH** — both blockers independently confirmed in-source (`btstubs.cpp:62` NULL pool; SetSource zero callers).
- Only residual **UNKNOWN** (non-blocking, for the *alive* path, not the landing): the exact `Player__StatusMessage` record layout (`+0xc` messageId, `+0x10` nameEntity) and the App+0xc8 name-bitmap cache shape needed inside `BTResolveMessageBoard` once `StatusMessagePool` is wired — decode `FUN_0042e580` (AddStatusMessage) + the message-record ctor `FUN_0042d990` to expose them as compiled accessors. The default landing (safe no-op) does not touch these.
+71
View File
@@ -0,0 +1,71 @@
I now have everything verified against the raw decomp, the engine headers, the CFG, and the sibling reconstructions. Here is the adversarial verdict.
---
# ADVERSARIAL VERIFY — SectorDisplay: verdict **APPROVE WITH CORRECTIONS**
The spec is structurally sound and its layout corrections over the current (buggy) `btl4gau3.hpp` are **right**. I confirmed sizeof, every ctor offset, the base chain, the Make idiom, the methodDescription row set (against the live CFG), the dtor/BecameActive/Execute structure, and all five systemic-check verdicts. **One substantive error** (a wrong rounding claim with a fabricated justification) and **two confidence downgrades** (disassembly-only claims) must be fixed before implementing.
## What I verified as CORRECT (keep as-is)
- **Base = `GraphicGauge`, base ctor `FUN_00444818`, sizeof `0xC4`** — Make allocs `FUN_00402298(0xc4)` (part_014.c:2632). ✓
- **Every ctor offset** vs the raw `param_1[N]` writes (part_014.c:26662704): `gridImage@0x94`, `cellWidth@0x98=width/14`, `gridLeft@0x9C=cw*12`, `gridRight@0xA0=cw*13-1`, `gridHeight@0xA4`, `numericColor@0xA8`, `gridColor@0xAC`, both `500`s @0xB0/@0xB4, `numericA@0xBC (x=0)`, `numericB@0xC0 (x=cw*4)`. ✓
- **The header-layout fixes are real bugs in the current header** — it omits `@0xA8` and `@0xB8`, mislabels `@0x90` as `enabled` and `@0xB0/@0xB4` as `baseLine/dirty`, and wrongly gives the ctor **two** image strings (the raw has ONE, `param_9`, used for both grid + font). Spec fixes all of these. ✓
- **`@0x90` = `subject` Entity\* set by `LinkToEntity` (slot 9), NOT `enabled`/`SetEnable`** — well-founded: `FUN_004ca068` is a bare `this[0x24]=param` setter, base slot9 is the empty `LinkToEntity`, and Execute gates on `+0x90 != 0` and reads a *position* from it. This is the correct reinterpretation of the current header. ✓
- **methodDescription 5-row set** (`typeRate, typeModeMask, typeString, typeColor, typeColor`) — matches the Make's DAT_ reads AND the live CFG (`content/GAUGE/L4GAUGE.CFG:5146`): `sectorDisplay(K, ModeAlwaysActive, helv15.pcc, 0, 3)` with `offset=(125,579)` supplied separately. ✓
- **Make/ctor base-chain idiom** — identical to the registered sibling `PilotList` (`btl4gau3.cpp:226238, 253`): `ParameterDescription *p = methodDescription.parameterList; … GraphicGauge(rate,mode,renderer,0,port,id)`. ✓
- **dtor** (implicit base, release grid, free name, delete numerics), **BecameActive** (dirty=1, ForceUpdate B-then-A, non-inactivating), **Execute** grid blit (`MoveToAbsolute(cw*3,0)`+`SetColor(gridColor@0xAC)`+`DrawBitMap(0,grid,gridLeft,0,gridRight,gridHeight)`) — all match the raw byte-for-byte. ✓
- **Systemic checks all correct**: Execute (slot16) + non-inactivating BecameActive (slot3) both overridden → no `Gauge::Execute` `Fail``abort`; the two NumericDisplays are private members drawn directly, not registered children → no child-`GuardedExecute` recursion; no shadow/alias/phantom fields; `subject`-NULL guarded with the viewpoint fallback; single cockpit gauge, not per-mech. ✓
- **`renderer->warehousePointer` is reachable in member methods/dtor** — confirmed by `PlayerStatus::Execute` (`btl4gau3.cpp:673`). Registration line goes before `&BTL4ChainToPrevious` (`btl4grnd.cpp:150`). ✓
## CHANGES I made (what was wrong)
**① CRITICAL — `FUN_004dcd94` is `ROUND`, not truncate.** Confirmed at its definition (`part_015.c:3769`): `local_10 = (undefined4)(longlong)ROUND(in_ST0);` — round-to-nearest (x87 `frndint`, half-to-even). The codebase's own map agrees (`btl4gau3.cpp:36` "FUN_004dcd94 Round"; CLAUDE.md). The spec asserts the **opposite** ("truncate toward zero … RC=chop `or 0xc`, `fistp``(long)` not round-to-nearest") — that justification is **fabricated**; there is no `or 0xc`/`fistp` in the function. Fix: the two numerics must **round**, not C-truncate. Use `lrintf` (default FE_TONEAREST == `frndint`) or the engine `Round`, never `(int)(x)`.
**② Downgrade the position data-binding to disassembly-sourced / MEDIUM confidence.** The pseudocode Execute (`FUN_004ca07c`, part_014.c:27892794) shows **only** `iVar2 = FUN_004dcd94(); iVar3 = FUN_004dcd94();` — the decompiler dropped the x87 operand loads. So the *entire* mapping (that `iVar2 = -Z·0.01`, `iVar3 = +X·0.01`, the `fchs`, and the `0.01` scale) is the reviewer's disassembly, which was **provably wrong on `FUN_004dcd94`**. The `subject != 0` gate + the two `+500` offsets + drawing two numerics are pseudocode-solid; the **which-axis / sign / scale** are not. Keep the mapping as the best structural reconstruction but mark it, and re-disassemble `@0x4ca0890x4ca0b9` to lock the axis/sign/scale before claiming fidelity. (The read itself — `subject->localOrigin.linearPosition` via the engine `Entity` type — is confirmed as the established radar pattern: `btl4rdr.cpp:150` reads `entity->localOrigin.linearPosition`; `ENTITY.h:140 Origin localOrigin`; entity+0x100 == that per `btl4rdr.cpp:622`.)
**③ Unify the `FUN_00442aec` naming to `GetIfAlreadyExists`** (the codebase's established name, `btl4gau3.cpp:128129 "peek, no AddRef"`). The spec inconsistently modeled the identical call as `bitMapBin.Get` (held) in the ctor but `GetIfAlreadyExists` (peek) in Make/Execute. Use `GetIfAlreadyExists` in all three; keep the dtor `Release` (`FUN_00442c12`, part_014.c:2717) balancing the ctor's held reference and the Make existence-check Release (part_014.c:2640) — that is binary-faithful.
**④ Minor:** remove the now-unused static `DAT_0051a2bc/0300/0344` placeholders (`btl4gau3.cpp:6971`) — the rewired Make reads `methodDescription.parameterList` (same as the PlayerStatus rewire, line 7273). Drop the redundant `(L4Warehouse*)` cast in Make (`warehousePointer` is already typed, per line 128). Confirm `NumericDisplay::unsignedFormat == 0` (raw format arg is literal `0`); if unsure use `(NumericDisplay::Format)0`. The `offsetX/offsetY` names are misleading (both `500`; A feeds the Z-numeric, B the X-numeric) — rename `sectorBaseA/sectorBaseB` or just comment "both = 500".
## Corrected Execute (the only body that changes)
```cpp
// @0x4ca07c -- Execute (slot 16). RAW: iVar2/iVar3 = FUN_004dcd94() = ROUND(ST0)
// (part_015.c:3769); the x87 operand loads were dropped by the decompiler, so the
// -Z / +X axis assignment, the fchs and the 0.01 scale are DISASSEMBLY-sourced
// (@0x4ca089-0x4ca0b9) and MEDIUM-confidence -- re-verify before claiming fidelity.
void SectorDisplay::Execute()
{
Entity *s = subject; // this+0x90
if (s == NULL && application != NULL) // PORT: LinkToEntity broadcast
s = (Entity *)application->GetViewpointEntity(); // may be unwired -> fall back
if (s == NULL)
return; // binary gates purely on subject!=0
const Point3D &pos = s->localOrigin.linearPosition; // engine type (NOT raw +0x100)
int vA = (int)lrintf(-pos.z * 0.01f) + sectorBaseA; // ROUND, not (int) truncate
int vB = (int)lrintf( pos.x * 0.01f) + sectorBaseB;
numericA->Draw(&localView, (Scalar)vA); // FUN_00470430
numericB->Draw(&localView, (Scalar)vB);
if (dirty) { // this+0xB8
dirty = 0;
BitMap *grid = ((L4Warehouse *)renderer->warehousePointer)
->bitMapBin.GetIfAlreadyExists(gridImage); // FUN_00442aec
if (grid != NULL) {
localView.MoveToAbsolute(cellWidth * 3, 0); // vtbl+0x24
localView.SetColor(gridColor); // vtbl+0x18 (this+0xAC)
localView.DrawBitMap(0, grid, gridLeft, 0, gridRight, gridHeight); // vtbl+0x54
}
}
}
```
Everything else in the spec (identity table, the `0xC4` overflow-lock, the methodDescription literal, the registration line, ctor/dtor/Make/BecameActive/TestInstance bodies) is **correct as written** — apply only ①–④.
## FINAL CONFIDENCE
**HIGH** on structure, layout, registration, and all systemic-check verdicts (pseudocode-verified). **MEDIUM** on the numeric data-binding specifics: the value is a *rounded* `±position·0.01 + 500`, but the exact axis→numeric assignment and the sign of Z are disassembly-only and the reviewer's disassembly was demonstrably unreliable (it inverted the rounding mode). Re-disassemble `@0x4ca0890x4ca0b9` to lock the axis/sign/scale; nothing else blocks implementation.
**Remaining UNKNOWNS (non-blocking):** exact axis/sign/scale of the position read (②); whether `GaugeRenderer::LinkToEntity` is broadcast in the port (the viewpoint fallback makes it LIVE either way); `NumericDisplay::unsignedFormat`'s enum value (raw = `0`).
+47
View File
@@ -0,0 +1,47 @@
I have now verified every load-bearing claim against the raw decomp, the vtable data, the BackgroundBitmap twin, and the established reconstruction templates. Here is my adversarial-verification verdict.
---
# VERIFY VERDICT: PrepEngrScreen spec — **CONFIRMED CORRECT** (with 3 minor strengthenings)
The colleague's spec is **sound and ready-to-implement**. Its three headline corrections to the existing stub are all independently verified. I found no incorrect claim; the changes below are hardening, not fixes.
## Core claims — all VERIFIED against the raw
**(a) Base class / sizeof / ctor offsets — CORRECT**
- Base = `GraphicGaugeBackground`, **confirmed**: PrepEngr's ctor `FUN_004c7bf0` calls base ctor `FUN_004440d4` (part_014.c:1213), and `FUN_004440d4` is the ctor called by `BackgroundBitmap` and its siblings (part_009.c:1633/1695/1758/1838/1956) — the L4GAUGE `GraphicGaugeBackground` leaves. 5-arg base ctor `(mode, renderer, owner, port, id)` matches `GraphicGaugeBackground(ModeMask, GaugeRenderer*, unsigned, int, const char*)` (GAUGE.h:117). The stub's `public Gauge` is **wrong** → change to `GraphicGaugeBackground`.
- sizeof **0x90** = `FUN_00402298(0x90)` (part_014.c:1189). ✓
- ctor field writes: `param_1[0x1b]=param_7`(screen)@0x6C, `param_1[0x1c]=param_6`(mech)@0x70 (part_014.c:1215-1216). **Stub has these two swapped** (btl4gau2.hpp:318-319) → spec's fix is correct.
- ctor param order `(mode, renderer, owner, port, mech, screen, img0..6, id)` matches the Make→ctor call (part_014.c:1191). ✓
**(b) methodDescription rows — CORRECT.** CFG `prepEngr(...)` (L4GAUGE.CFG:4595) has exactly 9 args: `ModeMFD1Eng1, 1, helv42.pcc, escnd.pcc, edmg.pcc, bteslev.pcc, bteunjm.pcc, espeed.pcc, btesrnge.pcc``typeModeMask, typeInteger, 7×typeString`. Global-DAT spacing confirms the parameterList mapping: `sizeof(ParameterDescription)=0x44` (4 + `maxStringLength`=0x40, GAUGREND.h:242-262), and `DAT_0051936c`(screen)=`parameterList[1].data`, `DAT_005193b0`(img0)=`parameterList[2].data.string`, stride 0x44 — exactly the reconstruction's read `p[i].data.{integer,string,modeMask}`. ✓
**(c) Container-Execute rule — CORRECTLY N/A.** `Execute` first appears on `Gauge` (GAUGE.h:366, vtable slot ~14); a `GraphicGaugeBackground` vtable has only 13 slots and no `Execute`. **The overridden slot is BecameActive, not Execute** — proved three ways:
1. PrepEngr vtable (vtables.tsv row `0051a040`, ptr `0051a06c`) = `[4c7d14(dtor), 4173b8, 417938, 4c7e48, 444004…]`**byte-identical shape** to BackgroundBitmap (`004fb918`, ptr `471d7c`) = `[471d7c(dtor), 4173b8, 417938, 471dec, 444004…]`.
2. `BackgroundBitmap` is *explicitly* a `GraphicGaugeBackground` (L4GAUGE.cpp:2443) whose **only non-dtor override is `BecameActive()`** (L4GAUGE.cpp:2511) → its single override slot (3) = BecameActive → PrepEngr's slot 3 (`4c7e48`) = BecameActive.
3. Slots 1/2 are Node virtuals: `FUN_004173b8` = `return 0` = `Node::ReceiveNodeCommand` (node.h:10 inline `{return 0;}`); `FUN_00417938` = `ReleaseLinkHandler` — so slot 3 is the *first GaugeBase virtual* = `BecameActive` (GAUGE.h:52).
`FUN_004c7e48` is a paint-on-activation (walk roster → draw labels into `localView`), semantically a BecameActive, not a per-frame Execute. No container/child-chain exists (ctor builds none; dtor `FUN_004c7d14` only frees 7 images + implicit localView/base dtor) → the abort-on-unoverridden-Execute hazard does not apply. ✓
**(d) shadow/alias/phantom / databinding — CORRECTLY handled.** No shadow/alias/phantom fields. Roster read uses the databinding-safe `Entity::GetSubsystemCount()/GetSubsystem(i)` (ENTITY.h:167-177, reads the reconstructed `subsystemArray/subsystemCount`) + `BTGetSubsystemAuxScreen` — the exact `VehicleSubSystems::Make` pattern (btl4gau2.cpp:1621-1636). NumericDisplay/DrawPortBackground operate on the object's own `localView`/`warehouse`. The two raw-offset reads (`hs+0x1D4`, `sub+0x224`) are correctly guarded fail-soft and flagged (§6).
**(e) every-mech / abort / NULL — no crash risk.** `mech` null-checked; `GetSubsystem(i)` bounded by `i<count` (its internal `Verify(index<subsystemCount)` can't trip); `ResolveLink("HeatSink")` null-guarded before the `+0x1D4` read; the `0x50f4bc` filter + auxScreen check gate every deref. Not an every-mech path (gauge is bound to the viewpoint mech). ✓
## Bonus confirmations (spec was even more right than it claimed)
- **NumericDisplay usage is EXACT, not just "structural."** L4GAUGE.h:22-63 defines `class NumericDisplay` with `NumericDisplay(L4Warehouse*, int x, int y, const char* font, int digits, NumericFormat, int bg, int fg)` and `void Draw(GraphicsView*, Scalar)`. The spec's `NumericDisplay num(warehouse,0xe9,0x176,font,2,unsignedFormat,0,0xff); num.Draw(&localView,(Scalar)screenNumber)` maps **1:1** to the raw `FUN_004700bc(…,0xe9,0x176,font,2,0,0,0xff)` + `FUN_00470430(…)` + dtor `FUN_0047018c` (part_014.c:1310-1313). `Draw` (not `DrawAt`) is the correct method name.
- **`DrawPortBackground(GraphicsView*, L4Warehouse*, int x, int y, const char*)`** = `FUN_004c2ec4` (btl4gau2.cpp:923) — signature matches the spec's calls; the raw's `(x,y,view,warehouse,name)` arg order is correctly reordered by the existing helper.
## Changes I made (3, all hardening)
1. **Make allocation — switch to plain `new PrepEngrScreen(...)`** (drop `operator new(0x90)`+placement-new). The reconstructed `GraphicsView localView` is not byte-exact to the 1995 `0x24..0x6B` window, so `sizeof(PrepEngrScreen)` may exceed 0x90 — a fixed `operator new(0x90)` would then heap-overflow. `new PrepEngrScreen(...)` allocates the true size and matches `BackgroundBitmap::Make` (L4GAUGE.cpp:2379); no external code reads this object at raw offsets, so the binary alloc size is not load-bearing. Keep `static_assert(sizeof(PrepEngrScreen) <= 0x90)` only as a compile-time canary. Optionally add `Register_Object(g);` after construction for parity with BackgroundBitmap (debug leak-tracking only).
2. **§6 #4 UPGRADED from "unknown" to CONFIRMED FAITHFUL.** `BTGetSubsystemAuxScreen` (powersub.cpp:1333) applies `BTIsPoweredSubsystem` = `FUN_0041a1a4(…,0x50f4bc)` — the *exact* filter the raw uses (part_014.c:1308) — and returns `auxScreenNumber` via the real `PoweredSubsystem` type (+0x104 = binary `sub+0x1DC`). So folding the filter + `auxScreen==screenNumber` gate into the bridge is byte-faithful; the qualifying-subsystem set is identical.
3. **Acknowledge the dropped erase in the label block.** The raw does 3 unresolved `localView` vtbl calls (`+0x18/+0x24/+0x48`, args not recovered by Ghidra) *before* the `sub+0x224` label draw (part_014.c:1323-1325) — a cell-erase/setup. The spec drops them and substitutes `SetColor(0xff)` + `DrawPortBackground(label64)`. Cosmetic only; add to the §6 best-effort list next to #1.
## Residual UNKNOWNS (unchanged from spec, all cosmetic, none block registration)
- `sub+0x224` drawn-label ≠ `auxScreenLabel` (`sub+0x1E4`, what the bridge returns) — the label bitmap name may differ; needs a subsystem-ctor field-map pass (`disas2.py`). Guarded/flagged.
- `resolvedHeatSink+0x1D4` (1-digit sink index) is past byte-exact `HeatSink` (0x1D0) → resolved link must be a larger subclass; guarded raw read, best-effort.
- `DrawStatusCells` C-NULL branch `vtbl+0x4c(99,10)` — unresolved GraphicsView slot; plain fill substituted.
## FINAL CONFIDENCE: **HIGH**
Identity, base class, the BecameActive slot, layout, ctor/dtor/Make/BecameActive bodies, the 9-param methodDescription, and the `BTL4MethodDescription[]` registration line are all verified against `part_014.c`, `vtables.tsv`, `L4GAUGE.cpp/h`, `GAUGE.h`, `ENTITY.h`, and `powersub.cpp`. Registering `&PrepEngrScreen::methodDescription` (btl4grnd.cpp, before `&BTL4ChainToPrevious`) + the corrected header (`public GraphicGaugeBackground`, `void BecameActive()`, `screenNumber@0x6C`/`mech@0x70`) + the bodies as written (with change #1) will build and stop the 12 `prepEngr(...)` lines from parse-skipping. The only behavioral caveats are the two flagged cosmetic label reads.