//===========================================================================//
// File:	btl4gau2.cpp                                                        //
// Project: BattleTech          Brick: Gauge Renderer Manager                 //
// Contents: Cockpit instrument library, part 2 -- composite panel gauges      //
//           (see btl4gau2.hpp).                                               //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
// 02/22/96  CPB  Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1996, Virtual World Entertainment, Inc. All rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BTL4OPT.EXE).  Behaviour follows the
// Ghidra pseudo-C in recovered/all/part_014.c (@004c6798..@004c9b50).  Class /
// member names taken from the CODE name-string pool; per-method @ADDR evidence
// is cited inline.  This TU links between btl4gaug.obj and btl4gau3.obj.
//
// Engine-internal helper map (consistent with btl4gaug.cpp / btl4rdr.cpp):
//      FUN_00444818 GraphicGauge ctor    FUN_00444870 GraphicGauge dtor
//      FUN_004448ac GraphicGauge::TestInstance
//      FUN_004440d4 Gauge-group ctor     FUN_00443f7c Gauge-group dtor
//      FUN_0044ae0c ChainOf<Gauge*> dtor (PrepEngrScreen child list)
//      FUN_004442b8 default GaugeRate    FUN_004442dc default ModeMask
//      FUN_00444290 alt default rate/mask
//      FUN_004700ac GaugeRenderer::InternName       FUN_004022e8 FreeName
//      FUN_00442aec/00442c12 BitMapCache  AddRef/Release (renderer+0x4C, +4)
//      FUN_00442d2b/00442e51 PixMapCache  AddRef/Release (renderer+0x4C, +0x20)
//      FUN_0041bfc0 Entity::FindAttributeIndex(name)
//      FUN_0041f98c Entity::FindSubObject(name)
//      FUN_00447f1c GaugeRenderer::InstallPort / FUN_00447f84 GetGraphicsPort
//      FUN_00417ab4 SharedData::Resolve / operator Entity*
//      FUN_0041a1a4 IsDerivedFrom(classDerivations)  (0x50f4bc = Generator)
//      FUN_0042076c Subsystem::FindDamageZone(record) -> index
//      FUN_004dcd94 Round(Scalar) -> int
//      FUN_004700bc NumericDisplay ctor   FUN_0047018c ::dtor
//      FUN_004703f4 NumericDisplay::Reset FUN_00470430 ::DrawAt FUN_00470758 ::Erase
//      FUN_00470ec8 NumericDisplay::SetColors  FUN_00470cfc NumericDisplayInteger ctor
//      FUN_00470888 NumericDisplayScalar ctor   FUN_00473c94 NumericDisplayScalarTwoState
//      FUN_004739d8 TwoState ctor        FUN_00471d00 BackgroundBitmap ctor
//      FUN_00472ef0 BarGraph ctor        FUN_00472fb4 BarGraph dtor
//      FUN_00474855 GaugeConnectionDirectOf<Scalar>::ctor  FUN_004749de <int>
//      FUN_004c2ec4 DrawClippedBitmap (file-private, btl4gaug)  FUN_004c2f88 -> 7
//      FUN_004c3134/004c31ec/004c3288  shared GaugeConnection ctors (btl4gaug TU)
//      FUN_004c4170 HorizTwoPartBar  FUN_004c4724 VertTwoPartBar
//      FUN_004c4d88 OneOfSeveral     FUN_004c5148 OneOfSeveralInt
//      FUN_004c5470 OneOfSeveralStates  FUN_004c5b7c LeakGauge(BitMapInverseWipe)
//      FUN_004c61c8 BitMapInverseWipeScalar  FUN_004c6244 SegmentArc270
//      FUN_004c3b98 ColorMapperArmor ctor (btl4gaug)
//      FUN_0040385c Verify   FUN_004dbb24 DebugStream<<char*  FUN_004db78c <<int
//      DAT_00524e20 DebugStream (warning channel)   DAT_004efc94 the Application
//
// Recovered constant-pool values:
//      0x43660000=230.0f / 0x433b0000=187.0f  SeekVoltageGraph x/y scale
//      0x461c3c00=9999.0f  BecameActive "force redraw" sentinel
//      0x463b8000=12000.0f generator max voltage
//      0x42c80000=100.0f   HeatSinkCluster numeric scale base
//      _DAT_004c8df0 heat numeric scale     _DAT_004c92e0 warning threshold
//      _DAT_004c6bdc/_DAT_004c6be0 seek-curve plot step/limit
//      DAT_0051936c PrepEngr engineer-screen number (1..12)
//

// See btl4gaug.cpp: suppress heat.hpp's optional DebugStream so the shared
// mechrecon ReconStream definition is the only one active in this TU.
#define BT_DEBUGSTREAM_DEFINED

#include <bt.hpp>
#pragma hdrstop

#if !defined(BTL4GAU2_HPP)
#	include <btl4gau2.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif

static const Scalar SeekVoltageXScale   = 230.0f;	// 0x43660000
static const Scalar SeekVoltageYScale   = 187.0f;	// 0x433b0000
static const Scalar RedrawSentinel      = 9999.0f;	// 0x461c3c00
static const Scalar GeneratorMaxVoltage = 12000.0f;	// 0x463b8000
static const Scalar HeatNumericBase     = 100.0f;	// 0x42c80000

// (DebugStream warning channel, DAT_00524e20, is provided by the shared
//  reconstruction headers -- heat.hpp / mechrecon.hpp.)

//
// Unrecovered .data resource pool used by GeneratorCluster::Make.  Concrete
// bytes were not recovered from the optimised image; typed placeholders so the
// resolve-and-construct path compiles.  (BEST-EFFORT.)
//
static char DAT_00518f24[] = "GeneratorCluster";	// subsystem name to resolve
static int  DAT_00518ee0 = 0;						// GaugeRate
static char DAT_00518f68[] = "";					// background image name

//
// Original FUN_0041bfc0 = Entity::FindAttributeIndex(name) -> ordinal.  The
// WinTesla engine exposes attribute lookup via Simulation::AttributeIndexSet
// (Find/FindEntry by name) rather than a name->ordinal call, so this best-effort
// shim stands in for it; the stored indices are consumed by the (summarised)
// SeekVoltageGraph::Execute plot.
//
static int FindAttributeIndex(Entity *, const char *) { return -1; }

//
// FUN_0041bfc0 == Entity::FindAttributeIndex(name) returns a POINTER into the
// subsystem (subsystem + attributeMemberOffset), or 0 on miss.  The composite
// cluster gauges pass that pointer straight to their child gauges as the value
// source.  The WinTesla engine exposes exactly this via Simulation::
// GetAttributePointer(const char*) (SIMULATE.h), which returns a member pointer
// or &Simulation::NullAttribute (always safe to deref) -- so we resolve through
// it.  A null subsystem -> 0.
//
static void *AttributePointerOf(void *subsystem, const char *name)
{
	if (subsystem == NULL)
		return NULL;
	void *p = ((Simulation *)subsystem)->GetAttributePointer(name);	// FUN_0041bfc0
	// GetAttributePointer returns &NullAttribute (not 0) on a miss; the composite
	// ctors gate optional children on a 0 result, so normalise the miss to NULL.
	if (p == (void *)&Simulation::NullAttribute)
		return NULL;
	return p;
}

//
// FUN_00417ab4 -- SubsystemConnection::Resolve: follow the plug's bound link to the
// resolved subsystem.  The link is `*(plug+8)`; the resolved subsystem is
// `*(link+8)`.  An unbound plug (link == 0) resolves to 0 -- the authentic
// behaviour when a subsystem has no attached voltage source / cooling master (the
// connection then reads its "no source" branch, *destination = 0).
//
static void *ResolveLink(void *plug)
{
	if (plug == 0)
		return 0;
	void *link = *(void **)((char *)plug + 8);
	if (link == 0)
		return 0;
	return *(void **)((char *)link + 8);
}

//
// Bridge (defined in powersub.cpp) that reads the engineering-screen assignment
// off the real PoweredSubsystem type (auxScreenNumber/Placement/Label) -- the
// reconstructed heat-leaf branch is not byte-exact, so the raw sub+0x1dc/0x1e0/
// 0x1e4 offsets read garbage; the named fields (populated by the PoweredSubsystem
// ctor from the resource) are correct.  Returns False for non-PoweredSubsystem-
// derived subsystems (== the vehicleSubSystems type filter).
//
extern bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64);

//
// FUN_0041bf44 -- GetAttributePointer(index): a pointer to the subsystem's Nth
// attribute member (the cooling-loop lamp reaches its cooling-master link at
// attribute slot 3), or 0 if the index is out of range.  The engine exposes this
// via Simulation::GetAttributePointer(AttributeID); a miss returns &NullAttribute,
// normalised to 0.
//
static void *FindLinkedSubsystem(void *subsystem, int slot)
{
	if (subsystem == 0)
		return 0;
	void *p = ((Simulation *)subsystem)->GetAttributePointer((Simulation::AttributeID)slot);
	if (p == (void *)&Simulation::NullAttribute)
		return 0;
	return p;
}

//
// FUN_0041a1a4 IsDerivedFrom(0x50f4bc == PoweredSubsystem's ClassDerivations).
// Gates the SubsystemCluster's generator stateLamp.  Every subsystem that reaches
// a SubsystemCluster passed the vehicleSubSystems type filter (BTGetSubsystemAuxScreen
// -> the SAME IsDerivedFrom(PoweredSubsystem) check), so this is always True here;
// the real check is done in powersub.cpp via BTIsPoweredSubsystem for robustness.
//
extern bool BTIsPoweredSubsystem(Subsystem *sub);
static Logical IsGeneratorDerived(Subsystem *sub) { return BTIsPoweredSubsystem(sub) ? True : False; }


//###########################################################################
//   File-private GaugeConnection classes for the composite-cluster lamps.
//   Each is the reconstruction of one of the three shared connection ctors
//   the sub-gauges attach (mirrors the btl4gaug.cpp connection pattern: derive
//   GaugeConnection, store dst/src, override Update() = the vtable sampler).
//###########################################################################

//
// CoolingLoopConnection -- @004c3134 ctor / @004c31a0 sampler (vt 0x518ea8).
// When the source subsystem's coolant loop is active (source+0x134 == 1) it
// follows the linked cooling master (FindLinkedSubsystem(source,3) then Resolve)
// and copies its loop-lamp state (+0x1d4); otherwise 0.
//
class CoolingLoopConnection : public GaugeConnection
{
public:
	CoolingLoopConnection(int *destination, void *source)
		: GaugeConnection(0),					// FUN_00444124(this,0)
		  source(source), destination(destination) {}
	void Update()								// @004c31a0
	{
		void *resolved = 0;
		if (source != 0 && *(int *)((char *)source + 0x134) == 1)
		{
			void *linked = FindLinkedSubsystem(source, 3);
			if (linked != 0)
				resolved = ResolveLink(linked);
		}
		*destination = (resolved == 0) ? 0 : *(int *)((char *)resolved + 0x1d4);
	}
protected:
	void	*source;							// @0x10  this[4]
	int		*destination;						// @0x14  this[5]
};

