#118: the eject slot graphic lands -- BitMapInverseWipeScalar reconstructed

The operator's field report ("eject slot flashing but no graphic in it") was
the tracked-NULL bring-up stub in BallisticWeaponCluster. Reconstructed the
missing gauge class from the binary:

- BitMapInverseWipeScalar @004c61c8 (vtable 0x518a14; base @004c5e84,
  Execute @004c5fb8, BecameActive @004c5fa4): a bitmap COLUMN SWEEP whose
  level tracks a live Scalar -- two spans with INVERTED colour pairs,
  [0..level] fg colorA / bg colorB, remainder the inverse.
- Wired per the binary call site (part_014.c:2312): bteejtm.pcc at (0xF,0)
  on the weapon's eng port, colours 0/0xFF, watching the weapon's
  PercentOfEject (@0x3F8, the round-eject 0..1 progress) via a new
  complete-type bridge BTWeaponPercentOfEjectPtr (databinding rule).

At idle (level 0) the EJECT graphic now renders in the slot; during a
round-eject cycle it sweeps with the countdown. BTEEJTM.PCC ships in
content/GAUGE (the loose gauge bitmap set). Smoke-tested: constructs clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-03 17:53:33 -05:00
co-authored by Claude Fable 5
parent 611b1a8bbd
commit 751b1159b5
5 changed files with 156 additions and 5 deletions
+16 -5
View File
@@ -2020,11 +2020,22 @@ BallisticWeaponCluster::BallisticWeaponCluster(
destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&failedState, "TwoState"); // the destroyed X: bright when FAILED engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&failedState, "TwoState"); // the destroyed X: bright when FAILED
// ejectWipe (BitMapInverseWipeScalar @004c61c8, eject-timer wipe reading // ejectWipe (BitMapInverseWipeScalar @004c61c8): the round-EJECT progress
// subsys+0x3f8). BRING-UP: the BitMapInverseWipeScalar class is not yet // sweep -- bteejtm.pcc revealed column-by-column as the weapon's
// declared/reconstructed in btl4gaug (only the non-Scalar BitMapInverseWipe); // PercentOfEject (@0x3F8) runs 0..1. Binary call (part_014.c:2312):
// tracked NULL until it lands. // FUN_004c61c8(., rate, eng_mode, renderer, ownerID, engPort, 0xF, 0,
ejectWipe = NULL; // @0x44 // "bteejtm.pcc", 0, 0xFF, weapon+0x3f8, name). Reconstructed 2026-08-03
// (#118 field report: the eject slot flashed EMPTY -- this was the
// tracked-NULL stub).
{
extern Scalar *BTWeaponPercentOfEjectPtr(void *weapon);
Scalar *ejectPct = BTWeaponPercentOfEjectPtr(subsystem_in);
ejectWipe = (ejectPct != NULL)
? new BitMapInverseWipeScalar(ChildRate(), eng_mode, renderer_in,
owner_ID, engPort, 0xF, 0, "bteejtm.pcc", 0, 0xFF,
ejectPct, "BitMapInverseWipeScalar")
: NULL; // @0x44
}
} }
BallisticWeaponCluster::~BallisticWeaponCluster() BallisticWeaponCluster::~BallisticWeaponCluster()
+95
View File
@@ -2152,6 +2152,101 @@ void BitMapInverseWipe::Execute()
} }
//###########################################################################
// BitMapInverseWipeScalar @004c61c8 ctor / @004c5fb8 Execute
//###########################################################################
//
// The weapon-eng round-EJECT progress sweep (bteejtm.pcc <- PercentOfEject).
// Base ctor @004c5e84 interns the image and captures colours + the bitmap
// geometry; the @004c61c8 tail adds the Scalar value-watcher (FUN_00474855
// into this+0xA8) -- our GaugeConnectionDirectOf<Scalar> is that watcher.
//
BitMapInverseWipeScalar::BitMapInverseWipeScalar(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
int owner_ID, int graphics_port_number, int x, int y, const char *image,
int color_a, int color_b, Scalar *value_pointer,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, owner_ID, graphics_port_number,
identification_string)
{
localView.SetOrigin(x, y);
imageName = new char[strlen(image) + 1];
strcpy(imageName, image);
colorA = color_a;
colorB = color_b;
value = 0.0f;
previousLevel = -1;
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
BitMap *bmp = warehouse->bitMapBin.Get(imageName);
if (bmp == NULL)
{
width = 0;
height = 0;
}
else
{
width = bmp->Data.Size.x;
height = bmp->Data.Size.y;
}
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &value, value_pointer));
}
BitMapInverseWipeScalar::~BitMapInverseWipeScalar()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(imageName);
delete[] imageName;
imageName = NULL;
}
Logical BitMapInverseWipeScalar::TestInstance() const { return GraphicGauge::TestInstance(); }
void BitMapInverseWipeScalar::BecameActive() // @004c5fa4
{
previousLevel = -1;
}
//
// @004c5fb8 -- Execute. level = round(value x width) clamped to [0, width]
// (the x87 form Ghidra drops -- fild width / fmul value, gotcha #19), and on
// a change draw the two column spans with INVERTED colour pairs: [0..level]
// fg colorA over bg colorB, [level+1..width] fg colorB over bg colorA.
//
void BitMapInverseWipeScalar::Execute()
{
Scalar scaled = value * (Scalar)width;
int level = (int)(scaled + (scaled < 0.0f ? -0.5f : 0.5f)); // FUN_004dcd94 round
if (level < 0) level = 0;
if (level > width) level = width;
if (level != previousLevel)
{
previousLevel = level;
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
BitMap *bmp = warehouse->bitMapBin.Get(imageName);
if (bmp != NULL)
{
int split = level;
if (split != 0)
{
localView.MoveToAbsolute(0, 0);
localView.SetColor(colorA);
localView.DrawBitMap(colorB, bmp, 0, 0, split, height);
++split; // the binary's iVar2++
}
if (split < width)
{
localView.MoveToAbsolute(split, 0);
localView.SetColor(colorB);
localView.DrawBitMap(colorA, bmp, split, 0, width, height);
}
warehouse->bitMapBin.Release(imageName);
}
}
}
//########################################################################### //###########################################################################
//########################################################################### //###########################################################################
// HeadingPointer @004c554c Make / @004c562c ctor // HeadingPointer @004c554c Make / @004c562c ctor
+33
View File
@@ -541,6 +541,39 @@
Scalar value; // @0xB4 this[0x2D] (connection) Scalar value; // @0xB4 this[0x2D] (connection)
}; };
//#######################################################################
// BitMapInverseWipeScalar @004c61c8 (vtable 0x518a14; base ctor @004c5e84,
// Execute @004c5fb8, BecameActive @004c5fa4). A bitmap COLUMN SWEEP whose
// level tracks a live Scalar 0..1 (the ctor's trailing Scalar* rides a
// value-watcher into this+0xA8): columns [0..level] draw fg colorA over
// bg colorB, the remainder draws the INVERSE -- the weapon-engineering
// panel's round-EJECT progress graphic (bteejtm.pcc, watching the
// weapon's PercentOfEject @0x3F8). Reconstructed 2026-08-03 (#118: the
// operator saw the eject slot flash with NO graphic -- this was the
// tracked-NULL bring-up stub in BallisticWeaponCluster).
//#######################################################################
class BitMapInverseWipeScalar :
public GraphicGauge
{
public:
BitMapInverseWipeScalar( // @004c61c8
GaugeRate, ModeMask, L4GaugeRenderer *, int owner_ID,
int graphics_port_number, int x, int y, const char *image,
int color_a, int color_b, Scalar *value_pointer, const char *);
~BitMapInverseWipeScalar();
Logical TestInstance() const;
void BecameActive(); // @004c5fa4 (previousLevel = -1)
void Execute(); // @004c5fb8 (two-span inverse sweep)
protected:
char *imageName; // @0x90
int colorA; // @0x94
int colorB; // @0x98
int width; // @0x9C bitmap width (columns)
int height; // @0xA0 bitmap height
int previousLevel; // @0xA4
Scalar value; // @0xA8 (connection: the 0..1 progress)
};
//####################################################################### //#######################################################################
// SegmentArc gauges -- needle / dial over a 270-degree arc. Derived from // SegmentArc gauges -- needle / dial over a 270-degree arc. Derived from
// the MUNGA L4 arc primitives (FUN_004745e0 / FUN_00473f44). // the MUNGA L4 arc primitives (FUN_004745e0 / FUN_00473f44).
+11
View File
@@ -268,6 +268,17 @@ void *
? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0; ? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0;
} }
// Eject-wipe gauge bridge (#118): the BallisticWeaponCluster's
// BitMapInverseWipeScalar watches the weapon's PercentOfEject (@0x3F8, the
// binary passes param_8+0x3f8 straight into the @004c61c8 ctor). Complete-
// type TU per the databinding rule -- never raw-offset a compiled object.
Scalar *
BTWeaponPercentOfEjectPtr(void *weapon)
{
return (weapon != 0)
? &((ProjectileWeapon *)weapon)->percentOfEject : 0;
}
//############################################################################# //#############################################################################
// Construction / Destruction // Construction / Destruction
+1
View File
@@ -360,6 +360,7 @@ class NotationFile;
AmmoBinConnection AmmoBinConnection
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix) friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix)
friend Scalar *BTWeaponPercentOfEjectPtr(void *weapon); // eject-wipe gauge bridge (#118)
// (ctor @004bcbb0, dtor @004bcbcf, link vtable // (ctor @004bcbb0, dtor @004bcbcf, link vtable
// @00512424; resolved via the resource AmmoBin // @00512424; resolved via the resource AmmoBin
// index at +0x1C0) -- LAST field, object ends 0x448 // index at +0x1C0) -- LAST field, object ends 0x448