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>
19 KiB
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 NumericDisplays 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
@0x4ca194is the 80-bit extended0.01(= ÷100 → 100-unit sectors); decoded exact. -Zusesfchs@0x4ca09c; both truncate toward zero (FUN_004dcd94sets FPU RC=chopor 0xc,fistp— it is(long), not round-to-nearest).entity+0x100==Entity::localOrigin.linearPositionis already established by the reconstructed radar (btl4rdr.cpp:150,622FUN_00408440(this+0x310, owner+0x100));.x@+0x100, .z@+0x108.Origin localOriginis a base-Entitymember (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:
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):
// 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
//======================= 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):
&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) andBecameActive(slot 3, non-inactivating) are both overridden, so the baseGauge::ExecuteFail→abort()is never reached. The twoNumericDisplays are private sub-objects drawn directly (not registered gauges), so there's no child-Execute/GuardedExecuterecursion. - Shadow fields: none vs the engine base —
subject@0x90is the first subclass field (baseGraphicGaugeends 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@0xA8is a real stored field (consumed only by the child ctors, never re-read — faithful, not phantom);offsetX/offsetYare two independent500s; all members lie withinsizeof 0xC4. - Databinding NULL risks: all guarded —
subject==NULLearly-returns (+viewpoint fallback); missing grid image →GetIfAlreadyExistsNULL → blit skipped + Make warns;renderer->warehousePointeralways set by the base. Add anumericA/numericB != NULLguard beforeDrawif you keep the binary'snew-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.linearPositionthrough the engineEntitytype (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):
- Whether
GaugeRenderer::LinkToEntityis broadcast with a real viewpoint in the port. Theapplication->GetViewpointEntity()fallback makes the gauge LIVE either way; if you confirm the broadcast fires, the fallback is inert. InternName/FreeName(FUN_004700ac/FUN_004022e8) exact engine symbols — ifNumericDisplay's ctor copies the font name internally (siblings passp[2].data.stringuncopied), you may storep[2].data.stringdirectly and drop the intern/free pair. Cosmetic.bitMapBin.Get(ctor, held) vsGetIfAlreadyExists(peek) ref-counting — both compile; the ctorGetis balanced by the dtorRelease(binary-faithful). IfGet/Releasearen't symmetric on the WinTeslaL4Warehouse, useGetIfAlreadyExistsin the ctor and drop the dtorRelease.FUN_004ca030(emptyret) is an unreferencedTestClass/dead stub — not needed for function.- 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).