//
// PowerSourceConnection -- @004c31ec ctor / @004c3258 sampler (vt 0x518e9c).
// Resolves the power source (Resolve(source)) and copies its bus-lamp state
// (+0x1e0); 0 if unresolved.
//
class PowerSourceConnection : public GaugeConnection
{
public:
	PowerSourceConnection(int *destination, void *source)
		: GaugeConnection(0), source(source), destination(destination) {}
	void Update()								// @004c3258
	{
		void *resolved = ResolveLink(source);
		*destination = (resolved == 0) ? 0 : *(int *)((char *)resolved + 0x1e0);
	}
protected:
	void	*source;							// @0x10
	int		*destination;						// @0x14
};

//
// GeneratorVoltageConnection -- @004c3288 ctor / @004c32f4 sampler (vt 0x518e90).
// Resolves the voltage source and copies its output voltage (+0x1dc); 0 if
// unresolved.  Drives ScalarBarGauge / SeekVoltageGraph value slots.
//
class GeneratorVoltageConnection : public GaugeConnection
{
public:
	GeneratorVoltageConnection(Scalar *destination, void *source)
		: GaugeConnection(0), source(source), destination(destination) {}
	void Update()								// @004c32f4
	{
		void *resolved = ResolveLink(source);
		*destination = (resolved == 0) ? 0.0f
									   : *(Scalar *)((char *)resolved + 0x1dc);
	}
protected:
	void	*source;							// @0x10
	Scalar	*destination;						// @0x14
};


//###########################################################################
//###########################################################################
//   SeekVoltageGraph                @004c6798
//###########################################################################
//###########################################################################

//
// @004c6798 -- ctor (vtable PTR_FUN_0051a1fc).  GraphicGauge base, interns and
// ref-counts the "destroyed" bitmap, resolves the four SeekVoltage attribute
// indices off the weapon subsystem, sets the port extent (0x97,0x80,0x17d,0x13b)
// and stashes the 230x187 plot scale.
//
SeekVoltageGraph::SeekVoltageGraph(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer,
	int					graphics_port_number,
	Entity				*subsystem,
	AttributeAccessor	*seek_voltage_pointer,
	const char			*destroyed_image,
	Logical				draw_cursor,
	const char			*identification_string
):
	GraphicGauge(rate, mode_mask, renderer, /*owner*/0xFFFF,
		graphics_port_number, identification_string)	// FUN_00444818
{
	// Original interned a copy of the name (FUN_004700ac) + AddRef'd it in the
	// bitmap cache; retain the persistent resource string and AddRef the bitmap.
	destroyedImage   = (char *)destroyed_image;
	renderer->warehousePointer->bitMapBin.Get(destroyedImage);		// FUN_00442aec (AddRef)

	currentSeekIndex = FindAttributeIndex(subsystem, "CurrentSeekVoltageIndex");	// FUN_0041bfc0
	minSeekIndex     = FindAttributeIndex(subsystem, "MinSeekVoltageIndex");
	maxSeekIndex     = FindAttributeIndex(subsystem, "MaxSeekVoltageIndex");
	seekVoltageTable = FindAttributeIndex(subsystem, "SeekVoltage");

	voltageSubsystem = (Entity *)seek_voltage_pointer;	// this[0x29]
	this->subsystem  = (Subsystem *)subsystem;			// this[0x2A]

	// graphicsView -> GraphicGauge::localView.  SetExtent == SetPositionWithinPort;
	// the original SetClip() (vtbl+0x0C) constrained drawing to that extent.
	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]
}

//
// @004c68ac -- dtor.  Release the destroyed bitmap, GraphicGauge::~GraphicGauge.
//
SeekVoltageGraph::~SeekVoltageGraph()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	warehouse->bitMapBin.Release(destroyedImage);
}

Logical SeekVoltageGraph::TestInstance() const
{
	return GraphicGauge::TestInstance();
}

//
// @004c6920 -- BecameActive: previousVoltage = 9999.0f (force first redraw).
//
void SeekVoltageGraph::BecameActive()
{
	previousVoltage = RedrawSentinel;				// @0xAC  force first redraw
	destroyedShown  = 0;
}

//
// @004c6934 -- Execute (the seek-voltage curve plot).  BRING-UP: the full plot
// (SetupClip/DrawTicks/DrawCursor over a 0..1 voltage polyline) needs the live
// seek-voltage sampler (subsys vtbl+0x3c) + the four attribute indices; the
// attribute resolve is summarised (FindAttributeIndex returns -1), so the plot is
// deferred and Execute is inert until the sampler + attribute reads land.  Kept as
// a defined no-op so the gauge's vtable is complete (no /FORCE crash when the
// Myomer/Energy cluster ticks its SeekVoltageGraph child).
//
void SeekVoltageGraph::Execute()
{
	// (seek-voltage plot deferred -- see note above.)
}

//
// @004c6934 (original notes preserved) -- Execute.  If the subsystem is destroyed
// (subsys+0x40 == 1) draw
// the centred "destroyed" bitmap once.  Otherwise, when the live seek voltage
// (subsys vtbl+0x3c sampler) changes: clear (SetupClip @004c6be4), re-plot the
// 0..1 voltage curve as a polyline (step _DAT_004c6bdc up to _DAT_004c6be0,
// Round each point), then draw the min/max tick marks (DrawTicks @004c6c6c)
// and, if drawCursor, the live cursor at the current seek index
// (DrawCursor @004c6c30).
//


//###########################################################################
//   ConfigMapGauge                  @004c6d80
//###########################################################################

//
// @004c6d80 -- ctor (vtable PTR_FUN_0051a1b8).  GraphicGauge base; interns the
// joystick base bitmap "btjoy.pcc" (BitMapCache AddRef) + the four config-state
// pixmaps cm_off/cm_other/cm_only/cm_both.pcc (PixMapCache AddRef); sets the port
// origin (x,y); subsystem = param_9; color = 0.
//
ConfigMapGauge::ConfigMapGauge(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer_in,
	int					graphics_port_number,
	int					x,
	int					y,
	Entity				*subsystem_in,
	const char			*identification_string
):
	GraphicGauge(rate, mode_mask, renderer_in, 0 /*owner*/,		// FUN_00444818
		graphics_port_number, identification_string)
{
	joystickImage = (char *)"btjoy.pcc";			// @0x98  this[0x26]
	stateImage[0] = (char *)"cm_off.pcc";			// @0x9C  this[0x27]
	stateImage[1] = (char *)"cm_other.pcc";			// @0xA0  this[0x28]
	stateImage[2] = (char *)"cm_only.pcc";			// @0xA4  this[0x29]
	stateImage[3] = (char *)"cm_both.pcc";			// @0xA8  this[0x2A]

	localView.SetOrigin(x, y);						// vtbl+0x10 (this+0x48)

	color     = 0;									// @0x94  this[0x25]
	subsystem = subsystem_in;						// @0x90  this[0x24]

	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
	warehouse->bitMapBin.Get(joystickImage);		// FUN_00442aec
	for (int i = 0; i < 4; i++)
		warehouse->pixelMap8Bin.Get(stateImage[i]);	// FUN_00442d2b
}

//
// @004c6e54 -- dtor: release btjoy bitmap + 4 pixmaps, GraphicGauge::~GraphicGauge.
//
ConfigMapGauge::~ConfigMapGauge()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	warehouse->bitMapBin.Release(joystickImage);	// FUN_00442c12
	for (int i = 0; i < 4; i++)
		warehouse->pixelMap8Bin.Release(stateImage[i]);	// FUN_00442e51
}

Logical
	ConfigMapGauge::TestInstance() const
{
	return GraphicGauge::TestInstance();
}

//
// @004c6ee0 -- SetColor: color (this[0x25]) = the passed value.
//
void
	ConfigMapGauge::SetColor(int color_in)			// @004c6ee0
{
	color = color_in;
}

//
// @004c6ef4 -- BecameActive: force a full redraw (dirty=1, previousState = -1).
//
void
	ConfigMapGauge::BecameActive()
{
	dirty = True;									// @0xBC  this[0x2F]
	for (int i = 0; i < 4; i++)
		previousState[i] = -1;						// @0xAC  this[0x2B..0x2E]
}

//
// @004c6f1c -- Execute.  When enabled (color != 0): on the dirty flag, blit the
// base joystick bitmap; then for each of the 4 config-map slots read the control-
// mapper state and, if changed, stretch-blit the matching cm_* pixmap.
//
// BRING-UP (marked): the per-slot control-mapper state comes from the App's
// ModeManager sampled through a .data port/colour table (DAT_00518eb8) at raw
// offsets (app+0x3c -> +0x1c0 + slot*0x20, sampler vtbl+0x24).  That is the same
// raw-App-offset read class that AVs before the ModeManager layout is
// reconstructed (see the FillPilotArray fix, §10), and DAT_00518eb8 was not
// recovered from the optimised image -- so the mapper-state loop is guarded off
// and only the base-bitmap blit runs.  The full loop lands with the ModeManager
// reconstruction.
//
void
	ConfigMapGauge::Execute()
{
	if (color == 0)
		return;

	if (dirty)
	{
		dirty = False;
		L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
		BitMap *base = warehouse->bitMapBin.Get(joystickImage);	// FUN_00442aec
		if (base != NULL)
		{
			localView.SetColor(0xFF);						// vtbl+0x18
			localView.DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, base);	// vtbl+0x54
		}
		warehouse->bitMapBin.Release(joystickImage);
	}
	// (control-mapper state loop deferred -- see note above.)
}


//###########################################################################
//   OneOfSeveral data-driven lamps  @004c70a4 / @004c7160
//###########################################################################

//
// @004c70a4 -- AnimatedSubsystemLamp ctor (vtable PTR_FUN_0051a174): OneOfSeveral
// (fromImageStrip=1) + a CoolingLoopConnection feeding the selected frame from
// the subsystem's cooling-loop state.  Otherwise inherits OneOfSeveral::Execute.
//
AnimatedSubsystemLamp::AnimatedSubsystemLamp(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer_in,
	int					graphics_port_number,
	int					x,
	int					y,
	const char			*image,
	int					columns,
	int					rows,
	void				*subsystem,
	const char			*identification_string
):
	OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number,		// FUN_004c4d88
		x, y, True /*fromImageStrip*/, image, 0, 0, columns, rows,
		identification_string)
{
	selected = 0;												// @0xAC  this[0x2B]
	AddConnection(new CoolingLoopConnection(&selected, subsystem));	// FUN_004c3134
}

AnimatedSubsystemLamp::~AnimatedSubsystemLamp() {}				// @004c7134 (base chain)

