Files
TeslaRel410/restoration/source410/BT_L4/BTL4GAU2.CPP
T
CydandClaude Fable 5 c8350408fc BT410 5.3.40: correction -- the 'exact' heads are exact only on lucky runs
Same binary (md5 identical), different run: Eng1/Eng2/Comm read
17981/17981/15656 on some runs and 19011/19011/16909 on others.  The shipped
binary is STABLE across three runs, so this is ours.  Eleven consecutive
clean runs is what let it hide, and the dirty numbers are exactly the
pre-gate numbers from 5.3.32 -- the same artifact recurring.

Read off the raw words rather than inferred: in the artifact rect the shipped
binary writes ONE masked plane bit per pixel (0x4000 = Heat, 0x002D = sec),
while we write arbitrary 16-bit words (0x8BD6 / 0x8AD6 / 0x8AC0) that light
sec+overlay+Mfd1+Eng1+Eng2+Comm at once.  Raw values going into a
plane-packed buffer -- which is why one bad draw appears as extra pixels on
six heads and as 483 MISSING on Heat at the same time.

Intermittent because these widgets are change-driven: on runs where the
coolant value never moves after the first paint, the bad draw never happens.

The six fixes of 5.3.33-39 stand (each moved specific attributable pixels).
What must be restated is the claim that four heads are exact -- true only
when this defect does not fire.  The rig now needs a run-variance check
before any per-head number is believed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:39:11 -05:00

