gauges: register + wire the map (radar) -- the tactical display renders

MapDisplay (the radar/threat-map gauge, btl4rdr.cpp) was fully reconstructed
(ctor + Execute + DrawViewWedge/DrawStatic/DrawMoving/DrawNames) but never
registered and its data attributes never published.  Wire it up end-to-end:

DATA (the attributes the map binds):
* Mech table extended to the full 0x15..0x38 (was the 0x21 prefix): add
  RadarRange@0x2f -> radarRange, RadarLinearPosition@0x30 -> radarLinearPosition
  (Point3D* -> &localOrigin.linearPosition), RadarAngularPosition@0x31 ->
  radarAngularPosition (Quaternion* -> &localOrigin.angularPosition), DuckState
  @0x37 -> duckState; the rest bind the shared attrPad (kept dense so Find can't
  strcmp a gap).  New members set in the Mech ctor; radarRange defaults to 500
  (SetTargetRange is still stubbed) so nearby contacts are on-scale.  The map's
  position/angle attributes resolve to Point3D**/Quaternion** (AttributePointer
  is int Simulation::*, so the ATTRIBUTE_ENTRY reinterpret binds pointer members
  fine -- verified live: posPtr/anglePtr resolve).
* Sensor (named "Avionics") publishes RadarPercent/SelfTest/BadVoltage: define
  Sensor::AttributePointers[] + build the previously-empty Sensor::AttributeIndex
  chained to PoweredSubsystem::GetAttributeIndex() (= HeatSink's dense index).

WIDGET (the registration glue -- NOT in the assert-anchored decomp, so
reconstructed from the config + ctor):
* MapDisplay::methodDescription (config "map") + MapDisplay::Make.  The
  offset_position keyword "center"/"bottom" is typed as a STRING and converted
  in Make, avoiding a ModeManager named-constant registration.  Registered in
  BTL4MethodDescription[] (btl4grnd.cpp now includes btl4rdr.hpp).

Three crash fixes exposed once the map ran (each a pre-existing stub the map is
the first consumer of):
* Sensor radarPercent went hugely negative (RadarBaseline - HeatMasterEnergy(),
  where the inherited heatEnergy is un-normalized in the not-byte-exact heat-leaf
  branch) -> the radar reported non-operational.  Guard: treat an out-of-[0,1]
  heat term as no penalty (marked bring-up; real overheat penalty needs the heat
  normalization).
* ResolveOperatorEntity was a stub returning NULL -> EntityPosition(NULL) crashed
  CalculateBounds.  Fixed to the renderer linked-entity + application viewpoint
  fallback (same as HeadingPointer).
* MapName::ExtractFromEntity didn't guard a NULL entity (the binary's Verify(False)
  iterator-mismatch path compiles out at DEBUG_LEVEL 0) -> EntityPosition(NULL).
  Guarded.