//
// @004c7160 -- AnimatedSourceLamp ctor (vtable PTR_FUN_0051a130): identical shape
// but a PowerSourceConnection (@004c31ec) drives the frame from the resolved
// power source's bus-lamp state.
//
AnimatedSourceLamp::AnimatedSourceLamp(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer_in,
	int					graphics_port_number,
	int					x,
	int					y,
	const char			*image,
	int					columns,
	int					rows,
	void				*source,
	const char			*identification_string
):
	OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number,
		x, y, True, image, 0, 0, columns, rows, identification_string)
{
	selected = 0;
	AddConnection(new PowerSourceConnection(&selected, source));	// FUN_004c31ec
}

AnimatedSourceLamp::~AnimatedSourceLamp() {}					// @004c71f0


//###########################################################################
//   ScalarBarGauge                  @004c721c
//###########################################################################

//
// @004c721c -- ctor (vtable PTR_FUN_0051a0e4).  Chains the MUNGA L4 bar primitive
// (@00472ef0 == BarGraphBitMapScalar) with no auto-connection (value=NULL), then
// AddConnection(new GeneratorVoltageConnection @004c3288) writing the inherited
// WipeGaugeScalar::currentValue (@0xB4, this[0x2D]) from the resolved voltage
// source.  Generator-voltage bar, range 0..12000.0f.  dtor @004c733c chains the
// bar dtor @00472fb4 (implicit).
//
ScalarBarGauge::ScalarBarGauge(
	GaugeRate					rate,
	ModeMask					mode_mask,
	L4GaugeRenderer				*renderer_in,
	int							graphics_port_number,
	int							left,
	int							bottom,
	int							right,
	int							top,
	const char					*tile_image,
	int							foreground_color,
	int							background_color,
	WipeGaugeScalar::Direction	direction,
	Scalar						min,
	Scalar						max,
	void						*voltage_source,
	const char					*identification_string
):
	BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/,	// FUN_00472ef0
		graphics_port_number, left, bottom, right, top, tile_image,
		foreground_color, background_color, direction, min, max,
		(Scalar *)0 /*no auto-connection*/, identification_string)
{
	AddConnection(new GeneratorVoltageConnection(&currentValue, voltage_source));	// FUN_004c3288
}

ScalarBarGauge::~ScalarBarGauge() {}							// @004c733c (base chain)


//###########################################################################
//###########################################################################
//   GeneratorCluster  (config keyword "GeneratorCluster" -- the 4 generator
//   engineering panels, buttons 9-12 / GeneratorA..D; vtable PTR_FUN_0051a0a0).
//   Reconstructed from part_014.c:891-1013 (Make @004c7368 / ctor @004c746c /
//   dtor @004c7778).  Was PROSE + a placeholder Make -> the "GeneratorCluster"
//   config lines parse-skipped and the 4 panels never built.  Only the dtor is
//   overridden; Execute/BecameActive/TestInstance are inherited from GraphicGauge.
//###########################################################################
//###########################################################################

static inline GaugeRate ChildRate();		// fwd-decl (defined later, after SubsystemCluster)

//
// @004c72ac -- ScalarBarGauge plain-Scalar variant.  Same bar body as @004c721c but
// a GaugeConnectionDirectOf<Scalar> from a DIRECT Scalar* (the generator's
// OutputVoltage attribute pointer) instead of a GeneratorVoltageConnection.
//
ScalarBarGauge::ScalarBarGauge(
	GaugeRate					rate,
	ModeMask					mode_mask,
	L4GaugeRenderer				*renderer_in,
	int							graphics_port_number,
	int							left,
	int							bottom,
	int							right,
	int							top,
	const char					*tile_image,
	int							foreground_color,
	int							background_color,
	WipeGaugeScalar::Direction	direction,
	Scalar						min,
	Scalar						max,
	Scalar						*value_source,			// DIRECT scalar
	const char					*identification_string
):
	BarGraphBitMapScalar(rate, mode_mask, renderer_in, 0 /*owner*/,	// FUN_00472ef0
		graphics_port_number, left, bottom, right, top, tile_image,
		foreground_color, background_color, direction, min, max,
		(Scalar *)0 /*no auto-connection*/, identification_string)
{
	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &currentValue, value_source));	// FUN_00474855
}

// CFG shape (L4GAUGE.CFG:5046): NO leading rate token; p[0] = the mode mask (the
// panel's own rate is hard-wired 0xFFFF).
MethodDescription
	GeneratorCluster::methodDescription =
	{
		"GeneratorCluster",
		GeneratorCluster::Make,
		{
			{ ParameterDescription::typeModeMask, NULL },	// p[0]  mode mask (ModeAlwaysActive)
			{ ParameterDescription::typeString,   NULL },	// p[1]  generator name (GeneratorA..D)
			{ ParameterDescription::typeString,   NULL },	// p[2]  background pcc
			{ ParameterDescription::typeColor,    NULL },	// p[3]  extra/leak colorA
			{ ParameterDescription::typeString,   NULL },	// p[4]  temperature tile
			{ ParameterDescription::typeColor,    NULL },	// p[5]  temp bg color
			{ ParameterDescription::typeColor,    NULL },	// p[6]  temp fill color
			{ ParameterDescription::typeString,   NULL },	// p[7]  voltage tile
			{ ParameterDescription::typeColor,    NULL },	// p[8]  voltage fg color
			{ ParameterDescription::typeString,   NULL },	// p[9]  leak pcc
			{ ParameterDescription::typeColor,    NULL },	// p[10] leak colorB
			{ ParameterDescription::typeString,   NULL },	// p[11] lamp A pcc
			{ ParameterDescription::typeString,   NULL },	// p[12] lamp B pcc
			{ ParameterDescription::typeColor,    NULL },	// p[13] lamp on color
			{ ParameterDescription::typeColor,    NULL },	// p[14] lamp off color
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c7368 -- Make.  Resolve the named generator (config p[1], e.g. "GeneratorA");
// warn + bail if absent.  Then alloc 0xB4 + construct.
//
Logical
	GeneratorCluster::Make(
		int				display_port_index,
		Vector2DOf<int>	position,
		Entity			*entity,
		GaugeRenderer	*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	Subsystem *subsystem = entity->FindSubsystem(p[1].data.string);	// FUN_0041f98c (GeneratorA..D)
	if (subsystem == NULL)
	{
		DebugStream << "Subsystem " << p[1].data.string << " not found\n";
		return False;
	}

	GeneratorCluster *gauge = (GeneratorCluster *)operator new(0xb4);	// FUN_00402298(0xb4)
	if (gauge != NULL)
	{
		new (gauge) GeneratorCluster(
			p[0].data.modeMask,					// mode (ModeAlwaysActive)
			(L4GaugeRenderer *)gauge_renderer,
			0,									// owner_ID (raw hard-wires 0)
			display_port_index,					// graphics_port_number
			position.x, position.y,				// panel origin
			subsystem,							// resolved generator
			p[2].data.string,					// background image
			p[3].data.color,					// extra/leak colorA
			p[4].data.string,					// temperature tile
			p[5].data.color,					// temp bg color
			p[6].data.color,					// temp fill color
			p[7].data.string,					// voltage tile
			p[8].data.color,					// voltage fg color
			p[9].data.string,					// leak image
			p[10].data.color,					// leak colorB
			p[11].data.string,					// lampA image
			p[12].data.string,					// lampB image
			p[13].data.color,					// lamp on color
			p[14].data.color,					// lamp off color
			"GeneratorCluster");
	}
	return True;
}

//
// @004c746c -- ctor (vtable PTR_FUN_0051a0a0).  GraphicGauge base (own rate hard-
// wired 0xFFFF; mode from config).  Resolve the generator's temp/voltage attributes,
// intern + AddRef the panel background pixmap, build the 5 child gauges into this
// panel's port at offsets relative to the panel origin (x,y).
//
GeneratorCluster::GeneratorCluster(
	ModeMask		 mode_mask,
	L4GaugeRenderer *renderer_in,
	int				 owner_ID,
	int				 graphics_port_number,
	int				 x,
	int				 y,
	Subsystem		*subsystem_in,
	const char		*background_image,
	int				 extra_color,
	const char		*temp_tile,
	int				 temp_bg_color,
	int				 temp_fill_color,
	const char		*voltage_tile,
	int				 voltage_fg_color,
	const char		*leak_image,
	int				 leak_color_b,
	const char		*lampA_image,
	const char		*lampB_image,
	int				 lamp_on_color,
	int				 lamp_off_color,
	const char		*identification_string
):
	GraphicGauge((GaugeRate)0xFFFF, mode_mask, renderer_in, owner_ID,	// FUN_00444818
		graphics_port_number, identification_string)
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;

	GaugeRate rate1 = ChildRate();		// VertTwoPartBar
	GaugeRate rate2 = ChildRate();		// ScalarBarGauge
	GaugeRate rate3 = ChildRate();		// LeakGauge
	GaugeRate rate4 = ChildRate();		// both lamps

	Scalar *currentTemp = (Scalar *)AttributePointerOf(subsystem_in, "CurrentTemperature");
	Scalar *degradeTemp = (Scalar *)AttributePointerOf(subsystem_in, "DegradationTemperature");
	Scalar *failTemp    = (Scalar *)AttributePointerOf(subsystem_in, "FailureTemperature");
	Scalar *coolantLeak = (Scalar *)AttributePointerOf(subsystem_in, "CoolantMassLeakRate");
	Scalar *outputVolt  = (Scalar *)AttributePointerOf(subsystem_in, "OutputVoltage");

	localView.SetOrigin(x, y);			// vtbl+0x10
	secondaryColor = leak_color_b;		// @0x94 (stored; unread here)

	backgroundImage = new char[strlen(background_image) + 1];	// @0x90
	strcpy(backgroundImage, background_image);
	warehouse->pixelMap8Bin.Get(backgroundImage, False);		// AddRef

	// child 1: vertical two-part temperature bar
	temperatureBar = new VertTwoPartBar(rate1, mode_mask, renderer_in, graphics_port_number,
		x + 5, y + 0x29, x + 0xC, y + 0x5C,
		temp_tile, temp_bg_color, temp_fill_color, extra_color,
		currentTemp, degradeTemp, failTemp, "VertTwoPartBar");

	// child 2: generator-voltage bar 0..12000 (OutputVoltage); @004c72ac Scalar* variant
	voltageBar = new ScalarBarGauge(rate2, mode_mask, renderer_in, graphics_port_number,
		x + 0x18, y + 0x29, x + 0x1C, y + 0x5C,
		voltage_tile, voltage_fg_color, extra_color,
		WipeGaugeScalar::wipeUp, 0.0f, 12000.0f, outputVolt,
		"GeneratorVoltage(scalar)");

	// child 3: coolant-leak inverse-wipe (CoolantMassLeakRate); third=*(subsys+0x150), frames=3
	leakGauge = new BitMapInverseWipe(rate3, mode_mask, renderer_in, graphics_port_number,
		x, y + 0x27, leak_image, extra_color, leak_color_b,
		*(int *)((char *)subsystem_in + 0x150),		// third
		3,											// frames
		coolantLeak, "LeakGauge");

	// child 4/5: two status lamps (subsys+0x1D4 == generatorOn)
	lampA = new TwoState(rate4, mode_mask, renderer_in, (unsigned)owner_ID, graphics_port_number,
		x + 0xC, y + 2, lampA_image, lamp_on_color, lamp_off_color,
		(int *)((char *)subsystem_in + 0x1D4), "TwoState");
	lampB = new TwoState(rate4, mode_mask, renderer_in, (unsigned)owner_ID, graphics_port_number,
		x + 2, y + 2, lampB_image, lamp_on_color, lamp_off_color,
		(int *)((char *)subsystem_in + 0x1D4), "TwoState");

	subsystem = subsystem_in;			// @0xAC
	_reserved0xB0 = 0;					// @0xB0 (raw leaves uninit; zero for determinism)
}

//
// @004c7778 -- dtor.  Release the background pixmap + free the interned name; the
// base chain runs implicitly.  The 5 children are NOT deleted here (faithful to the
// binary: each self-registered with the renderer via the base Gauge ctor and is
// owned there -- deleting would double-free with the renderer chain teardown).
//
GeneratorCluster::~GeneratorCluster()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	warehouse->pixelMap8Bin.Release(backgroundImage);
	delete[] backgroundImage;
	backgroundImage = NULL;
}

