diff --git a/docs/GAUGE_COMPOSITE.md b/docs/GAUGE_COMPOSITE.md index 685c5de..7934093 100644 --- a/docs/GAUGE_COMPOSITE.md +++ b/docs/GAUGE_COMPOSITE.md @@ -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 diff --git a/engine/MUNGA/GAUGREND.h b/engine/MUNGA/GAUGREND.h index e433c37..f7ed925 100644 --- a/engine/MUNGA/GAUGREND.h +++ b/engine/MUNGA/GAUGREND.h @@ -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 diff --git a/game/reconstructed/btl4gaug.cpp b/game/reconstructed/btl4gaug.cpp index e2f4c94..806278d 100644 --- a/game/reconstructed/btl4gaug.cpp +++ b/game/reconstructed/btl4gaug.cpp @@ -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(¤tColorIndex, 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 /*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(¤tColorIndex, entity, zone_indices); + AddConnection(connection); +} + +ColorMapperMultiArmor::~ColorMapperMultiArmor() +{ + // base-dtor chain (~ColorMapper -> ~Gauge) releases the connection + palettes. +} //########################################################################### // ColorMapperCritical @004c3ddc Make / @004c3e40 ctor diff --git a/game/reconstructed/btl4gaug.hpp b/game/reconstructed/btl4gaug.hpp index ecf8fb0..9cd5ab9 100644 --- a/game/reconstructed/btl4gaug.hpp +++ b/game/reconstructed/btl4gaug.hpp @@ -165,6 +165,9 @@ public ColorMapper { public: + // Registered in BTL4MethodDescription[] (btl4grnd.cpp) as "colorMapperMultiArmor". + static MethodDescription methodDescription; + static Logical Make(int, Vector2DOf, Entity *, GaugeRenderer *); // @004c3c48 ColorMapperMultiArmor( // @004c3d60 GaugeRate, ModeMask, L4GaugeRenderer *, int, int, diff --git a/game/reconstructed/btl4grnd.cpp b/game/reconstructed/btl4grnd.cpp index 907f7f2..f4446bb 100644 --- a/game/reconstructed/btl4grnd.cpp +++ b/game/reconstructed/btl4grnd.cpp @@ -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 };