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
+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
}
}
}
//###########################################################################