gauges: reconstruct cmArmor -- the per-zone ARMOR DAMAGE schematic [widget recon 3]

Each armor zone on the cockpit schematic is now tinted by that zone's live
damage. Render-verified: the schematic shows an all-green Blackhawk (all zones
damageLevel=0 at spawn), replacing the static green+red it showed before cmArmor
was registered -- i.e. cmArmor now owns those zone colors and shows the real
undamaged state; damaged zones shift toward red.

Reuses the ColorMapper base (from cmHeat) + two pieces reconstructed from the
binary (Ghidra dropped the x87, recovered by disassembly):
- ArmorZoneConnection (@004c33a4/@004c3430): resolves one zone from the owner's
  inherited Entity::damageZones[zone_index]; per-frame feed = zone==NULL ? 100
  : Round(zone->damageLevel * 100) (the 0..1 damage ratio -> 0..100 percentage
  -> the colour index ColorMapper::Execute pushes into the palette slot).
- ColorMapperArmor::Make/ctor (@004c3aa4/@004c3b98): Make resolves the CFG zone
  name (dz_ltorso etc., the 6th param) to an index via Entity::GetDamageZoneIndex
  (== the binary's FUN_0042076c, scanning damageZones[] by name); the ctor wires
  the ArmorZoneConnection.

Decomp fact (corrects the cmHeat note): the ColorMapper base ctor takes NINE
args -- an owner_ID sits between renderer and graphics_port_number. Our 8-arg
ColorMapper::ColorMapper folds owner_ID=0 in (gauges have owner 0), so it is
equivalent; cmArmor's mapping matches cmHeat plus the zone name.

Verified: parses, builds, all dz_* zones resolve (0 "not found"), no /FORCE
unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes). The dynamic
red-on-damage needs the player to take damage (the passive spawn dummy never
hits back). 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:30:45 -05:00
co-authored by Claude Opus 4.8
parent e869b00181
commit 1d2ddd8399
4 changed files with 166 additions and 11 deletions
+26 -1
View File
@@ -282,7 +282,32 @@ the Ghidra pseudo-C for Execute@004c5914 + ctor@004c562c had the x87 endpoint ma
float computation Ghidra dropped by PE-parsing + capstone-disassembling the fn (`scratchpad/disas_hp.py`) —
read the `fild/fmul/fadd` stream + the `.data` float pool.**
**Next increments (priority order from the map):** `cmArmor`/`colorMapperMultiArmor` (armor schematic, needs the
**✅ Increment 3 — `cmArmor` (ColorMapperArmor) DONE — the ARMOR DAMAGE schematic, per-zone.** Each of the
mech's armor zones on the cockpit schematic is now tinted by that zone's LIVE damage. Render-verified: the
schematic shows the Blackhawk silhouette all-green (all zones `damageLevel`=0 at spawn) — which REPLACED the
static green+red the schematic showed before cmArmor was registered (i.e. cmArmor now owns those zone colors,
showing the real undamaged state; damaged zones shift toward red via the decomp-verified path). Reuses the
ColorMapper base (done in incr.1) + adds two pieces reconstructed from the binary + a disassembly of the x87
Ghidra dropped (`scratchpad/disas_hp.py`):
- **`ArmorZoneConnection`** (@004c33a4 ctor / @004c3430 Transfer): resolves ONE zone from the owner's
inherited `Entity::damageZones[zone_index]`; per-frame feed = `zone==NULL ? 100 : Round(zone->damageLevel
@0x158 * 100)` (the damage ratio 0..1 → 0..100 percentage → the colour index ColorMapper::Execute pushes).
- **`ColorMapperArmor::Make`/ctor** (@004c3aa4/@004c3b98): `Make` resolves the CFG zone NAME (`dz_ltorso` etc.,
the 6th param) to an index via `Entity::GetDamageZoneIndex(CString)` — which IS the binary's `FUN_0042076c`
(scans `damageZones[]` matching each zone's name @0x15c); the ctor wires the ArmorZoneConnection.
- **DECOMP FACT (corrects the cmHeat note): the ColorMapper base ctor `FUN_004c37dc` takes NINE args** — there
is an `owner_ID` between `renderer` and `graphics_port_number` (`Gauge(rate,mode,renderer,owner_ID,id)` +
`colorSlot=param_7`, `graphicsPort=GetGraphicsPort(param_6)`). Our 8-arg `ColorMapper::ColorMapper` folds
`owner_ID=0` in (gauges have owner 0), so it's equivalent — cmArmor's mapping matches cmHeat (gpn=port,
colorSlot=p[2], palettes=p[3]/p[4]) plus the zone name (p[5]). Verified: parses, builds, **all `dz_*` zones
resolve (0 "not found")**, no `/FORCE` unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes). NOTE:
the dynamic red-on-damage needs the PLAYER to take damage — the passive `BT_SPAWN_ENEMY` dummy never hits
back, so a live demo of a zone reddening needs a player-damage path (real MP combat, or a test hook).
⚠ The sibling `colorMapperMultiArmor` (worst-of-8-zones, MultiArmorConnection @004c346c) + `cmCrit`
(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
`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
+136 -10
View File
@@ -178,6 +178,46 @@ void
}
//
// ArmorZoneConnection (@004c33a4 ctor / @004c3430 Transfer) -- drives a
// ColorMapper's colour index from ONE damage zone's live damage ratio. The ctor
// resolves the zone from the owner's inherited Entity::damageZones[zone_index]
// (raw *(entity+0x120)[idx]); Transfer feeds it each frame. Used by ColorMapperArmor.
//
class ArmorZoneConnection : public GaugeConnection
{
public:
ArmorZoneConnection(int *destination, Entity *entity, int zone_index)
: GaugeConnection(0), // FUN_00444124(this,0)
zone((zone_index < 0) ? NULL : entity->damageZones[zone_index]),
currentColorIndex(destination)
{}
protected:
//
// Per-frame feed (@004c3430): no zone -> 100 (fail-safe full tint); else the
// zone's damage RATIO (damageLevel @0x158, 0..1) scaled to a 0..100 percentage
// and rounded -- ColorMapper::Execute clamps it + pushes the palette slot,
// recolouring that zone on the cockpit ARMOR DAMAGE schematic.
//
void Update(); // override
DamageZone *zone; // source@0x10
int *currentColorIndex; // destination@0x14
};
void
ArmorZoneConnection::Update()
{
if (zone == NULL)
{
*currentColorIndex = 100;
return;
}
*currentColorIndex = HeatRound(zone->damageLevel * 100.0f); // zone+0x158 * 100
}
//###########################################################################
//###########################################################################
// File-private GaugeConnection subclasses
@@ -377,19 +417,105 @@ void
// ColorMapperArmor @004c3aa4 Make / @004c3b98 ctor
//###########################################################################
//
// @004c3aa4 -- Make: build the damage-zone descriptor (DAT_00514414), look up
// the zone index on the host subsystem (FUN_0042076c; warns "damage zone ...
// not found" on failure), allocate (0x70) and construct a ColorMapperArmor
// wired with an ArmorZoneConnection.
// Tints ONE damage zone's colour on the cockpit ARMOR DAMAGE schematic by that
// zone's live damage. CFG shape (L4GAUGE.CFG:4789, inside the colorMapArmor
// macro): cmArmor( rate, mode, colourSlot, paletteA, paletteB, zoneName )
// e.g. cmArmor(H, ModeSecondaryDamage, 32, adpal.pcc, adpal2.pcc, dz_ltorso);
//
// @004c3b98 -- ctor: ColorMapper base (vtable PTR_FUN_00518dcc), this[0x1B]=0,
// then AddConnection(new ArmorZoneConnection(&currentColorIndex, subsystem,
// zone_index)) (connection ctor FUN_004c33a4, 0x18 bytes).
MethodDescription
ColorMapperArmor::methodDescription =
{
"cmArmor",
ColorMapperArmor::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 }, // damage-zone name (e.g. "dz_ltorso")
PARAMETER_DESCRIPTION_END
}
};
//
// ArmorZoneConnection::Transfer @004c3430:
// if (source == 0) *destination = 100;
// else *destination = Round(<zone damage %>);
// @004c3aa4 -- Make. Resolve the named damage zone to an index on the entity,
// then construct. (The binary built a transient zone-name descriptor and called
// FUN_0042076c; Entity::GetDamageZoneIndex IS that lookup -- it scans
// damageZones[] matching each zone's name.)
//
Logical
ColorMapperArmor::Make(
int display_port_index,
Vector2DOf<int> /*position*/,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
int zone_index = entity->GetDamageZoneIndex(CString(p[5].data.string)); // FUN_0042076c
if (zone_index < 0)
{
DebugStream << "ColorMapperArmor warning: damage zone "
<< p[5].data.string << " not found\n";
}
ColorMapperArmor *gauge = (ColorMapperArmor *)operator new(0x70); // FUN_00402298(0x70)
if (gauge != NULL)
{
new (gauge) ColorMapperArmor( // FUN_004c3b98
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
display_port_index, // graphics port number (the runtime port)
p[2].data.color, // colour slot
entity,
p[3].data.string, // palette A
p[4].data.string, // palette B
zone_index,
"ColorMapperArmor");
}
return True;
}
//
// @004c3b98 -- ctor: ColorMapper base + wire an ArmorZoneConnection feeding the
// resolved damage zone's live damage to the colour index.
//
ColorMapperArmor::ColorMapperArmor(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer,
int graphics_port_number,
int color_index,
Entity *entity,
const char *palette_a,
const char *palette_b,
int damage_zone_index,
const char *identification_string
):
ColorMapper( // FUN_004c37dc (owner_ID 0 folded in, as for cmHeat)
rate, mode_mask, renderer, graphics_port_number,
color_index, palette_a, palette_b, identification_string)
{
unused = 0; // this[0x1B] @0x6C
ArmorZoneConnection *connection =
(ArmorZoneConnection *)operator new(0x18); // FUN_004c33a4 (0x18 bytes)
if (connection != NULL)
new (connection) ArmorZoneConnection(&currentColorIndex, entity, damage_zone_index);
AddConnection(connection); // (*this+0x34)
}
//
// ColorMapperArmor destructor. No extra work: the ArmorZoneConnection is
// released by the base Gauge teardown, the palettes by ~ColorMapper -- the
// base-dtor chain runs implicitly at the closing brace.
//
ColorMapperArmor::~ColorMapperArmor()
{
}
//###########################################################################
// ColorMapperMultiArmor @004c3c48 Make / @004c3d60 ctor
+3
View File
@@ -140,6 +140,9 @@
public ColorMapper
{
public:
// Registered in BTL4MethodDescription[] (btl4grnd.cpp) as "cmArmor".
static MethodDescription methodDescription;
static Logical Make( // @004c3aa4
int display_port_index, Vector2DOf<int> position,
Entity *entity, GaugeRenderer *gauge_renderer);
+1
View File
@@ -131,6 +131,7 @@ MethodDescription
//
&ColorMapperHeat::methodDescription, // "cmHeat" -- heat-driven palette tint
&HeadingPointer::methodDescription, // "headingPointer" -- compass needle + heading
&ColorMapperArmor::methodDescription, // "cmArmor" -- per-zone armor-damage tint
&BTL4ChainToPrevious
};