Files
TeslaRel410/restoration/source410/BT_L4/BTL4GAU2.CPP
T
CydandClaude Fable 5 945812fef1 BT410 5.3.37: the panel title banner was off by exactly one row
y+0xb3 -> y+0xb4.  Earlier this session I measured 0xb3 as the optimum and
wrote that the banner was at most 1-2px off -- that reading was confounded:
the strip-art misplacement (5.3.35) was ten times larger and overlapped the
same bands, so it dominated the metric and hid the one-row error.  With the
strip art correct the title fringe isolated cleanly at 553/553 missing/extra
per head, and +1 collapsed it.

  head   missing         extra
  Mfd1   1107 ->  140    1646 ->  679
  Mfd2   1616 ->   97    1981 ->  462
  Mfd3    584 ->   31    1014 ->  461

Cockpit: 97.9% identical / 97% coverage, from 89.7%/92% at the start of the
session.  Method note for the worksheet: fix the largest error on a head
FIRST, then re-measure the small ones -- a big overlapping defect makes a
small one measure as noise, or as already-optimal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:42:18 -05:00

3119 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
<< " 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.