//
// BecameActive -- paint the panel's static background pixmap.  MUST override the
// default (which self-inactivates the panel + warns); a no-op keeps the panel
// active so its 5 child gauges (self-registered) keep drawing.  The bg is drawn
// here (on activation) beneath the children.
//
void
	GeneratorCluster::BecameActive()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	PixelMap8 *bg = warehouse->pixelMap8Bin.Get(backgroundImage);	// already AddRef'd by the ctor
	if (bg != NULL)
	{
		localView.MoveToAbsolute(0, 0);							// vtbl+0x24
		localView.DrawPixelMap8(True, 0, bg, 0, 0,				// vtbl+0x5C (opaque, full image)
			bg->Data.Size.x - 1, bg->Data.Size.y - 1);
	}
	warehouse->pixelMap8Bin.Release(backgroundImage);			// balance this Get
}

//
// Execute -- the panel itself has no per-frame content (the temp/voltage/leak/lamp
// children each Execute independently), but Execute MUST be overridden: the engine
// base Gauge::Execute is Fail(...)->abort (GAUGE.cpp:598).  No-op.
//
void
	GeneratorCluster::Execute()
{
}


//###########################################################################
//###########################################################################
//   PrepEngrScreen                  @004c7b30 Make / @004c7bf0 ctor
//###########################################################################
//###########################################################################

// forward: the tiled-blit helper is defined lower in this TU (FUN_004c2ec4).
static void DrawPortBackground(GraphicsView *view, L4Warehouse *warehouse,
							   int x, int y, const char *tile);

// ---- FUN_004c79c8 : draw the three status-label cells (+ optional top erase box).
// Each cell blits its label .pcc, or blanks the cell.  Absolute cell rectangles are
// the binary's exact coords (part_014.c:1127-1166).
static void
	DrawStatusCells(GraphicsView *view, L4Warehouse *warehouse,
					int topBox, const char *A, const char *B, const char *C)
{
	if (topBox)												// :1131-1135
	{
		view->SetColor(0);
		view->MoveToAbsolute(0x97, 0x80);
		view->DrawFilledRectangleToAbsolute(0x17d, 0x13b);
	}
	if (A == NULL) { view->SetColor(0); view->MoveToAbsolute(0x15a, 0x145);	// :1136-1140
		view->DrawFilledRectangleToAbsolute(0x17f, 0x159); }
	else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x15a, 0x145, A); }
	if (B == NULL) { view->SetColor(0); view->MoveToAbsolute(0x85, 0x58);	// :1145-1149
		view->DrawFilledRectangleToAbsolute(0x17f, 0x7c); }
	else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x85, 0x58, B); }
	if (C == NULL) { view->SetColor(0); view->MoveToAbsolute(0x2a, 0x34);	// :1154-1160
		// raw uses an extra GraphicsView vtbl+0x4c(99,10) compound-blank; a plain
		// fill at the same cell is the faithful-enough substitute (marked).
		view->DrawFilledRectangleToAbsolute(0x9f, 0x2d); }
	else { view->SetColor(0xff); DrawPortBackground(view, warehouse, 0x2a, 0x34, C); }
}

// CFG shape (L4GAUGE.CFG:4595): mode, int screen(1..12), then 7 .pcc names
// (font + 6 labels).  9 params.
MethodDescription
	PrepEngrScreen::methodDescription =
	{
		"prepEngr",
		PrepEngrScreen::Make,
		{
			{ ParameterDescription::typeModeMask, NULL },	// p[0] mode (ModeMFD*Eng*)
			{ ParameterDescription::typeInteger,  NULL },	// p[1] screen number 1..12
			{ ParameterDescription::typeString,   NULL },	// p[2] font    helv42.pcc  -> statusImage[0]
			{ ParameterDescription::typeString,   NULL },	// p[3] escnd.pcc           -> statusImage[1]
			{ ParameterDescription::typeString,   NULL },	// p[4] edmg.pcc            -> statusImage[2]
			{ ParameterDescription::typeString,   NULL },	// p[5] bteslev.pcc         -> statusImage[3]
			{ ParameterDescription::typeString,   NULL },	// p[6] bteunjm.pcc         -> statusImage[4]
			{ ParameterDescription::typeString,   NULL },	// p[7] espeed.pcc          -> statusImage[5]
			{ ParameterDescription::typeString,   NULL },	// p[8] btesrnge.pcc        -> statusImage[6]
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c7b30 -- Make.  Validate the screen number (p[1], 1..12) then construct.
// The GraphicGaugeBackground base ctor self-registers with the renderer, so the
// gauge's BecameActive fires whenever its Eng screen is selected.  We `new` the
// object (real compiled size) rather than operator new(0x90) -- the 2007 base
// isn't byte-exact, and nothing reads this object at raw offsets.
//
Logical
	PrepEngrScreen::Make(
		int				display_port_index,
		Vector2DOf<int>	/*position -- PrepEngr draws at absolute coords*/,
		Entity			*entity,
		GaugeRenderer	*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	int screen = p[1].data.integer;
	if (screen < 1 || screen > 12)						// :1182
	{
		DEBUG_STREAM << "PrepEngr: screen number out of range " << screen << "\n";	// :1183
		return False;
	}

	new PrepEngrScreen(									// FUN_00402298(0x90) in the binary
		p[0].data.modeMask,								// mode (ModeMFD*Eng*)
		(L4GaugeRenderer *)gauge_renderer,
		0,											// owner_ID (raw hard-wires 0)
		display_port_index,								// graphics_port_number
		entity,										// mech
		screen,										// screen number
		p[2].data.string, p[3].data.string, p[4].data.string,
		p[5].data.string, p[6].data.string, p[7].data.string,
		p[8].data.string,
		"PrepEngr");
	return True;
}

//
// @004c7bf0 -- ctor.  GraphicGaugeBackground base (mode/renderer/0/port/"PrepEngr");
// store screen + mech; copy the 7 image names and pre-load (AddRef) each from the
// bitmap cache.
//
PrepEngrScreen::PrepEngrScreen(
	ModeMask mode_mask, L4GaugeRenderer *renderer_in, unsigned int owner_ID,
	int graphics_port_number, Entity *mech_in, int screen_number,
	const char *img0, const char *img1, const char *img2, const char *img3,
	const char *img4, const char *img5, const char *img6,
	const char *identification_string
):
	GraphicGaugeBackground(mode_mask, renderer_in, owner_ID,		// FUN_004440d4
		graphics_port_number, identification_string)
{
	screenNumber = screen_number;					// @0x6C (:1216)
	mech         = mech_in;							// @0x70 (:1215)

	const char  *src[7] = { img0, img1, img2, img3, img4, img5, img6 };
	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;	// renderer+0x4c
	for (int i = 0; i < 7; i++)						// :1217-1238
	{
		if (src[i] != NULL) {
			statusImage[i] = new char[strlen(src[i]) + 1];	// nameCopy (FUN_004700ac)
			strcpy(statusImage[i], src[i]);
			warehouse->bitMapBin.Get(statusImage[i]);		// FUN_00442aec pre-load/AddRef
		} else {
			statusImage[i] = NULL;
		}
	}
}

//
// @004c7d14 -- dtor.  Release the 7 pre-loaded images + free their name copies;
// the localView + base dtor run implicitly (do NOT emit explicit base-dtor calls).
//
PrepEngrScreen::~PrepEngrScreen()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	for (int i = 0; i < 7; i++)						// :1254-1274
	{
		if (statusImage[i] != NULL) {
			warehouse->bitMapBin.Release(statusImage[i]);	// FUN_00442c12
			delete[] statusImage[i];
			statusImage[i] = NULL;
		}
	}
}

//
// @004c7e48 -- BecameActive.  Find the subsystem assigned to this screen and paint
// its labels (roster + aux-screen read via the databinding-safe accessors + the
// PoweredSubsystem bridge, exactly like VehicleSubSystems::Make).  NON-inactivating
// (a real paint; does not chain the base) -> the paint-on-activation contract.
//
void
	PrepEngrScreen::BecameActive()
{
	if (mech == NULL)  return;
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	const char  *font      = statusImage[0];			// helv42.pcc

	int count = mech->GetSubsystemCount();				// mech+0x124
	for (int i = 1; i < count; i++)						// skip slot 0 (controls mapper)
	{
		Subsystem *sub = mech->GetSubsystem(i);			// mech+0x128[i]
		if (sub == NULL)  continue;

		int  auxScreen = 0, placement = -1;  char label64[64];  label64[0] = '\0';
		if (!BTGetSubsystemAuxScreen(sub, &auxScreen, &placement, label64))	// filter + sub+0x1dc
			continue;									// not a PoweredSubsystem-derived weapon
		if (auxScreen != screenNumber)					// :1309
			continue;

		// ---- numeric #1 : the screen number, 2 digits @(0xE9,0x176)  (:1310-1313)
		{
			NumericDisplay num(warehouse, 0xe9, 0x176, font, 2,
				NumericDisplay::unsignedFormat, 0, 0xff);		// FUN_004700bc
			num.Draw(&localView, (Scalar)screenNumber);			// FUN_00470430
		}

		// ---- numeric #2 : linked heat-sink number, 1 digit @(0x15E,5)  (:1315-1321)
		// BEST-EFFORT (marked): the resolved sink's +0x1D4 index is a raw offset on
		// a not-byte-exact object -> guarded (NULL link / read 0).
		int hsNumber = 0;
		void *hsLink = AttributePointerOf(sub, "HeatSink");			// FUN_0041bfc0
		void *hs     = (hsLink != NULL) ? ResolveLink(hsLink) : NULL;	// FUN_00417ab4
		if (hs != NULL)
			hsNumber = *(int *)((char *)hs + 0x1d4);
		{
			NumericDisplay num(warehouse, 0x15e, 5, font, 1,
				NumericDisplay::unsignedFormat, 0, 0xff);
			num.Draw(&localView, (Scalar)hsNumber);
		}

		// ---- subsystem label bitmap @(0x125,0x172)  (:1322-1329).  The binary draws
		// the .pcc name at sub+0x224; the aux-screen bridge's label64 is the
		// databinding-safe equivalent (BEST-EFFORT).
		if (label64[0] != '\0') {
			localView.SetColor(0xff);
			DrawPortBackground(&localView, warehouse, 0x125, 0x172, label64);
		}

		// ---- type-specific label cells (switch on ClassID, :1330-1357)
		switch (sub->GetClassID())							// sub+4
		{
		case 0xBC3:		// Sensor
			DrawStatusCells(&localView, warehouse, 1, NULL, NULL, NULL);
			localView.SetColor(0xff);
			DrawPortBackground(&localView, warehouse, 0xb8, 0x8c, statusImage[6]);	// btesrnge
			return;
		case 0xBC6:		// Myomers
			DrawStatusCells(&localView, warehouse, 0, NULL, statusImage[5], statusImage[3]);
			return;
		case 0xBC8:		// Emitter
		case 0xBD4:		// PPC
			DrawStatusCells(&localView, warehouse, 0, statusImage[1], statusImage[2], statusImage[3]);
			return;
		case 0xBCD:		// ProjectileWeapon
		case 0xBD0:		// MissileLauncher
			DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[4]);
			return;
		case 0xBCE:		// GaussRifle -- not yet supported
			DEBUG_STREAM << "PrepEngr: Gauss rifle not yet supported\n";	// :1353
			DrawStatusCells(&localView, warehouse, 1, NULL, NULL, statusImage[3]);
			return;
		default:
			return;
		}
	}
}



