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

294 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`).