gauges: colorMapperMultiArmor (worst-of-8-zones) + fix the interpreterTable overflow [widget recon 4]

Reconstructs colorMapperMultiArmor -- tints one schematic colour (a whole torso
section) by the WORST of up to 8 damage zones:
- MultiArmorConnection (@004c346c/@004c34f4): scans 8 zones via
  entity->damageZones[idx], keeps the max damageLevel, count==0 ? 100 :
  Round(worst*100).
- ColorMapperMultiArmor::Make/ctor (@004c3c48/@004c3d60): resolves 8 named
  zones to indices via Entity::GetDamageZoneIndex (13-param methodDescription).

ALSO fixes a latent HEAP CORRUPTION that registering this widget exposed (and
that would have blocked every further widget): GaugeInterpreter's bytecode
buffer is a fixed interpreterTableSize=46864 (GAUGREND.h) tuned for RP's config,
and its Insert() bounds guard is a Verify() that compiles out at DEBUG_LEVEL 0.
As each BT gauge widget is registered, more of BT's larger L4GAUGE.CFG resolves
into bytecode (vs being parse-skipped); colorMapperMultiArmor's 13 params x
hundreds of calls pushed the total past 46864 -> silent overflow past the char[]
-> heap smash detected later in mission bitmap loading (not the gauge code).
Fix: interpreterTableSize -> 262144 (sized for BT's full config).

Verified: parses, builds, all zones resolve, no /FORCE unresolved, combat
un-regressed (TARGET DESTROYED, 0 crashes), stable. Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-06 16:56:13 -05:00
co-authored by Claude Opus 4.8
parent 1d2ddd8399
commit fb8c1d6ace
5 changed files with 191 additions and 11 deletions
+23 -1
View File
@@ -307,7 +307,29 @@ Ghidra dropped (`scratchpad/disas_hp.py`):
(ColorMapperCritical, CriticalConnection @004c3598) are the same shape — easy follow-ups now the pattern
+ the ColorMapper base facts are pinned.
**Next increments (priority order from the map):** `colorMapperMultiArmor`/`cmCrit` (same ColorMapper pattern), then the
**✅ Increment 4 — `colorMapperMultiArmor` DONE — worst-of-N-zones tint.** Tints one schematic colour (a whole
torso SECTION) by the worst of up to 8 damage zones. Reconstructed `MultiArmorConnection` (@004c346c ctor /
@004c34f4 Transfer: scan 8 zones via `entity->damageZones[idx]`, keep max `damageLevel`, `count==0 ? 100 :
Round(worst*100)`) + `ColorMapperMultiArmor::Make`/ctor (@004c3c48/@004c3d60 — resolves 8 zone names to
indices via `GetDamageZoneIndex`, 13-param methodDescription). Verified: parses, builds, all zones resolve,
combat un-regressed (TARGET DESTROYED, 0 crashes).
**⭐⭐ THE `interpreterTable` OVERFLOW — a latent heap-corruption fixed (every future widget needed this).**
Registering colorMapperMultiArmor CRASHED — but not in the gauge code: a heap corruption surfaced later in
mission bitmap loading (`ObjectNameList::AddEntry` → malloc AV). ROOT CAUSE: `GaugeInterpreter`'s bytecode
buffer is a FIXED `interpreterTableSize = 46864` (GAUGREND.h) tuned for RP's config, and its `Insert()` bounds
guard is a `Verify()` that COMPILES OUT at DEBUG_LEVEL 0 (the same dead-`Verify` class as the BNDGBOX/heat
bugs). As each BT gauge widget is registered, more of BT's (larger) L4GAUGE.CFG resolves into bytecode instead
of being parse-skipped; cmHeat+headingPointer+cmArmor got close, colorMapperMultiArmor (13 params × ~hundreds
of calls) tipped it past 46864 → silent overflow past the `char[]` → heap smash detected at the next big alloc.
FIX: `interpreterTableSize` → 262144 (sized for BT's full config). ⚠ This was going to block EVERY further
widget — the table is cumulative across all registered gauges. **LESSON (add to the checklist): a heap
corruption that surfaces in an INNOCENT later alloc, right after registering a gauge, is the interpreterTable
overflow — the Insert bounds-`Verify` is dead at DEBUG_LEVEL 0.**
**Next increments (priority order from the map):** `cmCrit` (ColorMapperCritical -- needs the subsystem
`damageZone@0xE0` shadow resolved; CriticalConnection = state==1?100:Round(subsys->damageZone->damageLevel*100)),
then the
`ArmorZoneConnection`/`MultiArmorConnection` classes reconstructed); then the attribute-table wave (`vertBar`/
`segmentArcRatio` speed/`GeneratorCluster` — need `AttributePointers[]` on Mech/HeatableSubsystem, a separate
pass); the XL items (`map`/`vehicleSubSystems`/`PlayerStatus`) last. The POD path needs the FULL 23-entry
+8 -1
View File
@@ -206,7 +206,14 @@ protected:
enum
{
interpreterTableSize = 46864
// The original (RP) value 46864 is too small for BattleTech's larger
// gauge config: as more BT gauge widgets are registered, more of
// L4GAUGE.CFG resolves into interpreter bytecode instead of being
// parse-skipped, overflowing the fixed table. The Insert() bounds guard
// is a Verify() that compiles out at DEBUG_LEVEL 0, so the overflow was
// a SILENT heap corruption (detected later in mission bitmap loading).
// Sized generously for BT's full config. (was 46864)
interpreterTableSize = 262144
};
enum
+156 -9
View File
@@ -218,6 +218,59 @@ void
}
//
// MultiArmorConnection (@004c346c ctor / @004c34f4 Transfer) -- drives a
// ColorMapper's colour index from the WORST of up to 8 damage zones. The ctor
// copies the 8 zone indices + the owner; Transfer scans them each frame and
// feeds the maximum damage ratio. Used by ColorMapperMultiArmor.
//
class MultiArmorConnection : public GaugeConnection
{
public:
MultiArmorConnection(int *destination, Entity *entity, const int *zone_indices)
: GaugeConnection(0), // FUN_00444124(this,0)
owner(entity),
currentColorIndex(destination)
{
for (int i = 0; i < 8; i++)
zoneIndex[i] = zone_indices[i];
}
protected:
//
// Per-frame feed (@004c34f4): scan the (up to 8) zones -- skipping index < 0
// ("unused") and absent zones -- keep the WORST (max) damageLevel (@0x158),
// scale to 0..100 and round; no zone present -> 100.
//
void Update(); // override
Entity *owner; // @0x10
int zoneIndex[8]; // @0x18..0x34
int *currentColorIndex; // @0x38
};
void
MultiArmorConnection::Update()
{
Scalar worst = 0.0f;
int count = 0;
for (int i = 0; i < 8; i++)
{
if (zoneIndex[i] >= 0)
{
DamageZone *zone = owner->damageZones[zoneIndex[i]];
if (zone != NULL)
{
++count;
if (worst < zone->damageLevel)
worst = zone->damageLevel;
}
}
}
*currentColorIndex = (count == 0) ? 100 : HeatRound(worst * 100.0f);
}
//###########################################################################
//###########################################################################
// File-private GaugeConnection subclasses
@@ -521,18 +574,112 @@ ColorMapperArmor::~ColorMapperArmor()
// ColorMapperMultiArmor @004c3c48 Make / @004c3d60 ctor
//###########################################################################
//
// @004c3c48 -- Make: probe an 8-entry table of damage-zone descriptors
// (DAT_00514704 stride 0x44); requires at least one to resolve (else warns
// "No Damage zones found"). Allocates (0x6c) and constructs.
// Tints one schematic colour by the WORST of up to 8 damage zones (e.g. a whole
// torso section). CFG shape (L4GAUGE.CFG:96):
// colorMapperMultiArmor( rate, mode, colourSlot, paletteA, paletteB,
// zone1, zone2, ... zone8 ) ("unused" for empty slots)
//
// @004c3d60 -- ctor: ColorMapper base (vtable PTR_FUN_00518d88) +
// AddConnection(new MultiArmorConnection(&currentColorIndex, subsystem,
// zoneIndices[8])) (connection ctor FUN_004c346c, 0x3c bytes).
MethodDescription
ColorMapperMultiArmor::methodDescription =
{
"colorMapperMultiArmor",
ColorMapperMultiArmor::Make,
{
{ ParameterDescription::typeRate, NULL }, // rate ID
{ ParameterDescription::typeModeMask, NULL }, // mode mask
{ ParameterDescription::typeColor, NULL }, // hardware colour slot
{ ParameterDescription::typeString, NULL }, // palette name A
{ ParameterDescription::typeString, NULL }, // palette name B
{ ParameterDescription::typeString, NULL }, // zone 1
{ ParameterDescription::typeString, NULL }, // zone 2
{ ParameterDescription::typeString, NULL }, // zone 3
{ ParameterDescription::typeString, NULL }, // zone 4
{ ParameterDescription::typeString, NULL }, // zone 5
{ ParameterDescription::typeString, NULL }, // zone 6
{ ParameterDescription::typeString, NULL }, // zone 7
{ ParameterDescription::typeString, NULL }, // zone 8
PARAMETER_DESCRIPTION_END
}
};
//
// MultiArmorConnection::Transfer @004c34f4:
// scan the 8 zones; keep the worst (max) damage ratio (zone+0x158);
// no zone present -> *destination = 100, else Round(worst).
// @004c3c48 -- Make. Resolve up to 8 named zones to indices; build if any
// resolves. (The binary flag test is `index != 0`, so a valid zone OR an
// "unused"/-1 slot both count as "present" -- i.e. it always builds; the
// Transfer simply skips index < 0.)
//
Logical
ColorMapperMultiArmor::Make(
int display_port_index,
Vector2DOf<int> /*position*/,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
int zone_indices[8];
Logical any = False;
for (int i = 0; i < 8; i++)
{
zone_indices[i] = entity->GetDamageZoneIndex(CString(p[5 + i].data.string)); // FUN_0042076c
if (zone_indices[i] != 0) // binary: iVar2 != 0 (quirk -- see note above)
any = True;
}
if (!any)
{
DebugStream << "colorMapperMultiArmor: No Damage zones found\n";
return False;
}
ColorMapperMultiArmor *gauge = (ColorMapperMultiArmor *)operator new(0x6c); // FUN_00402298(0x6c)
if (gauge != NULL)
{
new (gauge) ColorMapperMultiArmor( // FUN_004c3d60
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
display_port_index, // graphics port number
p[2].data.color, // colour slot
entity,
p[3].data.string, // palette A
p[4].data.string, // palette B
zone_indices,
"ColorMapperMultiArmor");
}
return True;
}
//
// @004c3d60 -- ctor: ColorMapper base + a MultiArmorConnection over the 8 zones.
//
ColorMapperMultiArmor::ColorMapperMultiArmor(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer,
int graphics_port_number,
int color_index,
Entity *entity,
const char *palette_a,
const char *palette_b,
const int *zone_indices,
const char *identification_string
):
ColorMapper( // FUN_004c37dc
rate, mode_mask, renderer, graphics_port_number,
color_index, palette_a, palette_b, identification_string)
{
MultiArmorConnection *connection =
(MultiArmorConnection *)operator new(0x3c); // FUN_004c346c (0x3c bytes)
if (connection != NULL)
new (connection) MultiArmorConnection(&currentColorIndex, entity, zone_indices);
AddConnection(connection);
}
ColorMapperMultiArmor::~ColorMapperMultiArmor()
{
// base-dtor chain (~ColorMapper -> ~Gauge) releases the connection + palettes.
}
//###########################################################################
// ColorMapperCritical @004c3ddc Make / @004c3e40 ctor
+3
View File
@@ -165,6 +165,9 @@
public ColorMapper
{
public:
// Registered in BTL4MethodDescription[] (btl4grnd.cpp) as "colorMapperMultiArmor".
static MethodDescription methodDescription;
static Logical Make(int, Vector2DOf<int>, Entity *, GaugeRenderer *); // @004c3c48
ColorMapperMultiArmor( // @004c3d60
GaugeRate, ModeMask, L4GaugeRenderer *, int, int,
+1
View File
@@ -132,6 +132,7 @@ MethodDescription
&ColorMapperHeat::methodDescription, // "cmHeat" -- heat-driven palette tint
&HeadingPointer::methodDescription, // "headingPointer" -- compass needle + heading
&ColorMapperArmor::methodDescription, // "cmArmor" -- per-zone armor-damage tint
&ColorMapperMultiArmor::methodDescription, // "colorMapperMultiArmor" -- worst-of-8-zones tint
&BTL4ChainToPrevious
};