//###########################################################################
//###########################################################################
//   SubsystemCluster family         @004c8140 base
//###########################################################################
//###########################################################################

//
// @004c8140 -- SubsystemCluster ctor (vtable PTR_FUN_0051a020).  GraphicGauge
// base.  Resolves InputVoltage/HeatSink/CurrentTemperature/Degradation/Failure
// attribute indices, then builds the panel's child gauges into its port:
//      this[0x26] HorizTwoPartBar (temperature)            @004c4170
//      this[0x27] AnimatedSubsystemLamp ("CoolingLoop",btploop) @004c70a4
//      this[0x29] AnimatedSourceLamp   ("PowerSource",btpbus)  @004c7160 (if InputVoltage)
//      this[0x24] BackgroundBitmap (if background present)  @00471d00
//      this[0x28] AnimatedSubsystemLamp (bteloop)           @004c70a4
//      this[0x2D] OneOfSeveralInt (btecmode, subsys+0x134)  @004c5148
//      this[0x2A] AnimatedSourceLamp (btebus)               @004c7160 (if InputVoltage)
//      this[0x25] ScalarBarGauge (evolt, GeneratorVoltage)  @004c721c (if InputVoltage)
//      this[0x2E] OneOfSeveralStates (btemode, subsys+0x2b8) @004c5470
//                 (only when subsystem IsDerivedFrom Generator 0x50f4bc)
//      this[0x2C] VertTwoPartBar x2 (temperatures)          @004c4724
//      this[0x2F] LeakGauge (eleak, CoolantMassLeakRate)    @004c5b7c
//      this[0x30] subsystem ; this[0x31] operating
// @004c87dc -- dtor: GraphicGauge::~GraphicGauge (children freed by subclasses).
// @004c88e4 -- Execute: operating = (subsys+0x40==1); when it changes, repaint
// the panel frame (operational => coloured rectangle; offline => background
// bitmap blit) and forward enable to the temperature/loop children
// (SetEnable @004c8a28 / FUN_00444650).
//
// @004c8a6c -- HeatSinkCluster ctor (vtable PTR_FUN_00519fd4): chains
// SubsystemCluster, this[0x36]=100.0f, then builds four failure TwoStates
// (this[0x37] heat, this[0x38] btehfail, this[0x39] btepfail subsys+0x324,
// this[0x3A] btesfail subsys+0x320) and a NumericDisplayScalar (this[0x3B],
// helv18, this[0x36]) plus a Scalar connection (&this[0x35], subsys+0x31c).
// Execute @004c8db0: failFlag = (failSubsystem+0x40==1); heatLoadScaled =
// heatLoad * _DAT_004c8df0; chain SubsystemCluster::Execute.
//
// @004c8fc4 -- WeaponCluster ctor (vtable PTR_FUN_00519f38): chains
// SubsystemCluster; resolves "PercentDone"; interns the cluster image
// (this[0x33]); warns "WeaponCluster missing <name>" if absent; centres a
// SegmentArc270 recharge dial (this[0x34], 0..360, @004c6244) and a
// ConfigMapGauge (this[0x35], "ConfigMap", @004c6d80); adds a Scalar connection
// (&percentDone, PercentDone).  Execute @004c9290: chain base Execute; when
// percentDone > _DAT_004c92e0 toggle the warning lamp (DrawWarningLamp @004c932c).
//
// @004c93b0 -- EnergyWeaponCluster ctor (vtable PTR_FUN_00519ee8): chains
// WeaponCluster; adds a SeekVoltageGraph (this[0x3A], "EngrGraph", edestryd.pcc,
// @004c6798) reading OutputVoltage, and a seek-step OneOfSeveralInt (this[0x3B],
// bteseek.pcc, subsys+0x3f0, @004c5148).  dtor @004c94dc.
//
// @004c9558 -- BallisticWeaponCluster ctor (vtable PTR_FUN_00519e98): chains
// WeaponCluster; resolves the ammo bin (Resolve subsys+0x43c) and builds:
//      this[0x45]/[0x46] NumericDisplayInteger (ammo counts)     @00470cfc
//      this[0x42] TwoState (btejam)  this[0x43] TwoState (btefire) @004739d8
//      this[0x3F] GraphicsViewRecord erase tracker               @0044ad78
//      this[0x40] NumericDisplayScalarTwoState (helv42)          @00473c94
//      this[0x41] TwoState (edestryd)                            @004739d8
//      this[0x44] BitMapInverseWipeScalar (bteejtm, subsys+0x3f8) @004c61c8
// Execute @004c9a38: jammed = (subsys+0x364==5 weaponAlarm Jammed); read the
// ammo-bin reload state (bin+0x18c) and, while reloading, compute elapsed
// seconds (frame clock @00414b60 / DAT_0052140c); chain WeaponCluster::Execute.
// TestInstance @004c9adc: True unless the ammo bin is present and the eject
// wipe is mid-cycle.  dtor @004c9940 frees the GraphicsViewRecord + numeric.
//

//
// FUN_004c2ec4 -- draw a clipped port-background bitmap: MoveTo(x,y), fetch the
// tile from the BitMap cache, opaque-blit it, release.  (Used by the cluster
// ctor to paint the panel's static frame image into the MFD port.)
//
static void DrawPortBackground(GraphicsView *view, L4Warehouse *warehouse,
							   int x, int y, const char *tile)
{
	view->MoveToAbsolute(x, y);						// vtbl+0x24
	BitMap *bmp = warehouse->bitMapBin.Get(tile);	// FUN_00442aec
	if (bmp != NULL)
		view->DrawBitMapOpaque(0 /*background*/, 0 /*rotation*/, bmp);	// vtbl+0x58
	warehouse->bitMapBin.Release(tile);				// FUN_00442c12
}

// A default per-child load-distribution rate (the ctor uses FUN_004442b8/dc/290,
// the NextNthTierGaugeRate allocators).
static inline GaugeRate ChildRate() { return Gauge::NextThirdTierGaugeRate(); }


//###########################################################################
//   SubsystemCluster  @004c8140  (vtable 0x51a020)
//###########################################################################