Verified live (BT_DEV_GAUGES): the radar now draws the view wedge (the mech's
180-deg FOV cone) and completes the full wedge/static/moving/names cycle with
operating=1, cap=1, scale=500, position/angle resolved, 0 crashes.  Combat un-
regressed (TARGET DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK.
Permanent gated diagnostic: BT_MAP_LOG traces the phase/draw pipeline.  Contacts
show 0 (the spatial-query entity classification is a follow-up); DuckState now
resolves for the duck button too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-06 21:16:29 -05:00
co-authored by Claude Opus 4.8
parent eae631ba04
commit 48ed3a8b73
5 changed files with 181 additions and 7 deletions
+2
View File
@@ -66,6 +66,7 @@
#endif
#if !defined(BTL4GAUG_HPP)
# include <btl4gaug.hpp> // the BT gauge classes (Compass, etc.)
# include <btl4rdr.hpp> // MapDisplay (the "map" radar gauge)
#endif
#if !defined(L4LAMP_HPP)
# include <l4lamp.hpp>
@@ -137,6 +138,7 @@ MethodDescription
&VertTwoPartBar::methodDescription, // "vertBar" -- vertical fill bar (coolant/heat)
&SegmentArcRatio::methodDescription, // "segmentArcRatio" -- segmented arc dial (speed)
&OneOfSeveralPixInt::methodDescription, // "oneOfSeveralPixInt" -- button-state lamp (duck/light/mode)
&MapDisplay::methodDescription, // "map" -- the radar / tactical display
&BTL4ChainToPrevious
};
+109 -5
View File
@@ -212,9 +212,11 @@ void
)
{
//
// The operator's own mech (exclude_entity) draws no name.
// The operator's own mech (exclude_entity) draws no name. A NULL entity
// (the binary's Verify(False) count/iterator-mismatch path, which compiles
// out at DEBUG_LEVEL 0) would crash EntityPosition(NULL) below -- guard it.
//
if (entity == exclude_entity)
if (entity == NULL || entity == exclude_entity)
{
nameBitmap = NULL; // param_1[6] = 0
return;
@@ -290,6 +292,77 @@ void
//###########################################################################
//###########################################################################
//#############################################################################
// Method Description + Make -- the config "map" primitive registration glue.
//
// NOT in the assert-anchored decomp (the ctor @004c1c24 has no exported caller
// and the "map" string was not captured), so this is reconstructed from the
// config invocation (L4GAUGE.CFG:4923) + the ctor signature. The offset_position
// keyword "center"/"bottom" is typed as a STRING and converted in Make, so it
// needs no ModeManager named-constant registration.
//
MethodDescription
MapDisplay::methodDescription =
{
"map",
MapDisplay::Make,
{
// map( rate, mode, (w,h), offsetPos, viewDeg, bg,static,hotbox,
// maxRange, scaleAttr, positionAttr, angleAttr, capabilitiesAttr )
{ ParameterDescription::typeRate, NULL }, // rate ID
{ ParameterDescription::typeModeMask, NULL }, // mode mask
{ ParameterDescription::typeVector, NULL }, // (width,height)
{ ParameterDescription::typeString, NULL }, // offset position ("center"/"bottom")
{ ParameterDescription::typeInteger, NULL }, // view width (degrees)
{ ParameterDescription::typeColor, NULL }, // background colour
{ ParameterDescription::typeColor, NULL }, // static/default colour
{ ParameterDescription::typeColor, NULL }, // hotbox colour
{ ParameterDescription::typeScalar, NULL }, // maximum range (metres)
{ ParameterDescription::typeAttribute, NULL }, // scale (RadarRange)
{ ParameterDescription::typeAttribute, NULL }, // position (RadarLinearPosition, Point3D*)
{ ParameterDescription::typeAttribute, NULL }, // angle (RadarAngularPosition, Quaternion*)
{ ParameterDescription::typeAttribute, NULL }, // capabilities (Avionics/RadarPercent)
PARAMETER_DESCRIPTION_END
}
};
Logical
MapDisplay::Make(
int display_port_index,
Vector2DOf<int> position,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
OffsetPosition offset =
(strcmp(p[3].data.string, "bottom") == 0) ? bottom : center;
MapDisplay *gauge = (MapDisplay *)operator new(sizeof(MapDisplay));
if (gauge != NULL)
{
new (gauge) MapDisplay(
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
0, // owner_ID
display_port_index,
position.x, position.y, // left, bottom
position.x + p[2].data.vector.x, // right
position.y + p[2].data.vector.y, // top
offset,
p[4].data.integer, // view width (degrees)
p[5].data.color, p[6].data.color, p[7].data.color, // bg, static, hotbox
p[8].data.scalar, // maximum range
entity,
(Scalar *)p[9].data.attributePointer, // scale (RadarRange)
(Point3D **)p[10].data.attributePointer, // position (RadarLinearPosition)
(Quaternion **)p[11].data.attributePointer, // angle (RadarAngularPosition)
(Scalar *)p[12].data.attributePointer); // capabilities (Avionics/RadarPercent)
}
return True;
}
//#############################################################################
// Construction / Destruction
//
@@ -451,15 +524,19 @@ void
MapDisplay::Execute()
{
int phase = operatingPhase++; // this+0x354
const int mlog = getenv("BT_MAP_LOG") ? 1 : 0;
if (phase == 0)
{
operating = (OperatingEpsilon < currentCapabilitiesRatio); // 0.0f < ratio
if (mlog) DEBUG_STREAM << "[map] p0 operating=" << (int)operating
<< " cap=" << currentCapabilitiesRatio << " scale=" << currentScale << "\n" << std::flush;
if (operating)
{
maximumDistanceSquared = currentCapabilitiesRatio * maximumRange;
maximumDistanceSquared = maximumDistanceSquared * maximumDistanceSquared;
CalculateBounds(); // FUN_004c2178
if (mlog) DEBUG_STREAM << "[map] p0 bounds ok\n" << std::flush;
}
}
else if (phase == 1)
@@ -470,6 +547,7 @@ void
renderer->GetStaticEntitiesWithinBounds( // FUN_00443ba4(renderer+0xB0)
&staticEntityList,
xMin, yMin, zMin, xMax, yMax, zMax);
if (mlog) DEBUG_STREAM << "[map] p1 static gathered\n" << std::flush;
}
}
else if (phase == 2)
@@ -480,6 +558,7 @@ void
renderer->GetMovingEntitiesWithinBounds( // FUN_0044366c(renderer+0x94)
&movingEntityList,
xMin, yMin, zMin, xMax, yMax, zMax);
if (mlog) DEBUG_STREAM << "[map] p2 moving gathered\n" << std::flush;
}
}
else
@@ -491,6 +570,17 @@ void
{
DrawDisplay(); // FUN_004c22c4
}
if (getenv("BT_MAP_LOG"))
{
static int s_c = 0;
if ((s_c++ % 15) == 0)
DEBUG_STREAM << "[map] operating=" << (int)operating
<< " scale=" << currentScale
<< " cap=" << currentCapabilitiesRatio
<< " posPtr=" << (currentPositionPointer ? 1 : 0)
<< " anglePtr=" << (currentAngularPointer ? 1 : 0)
<< "\n" << std::flush;
}
}
operatingPhase = 0;
}
@@ -613,10 +703,16 @@ void
flashState = !flashState; // this+0x374 ^= 1
localView.AttachRecorder(&previousDrawing); // (this+0x48 vtbl+0x64)
const int mlog = getenv("BT_MAP_LOG") ? 1 : 0;
if (mlog) DEBUG_STREAM << "[map] draw: wedge...\n" << std::flush;
DrawViewWedge(); // FUN_004c2484
if (mlog) DEBUG_STREAM << "[map] draw: static...\n" << std::flush;
DrawStatic(worldToView); // FUN_004c25b4
if (mlog) DEBUG_STREAM << "[map] draw: moving...\n" << std::flush;
DrawMoving(worldToView); // FUN_004c2788
if (mlog) DEBUG_STREAM << "[map] draw: names...\n" << std::flush;
DrawNames(worldToView); // FUN_004c28c4
if (mlog) DEBUG_STREAM << "[map] draw: done\n" << std::flush;
localView.DetachRecorder(); // (this+0x48 vtbl+0x68)
}
@@ -789,6 +885,8 @@ void
iter.First();
int count = iter.GetSize(); // vtbl+0x14
if (getenv("BT_MAP_LOG"))
DEBUG_STREAM << "[map] names count=" << count << "\n" << std::flush;
if (count < 1)
{
return;
@@ -860,12 +958,18 @@ Pip *
//
// FUN_00417ab4 -- the gauge renderer's "operator entity" (the mech whose
// cockpit this gauge belongs to). Not exposed by the WinTesla GaugeRenderer.
// cockpit this gauge belongs to). The renderer's linked-entity socket may be
// unwired on WinTesla, so fall back to the application viewpoint entity (the mech
// we are driving) -- the same fallback HeadingPointer::Execute uses. Returning
// NULL crashes the map's CalculateBounds/DrawDisplay (EntityPosition(NULL)).
//
static Entity *
ResolveOperatorEntity(GaugeRenderer * /*renderer*/)
ResolveOperatorEntity(GaugeRenderer *renderer)
{
return NULL;
Entity *e = renderer ? ((L4GaugeRenderer *)renderer)->GetLinkedEntity() : NULL;
if (e == NULL && application != NULL)
e = (Entity *)application->GetViewpointEntity();
return e;
}
//
+30 -1
View File
@@ -426,7 +426,30 @@ const Mech::IndexEntry
ATTRIBUTE_ENTRY(Mech, FootStep, attrPad), // 0x1e
ATTRIBUTE_ENTRY(Mech, AnimationState, attrPad), // 0x1f
ATTRIBUTE_ENTRY(Mech, ReplicantAnimationState, attrPad), // 0x20
ATTRIBUTE_ENTRY(Mech, LinearSpeed, linearSpeed) // 0x21 (live forward speed)
ATTRIBUTE_ENTRY(Mech, LinearSpeed, linearSpeed), // 0x21 (live forward speed)
ATTRIBUTE_ENTRY(Mech, ClimbRate, attrPad), // 0x22
ATTRIBUTE_ENTRY(Mech, AccelerationLastFrame, attrPad), // 0x23
ATTRIBUTE_ENTRY(Mech, AngularSpeed, attrPad), // 0x24
ATTRIBUTE_ENTRY(Mech, ArmorDamageLevel, attrPad), // 0x25
ATTRIBUTE_ENTRY(Mech, SubsystemDamageLevel, attrPad), // 0x26
ATTRIBUTE_ENTRY(Mech, MyomerDamageLevel, attrPad), // 0x27
ATTRIBUTE_ENTRY(Mech, TestButton1, attrPad), // 0x28
ATTRIBUTE_ENTRY(Mech, TestButton2, attrPad), // 0x29
ATTRIBUTE_ENTRY(Mech, TestButton3, attrPad), // 0x2a
ATTRIBUTE_ENTRY(Mech, TestButton4, attrPad), // 0x2b
ATTRIBUTE_ENTRY(Mech, TestButton5, attrPad), // 0x2c
ATTRIBUTE_ENTRY(Mech, TestButton6, attrPad), // 0x2d
ATTRIBUTE_ENTRY(Mech, ReduceButton, attrPad), // 0x2e
ATTRIBUTE_ENTRY(Mech, RadarRange, radarRange), // 0x2f (radar scale)
ATTRIBUTE_ENTRY(Mech, RadarLinearPosition, radarLinearPosition), // 0x30 (Point3D* -> localOrigin)
ATTRIBUTE_ENTRY(Mech, RadarAngularPosition, radarAngularPosition),// 0x31 (Quaternion* -> localOrigin)
ATTRIBUTE_ENTRY(Mech, RearFiring, attrPad), // 0x32
ATTRIBUTE_ENTRY(Mech, RequestDuckAnimation, attrPad), // 0x33
ATTRIBUTE_ENTRY(Mech, UnstablePercentage, attrPad), // 0x34
ATTRIBUTE_ENTRY(Mech, SuperStop, attrPad), // 0x35
ATTRIBUTE_ENTRY(Mech, IncomingLock, attrPad), // 0x36
ATTRIBUTE_ENTRY(Mech, DuckState, duckState), // 0x37 (crouch posture, int)
ATTRIBUTE_ENTRY(Mech, DistanceToMissile, attrPad) // 0x38
};
Mech::AttributeIndexSet&
@@ -572,6 +595,12 @@ Mech::Mech(
sensorSubsystem = 0; // this[0x1f7] (0xBBE) -- same hazard, zero it too
attrPad = 0.0f; // shared read pad for un-populated gauge attributes
linearSpeed = 0.0f; // live forward ground speed (LinearSpeed gauge); set per frame
radarRange = 500.0f; // radar display scale (SetTargetRange is stubbed;
// 500m default zoom so nearby contacts are visible,
// vs the config maximum_range=4000 outer detection edge)
radarLinearPosition = &localOrigin.linearPosition; // map reads the mech's live world position...
radarAngularPosition= &localOrigin.angularPosition; // ...and orientation (pointers into the base origin)
duckState = 0; // not crouching
ZeroBlock(this + 0xca, 6); // this[0xca..0xcf] = 0
ZeroBlock(this + 0x153, 6); // this[0x153..0x158] = 0
+8
View File
@@ -568,6 +568,14 @@ protected:
// forward ground speed the speed readout / arc / radar consume (binary @0x81c).
Scalar attrPad;
Scalar linearSpeed;
// Radar/map gauge attributes (binary @0x404/0x408/0x40c/0x3f8). The map
// widget reads position/angle as POINTERS into the mech's live origin, so
// radarLinearPosition/radarAngularPosition point at localOrigin.{linear,
// angular}Position (set in the ctor); radarRange is the radar scale.
Scalar radarRange; // 0x2f RadarRange (scale/max)
Point3D *radarLinearPosition; // 0x30 RadarLinearPosition
Quaternion *radarAngularPosition; // 0x31 RadarAngularPosition
int duckState; // 0x37 DuckState (crouch posture)
// --- raw / engine-shim members touched by the ctor slice -------------
void *vtable; // installed Mech vtable ptr
+32 -1
View File
@@ -103,8 +103,29 @@ Derivation
Receiver::MessageHandlerSet
Sensor::MessageHandlers;
//
// Attribute Support -- the gauge bindings for the Sensor (named "Avionics" in the
// cockpit config): Avionics/RadarPercent (the map's capabilities ratio), SelfTest,
// BadVoltage. SENSOR.HPP declares the enum + AttributePointers[]; the empty
// default-ctor AttributeIndex published NOTHING (not even SimulationState). Build
// it from the table chained to PoweredSubsystem::GetAttributeIndex() (which
// resolves to HeatSink's dense index) so the merged index stays dense (ids run
// contiguously from PoweredSubsystem::NextAttributeID).
//
const Sensor::IndexEntry
Sensor::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Sensor, RadarPercent, radarPercent), // @0x31C (map capabilities ratio)
ATTRIBUTE_ENTRY(Sensor, SelfTest, selfTest),
ATTRIBUTE_ENTRY(Sensor, BadVoltage, badVoltage)
};
Sensor::AttributeIndexSet
Sensor::AttributeIndex;
Sensor::AttributeIndex(
ELEMENTS(Sensor::AttributePointers),
Sensor::AttributePointers,
PoweredSubsystem::GetAttributeIndex()
);
Sensor::SharedData
Sensor::DefaultData(
@@ -256,6 +277,16 @@ void
// this[0x38] is the resolved thermal master sink; +0x158 == heatEnergy.
radarPercent = RadarBaseline - HeatMasterEnergy();
// ⚠ BRING-UP GUARD (heat-leaf branch not byte-exact): the inherited heatEnergy
// reads un-normalized/garbage here (~3850) so radarPercent goes hugely negative
// and the radar reports non-operational. radarPercent is authentically a [0,1]
// capability ratio, so when the heat term is out of range treat it as no penalty
// (full radar). A real overheat penalty needs the heat-energy normalization
// (deferred with the heat-leaf re-base). Exposed once the map gauge began
// reading Avionics/RadarPercent; inert before.
if (HeatMasterEnergy() < 0.0f || HeatMasterEnergy() > RadarBaseline)
radarPercent = RadarBaseline;
if (HeatModelOff()) // this[0x10] == 1
{
radarPercent = 0.0f;