Files
BT411/game/reconstructed/btl4gau2.cpp
T
Joe DiPrimaandClaude Opus 5 e6103d02b2 #97: leak gauge reaches all THREE levels -- the normalisation was dropped with an x87 expression
Players: "you can get up to three traingles which drains really quickly" ...
"only seeing one level (lowest) right now" (Oracle), corroborated by Draco
("3 triple delta being the top level").

The display was never the problem: BitMapInverseWipe is already built with
frames=3 and stacks three segments at two levels each (0..6).  The LEVEL
computation was wrong.

Our Execute rounded the raw leak rate:
    level = round(value)
The binary's @004c5d08 does NOT.  Ghidra renders the round as a bare
`FUN_004dcd94()` because it drops the x87 expression feeding __ftol -- exactly
KB gotcha 19, which we already had written down.  The real prologue, disassembled:

    fild  dword ptr [ebp-0x14]    ; ST0 = (float)fullWidth   (frames*2 = 6)
    fmul  dword ptr [ebx+0xb4]    ; ST0 *= value             (CoolantMassLeakRate)
    fdiv  dword ptr [ebx+0xb0]    ; ST0 /= third             (full-scale divisor)
    call  0x4dcd94                ; level = round(ST0)

i.e. level = round(fullWidth * leakRate / fullScale).