//
// @004c8140 -- ctor.  rate/mode are hardcoded 0xFFFF/0xFFFFFFFF in the base
// GraphicGauge; the two bit-plane masks are mfd_mode (MFD-port children) and
// eng_mode (engineering-port children).  Builds the panel's child gauges (see
// the header comment block) parented to the MFD port (param_5) and the engineering
// port (resolved from eng_port_name); optional children gate on the InputVoltage
// attribute (power bus present) and the damage-zone-index background image.
//
SubsystemCluster::SubsystemCluster(
	ModeMask			mfd_mode,
	L4GaugeRenderer		*renderer_in,
	int					owner_ID,
	int					mfd_port,
	int					x,
	int					y,
	Subsystem			*subsystem_in,
	ModeMask			eng_mode,
	const char			*eng_port_name,
	const char			*tile_image,
	const char			*label,
	char				**image_names,
	const char			*identification_string
):
	GraphicGauge((GaugeRate)0xFFFF, (ModeMask)0xFFFFFFFF, renderer_in,	// FUN_00444818
		owner_ID, mfd_port, identification_string)
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;

	// Resolve the temperature / power attributes off the subsystem.
	void *inputVoltage = AttributePointerOf(subsystem_in, "InputVoltage");
	void *heatSink     = AttributePointerOf(subsystem_in, "HeatSink");
	void *currentTemp  = AttributePointerOf(subsystem_in, "CurrentTemperature");
	void *degradeTemp  = AttributePointerOf(subsystem_in, "DegradationTemperature");
	void *failTemp     = AttributePointerOf(subsystem_in, "FailureTemperature");

	//------------------------------------------------------------------ MFD port
	temperatureBar = new HorizTwoPartBar(ChildRate(), mfd_mode, renderer_in,	// @004c4170
		mfd_port, x + 0x13, y + 0x10, x + 0xdc, y + 0x1f, tile_image,
		0xff, 0, (Scalar *)currentTemp, (Scalar *)degradeTemp, (Scalar *)failTemp,
		"HorizTwoPartBar");

	coolingLoopA = new AnimatedSubsystemLamp(ChildRate(), mfd_mode, renderer_in,	// @004c70a4
		mfd_port, x + 0xe5, y + 0xe, "btploop.pcc", 7, 1, subsystem_in, "CoolingLoop");

	if (inputVoltage == NULL)
		powerSourceA = NULL;
	else
		powerSourceA = new AnimatedSourceLamp(ChildRate(), mfd_mode, renderer_in,	// @004c7160
			mfd_port, x + 0x10e, y + 0xe, "btpbus.pcc", 5, 1,
			inputVoltage, "PowerSource");

	// Background: image_names[ auxScreenPlacement ] if present.  (Placement@0x1e0 is
	// read via the PoweredSubsystem bridge, not the non-byte-exact raw offset.)
	int dzIndex = -1;
	BTGetSubsystemAuxScreen(subsystem_in, NULL, &dzIndex, NULL);	// auxScreenPlacement
	background = NULL;
	if (dzIndex >= 0 && image_names[dzIndex] != NULL)
		background = new BackgroundBitmap(mfd_mode, renderer_in, owner_ID,	// @00471d00
			mfd_port, x + 0x90, y + 0x36, image_names[dzIndex], 0, 0xff, 0,
			"BackgroundBitmap");

	// Paint the panel frame image into the MFD port.
	localView.SetOrigin(x, y);						// vtbl+0x10
	localView.SetColor(0xff);						// vtbl+0x18
	DrawPortBackground(&localView, warehouse, 9, 0xb4, label);	// FUN_004c2ec4

	//----------------------------------------------------------- engineering port
	int engPort = renderer_in->FindGraphicsPort(eng_port_name);	// FUN_00447f1c (name -> port number)

	// Attributes off the linked heat subsystem (Resolve(heatSink)).
	void *linkedHeat = ResolveLink(heatSink);					// FUN_00417ab4
	void *linkTemp    = AttributePointerOf(linkedHeat, "CurrentTemperature");
	void *linkDegrade = AttributePointerOf(linkedHeat, "DegradationTemperature");
	void *coolantLeak = AttributePointerOf(subsystem_in, "CoolantMassLeakRate");

	coolingLoopB = new AnimatedSubsystemLamp(ChildRate(), eng_mode, renderer_in,	// @004c70a4
		engPort, 0x254, 0x13b, "bteloop.pcc", 7, 1, subsystem_in, "CoolingLoop");

	modeLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in,			// @004c5148
		engPort, 400, 10, "btecmode.pcc", 1, 2,
		(int *)((char *)subsystem_in + 0x134), "OneOfSeveralInt");	// BEST-EFFORT raw offset

	if (inputVoltage == NULL)
	{
		powerSourceB        = NULL;
		generatorVoltageBar = NULL;
		stateLamp           = NULL;
	}
	else
	{
		powerSourceB = new AnimatedSourceLamp(ChildRate(), eng_mode, renderer_in,	// @004c7160
			engPort, 0x1a, 0xd0, "btebus.pcc", 5, 1, inputVoltage, "PowerSource");

		generatorVoltageBar = new ScalarBarGauge(ChildRate(), eng_mode, renderer_in,	// @004c721c
			engPort, 0x89, 0x82, 0x93, 0x13b, "evolt.pcc", 0xff, 0,
			WipeGaugeScalar::wipeUp, 0.0f, GeneratorMaxVoltage, inputVoltage,
			"GeneratorVoltage(slotOf)");

		// stateLamp only when the subsystem is a Generator (IsDerivedFrom 0x50f4bc).
		if (IsGeneratorDerived(subsystem_in))
			stateLamp = new OneOfSeveralStates(ChildRate(), eng_mode, renderer_in,	// @004c5470
				engPort, 0xbe, 0, "btemode.pcc", 1, 3,
				(Entity *)((char *)subsystem_in + 0x2b8), "OneOfSeveralStates");	// BEST-EFFORT raw
		else
			stateLamp = NULL;
	}

	vertBarA = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in,			// @004c4724
		engPort, 0x1d1, 0x5c, 0x1eb, 0x124, tile_image, 0xff, 0xff, 0,
		(Scalar *)currentTemp, (Scalar *)degradeTemp, (Scalar *)failTemp, "VertTwoPartBar");

	vertBarB = new VertTwoPartBar(ChildRate(), eng_mode, renderer_in,
		engPort, 0x222, 0x5c, 0x23d, 0x124, tile_image, 0xff, 0xff, 0,
		(Scalar *)linkTemp, (Scalar *)linkDegrade, (Scalar *)failTemp, "VertTwoPartBar");

	leakGauge = new BitMapInverseWipe(ChildRate(), eng_mode, renderer_in,		// @004c5b7c
		engPort, 0x255, 0xe0, "eleak.pcc", 0, 0xff,
		*(int *)((char *)subsystem_in + 0x150) /*third*/, 3 /*frames*/,
		(Scalar *)coolantLeak, "LeakGauge");		// BEST-EFFORT raw (subsys+0x150)

	operating         = False;						// @0xC4  this[0x31]
	subsystem         = subsystem_in;				// @0xC0  this[0x30]
	previousDrawState = 0;							// @0xC8  this[0x32]
}

//
// @004c87dc -- dtor.  Free the owned children, then GraphicGauge::~GraphicGauge
// (implicit).  (The binary frees children through the vtbl-slot-3 ReleaseChildren
// path; done here for RAII correctness.)
//
SubsystemCluster::~SubsystemCluster()
{
	ReleaseChildren();								// FUN_004c8820
}

//
// @004c8820 -- free each owned child gauge (delete via its own dtor).
//
void SubsystemCluster::ReleaseChildren()
{
	delete background;			background = NULL;
	delete generatorVoltageBar;	generatorVoltageBar = NULL;
	delete temperatureBar;		temperatureBar = NULL;
	delete coolingLoopA;		coolingLoopA = NULL;
	delete coolingLoopB;		coolingLoopB = NULL;
	delete powerSourceA;		powerSourceA = NULL;
	delete powerSourceB;		powerSourceB = NULL;
	delete vertBarA;			vertBarA = NULL;
	delete vertBarB;			vertBarB = NULL;
	delete modeLamp;			modeLamp = NULL;
	delete stateLamp;			stateLamp = NULL;
	delete leakGauge;			leakGauge = NULL;
}

//
// @004c89c4 -- draw the panel's border frame (two thick diagonals of the box).
//
void SubsystemCluster::DrawPanelFrame(int color)
{
	localView.SetColor(color);						// vtbl+0x18
	localView.MoveToAbsolute(9, 0x33);				// vtbl+0x24
	localView.DrawThickLineToAbsolute(0x132, 0xab);	// vtbl+0x38
	localView.MoveToAbsolute(9, 0xab);
	localView.DrawThickLineToAbsolute(0x132, 0x33);
}

//
// @004c8a28 -- forward an enable/disable to the always-present children (temp bar,
// cooling loop, power source A).
//
void SubsystemCluster::SetChildrenEnable(int enable)
{
	// FUN_00444650 forwards visibility; enable==0 (offline) => Disable(True).
	if (temperatureBar) temperatureBar->Disable(enable == 0);
	if (coolingLoopA)   coolingLoopA->Disable(enable == 0);
	if (powerSourceA)   powerSourceA->Disable(enable == 0);
}

//
// @004c8990 -- the operational-state virtual (drawn as TestInstance): active
// unless the subsystem is offline (operating==0 AND subsystem+0x278 == 4).
//
int SubsystemCluster::GetDrawState()
{
	if (operating != 0)
		return 1;
	if (*(int *)((char *)subsystem + 0x278) != 4)	// BEST-EFFORT raw offset
		return 1;
	return 0;
}

Logical SubsystemCluster::TestInstance() const
{
	return GraphicGauge::TestInstance();
}

//
// @004c88e4 -- Execute (shared by the whole family).  operating = (subsystem is
// operational, +0x40 == 1).  On a draw-state change repaint the panel frame:
// offline => frame(0) + redraw background bitmap; online => black fill + frame(0xff);
// and forward the enable to the temperature / loop children.
//
void SubsystemCluster::Execute()
{
	operating = (*(int *)((char *)subsystem + 0x40) == 1);	// BEST-EFFORT raw offset

	int state = GetDrawState();
	if (state != previousDrawState)
	{
		previousDrawState = state;
		SetChildrenEnable(state);
		if (state == 0)
		{
			DrawPanelFrame(0);
			if (background != NULL)
				background->BecameActive();			// (child vtbl+0xc) force redraw
		}
		else
		{
			localView.SetColor(0);						// vtbl+0x18
			localView.MoveToAbsolute(1, 0x32);			// vtbl+0x24
			localView.DrawFilledRectangleToAbsolute(0x136, 0xad);	// vtbl+0x48
			DrawPanelFrame(0xff);
		}
	}
}

void SubsystemCluster::BecameActive()
{
	previousDrawState = -1;							// force first-frame repaint
}


// Recovered .data tuning constants (read from the optimised image).
static const Scalar HeatLoadScale     = 100.0f;		// _DAT_004c8df0
static const Scalar WeaponWarnThreshold = 0.99f;	// _DAT_004c92e0


//###########################################################################
//   HeatSinkCluster  @004c8a6c  (vtable 0x519fd4)
//###########################################################################

//
// @004c8a6c -- ctor: SubsystemCluster base + four failure TwoStates (heat / heat-
// sink / power / structure) + a NumericDisplayScalar read-out, plus a Scalar
// connection feeding heatLoad from the subsystem (+0x31c).
//
HeatSinkCluster::HeatSinkCluster(
	ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
	int x, int y, Subsystem *subsystem_in, Subsystem *hud, ModeMask eng_mode,
	const char *eng_port_name, const char *tile_image, const char *label,
	const char *fail_lamp_image, char **image_names, const char *identification_string
):
	SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
		eng_mode, eng_port_name, tile_image, label, image_names, identification_string)
{
	heatLoadScaled = HeatNumericBase;					// @0x36  100.0f
	int engPort = renderer_in->FindGraphicsPort(eng_port_name);

	heatFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,	// @004739d8
		engPort, 200, 0x110, fail_lamp_image, 0, 0xff, (int *)&operating, "TwoState");

	failSubsystem = hud;								// @0x33
	failFlag      = 0;									// @0x34

	hsFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
		engPort, 0x97, 0xe8, "btehfail.pcc", 0, 0xff, (int *)&failFlag, "TwoState");

	powerFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
		engPort, 0x97, 0xc0, "btepfail.pcc", 0, 0xff,
		(int *)((char *)subsystem_in + 0x324), "TwoState");	// BEST-EFFORT raw offset

	structFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
		engPort, 0x97, 0x98, "btesfail.pcc", 0xff, 0,
		(int *)((char *)subsystem_in + 0x320), "TwoState");	// BEST-EFFORT raw offset

	heatNumeric = new NumericDisplayScalar(ChildRate(), eng_mode, renderer_in,	// @00470888
		owner_ID, engPort, 0x132, 0x8c, 0x15a, 0xaa, "helv18.pcc", 3,
		(NumericDisplay::NumericFormat)0, 0, 0xff, &heatLoadScaled,
		"NumericDisplayScalar");

	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &heatLoad,				// @00474855
		(Scalar *)((char *)subsystem_in + 0x31c)));		// BEST-EFFORT raw offset
}