3121 lines
105 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) and RE-HOSTED onto the
// authentic 1995 engine (MUNGA GAUGE/GAUGREND, MUNGA_L4 L4GAUGE) for the
// BC++4.52 build. Behaviour follows the Ghidra pseudo-C @004c6798..@004c9b50
// (+ VehicleSubSystems::Make @004cbaf0); per-method @ADDR evidence is cited
// inline. This TU links between btl4gaug.obj and btl4gau3.obj (BTL4.MAK).
//
// DATA BINDINGS (the re-host rule: bind by NAME through the engine's
// Simulation::GetAttributePointer, or through a documented accessor on the
// reconstructed subsystems -- NEVER a raw 1995 byte offset):
// LIVE PercentDone / TriggerState / WeaponState / RearFiring / ChargeLevel
// -- published by name (mechweap.cpp / emitter.cpp attribute tables).
// LIVE generator voltage / number / on-lamp -- PoweredSubsystem::
// ResolveVoltageSource() -> Generator::MeasuredVoltage() /
// GetGeneratorNumber(), and the PUBLIC Generator::outputVoltage /
// generatorOn members (powersub.hpp).
// LIVE destroyed / failed -- Simulation::GetSimulationState() ==
// MechSubsystem::DestroyedState, MechSubsystem::
// GetSubsystemDamageLevel() >= 1.0 (the PPC-dial polarity truth
// source, mechdmg.cpp DistributeCriticalHit).
// LIVE jam -- MechWeapon::GetWeaponState() == MechWeapon::JammedState
// (retires the binary's raw *(subsys+0x364) read).
// NAME-DEFERRED CurrentTemperature / DegradationTemperature /
// FailureTemperature / CoolantMassLeakRate / HeatLoad /
// CoolantAvailable / CurrentSeekVoltageIndex / Min / Max /
// SeekVoltage -- bound by name; the source410 heat / powersub /
// seek attribute waves do not publish them yet, so the resolve
// misses and the widget reads the unbound zero cell. Each binding
// goes LIVE automatically the day its table row lands.
// INERT (see the stub ledger at each site) -- seek-voltage response
// sampler, cooling-loop lamp frame, linked-sink temperatures,
// ammo count / feed state, aux-screen number / placement / label.
//
// 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 (_DAT_004c8df0)
// _DAT_004c92e0=0.99f warning threshold
// _DAT_004c6bdc=1200.0f / _DAT_004c6be0=12000.0f seek-curve step/limit
// ld80 @004c6bd0 / @004c6d74 = 8.3333333333333e-05 (exactly 1/12000) --
// the voltage->y normaliser (y = v * (1/12000) * yScale)
//
#include <btl4.hpp>
#pragma hdrstop
#if !defined(BTL4GAU2_HPP)
# include <btl4gau2.hpp>
#endif
#if !defined(BTL4MODE_HPP)
# include <btl4mode.hpp>
#endif
#if !defined(L4CTRL_HPP)
# include <l4ctrl.hpp>
#endif
#if !defined(L4WRHOUS_HPP)
# include <l4wrhous.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(MECHSUB_HPP)
# include <mechsub.hpp>
#endif
#if !defined(POWERSUB_HPP)
# include <powersub.hpp>
#endif
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp>
#endif
#if !defined(PROJWEAP_HPP)
# include <projweap.hpp>
#endif
#if !defined(HEAT_HPP)
# include <heat.hpp>
#endif
#include <stdlib.h>
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):
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
// 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
//
//#############################################################################
// UNBOUND-FEED CELLS. Every engine widget copies through its value pointer
// each frame (GaugeConnectionDirectOf<T> hard-verifies and derefs the source
// at construction -- a NULL feed is a crash, not a blank). Any binding whose
// attribute row / accessor is not reconstructed yet is pointed at one of
// these zero cells instead: the widget draws, the value is static, and the
// binding site documents what will light it up. The cells are read-only to
// every consumer.
//#############################################################################
//
static Scalar UnboundScalarSource = 0.0f;
static int UnboundIntegerSource = 0;
//
// FUN_0041bfc0 == the engine's name-keyed attribute-pointer resolve. The
// authentic Simulation::GetAttributePointer(const char*) (SIMULATE.CPP:444)
// already returns NULL on a miss, so no NullAttribute normalisation is
// needed here -- only the NULL-subsystem guard.
//
static void *
AttributePointerOf(
void *subsystem,
const char *name
)
{
if (subsystem == NULL)
{
return NULL;
}
return ((Subsystem *)subsystem)->GetAttributePointer(name);
}
//
// Feed resolvers: attribute by name, or the unbound zero cell on a miss.
// (The composite ctors use the RAW AttributePointerOf only where the binary
// itself gated behaviour on a missing attribute.)
//
static Scalar *
ScalarAttributeOf(
void *subsystem,
const char *name
)
{
Scalar
*pointer = (Scalar *)AttributePointerOf(subsystem, name);
return (pointer != NULL) ? pointer : &UnboundScalarSource;
}
static int *
IntegerAttributeOf(
void *subsystem,
const char *name
)
{
int
*pointer = (int *)AttributePointerOf(subsystem, name);
return (pointer != NULL) ? pointer : &UnboundIntegerSource;
}
//
//#############################################################################
// Accessor bridges onto the reconstructed subsystems (replace the WinTesla
// port's extern "BT*" bridge functions with direct reads through the
// documented public interfaces).
//#############################################################################
//
//
// The binary's IsDerivedFrom(0x50f4bc) gate (FUN_0041a1a4): PoweredSubsystem's
// ClassDerivations. Gates the power-bus children (the 1995 ctor gated them on
// the "InputVoltage" attribute resolving, which is the same population).
//
static Logical
IsPoweredSubsystemDerived(Subsystem *subsystem)
{
if (subsystem == NULL)
{
return False;
}
return subsystem->IsDerivedFrom(PoweredSubsystem::ClassDerivations);
}
//
// The powering generator, resolved through the NAMED connection (powersub.hpp
// ResolveVoltageSource) -- the databinding-rule replacement for the binary's
// ResolveLink(AttributePointerOf("InputVoltage")) walk. NULL when the
// subsystem is unpowered, the tap is unresolved, or the source is not a
// Generator.
//
static Generator *
ResolvedGeneratorOf(void *subsystem)
{
Subsystem
*source;
if (!IsPoweredSubsystemDerived((Subsystem *)subsystem))
{
return NULL;
}
source = ((PoweredSubsystem *)subsystem)->ResolveVoltageSource();
if (source == NULL || !source->IsDerivedFrom(Generator::ClassDerivations))
{
return NULL;
}
return (Generator *)source;
}
//
// The destroyed test == binary *(subsystem+0x40) == 1: the simulation state
// word, 1 == MechSubsystem::DestroyedState (the same hard-failure gate the
// weapon simulations check).
//
static Logical
SubsystemDestroyed(Subsystem *subsystem)
{
if (subsystem == NULL)
{
return False;
}
return subsystem->GetSimulationState() == MechSubsystem::DestroyedState;
}
//
// PPC-dial POLARITY truth source (kept on re-host): a subsystem is FAILED
// when its own damage zone saturates (DistributeCriticalHit destroys at
// >= 1.0, mechdmg.cpp) -- read through the public accessor.
//
static Scalar
SubsystemDamageLevelOf(Subsystem *subsystem)
{
if (subsystem == NULL ||
!subsystem->IsDerivedFrom(MechSubsystem::ClassDerivations))
{
return 0.0f;
}
return ((MechSubsystem *)subsystem)->GetSubsystemDamageLevel();
}
//
// The jam test (retires the binary's raw *(subsys+0x364)==5 -- our recon
// publishes the weapon-state alarm through GetWeaponState, mechweap.hpp).
//
static Logical
WeaponJammedOf(Subsystem *subsystem)
{
if (subsystem == NULL ||
!subsystem->IsDerivedFrom(MechWeapon::ClassDerivations))
{
return False;
}
return ((MechWeapon *)subsystem)->GetWeaponState() == MechWeapon::JammedState
? True : False;
}
//
// INERT STUB -- the seek-voltage RESPONSE sampler. The 1995 subsystem
// published a voltage-response virtual (binary subsystem vtbl slot 15:
// Emitter @004bb42c = sqrt(seekPower(v)/2.0e8), Myomers @004b8f94 =
// sqrt(speed(v)*3.6/350)). Neither response model is reconstructed on the
// source410 subsystems, so the plot samples a flat response: the graph still
// performs its box-clear / erase duties (the shared Eng bit-plane's top-box
// eraser, Gitea #10 finding A) and the destroyed bitmap still shows, but the
// curve is a stand-in until the emitter/myomer response wave lands.
//
static Scalar
SeekResponseOf(
Subsystem *,
Scalar
)
{
return 0.0f;
}
//###########################################################################
// File-private GaugeConnection classes for the composite-cluster lamps.
// Each derives GaugeConnection, stores dst/src, and overrides Update()
// (the per-frame sampler Gauge::Update drives -- GAUGE.CPP:569).
//###########################################################################
//
// The cooling-loop lamp's frame: the loop number of the Condenser this
// subsystem's sink is plumbed to, or 0 ("OFF") when it has no coolant,
// no loop master, or a master that is not a Condenser.
//
static int
CoolingLoopFrameOf(void *source)
{
Subsystem
*subsystem = (Subsystem *)source;
if (subsystem == NULL
|| !subsystem->IsDerivedFrom(HeatSink::ClassDerivations))
{
return 0;
}
HeatSink
*sink = (HeatSink *)subsystem;
if (sink->GetCoolantAvailable() != 1)
{
return 0;
}
Subsystem
*master = sink->ResolveCoolingMaster();
if (master == NULL
|| !master->IsDerivedFrom(Condenser::ClassDerivations))
{
return 0;
}
return ((Condenser *)master)->GetCondenserNumber();
}
//
// CoolingLoopConnection -- @004c3134 ctor / @004c31a0 sampler (vt 0x518ea8).
// The binary: when the source subsystem's coolant loop is available
// (HeatSink coolantAvailable == 1) it follows the linked cooling master (the
// "HeatSink" link == linkedSinks) and shows its Condenser loop number
// (condenserNumber == the image-strip frame); otherwise 0.
//
// LIVE (5.3.36): heat.hpp now publishes GetCoolantAvailable() /
// ResolveCoolingMaster() / GetCondenserNumber(), so the lamp follows the
// binary exactly -- the same shape as PowerSourceConnection below. A sink
// with no coolant, no loop master, or a master that is not a Condenser
// reads frame 0 ("OFF"), which is what the box showed for every panel
// while this was stubbed.
//
class CoolingLoopConnection : public GaugeConnection
{
public:
CoolingLoopConnection(int *destination, void *source):
GaugeConnection(0),
source(source),
destination(destination)
{}
void Update() // @004c31a0
{
*destination = CoolingLoopFrameOf(source);
}
protected:
void *source; // @0x10 this[4]
int *destination; // @0x14 this[5]
};
//
// PowerSourceConnection -- @004c31ec ctor / @004c3258 sampler (vt 0x518e9c).
// The binary resolved the subsystem's InputVoltage link and read the
// generator's number (1..4 -> the btpbus generator-letter frame A..D;
// 0 == OFF). LIVE: resolved through the named voltageSource connection
// (ResolveVoltageSource / GetGeneratorNumber, powersub.hpp) -- the
// databinding-rule rewrite that fixed the frozen bus lamps.
//
class PowerSourceConnection : public GaugeConnection
{
public:
PowerSourceConnection(int *destination, void *source):
GaugeConnection(0),
source(source),
destination(destination)
{}
void Update() // @004c3258
{
Generator
*generator = ResolvedGeneratorOf(source);
*destination = (generator != NULL) ? generator->GetGeneratorNumber() : 0;
}
protected:
void *source; // @0x10
int *destination; // @0x14
};
//
// GeneratorVoltageConnection -- @004c3288 ctor / @004c32f4 sampler (vt
// 0x518e90). The binary resolved the subsystem's InputVoltage link and
// copied its output voltage; 0 if unresolved. Drives the eng-page
// generator-voltage bar (ScalarBarGauge) + the MyomerCluster seek graph.
// LIVE: ResolveVoltageSource()->MeasuredVoltage() (powersub.hpp).
//
class GeneratorVoltageConnection : public GaugeConnection
{
public:
GeneratorVoltageConnection(Scalar *destination, void *source):
GaugeConnection(0),
source(source),
destination(destination)
{}
void Update() // @004c32f4
{
Generator
*generator = ResolvedGeneratorOf(source);
*destination = (generator != NULL) ? generator->MeasuredVoltage() : 0.0f;
}
protected:
void *source; // @0x10
Scalar *destination; // @0x14
};
//###########################################################################
//###########################################################################
// SeekVoltageGraph @004c6798
// (fully reconstructed from the capstone disasm of @004c6798/@004c6920/
// @004c6934/@004c6be4/@004c6c30/@004c6c6c -- Ghidra had dropped every
// x87 argument.)
//###########################################################################
//###########################################################################
//
// @004c6798 -- ctor (vtable PTR_FUN_0051a1fc). GraphicGauge base, AddRefs
// the "destroyed" bitmap, resolves the four SeekVoltage attribute POINTERS
// off the weapon subsystem BY NAME (a miss reads NULL and gates the tick /
// cursor draws -- the emitter/myomer seek rows are not published by the
// source410 tables yet, so today they gate CLOSED), 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_in,
int graphics_port_number,
Entity *subsystem_in,
AttributeAccessor *seek_voltage_pointer,
const char *destroyed_image,
Logical draw_cursor,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0xFFFF /*owner*/,
graphics_port_number, identification_string)
{
Check_Pointer(this);
//------------------------------------------------------------
// Retain the persistent resource string and AddRef the bitmap
// (the callers pass string literals; released in the dtor).
//------------------------------------------------------------
destroyedImage = (char *)destroyed_image;
((L4Warehouse *)renderer_in->warehousePointer)->
bitMapBin.Get(destroyedImage); // AddRef
//------------------------------------------------------------
// The four seek attributes, by NAME (NULL on a miss -- gated).
//------------------------------------------------------------
currentSeekIndexPtr = (int *)AttributePointerOf(subsystem_in, "CurrentSeekVoltageIndex"); // this[0x25]
minSeekIndexPtr = (int *)AttributePointerOf(subsystem_in, "MinSeekVoltageIndex"); // this[0x26]
maxSeekIndexPtr = (int *)AttributePointerOf(subsystem_in, "MaxSeekVoltageIndex"); // this[0x27]
seekVoltageTablePtr = (Scalar *)AttributePointerOf(subsystem_in, "SeekVoltage"); // this[0x28]
//------------------------------------------------------------
// The live-cursor voltage source; Execute derefs it every
// frame, so a missed binding is normalised to the zero cell.
//------------------------------------------------------------
liveVoltage = (Scalar *)seek_voltage_pointer; // this[0x29]
if (liveVoltage == NULL)
{
liveVoltage = &UnboundScalarSource;
}
subsystem = (Subsystem *)subsystem_in; // this[0x2A]
//------------------------------------------------------------
// @004c6798: SetOperation(Xor) + SetPositionWithinPort -- the
// XOR op is what lets DrawTicks/DrawCursorBox erase by redraw.
//------------------------------------------------------------
localView.SetOperation(GraphicsDisplay::Xor); // vtbl+0x0C
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]
//
// (The binary leaves this[0x2B]/[0x30..0x32] uninitialised -- the
// activation 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]
Check_Fpu();
}
//
// @004c68ac -- dtor. Release the destroyed bitmap; base chain implicit.
//
SeekVoltageGraph::~SeekVoltageGraph()
{
Check(this);
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
}
//
// @004c6be4 -- the full-box erase: 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::Replace); // vtbl+0x0C
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.
//
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(response(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()
{
int
i,
last;
//
// (Guard kept from the decode: the binary derefs min/max unguarded --
// its call sites only guarantee current+table non-null. On the re-host
// the seek attributes may legitimately be unpublished, so the guard is
// load-bearing.)
//
if (minSeekIndexPtr == NULL || maxSeekIndexPtr == NULL ||
seekVoltageTablePtr == NULL)
{
return;
}
last = *maxSeekIndexPtr; // this[0x27]
for (i = *minSeekIndexPtr; i <= last; i++) // this[0x26]
{
Scalar
gearVoltage = seekVoltageTablePtr[i]; // this[0x28][i]
int
x = Round(SeekResponseOf(subsystem, gearVoltage) * xScale),
y = Round(gearVoltage * SeekCurveYNorm * yScale);
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 (simulationState == DestroyedState): 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 slot) to
// poison the cache;
// * change-test: sample the response at 12000 V 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; 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.
//
void
SeekVoltageGraph::Execute()
{
Scalar
yStep,
response,
v;
if (SubsystemDestroyed(subsystem))
{
if (destroyedShown == 0)
{
destroyedShown = 1; // this[0x2E]
ClearGraph(); // @004c6be4
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
BitMap
*image = warehouse->bitMapBin.Get(destroyedImage);
if (image != NULL) // (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);
}
return;
}
if (destroyedShown != 0)
{
destroyedShown = 0;
BecameActive(); // own vtbl slot (@004c6920)
}
yStep = SeekCurveYNorm * yScale; // fld tbyte @004c6bd0 * this[0x2D]
//------------------------------------------------------------
// change-test: the response curve's value at max voltage.
//------------------------------------------------------------
response = SeekResponseOf(subsystem, GeneratorMaxVoltage);
if (previousVoltage != response) // this[0x2B]
{
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 (v = 0.0f; v < SeekCurveLimit; v += SeekCurveStep)
{
localView.DrawLineToAbsolute( // vtbl+0x30
Round(SeekResponseOf(subsystem, v) * xScale),
Round(v * yStep));
}
cursorX = -1; // this[0x30]
cursorY = -1; // this[0x31]
localView.SetOperation(GraphicsDisplay::Xor); // vtbl+0x0C back to Xor
if (currentSeekIndexPtr != NULL && seekVoltageTablePtr != NULL)
{
tickIndexShown = *currentSeekIndexPtr; // this[0x32]
DrawTicks(); // @004c6c6c
}
}
//------------------------------------------------------------
// gear-change tick maintenance (XOR pair: erase old, draw new).
//------------------------------------------------------------
if (currentSeekIndexPtr != NULL && seekVoltageTablePtr != NULL)
{
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]
{
Scalar
live;
if (cursorX >= 0)
{
DrawCursorBox(); // XOR-erase the old box
}
live = *liveVoltage; // *this[0x29]
cursorX = Round(SeekResponseOf(subsystem, live) * xScale);
cursorY = Round(live * yStep);
DrawCursorBox(); // @004c6c30
}
}
//###########################################################################
// ConfigMapGauge @004c6d80
//###########################################################################
//
// The 4-slot state table (DAT_00518eb4, PE-recovered {y, button} pairs; the
// button numbers resolve to the authentic L4CTRL.HPP names). Blit x = 0xc.
//
struct ConfigMapSlot
{
int
y;
int
button;
};
static const ConfigMapSlot
configMapSlot[4] =
{
{ 0x0d, LBE4ControlsManager::ButtonJoystickPinky }, // 0x45
{ 0x25, LBE4ControlsManager::ButtonJoystickThumbLow }, // 0x46
{ 0x3d, LBE4ControlsManager::ButtonJoystickTrigger }, // 0x40
{ 0x55, LBE4ControlsManager::ButtonJoystickThumbHigh }, // 0x47
};
//
// @004c6d80 -- ctor (vtable PTR_FUN_0051a1b8). GraphicGauge base; AddRefs the
// joystick base bitmap "btjoy.pcc" + the four config-state pixmaps
// cm_off/cm_other/cm_only/cm_both.pcc; sets the port origin (x,y);
// linkedEntity = 0 (armed later by the LinkToEntity broadcast).
//
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*/,
graphics_port_number, identification_string)
{
int
i;
Check_Pointer(this);
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)
//
// @0x94 is NOT a colour and @004c6ee0 is NOT an uncalled SetColor -- it
// is the virtual GaugeBase::LinkToEntity override (vtbl slot 9, verified
// against the binary vtable @0051a1b8). The engine broadcasts
// LinkToEntity(viewpointEntity) to every gauge when the viewpoint binds,
// which arms this gate -- the trigger-config joystick DOES render in the
// shipped game.
//
linkedEntity = NULL; // @0x94 this[0x25]
subsystem = subsystem_in; // @0x90 this[0x24]
L4Warehouse
*warehouse = (L4Warehouse *)renderer_in->warehousePointer;
warehouse->bitMapBin.Get(joystickImage); // AddRef
for (i = 0; i < 4; i++)
{
warehouse->pixelMap8Bin.Get(stateImage[i]); // AddRef
}
}
//
// @004c6e54 -- dtor: release btjoy bitmap + 4 pixmaps; base chain implicit.
//
ConfigMapGauge::~ConfigMapGauge()
{
int
i;
Check(this);
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(joystickImage);
for (i = 0; i < 4; i++)
{
warehouse->pixelMap8Bin.Release(stateImage[i]);
}
}
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()
{
int
i;
dirty = True; // @0xBC this[0x2F]
for (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.
//
// The sampler is LBE4ControlsManager::buttonGroup[btn].GetMapState (the
// engine ControlsUpdateManager, CONTROLS.HPP): 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 the
// streamed direct mapping registered is the weapon's TriggerState attribute
// (== &fireImpulse, published by name in mechweap.cpp) with a 0 message id;
// the mode is the authentic BTL4ModeManager::ModeNonMapping (== the binary's
// 0x10000 literal -- the persistent groups).
//
void
ConfigMapGauge::Execute()
{
int
slot;
if (linkedEntity == NULL) // @004c6f28: not yet linked to the viewpoint
{
return;
}
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
if (dirty)
{
BitMap
*base = warehouse->bitMapBin.Get(joystickImage);
dirty = False;
if (base != NULL)
{
localView.SetColor(0xFF); // vtbl+0x18
//
// issue #42 (doubled joystick), kept on re-host: the binary
// repositions to the view ORIGIN before the blit. Without it 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);
}
warehouse->bitMapBin.Release(joystickImage);
}
LBE4ControlsManager
*controls = (LBE4ControlsManager *)application->GetControlsManager();
if (controls == NULL || subsystem == NULL)
{
return;
}
//
// The mapping feed: the weapon's fire-button destination. (The stored
// pointer's dynamic type is Subsystem -- cast back before use.)
//
void
*destination = AttributePointerOf((Subsystem *)subsystem, "TriggerState");
for (slot = 0; slot < 4; slot++)
{
int
state = controls->buttonGroup[configMapSlot[slot].button].GetMapState(
destination, // &fireImpulse
(Receiver *)(Subsystem *)subsystem, // the subsystem itself
0, // message id (weapons use 0)
(ModeMask)BTL4ModeManager::ModeNonMapping); // the persistent groups
if (state != previousState[slot])
{
PixelMap8
*pix;
previousState[slot] = state;
pix = warehouse->pixelMap8Bin.Get(stateImage[state]);
if (pix != NULL)
{
localView.MoveToAbsolute(0xc, configMapSlot[slot].y); // vtbl+0x24
localView.DrawPixelMap8(0 /*opaque*/, 0 /*rotation*/, pix);
}
warehouse->pixelMap8Bin.Release(stateImage[state]);
}
}
}
//###########################################################################
// OneOfSeveral data-driven lamps @004c70a4 / @004c7160
//###########################################################################
//
// @004c70a4 -- AnimatedSubsystemLamp ctor (vtable PTR_FUN_0051a174):
// OneOfSeveral (fromImageStrip=1) + a CoolingLoopConnection feeding the
// INHERITED `selected` frame from the subsystem's cooling-loop state.
// Otherwise inherits OneOfSeveral::Execute.
//
// COLOR FIX kept (verified vs binary @004c70a4 / caller @004c8140): the
// binary passes bg=0xff, fg=0 (the MFD on-colour) -- an earlier transcript
// dropped them to 0,0 and the loop/gen boxes drew black-on-black.
//
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
):
OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number,
x, y, True /*fromImageStrip*/, image, 0xff, 0, columns, rows,
identification_string)
{
GaugeConnection
*connection;
//
// SHADOW-FIELD FIX kept: `selected` is the INHERITED OneOfSeveral member
// (@0xAC) -- a local redeclaration froze the lamp at frame 0.
//
selected = 0;
connection = new CoolingLoopConnection(&selected, subsystem); // @004c3134
Register_Object(connection);
AddConnection(connection);
}
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 generator number (the btpbus A..D letter lamp).
//
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
):
OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number,
x, y, True, image, 0xff, 0, columns, rows, identification_string)
{
GaugeConnection
*connection;
selected = 0;
connection = new PowerSourceConnection(&selected, source); // @004c31ec
Register_Object(connection);
AddConnection(connection);
}
AnimatedSourceLamp::~AnimatedSourceLamp() // @004c71f0
{
}
//###########################################################################
// ScalarBarGauge @004c721c / @004c72ac
//###########################################################################
//
// @004c721c -- the "slotOf" variant (vtable PTR_FUN_0051a0e4). Chains the
// engine BarGraphBitMapScalar with no auto-connection (value=NULL -- the
// engine ctor documents that a derived object then builds the connection),
// then adds a GeneratorVoltageConnection writing the inherited
// WipeGaugeScalar::currentValue (@0xB4) from the resolved voltage source.
// Generator-voltage bar, range 0..12000. dtor @004c733c = base chain.
//
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*/,
graphics_port_number, left, bottom, right, top, tile_image,
foreground_color, background_color, direction, min, max,
(Scalar *)NULL /*no auto-connection*/, identification_string)
{
GaugeConnection
*connection;
connection = new GeneratorVoltageConnection(&currentValue, voltage_source); // @004c3288
Register_Object(connection);
AddConnection(connection);
}
//
// @004c72ac -- the plain-Scalar variant. Same bar body but a
// GaugeConnectionDirectOf<Scalar> from a DIRECT Scalar* (the generator's own
// output-voltage member) instead of a GeneratorVoltageConnection.
// GeneratorCluster is its caller. Disambiguated from the void* overload by
// the Scalar* argument (exact match here, std-conversion to void*).
//
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,
const char *identification_string
):
BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/,
graphics_port_number, left, bottom, right, top, tile_image,
foreground_color, background_color, direction, min, max,
(Scalar *)NULL /*no auto-connection*/, identification_string)
{
GaugeConnection
*connection;
connection = new GaugeConnectionDirectOf<Scalar>(0, &currentValue, value_source);
Register_Object(connection);
AddConnection(connection);
}
ScalarBarGauge::~ScalarBarGauge() // @004c733c (base chain)
{
}
//###########################################################################
// shared cluster helpers
//###########################################################################
//
// 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 ctors 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
)
{
BitMap
*bmp;
view->MoveToAbsolute(x, y); // vtbl+0x24
bmp = warehouse->bitMapBin.Get(tile);
if (bmp != NULL)
{
view->DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, bmp);
}
warehouse->bitMapBin.Release(tile);
}
//
// A default per-child load-distribution rate (the binary ctors call the
// NextNthTierGaugeRate allocators FUN_004442b8/dc/290).
//
static inline GaugeRate
ChildRate()
{
return Gauge::NextThirdTierGaugeRate();
}
//
//#############################################################################
// AUX-SCREEN ASSIGNMENT (RECONSTRUCTION STAND-IN). The 1995 build read the
// authored auxScreenNumber / auxScreenPlacement / auxScreenLabel off the
// powered subsystem (binary sub+0x1dc/0x1e0/0x1e4, streamed from
// PoweredSubsystem__SubsystemResource). Our reconstructed subsystems free
// the resource block after construction (MECHSUB.HPP `resource` -- NEVER
// deref) and keep NO live copy of the aux fields, so:
// * the screen NUMBER is assigned SEQUENTIALLY in roster order over the
// displayable subsystem classes (deterministic, and identical between
// VehicleSubSystems::Make and PrepEngrScreen::BecameActive);
// * the strip-image PLACEMENT has no source -> the panel backdrop child is
// not built;
// * the label .pcc has no source -> the label blits are skipped.
// The ClassID dispatch itself is authentic. All three feeds go live when an
// aux-screen wave lands live members on PoweredSubsystem.
//#############################################################################
//
static Logical
IsPanelSubsystem(Subsystem *subsystem)
{
switch (subsystem->GetClassID())
{
case RegisteredClass::SensorClassID: // 0xBC3
case RegisteredClass::MyomersClassID: // 0xBC6
case RegisteredClass::EmitterClassID: // 0xBC8
case RegisteredClass::PPCClassID: // 0xBD4
case RegisteredClass::ProjectileWeaponClassID: // 0xBCD
case RegisteredClass::MissileLauncherClassID: // 0xBD0
case RegisteredClass::GaussRifleClassID: // 0xBCE (warned-unsupported)
return True;
default:
return False;
}
}
//
// The AUTHORED aux screen (binary sub+0x1dc). PoweredSubsystem caches the
// streamed block at ctor time (its resource memory dies with the Mech
// ctor's stream buffer), so the panels land on the screens the artists
// assigned -- not in roster order. A non-powered panel subsystem (or a
// model that authored nothing) falls back to the sequential walk.
//
static int
AuxScreenAssignmentOf(
Entity *mech,
Subsystem *subsystem
)
{
int
i,
position = 0,
count = mech->GetSubsystemCount();
if (subsystem->IsDerivedFrom(PoweredSubsystem::ClassDerivations))
{
int authored =
((PoweredSubsystem *)subsystem)->GetAuxScreenNumber();
if (authored > 0)
{
return authored;
}
}
for (i = 1; i < count; i++) // slot 0 = the controls mapper
{
Subsystem
*candidate = mech->GetSubsystem(i);
if (candidate == NULL || !IsPanelSubsystem(candidate))
{
continue;
}
++position;
if (candidate == subsystem)
{
return position; // 1-based aux screen
}
}
return 0;
}
//###########################################################################
//###########################################################################
// GeneratorCluster (config keyword "GeneratorCluster" -- the 4 generator
// engineering panels, buttons 9-12 / GeneratorA..D; vtable PTR_FUN_0051a0a0)
// Make @004c7368 / ctor @004c746c / dtor @004c7778.
//###########################################################################
//###########################################################################
//
// CFG shape (L4GAUGE.CFG:5046): NO leading rate token; p[0] = the mode mask
// (the panel's own rate is hard-wired 0xFFFF). KEYWORD + PARAMETER LIST ARE
// PARSE-CRITICAL -- the interpreter assigns opcodes by table position and
// Save()s each parameter after the opcode byte.
//
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. Native Make pattern: plain new +
// Register_Object (the binary's operator new(0xb4) + in-place construct).
//
Logical
GeneratorCluster::Make(
int display_port_index,
Vector2DOf<int> position,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription
*p = methodDescription.parameterList;
Check(gauge_renderer);
Subsystem
*subsystem = entity->FindSubsystem(p[1].data.string);
if (subsystem == NULL)
{
DEBUG_STREAM << "Subsystem " << p[1].data.string << " not found\n";
return False;
}
GeneratorCluster
*gauge = new GeneratorCluster(
p[0].data.modeMask, // mode (ModeAlwaysActive)
(L4GaugeRenderer *)gauge_renderer,
0, // owner_ID (binary 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");
Register_Object(gauge);
return True;
}
//
// @004c746c -- ctor (vtable PTR_FUN_0051a0a0). GraphicGauge base (own rate
// hard-wired 0xFFFF; mode from config). Resolves the generator's feeds,
// interns + AddRefs the panel background pixmap, builds the 5 child gauges
// into this panel's port at offsets relative to the panel origin (x,y).
//
// FEEDS: outputVoltage / generatorOn are PUBLIC Generator members
// (powersub.hpp:413/411) -- LIVE. The temperatures and the coolant leak are
// bound by NAME (unpublished by the source410 heat wave -> unbound cells
// until it lands). The leak gauge's `third` (binary *(subsys+0x150), a raw
// 1995-layout read) is INERT 0 -- the field is stored-but-unused by the base
// BitMapInverseWipe::Execute, so the constant is behaviour-neutral.
//
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)Gauge::gaugeRate_A, mode_mask, renderer_in,
owner_ID, graphics_port_number, identification_string)
{
Check_Pointer(this);
L4Warehouse
*warehouse = (L4Warehouse *)renderer_in->warehousePointer;
GaugeRate
rate1 = ChildRate(), // VertTwoPartBar
rate2 = ChildRate(), // ScalarBarGauge
rate3 = ChildRate(), // LeakGauge
rate4 = ChildRate(); // both lamps
Scalar
*currentTemp = ScalarAttributeOf(subsystem_in, "CurrentTemperature"),
*degradeTemp = ScalarAttributeOf(subsystem_in, "DegradationTemperature"),
*failTemp = ScalarAttributeOf(subsystem_in, "FailureTemperature"),
*coolantLeak = ScalarAttributeOf(subsystem_in, "CoolantMassLeakRate");
//
// LIVE generator feeds through the public members (a config typo could
// name a non-generator, so the derivation is checked).
//
Scalar
*outputVolt = &UnboundScalarSource;
int
*generatorOn = &UnboundIntegerSource;
if (subsystem_in != NULL &&
subsystem_in->IsDerivedFrom(Generator::ClassDerivations))
{
outputVolt = &((Generator *)subsystem_in)->outputVoltage;
generatorOn = &((Generator *)subsystem_in)->generatorOn; // binary subsys+0x1D4
}
localView.SetOrigin(x, y); // vtbl+0x10
secondaryColor = leak_color_b; // @0x94 (stored; unread here)
backgroundImage = nameCopy(background_image); // @0x90 (config string is transient)
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");
Register_Object(temperatureBar);
//------------------------------------------------------------
// child 2: generator-voltage bar 0..12000 (@004c72ac 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, GeneratorMaxVoltage, outputVolt,
"GeneratorVoltage(scalar)");
Register_Object(voltageBar);
//------------------------------------------------------------
// child 3: coolant-leak inverse-wipe (third INERT, frames=3)
//------------------------------------------------------------
leakGauge = new BitMapInverseWipe(rate3, mode_mask, renderer_in,
graphics_port_number,
x, y + 0x27, leak_image, extra_color, leak_color_b,
0 /*third: binary *(subsys+0x150); stored-unused*/, 3 /*frames*/,
coolantLeak, "LeakGauge");
Register_Object(leakGauge);
//------------------------------------------------------------
// child 4/5: two status lamps (generatorOn -- binary subsys+0x1D4)
//------------------------------------------------------------
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,
generatorOn, "TwoState");
Register_Object(lampA);
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,
generatorOn, "TwoState");
Register_Object(lampB);
if (getenv("BT_VIS_LOG"))
DEBUG_STREAM << "[gen] port=" << graphics_port_number
<< " x=" << x << " y=" << y
<< " lampA='" << lampA_image << "' at " << (x + 0xC) << "," << (y + 2)
<< " lampB='" << lampB_image << "' at " << (x + 2) << "," << (y + 2)
<< " on=" << *generatorOn
<< " col0=" << lamp_on_color << " col1=" << lamp_off_color
<< "\n" << flush;
subsystem = subsystem_in; // @0xAC
_reserved0xB0 = 0; // @0xB0 (binary leaves uninit; zeroed)
Check_Fpu();
}
//
// @004c7778 -- dtor. Release the background pixmap + free the interned name.
// 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()
{
Check(this);
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->pixelMap8Bin.Release(backgroundImage);
Unregister_Pointer(backgroundImage);
delete[] backgroundImage;
backgroundImage = NULL;
}
//
// BecameActive -- paint the panel's static background pixmap. MUST override
// the default (which self-inactivates the panel); a real paint keeps the
// panel active so its 5 self-registered child gauges keep drawing.
//
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("not overridden")
// (GAUGE.CPP:598; the 1995 base was a no-op).
//
void
GeneratorCluster::Execute()
{
}
//###########################################################################
//###########################################################################
// PrepEngrScreen @004c7b30 Make / @004c7bf0 ctor
//###########################################################################
//###########################################################################
//
// FUN_004c79c8 -- draw the three status-label cells (+ optional top erase
// box). Each cell blits its label .pcc, or blanks the cell. The absolute
// cell rectangles are the binary's exact coordinates. (The C-cell compound
// blank: the binary uses an extra GraphicsView relative-fill (99,10); a
// plain fill at the same cell is the marked faithful-enough substitute.)
//
static void
DrawStatusCells(
GraphicsView *view,
L4Warehouse *warehouse,
int topBox,
const char *A,
const char *B,
const char *C
)
{
if (topBox)
{
view->SetColor(0);
view->MoveToAbsolute(0x97, 0x80);
view->DrawFilledRectangleToAbsolute(0x17d, 0x13b);
}
if (A == NULL)
{
view->SetColor(0);
view->MoveToAbsolute(0x15a, 0x145);
view->DrawFilledRectangleToAbsolute(0x17f, 0x159);
}
else
{
view->SetColor(0xff);
DrawPortBackground(view, warehouse, 0x15a, 0x145, A);
}
if (B == NULL)
{
view->SetColor(0);
view->MoveToAbsolute(0x85, 0x58);
view->DrawFilledRectangleToAbsolute(0x17f, 0x7c);
}
else
{
view->SetColor(0xff);
DrawPortBackground(view, warehouse, 0x85, 0x58, B);
}
if (C == NULL)
{
view->SetColor(0);
view->MoveToAbsolute(0x2a, 0x34);
view->DrawFilledRectangleToAbsolute(0x9f, 0x2d); // (compound-blank substitute)
}
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. PARSE-CRITICAL.
//
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 self-registers with the
// renderer, so BecameActive fires whenever its Eng screen is selected.
//
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)
{
DEBUG_STREAM << "PrepEngr: screen number out of range " << screen << "\n";
return False;
}
PrepEngrScreen
*gauge = new PrepEngrScreen(
p[0].data.modeMask, // mode (ModeMFD*Eng*)
(L4GaugeRenderer *)gauge_renderer,
0, // owner_ID (binary 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");
Register_Object(gauge);
return True;
}
//
// @004c7bf0 -- ctor. GraphicGaugeBackground base; store screen + mech (FIX
// kept: an early transcript SWAPPED them); intern the 7 image names (the
// config strings are transient) and pre-load (AddRef) each from the 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,
graphics_port_number, identification_string)
{
int
i;
const char
*source[7];
Check_Pointer(this);
screenNumber = screen_number; // @0x6C
mech = mech_in; // @0x70
source[0] = img0; source[1] = img1; source[2] = img2;
source[3] = img3; source[4] = img4; source[5] = img5;
source[6] = img6;
L4Warehouse
*warehouse = (L4Warehouse *)renderer_in->warehousePointer;
for (i = 0; i < 7; i++)
{
if (source[i] != NULL)
{
statusImage[i] = nameCopy(source[i]);
warehouse->bitMapBin.Get(statusImage[i]); // 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.
//
PrepEngrScreen::~PrepEngrScreen()
{
int
i;
Check(this);
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
for (i = 0; i < 7; i++)
{
if (statusImage[i] != NULL)
{
warehouse->bitMapBin.Release(statusImage[i]);
Unregister_Pointer(statusImage[i]);
delete[] statusImage[i];
statusImage[i] = NULL;
}
}
}
//
// @004c7e48 -- BecameActive. Find the subsystem assigned to this screen and
// paint its labels, dispatched on ClassID. NON-inactivating (a real paint;
// does not chain the base) -> the paint-on-activation contract.
//
// STAND-INS at this site (see the aux-screen ledger above):
// * screen match = the sequential roster assignment;
// * numeric #2 (the linked heat-sink number, binary sink+0x1d4 ==
// Condenser::condenserNumber -- protected, no accessor) draws a static 0;
// * the subsystem label bitmap (binary sub+0x224 name -> @(0x125,0x172))
// is skipped -- no live auxScreenLabel.
//
void
PrepEngrScreen::BecameActive()
{
int
i,
count;
const char
*font;
if (mech == NULL)
{
return;
}
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
font = statusImage[0]; // helv42.pcc
if (font == NULL)
{
return;
}
count = mech->GetSubsystemCount();
for (i = 1; i < count; i++) // skip slot 0 (controls mapper)
{
Subsystem
*sub = mech->GetSubsystem(i);
if (sub == NULL || !IsPanelSubsystem(sub))
{
continue;
}
if (AuxScreenAssignmentOf(mech, sub) != screenNumber)
{
continue;
}
//---- numeric #1 : the screen number, 2 digits @(0xE9,0x176)
{
NumericDisplay
num(warehouse, 0xe9, 0x176, font, 2,
NumericDisplay::unsignedFormat, 0, 0xff);
num.Draw(&localView, (Scalar)screenNumber);
}
//---- numeric #2 : linked heat-sink number, 1 digit @(0x15E,5)
// INERT 0 (Condenser::condenserNumber unpublished -- see ledger).
{
NumericDisplay
num(warehouse, 0x15e, 5, font, 1,
NumericDisplay::unsignedFormat, 0, 0xff);
num.Draw(&localView, 0.0f);
}
//---- subsystem label bitmap @(0x125,0x172): the AUTHORED aux-screen
// label, cached on PoweredSubsystem at ctor time (the streamed
// resource dies with the Mech ctor's buffer). This is the panel
// title art -- "SENSOR CLUSTER", "MYOMERS", the weapon name and
// range strip.
//
if (sub->IsDerivedFrom(PoweredSubsystem::ClassDerivations))
{
const char *label =
((PoweredSubsystem *)sub)->GetAuxScreenLabel();
if (label != NULL && *label != '')
{
localView.SetColor(0xff);
DrawPortBackground(&localView, warehouse, 0x125, 0x172, label);
}
}
//---- type-specific label cells (the authentic ClassID dispatch)
switch (sub->GetClassID())
{
case RegisteredClass::SensorClassID: // 0xBC3
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, NULL);
localView.SetColor(0xff);
DrawPortBackground(&localView, warehouse, 0xb8, 0x8c, statusImage[6]); // btesrnge
return;
case RegisteredClass::MyomersClassID: // 0xBC6
DrawStatusCells(&localView, warehouse, 0, NULL, statusImage[5], statusImage[3]);
return;
case RegisteredClass::EmitterClassID: // 0xBC8
case RegisteredClass::PPCClassID: // 0xBD4
DrawStatusCells(&localView, warehouse, 0, statusImage[1], statusImage[2], statusImage[3]);
return;
case RegisteredClass::ProjectileWeaponClassID: // 0xBCD
case RegisteredClass::MissileLauncherClassID: // 0xBD0
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[4]);
return;
case RegisteredClass::GaussRifleClassID: // 0xBCE -- not yet supported
DEBUG_STREAM << "PrepEngr: Gauss rifle not yet supported\n";
DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[3]);
return;
default:
return;
}
}
}
//###########################################################################
//###########################################################################
// SubsystemCluster family @004c8140 base
//###########################################################################
//###########################################################################
//
// @004c8140 -- SubsystemCluster ctor (vtable PTR_FUN_0051a020). GraphicGauge
// base (rate/mode hard-wired every-frame / always-active; the two bit-plane
// masks arrive as mfd_mode and eng_mode). Child roster (binary build order):
// this[0x26] HorizTwoPartBar (temperature, MFD) @004c4170
// this[0x27] AnimatedSubsystemLamp ("CoolingLoop", btploop) @004c70a4
// this[0x29] AnimatedSourceLamp ("PowerSource", btpbus) @004c7160 (powered only)
// this[0x24] BackgroundBitmap (strip art -- STAND-IN: not built)
// this[0x28] AnimatedSubsystemLamp (bteloop, Eng) @004c70a4
// this[0x2D] OneOfSeveralInt (btecmode) @004c5148
// this[0x2A] AnimatedSourceLamp (btebus) @004c7160 (powered only)
// this[0x25] ScalarBarGauge (evolt, slotOf) @004c721c (powered only)
// this[0x2E] OneOfSeveralStates (btemode) @004c5470 (powered only)
// this[0x2B]/[0x2C] VertTwoPartBar x2 (temperatures) @004c4724
// this[0x2F] LeakGauge (eleak) @004c5b7c
//
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)Gauge::gaugeRate_A,
(ModeMask)ModeManager::ModeAlwaysActive,
renderer_in, owner_ID, mfd_port, identification_string)
{
Check_Pointer(this);
L4Warehouse
*warehouse = (L4Warehouse *)renderer_in->warehousePointer;
//
// The power-bus gate: the binary gated the power children on the
// "InputVoltage" attribute resolving -- the same population as the
// PoweredSubsystem derivation (every subsystem with a power bus is one).
//
Logical
powered = IsPoweredSubsystemDerived(subsystem_in);
//
// Temperature feeds by NAME (heat attribute wave pending -> unbound
// cells; the bars draw at minimum until it lands).
//
Scalar
*currentTemp = ScalarAttributeOf(subsystem_in, "CurrentTemperature"),
*degradeTemp = ScalarAttributeOf(subsystem_in, "DegradationTemperature"),
*failTemp = ScalarAttributeOf(subsystem_in, "FailureTemperature"),
*coolantLeak = ScalarAttributeOf(subsystem_in, "CoolantMassLeakRate");
//------------------------------------------------------------------ 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, currentTemp, degradeTemp, failTemp, "HorizTwoPartBar");
Register_Object(temperatureBar);
coolingLoopA = new AnimatedSubsystemLamp(ChildRate(), mfd_mode, renderer_in, // @004c70a4
mfd_port, x + 0xe5, y + 0xe, "btploop.pcc", 7, 1, subsystem_in, "CoolingLoop");
Register_Object(coolingLoopA);
if (!powered)
{
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"); // the SUBSYSTEM -- the connection
// resolves voltageSource by name
Register_Object(powerSourceA);
}
//
// Background strip art: the binary picks image_names[auxScreenPlacement]
// (sub+0x1e0) -- LIVE now that PoweredSubsystem caches the authored
// placement (the resource itself is freed with the Mech ctor's stream
// buffer). A model that authored nothing keeps the bare frame.
//
background = NULL;
int clusterPlacement = -1;
const char *clusterBackgroundName = NULL;
{
int placement = -1;
if (subsystem_in != NULL
&& subsystem_in->IsDerivedFrom(PoweredSubsystem::ClassDerivations))
{
placement =
((PoweredSubsystem *)subsystem_in)->GetAuxScreenPlacement();
clusterPlacement = placement;
}
if (placement >= 0 && placement < 8 && image_names != NULL
&& image_names[placement] != NULL)
{
//
// PLACEMENT (A/B rig, 5.3.35): the strip is the mech diagram
// with THIS subsystem's segment lit, and it belongs in the
// middle of the quadrant, not at its origin. Solved from the
// shipped framebuffer: our extra pixels clustered at panel x
// 0..70 against shipped's missing at x 140..230, and the
// cross-correlation peaked at dx=+144 dy=-54 on all three
// panels independently. y is bottom-up in this space, so up
// the screen is +54.
//
background = new BackgroundBitmap(
mfd_mode, renderer_in, owner_ID, mfd_port,
x + 0x90, y + 0x36, image_names[placement],
1, // opaque
0xff, 0 // fg / bg
);
Register_Object(background);
clusterBackgroundName = image_names[placement];
}
}
//
// The PANEL TITLE BANNER (299x26): the authored auxScreenLabel .pcc --
// "SENSOR CLUSTER" / "MYOMERS" / "ERMED LASER RANGE 500M" / the weapon
// names. Cached on PoweredSubsystem at ctor time (its resource dies
// with the Mech ctor's stream buffer). The banner sits at the TOP of
// the quadrant; BackgroundBitmap places by (left, BOTTOM), so the row
// is the panel origin plus the quadrant height less the banner.
//
titleBanner = NULL;
if (subsystem_in != NULL
&& subsystem_in->IsDerivedFrom(PoweredSubsystem::ClassDerivations))
{
const char *title =
((PoweredSubsystem *)subsystem_in)->GetAuxScreenLabel();
if (title != NULL && *title != '')
{
// Placement measured against the shipped framebuffer
// (the A/B rig): the banner's bottom row sits 0xb3 above the
// panel origin, inset 0x0e from its left edge.
titleBanner = new BackgroundBitmap(
mfd_mode, renderer_in, owner_ID, mfd_port,
x + 0x09, y + 0xb4, title,
1, 0xff, 0
);
Register_Object(titleBanner);
}
if (getenv("BT_VIS_LOG"))
DEBUG_STREAM << "[cluster] sub='"
<< ((subsystem_in != NULL) ? subsystem_in->GetName() : "(null)")
<< "' powered=" << (powered ? 1 : 0)
<< " placement=" << clusterPlacement
<< " bg='" << (clusterBackgroundName ? clusterBackgroundName : "(none)")
<< "' port=" << mfd_port
<< " engPortName='" << (eng_port_name ? eng_port_name : "(null)")
<< "' engPort=" << renderer_in->FindGraphicsPort(eng_port_name)
<< " x=" << x << " y=" << y
<< " title='" << (title ? title : "(null)")
<< "' banner=" << (titleBanner ? 1 : 0) << "\n" << flush;
}
//------------------------------------------------------------
// Paint the panel frame image into the MFD port.
//------------------------------------------------------------
localView.SetOrigin(x, y); // vtbl+0x10
localView.SetColor(0xff); // vtbl+0x18
if (label != NULL) // (STAND-IN: label may be absent)
{
DrawPortBackground(&localView, warehouse, 9, 0xb4, label); // FUN_004c2ec4
}
//----------------------------------------------------------- engineering port
int
engPort = renderer_in->FindGraphicsPort(eng_port_name);
//
// The second vertical bar tracked the LINKED heat sink's temperatures
// (the binary resolved the "HeatSink" link attribute and read the linked
// sink). Neither the link row nor a linked-sink accessor is published
// yet -- INERT FEEDS (the bar draws, stays at minimum).
//
Scalar
*linkTemp = &UnboundScalarSource,
*linkDegrade = &UnboundScalarSource;
coolingLoopB = new AnimatedSubsystemLamp(ChildRate(), eng_mode, renderer_in, // @004c70a4
engPort, 0x254, 0x13b, "bteloop.pcc", 7, 1, subsystem_in, "CoolingLoop");
Register_Object(coolingLoopB);
//
// The cooling-mode lamp: binary *(subsys+0x134) == the 1995 HeatSink
// coolantAvailable word. Bound by NAME ("CoolantAvailable") -- unbound
// until the heat attribute wave publishes it (frame 0 = OFF meanwhile).
//
modeLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148
engPort, 400, 10, "btecmode.pcc", 1, 2,
IntegerAttributeOf(subsystem_in, "CoolantAvailable"), "OneOfSeveralInt");
Register_Object(modeLamp);
if (!powered)
{
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");
Register_Object(powerSourceB);
generatorVoltageBar = new ScalarBarGauge(ChildRate(), eng_mode, renderer_in, // @004c721c
engPort, 0x89, 0x82, 0x93, 0x13b, "evolt.pcc", 0xff, 0,
WipeGaugeScalar::wipeUp, 0.0f, GeneratorMaxVoltage,
(void *)subsystem_in, // the SUBSYSTEM -- BTGeneratorVoltage-
"GeneratorVoltage(slotOf)"); // style resolve through voltageSource
Register_Object(generatorVoltageBar);
//
// The connect-mode lamp (btemode, 1x3). Binary source = subsys+0x2b8
// (the PoweredSubsystem modeAlarm region: Manual/Connected/Auto);
// gated IsDerivedFrom(0x50f4bc). STAND-IN: no mode accessor is
// published -- the subsystem itself is passed, so the lamp tracks the
// SIMULATION state word instead (frame 0 in normal play).
//
stateLamp = new OneOfSeveralStates(ChildRate(), eng_mode, renderer_in, // @004c5470
engPort, 0xbe, 0, "btemode.pcc", 1, 3,
subsystem_in, "OneOfSeveralStates");
Register_Object(stateLamp);
}
vertBarA = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in, // @004c4724
engPort, 0x1d1, 0x5c, 0x1eb, 0x124, tile_image, 0xff, 0xff, 0,
currentTemp, degradeTemp, failTemp, "VertTwoPartBar");
Register_Object(vertBarA);
vertBarB = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in,
engPort, 0x222, 0x5c, 0x23d, 0x124, tile_image, 0xff, 0xff, 0,
linkTemp, linkDegrade, failTemp, "VertTwoPartBar");
Register_Object(vertBarB);
leakGauge = new BitMapInverseWipe(ChildRate(), eng_mode, renderer_in, // @004c5b7c
engPort, 0x255, 0xe0, "eleak.pcc", 0, 0xff,
0 /*third: binary *(subsys+0x150); stored-unused*/, 3 /*frames*/,
coolantLeak, "LeakGauge");
Register_Object(leakGauge);
failedState = False; // @0xC4 this[0x31] (1 = destroyed)
subsystem = subsystem_in; // @0xC0 this[0x30]
previousDrawState = 0; // @0xC8 this[0x32]
Check_Fpu();
}
//
// @004c87dc -- dtor. Free the owned children (the binary frees them through
// the ReleaseChildren path @004c8820).
//
SubsystemCluster::~SubsystemCluster()
{
Check(this);
ReleaseChildren(); // @004c8820
}
//
// @004c8820 -- free each owned child gauge.
//
void
SubsystemCluster::ReleaseChildren()
{
if (background) { Unregister_Object(background); delete background; background = NULL; }
if (generatorVoltageBar){ Unregister_Object(generatorVoltageBar); delete generatorVoltageBar; generatorVoltageBar = NULL; }
if (temperatureBar) { Unregister_Object(temperatureBar); delete temperatureBar; temperatureBar = NULL; }
if (coolingLoopA) { Unregister_Object(coolingLoopA); delete coolingLoopA; coolingLoopA = NULL; }
if (coolingLoopB) { Unregister_Object(coolingLoopB); delete coolingLoopB; coolingLoopB = NULL; }
if (powerSourceA) { Unregister_Object(powerSourceA); delete powerSourceA; powerSourceA = NULL; }
if (powerSourceB) { Unregister_Object(powerSourceB); delete powerSourceB; powerSourceB = NULL; }
if (vertBarA) { Unregister_Object(vertBarA); delete vertBarA; vertBarA = NULL; }
if (vertBarB) { Unregister_Object(vertBarB); delete vertBarB; vertBarB = NULL; }
if (modeLamp) { Unregister_Object(modeLamp); delete modeLamp; modeLamp = NULL; }
if (stateLamp) { Unregister_Object(stateLamp); delete stateLamp; stateLamp = NULL; }
if (leakGauge) { Unregister_Object(leakGauge); 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 MFD children.
// POLARITY (PPC-dial fix, kept): the passed value is the DRAW STATE --
// 0 = alive (children run), nonzero = dead panel (children off).
//
void
SubsystemCluster::SetChildrenEnable(int enable)
{
if (temperatureBar) temperatureBar->Disable(enable != 0);
if (coolingLoopA) coolingLoopA->Disable(enable != 0);
if (powerSourceA) powerSourceA->Disable(enable != 0);
}
//
// @004c8990 -- the operational-state read. POLARITY (kept): 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] and is dropped -- a healthy subsystem reads
// ALIVE here until that field is decoded.
//
int
SubsystemCluster::GetDrawState()
{
return failedState ? 1 : 0;
}
Logical
SubsystemCluster::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004c88e4 -- Execute (shared by the whole family). failedState = the
// subsystem's own damage saturation (>= 1.0 destroys, mechdmg.cpp -- the
// PPC-dial polarity truth source; the binary read *(subsystem+0x40)==1).
// On a draw-state change repaint the panel frame: alive => frame(0) + redraw
// background bitmap; dead => black fill + frame(0xff); and forward the
// enable to the temperature / loop children.
//
void
SubsystemCluster::Execute()
{
int
state;
failedState = (SubsystemDamageLevelOf(subsystem) >= 1.0f) ? True : False;
state = GetDrawState();
if (state != previousDrawState)
{
previousDrawState = state;
SetChildrenEnable(state);
if (state == 0)
{
DrawPanelFrame(0);
if (background != NULL)
{
background->BecameActive(); // force the strip-art redraw
}
}
else
{
localView.SetColor(0); // vtbl+0x18
localView.MoveToAbsolute(1, 0x32); // vtbl+0x24
localView.DrawFilledRectangleToAbsolute(0x136, 0xad); // vtbl+0x48
DrawPanelFrame(0xff);
}
}
}
//
// (Re-host addition, kept from the decode: force the first-frame repaint on
// every activation -- the engine-base default would self-inactivate.)
//
void
SubsystemCluster::BecameActive()
{
previousDrawState = -1;
}
//###########################################################################
// 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.
//
// FEED LEDGER at this site:
// heatFailLamp <- own failedState LIVE
// hsFailLamp <- failFlag (the HUD subsystem's failure) LIVE (Execute)
// powerFailLamp <- binary subsys+0x324 INERT (unidentified
// structFailLamp <- binary subsys+0x320 1995-layout words;
// no recon member)
// heatLoad <- "HeatLoad" by name NAME-DEFERRED
// (binary subsys+0x31c == HeatableSubsystem::heatLoad;
// unpublished -> numeric reads 0 until the heat wave)
//
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)
{
GaugeConnection
*connection;
int
engPort = renderer_in->FindGraphicsPort(eng_port_name);
heatLoadScaled = HeatNumericBase; // @0xD8 (100.0f seed)
heatLoad = 0.0f; // @0xD4
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
Register_Object(heatFailLamp);
failSubsystem = hud; // @0xCC
failFlag = False; // @0xD0
hsFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x97, 0xe8, "btehfail.pcc", 0, 0xff,
(int *)&failFlag, "TwoState");
Register_Object(hsFailLamp);
powerFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x97, 0xc0, "btepfail.pcc", 0, 0xff,
&UnboundIntegerSource, "TwoState"); // INERT (binary subsys+0x324)
Register_Object(powerFailLamp);
structFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x97, 0x98, "btesfail.pcc", 0xff, 0, // (colour order = binary quirk)
&UnboundIntegerSource, "TwoState"); // INERT (binary subsys+0x320)
Register_Object(structFailLamp);
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");
Register_Object(heatNumeric);
connection = new GaugeConnectionDirectOf<Scalar>(0, &heatLoad,
ScalarAttributeOf(subsystem_in, "HeatLoad")); // NAME-DEFERRED
Register_Object(connection);
AddConnection(connection);
}
HeatSinkCluster::~HeatSinkCluster()
{
Check(this);
if (heatFailLamp) { Unregister_Object(heatFailLamp); delete heatFailLamp; heatFailLamp = NULL; }
if (hsFailLamp) { Unregister_Object(hsFailLamp); delete hsFailLamp; hsFailLamp = NULL; }
if (powerFailLamp) { Unregister_Object(powerFailLamp); delete powerFailLamp; powerFailLamp = NULL; }
if (structFailLamp) { Unregister_Object(structFailLamp); delete structFailLamp; structFailLamp = NULL; }
if (heatNumeric) { Unregister_Object(heatNumeric); delete heatNumeric; heatNumeric = NULL; }
}
//
// @004c8db0 -- Execute: failFlag = the HUD subsystem failed (the binary read
// *(failSubsystem+0x40)==1; re-hosted onto the same damage-saturation truth
// source the panel itself uses); heatLoadScaled = heatLoad * 100.0; chain
// SubsystemCluster::Execute.
//
void
HeatSinkCluster::Execute()
{
if (failSubsystem != NULL)
{
failFlag = (SubsystemDamageLevelOf(failSubsystem) >= 1.0f ||
SubsystemDestroyed(failSubsystem)) ? True : False;
}
heatLoadScaled = heatLoad * HeatLoadScale;
SubsystemCluster::Execute();
}
//###########################################################################
// MyomerCluster @004c8df4 (vtable 0x519f88)
//###########################################################################
//
// @004c8df4 -- ctor: SubsystemCluster base + a SeekVoltageGraph ("EngrGraph")
// fed by a GeneratorVoltageConnection (writes seekValue) + a seek-step
// OneOfSeveralInt (bteseek.pcc). Execute = base.
//
// The seek-step feed: the binary's +0x800 was OOB for a 0x358 Myomers
// (garbage); the decode re-pointed it at Myomers::currentSeekVoltageIndex
// (@0x320, INFERRED -- @004c8df4 isn't in the assert-anchored export). The
// re-host binds the same member BY NAME ("CurrentSeekVoltageIndex" --
// unpublished by the source410 Myomers table yet -> frame 0 until it lands).
//
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)
{
GaugeConnection
*connection;
int
engPort = renderer_in->FindGraphicsPort(eng_port_name);
seekValue = 0.0f; // @0xCC (connection dst)
seekGraph = (GraphicGauge *)new SeekVoltageGraph(ChildRate(), eng_mode, // @004c6798
renderer_in, engPort, (Entity *)subsystem_in,
(AttributeAccessor *)&seekValue, "edestryd.pcc", False, "EngrGraph");
Register_Object(seekGraph);
seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148
engPort, 0xf, 0, "bteseek.pcc", 1, 4,
IntegerAttributeOf(subsystem_in, "CurrentSeekVoltageIndex"),
"OneOfSeveralInt");
Register_Object(seekStepLamp);
//
// The graph's live input: the powering generator's voltage, resolved
// through the named voltageSource connection (LIVE).
//
connection = new GeneratorVoltageConnection(&seekValue, subsystem_in); // @004c3288
Register_Object(connection);
AddConnection(connection);
}
MyomerCluster::~MyomerCluster()
{
Check(this);
if (seekGraph) { Unregister_Object(seekGraph); delete seekGraph; seekGraph = NULL; }
if (seekStepLamp) { Unregister_Object(seekStepLamp); delete seekStepLamp; seekStepLamp = NULL; }
}
//###########################################################################
// WeaponCluster @004c8fc4 (vtable 0x519f38)
//###########################################################################
//
// @004c8fc4 -- ctor: SubsystemCluster base; interns the cluster image;
// centres a SegmentArc270 recharge dial (reads PercentDone -- PUBLISHED,
// LIVE) + 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)
{
GaugeConnection
*connection;
Scalar
*percentDoneAttr = ScalarAttributeOf(subsystem_in, "PercentDone"); // LIVE (mechweap.cpp 0x12)
//------------------------------------------------------------
// Intern the cluster image + derive the warning-lamp centre.
//------------------------------------------------------------
clusterImage = nameCopy(cluster_image); // @0xCC
L4Warehouse
*warehouse = (L4Warehouse *)renderer_in->warehousePointer;
BitMap
*img = warehouse->bitMapBin.Get(clusterImage); // ctor AddRef (dtor releases)
if (img == NULL)
{
DEBUG_STREAM << "WeaponCluster missing " << clusterImage << "\n";
warningCenterX = 0; // @0xDC
warningCenterY = 0; // @0xE0
}
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 (a real 0..1 fraction).
//------------------------------------------------------------
rechargeArc = (GraphicGauge *)new SegmentArc270(
(GaugeRate)Gauge::gaugeRate_A, mfd_mode,
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, percentDoneAttr, True,
"SegmentArc270");
Register_Object(rechargeArc);
configMap = new ConfigMapGauge(ChildRate(), mfd_mode, renderer_in, // @004c6d80
mfd_port, x + 0xee, y + 0x38, (Entity *)subsystem_in, "ConfigMap");
Register_Object(configMap);
warningState = 0; // @0xE4
connection = new GaugeConnectionDirectOf<Scalar>(0, &percentDone, percentDoneAttr);
Register_Object(connection);
AddConnection(connection);
}
WeaponCluster::~WeaponCluster()
{
Check(this);
L4Warehouse
*warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(clusterImage);
Unregister_Pointer(clusterImage);
delete[] clusterImage;
clusterImage = NULL;
if (rechargeArc) { Unregister_Object(rechargeArc); delete rechargeArc; rechargeArc = NULL; }
if (configMap) { Unregister_Object(configMap); delete configMap; configMap = NULL; }
}
//
// @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 (draw-state 0 = alive panels only).
//
// PPC-dial REPAINT FIX (kept on re-host): 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, dot still toggles"). Detect the repaint and
// reset the arc / configMap / lamp draw memory so they fully redraw.
//
void
WeaponCluster::Execute()
{
int
preDrawState = previousDrawState;
SubsystemCluster::Execute();
if (previousDrawState != preDrawState)
{
if (rechargeArc) rechargeArc->BecameActive();
if (configMap) configMap->BecameActive();
warningState = -2; // lamp: force redraw too
}
if (getenv("BT_VIS_LOG"))
{
static int weaponWarnSamples = 0;
if (weaponWarnSamples < 16)
{
++weaponWarnSamples;
DEBUG_STREAM << "[warn] " << identificationString
<< " drawState=" << previousDrawState
<< " percentDone=" << percentDone
<< " warnState=" << warningState << "\n" << 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",
// live cursor) + a seek-step OneOfSeveralInt (bteseek.pcc).
//
// The live-cursor voltage: the binary bound the emitter's "OutputVoltage"
// attribute (the raw charge voltage, binary currentLevel @0x414). Our
// recon publishes that SAME member as "ChargeLevel" (emitter.hpp:159, the
// authentic 0x1D row) -- so the authentic name is tried first and the
// source410 alias second: the cursor is LIVE on every emitter/PPC today.
//
// The seek-step: binary subsys+0x3f0 == Emitter::seekVoltageIndex, bound BY
// NAME ("CurrentSeekVoltageIndex" -- unpublished by the source410 Emitter
// table yet -> frame 0 until that row lands).
//
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");
if (outputVoltage == NULL)
{
outputVoltage = AttributePointerOf(subsystem_in, "ChargeLevel"); // source410 alias
}
seekGraph = new SeekVoltageGraph(ChildRate(), eng_mode, renderer_in, // @004c6798
engPort, (Entity *)subsystem_in, (AttributeAccessor *)outputVoltage,
"edestryd.pcc", True, "EngrGraph");
Register_Object(seekGraph);
seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in, // @004c5148
engPort, 0xf, 0, "bteseek.pcc", 1, 4,
IntegerAttributeOf(subsystem_in, "CurrentSeekVoltageIndex"),
"OneOfSeveralInt");
Register_Object(seekStepLamp);
}
EnergyWeaponCluster::~EnergyWeaponCluster()
{
Check(this);
if (seekGraph) { Unregister_Object(seekGraph); delete seekGraph; seekGraph = NULL; }
if (seekStepLamp) { Unregister_Object(seekStepLamp); delete seekStepLamp; seekStepLamp = NULL; }
}
//###########################################################################
// BallisticWeaponCluster @004c9558 (vtable 0x519e98)
//###########################################################################
//
//
// The linked-bin round count for the ammo readout: ProjectileWeapon
// resolves its own bin (the link is protected), so the cluster asks the
// weapon. -1 (no bin) reads as 0 rounds on the dial.
//
static int
AmmoRoundsOf(Subsystem *subsystem)
{
if (subsystem == NULL
|| !subsystem->IsDerivedFrom(ProjectileWeapon::ClassDerivations))
{
return 0;
}
int
rounds = ((ProjectileWeapon *)subsystem)->GetAmmoCount();
return (rounds > 0) ? rounds : 0;
}
// @004c9558 -- ctor: WeaponCluster base + the ammo panel: two
// NumericDisplayIntegers (ammo counts), jam/fire TwoStates, a
// NumericDisplayScalarTwoState (reload seconds), a destroyed TwoState, plus
// the erase tracker and eject wipe.
//
// FEED LEDGER at this site:
// jam lamp <- jammed = GetWeaponState()==JammedState LIVE (Execute)
// destroyed X <- own failedState LIVE
// ammo counters <- INERT 0: the weapon's ammo store is
// ProjectileWeapon::ammoBinLink (PROJWEAP.HPP:102) --
// protected, no accessor, no published attribute; the
// AmmoBin accessors (GetAmmoCount/GetAmmoState) are
// unreachable without the link. (The binary read the
// resolved bin's count.)
// fire lamp / <- INERT 0: the feed/reload state (binary bin+0x18c) has
// reload numeric no recon member either.
// ammoErase <- NULL: GraphicsViewRecord tracker (binary @0044ad78) --
// bring-up NULL kept; the numeric self-erases.
// ejectWipe <- NULL: BitMapInverseWipeScalar (@004c61c8) is not
// reconstructed in btl4gaug (notes-only); bring-up NULL.
// reload timer <- left 0 (the binary's frame clock DAT_0052140c
// equivalent is not wired).
//
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)
{
int
engPort = renderer_in->FindGraphicsPort(eng_port_name);
//
// LIVE: the linked bin's rounds, refreshed by Execute.
//
ammoRounds = AmmoRoundsOf(subsystem_in);
int
*ammoValue = &ammoRounds;
reloadStartTime = 0; // @0xF4
reloadSeconds = 0.0f; // @0xF8
jammed = False; // @0xE8
firing = False; // @0xEC
reloading = False; // @0xF0
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");
Register_Object(ammoCountA);
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");
Register_Object(ammoCountB);
jamLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, // @004739d8
engPort, 0xa0, 0xb2, "btejam.pcc", 0, 0xff, (int *)&jammed, "TwoState");
Register_Object(jamLamp);
fireLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
engPort, 0x121, 0xb6, "btefire.pcc", 0, 0xff, (int *)&firing, "TwoState");
Register_Object(fireLamp);
ammoErase = NULL; // @0xFC (bring-up NULL, see ledger)
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");
Register_Object(ammoNumeric);
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
Register_Object(destroyedLamp);
ejectWipe = NULL; // @0x110 (bring-up NULL, see ledger)
}
BallisticWeaponCluster::~BallisticWeaponCluster()
{
Check(this);
if (ammoNumeric) { Unregister_Object(ammoNumeric); delete ammoNumeric; ammoNumeric = NULL; }
if (ammoCountA) { Unregister_Object(ammoCountA); delete ammoCountA; ammoCountA = NULL; }
if (ammoCountB) { Unregister_Object(ammoCountB); delete ammoCountB; ammoCountB = NULL; }
if (jamLamp) { Unregister_Object(jamLamp); delete jamLamp; jamLamp = NULL; }
if (fireLamp) { Unregister_Object(fireLamp); delete fireLamp; fireLamp = NULL; }
if (destroyedLamp) { Unregister_Object(destroyedLamp); delete destroyedLamp; destroyedLamp = NULL; }
delete ammoErase; ammoErase = NULL; // (never allocated today)
delete ejectWipe; ejectWipe = NULL;
}
//
// @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: jammed = the weapon-state alarm reads Jammed (LIVE
// through GetWeaponState -- the binary's raw *(subsys+0x364)==5 retired);
// the ammo-bin feed/reload state is INERT (see the ctor ledger); chain
// WeaponCluster::Execute.
//
void
BallisticWeaponCluster::Execute()
{
jammed = WeaponJammedOf(subsystem);
ammoRounds = AmmoRoundsOf(subsystem);
//
// INERT: reload/feed state (binary bin+0x18c) -- no recon member; the
// fire lamp stays dark and the reload numeric stays gated off.
//
firing = False;
reloading = False;
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) == lit digits on black
// dot PRESENT (on!=0) -> SetColors(bg=0xff, fg=0) == dark digits cut out
// of the solid 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 FUN_004c932c.)
//
void
BallisticWeaponCluster::DrawWarningLamp(int on)
{
WeaponCluster::DrawWarningLamp(on); // @004c932c: draw the dot
if (ammoCountA)
{
((NumericDisplayInteger *)ammoCountA)->SetColors(
on ? 0xff : 0,
on ? 0 : 0xff);
}
}
//
// @004c9adc -- TestInstance. The binary: True unless the ammo bin is
// present with rounds and the weapon isn't jammed (then the base test runs).
// The bin is unreachable on the re-host (see the ctor ledger), so the
// jam-side of the predicate is kept and the ammo-empty gate is deferred
// with the ammo feed.
//
Logical
BallisticWeaponCluster::TestInstance() const
{
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 aux screen - 1. The
// recovered mask constants resolve exactly onto the authentic BTL4ModeManager
// names (BTL4MODE.HPP): planeMask = the MFD quad plane, subBit = the
// engineering sub-screen plane.
//
struct AuxScreenGeometry
{
int
x, y;
ModeMask
planeMask;
ModeMask
subBit;
const char
*mfdPortName;
const char
*engPortName;
int
planeIndex;
};
static const AuxScreenGeometry
auxScreenGeometry[12] =
{
{ 0, 0xf0, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng1, "mfd1", "eng1", 0 }, // grp 1
{ 0x142, 0xf0, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng2, "mfd1", "eng1", 0 }, // grp 2
{ 0, 0x1c, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng3, "mfd1", "eng1", 0 }, // grp 3
{ 0x142, 0x1c, BTL4ModeManager::ModeMFD1Quad, BTL4ModeManager::ModeMFD1Eng4, "mfd1", "eng1", 1 }, // grp 4
{ 0, 0xf0, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng1, "mfd2", "eng2", 1 }, // grp 5
{ 0x142, 0xf0, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng2, "mfd2", "eng2", 1 }, // grp 6
{ 0, 0x1c, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng3, "mfd2", "eng2", 1 }, // grp 7
{ 0x142, 0x1c, BTL4ModeManager::ModeMFD2Quad, BTL4ModeManager::ModeMFD2Eng4, "mfd2", "eng2", 2 }, // grp 8
{ 0, 0xf0, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng1, "mfd3", "eng3", 2 }, // grp 9
{ 0x142, 0xf0, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng2, "mfd3", "eng3", 2 }, // grp 10
{ 0, 0x1c, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng3, "mfd3", "eng3", 2 }, // grp 11
{ 0x142, 0x1c, BTL4ModeManager::ModeMFD3Quad, BTL4ModeManager::ModeMFD3Eng4, "mfd3", "eng3", 2 }, // grp 12
};
//
// @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.
//
// STAND-INS (see the aux-screen ledger): the screen number is the sequential
// roster assignment; the label .pcc is absent (NULL -> the panel-frame label
// blit is skipped). The energy branch's cluster-image pick keyed
// *(sub+0x334)==0 -> qcircle / else qcircr in the binary; that word is
// BEST-EFFORT identified as the rear-firing flag and read through the
// PUBLISHED "RearFiring" attribute here.
//
Logical
VehicleSubSystems::Make(
int /*display_port_index*/,
Vector2DOf<int> /*position*/,
Entity *entity,
GaugeRenderer *gauge_renderer
)
{
L4GaugeRenderer
*renderer = (L4GaugeRenderer *)gauge_renderer;
char
*imageNames[8];
int
p,
i,
count;
Check(gauge_renderer);
Check(entity);
//------------------------------------------------------------
// The 8 status strip bitmaps (qXXX0.pcc..qXXX7.pcc).
//------------------------------------------------------------
for (p = 0; p < 8; p++)
{
imageNames[p] = methodDescription.parameterList[p].data.string;
}
count = entity->GetSubsystemCount();
for (i = 1; i < count; i++) // slot 0 = the controls mapper
{
Subsystem
*sub = entity->GetSubsystem(i);
if (sub == NULL || !IsPanelSubsystem(sub))
{
continue;
}
int
auxScreen = AuxScreenAssignmentOf(entity, sub),
group = auxScreen - 1;
if (group < 0)
{
DEBUG_STREAM << "Auxiliary screen position = zero\n";
continue;
}
if (group >= 12)
{
DEBUG_STREAM << "Illegal auxiliary screen position =" << group << "\n";
continue;
}
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[gau2] cluster slot=" << i
<< " classID=" << (int)sub->GetClassID()
<< " auxScreen=" << auxScreen
<< " name=" << sub->GetName() << endl << flush;
}
const AuxScreenGeometry
&g = auxScreenGeometry[group];
int
mfdPort = renderer->FindGraphicsPort(g.mfdPortName);
char
*label = NULL, // STAND-IN: no live auxScreenLabel
*tile = (char *)"btwarn.pcc";
switch (sub->GetClassID())
{
case RegisteredClass::SensorClassID: // 0xBC3 -> HeatSinkCluster (+ "HUD")
{
Subsystem
*hud = entity->FindSubsystem("HUD");
HeatSinkCluster
*cluster = new HeatSinkCluster(g.planeMask, renderer, 0, mfdPort,
g.x, g.y, sub, hud, g.subBit, g.engPortName, tile, label,
"edestryd.pcc", imageNames, "SensorCluster");
Register_Object(cluster);
break;
}
case RegisteredClass::MyomersClassID: // 0xBC6 -> MyomerCluster
{
MyomerCluster
*cluster = new MyomerCluster(g.planeMask, renderer, 0, mfdPort,
g.x, g.y, sub, g.subBit, g.engPortName, tile, label,
imageNames, "MyomerCluster");
Register_Object(cluster);
break;
}
case RegisteredClass::EmitterClassID: // 0xBC8
case RegisteredClass::PPCClassID: // 0xBD4
{
//
// BEST-EFFORT (binary *(sub+0x334)==0): the ring-art pick, read
// through the published RearFiring attribute.
//
const char
*clusterImage = "qcircle.pcc";
Logical
*rearFiring = (Logical *)AttributePointerOf(sub, "RearFiring");
if (rearFiring != NULL && *rearFiring != 0)
{
clusterImage = "qcircr.pcc";
}
EnergyWeaponCluster
*cluster = new EnergyWeaponCluster(g.planeMask, renderer, 0, mfdPort,
g.x, g.y, sub, g.subBit, g.engPortName, tile, label,
clusterImage, imageNames, "EnergyWeaponCluster");
Register_Object(cluster);
break;
}
case RegisteredClass::ProjectileWeaponClassID: // 0xBCD
case RegisteredClass::MissileLauncherClassID: // 0xBD0
{
BallisticWeaponCluster
*cluster = 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");
Register_Object(cluster);
break;
}
case RegisteredClass::GaussRifleClassID: // 0xBCE -- not yet supported
DEBUG_STREAM << "Gauss rifle not yet supported\n";
break;
default:
break;
}
}
return True;
}
//
// The registered method: config primitive "vehicleSubSystems(q0..q7.pcc)".
// KEYWORD + PARAMETER LIST ARE PARSE-CRITICAL.
//
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
PARAMETER_DESCRIPTION_END
}
};
// === btl4gau3.cpp begins at @004c9bd0 (PlayerStatusMappingGroup) -- not this TU.