Without the normalisation the gauge rounded a value that never exceeds ~1.0
(coolantDraw = zoneDamage * heatLoad, and heatLoad is clamp(0.002*T, 0, 1) --
constants re-verified from the image, including the 80-bit extended 0.002).  So
level could only ever be 0, or 1 via the >0.0025 floor: ONE triangle, always, no
matter how bad the leak.  Three of the six condensers being mis-lamped (#98) hid
how systematic this was.

Also corrected on the way:
  * `third` was declared int but the binary FDIVs it -- it is a float.  The two
    call sites passed *(int *)(subsystem+0x150), i.e. the BIT PATTERN of a float
    read from a RAW OFFSET into our own layout (the databinding trap): at +0x150
    our layout has filterDecay, not the binary's field.  Now resolved through a
    complete-type bridge, BTHeatSinkLeakFullScale.
  * filterDecay was initialised 0.4f; @004b8fec writes 0.15f (param_1[0x54] =
    0x3e19999a).  It is written-once-never-read elsewhere in the port, so the
    correction is behaviourally safe and it IS the gauge's full scale.

Verified live: Condenser6 at 10% zone damage -> draw 0.0358 -> level 1 (one
triangle); the same formula reaches all three at heavier damage, and the mapping
spans draw 0.0026 (half of one) to 0.15 (three full).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:54:13 -05:00

2292 lines
95 KiB
C++

//===========================================================================//
// File: btl4gau2.cpp //
// Project: BattleTech Brick: Gauge Renderer Manager //
// Contents: Cockpit instrument library, part 2 -- composite panel gauges //
// (see btl4gau2.hpp). //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// 02/22/96 CPB Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1996, Virtual World Entertainment, Inc. All rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BTL4OPT.EXE). Behaviour follows the
// Ghidra pseudo-C in recovered/all/part_014.c (@004c6798..@004c9b50). Class /
// member names taken from the CODE name-string pool; per-method @ADDR evidence
// is cited inline. This TU links between btl4gaug.obj and btl4gau3.obj.
//
// Engine-internal helper map (consistent with btl4gaug.cpp / btl4rdr.cpp):
// FUN_00444818 GraphicGauge ctor FUN_00444870 GraphicGauge dtor
// FUN_004448ac GraphicGauge::TestInstance
// FUN_004440d4 Gauge-group ctor FUN_00443f7c Gauge-group dtor
// FUN_0044ae0c ChainOf<Gauge*> dtor (PrepEngrScreen child list)
// FUN_004442b8 default GaugeRate FUN_004442dc default ModeMask
// FUN_00444290 alt default rate/mask
// FUN_004700ac GaugeRenderer::InternName FUN_004022e8 FreeName
// FUN_00442aec/00442c12 BitMapCache AddRef/Release (renderer+0x4C, +4)
// FUN_00442d2b/00442e51 PixMapCache AddRef/Release (renderer+0x4C, +0x20)
// FUN_0041bfc0 Entity::FindAttributeIndex(name)
// FUN_0041f98c Entity::FindSubObject(name)
// FUN_00447f1c GaugeRenderer::InstallPort / FUN_00447f84 GetGraphicsPort
// FUN_00417ab4 SharedData::Resolve / operator Entity*
// FUN_0041a1a4 IsDerivedFrom(classDerivations) (0x50f4bc = PoweredSubsystem -- pinned #47 via @004b1208 = PoweredSubsystem::TestInstance; the old 'Generator' reading was wrong)
// FUN_0042076c Subsystem::FindDamageZone(record) -> index
// FUN_004dcd94 Round(Scalar) -> int
// FUN_004700bc NumericDisplay ctor FUN_0047018c ::dtor
// FUN_004703f4 NumericDisplay::Reset FUN_00470430 ::DrawAt FUN_00470758 ::Erase
// FUN_00470ec8 NumericDisplay::SetColors FUN_00470cfc NumericDisplayInteger ctor
// FUN_00470888 NumericDisplayScalar ctor FUN_00473c94 NumericDisplayScalarTwoState
// FUN_004739d8 TwoState ctor FUN_00471d00 BackgroundBitmap ctor
// FUN_00472ef0 BarGraph ctor FUN_00472fb4 BarGraph dtor
// FUN_00474855 GaugeConnectionDirectOf<Scalar>::ctor FUN_004749de <int>
// FUN_004c2ec4 DrawClippedBitmap (file-private, btl4gaug) FUN_004c2f88 -> 7
// FUN_004c3134/004c31ec/004c3288 shared GaugeConnection ctors (btl4gaug TU)
// FUN_004c4170 HorizTwoPartBar FUN_004c4724 VertTwoPartBar
// FUN_004c4d88 OneOfSeveral FUN_004c5148 OneOfSeveralInt
// FUN_004c5470 OneOfSeveralStates FUN_004c5b7c LeakGauge(BitMapInverseWipe)
// FUN_004c61c8 BitMapInverseWipeScalar FUN_004c6244 SegmentArc270
// FUN_004c3b98 ColorMapperArmor ctor (btl4gaug)
// FUN_0040385c Verify FUN_004dbb24 DebugStream<<char* FUN_004db78c <<int
// DAT_00524e20 DebugStream (warning channel) DAT_004efc94 the Application
//
// Recovered constant-pool values:
// 0x43660000=230.0f / 0x433b0000=187.0f SeekVoltageGraph x/y scale
// 0x461c3c00=9999.0f BecameActive "force redraw" sentinel
// 0x463b8000=12000.0f generator max voltage
// 0x42c80000=100.0f HeatSinkCluster numeric scale base
// _DAT_004c8df0 heat numeric scale _DAT_004c92e0 warning threshold
// _DAT_004c6bdc=1200.0f / _DAT_004c6be0=12000.0f seek-curve plot step/limit
// ld80 @004c6bd0 / @004c6d74 = 8.3333333333333e-05 (exactly 1/12000) --
// the voltage->y normaliser (y = v * (1/12000) * yScale)
// DAT_0051936c PrepEngr engineer-screen number (1..12)
//
// See btl4gaug.cpp: suppress heat.hpp's optional DebugStream so the shared
// mechrecon ReconStream definition is the only one active in this TU.
#define BT_DEBUGSTREAM_DEFINED
#include <bt.hpp>
// task #6 (ConfigMapGauge state loop): the LBE4 buttonGroup sampler.
#include <btl4app.hpp>
#include <btl4mode.hpp>
#include <l4ctrl.hpp>
#pragma hdrstop
#if !defined(BTL4GAU2_HPP)
# include <btl4gau2.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
static const Scalar SeekVoltageXScale = 230.0f; // 0x43660000
static const Scalar SeekVoltageYScale = 187.0f; // 0x433b0000
static const Scalar RedrawSentinel = 9999.0f; // 0x461c3c00
static const Scalar GeneratorMaxVoltage = 12000.0f; // 0x463b8000
static const Scalar HeatNumericBase = 100.0f; // 0x42c80000
// SeekVoltageGraph plot constants (capstone disasm of @004c6934, task/issue #11):
static const Scalar SeekCurveStep = 1200.0f; // _DAT_004c6bdc (10 segments)
static const Scalar SeekCurveLimit = 12000.0f; // _DAT_004c6be0 (y-axis full scale)
static const Scalar SeekCurveYNorm = (Scalar)(1.0 / 12000.0); // ld80 @004c6bd0/@004c6d74
// (DebugStream warning channel, DAT_00524e20, is provided by the shared
// reconstruction headers -- heat.hpp / mechrecon.hpp.)
//
// Unrecovered .data resource pool used by GeneratorCluster::Make. Concrete
// bytes were not recovered from the optimised image; typed placeholders so the
// resolve-and-construct path compiles. (BEST-EFFORT.)
//
static char DAT_00518f24[] = "GeneratorCluster"; // subsystem name to resolve
static int DAT_00518ee0 = 0; // GaugeRate
static char DAT_00518f68[] = ""; // background image name
//
// FUN_0041bfc0 == Entity::FindAttributeIndex(name) returns a POINTER into the
// subsystem (subsystem + attributeMemberOffset), or 0 on miss. The composite
// cluster gauges pass that pointer straight to their child gauges as the value
// source. The WinTesla engine exposes exactly this via Simulation::
// GetAttributePointer(const char*) (SIMULATE.h), which returns a member pointer
// or &Simulation::NullAttribute (always safe to deref) -- so we resolve through
// it. A null subsystem -> 0.
//
static void *AttributePointerOf(void *subsystem, const char *name)
{
if (subsystem == NULL)
return NULL;
void *p = ((Simulation *)subsystem)->GetAttributePointer(name); // FUN_0041bfc0
// GetAttributePointer returns &NullAttribute (not 0) on a miss; the composite
// ctors gate optional children on a 0 result, so normalise the miss to NULL.
if (p == (void *)&Simulation::NullAttribute)
return NULL;
return p;
}
//
// FUN_00417ab4 -- SubsystemConnection::Resolve: follow the plug's bound link to the
// resolved subsystem. The link is `*(plug+8)`; the resolved subsystem is
// `*(link+8)`. An unbound plug (link == 0) resolves to 0 -- the authentic
// behaviour when a subsystem has no attached voltage source / cooling master (the
// connection then reads its "no source" branch, *destination = 0).
//
static void *ResolveLink(void *plug)
{
if (plug == 0)
return 0;
void *link = *(void **)((char *)plug + 8);
if (link == 0)
return 0;
return *(void **)((char *)link + 8);
}
//
// Bridge (defined in powersub.cpp) that reads the engineering-screen assignment
// off the real PoweredSubsystem type (auxScreenNumber/Placement/Label) -- the
// reconstructed heat-leaf branch is not byte-exact, so the raw sub+0x1dc/0x1e0/
// 0x1e4 offsets read garbage; the named fields (populated by the PoweredSubsystem
// ctor from the resource) are correct. Returns False for non-PoweredSubsystem-
// derived subsystems (== the vehicleSubSystems type filter).
//
extern bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64);
// (FUN_0041bf44 GetAttributePointer-by-index helper removed with the task #7
// fix: its only caller was CoolingLoopConnection, which now resolves the
// cooling master through the named-member bridge BTCoolingLoopFrame -- reaching
// an attribute by RAW numeric index is exactly what broke when the audio
// attribute rows grew the chained tables.)
//
// FUN_0041a1a4 IsDerivedFrom(0x50f4bc == PoweredSubsystem's ClassDerivations).
// Gates the SubsystemCluster's generator stateLamp. Every subsystem that reaches
// a SubsystemCluster passed the vehicleSubSystems type filter (BTGetSubsystemAuxScreen
// -> the SAME IsDerivedFrom(PoweredSubsystem) check), so this is always True here;
// the real check is done in powersub.cpp via BTIsPoweredSubsystem for robustness.
//
extern bool BTIsPoweredSubsystem(Subsystem *sub);
static Logical IsGeneratorDerived(Subsystem *sub) { return BTIsPoweredSubsystem(sub) ? True : False; }
//###########################################################################
// File-private GaugeConnection classes for the composite-cluster lamps.
// Each is the reconstruction of one of the three shared connection ctors
// the sub-gauges attach (mirrors the btl4gaug.cpp connection pattern: derive
// GaugeConnection, store dst/src, override Update() = the vtable sampler).
//###########################################################################
//
// CoolingLoopConnection -- @004c3134 ctor / @004c31a0 sampler (vt 0x518ea8).
// When the source subsystem's coolant loop is available (coolantAvailable == 1,
// the binary's source+0x134 gate) it follows the linked cooling master (binary
// attr id 3 == "HeatSink" == linkedSinks) and shows its Condenser loop number
// (+0x1d4 == condenserNumber, the image-strip frame); otherwise 0.
// task #7 REGRESSION FIX (2026-07-17): the old raw GetAttributePointer(3) +
// *(+0x1d4) walk AV'd after the audio attribute-table rows shifted the chained
// ids (BT_DEV_GAUGES crash). The sampler now routes through a complete-type
// bridge (heat.cpp) reading NAMED members -- the databinding rule (gotcha 8).
//
extern int BTCoolingLoopFrame(void *subsystem);
class CoolingLoopConnection : public GaugeConnection
{
public:
CoolingLoopConnection(int *destination, void *source)
: GaugeConnection(0), // FUN_00444124(this,0)
source(source), destination(destination) {}
void Update() // @004c31a0
{
*destination = BTCoolingLoopFrame(source);
}
protected:
void *source; // @0x10 this[4]
int *destination; // @0x14 this[5]
};
//
// PowerSourceConnection -- @004c31ec ctor / @004c3258 sampler (vt 0x518e9c).
// The binary resolves the subsystem's InputVoltage link and reads
// resolved+0x1E0 == Generator::generatorNumber (1..4 -> the btpbus generator-
// letter frame A..D; 0 == OFF).
// DATABINDING FIX 2026-07-20 (gotcha 8, sibling of the CoolingLoop fix above):
// the binary's path was ResolveLink(AttributePointerOf(subsystem,"InputVoltage"))
// but the BT_DEV_GAUGES audio attribute rows shifted the chained attribute ids
// so AttributePointerOf no longer lands on voltageSource@0x1D0 -- the link always
// resolved to 0 and every generator lamp drew frame 0 ("OFF"). 'source' now
// carries the SUBSYSTEM directly and BTPowerSourceFrame (powersub.cpp) reads the
// NAMED voltageSource member via ResolveVoltageSource(), bypassing the attribute
// table.
//
extern int BTPowerSourceFrame(void *subsystem);
class PowerSourceConnection : public GaugeConnection
{
public:
PowerSourceConnection(int *destination, void *source)
: GaugeConnection(0), source(source), destination(destination) {}
void Update() // @004c3258
{
*destination = BTPowerSourceFrame(source);
}
protected:
void *source; // @0x10
int *destination; // @0x14
};
//
// GeneratorVoltageConnection -- @004c3288 ctor / @004c32f4 sampler (vt 0x518e90).
// The binary resolves the subsystem's InputVoltage link and copies its output
// voltage (resolved+0x1DC == Generator::outputVoltage); 0 if unresolved. Drives
// the eng-page generator-voltage bar (ScalarBarGauge) + the MyomerCluster
// seek-voltage graph.
// DATABINDING FIX 2026-07-21 (gotcha 8, sibling of the PowerSource/CoolingLoop
// fixes): the ResolveLink(AttributePointerOf(subsystem,"InputVoltage")) path is
// dead after the BT_DEV_GAUGES audio attribute rows shifted the chained ids, so
// the bar always read 0. 'source' now carries the SUBSYSTEM and
// BTGeneratorVoltage (powersub.cpp) reads the NAMED member via
// ResolveVoltageSource()->MeasuredVoltage(), bypassing the attribute table.
//
extern Scalar BTGeneratorVoltage(void *subsystem);
class GeneratorVoltageConnection : public GaugeConnection
{
public:
GeneratorVoltageConnection(Scalar *destination, void *source)
: GaugeConnection(0), source(source), destination(destination) {}
void Update() // @004c32f4
{
*destination = BTGeneratorVoltage(source);
}
protected:
void *source; // @0x10
Scalar *destination; // @0x14
};
//###########################################################################
//###########################################################################
// SeekVoltageGraph @004c6798
// (issue #11: fully reconstructed from the capstone disasm of
// @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/@004c6c6c --
// Ghidra had dropped every x87 argument; tools/disas2.py recovered them.)
//###########################################################################
//###########################################################################
//
// The subsystem's voltage-response sampler == the binary subsystem vtbl+0x3C
// (slot 15): Emitter @004bb42c = sqrt(seekPower(v)/2.0e8), Myomers @004b8f94 =
// sqrt(speed(v)*3.6/350). Reached through a complete-type dispatch bridge
// (emitter.cpp -> myomers.cpp) per the databinding rule (gotcha 8).
//
extern Scalar BTSeekVoltageSample(void *subsystem, Scalar voltage);
//
// The destroyed test == *(subsystem+0x40) == 1 (MechSubsystem::simulationState,
// 1 = Destroyed). Named-member bridge in powersub.cpp (databinding rule).
//
extern int BTSubsystemDestroyed(Subsystem *subsystem);
//
// @004c6798 -- ctor (vtable PTR_FUN_0051a1fc). GraphicGauge base, interns and
// ref-counts the "destroyed" bitmap, resolves the four SeekVoltage attribute
// POINTERS off the weapon subsystem (FUN_0041bfc0 returns subsystem+offset or
// 0), sets the view XOR draw op + the port extent (0x97,0x80,0x17d,0x13b) and
// stashes the 230x187 plot scale.
//
SeekVoltageGraph::SeekVoltageGraph(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer,
int graphics_port_number,
Entity *subsystem,
AttributeAccessor *seek_voltage_pointer,
const char *destroyed_image,
Logical draw_cursor,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer, /*owner*/0xFFFF,
graphics_port_number, identification_string) // FUN_00444818
{
// Original interned a copy of the name (FUN_004700ac) + AddRef'd it in the
// bitmap cache; retain the persistent resource string and AddRef the bitmap.
destroyedImage = (char *)destroyed_image;
renderer->warehousePointer->bitMapBin.Get(destroyedImage); // FUN_00442aec (AddRef)
// FUN_0041bfc0(subsystem, name) == the engine's name-keyed attribute-pointer
// resolve (subsystem+memberOffset, or 0 on a miss); the composite ctors gate
// the tick/cursor draws on a 0 result.
currentSeekIndexPtr = (int *)AttributePointerOf(subsystem, "CurrentSeekVoltageIndex"); // this[0x25]
minSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MinSeekVoltageIndex"); // this[0x26]
maxSeekIndexPtr = (int *)AttributePointerOf(subsystem, "MaxSeekVoltageIndex"); // this[0x27]
seekVoltageTablePtr = (Scalar *)AttributePointerOf(subsystem, "SeekVoltage"); // this[0x28]
liveVoltage = (Scalar *)seek_voltage_pointer; // this[0x29]
this->subsystem = (Subsystem *)subsystem; // this[0x2A]
// @004c6798: (vtbl+0x0C)(3) then (vtbl+0x08)(0x97,0x80,0x17d,0x13b) --
// SetOperation(Xor) + SetPositionWithinPort (GRAPH2D.h vtable order). The
// XOR op is what lets DrawTicks/DrawCursorBox erase by redrawing.
localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C Xor
localView.SetPositionWithinPort(0x97, 0x80, 0x17d, 0x13b); // vtbl+0x08
xScale = SeekVoltageXScale; // this[0x2C]
yScale = SeekVoltageYScale; // this[0x2D]
drawCursor = draw_cursor; // this[0x2F]
destroyedShown = 0; // this[0x2E]
// (binary leaves this[0x2B]/[0x30..0x32] uninitialised -- activation's
// BecameActive sentinel forces the first replot, which seeds them before
// any read. Seeded here for determinism.)
previousVoltage = RedrawSentinel; // this[0x2B]
cursorX = -1; // this[0x30]
cursorY = -1; // this[0x31]
tickIndexShown = -1; // this[0x32]
}
//
// @004c68ac -- dtor. Release the destroyed bitmap, GraphicGauge::~GraphicGauge.
//
SeekVoltageGraph::~SeekVoltageGraph()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(destroyedImage);
}
Logical SeekVoltageGraph::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004c6920 -- BecameActive: previousVoltage = 9999.0f (poison the cached
// response so the next Execute clears + replots the whole box -- THE top-box
// eraser on a page switch; the binary body writes ONLY this[0x2B]).
//
void SeekVoltageGraph::BecameActive()
{
previousVoltage = RedrawSentinel; // @0xAC force full redraw
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] BecameActive sub="
<< (subsystem ? subsystem->GetName() : "?") << "\n" << std::flush;
}
//
// @004c6be4 -- the full-box erase ("SetupClip" in the old notes; it is really
// a clear): SetOperation(Replace), SetColor(0), MoveToAbsolute(0,0),
// DrawFilledRectangleToAbsolute(1000,1000) -- clipped to the view = the whole
// (0x97,0x80)-(0x17d,0x13b) top data box painted black. NOTE: leaves the view
// in Replace mode (the polyline plot relies on that; Execute restores Xor).
//
void SeekVoltageGraph::ClearGraph()
{
localView.SetOperation((GraphicsDisplay::Operation)0); // vtbl+0x0C Replace
localView.SetColor(0); // vtbl+0x18
localView.MoveToAbsolute(0, 0); // vtbl+0x24
localView.DrawFilledRectangleToAbsolute(1000, 1000); // vtbl+0x48
}
//
// @004c6c30 -- the live cursor: an 8x8 filled square centred on
// (cursorX,cursorY), drawn in the view's current (Xor) op so a second call
// erases it. MoveToAbsolute(x-4,y-4) + DrawFilledRectangleToRelative(8,8).
//
void SeekVoltageGraph::DrawCursorBox()
{
localView.MoveToAbsolute(cursorX - 4, cursorY - 4); // vtbl+0x24
localView.DrawFilledRectangleToRelative(8, 8); // vtbl+0x4C
}
//
// @004c6c6c -- the per-gear tick marks (XOR). For each seek index i in
// [*min..*max]: V = seekVoltage[i]; x = Round(sample(V) * xScale);
// y = Round(V * (1/12000) * yScale) [ld80 @004c6d74]. The gear the highlight
// was last drawn at (this[0x32]) gets the full L (axis->point->axis); the rest
// get 10-pixel axis stubs. Called in pairs to move the highlight (XOR erase +
// redraw).
//
void SeekVoltageGraph::DrawTicks()
{
// (port guard: the binary derefs min/max unguarded -- its call sites only
// guarantee current+table non-null; all four always resolve on the
// emitter/myomer subsystems that own a graph.)
if (minSeekIndexPtr == 0 || maxSeekIndexPtr == 0)
return;
int last = *maxSeekIndexPtr; // this[0x27] deref
for (int i = *minSeekIndexPtr; i <= last; i++) // this[0x26] deref
{
Scalar gearVoltage = seekVoltageTablePtr[i]; // this[0x28][i]
int x = Round(BTSeekVoltageSample(subsystem, gearVoltage) * xScale); // (vtbl+0x3C)(V)*230
int y = Round(gearVoltage * SeekCurveYNorm * yScale); // V/12000*187
if (i == tickIndexShown) // this+0xC8
{
localView.MoveToAbsolute(0, y); // vtbl+0x24
localView.DrawLineToAbsolute(x, y); // vtbl+0x30
localView.DrawLineToAbsolute(x, 0);
}
else
{
localView.MoveToAbsolute(0, y);
localView.DrawLineToAbsolute(10, y);
localView.MoveToAbsolute(x, 0);
localView.DrawLineToAbsolute(x, 10);
}
}
}
//
// @004c6934 -- Execute (the seek-voltage response plot; full x87 recovery).
//
// DESTROYED branch (subsys simulationState==1): once, clear the box and draw
// the centred "destroyed" bitmap (edestryd.pcc) at
// ((Round(xScale)-w)/2, (Round(yScale)-h)/2), color 0xFF.
//
// LIVE branch:
// * on the destroyed->alive edge, call BecameActive (own vtbl+0xC == slot 3
// of PTR_0051a1fc = @004c6920) to poison the cache;
// * change-test: sample the response at 12000 V (0x463b8000) and compare with
// the cached this[0x2B]; on change (or the 9999 activation sentinel):
// CLEAR (@004c6be4 -- the ghost eraser), SetColor(0xFF), MoveTo(0,0), then
// the polyline v = 0,1200,...,10800 (step _DAT_004c6bdc=1200 while
// v < _DAT_004c6be0=12000): LineTo(Round(sample(v)*230),
// Round(v * [1/12000 * 187])), reset the cursor slots to -1, restore
// SetOperation(Xor), and draw the gear ticks at the current index;
// * tick maintenance: when the current seek index moved, XOR-erase the old
// highlight + redraw at the new (two DrawTicks calls);
// * cursor (draw_cursor pages only): XOR-erase the old box (if any), sample
// at the LIVE voltage (*this[0x29]) and draw the new 8x8 box at
// (Round(sample(V)*230), Round(V * [1/12000 * 187])).
//
void SeekVoltageGraph::Execute()
{
if (BTSubsystemDestroyed(subsystem)) // *(subsys+0x40) == 1
{
if (destroyedShown == 0)
{
destroyedShown = 1; // this[0x2E]
ClearGraph(); // @004c6be4
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
BitMap *image = warehouse->bitMapBin.Get(destroyedImage); // FUN_00442aec
if (image != NULL) // (port guard; binary derefs unchecked)
{
localView.MoveToAbsolute( // vtbl+0x24 centre the bitmap
(Round(xScale) - image->Data.Size.x) >> 1,
(Round(yScale) - image->Data.Size.y) >> 1);
localView.SetColor(0xff); // vtbl+0x18
localView.DrawBitMap(0, image, // vtbl+0x54
0, 0, image->Data.Size.x - 1, image->Data.Size.y - 1);
}
warehouse->bitMapBin.Release(destroyedImage); // FUN_00442c12
}
return;
}
if (destroyedShown != 0)
{
destroyedShown = 0;
BecameActive(); // own vtbl+0xC (@004c6920)
}
Scalar yStep = SeekCurveYNorm * yScale; // fld tbyte @004c6bd0 * this[0x2D]
// change-test: the response curve's value at max voltage.
Scalar response = BTSeekVoltageSample(subsystem, GeneratorMaxVoltage); // (vtbl+0x3C)(12000.0f)
if (previousVoltage != response) // this[0x2B]
{
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] replot sub="
<< (subsystem ? subsystem->GetName() : "?")
<< " response12k=" << response << "\n" << std::flush;
previousVoltage = response;
ClearGraph(); // @004c6be4 -- erases the WHOLE box
localView.SetColor(0xff); // vtbl+0x18 (Replace mode from the clear)
localView.MoveToAbsolute(0, 0); // vtbl+0x24
for (Scalar v = 0.0f; v < SeekCurveLimit; v += SeekCurveStep)
{
int x = Round(BTSeekVoltageSample(subsystem, v) * xScale);
int y = Round(v * yStep);
localView.DrawLineToAbsolute(x, y); // vtbl+0x30
}
cursorX = -1; // this[0x30]
cursorY = -1; // this[0x31]
localView.SetOperation((GraphicsDisplay::Operation)3); // vtbl+0x0C back to Xor
if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0)
{
tickIndexShown = *currentSeekIndexPtr; // this[0x32]
DrawTicks(); // @004c6c6c
}
}
// gear-change tick maintenance (XOR pair: erase old highlight, draw new).
if (currentSeekIndexPtr != 0 && seekVoltageTablePtr != 0)
{
int currentIndex = *currentSeekIndexPtr;
if (currentIndex != tickIndexShown)
{
DrawTicks(); // erase at the old index
tickIndexShown = currentIndex;
DrawTicks(); // draw at the new
}
}
// live-voltage cursor (drawCursor pages: the emitter/PPC graphs).
if (drawCursor != 0) // this[0x2F]
{
if (cursorX >= 0)
DrawCursorBox(); // XOR-erase the old box
Scalar live = *liveVoltage; // *this[0x29]
cursorX = Round(BTSeekVoltageSample(subsystem, live) * xScale);
cursorY = Round(live * yStep);
DrawCursorBox(); // @004c6c30
}
}
//###########################################################################
// ConfigMapGauge @004c6d80
//###########################################################################
//
// @004c6d80 -- ctor (vtable PTR_FUN_0051a1b8). GraphicGauge base; interns the
// joystick base bitmap "btjoy.pcc" (BitMapCache AddRef) + the four config-state
// pixmaps cm_off/cm_other/cm_only/cm_both.pcc (PixMapCache AddRef); sets the port
// origin (x,y); subsystem = param_9; linkedEntity = 0.
//
ConfigMapGauge::ConfigMapGauge(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int x,
int y,
Entity *subsystem_in,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0 /*owner*/, // FUN_00444818
graphics_port_number, identification_string)
{
joystickImage = (char *)"btjoy.pcc"; // @0x98 this[0x26]
stateImage[0] = (char *)"cm_off.pcc"; // @0x9C this[0x27]
stateImage[1] = (char *)"cm_other.pcc"; // @0xA0 this[0x28]
stateImage[2] = (char *)"cm_only.pcc"; // @0xA4 this[0x29]
stateImage[3] = (char *)"cm_both.pcc"; // @0xA8 this[0x2A]
localView.SetOrigin(x, y); // vtbl+0x10 (this+0x48)
linkedEntity = 0; // @0x94 this[0x25]
// CORRECTION (2026-07-21, retires the task #6 "dormant" claim): @0x94 is NOT
// a colour and @004c6ee0 is NOT an uncalled SetColor -- it is the virtual
// GaugeBase::LinkToEntity override (vtbl slot 9, +0x24; verified against the
// binary vtable @0051a1b8 + the engine GaugeBase roster). The engine
// broadcasts LinkToEntity(viewpointEntity) to every gauge when the viewpoint
// binds (APP.cpp:1277 -> GAUGREND.cpp:3011), which arms this gate -- the
// trigger-config joystick DOES render in the shipped game.
subsystem = subsystem_in; // @0x90 this[0x24]
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
warehouse->bitMapBin.Get(joystickImage); // FUN_00442aec
for (int i = 0; i < 4; i++)
warehouse->pixelMap8Bin.Get(stateImage[i]); // FUN_00442d2b
}
//
// @004c6e54 -- dtor: release btjoy bitmap + 4 pixmaps, GraphicGauge::~GraphicGauge.
//
ConfigMapGauge::~ConfigMapGauge()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(joystickImage); // FUN_00442c12
for (int i = 0; i < 4; i++)
warehouse->pixelMap8Bin.Release(stateImage[i]); // FUN_00442e51
}
Logical
ConfigMapGauge::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004c6ee0 -- LinkToEntity (GaugeBase vtbl slot 9): linkedEntity (this[0x25]) =
// the viewpoint entity. Broadcast by GaugeRenderer::LinkToEntity when the
// viewpoint binds; arms the Execute gate.
//
void
ConfigMapGauge::LinkToEntity(Entity *entity) // @004c6ee0
{
linkedEntity = entity;
}
//
// @004c6ef4 -- BecameActive: force a full redraw (dirty=1, previousState = -1).
//
void
ConfigMapGauge::BecameActive()
{
dirty = True; // @0xBC this[0x2F]
for (int i = 0; i < 4; i++)
previousState[i] = -1; // @0xAC this[0x2B..0x2E]
}
//
// @004c6f1c -- Execute. Once linked (linkedEntity != 0): on the dirty flag, blit the
// base joystick bitmap; then for each of the 4 config-map slots read the fire
// button's map state and, if changed, blit the matching cm_* pixmap.
//
// task #6 UNGUARD: the old deferral rationale was wrong on both counts --
// (a) the sampler is NOT the App ModeManager; it is
// LBE4ControlsManager::buttonGroup[btn].GetMapState (group vtbl+0x24, all
// T0 engine, callable by name -- the same pattern btl4mppr.cpp compiles),
// (b) DAT_00518eb4 IS recoverable from the PE image: 4 {y, button} pairs
// {0x0d,0x45 Pinky} {0x25,0x46 ThumbLow} {0x3d,0x40 Trigger}
// {0x55,0x47 ThumbHigh}, blit x = 0xc.
// GetMapState(direct_destination, event_receiver, message_id, 0x10000) returns
// unmapped=0 / mappedByOthers=1 / mappedByMe=2 / both=3 == the cm_off/cm_other/
// cm_only/cm_both frame index -- the per-weapon lamp column over the btjoy
// joystick image. The feed (subsys+0xE8/+0xEC = &fireImpulse/0 for weapons)
// comes through the complete-type bridge BTSubsystemControlFeed (mechsub.cpp).
//
extern void BTSubsystemControlFeed(void *subsystem_in, void **destination_out,
int *message_id_out, void **receiver_out);
void
ConfigMapGauge::Execute()
{
if (linkedEntity == 0) // @004c6f28: not yet linked to the viewpoint
return;
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
if (dirty)
{
dirty = False;
BitMap *base = warehouse->bitMapBin.Get(joystickImage); // FUN_00442aec
if (base != NULL)
{
localView.SetColor(0xFF); // vtbl+0x18
// issue #42 (doubled joystick): the binary repositions to the view
// ORIGIN before the blit -- @004c6f1c vtbl+0x24 (0,0) between
// SetColor and DrawBitMapOpaque. The transcription had dropped it,
// so the FIRST draw worked (fresh cursor) and every forced redraw
// (the DISPLAY page round-trip's BecameActive) blitted at the state
// loop's last cursor (0xc, 0x55) -- the offset ghost joystick.
localView.MoveToAbsolute(0, 0); // vtbl+0x24
localView.DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, base); // vtbl+0x54
}
warehouse->bitMapBin.Release(joystickImage);
}
// The 4-slot state loop (DAT_00518eb4, PE-recovered {y, button} pairs).
static const struct { int y; int button; } kSlot[4] =
{
{ 0x0d, 0x45 }, // ButtonJoystickPinky
{ 0x25, 0x46 }, // ButtonJoystickThumbLow
{ 0x3d, 0x40 }, // ButtonJoystickTrigger
{ 0x55, 0x47 }, // ButtonJoystickThumbHigh
};
LBE4ControlsManager *controls =
(LBE4ControlsManager *)application->GetControlsManager(); // DAT_004efc94+0x3c
if (controls == NULL || subsystem == NULL)
return;
void *destination = 0;
int messageID = 0;
void *receiver = 0;
BTSubsystemControlFeed(subsystem, &destination, &messageID, &receiver);
for (int slot = 0; slot < 4; slot++)
{
int state = controls->buttonGroup[kSlot[slot].button].GetMapState( // group vtbl+0x24
(ControlsButton *)destination, // *(subsys+0xE8)
(Receiver *)receiver, // the subsystem itself
messageID, // *(subsys+0xEC)
0x10000 /*ModeNonMapping -- the persistent groups*/);
if (state != previousState[slot])
{
previousState[slot] = state;
PixelMap8 *pix =
(PixelMap8 *)warehouse->pixelMap8Bin.Get(stateImage[state]); // FUN_00442d2b
if (pix != NULL)
{
localView.MoveToAbsolute(0xc, kSlot[slot].y); // vtbl+0x24
localView.DrawPixelMap8(0 /*opaque*/, 0 /*rotation*/, pix); // vtbl+0x5c
}
warehouse->pixelMap8Bin.Release(stateImage[state]); // FUN_00442e51
}
}
}
//###########################################################################
// OneOfSeveral data-driven lamps @004c70a4 / @004c7160
//###########################################################################
//
// @004c70a4 -- AnimatedSubsystemLamp ctor (vtable PTR_FUN_0051a174): OneOfSeveral
// (fromImageStrip=1) + a CoolingLoopConnection feeding the selected frame from
// the subsystem's cooling-loop state. Otherwise inherits OneOfSeveral::Execute.
//
AnimatedSubsystemLamp::AnimatedSubsystemLamp(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int x,
int y,
const char *image,
int columns,
int rows,
void *subsystem,
const char *identification_string
):
// COLOR FIX 2026-07-20 (verified vs binary @004c70a4 / caller SubsystemCluster
// @004c8140): the ctor DROPPED the background/foreground color params the binary
// passes and hardcoded 0,0 -- so Execute's SetColor(0)+DrawBitMapOpaque(0) drew
// the loop/gen boxes black-on-black (invisible). The binary passes bg=0xff,fg=0
// (the MFD on-color, same as the neighboring temp bar) -- restored.
OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number, // FUN_004c4d88
x, y, True /*fromImageStrip*/, image, 0xff, 0, columns, rows,
identification_string)
{
selected = 0; // @0xAC this[0x2B]
AddConnection(new CoolingLoopConnection(&selected, subsystem)); // FUN_004c3134
}
AnimatedSubsystemLamp::~AnimatedSubsystemLamp() {} // @004c7134 (base chain)
//
// @004c7160 -- AnimatedSourceLamp ctor (vtable PTR_FUN_0051a130): identical shape
// but a PowerSourceConnection (@004c31ec) drives the frame from the resolved
// power source's bus-lamp state.
//
AnimatedSourceLamp::AnimatedSourceLamp(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int x,
int y,
const char *image,
int columns,
int rows,
void *source,
const char *identification_string
):
// COLOR FIX 2026-07-20 (same drop as AnimatedSubsystemLamp above): restore the
// binary's bg=0xff,fg=0 so the generator-letter bus lamp draws (was 0,0 = blank).
OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number,
x, y, True, image, 0xff, 0, columns, rows, identification_string)
{
selected = 0;
AddConnection(new PowerSourceConnection(&selected, source)); // FUN_004c31ec
}
AnimatedSourceLamp::~AnimatedSourceLamp() {} // @004c71f0
//###########################################################################
// ScalarBarGauge @004c721c
//###########################################################################
//
// @004c721c -- ctor (vtable PTR_FUN_0051a0e4). Chains the MUNGA L4 bar primitive
// (@00472ef0 == BarGraphBitMapScalar) with no auto-connection (value=NULL), then
// AddConnection(new GeneratorVoltageConnection @004c3288) writing the inherited
// WipeGaugeScalar::currentValue (@0xB4, this[0x2D]) from the resolved voltage
// source. Generator-voltage bar, range 0..12000.0f. dtor @004c733c chains the
// bar dtor @00472fb4 (implicit).
//
ScalarBarGauge::ScalarBarGauge(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int left,
int bottom,
int right,
int top,
const char *tile_image,
int foreground_color,
int background_color,
WipeGaugeScalar::Direction direction,
Scalar min,
Scalar max,
void *voltage_source,
const char *identification_string
):
BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/, // FUN_00472ef0
graphics_port_number, left, bottom, right, top, tile_image,
foreground_color, background_color, direction, min, max,
(Scalar *)0 /*no auto-connection*/, identification_string)
{
AddConnection(new GeneratorVoltageConnection(&currentValue, voltage_source)); // FUN_004c3288
}
ScalarBarGauge::~ScalarBarGauge() {} // @004c733c (base chain)
//###########################################################################
//###########################################################################
// GeneratorCluster (config keyword "GeneratorCluster" -- the 4 generator
// engineering panels, buttons 9-12 / GeneratorA..D; vtable PTR_FUN_0051a0a0).
// Reconstructed from part_014.c:891-1013 (Make @004c7368 / ctor @004c746c /
// dtor @004c7778). Was PROSE + a placeholder Make -> the "GeneratorCluster"
// config lines parse-skipped and the 4 panels never built. Only the dtor is
// overridden; Execute/BecameActive/TestInstance are inherited from GraphicGauge.
//###########################################################################
//###########################################################################
static inline GaugeRate ChildRate(); // fwd-decl (defined later, after SubsystemCluster)
//
// @004c72ac -- ScalarBarGauge plain-Scalar variant. Same bar body as @004c721c but
// a GaugeConnectionDirectOf<Scalar> from a DIRECT Scalar* (the generator's
// OutputVoltage attribute pointer) instead of a GeneratorVoltageConnection.
//
ScalarBarGauge::ScalarBarGauge(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int left,
int bottom,
int right,
int top,
const char *tile_image,
int foreground_color,
int background_color,
WipeGaugeScalar::Direction direction,
Scalar min,
Scalar max,
Scalar *value_source, // DIRECT scalar
const char *identification_string
):
BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/, // FUN_00472ef0
graphics_port_number, left, bottom, right, top, tile_image,
foreground_color, background_color, direction, min, max,
(Scalar *)0 /*no auto-connection*/, identification_string)
{
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &currentValue, value_source)); // FUN_00474855
}
// CFG shape (L4GAUGE.CFG:5046): NO leading rate token; p[0] = the mode mask (the
// panel's own rate is hard-wired 0xFFFF).
MethodDescription
GeneratorCluster::methodDescription =
{
"GeneratorCluster",
GeneratorCluster::Make,
{
{ ParameterDescription::typeModeMask, NULL }, // p[0] mode mask (ModeAlwaysActive)
{ ParameterDescription::typeString, NULL }, // p[1] generator name (GeneratorA..D)
{ ParameterDescription::typeString, NULL }, // p[2] background pcc
{ ParameterDescription::typeColor, NULL }, // p[3] extra/leak colorA
{ ParameterDescription::typeString, NULL }, // p[4] temperature tile
{ ParameterDescription::typeColor, NULL }, // p[5] temp bg color
{ ParameterDescription::typeColor, NULL }, // p[6] temp fill color
{ ParameterDescription::typeString, NULL }, // p[7] voltage tile
{ ParameterDescription::typeColor, NULL }, // p[8] voltage fg color
{ ParameterDescription::typeString, NULL }, // p[9] leak pcc
{ ParameterDescription::typeColor, NULL }, // p[10] leak colorB
{ ParameterDescription::typeString, NULL }, // p[11] lamp A pcc
{ ParameterDescription::typeString, NULL }, // p[12] lamp B pcc
{ ParameterDescription::typeColor, NULL }, // p[13] lamp on color
{ ParameterDescription::typeColor, NULL }, // p[14] lamp off color
PARAMETER_DESCRIPTION_END
}
};
//
// @004c7368 -- Make. Resolve the named generator (config p[1], e.g. "GeneratorA");
// warn + bail if absent. Then alloc 0xB4 + construct.
//
Logical
GeneratorCluster::Make(
int display_port_index,
Vector2DOf<int> position,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
Subsystem *subsystem = entity->FindSubsystem(p[1].data.string); // FUN_0041f98c (GeneratorA..D)
if (subsystem == NULL)
{
DebugStream << "Subsystem " << p[1].data.string << " not found\n";
return False;
}
GeneratorCluster *gauge = (GeneratorCluster *)operator new(0xb4); // FUN_00402298(0xb4)
if (gauge != NULL)
{
new (gauge) GeneratorCluster(
p[0].data.modeMask, // mode (ModeAlwaysActive)
(L4GaugeRenderer *)gauge_renderer,
0, // owner_ID (raw hard-wires 0)
display_port_index, // graphics_port_number
position.x, position.y, // panel origin
subsystem, // resolved generator
p[2].data.string, // background image
p[3].data.color, // extra/leak colorA
p[4].data.string, // temperature tile
p[5].data.color, // temp bg color
p[6].data.color, // temp fill color
p[7].data.string, // voltage tile
p[8].data.color, // voltage fg color
p[9].data.string, // leak image
p[10].data.color, // leak colorB
p[11].data.string, // lampA image
p[12].data.string, // lampB image
p[13].data.color, // lamp on color
p[14].data.color, // lamp off color
"GeneratorCluster");
}
return True;
}
//
// @004c746c -- ctor (vtable PTR_FUN_0051a0a0). GraphicGauge base (own rate hard-
// wired 0xFFFF; mode from config). Resolve the generator's temp/voltage attributes,
// intern + AddRef the panel background pixmap, build the 5 child gauges into this
// panel's port at offsets relative to the panel origin (x,y).
//
GeneratorCluster::GeneratorCluster(
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int owner_ID,
int graphics_port_number,
int x,
int y,
Subsystem *subsystem_in,
const char *background_image,
int extra_color,
const char *temp_tile,
int temp_bg_color,
int temp_fill_color,
const char *voltage_tile,
int voltage_fg_color,
const char *leak_image,
int leak_color_b,
const char *lampA_image,
const char *lampB_image,
int lamp_on_color,
int lamp_off_color,
const char *identification_string
):
GraphicGauge((GaugeRate)0xFFFF, mode_mask, renderer_in, owner_ID, // FUN_00444818
graphics_port_number, identification_string)
{
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
GaugeRate rate1 = ChildRate(); // VertTwoPartBar
GaugeRate rate2 = ChildRate(); // ScalarBarGauge
GaugeRate rate3 = ChildRate(); // LeakGauge
GaugeRate rate4 = ChildRate(); // both lamps
Scalar *currentTemp = (Scalar *)AttributePointerOf(subsystem_in, "CurrentTemperature");
Scalar *degradeTemp = (Scalar *)AttributePointerOf(subsystem_in, "DegradationTemperature");
Scalar *failTemp = (Scalar *)AttributePointerOf(subsystem_in, "FailureTemperature");
extern Scalar BTHeatSinkLeakFullScale(::Subsystem *sub); // heat.cpp (#97)
Scalar *coolantLeak = (Scalar *)AttributePointerOf(subsystem_in, "CoolantMassLeakRate");
Scalar *outputVolt = (Scalar *)AttributePointerOf(subsystem_in, "OutputVoltage");
localView.SetOrigin(x, y); // vtbl+0x10
secondaryColor = leak_color_b; // @0x94 (stored; unread here)
backgroundImage = new char[strlen(background_image) + 1]; // @0x90
strcpy(backgroundImage, background_image);
warehouse->pixelMap8Bin.Get(backgroundImage, False); // AddRef
// child 1: vertical two-part temperature bar
temperatureBar = new VertTwoPartBar(rate1, mode_mask, renderer_in, graphics_port_number,
x + 5, y + 0x29, x + 0xC, y + 0x5C,
temp_tile, temp_bg_color, temp_fill_color, extra_color,
currentTemp, degradeTemp, failTemp, "VertTwoPartBar");
// child 2: generator-voltage bar 0..12000 (OutputVoltage); @004c72ac Scalar* variant
voltageBar = new ScalarBarGauge(rate2, mode_mask, renderer_in, graphics_port_number,
x + 0x18, y + 0x29, x + 0x1C, y + 0x5C,
voltage_tile, voltage_fg_color, extra_color,
WipeGaugeScalar::wipeUp, 0.0f, 12000.0f, outputVolt,
"GeneratorVoltage(scalar)");
// child 3: coolant-leak inverse-wipe (CoolantMassLeakRate); third=*(subsys+0x150), frames=3
leakGauge = new BitMapInverseWipe(rate3, mode_mask, renderer_in, graphics_port_number,
x, y + 0x27, leak_image, extra_color, leak_color_b,
BTHeatSinkLeakFullScale(subsystem_in), // third: full-scale divisor (#97 bridge)
3, // frames
coolantLeak, "LeakGauge");
// child 4/5: two status lamps (subsys+0x1D4 == generatorOn)
lampA = new TwoState(rate4, mode_mask, renderer_in, (unsigned)owner_ID, graphics_port_number,
x + 0xC, y + 2, lampA_image, lamp_on_color, lamp_off_color,
(int *)((char *)subsystem_in + 0x1D4), "TwoState");
lampB = new TwoState(rate4, mode_mask, renderer_in, (unsigned)owner_ID, graphics_port_number,
x + 2, y + 2, lampB_image, lamp_on_color, lamp_off_color,
(int *)((char *)subsystem_in + 0x1D4), "TwoState");
subsystem = subsystem_in; // @0xAC
_reserved0xB0 = 0; // @0xB0 (raw leaves uninit; zero for determinism)
}
//
// @004c7778 -- dtor. Release the background pixmap + free the interned name; the
// base chain runs implicitly. The 5 children are NOT deleted here (faithful to the
// binary: each self-registered with the renderer via the base Gauge ctor and is
// owned there -- deleting would double-free with the renderer chain teardown).
//
GeneratorCluster::~GeneratorCluster()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->pixelMap8Bin.Release(backgroundImage);
delete[] backgroundImage;
backgroundImage = NULL;
}
//
// BecameActive -- paint the panel's static background pixmap. MUST override the
// default (which self-inactivates the panel + warns); a no-op keeps the panel
// active so its 5 child gauges (self-registered) keep drawing. The bg is drawn
// here (on activation) beneath the children.
//
void
GeneratorCluster::BecameActive()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
PixelMap8 *bg = warehouse->pixelMap8Bin.Get(backgroundImage); // already AddRef'd by the ctor
if (bg != NULL)
{
localView.MoveToAbsolute(0, 0); // vtbl+0x24
localView.DrawPixelMap8(True, 0, bg, 0, 0, // vtbl+0x5C (opaque, full image)
bg->Data.Size.x - 1, bg->Data.Size.y - 1);
}
warehouse->pixelMap8Bin.Release(backgroundImage); // balance this Get
}
//
// Execute -- the panel itself has no per-frame content (the temp/voltage/leak/lamp
// children each Execute independently), but Execute MUST be overridden: the engine
// base Gauge::Execute is Fail(...)->abort (GAUGE.cpp:598). No-op.
//
void
GeneratorCluster::Execute()
{
}
//###########################################################################
//###########################################################################
// PrepEngrScreen @004c7b30 Make / @004c7bf0 ctor
//###########################################################################
//###########################################################################
// forward: the tiled-blit helper is defined lower in this TU (FUN_004c2ec4).
static void DrawPortBackground(GraphicsView *view, L4Warehouse *warehouse,
int x, int y, const char *tile);
// ---- FUN_004c79c8 : draw the three status-label cells (+ optional top erase box).
// Each cell blits its label .pcc, or blanks the cell. Absolute cell rectangles are
// the binary's exact coords (part_014.c:1127-1166).
static void
DrawStatusCells(GraphicsView *view, L4Warehouse *warehouse,
int topBox, const char *A, const char *B, const char *C)
{
if (topBox) // :1131-1135
{
view->SetColor(0);
view->MoveToAbsolute(0x97, 0x80);
view->DrawFilledRectangleToAbsolute(0x17d, 0x13b);
}
if (A == NULL) { view->SetColor(0); view->MoveToAbsolute(0x15a, 0x145); // :1136-1140
view->DrawFilledRectangleToAbsolute(0x17f, 0x159); }
else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x15a, 0x145, A); }
if (B == NULL) { view->SetColor(0); view->MoveToAbsolute(0x85, 0x58); // :1145-1149
view->DrawFilledRectangleToAbsolute(0x17f, 0x7c); }
else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x85, 0x58, B); }
if (C == NULL) { view->SetColor(0); view->MoveToAbsolute(0x2a, 0x34); // :1154-1160
// raw uses an extra GraphicsView vtbl+0x4c(99,10) compound-blank; a plain
// fill at the same cell is the faithful-enough substitute (marked).
view->DrawFilledRectangleToAbsolute(0x9f, 0x2d); }
else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x2a, 0x34, C); }
}
// CFG shape (L4GAUGE.CFG:4595): mode, int screen(1..12), then 7 .pcc names
// (font + 6 labels). 9 params.
MethodDescription
PrepEngrScreen::methodDescription =
{
"prepEngr",
PrepEngrScreen::Make,
{
{ ParameterDescription::typeModeMask, NULL }, // p[0] mode (ModeMFD*Eng*)
{ ParameterDescription::typeInteger, NULL }, // p[1] screen number 1..12
{ ParameterDescription::typeString, NULL }, // p[2] font helv42.pcc -> statusImage[0]
{ ParameterDescription::typeString, NULL }, // p[3] escnd.pcc -> statusImage[1]
{ ParameterDescription::typeString, NULL }, // p[4] edmg.pcc -> statusImage[2]
{ ParameterDescription::typeString, NULL }, // p[5] bteslev.pcc -> statusImage[3]
{ ParameterDescription::typeString, NULL }, // p[6] bteunjm.pcc -> statusImage[4]
{ ParameterDescription::typeString, NULL }, // p[7] espeed.pcc -> statusImage[5]
{ ParameterDescription::typeString, NULL }, // p[8] btesrnge.pcc -> statusImage[6]
PARAMETER_DESCRIPTION_END
}
};
//
// @004c7b30 -- Make. Validate the screen number (p[1], 1..12) then construct.
// The GraphicGaugeBackground base ctor self-registers with the renderer, so the
// gauge's BecameActive fires whenever its Eng screen is selected. We `new` the
// object (real compiled size) rather than operator new(0x90) -- the 2007 base
// isn't byte-exact, and nothing reads this object at raw offsets.
//
Logical
PrepEngrScreen::Make(
int display_port_index,
Vector2DOf<int> /*position -- PrepEngr draws at absolute coords*/,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
int screen = p[1].data.integer;
if (screen < 1 || screen > 12) // :1182
{
DEBUG_STREAM << "PrepEngr: screen number out of range " << screen << "\n"; // :1183
return False;
}
new PrepEngrScreen( // FUN_00402298(0x90) in the binary
p[0].data.modeMask, // mode (ModeMFD*Eng*)
(L4GaugeRenderer *)gauge_renderer,
0, // owner_ID (raw hard-wires 0)
display_port_index, // graphics_port_number
entity, // mech
screen, // screen number
p[2].data.string, p[3].data.string, p[4].data.string,
p[5].data.string, p[6].data.string, p[7].data.string,
p[8].data.string,
"PrepEngr");
return True;
}
//
// @004c7bf0 -- ctor. GraphicGaugeBackground base (mode/renderer/0/port/"PrepEngr");
// store screen + mech; copy the 7 image names and pre-load (AddRef) each from the
// bitmap cache.
//
PrepEngrScreen::PrepEngrScreen(
ModeMask mode_mask, L4GaugeRenderer *renderer_in, unsigned int owner_ID,
int graphics_port_number, Entity *mech_in, int screen_number,
const char *img0, const char *img1, const char *img2, const char *img3,
const char *img4, const char *img5, const char *img6,
const char *identification_string
):
GraphicGaugeBackground(mode_mask, renderer_in, owner_ID, // FUN_004440d4
graphics_port_number, identification_string)
{
screenNumber = screen_number; // @0x6C (:1216)
mech = mech_in; // @0x70 (:1215)
const char *src[7] = { img0, img1, img2, img3, img4, img5, img6 };
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer; // renderer+0x4c
for (int i = 0; i < 7; i++) // :1217-1238
{
if (src[i] != NULL) {
statusImage[i] = new char[strlen(src[i]) + 1]; // nameCopy (FUN_004700ac)
strcpy(statusImage[i], src[i]);
warehouse->bitMapBin.Get(statusImage[i]); // FUN_00442aec pre-load/AddRef
} else {
statusImage[i] = NULL;
}
}
}
//
// @004c7d14 -- dtor. Release the 7 pre-loaded images + free their name copies;
// the localView + base dtor run implicitly (do NOT emit explicit base-dtor calls).
//
PrepEngrScreen::~PrepEngrScreen()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
for (int i = 0; i < 7; i++) // :1254-1274
{
if (statusImage[i] != NULL) {
warehouse->bitMapBin.Release(statusImage[i]); // FUN_00442c12
delete[] statusImage[i];
statusImage[i] = NULL;
}
}
}
//
// @004c7e48 -- BecameActive. Find the subsystem assigned to this screen and paint
// its labels (roster + aux-screen read via the databinding-safe accessors + the
// PoweredSubsystem bridge, exactly like VehicleSubSystems::Make). NON-inactivating
// (a real paint; does not chain the base) -> the paint-on-activation contract.
//
void
PrepEngrScreen::BecameActive()
{
if (mech == NULL) return;
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
const char *font = statusImage[0]; // helv42.pcc
int count = mech->GetSubsystemCount(); // mech+0x124
for (int i = 1; i < count; i++) // skip slot 0 (controls mapper)
{
Subsystem *sub = mech->GetSubsystem(i); // mech+0x128[i]
if (sub == NULL) continue;
int auxScreen = 0, placement = -1; char label64[64]; label64[0] = '\0';
if (!BTGetSubsystemAuxScreen(sub, &auxScreen, &placement, label64)) // filter + sub+0x1dc
continue; // not a PoweredSubsystem-derived weapon
if (auxScreen != screenNumber) // :1309
continue;
// ---- numeric #1 : the screen number, 2 digits @(0xE9,0x176) (:1310-1313)
{
NumericDisplay num(warehouse, 0xe9, 0x176, font, 2,
NumericDisplay::unsignedFormat, 0, 0xff); // FUN_004700bc
num.Draw(&localView, (Scalar)screenNumber); // FUN_00470430
}
// ---- numeric #2 : linked heat-sink number, 1 digit @(0x15E,5) (:1315-1321)
// BEST-EFFORT (marked): the resolved sink's +0x1D4 index is a raw offset on
// a not-byte-exact object -> guarded (NULL link / read 0).
int hsNumber = 0;
void *hsLink = AttributePointerOf(sub, "HeatSink"); // FUN_0041bfc0
void *hs = (hsLink != NULL) ? ResolveLink(hsLink) : NULL; // FUN_00417ab4
if (hs != NULL)
hsNumber = *(int *)((char *)hs + 0x1d4);
{
NumericDisplay num(warehouse, 0x15e, 5, font, 1,
NumericDisplay::unsignedFormat, 0, 0xff);
num.Draw(&localView, (Scalar)hsNumber);
}
// ---- subsystem label bitmap @(0x125,0x172) (:1322-1329). The binary draws
// the .pcc name at sub+0x224; the aux-screen bridge's label64 is the
// databinding-safe equivalent (BEST-EFFORT).
if (label64[0] != '\0') {
localView.SetColor(0xff);
DrawPortBackground(&localView, warehouse, 0x125, 0x172, label64);
}
// ---- type-specific label cells (switch on ClassID, :1330-1357)
switch (sub->GetClassID()) // sub+4
{
case 0xBC3: // Sensor
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, NULL);
localView.SetColor(0xff);
DrawPortBackground(&localView, warehouse, 0xb8, 0x8c, statusImage[6]); // btesrnge
return;
case 0xBC6: // Myomers
DrawStatusCells(&localView, warehouse, 0, NULL, statusImage[5], statusImage[3]);
return;
case 0xBC8: // Emitter
case 0xBD4: // PPC
DrawStatusCells(&localView, warehouse, 0, statusImage[1], statusImage[2], statusImage[3]);
return;
case 0xBCD: // ProjectileWeapon
case 0xBD0: // MissileLauncher
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[4]);
return;
case 0xBCE: // GaussRifle -- not yet supported
DEBUG_STREAM << "PrepEngr: Gauss rifle not yet supported\n"; // :1353
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[3]);
return;
default:
return;
}
}
}
//###########################################################################
//###########################################################################
// SubsystemCluster family @004c8140 base
//###########################################################################
//###########################################################################
//
// @004c8140 -- SubsystemCluster ctor (vtable PTR_FUN_0051a020). GraphicGauge
// base. Resolves InputVoltage/HeatSink/CurrentTemperature/Degradation/Failure
// attribute indices, then builds the panel's child gauges into its port:
// this[0x26] HorizTwoPartBar (temperature) @004c4170
// this[0x27] AnimatedSubsystemLamp ("CoolingLoop",btploop) @004c70a4
// this[0x29] AnimatedSourceLamp ("PowerSource",btpbus) @004c7160 (if InputVoltage)
// this[0x24] BackgroundBitmap (if background present) @00471d00
// this[0x28] AnimatedSubsystemLamp (bteloop) @004c70a4
// this[0x2D] OneOfSeveralInt (btecmode, subsys+0x134) @004c5148
// this[0x2A] AnimatedSourceLamp (btebus) @004c7160 (if InputVoltage)
// this[0x25] ScalarBarGauge (evolt, GeneratorVoltage) @004c721c (if InputVoltage)
// this[0x2E] OneOfSeveralStates (btemode, subsys+0x2b8) @004c5470
// (only when subsystem IsDerivedFrom Generator 0x50f4bc)
// this[0x2C] VertTwoPartBar x2 (temperatures) @004c4724
// this[0x2F] LeakGauge (eleak, CoolantMassLeakRate) @004c5b7c
// this[0x30] subsystem ; this[0x31] operating
// @004c87dc -- dtor: GraphicGauge::~GraphicGauge (children freed by subclasses).
// @004c88e4 -- Execute: operating = (subsys+0x40==1); when it changes, repaint
// the panel frame (operational => coloured rectangle; offline => background
// bitmap blit) and forward enable to the temperature/loop children
// (SetEnable @004c8a28 / FUN_00444650).
//
// @004c8a6c -- HeatSinkCluster ctor (vtable PTR_FUN_00519fd4): chains
// SubsystemCluster, this[0x36]=100.0f, then builds four failure TwoStates
// (this[0x37] heat, this[0x38] btehfail, this[0x39] btepfail subsys+0x324,
// this[0x3A] btesfail subsys+0x320) and a NumericDisplayScalar (this[0x3B],
// helv18, this[0x36]) plus a Scalar connection (&this[0x35], subsys+0x31c).
// Execute @004c8db0: failFlag = (failSubsystem+0x40==1); heatLoadScaled =
// heatLoad * _DAT_004c8df0; chain SubsystemCluster::Execute.
//
// @004c8fc4 -- WeaponCluster ctor (vtable PTR_FUN_00519f38): chains
// SubsystemCluster; resolves "PercentDone"; interns the cluster image
// (this[0x33]); warns "WeaponCluster missing <name>" if absent; centres a
// SegmentArc270 recharge dial (this[0x34], 0..360, @004c6244) and a
// ConfigMapGauge (this[0x35], "ConfigMap", @004c6d80); adds a Scalar connection
// (&percentDone, PercentDone). Execute @004c9290: chain base Execute; when
// percentDone > _DAT_004c92e0 toggle the warning lamp (DrawWarningLamp @004c932c).
//
// @004c93b0 -- EnergyWeaponCluster ctor (vtable PTR_FUN_00519ee8): chains
// WeaponCluster; adds a SeekVoltageGraph (this[0x3A], "EngrGraph", edestryd.pcc,
// @004c6798) reading OutputVoltage, and a seek-step OneOfSeveralInt (this[0x3B],
// bteseek.pcc, subsys+0x3f0, @004c5148). dtor @004c94dc.
//
// @004c9558 -- BallisticWeaponCluster ctor (vtable PTR_FUN_00519e98): chains
// WeaponCluster; resolves the ammo bin (Resolve subsys+0x43c) and builds:
// this[0x45]/[0x46] NumericDisplayInteger (ammo counts) @00470cfc
// this[0x42] TwoState (btejam) this[0x43] TwoState (btefire) @004739d8
// this[0x3F] GraphicsViewRecord erase tracker @0044ad78
// this[0x40] NumericDisplayScalarTwoState (helv42) @00473c94
// this[0x41] TwoState (edestryd) @004739d8
// this[0x44] BitMapInverseWipeScalar (bteejtm, subsys+0x3f8) @004c61c8
// Execute @004c9a38: jammed = (subsys+0x364==5 weaponAlarm Jammed); read the
// ammo-bin reload state (bin+0x18c) and, while reloading, compute elapsed
// seconds (frame clock @00414b60 / DAT_0052140c); chain WeaponCluster::Execute.
// TestInstance @004c9adc: True unless the ammo bin is present and the eject
// wipe is mid-cycle. dtor @004c9940 frees the GraphicsViewRecord + numeric.
//
//
// FUN_004c2ec4 -- draw a clipped port-background bitmap: MoveTo(x,y), fetch the
// tile from the BitMap cache, opaque-blit it, release. (Used by the cluster
// ctor to paint the panel's static frame image into the MFD port.)
//
static void DrawPortBackground(GraphicsView *view, L4Warehouse *warehouse,
int x, int y, const char *tile)
{
view->MoveToAbsolute(x, y); // vtbl+0x24
BitMap *bmp = warehouse->bitMapBin.Get(tile); // FUN_00442aec
if (bmp != NULL)
view->DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, bmp); // vtbl+0x58
warehouse->bitMapBin.Release(tile); // FUN_00442c12
}
// A default per-child load-distribution rate (the ctor uses FUN_004442b8/dc/290,
// the NextNthTierGaugeRate allocators).
static inline GaugeRate ChildRate() { return Gauge::NextThirdTierGaugeRate(); }
//###########################################################################
// SubsystemCluster @004c8140 (vtable 0x51a020)
//###########################################################################
//
// @004c8140 -- ctor. rate/mode are hardcoded 0xFFFF/0xFFFFFFFF in the base
// GraphicGauge; the two bit-plane masks are mfd_mode (MFD-port children) and
// eng_mode (engineering-port children). Builds the panel's child gauges (see
// the header comment block) parented to the MFD port (param_5) and the engineering
// port (resolved from eng_port_name); optional children gate on the InputVoltage
// attribute (power bus present) and the damage-zone-index background image.
//
SubsystemCluster::SubsystemCluster(
ModeMask mfd_mode,
L4GaugeRenderer *renderer_in,
int owner_ID,
int mfd_port,
int x,
int y,
Subsystem *subsystem_in,
ModeMask eng_mode,
const char *eng_port_name,
const char *tile_image,
const char *label,
char **image_names,
const char *identification_string
):
GraphicGauge((GaugeRate)0xFFFF, (ModeMask)0xFFFFFFFF, renderer_in, // FUN_00444818
owner_ID, mfd_port, identification_string)
{
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
// Resolve the temperature / power attributes off the subsystem.
void *inputVoltage = AttributePointerOf(subsystem_in, "InputVoltage");
void *heatSink = AttributePointerOf(subsystem_in, "HeatSink");
void *currentTemp = AttributePointerOf(subsystem_in, "CurrentTemperature");
void *degradeTemp = AttributePointerOf(subsystem_in, "DegradationTemperature");
void *failTemp = AttributePointerOf(subsystem_in, "FailureTemperature");
//------------------------------------------------------------------ MFD port
temperatureBar = new HorizTwoPartBar(ChildRate(), mfd_mode, renderer_in, // @004c4170
mfd_port, x + 0x13, y + 0x10, x + 0xdc, y + 0x1f, tile_image,
0xff, 0, (Scalar *)currentTemp, (Scalar *)degradeTemp, (Scalar *)failTemp,
"HorizTwoPartBar");
coolingLoopA = new AnimatedSubsystemLamp(ChildRate(), mfd_mode, renderer_in, // @004c70a4
mfd_port, x + 0xe5, y + 0xe, "btploop.pcc", 7, 1, subsystem_in, "CoolingLoop");
if (inputVoltage == NULL)
powerSourceA = NULL;
else
powerSourceA = new AnimatedSourceLamp(ChildRate(), mfd_mode, renderer_in, // @004c7160
mfd_port, x + 0x10e, y + 0xe, "btpbus.pcc", 5, 1,
subsystem_in, "PowerSource"); // pass the SUBSYSTEM -- BTPowerSourceFrame
// reads voltageSource by name (attribute-shift
// databinding fix); NOT the inputVoltage slot
// Background: image_names[ auxScreenPlacement ] if present. (Placement@0x1e0 is
// read via the PoweredSubsystem bridge, not the non-byte-exact raw offset.)
int dzIndex = -1;
BTGetSubsystemAuxScreen(subsystem_in, NULL, &dzIndex, NULL); // auxScreenPlacement
background = NULL;
if (dzIndex >= 0 && image_names[dzIndex] != NULL)
background = new BackgroundBitmap(mfd_mode, renderer_in, owner_ID, // @00471d00
mfd_port, x + 0x90, y + 0x36, image_names[dzIndex], 0, 0xff, 0,
"BackgroundBitmap");
// Paint the panel frame image into the MFD port.
localView.SetOrigin(x, y); // vtbl+0x10
localView.SetColor(0xff); // vtbl+0x18
DrawPortBackground(&localView, warehouse, 9, 0xb4, label); // FUN_004c2ec4
//----------------------------------------------------------- engineering port
int engPort = renderer_in->FindGraphicsPort(eng_port_name); // FUN_00447f1c (name -> port number)
// Attributes off the linked heat subsystem (Resolve(heatSink)).
void *linkedHeat = ResolveLink(heatSink); // FUN_00417ab4
void *linkTemp = AttributePointerOf(linkedHeat, "CurrentTemperature");
void *linkDegrade = AttributePointerOf(linkedHeat, "DegradationTemperature");
extern Scalar BTHeatSinkLeakFullScale(::Subsystem *sub); // heat.cpp (#97)
void *coolantLeak = AttributePointerOf(subsystem_in, "CoolantMassLeakRate");
coolingLoopB = new AnimatedSubsystemLamp(ChildRate(), eng_mode, renderer_in, // @004c70a4
engPort, 0x254, 0x13b, "bteloop.pcc", 7, 1, subsystem_in, "CoolingLoop");
modeLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148
engPort, 400, 10, "btecmode.pcc", 0xff, 0, 1, 2, // colours byte-verified:
(int *)((char *)subsystem_in + 0x134), "OneOfSeveralInt"); // part_014.c:1479-1481
// (+0x134 = coolantAvailable)
if (inputVoltage == NULL)
{
powerSourceB = NULL;
generatorVoltageBar = NULL;
stateLamp = NULL;
}
else
{
powerSourceB = new AnimatedSourceLamp(ChildRate(), eng_mode, renderer_in, // @004c7160
engPort, 0x1a, 0xd0, "btebus.pcc", 5, 1, subsystem_in, "PowerSource"); // see powerSourceA
generatorVoltageBar = new ScalarBarGauge(ChildRate(), eng_mode, renderer_in, // @004c721c
engPort, 0x89, 0x82, 0x93, 0x13b, "evolt.pcc", 0xff, 0,
WipeGaugeScalar::wipeUp, 0.0f, GeneratorMaxVoltage, subsystem_in, // pass the SUBSYSTEM
"GeneratorVoltage(slotOf)"); // -> BTGeneratorVoltage reads voltage by name (attr-shift fix)
// stateLamp gate: IsDerivedFrom 0x50f4bc = POWEREDSUBSYSTEM (#47 correction --
// the old 'Generator' label was wrong; every powered subsystem passes).
if (IsGeneratorDerived(subsystem_in))
// bg=0xff fg=0 -- byte-verified from the binary's own call site
// @004c866c (`push 3 / push 1 / push 0 / push 0xff / push
// "btemode.pcc"`). These were previously dropped and hardcoded 0/0
// inside the ctor, which painted the lamp in colour 0: the connect
// mode cycled correctly but nothing was ever drawn, so Auto looked
// like it did not exist.
stateLamp = new OneOfSeveralStates(ChildRate(), eng_mode, renderer_in, // @004c5470
engPort, 0xbe, 0, "btemode.pcc", 0xff, 0, 1, 3,
(Entity *)((char *)subsystem_in + 0x2b8), "OneOfSeveralStates"); // +0x2b8 = modeAlarm
// (offset static_asserted)
else
stateLamp = NULL;
}
vertBarA = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in, // @004c4724
engPort, 0x1d1, 0x5c, 0x1eb, 0x124, tile_image, 0xff, 0xff, 0,
(Scalar *)currentTemp, (Scalar *)degradeTemp, (Scalar *)failTemp, "VertTwoPartBar");
vertBarB = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in,
engPort, 0x222, 0x5c, 0x23d, 0x124, tile_image, 0xff, 0xff, 0,
(Scalar *)linkTemp, (Scalar *)linkDegrade, (Scalar *)failTemp, "VertTwoPartBar");
leakGauge = new BitMapInverseWipe(ChildRate(), eng_mode, renderer_in, // @004c5b7c
engPort, 0x255, 0xe0, "eleak.pcc", 0, 0xff,
BTHeatSinkLeakFullScale(subsystem_in) /*third*/, 3 /*frames*/,
(Scalar *)coolantLeak, "LeakGauge"); // BEST-EFFORT raw (subsys+0x150)
failedState = False; // @0xC4 this[0x31] (1 = destroyed)
subsystem = subsystem_in; // @0xC0 this[0x30]
previousDrawState = 0; // @0xC8 this[0x32]
}
//
// @004c87dc -- dtor. Free the owned children, then GraphicGauge::~GraphicGauge
// (implicit). (The binary frees children through the vtbl-slot-3 ReleaseChildren
// path; done here for RAII correctness.)
//
SubsystemCluster::~SubsystemCluster()
{
ReleaseChildren(); // FUN_004c8820
}
//
// @004c8820 -- free each owned child gauge (delete via its own dtor).
//
void SubsystemCluster::ReleaseChildren()
{
delete background; background = NULL;
delete generatorVoltageBar; generatorVoltageBar = NULL;
delete temperatureBar; temperatureBar = NULL;
delete coolingLoopA; coolingLoopA = NULL;
delete coolingLoopB; coolingLoopB = NULL;
delete powerSourceA; powerSourceA = NULL;
delete powerSourceB; powerSourceB = NULL;
delete vertBarA; vertBarA = NULL;
delete vertBarB; vertBarB = NULL;
delete modeLamp; modeLamp = NULL;
delete stateLamp; stateLamp = NULL;
delete leakGauge; leakGauge = NULL;
}
//
// @004c89c4 -- draw the panel's border frame (two thick diagonals of the box).
//
void SubsystemCluster::DrawPanelFrame(int color)
{
localView.SetColor(color); // vtbl+0x18
localView.MoveToAbsolute(9, 0x33); // vtbl+0x24
localView.DrawThickLineToAbsolute(0x132, 0xab); // vtbl+0x38
localView.MoveToAbsolute(9, 0xab);
localView.DrawThickLineToAbsolute(0x132, 0x33);
}
//
// @004c8a28 -- forward an enable/disable to the always-present children (temp bar,
// cooling loop, power source A).
//
void SubsystemCluster::SetChildrenEnable(int enable)
{
// POLARITY (PPC-dial fix, take 3): the passed value is the DRAW STATE --
// 0 = alive (children run), nonzero = dead panel (children off). The old
// reading (`enable==0 => Disable`) had it backwards.
if (temperatureBar) temperatureBar->Disable(enable != 0);
if (coolingLoopA) coolingLoopA->Disable(enable != 0);
if (powerSourceA) powerSourceA->Disable(enable != 0);
}
//
// @004c8990 -- the operational-state virtual (drawn as TestInstance): active
// unless the subsystem is offline (operating==0 AND subsystem+0x278 == 4).
//
int SubsystemCluster::GetDrawState()
{
// POLARITY (PPC-dial fix, take 3): state 1 = the DEAD-panel presentation
// (black fill + bright frame + X lamps lit); state 0 = ALIVE (frame 0 +
// background art; the WeaponCluster lamp gate runs ONLY in state 0). The
// binary's secondary condition (*(subsystem+0x278) != 4 -> 1) remains
// unidentified [T4] -- a healthy subsystem reads ALIVE here until that
// field is decoded (open-questions).
return failedState ? 1 : 0;
}
Logical SubsystemCluster::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004c88e4 -- Execute (shared by the whole family). operating = (subsystem is
// operational, +0x40 == 1). On a draw-state change repaint the panel frame:
// offline => frame(0) + redraw background bitmap; online => black fill + frame(0xff);
// and forward the enable to the temperature / loop children.
//
void SubsystemCluster::Execute()
{
// PPC-dial fix (2026-07-12, take 3 -- POLARITY): the binary's
// *(subsystem+0x40)==1 means FAILED (it lights the X TwoStates, which the
// engine draws bright on NONZERO, and selects the black-fill dead-panel
// look). The recon's old name `operating` was backwards, which inverted
// the X lamps, the panel look, the children enable AND the weapon lamp
// gate (which runs only in draw-state 0 = alive). Truth source: the
// subsystem's own damage saturation (DistributeCriticalHit destroys at
// >= 1.0, mechdmg.cpp).
{
extern Scalar BTSubsystemDamageLevelOf(void *subsystem_in);
failedState = (BTSubsystemDamageLevelOf(subsystem) >= 1.0f);
}
if (getenv("BT_TEMPBAR_LOG"))
{
Scalar *t = (Scalar *)AttributePointerOf(subsystem, "CurrentTemperature");
Scalar *dg = (Scalar *)AttributePointerOf(subsystem, "DegradationTemperature");
Scalar *fl = (Scalar *)AttributePointerOf(subsystem, "FailureTemperature");
static int s_tbN = 0;
if (t != 0 && ((++s_tbN) % 30) == 0)
DEBUG_STREAM << "[tempbar] cluster EXECUTING "
<< (subsystem ? ((Subsystem *)subsystem)->GetName() : "?")
<< " T=" << *t
<< " degT=" << (dg ? *dg : -1.0f)
<< " failT=" << (fl ? *fl : -1.0f)
<< " drawState=" << GetDrawState() << "\n" << std::flush;
}
int state = GetDrawState();
if (state != previousDrawState)
{
previousDrawState = state;
SetChildrenEnable(state);
if (state == 0)
{
DrawPanelFrame(0);
if (background != NULL)
background->BecameActive(); // (child vtbl+0xc) force redraw
}
else
{
localView.SetColor(0); // vtbl+0x18
localView.MoveToAbsolute(1, 0x32); // vtbl+0x24
localView.DrawFilledRectangleToAbsolute(0x136, 0xad); // vtbl+0x48
DrawPanelFrame(0xff);
}
}
}
void SubsystemCluster::BecameActive()
{
previousDrawState = -1; // force first-frame repaint
}
// Recovered .data tuning constants (read from the optimised image).
static const Scalar HeatLoadScale = 100.0f; // _DAT_004c8df0
static const Scalar WeaponWarnThreshold = 0.99f; // _DAT_004c92e0
//###########################################################################
// HeatSinkCluster @004c8a6c (vtable 0x519fd4)
//###########################################################################
//
// @004c8a6c -- ctor: SubsystemCluster base + four failure TwoStates (heat / heat-
// sink / power / structure) + a NumericDisplayScalar read-out, plus a Scalar
// connection feeding heatLoad from the subsystem (+0x31c).
//
HeatSinkCluster::HeatSinkCluster(
ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
int x, int y, Subsystem *subsystem_in, Subsystem *hud, ModeMask eng_mode,
const char *eng_port_name, const char *tile_image, const char *label,
const char *fail_lamp_image, char **image_names, const char *identification_string
):
SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
eng_mode, eng_port_name, tile_image, label, image_names, identification_string)
{
heatLoadScaled = HeatNumericBase; // @0x36 100.0f
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
heatFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, // @004739d8
engPort, 200, 0x110, fail_lamp_image, 0, 0xff, (int *)&failedState, "TwoState"); // bright when FAILED
failSubsystem = hud; // @0x33
failFlag = 0; // @0x34
hsFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x97, 0xe8, "btehfail.pcc", 0, 0xff, (int *)&failFlag, "TwoState");
powerFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x97, 0xc0, "btepfail.pcc", 0, 0xff,
(int *)((char *)subsystem_in + 0x324), "TwoState"); // BEST-EFFORT raw offset
structFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x97, 0x98, "btesfail.pcc", 0xff, 0,
(int *)((char *)subsystem_in + 0x320), "TwoState"); // BEST-EFFORT raw offset
heatNumeric = new NumericDisplayScalar(ChildRate(), eng_mode, renderer_in, // @00470888
owner_ID, engPort, 0x132, 0x8c, 0x15a, 0xaa, "helv18.pcc", 3,
(NumericDisplay::NumericFormat)0, 0, 0xff, &heatLoadScaled,
"NumericDisplayScalar");
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &heatLoad, // @00474855
(Scalar *)((char *)subsystem_in + 0x31c))); // BEST-EFFORT raw offset
}
HeatSinkCluster::~HeatSinkCluster()
{
delete heatFailLamp; delete hsFailLamp; delete powerFailLamp;
delete structFailLamp; delete heatNumeric;
}
//
// @004c8db0 -- Execute: failFlag = (failSubsystem operational, +0x40 == 1);
// heatLoadScaled = heatLoad * 100.0; chain SubsystemCluster::Execute.
//
void HeatSinkCluster::Execute()
{
if (failSubsystem != NULL)
failFlag = (*(int *)((char *)failSubsystem + 0x40) == 1); // BEST-EFFORT raw
heatLoadScaled = heatLoad * HeatLoadScale;
SubsystemCluster::Execute();
}
//###########################################################################
// MyomerCluster @004c8df4 (vtable 0x519f88)
//###########################################################################
//
// @004c8df4 -- ctor: SubsystemCluster base + a SeekVoltageGraph ("EngrGraph")
// fed by an InputVoltage GeneratorVoltageConnection (writes seekValue@0x33) + a
// seek-step OneOfSeveralInt (bteseek.pcc, subsys+0x800). Execute = base.
//
MyomerCluster::MyomerCluster(
ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
const char *eng_port_name, const char *tile_image, const char *label,
char **image_names, const char *identification_string
):
SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
eng_mode, eng_port_name, tile_image, label, image_names, identification_string)
{
seekValue = 0.0f; // @0x33 (connection dst)
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
seekGraph = (GraphicGauge *)new SeekVoltageGraph(ChildRate(), eng_mode, // @004c6798
renderer_in, engPort, (Entity *)subsystem_in,
(AttributeAccessor *)&seekValue, "edestryd.pcc", False, "EngrGraph");
seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148
engPort, 0xf, 0, "bteseek.pcc", 0xff, 0, 1, 4, // colours byte-verified: part_014.c:2146-2147
// gauge-complete wave: 0x800 was OOB for a 0x358 Myomers (garbage) -> the seek
// gear step is Myomers::currentSeekVoltageIndex@0x320 (the analog of the
// EnergyWeaponCluster's Emitter::seekVoltageIndex@0x3f0; same 1..4 semantics).
// INFERRED (the MyomerCluster ctor @004c8df4 isn't in the assert-anchored export).
(int *)((char *)subsystem_in + 0x320), "OneOfSeveralInt");
// Pass the SUBSYSTEM -- BTGeneratorVoltage resolves voltageSource by name
// (the attribute-table shift makes AttributePointerOf("InputVoltage") dead).
AddConnection(new GeneratorVoltageConnection(&seekValue, subsystem_in)); // @004c3288
}
MyomerCluster::~MyomerCluster()
{
delete seekGraph; delete seekStepLamp;
}
//###########################################################################
// WeaponCluster @004c8fc4 (vtable 0x519f38)
//###########################################################################
//
// @004c8fc4 -- ctor: SubsystemCluster base; interns the cluster image; centres a
// SegmentArc270 recharge dial (reads PercentDone) + a ConfigMapGauge; adds a
// Scalar connection feeding percentDone. The warning-lamp centre is derived from
// the cluster image size, or warned "WeaponCluster missing <name>".
//
WeaponCluster::WeaponCluster(
ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
const char *eng_port_name, const char *tile_image, const char *label,
const char *cluster_image, char **image_names, const char *identification_string
):
SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
eng_mode, eng_port_name, tile_image, label, image_names, identification_string)
{
void *percentDoneAttr = AttributePointerOf(subsystem_in, "PercentDone");
// Intern the cluster image + derive the warning-lamp centre from its size.
clusterImage = (char *)cluster_image; // @0x33 (FUN_004700ac intern)
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
BitMap *img = warehouse->bitMapBin.Get(clusterImage);
if (img == NULL)
{
DebugStream << "WeaponCluster missing " << clusterImage << "\n";
warningCenterX = 0; // @0x37
warningCenterY = 0; // @0x38
}
else
{
warningCenterX = 0x41 - (img->Data.Size.x >> 1);
warningCenterY = 0x6d - (img->Data.Size.y >> 1);
}
// rechargeArc (SegmentArc270 @004c6244): the 0..360 recharge dial reading the
// weapon's PercentDone attribute (a real 0..1 fraction).
rechargeArc = (GraphicGauge *)new SegmentArc270((GaugeRate)0xffff, mfd_mode, // @004c6244
renderer_in, owner_ID, mfd_port, x + 0x41, y + 0x6d, 40.0f, 60.0f, 40.0f,
60.0f, 0.0f, 360.0f, 0x14, 0, 0xff, (Scalar *)percentDoneAttr, True,
"SegmentArc270");
configMap = new ConfigMapGauge(ChildRate(), mfd_mode, renderer_in, // @004c6d80
mfd_port, x + 0xee, y + 0x38, (Entity *)subsystem_in, "ConfigMap");
warningState = 0; // @0x39
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &percentDone, // @00474855
(Scalar *)percentDoneAttr));
// PANEL DIAGNOSTIC (PPC-dial investigation 2026-07-12): remember the
// weapon's name so the Execute probe can attribute its readings; tag the
// recharge arc with the same name for its own [arc] probe.
diagWeaponName = (subsystem_in != 0) ? subsystem_in->GetName() : "?";
((SegmentArc270 *)rechargeArc)->diagName = diagWeaponName;
}
WeaponCluster::~WeaponCluster()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(clusterImage);
delete rechargeArc; delete configMap;
}
//
// @004c932c -- DrawWarningLamp: blit the cluster warning image at its centre,
// coloured on/off by the passed enable.
//
void WeaponCluster::DrawWarningLamp(int on)
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
BitMap *img = warehouse->bitMapBin.Get(clusterImage);
if (img != NULL)
{
localView.MoveToAbsolute(warningCenterX, warningCenterY); // vtbl+0x24
localView.SetColor(on ? 0xff : 0); // vtbl+0x18
localView.DrawBitMap(0, img); // vtbl+0x54
}
warehouse->bitMapBin.Release(clusterImage);
}
//
// @004c9258 -- BecameActive: reset the warning state + chain the children.
//
void WeaponCluster::BecameActive()
{
warningState = -2;
if (configMap) configMap->BecameActive();
if (rechargeArc) rechargeArc->BecameActive();
SubsystemCluster::BecameActive();
}
//
// @004c9290 -- Execute: chain the base; when percentDone crosses the warn
// threshold toggle the warning lamp.
//
void WeaponCluster::Execute()
{
// PPC-dial fix (2026-07-12): the base Execute BLACK-FILLS + repaints the
// panel on every draw-state flip -- wiping the recharge arc's pixels while
// the arc's INCREMENTAL draw memory (previousSegment) still says "lit".
// The stale arc then draws nothing until the value crosses new segments =
// "tick marks frozen (at the backdrop's painted ring), dot still toggles"
// (user-reported, intermittent). Detect the repaint and reset the arc's
// draw memory so it fully redraws its true fill.
int preDrawState = previousDrawState;
SubsystemCluster::Execute();
if (previousDrawState != preDrawState)
{
if (rechargeArc) rechargeArc->BecameActive();
if (configMap) configMap->BecameActive();
warningState = -2; // lamp: force redraw too
}
// PANEL DIAGNOSTIC (PPC-dial investigation): 1 Hz per-panel dump of the
// exact value the recharge arc + warning lamp read. Answers whether a
// frozen dial is a DATA fault (percentDone pegged) or a DRAW fault
// (value cycles, segments don't).
if (getenv("BT_PANEL_LOG"))
{
static int s_pl = 0;
if ((s_pl++ % 61) == 0) // 61 PRIME: a shared counter at %60 ALIASES
// to one cluster when the panel count divides
// 60 (the Avatar's 6 panels -> only AFC100
// ever printed; the emitter probe's %97
// comment taught this lesson already)
DEBUG_STREAM << "[panel] " << (diagWeaponName ? diagWeaponName : "?")
<< " percentDone=" << percentDone
<< " warn=" << warningState << "\n" << std::flush;
}
if (previousDrawState == 0)
{
int warn = (WeaponWarnThreshold < percentDone) ? 1 : 0;
if (warn != warningState)
{
warningState = warn;
DrawWarningLamp(warn);
}
}
}
//###########################################################################
// EnergyWeaponCluster @004c93b0 (vtable 0x519ee8)
//###########################################################################
//
// @004c93b0 -- ctor: WeaponCluster base + a SeekVoltageGraph ("EngrGraph",
// OutputVoltage) + a seek-step OneOfSeveralInt (bteseek.pcc, subsys+0x3f0).
//
EnergyWeaponCluster::EnergyWeaponCluster(
ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
const char *eng_port_name, const char *tile_image, const char *label,
const char *cluster_image, char **image_names, const char *identification_string
):
WeaponCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
eng_mode, eng_port_name, tile_image, label, cluster_image, image_names,
identification_string)
{
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
void *outputVoltage = AttributePointerOf(subsystem_in, "OutputVoltage");
seekGraph = new SeekVoltageGraph(ChildRate(), eng_mode, renderer_in, // @004c6798
engPort, (Entity *)subsystem_in, (AttributeAccessor *)outputVoltage,
"edestryd.pcc", True, "EngrGraph");
seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148
engPort, 0xf, 0, "bteseek.pcc", 0xff, 0, 1, 4, // colours byte-verified: part_014.c:2146-2147
(int *)((char *)subsystem_in + 0x3f0), "OneOfSeveralInt"); // BEST-EFFORT raw
}
EnergyWeaponCluster::~EnergyWeaponCluster()
{
delete seekGraph; delete seekStepLamp;
}
//###########################################################################
// BallisticWeaponCluster @004c9558 (vtable 0x519e98)
//###########################################################################
//
// @004c9558 -- ctor: WeaponCluster base + the ammo panel: two NumericDisplay
// Integers (ammo counts off the resolved ammo bin +0x180), jam/fire TwoStates,
// a GraphicsViewRecord erase tracker, a NumericDisplayScalarTwoState, a destroyed
// TwoState, and an eject BitMapInverseWipeScalar (bteejtm, subsys+0x3f8).
//
BallisticWeaponCluster::BallisticWeaponCluster(
ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
const char *eng_port_name, const char *tile_image, const char *label,
const char *cluster_image, char **image_names, const char *font_a,
const char *font_b, const char *identification_string
):
WeaponCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
eng_mode, eng_port_name, tile_image, label, cluster_image, image_names,
identification_string)
{
reloadStartTime = 0; // @0x3D (frame clock)
reloadSeconds = 0.0f; // @0x3E
jammed = firing = reloading = False;
// AFC100-counter fix (2026-07-12): the raw ResolveLink walk assumed the
// 1995 connection internals -- resolve through the weapon's own typed
// connection instead (the same call ConsumeRound fires through).
extern void *BTWeaponAmmoBin(void *weapon);
void *ammoBin = BTWeaponAmmoBin(subsystem_in);
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
// Ammo count via the complete-type bridge (Streak-gauge fix, 2026-07-12):
// the old raw `bin+0x180` was the BINARY's ammoCount offset -- garbage on
// our compiled AmmoBin (base sizes differ), so the counter never moved.
extern int *BTAmmoBinCountPtr(void *bin);
int *ammoValue = BTAmmoBinCountPtr(ammoBin);
ammoCountA = new NumericDisplayInteger(ChildRate(), mfd_mode, renderer_in, // @00470cfc
owner_ID, mfd_port, x + 0x27, y + 100, x + 0x5f, y + 0x76, font_a, 4,
(NumericDisplay::NumericFormat)0, 0, 0xff, ammoValue, "NumericDisplayInteger");
ammoCountB = new NumericDisplayInteger(ChildRate(), eng_mode, renderer_in,
owner_ID, engPort, 0xdc, 0x104, 0x140, 300, font_b, 4,
(NumericDisplay::NumericFormat)0, 0, 0xff, ammoValue, "NumericDisplayInteger");
jammed = 0; // @0x3A
jamLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, // @004739d8
engPort, 0xa0, 0xb2, "btejam.pcc", 0, 0xff, (int *)&jammed, "TwoState");
firing = reloading = 0; // @0x3B / @0x3C
fireLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x121, 0xb6, "btefire.pcc", 0, 0xff, (int *)&firing, "TwoState");
// Ammo erase-tracker GraphicsViewRecord (@0044ad78) -- bring-up: the record
// class is engine-internal; tracked as a NULL erase (the numeric self-erases).
ammoErase = NULL; // @0x3F (GraphicsViewRecord, deferred)
ammoNumeric = new NumericDisplayScalarTwoState(ChildRate(), eng_mode, // @00473c94
renderer_in, owner_ID, engPort, 0x130, 0xbc, 0x162, 0xee, "helv42.pcc", 2,
(NumericDisplay::NumericFormat)0, 0, 0xff, &reloadSeconds, (int *)&firing,
"NumericDisplayScalarTwoState");
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
// ejectWipe (BitMapInverseWipeScalar @004c61c8, eject-timer wipe reading
// subsys+0x3f8). BRING-UP: the BitMapInverseWipeScalar class is not yet
// declared/reconstructed in btl4gaug (only the non-Scalar BitMapInverseWipe);
// tracked NULL until it lands.
ejectWipe = NULL; // @0x44
}
BallisticWeaponCluster::~BallisticWeaponCluster()
{
delete ammoNumeric; delete ammoErase; delete ammoCountA;
delete ammoCountB; delete jamLamp; delete fireLamp;
delete destroyedLamp; delete ejectWipe;
}
//
// @004c99cc -- BecameActive: chain the ammo children then WeaponCluster.
//
void BallisticWeaponCluster::BecameActive()
{
if (ammoCountA) ammoCountA->BecameActive();
if (ammoCountB) ammoCountB->BecameActive();
if (jamLamp) jamLamp->BecameActive();
if (fireLamp) fireLamp->BecameActive();
if (ammoNumeric) ammoNumeric->BecameActive();
if (destroyedLamp) destroyedLamp->BecameActive();
if (ejectWipe) ejectWipe->BecameActive();
WeaponCluster::BecameActive();
}
//
// @004c9a38 -- Execute (Gitea #47 CORRECTION, raw decomp re-read):
//
// this[0x3a] = (subsys+0x364 == 5) jam icon (works)
// this[0x3b] = *(bin + 0x18C) <- cookOffArmed!
// if armed: this[0x3d] = *Now() - *(bin + 0x190) (400 == 0x190 = cookOffTime)
// this[0x3e] = this[0x3d] / DAT_0052140c (ticksPerSecond)
//
// bin+0x18C is NOT "the reload state" -- it is the BAY-FIRE cook-off flag, and
// the numeric this[0x3e] is the countdown seconds RELATIVE TO DETONATION
// (negative while burning, counting up to 0). The old reconstruction bridged
// it to BTAmmoBinFeeding, so the "fire" TwoState (btefire.pcc) blinked with
// every feed cycle and never lit on an actual bay fire -- RajelAran's
// "it doesn't". The `firing/reloading/reloadSeconds` member names below are
// historic (kept to avoid churning the header); semantically they are
// bayFireArmed / bayFireArmed-latch / secondsToDetonation.
//
void BallisticWeaponCluster::Execute()
{
jammed = (*(int *)((char *)subsystem + 0x364) == 5); // BEST-EFFORT raw (weaponAlarm==Jammed)
void *ammoBin = ResolveLink((char *)subsystem + 0x43c); // FUN_00417ab4
extern int BTAmmoBinCookOffArmed(void *bin); // bin+0x18C (complete-type bridge)
extern int BTAmmoBinCookOffTime(void *bin); // bin+0x190
int bayFire = BTAmmoBinCookOffArmed(ammoBin);
firing = bayFire; // the btefire TwoState input
if (bayFire == 0)
{
reloading = 0;
}
else
{
reloading = 1;
reloadStartTime = ((volatile Time &)Now()).ticks // this[0x3d] = now - cookOffTime
- BTAmmoBinCookOffTime(ammoBin);
reloadSeconds = (Scalar)reloadStartTime // this[0x3e] = ticks -> seconds
/ (Scalar)SystemClock::GetTicksPerSecond(); // DAT_0052140c
}
WeaponCluster::Execute();
}
//
// @004c9b50 -- DrawWarningLamp override: draw the fire-ready "dot" (the base
// blits the cluster image in on-colour 0xff / off-colour 0), THEN swap the
// base-page ammo count's colours so the digits stay legible against it:
// dot ABSENT (on==0) -> SetColors(bg=0, fg=0xff) == GREEN digits on black
// dot PRESENT (on!=0) -> SetColors(bg=0xff, fg=0) == BLACK digits cut out of
// the solid green dot (the stencil look)
// The SetColors call ForceUpdate()s the numeric so it repaints over the freshly
// drawn dot on the next child-execute pass. (Verified vs binary FUN_004c9b50 +
// the base DrawWarningLamp FUN_004c932c; the port had only the base, so the
// ammo count stayed green-on-black and clashed with the green dot.)
//
void BallisticWeaponCluster::DrawWarningLamp(int on)
{
WeaponCluster::DrawWarningLamp(on); // FUN_004c932c: draw the dot
if (ammoCountA)
((NumericDisplayInteger *)ammoCountA)->SetColors(on ? 0xff : 0,
on ? 0 : 0xff);
}
//
// @004c9adc -- TestInstance: True unless the ammo bin is present and the eject
// wipe is mid-cycle.
//
Logical BallisticWeaponCluster::TestInstance() const
{
void *ammoBin = ResolveLink((char *)subsystem + 0x43c);
if (ammoBin == NULL || *(int *)((char *)ammoBin + 0x180) == 0)
return True;
if (jammed != 0)
return True;
return SubsystemCluster::TestInstance();
}
//###########################################################################
// vehicleSubSystems Make dispatcher @004cbaf0
//###########################################################################
//
// The 12 auxiliary-screen geometry rows (@0x51bf34, x/y/planeMask/subBit/mfdPort/
// engPort/planeIndex), keyed by subsystem[0x1dc]-1. planeMask -> the MFD child
// bit-plane mode; subBit -> the engineering child bit-plane mode; mfd/eng names
// select the two graphics ports.
//
struct AuxScreenGeometry
{
int x, y;
ModeMask planeMask;
ModeMask subBit;
const char *mfdPortName;
const char *engPortName;
int planeIndex;
};
static const AuxScreenGeometry kAuxGeom[12] =
{
{ 0, 0xf0, 0x1, 0x2, "mfd1", "eng1", 0 }, // grp 1
{ 0x142, 0xf0, 0x1, 0x4, "mfd1", "eng1", 0 }, // grp 2
{ 0, 0x1c, 0x1, 0x8, "mfd1", "eng1", 0 }, // grp 3
{ 0x142, 0x1c, 0x1, 0x10, "mfd1", "eng1", 1 }, // grp 4
{ 0, 0xf0, 0x20, 0x40, "mfd2", "eng2", 1 }, // grp 5
{ 0x142, 0xf0, 0x20, 0x80, "mfd2", "eng2", 1 }, // grp 6
{ 0, 0x1c, 0x20, 0x100, "mfd2", "eng2", 1 }, // grp 7
{ 0x142, 0x1c, 0x20, 0x200, "mfd2", "eng2", 2 }, // grp 8
{ 0, 0xf0, 0x400, 0x800, "mfd3", "eng3", 2 }, // grp 9
{ 0x142, 0xf0, 0x400, 0x1000, "mfd3", "eng3", 2 }, // grp 10
{ 0, 0x1c, 0x400, 0x2000, "mfd3", "eng3", 2 }, // grp 11
{ 0x142, 0x1c, 0x400, 0x4000, "mfd3", "eng3", 2 }, // grp 12
};
//
// FUN_0041a1a4(**(sub+0xc), 0x50f4bc) -- the pre-dispatch type filter (an
// IsDerivedFrom against a ClassDerivations object). The classID switch below is
// the real per-panel filter, so a permissive True is faithful for the displayable
// classes; the exact ClassDerivations resolution lands with a shared accessor.
//
static Logical SubsystemDisplayFilter(Subsystem *) { return True; }
//
// @004cbaf0 -- vehicleSubSystems Make. Build the 8 strip-image pointers from the
// method params, then for each roster subsystem (skipping slot 0) build the panel
// cluster for its classID onto the auxiliary MFD position named by subsystem[0x1dc].
//
Logical
VehicleSubSystems::Make(
int /*display_port_index*/,
Vector2DOf<int> /*position*/,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
L4GaugeRenderer *renderer = (L4GaugeRenderer *)gauge_renderer;
// The 8 status strip bitmaps (qXXX0.pcc..qXXX7.pcc) from the method params.
char *imageNames[8];
for (int p = 0; p < 8; p++)
imageNames[p] = methodDescription.parameterList[p].data.string;
int count = entity->GetSubsystemCount(); // entity+0x124
if (getenv("BT_VSS_LOG"))
DEBUG_STREAM << "[vss] Make called: subsystemCount=" << count << "\n" << std::flush;
for (int i = 0; i < count; i++)
{
if (i == 0) // slot 0 = the controls mapper
continue;
Subsystem *sub = entity->GetSubsystem(i); // entity+0x128[i]
if (sub == NULL)
continue;
// Read the engineering-screen assignment via the PoweredSubsystem bridge
// (also the type filter -- False == not PoweredSubsystem-derived).
int auxScreen = 0, placement = -1;
char labelBuf[64];
if (!BTGetSubsystemAuxScreen(sub, &auxScreen, &placement, labelBuf)) // FUN_0041a1a4 + sub[0x1dc]/[0x1e4]
continue;
int group = auxScreen - 1; // aux-screen position 1..12
if (getenv("BT_VSS_LOG"))
DEBUG_STREAM << "[vss] i=" << i << " classID=" << (int)sub->GetClassID()
<< " auxScreen=" << auxScreen << " placement=" << placement
<< " label='" << labelBuf << "'\n" << std::flush;
if (group < 0)
{
DebugStream << "Auxiliary screen position = zero\n";
continue;
}
if (group >= 12)
{
DebugStream << "Illegal auxiliary screen position =" << group << "\n";
continue;
}
const AuxScreenGeometry &g = kAuxGeom[group];
int mfdPort = renderer->FindGraphicsPort((char *)g.mfdPortName); // FUN_00447f1c
char *label = labelBuf; // the subsystem label (bridge)
char *tile = (char *)"btwarn.pcc";
switch (sub->GetClassID()) // sub+4
{
case 0xBC3: // Sensor -> HeatSinkCluster (shows the "HUD" subsystem)
{
Subsystem *hud = entity->FindSubsystem((char *)"HUD"); // FUN_0041f98c
new HeatSinkCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub,
hud, g.subBit, g.engPortName, tile, label, "edestryd.pcc",
imageNames, "SensorCluster");
break;
}
case 0xBC6: // Myomers -> MyomerCluster
new MyomerCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub,
g.subBit, g.engPortName, tile, label, imageNames, "MyomerCluster");
break;
case 0xBC8: // Emitter
case 0xBD4: // PPC
{
const char *clusterImage =
(*(int *)((char *)sub + 0x334) == 0) ? "qcircle.pcc" : "qcircr.pcc";
new EnergyWeaponCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub,
g.subBit, g.engPortName, tile, label, clusterImage, imageNames,
"EnergyWeaponCluster");
break;
}
case 0xBCD: // ProjectileWeapon
case 0xBD0: // MissileLauncher
new BallisticWeaponCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y,
sub, g.subBit, g.engPortName, tile, label, "qcircle.pcc", imageNames,
"smlfont.pcc", "helv42.pcc", "ProjWeaponCluster");
break;
case 0xBCE: // GaussRifle -- not yet supported
DebugStream << "Gauss rifle not yet supported\n";
break;
default:
break;
}
}
return True;
}
//
// The registered method: config primitive "vehicleSubSystems(q0.pcc..q7.pcc)".
//
MethodDescription
VehicleSubSystems::methodDescription =
{
"vehicleSubSystems",
VehicleSubSystems::Make,
{
{ ParameterDescription::typeString, NULL }, // qXXX0.pcc
{ ParameterDescription::typeString, NULL }, // qXXX1.pcc
{ ParameterDescription::typeString, NULL }, // qXXX2.pcc
{ ParameterDescription::typeString, NULL }, // qXXX3.pcc
{ ParameterDescription::typeString, NULL }, // qXXX4.pcc
{ ParameterDescription::typeString, NULL }, // qXXX5.pcc
{ ParameterDescription::typeString, NULL }, // qXXX6.pcc
{ ParameterDescription::typeString, NULL }, // qXXX7.pcc
{ ParameterDescription::typeEmpty, NULL }
}
};
// === btl4gau3.cpp begins at @004c9bd0 (PlayerStatusMappingGroup) -- not this TU.