HeatSinkCluster::~HeatSinkCluster()
{
	delete heatFailLamp;	delete hsFailLamp;		delete powerFailLamp;
	delete structFailLamp;	delete heatNumeric;
}

//
// @004c8db0 -- Execute: failFlag = (failSubsystem operational, +0x40 == 1);
// heatLoadScaled = heatLoad * 100.0; chain SubsystemCluster::Execute.
//
void HeatSinkCluster::Execute()
{
	if (failSubsystem != NULL)
		failFlag = (*(int *)((char *)failSubsystem + 0x40) == 1);	// BEST-EFFORT raw
	heatLoadScaled = heatLoad * HeatLoadScale;
	SubsystemCluster::Execute();
}


//###########################################################################
//   MyomerCluster  @004c8df4  (vtable 0x519f88)
//###########################################################################

//
// @004c8df4 -- ctor: SubsystemCluster base + a SeekVoltageGraph ("EngrGraph")
// fed by an InputVoltage GeneratorVoltageConnection (writes seekValue@0x33) + a
// seek-step OneOfSeveralInt (bteseek.pcc, subsys+0x800).  Execute = base.
//
MyomerCluster::MyomerCluster(
	ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
	int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
	const char *eng_port_name, const char *tile_image, const char *label,
	char **image_names, const char *identification_string
):
	SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
		eng_mode, eng_port_name, tile_image, label, image_names, identification_string)
{
	seekValue = 0.0f;									// @0x33  (connection dst)
	int engPort = renderer_in->FindGraphicsPort(eng_port_name);

	seekGraph = (GraphicGauge *)new SeekVoltageGraph(ChildRate(), eng_mode,		// @004c6798
		renderer_in, engPort, (Entity *)subsystem_in,
		(AttributeAccessor *)&seekValue, "edestryd.pcc", False, "EngrGraph");

	seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in,		// @004c5148
		engPort, 0xf, 0, "bteseek.pcc", 1, 4,
		// gauge-complete wave: 0x800 was OOB for a 0x358 Myomers (garbage) -> the seek
		// gear step is Myomers::currentSeekVoltageIndex@0x320 (the analog of the
		// EnergyWeaponCluster's Emitter::seekVoltageIndex@0x3f0; same 1..4 semantics).
		// INFERRED (the MyomerCluster ctor @004c8df4 isn't in the assert-anchored export).
		(int *)((char *)subsystem_in + 0x320), "OneOfSeveralInt");

	void *inputVoltage = AttributePointerOf(subsystem_in, "InputVoltage");
	AddConnection(new GeneratorVoltageConnection(&seekValue, inputVoltage));		// @004c3288
}

MyomerCluster::~MyomerCluster()
{
	delete seekGraph;	delete seekStepLamp;
}


//###########################################################################
//   WeaponCluster  @004c8fc4  (vtable 0x519f38)
//###########################################################################

//
// @004c8fc4 -- ctor: SubsystemCluster base; interns the cluster image; centres a
// SegmentArc270 recharge dial (reads PercentDone) + a ConfigMapGauge; adds a
// Scalar connection feeding percentDone.  The warning-lamp centre is derived from
// the cluster image size, or warned "WeaponCluster missing <name>".
//
WeaponCluster::WeaponCluster(
	ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
	int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
	const char *eng_port_name, const char *tile_image, const char *label,
	const char *cluster_image, char **image_names, const char *identification_string
):
	SubsystemCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
		eng_mode, eng_port_name, tile_image, label, image_names, identification_string)
{
	void *percentDoneAttr = AttributePointerOf(subsystem_in, "PercentDone");

	// Intern the cluster image + derive the warning-lamp centre from its size.
	clusterImage = (char *)cluster_image;				// @0x33  (FUN_004700ac intern)
	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
	BitMap *img = warehouse->bitMapBin.Get(clusterImage);
	if (img == NULL)
	{
		DebugStream << "WeaponCluster missing " << clusterImage << "\n";
		warningCenterX = 0;								// @0x37
		warningCenterY = 0;								// @0x38
	}
	else
	{
		warningCenterX = 0x41 - (img->Data.Size.x >> 1);
		warningCenterY = 0x6d - (img->Data.Size.y >> 1);
	}

	// rechargeArc (SegmentArc270 @004c6244): the 0..360 recharge dial reading the
	// weapon's PercentDone attribute (a real 0..1 fraction).
	rechargeArc = (GraphicGauge *)new SegmentArc270((GaugeRate)0xffff, mfd_mode,	// @004c6244
		renderer_in, owner_ID, mfd_port, x + 0x41, y + 0x6d, 40.0f, 60.0f, 40.0f,
		60.0f, 0.0f, 360.0f, 0x14, 0, 0xff, (Scalar *)percentDoneAttr, True,
		"SegmentArc270");

	configMap = new ConfigMapGauge(ChildRate(), mfd_mode, renderer_in,			// @004c6d80
		mfd_port, x + 0xee, y + 0x38, (Entity *)subsystem_in, "ConfigMap");

	warningState = 0;									// @0x39
	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &percentDone,			// @00474855
		(Scalar *)percentDoneAttr));
}

WeaponCluster::~WeaponCluster()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	warehouse->bitMapBin.Release(clusterImage);
	delete rechargeArc;		delete configMap;
}

//
// @004c932c -- DrawWarningLamp: blit the cluster warning image at its centre,
// coloured on/off by the passed enable.
//
void WeaponCluster::DrawWarningLamp(int on)
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	BitMap *img = warehouse->bitMapBin.Get(clusterImage);
	if (img != NULL)
	{
		localView.MoveToAbsolute(warningCenterX, warningCenterY);	// vtbl+0x24
		localView.SetColor(on ? 0xff : 0);							// vtbl+0x18
		localView.DrawBitMap(0, img);								// vtbl+0x54
	}
	warehouse->bitMapBin.Release(clusterImage);
}

//
// @004c9258 -- BecameActive: reset the warning state + chain the children.
//
void WeaponCluster::BecameActive()
{
	warningState = -2;
	if (configMap)   configMap->BecameActive();
	if (rechargeArc) rechargeArc->BecameActive();
	SubsystemCluster::BecameActive();
}

//
// @004c9290 -- Execute: chain the base; when percentDone crosses the warn
// threshold toggle the warning lamp.
//
void WeaponCluster::Execute()
{
	SubsystemCluster::Execute();
	if (previousDrawState == 0)
	{
		int warn = (WeaponWarnThreshold < percentDone) ? 1 : 0;
		if (warn != warningState)
		{
			warningState = warn;
			DrawWarningLamp(warn);
		}
	}
}


//###########################################################################
//   EnergyWeaponCluster  @004c93b0  (vtable 0x519ee8)
//###########################################################################

//
// @004c93b0 -- ctor: WeaponCluster base + a SeekVoltageGraph ("EngrGraph",
// OutputVoltage) + a seek-step OneOfSeveralInt (bteseek.pcc, subsys+0x3f0).
//
EnergyWeaponCluster::EnergyWeaponCluster(
	ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
	int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
	const char *eng_port_name, const char *tile_image, const char *label,
	const char *cluster_image, char **image_names, const char *identification_string
):
	WeaponCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
		eng_mode, eng_port_name, tile_image, label, cluster_image, image_names,
		identification_string)
{
	int engPort = renderer_in->FindGraphicsPort(eng_port_name);
	void *outputVoltage = AttributePointerOf(subsystem_in, "OutputVoltage");

	seekGraph = new SeekVoltageGraph(ChildRate(), eng_mode, renderer_in,			// @004c6798
		engPort, (Entity *)subsystem_in, (AttributeAccessor *)outputVoltage,
		"edestryd.pcc", True, "EngrGraph");

	seekStepLamp = new OneOfSeveralInt(ChildRate(), eng_mode, renderer_in,		// @004c5148
		engPort, 0xf, 0, "bteseek.pcc", 1, 4,
		(int *)((char *)subsystem_in + 0x3f0), "OneOfSeveralInt");	// BEST-EFFORT raw
}

EnergyWeaponCluster::~EnergyWeaponCluster()
{
	delete seekGraph;	delete seekStepLamp;
}


//###########################################################################
//   BallisticWeaponCluster  @004c9558  (vtable 0x519e98)
//###########################################################################

//
// @004c9558 -- ctor: WeaponCluster base + the ammo panel: two NumericDisplay
// Integers (ammo counts off the resolved ammo bin +0x180), jam/fire TwoStates,
// a GraphicsViewRecord erase tracker, a NumericDisplayScalarTwoState, a destroyed
// TwoState, and an eject BitMapInverseWipeScalar (bteejtm, subsys+0x3f8).
//
BallisticWeaponCluster::BallisticWeaponCluster(
	ModeMask mfd_mode, L4GaugeRenderer *renderer_in, int owner_ID, int mfd_port,
	int x, int y, Subsystem *subsystem_in, ModeMask eng_mode,
	const char *eng_port_name, const char *tile_image, const char *label,
	const char *cluster_image, char **image_names, const char *font_a,
	const char *font_b, const char *identification_string
):
	WeaponCluster(mfd_mode, renderer_in, owner_ID, mfd_port, x, y, subsystem_in,
		eng_mode, eng_port_name, tile_image, label, cluster_image, image_names,
		identification_string)
{
	reloadStartTime = 0;								// @0x3D  (frame clock)
	reloadSeconds   = 0.0f;								// @0x3E
	jammed = firing = reloading = False;

	void *ammoBin = ResolveLink((char *)subsystem_in + 0x43c);	// FUN_00417ab4 (raw)
	int engPort   = renderer_in->FindGraphicsPort(eng_port_name);

	// Ammo count off the bin (+0x180); BEST-EFFORT raw if the bin resolves.
	int *ammoValue = (ammoBin != NULL) ? (int *)((char *)ammoBin + 0x180) : NULL;

	ammoCountA = new NumericDisplayInteger(ChildRate(), mfd_mode, renderer_in,	// @00470cfc
		owner_ID, mfd_port, x + 0x27, y + 100, x + 0x5f, y + 0x76, font_a, 4,
		(NumericDisplay::NumericFormat)0, 0, 0xff, ammoValue, "NumericDisplayInteger");

	ammoCountB = new NumericDisplayInteger(ChildRate(), eng_mode, renderer_in,
		owner_ID, engPort, 0xdc, 0x104, 0x140, 300, font_b, 4,
		(NumericDisplay::NumericFormat)0, 0, 0xff, ammoValue, "NumericDisplayInteger");

	jammed = 0;											// @0x3A
	jamLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,			// @004739d8
		engPort, 0xa0, 0xb2, "btejam.pcc", 0, 0xff, (int *)&jammed, "TwoState");

	firing = reloading = 0;								// @0x3B / @0x3C
	fireLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
		engPort, 0x121, 0xb6, "btefire.pcc", 0, 0xff, (int *)&firing, "TwoState");

	// Ammo erase-tracker GraphicsViewRecord (@0044ad78) -- bring-up: the record
	// class is engine-internal; tracked as a NULL erase (the numeric self-erases).
	ammoErase = NULL;									// @0x3F  (GraphicsViewRecord, deferred)

	ammoNumeric = new NumericDisplayScalarTwoState(ChildRate(), eng_mode,		// @00473c94
		renderer_in, owner_ID, engPort, 0x130, 0xbc, 0x162, 0xee, "helv42.pcc", 2,
		(NumericDisplay::NumericFormat)0, 0, 0xff, &reloadSeconds, (int *)&firing,
		"NumericDisplayScalarTwoState");

	destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
		engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&operating, "TwoState");

	// ejectWipe (BitMapInverseWipeScalar @004c61c8, eject-timer wipe reading
	// subsys+0x3f8).  BRING-UP: the BitMapInverseWipeScalar class is not yet
	// declared/reconstructed in btl4gaug (only the non-Scalar BitMapInverseWipe);
	// tracked NULL until it lands.
	ejectWipe = NULL;									// @0x44
}

