Files
BT411/scratchpad/wf_1_SectorDisplay_decode.md
arcattackandClaude Opus 4.8 4a4ec6855c gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z
coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/
methodDescription/registration) -> the config line was parse-SKIPPED and never built.

Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c,
methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute
reads the linked mech's world position and shows two 100-unit sector numerics:
  numericA = Round(-localOrigin.z * 0.01) + 500
  numericB = Round( localOrigin.x * 0.01) + 500
(rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong
"truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm).
Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating),
Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked
(static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling;
the config image name is copied (nameCopy) since Execute reads it per-frame.
Registered in BTL4MethodDescription[].

VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1;
Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504
-- authentic 100-unit sectors from live mech position). Gauge composite renders full,
no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard).

Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each
unregistered gauge primitive the dev-parse skips -- the tool that pinned this down
(earlier "not built" runs were killed before the lazy gauge-renderer init).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:25:48 -05:00

19 KiB
Raw Permalink Blame History

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 (→GraphicGaugeBackgroundGaugeBase); 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 @0x4ca020FUN_004448acFUN_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 @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:

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
        }
    }
}

Registrationgame/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) and BecameActive (slot 3, non-inactivating) are both overridden, so the base Gauge::Execute Failabort() is never reached. The two NumericDisplays 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 (enabledsubject, missing +0xA8/+0xB8, baseLine/dirtyoffsetX/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 500s; 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).