BallisticWeaponCluster::~BallisticWeaponCluster()
{
	delete ammoNumeric;		delete ammoErase;		delete ammoCountA;
	delete ammoCountB;		delete jamLamp;			delete fireLamp;
	delete destroyedLamp;	delete ejectWipe;
}

//
// @004c99cc -- BecameActive: chain the ammo children then WeaponCluster.
//
void BallisticWeaponCluster::BecameActive()
{
	if (ammoCountA)    ammoCountA->BecameActive();
	if (ammoCountB)    ammoCountB->BecameActive();
	if (jamLamp)       jamLamp->BecameActive();
	if (fireLamp)      fireLamp->BecameActive();
	if (ammoNumeric)   ammoNumeric->BecameActive();
	if (destroyedLamp) destroyedLamp->BecameActive();
	if (ejectWipe)     ejectWipe->BecameActive();
	WeaponCluster::BecameActive();
}

//
// @004c9a38 -- Execute: jammed = (weaponAlarm subsys+0x364 == 5 Jammed); read the
// ammo-bin reload state (bin+0x18c) and, while reloading, compute the elapsed
// reload seconds; chain WeaponCluster::Execute.
//
void BallisticWeaponCluster::Execute()
{
	jammed = (*(int *)((char *)subsystem + 0x364) == 5);	// BEST-EFFORT raw (weaponAlarm==Jammed)
	void *ammoBin = ResolveLink((char *)subsystem + 0x43c);	// FUN_00417ab4
	int reloadState = (ammoBin != NULL) ? *(int *)((char *)ammoBin + 0x18c) : 0;
	firing = reloadState;
	if (reloadState == 0)
	{
		reloading = 0;
	}
	else
	{
		reloading = 1;
		// (elapsed reload seconds = (now - reloadStart) / frameClock; the frame
		//  clock DAT_0052140c is a runtime value -- left at the base value here.)
	}
	WeaponCluster::Execute();
}

//
// @004c9adc -- TestInstance: True unless the ammo bin is present and the eject
// wipe is mid-cycle.
//
Logical BallisticWeaponCluster::TestInstance() const
{
	void *ammoBin = ResolveLink((char *)subsystem + 0x43c);
	if (ammoBin == NULL || *(int *)((char *)ammoBin + 0x180) == 0)
		return True;
	if (jammed != 0)
		return True;
	return SubsystemCluster::TestInstance();
}


//###########################################################################
//   vehicleSubSystems  Make dispatcher  @004cbaf0
//###########################################################################

//
// The 12 auxiliary-screen geometry rows (@0x51bf34, x/y/planeMask/subBit/mfdPort/
// engPort/planeIndex), keyed by subsystem[0x1dc]-1.  planeMask -> the MFD child
// bit-plane mode; subBit -> the engineering child bit-plane mode; mfd/eng names
// select the two graphics ports.
//
struct AuxScreenGeometry
{
	int			x, y;
	ModeMask	planeMask;
	ModeMask	subBit;
	const char	*mfdPortName;
	const char	*engPortName;
	int			planeIndex;
};
static const AuxScreenGeometry kAuxGeom[12] =
{
	{ 0,     0xf0, 0x1,   0x2,    "mfd1", "eng1", 0 },	// grp 1
	{ 0x142, 0xf0, 0x1,   0x4,    "mfd1", "eng1", 0 },	// grp 2
	{ 0,     0x1c, 0x1,   0x8,    "mfd1", "eng1", 0 },	// grp 3
	{ 0x142, 0x1c, 0x1,   0x10,   "mfd1", "eng1", 1 },	// grp 4
	{ 0,     0xf0, 0x20,  0x40,   "mfd2", "eng2", 1 },	// grp 5
	{ 0x142, 0xf0, 0x20,  0x80,   "mfd2", "eng2", 1 },	// grp 6
	{ 0,     0x1c, 0x20,  0x100,  "mfd2", "eng2", 1 },	// grp 7
	{ 0x142, 0x1c, 0x20,  0x200,  "mfd2", "eng2", 2 },	// grp 8
	{ 0,     0xf0, 0x400, 0x800,  "mfd3", "eng3", 2 },	// grp 9
	{ 0x142, 0xf0, 0x400, 0x1000, "mfd3", "eng3", 2 },	// grp 10
	{ 0,     0x1c, 0x400, 0x2000, "mfd3", "eng3", 2 },	// grp 11
	{ 0x142, 0x1c, 0x400, 0x4000, "mfd3", "eng3", 2 },	// grp 12
};

//
// FUN_0041a1a4(**(sub+0xc), 0x50f4bc) -- the pre-dispatch type filter (an
// IsDerivedFrom against a ClassDerivations object).  The classID switch below is
// the real per-panel filter, so a permissive True is faithful for the displayable
// classes; the exact ClassDerivations resolution lands with a shared accessor.
//
static Logical SubsystemDisplayFilter(Subsystem *) { return True; }

//
// @004cbaf0 -- vehicleSubSystems Make.  Build the 8 strip-image pointers from the
// method params, then for each roster subsystem (skipping slot 0) build the panel
// cluster for its classID onto the auxiliary MFD position named by subsystem[0x1dc].
//
Logical
	VehicleSubSystems::Make(
		int				/*display_port_index*/,
		Vector2DOf<int>	/*position*/,
		Entity			*entity,
		GaugeRenderer	*gauge_renderer
	)
{
	L4GaugeRenderer *renderer = (L4GaugeRenderer *)gauge_renderer;

	// The 8 status strip bitmaps (qXXX0.pcc..qXXX7.pcc) from the method params.
	char *imageNames[8];
	for (int p = 0; p < 8; p++)
		imageNames[p] = methodDescription.parameterList[p].data.string;

	int count = entity->GetSubsystemCount();				// entity+0x124
	if (getenv("BT_VSS_LOG"))
		DEBUG_STREAM << "[vss] Make called: subsystemCount=" << count << "\n" << std::flush;
	for (int i = 0; i < count; i++)
	{
		if (i == 0)											// slot 0 = the controls mapper
			continue;
		Subsystem *sub = entity->GetSubsystem(i);			// entity+0x128[i]
		if (sub == NULL)
			continue;

		// Read the engineering-screen assignment via the PoweredSubsystem bridge
		// (also the type filter -- False == not PoweredSubsystem-derived).
		int   auxScreen = 0, placement = -1;
		char  labelBuf[64];
		if (!BTGetSubsystemAuxScreen(sub, &auxScreen, &placement, labelBuf))	// FUN_0041a1a4 + sub[0x1dc]/[0x1e4]
			continue;

		int group = auxScreen - 1;							// aux-screen position 1..12
		if (getenv("BT_VSS_LOG"))
			DEBUG_STREAM << "[vss] i=" << i << " classID=" << (int)sub->GetClassID()
						<< " auxScreen=" << auxScreen << " placement=" << placement
						<< " label='" << labelBuf << "'\n" << std::flush;
		if (group < 0)
		{
			DebugStream << "Auxiliary screen position = zero\n";
			continue;
		}
		if (group >= 12)
		{
			DebugStream << "Illegal auxiliary screen position =" << group << "\n";
			continue;
		}

		const AuxScreenGeometry &g = kAuxGeom[group];
		int    mfdPort = renderer->FindGraphicsPort((char *)g.mfdPortName);	// FUN_00447f1c
		char  *label   = labelBuf;							// the subsystem label (bridge)
		char  *tile    = (char *)"btwarn.pcc";

		switch (sub->GetClassID())							// sub+4
		{
		case 0xBC3:		// Sensor -> HeatSinkCluster (shows the "HUD" subsystem)
		{
			Subsystem *hud = entity->FindSubsystem((char *)"HUD");	// FUN_0041f98c
			new HeatSinkCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub,
				hud, g.subBit, g.engPortName, tile, label, "edestryd.pcc",
				imageNames, "SensorCluster");
			break;
		}
		case 0xBC6:		// Myomers -> MyomerCluster
			new MyomerCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub,
				g.subBit, g.engPortName, tile, label, imageNames, "MyomerCluster");
			break;
		case 0xBC8:		// Emitter
		case 0xBD4:		// PPC
		{
			const char *clusterImage =
				(*(int *)((char *)sub + 0x334) == 0) ? "qcircle.pcc" : "qcircr.pcc";
			new EnergyWeaponCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y, sub,
				g.subBit, g.engPortName, tile, label, clusterImage, imageNames,
				"EnergyWeaponCluster");
			break;
		}
		case 0xBCD:		// ProjectileWeapon
		case 0xBD0:		// MissileLauncher
			new BallisticWeaponCluster(g.planeMask, renderer, 0, mfdPort, g.x, g.y,
				sub, g.subBit, g.engPortName, tile, label, "qcircle.pcc", imageNames,
				"smlfont.pcc", "helv42.pcc", "ProjWeaponCluster");
			break;
		case 0xBCE:		// GaussRifle -- not yet supported
			DebugStream << "Gauss rifle not yet supported\n";
			break;
		default:
			break;
		}
	}
	return True;
}

//
// The registered method: config primitive "vehicleSubSystems(q0.pcc..q7.pcc)".
//
MethodDescription
	VehicleSubSystems::methodDescription =
	{
		"vehicleSubSystems",
		VehicleSubSystems::Make,
		{
			{ ParameterDescription::typeString, NULL },		// qXXX0.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX1.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX2.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX3.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX4.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX5.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX6.pcc
			{ ParameterDescription::typeString, NULL },		// qXXX7.pcc
			{ ParameterDescription::typeEmpty,  NULL }
		}
	};


// === btl4gau3.cpp begins at @004c9bd0 (PlayerStatusMappingGroup) -- not this TU.
