//===========================================================================//
// File:	btl4gaug.cpp                                                        //
// Project: BattleTech          Brick: Gauge Renderer Manager                 //
// Contents: The cockpit instrument gauge library (see btl4gaug.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_013.c (@004c2f94..@004c5e84) and
// part_014.c (@004c5e84..@004c6394).  Class/member names are taken from the
// CODE name-string pool; per-method @ADDR evidence is cited inline.
//
// Translation-unit extent (linked between btl4rdr.obj and btl4gau2.obj):
//      first gaug code  @004c2f94   (DrawTiledBitmap helper @004c2ff8)
//      anchor           @004c3f6c   ColorMapperHeat::ColorMapperHeat
//                                   assert "...\BTL4GAUG.CPP" line 0x68a
//      last gaug code   @004c6394   SegmentArcRatio   (+ dtor thunks @004c66xx)
//      btl4gau2 begins  @004c6798   (SeekVoltage gauge, string pool 0x5199xx)
//
// Engine-internal helper map (consistent with btl4rdr.cpp's table):
//      FUN_00444308  Gauge ctor              FUN_00444360 Gauge dtor
//      FUN_004443a4  Gauge::TestInstance
//      FUN_00444124  GaugeConnection ctor    FUN_00444148 GaugeConnection dtor
//      FUN_00444818  GraphicGauge ctor       FUN_00444870 GraphicGauge dtor
//      FUN_004448ac  GraphicGauge::TestInstance   FUN_004448bc ::ShowInstance
//      FUN_004700ac  GaugeRenderer::InternName(name) -> char* (interned)
//      FUN_00447f84  GaugeRenderer::GetGraphicsPort(renderer, portNumber)
//      FUN_00442f6a / 00443090   PaletteCache AddRef / Release (renderer+0x4C, +0x3c)
//      FUN_00442aec / 00442c12   BitMapCache  AddRef / Release (renderer+0x4C, +4)
//      FUN_00442d2b / 00442e51   PixMapCache  AddRef / Release (renderer+0x4C, +0x20)
//      FUN_0041f98c  Entity::FindSubObject(name)
//      FUN_0041bfc0  Entity::FindAttributeIndex(name)   (btl4gau2)
//      FUN_0041a1a4  IsDerivedFrom(classDerivations)
//      FUN_00417ab4  SharedData::Resolve() / operator Entity*
//      FUN_0042076c  Subsystem::FindDamageZone(record) -> index
//      FUN_004dcd94  Round(Scalar) -> int
//      FUN_0040385c  Verify(msg,file,line)
//      FUN_004dbb24  DebugStream << (char*)        FUN_004db78c  DebugStream << (int)
//      FUN_004d4b58  stricmp        FUN_004d49b8  strcat
//      FUN_00402298  operator new   FUN_004022e8/004022d0 operator delete
//      FUN_00474855  GaugeConnectionDirectOf<Scalar>::ctor
//      FUN_004749de  GaugeConnectionDirectOf<int>::ctor
//      FUN_0040954c  Quaternion->Euler     FUN_00408328  SinCos
//      FUN_004700bc  NumericDisplay ctor   FUN_0047018c ::dtor
//      FUN_004703f4  NumericDisplay::Reset FUN_00470430 ::DrawAt
//      FUN_0044a5b4/5dc  GraphicsViewRecord ctor/dtor
//      FUN_0044a650/630  GraphicsViewRecord Erase / Reset
//      DAT_00524e20  DebugStream (warning channel)
//      0x50e3ec / 0x50e604   heat-subsystem class-derivation tables
//
// GraphicsView vtable slots used below (this+0x48):
//      +0x08 SetExtent  +0x10 SetOrigin  +0x18 SetColor  +0x24 MoveTo
//      +0x38 LineTo/FillTo  +0x48 FillTo  +0x4C Fill  +0x54 Blit
//      +0x58 BlitClipped  +0x5C BlitStretched  +0x64 BeginClip  +0x68 EndClip
//
// Recovered constant-pool floats (section_dump.txt):
//      _DAT_004c452c = 0.0f      (HorizTwoPartBar low clamp)
//      _DAT_004c4b00 = 0.0f      (VertTwoPartBar low clamp)
//      _DAT_004c4d3c = 0.0f , _DAT_004c4d40 = 1.0f   (VertNormalSlider [0,1] clamp)
//      _DAT_004c5acc = 0.0f , 0x42652ee1 = 57.2958f  (HeadingPointer rad->deg)
//      _DAT_004c62d4 = 0.75f, _DAT_004c6484 = 0.75f  (arc span fraction)
//      _DAT_0050e3d8 = 0.0025f   (OneOfSeveralStates 'critical' threshold)
//      FUN_004c2f88() returns 7  (shared blip colour, defined in btl4rdr.cpp)
//

// The shared reconstruction headers heat.hpp and mechrecon.hpp each define a
// `DebugStream` diagnostic sink, and they do so incompatibly (std::ostream&
// vs the ReconStream artifact struct).  heat.hpp's variant is the optional one
// (gated on this guard); pre-defining it keeps the single mechrecon ReconStream
// definition active throughout this translation unit and avoids the clash.
#define BT_DEBUGSTREAM_DEFINED

#include <bt.hpp>
#pragma hdrstop

#if !defined(BTL4GAUG_HPP)
#	include <btl4gaug.hpp>
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif

static const Scalar ZeroClamp           = 0.0f;	// _DAT_004c452c / _DAT_004c4b00
static const Scalar OneClamp            = 1.0f;	// _DAT_004c4d40
static const Scalar RadiansToDegrees    = 57.2958f;	// 0x42652ee1
static const Scalar StatesCriticalLevel = 0.0025f;	// _DAT_0050e3d8


//###########################################################################
//   Reconstruction shims (file-local)
//
// The shipped BT engine exposed a warning DebugStream (DAT_00524e20) and a
// GaugeRenderer name-intern + palette/bitmap cache.  In the WinTesla engine
// these headers describe, the same concepts are reached through std::cerr and
// the Warehouse resource bins (Warehouse::palette8Bin), so the bodies below
// bind to those.  Behaviour is preserved; only the access path differs.
// (The DebugStream warning channel, DAT_00524e20, is provided by the shared
//  reconstruction headers -- heat.hpp / mechrecon.hpp.)
//###########################################################################

//
// HeatConnection (file-private) -- FUN_004c3664.  Drives a ColorMapper's colour
// index from a heat-bearing subsystem's live temperature.  Only its
// construction is emitted in this translation unit (ColorMapperHeat's ctor);
// the per-frame Transfer() is summarised in the notes above.
//
class HeatConnection : public GaugeConnection
{
public:
	HeatConnection(int *destination, Subsystem *source)
		: GaugeConnection(0),
		  currentColorIndex(destination),
		  heatSubsystem(source)
		{}

protected:
	//
	// The per-frame data feed -- @004c3720 (the recovered "Transfer").  The
	// gauge framework calls this once per frame (Plug/GaugeConnection vtbl
	// virtual Update()).  It samples the live heat-subsystem temperature and
	// writes the colour index that ColorMapper::Execute then pushes into the
	// hardware palette slot, tinting the cockpit heat art.
	//
	//   no subsystem            -> 100  (fail safe: full tint)
	//   subsystem destroyed     -> 100  (recovered "+0x40 == 1" path)
	//   else                    -> Round(currentTemperature @0x114)
	//
	// NOTE (flagged): the recovered code rounds the value resolved at +0x114
	// (currentTemperature) directly into the 0..255 colour index that
	// ColorMapper::Execute then clamps to [0,255].  The exact temperature->
	// percentage scaling (HEAT.TCP seeds currentTemperature at 300.0) is a
	// tuning detail that a human should reconcile against the original
	// l4gauge.cfg palette ramp; the live data path itself is correct.
	//
	void	Update();		// override

	int			*currentColorIndex;		// destination@0x18
	Subsystem	*heatSubsystem;			// source@0x10
};

//
// Round-to-nearest (engine Round @004dcd94).
//
static inline int HeatRound(Scalar value)
{
	return (int)((value >= 0.0f) ? (value + 0.5f) : (value - 0.5f));
}

void
	HeatConnection::Update()
{
	if (heatSubsystem == NULL)
	{
		*currentColorIndex = 100;
		return;
	}

	//
	// The ctor verified heatSubsystem derives from HeatableSubsystem / HeatSink
	// (BTL4GAUG.CPP:0x68a), so the live temperature and damage state are reached
	// through the HeatableSubsystem layout.
	//
	HeatableSubsystem *heat = (HeatableSubsystem *)heatSubsystem;

	if (heat->IsDamaged())			// inherited MechSubsystem simulationState (+0x40) -> max tint
	{
		*currentColorIndex = 100;
		return;
	}

	*currentColorIndex = HeatRound(heat->currentTemperature);	// +0x114 live feed
}


//
// ArmorZoneConnection (@004c33a4 ctor / @004c3430 Transfer) -- drives a
// ColorMapper's colour index from ONE damage zone's live damage ratio.  The ctor
// resolves the zone from the owner's inherited Entity::damageZones[zone_index]
// (raw *(entity+0x120)[idx]); Transfer feeds it each frame.  Used by ColorMapperArmor.
//
class ArmorZoneConnection : public GaugeConnection
{
public:
	ArmorZoneConnection(int *destination, Entity *entity, int zone_index)
		: GaugeConnection(0),									// FUN_00444124(this,0)
		  zone((zone_index < 0) ? NULL : entity->damageZones[zone_index]),
		  currentColorIndex(destination)
		{}

protected:
	//
	// Per-frame feed (@004c3430): no zone -> 100 (fail-safe full tint); else the
	// zone's damage RATIO (damageLevel @0x158, 0..1) scaled to a 0..100 percentage
	// and rounded -- ColorMapper::Execute clamps it + pushes the palette slot,
	// recolouring that zone on the cockpit ARMOR DAMAGE schematic.
	//
	void	Update();		// override

	DamageZone	*zone;					// source@0x10
	int			*currentColorIndex;		// destination@0x14
};

void
	ArmorZoneConnection::Update()
{
	if (zone == NULL)
	{
		*currentColorIndex = 100;
		return;
	}
	*currentColorIndex = HeatRound(zone->damageLevel * 100.0f);	// zone+0x158 * 100
}


//
// MultiArmorConnection (@004c346c ctor / @004c34f4 Transfer) -- drives a
// ColorMapper's colour index from the WORST of up to 8 damage zones.  The ctor
// copies the 8 zone indices + the owner; Transfer scans them each frame and
// feeds the maximum damage ratio.  Used by ColorMapperMultiArmor.
//
class MultiArmorConnection : public GaugeConnection
{
public:
	MultiArmorConnection(int *destination, Entity *entity, const int *zone_indices)
		: GaugeConnection(0),									// FUN_00444124(this,0)
		  owner(entity),
		  currentColorIndex(destination)
	{
		for (int i = 0; i < 8; i++)
			zoneIndex[i] = zone_indices[i];
	}

protected:
	//
	// Per-frame feed (@004c34f4): scan the (up to 8) zones -- skipping index < 0
	// ("unused") and absent zones -- keep the WORST (max) damageLevel (@0x158),
	// scale to 0..100 and round; no zone present -> 100.
	//
	void	Update();		// override

	Entity	*owner;					// @0x10
	int		zoneIndex[8];			// @0x18..0x34
	int		*currentColorIndex;		// @0x38
};

void
	MultiArmorConnection::Update()
{
	Scalar worst = 0.0f;
	int    count = 0;
	for (int i = 0; i < 8; i++)
	{
		if (zoneIndex[i] >= 0)
		{
			DamageZone *zone = owner->damageZones[zoneIndex[i]];
			if (zone != NULL)
			{
				++count;
				if (worst < zone->damageLevel)
					worst = zone->damageLevel;
			}
		}
	}
	*currentColorIndex = (count == 0) ? 100 : HeatRound(worst * 100.0f);
}


//
// CriticalConnection (@004c3598 ctor / @004c3610 Transfer) -- drives a
// ColorMapper's colour index from ONE subsystem's operational state.  Used by
// ColorMapperCritical.  Transfer: no subsystem -> 0 (blank); subsystem DESTROYED
// (simulationState == 1) -> 100 (full critical tint); else the subsystem's own
// damage-zone damageLevel (0..1) as a 0..100 percentage (@0xE0 -> @0x158 * 100).
//
class CriticalConnection : public GaugeConnection
{
public:
	CriticalConnection(int *destination, Subsystem *source)
		: GaugeConnection(0),									// FUN_00444124(this,0)
		  subsystem(source),
		  currentColorIndex(destination)
		{}

protected:
	void	Update();		// override

	Subsystem	*subsystem;				// @0x10 source
	int			*currentColorIndex;		// @0x14 destination
};

void
	CriticalConnection::Update()
{
	if (subsystem == NULL)
	{
		*currentColorIndex = 0;
		return;
	}
	MechSubsystem *sub = (MechSubsystem *)subsystem;
	if (sub->GetSimulationState() == 1)			// DestroyedState -> full critical tint
	{
		*currentColorIndex = 100;
		return;
	}
	// else: the subsystem's own damage zone (the proxy IS a real DamageZone).  The
	// binary reads @0xe0 unconditionally; guard NULL (not every recon subsystem has
	// its zone wired) -> 0, matching an intact/undamaged reading.
	DamageZone *dz = (DamageZone *)sub->GetDamageZoneProxy();
	*currentColorIndex = (dz != NULL) ? HeatRound(dz->damageLevel * 100.0f) : 0;
}


//###########################################################################
//###########################################################################
//   File-private GaugeConnection subclasses
//
//   Every gauge in this module that is "driven by mech subsystem state" is fed
//   through one of these tiny GaugeConnection objects.  The base GaugeConnection
//   stores  source@0x10  and  destination@0x14 ; Transfer() (a virtual) computes
//   a value from the source and writes it to *destination.  The gauge framework
//   calls Transfer() once per frame (after AddConnection, vtbl+0x34).
//
//   Layouts below use the ctor argument order:
//      FUN_00444124(this, 0)         base ctor
//      this[4] (@0x10) = source      this[5] (@0x14) = destination
//   BEST-EFFORT: these classes were emitted alongside the gauges that own them
//   and share this translation unit; their template spelling is inferred.
//###########################################################################
//###########################################################################

//
// SubsystemTorsoHeading connection -- @004c3134 ctor / @004c31a0 Transfer.
// If the owner mech's torso (sub-object index 3) is present, copies its
// heading (entity+0x1d4) to the destination, else 0.
//
// SubsystemNameId connection -- @004c31ec / @004c3258 (video-object+0x1e0).
// SubsystemAltId  connection -- @004c3288 / @004c32f4 (video-object+0x1dc).
//   (These three feed numeric / heading readouts.)
//
// StateConnection -- @004c3324 / @004c3390 -- copies Subsystem state@0x14.
//   Used by OneOfSeveralStates.
//
// ArmorZoneConnection -- @004c33a4 ctor / @004c3430 Transfer.
//   ctor resolves a single damage-zone slot:  source = Subsystem.zones@0x120
//   [zone_index]  (or 0 if zone_index < 0).  Transfer: no zone -> 100, else the
//   zone's damage percentage (Round).   Used by ColorMapperArmor.
//
// MultiArmorConnection -- @004c346c ctor / @004c34f4 Transfer.
//   ctor copies 8 zone indices (this[6..13]).  Transfer scans the 8 zones,
//   keeps the maximum damage ratio (zone+0x158), Round -> destination (100 if
//   none present).   Used by ColorMapperMultiArmor.
//
// CriticalConnection -- @004c3598 ctor / @004c3610 Transfer.
//   source = a Subsystem.  Transfer: no subsystem -> 0 ; subsystem operational
//   (subsys+0x40 == 1) -> 100 ; else Round of the live value.
//   Used by ColorMapperCritical.
//
// HeatConnection -- @004c3664 ctor / @004c3720 Transfer.
//   ctor: source = the heat subsystem; flag@0x14 records whether it is the
//   "primary" heat class (IsDerivedFrom 0x50e3ec) vs. the alternate (0x50e604);
//   destination@0x18.  Transfer: no subsystem -> 100 ; subsystem operational
//   (+0x40 == 1) -> 100 ; else Round( Resolve(subsys+0x114 currentTemperature) ).
//   Used by ColorMapperHeat -- THIS is the heat gauge's data feed.
//
// All Transfer bodies are reproduced inside the owning gauge's notes; the
// connection classes themselves are otherwise trivial (TestInstance returns
// True; ShowInstance is the inherited no-op).
//


//###########################################################################
//###########################################################################
//   ColorMapper  (base : Gauge)
//###########################################################################
//###########################################################################

//
// @004c37dc -- vtable PTR_FUN_00518e10, base name passed through to Gauge.
// Interns both palette names, flags twoColorMode when they differ, validates
// that both palettes exist in the renderer's palette cache (warns otherwise),
// stashes the target hardware palette slot and resolves the renderer graphics
// port that owns the live palette.
//
ColorMapper::ColorMapper(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer,
	int					graphics_port_number,
	int					color_index,
	const char			*palette_name_a,
	const char			*palette_name_b,
	const char			*identification_string
):
	Gauge(rate, mode_mask, renderer, /*owner*/0, identification_string)	// FUN_00444308
{
	// Original interned a private copy of each name (FUN_004700ac); the resource
	// strings handed in here persist for the gauge's lifetime, so retain them.
	paletteName[0] = (char *)palette_name_a;
	paletteName[1] = (char *)palette_name_b;
	twoColorMode   = (stricmp(paletteName[0], paletteName[1]) != 0);	// FUN_004d4b58

	//
	// Reference each palette in the warehouse palette bin (AddRef + load); a
	// missing palette is a content warning.  (PaletteCache AddRef = FUN_00442f6a.)
	//
	WarehouseBinOf<Palette8> &palettes = renderer->warehousePointer->palette8Bin;
	for (int i = 0; i < 2; ++i)
	{
		if (palettes.Get(paletteName[i]) == NULL)
		{
			DebugStream << "ColorMapper cannot find palette "
						<< paletteName[i] << "\n";
		}
	}

	previousColorIndex = -1;									// this[0x16] (force first push)
	previousRed = previousGreen = previousBlue = 0;
	colorSlot     = color_index;								// this[0x17]
	paletteToggle = 0;											// this[0x18]
	graphicsPort  = renderer->GetGraphicsPort(graphics_port_number);	// FUN_00447f84 -> this[0x1A]
}

//
// @004c38dc -- release both palette refs, then Gauge::~Gauge.  (The deleting
// destructor thunks @004c66ff / @004c6725 / @004c674b / @004c6771 forward here
// for the four derived ColorMappers.)
//
ColorMapper::~ColorMapper()
{
	WarehouseBinOf<Palette8> &palettes = renderer->warehousePointer->palette8Bin;
	for (int i = 0; i < 2; ++i)
	{
		palettes.Release(paletteName[i]);		// FUN_00443090 (release the AddRef'd palette)
		paletteName[i] = NULL;					// names not separately owned (no intern copy)
	}
	// base ~Gauge -- FUN_00444360
}

//
// @004c395c -- ColorMapper::BecameActive.  On (re)activation, force the next
// Execute (@004c3980) to re-push the hardware palette: invalidate the cached
// colour index + the last-written RGB so the "unchanged" fast-out can't skip the
// write (the surface may have been cleared/redrawn while the gauge was inactive).
//
void
	ColorMapper::BecameActive()
{
	currentColorIndex  = -1;			// this[0x15] @0x54
	previousColorIndex = -1;			// this[0x16] @0x58
	previousRed = previousGreen = previousBlue = 0xFF;	// this[0x19] @0x64-0x66
}

//
// @004c3980 -- the palette push.  Clamp the driving value to a [0,255] colour
// index, choose the active palette (flashing alternates the two when
// twoColorMode), read the index's R/G/B triple and, if it differs from last
// frame, write it straight into the hardware palette slot.  (Called by every
// derived ColorMapper's Execute -- e.g. ColorMapperCritical::Execute @004c3c1c.)
//
void
	ColorMapper::Execute()
{
	if (currentColorIndex > 254)	currentColorIndex = 255;
	if (currentColorIndex < 1)		currentColorIndex = 0;

	if (!twoColorMode)
	{
		if (currentColorIndex == previousColorIndex)
		{
			return;						// nothing changed
		}
		previousColorIndex = currentColorIndex;
	}
	else
	{
		//
		// Flash: alternate palette 0 / palette 1 every frame.
		//
		if (++paletteToggle > 1)
		{
			paletteToggle = 0;
		}
	}

	WarehouseBinOf<Palette8> &palettes = renderer->warehousePointer->palette8Bin;
	Palette8 *palette = palettes.GetIfAlreadyExists(paletteName[paletteToggle]);	// FUN_00442f6a (peek, no AddRef)
	if (palette != NULL)
	{
		//
		// Read the index's R/G/B triple and, if it differs from last frame,
		// push it straight into the hardware palette slot.
		//
		PaletteTriplet &entry = palette->Color[currentColorIndex];
		if (previousRed   != entry.Red   ||
			previousGreen != entry.Green ||
			previousBlue  != entry.Blue)
		{
			previousRed   = entry.Red;
			previousGreen = entry.Green;
			previousBlue  = entry.Blue;

			graphicsPort->SetColor(&entry, colorSlot);	// GraphicsPort::SetColor(PaletteTriplet*,int)
		}
	}
}


//###########################################################################
//   ColorMapperArmor          @004c3aa4 Make / @004c3b98 ctor
//###########################################################################
//
// Tints ONE damage zone's colour on the cockpit ARMOR DAMAGE schematic by that
// zone's live damage.  CFG shape (L4GAUGE.CFG:4789, inside the colorMapArmor
// macro):  cmArmor( rate, mode, colourSlot, paletteA, paletteB, zoneName )
//    e.g.  cmArmor(H, ModeSecondaryDamage, 32, adpal.pcc, adpal2.pcc, dz_ltorso);
//

MethodDescription
	ColorMapperArmor::methodDescription =
	{
		"cmArmor",
		ColorMapperArmor::Make,
		{
			{ ParameterDescription::typeRate,     NULL },	// rate ID
			{ ParameterDescription::typeModeMask, NULL },	// mode mask
			{ ParameterDescription::typeColor,    NULL },	// hardware colour slot
			{ ParameterDescription::typeString,   NULL },	// palette name A
			{ ParameterDescription::typeString,   NULL },	// palette name B
			{ ParameterDescription::typeString,   NULL },	// damage-zone name (e.g. "dz_ltorso")
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c3aa4 -- Make.  Resolve the named damage zone to an index on the entity,
// then construct.  (The binary built a transient zone-name descriptor and called
// FUN_0042076c; Entity::GetDamageZoneIndex IS that lookup -- it scans
// damageZones[] matching each zone's name.)
//
Logical
	ColorMapperArmor::Make(
		int					display_port_index,
		Vector2DOf<int>		/*position*/,
		Entity				*entity,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	int zone_index = entity->GetDamageZoneIndex(CString(p[5].data.string));	// FUN_0042076c
	if (zone_index < 0)
	{
		DebugStream << "ColorMapperArmor warning: damage zone "
					<< p[5].data.string << " not found\n";
	}

	ColorMapperArmor *gauge = (ColorMapperArmor *)operator new(0x70);	// FUN_00402298(0x70)
	if (gauge != NULL)
	{
		new (gauge) ColorMapperArmor(					// FUN_004c3b98
			p[0].data.rate, p[1].data.modeMask,
			(L4GaugeRenderer *)gauge_renderer,
			display_port_index,				// graphics port number (the runtime port)
			p[2].data.color,				// colour slot
			entity,
			p[3].data.string,				// palette A
			p[4].data.string,				// palette B
			zone_index,
			"ColorMapperArmor");
	}
	return True;
}

//
// @004c3b98 -- ctor: ColorMapper base + wire an ArmorZoneConnection feeding the
// resolved damage zone's live damage to the colour index.
//
ColorMapperArmor::ColorMapperArmor(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer,
	int					graphics_port_number,
	int					color_index,
	Entity				*entity,
	const char			*palette_a,
	const char			*palette_b,
	int					damage_zone_index,
	const char			*identification_string
):
	ColorMapper(						// FUN_004c37dc (owner_ID 0 folded in, as for cmHeat)
		rate, mode_mask, renderer, graphics_port_number,
		color_index, palette_a, palette_b, identification_string)
{
	unused = 0;						// this[0x1B] @0x6C

	ArmorZoneConnection *connection =
		(ArmorZoneConnection *)operator new(0x18);		// FUN_004c33a4 (0x18 bytes)
	if (connection != NULL)
		new (connection) ArmorZoneConnection(&currentColorIndex, entity, damage_zone_index);
	AddConnection(connection);						// (*this+0x34)
}

//
// ColorMapperArmor destructor.  No extra work: the ArmorZoneConnection is
// released by the base Gauge teardown, the palettes by ~ColorMapper -- the
// base-dtor chain runs implicitly at the closing brace.
//
ColorMapperArmor::~ColorMapperArmor()
{
}

//###########################################################################
//   ColorMapperMultiArmor      @004c3c48 Make / @004c3d60 ctor
//###########################################################################
//
// Tints one schematic colour by the WORST of up to 8 damage zones (e.g. a whole
// torso section).  CFG shape (L4GAUGE.CFG:96):
//    colorMapperMultiArmor( rate, mode, colourSlot, paletteA, paletteB,
//                           zone1, zone2, ... zone8 )   ("unused" for empty slots)
//

MethodDescription
	ColorMapperMultiArmor::methodDescription =
	{
		"colorMapperMultiArmor",
		ColorMapperMultiArmor::Make,
		{
			{ ParameterDescription::typeRate,     NULL },	// rate ID
			{ ParameterDescription::typeModeMask, NULL },	// mode mask
			{ ParameterDescription::typeColor,    NULL },	// hardware colour slot
			{ ParameterDescription::typeString,   NULL },	// palette name A
			{ ParameterDescription::typeString,   NULL },	// palette name B
			{ ParameterDescription::typeString,   NULL },	// zone 1
			{ ParameterDescription::typeString,   NULL },	// zone 2
			{ ParameterDescription::typeString,   NULL },	// zone 3
			{ ParameterDescription::typeString,   NULL },	// zone 4
			{ ParameterDescription::typeString,   NULL },	// zone 5
			{ ParameterDescription::typeString,   NULL },	// zone 6
			{ ParameterDescription::typeString,   NULL },	// zone 7
			{ ParameterDescription::typeString,   NULL },	// zone 8
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c3c48 -- Make.  Resolve up to 8 named zones to indices; build if any
// resolves.  (The binary flag test is `index != 0`, so a valid zone OR an
// "unused"/-1 slot both count as "present" -- i.e. it always builds; the
// Transfer simply skips index < 0.)
//
Logical
	ColorMapperMultiArmor::Make(
		int					display_port_index,
		Vector2DOf<int>		/*position*/,
		Entity				*entity,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	int zone_indices[8];
	Logical any = False;
	for (int i = 0; i < 8; i++)
	{
		zone_indices[i] = entity->GetDamageZoneIndex(CString(p[5 + i].data.string));	// FUN_0042076c
		if (zone_indices[i] != 0)		// binary: iVar2 != 0 (quirk -- see note above)
			any = True;
	}
	if (!any)
	{
		DebugStream << "colorMapperMultiArmor: No Damage zones found\n";
		return False;
	}

	ColorMapperMultiArmor *gauge = (ColorMapperMultiArmor *)operator new(0x6c);	// FUN_00402298(0x6c)
	if (gauge != NULL)
	{
		new (gauge) ColorMapperMultiArmor(				// FUN_004c3d60
			p[0].data.rate, p[1].data.modeMask,
			(L4GaugeRenderer *)gauge_renderer,
			display_port_index,			// graphics port number
			p[2].data.color,			// colour slot
			entity,
			p[3].data.string,			// palette A
			p[4].data.string,			// palette B
			zone_indices,
			"ColorMapperMultiArmor");
	}
	return True;
}

//
// @004c3d60 -- ctor: ColorMapper base + a MultiArmorConnection over the 8 zones.
//
ColorMapperMultiArmor::ColorMapperMultiArmor(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer,
	int					graphics_port_number,
	int					color_index,
	Entity				*entity,
	const char			*palette_a,
	const char			*palette_b,
	const int			*zone_indices,
	const char			*identification_string
):
	ColorMapper(						// FUN_004c37dc
		rate, mode_mask, renderer, graphics_port_number,
		color_index, palette_a, palette_b, identification_string)
{
	MultiArmorConnection *connection =
		(MultiArmorConnection *)operator new(0x3c);		// FUN_004c346c (0x3c bytes)
	if (connection != NULL)
		new (connection) MultiArmorConnection(&currentColorIndex, entity, zone_indices);
	AddConnection(connection);
}

ColorMapperMultiArmor::~ColorMapperMultiArmor()
{
	// base-dtor chain (~ColorMapper -> ~Gauge) releases the connection + palettes.
}

//###########################################################################
//   ColorMapperCritical        @004c3ddc Make / @004c3e40 ctor
//###########################################################################
//
// Tints one schematic colour by a subsystem's operational state (the "critical"
// secondary display mode).  CFG shape (L4GAUGE.CFG:143):
//    cmCrit( rate, mode, colourSlot, paletteA, paletteB, subsystemName )
//    e.g.  cmCrit(H, ModeSecondaryCritical, 32, adpal.pcc, adpal2.pcc, GeneratorA);
//

MethodDescription
	ColorMapperCritical::methodDescription =
	{
		"cmCrit",
		ColorMapperCritical::Make,
		{
			{ ParameterDescription::typeRate,     NULL },	// rate ID
			{ ParameterDescription::typeModeMask, NULL },	// mode mask
			{ ParameterDescription::typeColor,    NULL },	// hardware colour slot
			{ ParameterDescription::typeString,   NULL },	// palette name A
			{ ParameterDescription::typeString,   NULL },	// palette name B
			{ ParameterDescription::typeString,   NULL },	// subsystem name
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c3ddc -- Make.
//
Logical
	ColorMapperCritical::Make(
		int					display_port_index,
		Vector2DOf<int>		/*position*/,
		Entity				*entity,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	ColorMapperCritical *gauge = (ColorMapperCritical *)operator new(0x6c);	// FUN_00402298(0x6c)
	if (gauge != NULL)
	{
		new (gauge) ColorMapperCritical(				// FUN_004c3e40
			p[0].data.rate, p[1].data.modeMask,
			(L4GaugeRenderer *)gauge_renderer,
			display_port_index,			// graphics port number
			p[2].data.color,			// colour slot
			entity,
			p[3].data.string,			// palette A
			p[4].data.string,			// palette B
			p[5].data.string,			// subsystem name
			"ColorMapperCritical");
	}
	return True;
}

//
// @004c3e40 -- ctor: ColorMapper base + a CriticalConnection on the named subsystem.
//
ColorMapperCritical::ColorMapperCritical(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer,
	int					graphics_port_number,
	int					color_index,
	Entity				*entity,
	const char			*palette_a,
	const char			*palette_b,
	const char			*subsystem_name,
	const char			*identification_string
):
	ColorMapper(						// FUN_004c37dc
		rate, mode_mask, renderer, graphics_port_number,
		color_index, palette_a, palette_b, identification_string)
{
	Subsystem *subsystem = entity->FindSubsystem(subsystem_name);	// FUN_0041f98c
	if (subsystem == NULL)
	{
		DebugStream << "ColorMapperCritical warning: subsystem "
					<< subsystem_name << " does not exist\n";
	}

	CriticalConnection *connection =
		(CriticalConnection *)operator new(0x18);		// FUN_004c3598 (0x18 bytes)
	if (connection != NULL)
		new (connection) CriticalConnection(&currentColorIndex, subsystem);
	AddConnection(connection);
}

ColorMapperCritical::~ColorMapperCritical()
{
	// base-dtor chain (~ColorMapper -> ~Gauge) releases the connection + palettes.
}

//###########################################################################
//   ColorMapperHeat            @004c3f08 Make / @004c3f6c ctor   *** ANCHOR ***
//###########################################################################

//
// @004c3f08 -- Make.  Allocate a ColorMapperHeat (0x6c bytes) and construct it
// from the four resource strings (DAT_00514f98 rate, DAT_00514fdc mode-mask,
// DAT_00515020 port, DAT_00515064/0a8/0ec palette/slot/subsystem-name) and the
// class name "ColorMapperHeat" (0x5184c4).
//

//
// The gauge's configured parameters come LIVE from the config interpreter (the
// CFG "cmHeat" primitive) via methodDescription.parameterList -- the interpreter
// restores each instance's parsed params into this scratch list before calling
// Make (the engine pattern, cf. NumericDisplayScalar::Make L4GAUGE.cpp:569).
// The earlier DAT_* placeholders (unrecovered .data) are gone.
// CFG shape (verified L4GAUGE.CFG:182):
//    cmHeat( rate, modeMask, colourSlot, paletteA, paletteB, subsystemName )
//    e.g.   cmHeat(H, ModeSecondaryHeat, 32, heatpal.pcc, heatpal2.pcc, GeneratorA);
//
MethodDescription
	ColorMapperHeat::methodDescription =
	{
		"cmHeat",
		ColorMapperHeat::Make,
		{
			{ ParameterDescription::typeRate,     NULL },	// refresh-rate ID
			{ ParameterDescription::typeModeMask, NULL },	// display mode mask
			{ ParameterDescription::typeColor,    NULL },	// hardware colour slot
			{ ParameterDescription::typeString,   NULL },	// palette name A
			{ ParameterDescription::typeString,   NULL },	// palette name B
			{ ParameterDescription::typeString,   NULL },	// heat subsystem name
			PARAMETER_DESCRIPTION_END
		}
	};

Logical
	ColorMapperHeat::Make(
		int					display_port_index,
		Vector2DOf<int>		/*position*/,
		Entity				*entity,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	ColorMapperHeat *gauge =
		(ColorMapperHeat *)operator new(0x6c);		// FUN_00402298(0x6c)
	if (gauge != NULL)
	{
		new (gauge) ColorMapperHeat(				// FUN_004c3f6c
			p[0].data.rate,					// rate ID
			p[1].data.modeMask,				// mode mask
			(L4GaugeRenderer *)gauge_renderer,
			display_port_index,				// graphics port number (the runtime port)
			p[2].data.color,				// colour slot
			entity,
			p[3].data.string,				// palette name A
			p[4].data.string,				// palette name B
			p[5].data.string,				// heat subsystem name (e.g. "GeneratorA")
			"ColorMapperHeat");
	}
	return True;
}

//
// @004c3f6c -- ColorMapperHeat constructor.  THE ANCHOR (assert path
// "d:\tesla\bt\bt_l4\BTL4GAUG.CPP", line 0x68a).
//
// Builds a ColorMapper (vtable PTR_FUN_00518d00), locates the named heat
// subsystem on the host entity, *verifies it is a heat-bearing subsystem*,
// and wires a HeatConnection so that the subsystem's live temperature drives
// the palette tint.
//
ColorMapperHeat::ColorMapperHeat(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer,
	int					graphics_port_number,
	int					color_index,
	Entity				*entity,
	const char			*palette_a,
	const char			*palette_b,
	const char			*heat_subsystem_name,
	const char			*identification_string
):
	ColorMapper(						// FUN_004c37dc
		rate, mode_mask, renderer, graphics_port_number,
		color_index, palette_a, palette_b, identification_string)
{
	// vtable already set to PTR_FUN_00518d00 by this point.

	Subsystem *subsystem = entity->FindSubsystem(heat_subsystem_name);	// FUN_0041f98c
	if (subsystem == NULL)
	{
		DebugStream << "ColorMapperHeat warning: subsystem "
					<< heat_subsystem_name << " does not exist\n";	// FUN_004dbb24 x3
	}
	else
	{
		//
		// The subsystem MUST be heat-bearing (one of the two heat class
		// derivation tables) or the data feed is meaningless.
		//
		if (!subsystem->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()) &&	// FUN_0041a1a4 (0x50e3ec)
			!subsystem->IsDerivedFrom(*HeatSink::GetClassDerivations()))			// (0x50e604)
		{
			Verify(False);	// FUN_0040385c -- "Bad subsystem type" BTL4GAUG.CPP:0x68a
		}
	}

	//
	// HeatConnection: copies the subsystem's currentTemperature (@0x114) into
	// ColorMapper::currentColorIndex every frame (clamped to 0..100; 100 when
	// the subsystem is absent or fully operational).
	//
	HeatConnection *connection =
		(HeatConnection *)operator new(0x1c);		// FUN_00402298(0x1c)
	if (connection != NULL)
	{
		new (connection) HeatConnection(			// FUN_004c3664
			&currentColorIndex, subsystem);
	}
	AddConnection(connection);						// (*this+0x34)
}

//
// ColorMapperHeat destructor.  No extra work: the HeatConnection added above is
// released by the base Gauge teardown (RemoveAllConnections), and the two
// palettes by ~ColorMapper -- so the base-dtor chain (~ColorMapper -> ~Gauge)
// runs implicitly at the closing brace.  (The binary's deleting-dtor thunks
// @004c66ff/6725/674b/6771 forward to that same base chain.)
//
ColorMapperHeat::~ColorMapperHeat()
{
}


//###########################################################################
//###########################################################################
//   Two-part fill bars
//###########################################################################
//###########################################################################

//
// @004c2ff8 -- DrawTiledBitmap helper (file-private, shared by both bars).
// Tiles a source bitmap (param_7) across a destination rectangle, clipping the
// last partial row/column.  For each tile it MoveTo()s (vtbl+0x24) and issues
// a clipped blit (vtbl+0x58).  bitmap tile size is read from src+0x0C (width)
// and src+0x10 (height).
//
static void
	DrawTiledBitmap(
		GraphicsView	*view,
		int				originX,
		int				originY,
		int				width,
		int				height,
		int				color,
		BitMap			*tile
	)
{
	int tileW = tile->Data.Size.x;	// +0x0C
	int tileH = tile->Data.Size.y;	// +0x10
	(void)color;					// (BlitClipped colour arg has no GraphicsView::DrawBitMap analog)

	for (int y = 0; y < height; y += tileH)
	{
		int rowH = height - y;  if (rowH > tileH) rowH = tileH;
		if (rowH <= 0) continue;

		for (int x = 0; x < width; x += tileW)
		{
			int colW = width - x;  if (colW > tileW) colW = tileW;
			if (colW <= 0) continue;

			view->MoveToAbsolute(x, y);					// vtbl+0x24
			view->DrawBitMap(0, tile,					// vtbl+0x58 (clipped blit)
				originX, originY,
				originX + colW - 1, originY + rowH - 1);
		}
	}
}

//
// @004c407c -- HorizTwoPartBar::Make.  Allocates (0xb8), constructs, and
// verifies the tile image exists (warns "HorizTwoPartBar: Missing image ...").
//
// @004c4170 -- ctor: GraphicGauge base (vtable PTR_FUN_00518cbc), interns +
// ref-counts the tile image, sets the graphics-port extent
// (graphicsView.SetExtent(left,bottom,right,top)), stores fill/background
// colours and the bar size, then wires three GaugeConnectionDirectOf<Scalar>
// feeds (value, low, high -> this[0x24..0x26]).   FUN_00474855 x3.
//
// @004c4324 -- BecameActive: previousFill=0; previousFull=width.
//
// @004c4340 -- Execute: clamp value to [low,high], map to a pixel column,
// repaint only the changed sub-rectangles (tiled fill for the "filled" part,
// background fill for the remainder).  Uses _DAT_004c452c (0.0) as the floor.
//

//###########################################################################
//   VertTwoPartBar   @004c462c Make / @004c4724 ctor / @004c48e0 BecameActive
//                    @004c48fc Execute  (vtable PTR_FUN_00518c78)
//
// A vertical two-part fill bar (config keyword "vertBar").  Three Scalar
// connections drive it: value (current), low (warn threshold), high (max).  The
// bar grows bottom->top; pixels are value/high and low/high fractions of the bar
// height.  Used for the cockpit COOLANT bars (config binds current=CoolantMass,
// warn=CoolantCapacity, max=CoolantCapacity -- so the warn line sits at full and
// the bar simply empties as CoolantMass drops during firing).
//###########################################################################

MethodDescription
	VertTwoPartBar::methodDescription =
	{
		"vertBar",
		VertTwoPartBar::Make,
		{
			//
			// CFG shape (L4GAUGE.CFG:4521):
			//   vertBar( rate, mode, (w,h), tile.pcc, bg,fill,extra,
			//            currentAttr, warnAttr, maxAttr )
			//   e.g. vertBar(C,ModeAlwaysActive,(31,160),btwarn.pcc,255,255,0,
			//               HeatSink/CoolantMass, HeatSink/CoolantCapacity,
			//               HeatSink/CoolantCapacity);
			//
			{ ParameterDescription::typeRate,      NULL },	// rate ID
			{ ParameterDescription::typeModeMask,  NULL },	// mode mask
			{ ParameterDescription::typeVector,    NULL },	// (width,height)
			{ ParameterDescription::typeString,    NULL },	// tile bitmap name
			{ ParameterDescription::typeColor,     NULL },	// background colour -> @0x94
			{ ParameterDescription::typeColor,     NULL },	// fill colour       -> @0x98
			{ ParameterDescription::typeColor,     NULL },	// extra/empty colour-> @0x9C
			{ ParameterDescription::typeAttribute, NULL },	// current value  -> @0xB0
			{ ParameterDescription::typeAttribute, NULL },	// warn threshold -> @0xB4
			{ ParameterDescription::typeAttribute, NULL },	// max            -> @0xB8
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c462c -- Make.  Allocate + construct, then verify the tile bitmap exists
// (== the binary's "VertTwoPartBarNormalized: Missing image" warning path).
//
Logical
	VertTwoPartBar::Make(
		int					display_port_index,
		Vector2DOf<int>		position,
		Entity				* /*entity -- unused*/,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	VertTwoPartBar *gauge = (VertTwoPartBar *)operator new(0xbc);	// FUN_00402298(0xbc)
	if (gauge != NULL)
	{
		new (gauge) VertTwoPartBar(					// FUN_004c4724
			p[0].data.rate, p[1].data.modeMask,
			(L4GaugeRenderer *)gauge_renderer,
			display_port_index,						// graphics_port_number
			position.x, position.y,					// left, bottom
			position.x + p[2].data.vector.x,		// right = x + width
			position.y + p[2].data.vector.y,		// top   = y + height
			p[3].data.string,						// tile bitmap
			p[4].data.color,						// backgroundColor
			p[5].data.color,						// fillColor
			p[6].data.color,						// extraColor
			(Scalar *)p[7].data.attributePointer,	// current value source
			(Scalar *)p[8].data.attributePointer,	// warn threshold source
			(Scalar *)p[9].data.attributePointer,	// max source
			"VertTwoPartBar");
	}

	L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
	if (warehouse->bitMapBin.Get(p[3].data.string) == NULL)			// FUN_00442aec
	{
		DebugStream << "VertTwoPartBarNormalized: Missing image '"
			<< p[3].data.string << "'\n";
		return False;
	}
	warehouse->bitMapBin.Release(p[3].data.string);					// FUN_00442c12
	return True;
}

//
// @004c4724 -- ctor.  GraphicGauge base; intern + ref-count the tile bitmap; set
// the graphics-port extent (bottom->top); store the three colours and the bar
// size; wire three GaugeConnectionDirectOf<Scalar> feeds (value/low/high).
//
VertTwoPartBar::VertTwoPartBar(
	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					background_color,
	int					fill_color,
	int					extra_color,
	Scalar				*value_pointer,
	Scalar				*low_pointer,
	Scalar				*high_pointer,
	const char			*identification_string
):
	GraphicGauge(rate, mode_mask, renderer_in, 0,				// FUN_00444818 (owner_ID 0)
				 graphics_port_number, identification_string)
{
	// Own a copy of the tile-bitmap name (Make passes the transient interpreter
	// scratch buffer) and hold a ref for the gauge's life.
	tileImage = new char[strlen(tile_image) + 1];				// @0x90  FUN_004700ac
	strcpy(tileImage, tile_image);
	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
	warehouse->bitMapBin.Get(tileImage);						// FUN_00442aec (held)

	localView.SetPositionWithinPort(left, bottom, right, top);	// this+0x48 vtbl+0x08

	backgroundColor = background_color;							// @0x94  this[0x25]
	fillColor       = fill_color;								// @0x98  this[0x26]
	extraColor      = extra_color;								// @0x9C  this[0x27]
	width           = (right - left) - 1;						// @0xA0  this[0x28]
	height          = top - bottom;								// @0xA4  this[0x29]

	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &value, value_pointer));	// @0xB0
	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &low,   low_pointer));		// @0xB4
	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &high,  high_pointer));		// @0xB8
}

//
// @004c486c -- dtor.  Release the tile ref (keyed on tileImage, before the free),
// free the interned name; the base chain runs implicitly.
//
VertTwoPartBar::~VertTwoPartBar()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	warehouse->bitMapBin.Release(tileImage);					// FUN_00442c12
	delete[] tileImage;											// FUN_004022e8
	tileImage = NULL;
}

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

//
// @004c48e0 -- BecameActive: force a full redraw on the first Execute
// (previousFull=0, previousFill=width -- neither can match a real pixel pair).
//
void
	VertTwoPartBar::BecameActive()
{
	previousFull = 0;			// @0xAC  this[0x2B]
	previousFill = width;		// @0xA8  this[0x2A]
}

//
// @004c48fc -- Execute.  Clamp value to [0,high]; map value and warn to bar
// pixels (half-up round of height*x/high, clamped to [0,height]); if either
// changed, repaint: tile the bitmap over [0,warnPix), fill [warnPix,valPix) with
// fillColor, and clear [valPix,height) with extraColor.  (x87 pixel math recovered
// by disassembly @004c4940/@004c4960 -- Ghidra dropped the FPU args.)
//
void
	VertTwoPartBar::Execute()
{
	// clamp value to [0, high]  (@004c4908 fcomp 0.0 ; @004c4929 fcomp high)
	if (value < 0.0f)
		value = 0.0f;
	else if (value > high)
		value = high;

	int warnPix = (int)((Scalar)height * low   / high + 0.5f);	// warn threshold pixel
	int valPix  = (int)((Scalar)height * value / high + 0.5f);	// current value pixel
	if (warnPix < 0)      warnPix = 0;   else if (warnPix > height) warnPix = height;
	if (valPix  < 0)      valPix  = 0;   else if (valPix  > height) valPix  = height;

	if (warnPix != previousFull || valPix != previousFill)
	{
		L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
		BitMap *tile = warehouse->bitMapBin.Get(tileImage);		// FUN_00442aec

		localView.SetColor(backgroundColor);					// vtbl+0x18
		DrawTiledBitmap(&localView, 0, 0, width, warnPix - 1, extraColor, tile);
		warehouse->bitMapBin.Release(tileImage);				// FUN_00442c12

		if (warnPix < valPix)									// normal fill above the warn line
		{
			localView.SetColor(fillColor);
			localView.MoveToAbsolute(0, warnPix);				// vtbl+0x24
			localView.DrawFilledRectangleToAbsolute(width, valPix - 1);	// vtbl+0x48
		}
		if (valPix < height)									// empty region above the value
		{
			localView.SetColor(extraColor);
			localView.MoveToAbsolute(0, valPix);
			localView.DrawFilledRectangleToAbsolute(width, height);
		}

		previousFull = warnPix;
		previousFill = valPix;
	}
}

//
// @004c4b84 ctor / @004c4cac BecameActive / @004c4cc0 Execute
// -- VertNormalSlider (vtable PTR_FUN_00518c34): one GaugeConnectionDirectOf
// <Scalar> drives a normalised [0,1] value (clamped via _DAT_004c4d3c=0.0 /
// _DAT_004c4d40=1.0, saturating to 0x3f800000); Execute @004c4cc0 maps it to a
// row and calls Draw @004c4d48 which MoveTo()s and FillTo()s the lit rectangle.
//


//###########################################################################
//###########################################################################
//   OneOfSeveral family -- multi-frame bitmap selectors
//###########################################################################
//###########################################################################

//
// @004c4d88 -- OneOfSeveral base ctor (vtable PTR_FUN_00518bf0).  Stores the
// strip-vs-image flag (this[0x24]), interns the image name, computes the
// per-frame width/height by dividing the source bitmap (or pixmap) size by the
// column/row counts, and sets the graphics-port origin.  Two resource caches
// are used depending on fromImageStrip: BitMapCache (+4) vs PixMapCache (+0x20).
//
// @004c4e7c -- dtor: release the image from the appropriate cache, free the
// interned name, GraphicGauge::~GraphicGauge.
//
// @004c4f14 -- BecameActive: previousSelected = -1 (force first redraw).
//
// @004c4f28 -- Execute: if selected != previousSelected, compute the frame's
// (row,col) sub-rectangle within the strip and blit it (clipped, vtbl+0x5C/0x58)
// or stretch-blit the pixmap path.
//

//
// @004c5068 Make / @004c5148 ctor -- OneOfSeveralInt (vtable PTR_FUN_00518bac):
// OneOfSeveral(fromImageStrip=1) + one GaugeConnectionDirectOf<int>
// (FUN_004749de) feeding the frame index.   dtor @004c51d8.
//
// @004c5204 Make / @004c52d8 ctor -- OneOfSeveralPixInt (vtable PTR_FUN_00518b68):
// OneOfSeveral(fromImageStrip=0) + GaugeConnectionDirectOf<int>.  dtor @004c5364.
//
// @004c5390 Make / @004c5470 ctor -- OneOfSeveralStates (vtable PTR_FUN_00518b24):
// OneOfSeveral(fromImageStrip=1) + a StateConnection (FUN_004c3324) reading the
// subsystem state word @0x14.  BecameActive @004c552c clamps the state >= 0
// before chaining to OneOfSeveral::Execute @004c4f28.  dtor @004c5500.
//


//###########################################################################
//###########################################################################
//   HeadingPointer            @004c554c Make / @004c562c ctor
//###########################################################################
//###########################################################################

//
// Reconstructed from the binary (raw pseudo-C part_013.c:14560-14791 + a
// disassembly of Execute@004c5914 and ctor@004c562c to recover the x87 endpoint
// arithmetic Ghidra dropped).  The compass is a radial NEEDLE (a thick line from
// innerRadius to outerRadius at the heading angle) plus a NumericDisplay showing
// the heading in whole degrees.
//

MethodDescription
	HeadingPointer::methodDescription =
	{
		"headingPointer",
		HeadingPointer::Make,
		{
			//
			// CFG shape (L4GAUGE.CFG:4941):
			//   headingPointer( rate, mode, needleColor, numericFg, eraseColor,
			//                   innerRadius, outerRadius, font )
			//   e.g.  headingPointer(E, ModeAlwaysActive, 2,1,0, 20,39, helv15.pcc);
			//
			{ ParameterDescription::typeRate,     NULL },	// rate ID
			{ ParameterDescription::typeModeMask, NULL },	// mode mask
			{ ParameterDescription::typeColor,    NULL },	// needle colour  -> @0x90
			{ ParameterDescription::typeColor,    NULL },	// numeric fg     -> NumericDisplay
			{ ParameterDescription::typeColor,    NULL },	// erase/bg       -> @0x94 + ND bg
			{ ParameterDescription::typeInteger,  NULL },	// inner radius   -> @0x98
			{ ParameterDescription::typeInteger,  NULL },	// outer radius   -> @0x9C
			{ ParameterDescription::typeString,   NULL },	// font name
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c554c -- Make.  Allocate + construct the gauge, then probe that the glyph
// font exists (== NumericDisplayScalar::Make, L4GAUGE.cpp:626-640); the readout
// cannot draw without it.
//
Logical
	HeadingPointer::Make(
		int					display_port_index,
		Vector2DOf<int>		position,
		Entity				* /*entity -- unused*/,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	HeadingPointer *gauge = (HeadingPointer *)operator new(0xc0);	// FUN_00402298(0xc0)
	if (gauge != NULL)
	{
		new (gauge) HeadingPointer(					// FUN_004c562c
			p[0].data.rate, p[1].data.modeMask,
			(L4GaugeRenderer *)gauge_renderer,
			0,										// owner_ID
			display_port_index,						// graphics port number (the runtime port)
			position.x, position.y,
			p[2].data.color,						// needle colour
			p[3].data.color,						// numeric fg colour
			p[4].data.color,						// erase/bg colour
			p[5].data.integer,						// inner radius
			p[6].data.integer,						// outer radius (tip)
			p[7].data.string,						// font name
			"HeadingPointer");
	}

	L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
	if (warehouse->bitMapBin.Get(p[7].data.string) == NULL)			// FUN_00442aec
	{
		DebugStream << "HeadingPointer: missing font '" << p[7].data.string << "'\n";
		return False;
	}
	warehouse->bitMapBin.Release(p[7].data.string);					// FUN_00442c12
	return True;
}

//
// @004c562c -- ctor (vtable PTR_FUN_00518ae0).  GraphicGauge base; the embedded
// GraphicsViewRecord (previousDrawing) default-constructs as a member (== the
// binary's FUN_0044a5b4).  Set the port origin, store the colours/radii, hold a
// ref to the glyph font, and build an owned NumericDisplay centred on the compass
// mid-point (half the 3-digit glyph extent).
//
HeadingPointer::HeadingPointer(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer_in,
	unsigned int		owner_ID,
	int					graphics_port_number,
	int					x,
	int					y,
	int					needle_color,
	int					numeric_color,
	int					erase_color,
	int					inner_radius,
	int					outer_radius,
	const char			*font_name,
	const char			*identification_string
):
	GraphicGauge(rate, mode_mask, renderer_in, owner_ID,			// FUN_00444818
				 graphics_port_number, identification_string)
{
	localView.SetOrigin(x, y);					// this+0x48 vtbl+0x10

	needleColor = needle_color;					// @0x90
	eraseColor  = erase_color;					// @0x94
	innerRadius = inner_radius;					// @0x98
	outerRadius = outer_radius;					// @0x9C

	// Own a copy of the font name (in Make it is the transient interpreter scratch
	// buffer, so it MUST be copied -- the binary uses nameCopy/FUN_004700ac).
	imageName = new char[strlen(font_name) + 1];		// @0xA0
	strcpy(imageName, font_name);

	L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
	BitMap *font = warehouse->bitMapBin.Get(imageName);	// FUN_00442aec -- held for the gauge's life
	int fontW = (font != NULL) ? font->Data.Size.x : 0;	// BitMap+0x0C
	int fontH = (font != NULL) ? font->Data.Size.y : 0;	// BitMap+0x10

	int halfW = ((fontW / 14) * 3) / 2;			// 14 = totalDigitsPerFont; centre the 3-digit readout
	int halfH = fontH / 2;

	numericDisplay = new NumericDisplay(		// @0xBC  FUN_004700bc
		warehouse,
		-halfW, -halfH,							// centre on the compass mid-point
		imageName,
		3,										// number_of_digits
		NumericDisplay::unsignedFormat,			// format 0
		erase_color,							// background_color
		numeric_color);							// foreground_color
}

//
// @004c573c -- dtor.  Release the font ref (keyed on imageName, so before the
// free), free imageName, delete the NumericDisplay.  The base-dtor chain
// (~GraphicsViewRecord on previousDrawing, then ~GraphicGauge) runs implicitly.
//
HeadingPointer::~HeadingPointer()
{
	L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
	warehouse->bitMapBin.Release(imageName);	// FUN_00442c12 -- drop the ctor's Get ref
	delete[] imageName;							// FUN_004022e8
	imageName = NULL;

	delete numericDisplay;						// FUN_0047018c
	numericDisplay = NULL;
}

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

//
// @004c57d0 -- ShowInstance (debug dump).
//
void
	HeadingPointer::ShowInstance(char *indent)
{
	DebugStream << indent << "HeadingPointer:\n";
	char deeper[80];
	strcpy(deeper, indent);
	strcat(deeper, "...");
	DebugStream << deeper << "bgColor=" << eraseColor << "\n";	// @0x94 (printed first, per decomp)
	DebugStream << deeper << "fgColor=" << needleColor << "\n";	// @0x90
	GraphicGauge::ShowInstance(deeper);
}

//
// @004c58e8 -- BecameActive: force a full redraw + reset the readout.
//
void
	HeadingPointer::BecameActive()
{
	previousX = -999;			// 0xfffffc19 -- guaranteed != any real endpoint
	previousY = -999;
	numericDisplay->ForceUpdate();		// FUN_004703f4
}

//
// @004c5914 -- Execute.  Read the owner mech's heading from its orientation
// quaternion, draw the needle as a radial line innerRadius..outerRadius at that
// angle (recording the strokes so the next frame can erase them), and update the
// numeric readout.  Endpoint rounding is half-up: (int)(v + 0.5f), matching the
// binary's `fadd 0.5 ; _ftol`.
//
void
	HeadingPointer::Execute()
{
	Entity *owner = renderer->GetLinkedEntity();	// FUN_00417ab4(renderer+0x40)
	if (owner == NULL)
	{
		// BRING-UP: if the gauge renderer's entitySocket isn't wired to the viewpoint
		// mech, fall back to the player so the compass tracks the mech we're driving.
		// (In practice GetLinkedEntity resolves; this is belt-and-braces.)
		if (application != NULL)
			owner = (Entity *)application->GetViewpointEntity();
		if (owner == NULL)
			return;
	}

	// The binary read an EulerAngles component (FUN_0040954c) -- but the WinTesla MUNGA
	// EulerAngles(Quaternion) decomposition is AMBIGUOUS for a yawing mech: as the yaw
	// sweeps past +/-pi the quaternion double-cover flips it to a pitch=roll=pi branch,
	// so euler.yaw jumps discontinuously (the needle spins erratically).  YawPitchRoll
	// applies yaw FIRST, so its .yaw is the clean, continuous heading with pitch=roll~0.
	YawPitchRoll ypr;
	ypr = owner->localOrigin.angularPosition;		// entity+0x10c (the orientation quaternion)
	Scalar heading = -(Scalar)ypr.yaw;				// negate for the needle (as the binary did)

	SinCosPair sc;
	sc = Radian(heading);							// FUN_00408328 -- sin/cos of the heading

	// Needle endpoints: a radial line innerRadius..outerRadius along the heading.
	int ax = (int)((Scalar)innerRadius * sc.sine   + 0.5f);
	int ay = (int)((Scalar)innerRadius * sc.cosine + 0.5f);
	int bx = (int)((Scalar)outerRadius * sc.sine   + 0.5f);
	int by = (int)((Scalar)outerRadius * sc.cosine + 0.5f);

	if (bx != previousX || by != previousY)			// redraw only when the tip moves
	{
		previousX = bx;
		previousY = by;
		previousDrawing.Draw(&localView, eraseColor);	// FUN_0044a650 -- erase the last needle
		previousDrawing.Clear();						// FUN_0044a630
		localView.AttachRecorder(&previousDrawing);		// vtbl+0x64 -- record the new strokes
		localView.SetColor(needleColor);				// vtbl+0x18
		localView.MoveToAbsolute(ax, ay);				// vtbl+0x24
		localView.DrawThickLineToAbsolute(bx, by);		// vtbl+0x38
		localView.DetachRecorder();						// vtbl+0x68
	}

	// Numeric heading readout: whole degrees, normalized, displayed as (360 - deg).
	int deg = (int)((Scalar)ypr.yaw * 57.29578f + 0.5f);	// heading -> degrees (0x4c5ac8)
	if ((Scalar)deg < 0.0f)								// _DAT_004c5acc == 0.0f
		deg = (int)((Scalar)deg + 360.0f);
	int shown = (int)(360.0f - (Scalar)deg);
	numericDisplay->Draw(&localView, (Scalar)shown);	// FUN_00470430
}


//###########################################################################
//###########################################################################
//   BitMap wipe gauges and arc gauges
//###########################################################################
//###########################################################################

//
// @004c5b7c ctor / @004c5c80 dtor / @004c5cf4 BecameActive / @004c5d08 Execute
// -- BitMapInverseWipe (vtable PTR_FUN_00518a9c; name unconfirmed).
// A 3-frame strip (frameWidth = imageWidth/3) revealed in steps.  Execute
// @004c5d08 clamps the level to [0, frames], promotes it to 1 when the value
// is above _DAT_0050e3d8 (0.0025) but still rounds to 0, then blits each lit
// segment, stepping the colour through colorA/colorB by 2s.  Used as a base by
// btl4gau2's LeakGauge.
//
// @004c5e84 ctor / @004c5f30 dtor / @004c5fa4 BecameActive / @004c5fb8 Execute
// -- BitMapInverseWipe base (vtable PTR_FUN_00518a58): a two-segment reveal
// of one strip image; frame size read from the bitmap (src+0x0C/0x10).
//
// @004c61c8 ctor (vtable PTR_FUN_00518a14) -- the Scalar-driven variant:
// chains @004c5e84 then AddConnection(new GaugeConnectionDirectOf<Scalar>
// (&level)) (FUN_00474855).  Deleting-dtor thunk @004c66d9 -> @004c5f30.
//
// @004c6244 ctor (vtable PTR_FUN_005189d0) -- SegmentArc270: derives from the
// MUNGA L4 arc primitive (FUN_004745e0).  Precomputes the per-segment span:
//      segmentSpan = (int)( (float)(|n|/(|n|-1)) * 0.75f )    // _DAT_004c62d4
// Deleting-dtor thunk @004c66b3 -> FUN_00474094 (arc base dtor).
//
//###########################################################################
//   SegmentArcRatio   @004c62fc Make / @004c6394 ctor / @004c6488 Execute
//                     (vtable PTR_FUN_0051898c; base = engine SegmentArc)
//
// A segmented arc dial (config keyword "segmentArcRatio").  Two Scalar
// connections drive it -- numerator (value) and denominator (max) -- and Execute
// lights numerator/denominator of the arc.  Used for the cockpit SPEED arc
// (config binds numerator=LinearSpeed, denominator=MaxRunSpeed), so the arc
// sweeps as the mech accelerates.  Unlike VertTwoPartBar, the drawing is
// inherited from the engine SegmentArc base -- this class only computes the
// [0,1] fill fraction into the base's currentValue, then delegates the render.
//###########################################################################

MethodDescription
	SegmentArcRatio::methodDescription =
	{
		"segmentArcRatio",
		SegmentArcRatio::Make,
		{
			//
			// CFG shape (L4GAUGE.CFG:4964):
			//   segmentArcRatio( rate, mode, inner,outer, deg0,deg1, segs,
			//                    bg,fg, valueAttr, maxAttr )
			//   e.g. segmentArcRatio(C,ModeAlwaysActive,32,39,0,360,36,0,2,
			//                        LinearSpeed, MaxRunSpeed);
			//
			{ ParameterDescription::typeRate,      NULL },	// rate ID
			{ ParameterDescription::typeModeMask,  NULL },	// mode mask
			{ ParameterDescription::typeScalar,    NULL },	// inner radius
			{ ParameterDescription::typeScalar,    NULL },	// outer radius
			{ ParameterDescription::typeScalar,    NULL },	// start angle (deg)
			{ ParameterDescription::typeScalar,    NULL },	// end angle (deg)
			{ ParameterDescription::typeInteger,   NULL },	// segment count (+dir)
			{ ParameterDescription::typeColor,     NULL },	// background colour
			{ ParameterDescription::typeColor,     NULL },	// foreground colour
			{ ParameterDescription::typeAttribute, NULL },	// numerator (value)
			{ ParameterDescription::typeAttribute, NULL },	// denominator (max)
			PARAMETER_DESCRIPTION_END
		}
	};

//
// @004c62fc -- Make.  Allocate + construct; no resource to verify (returns True).
//
Logical
	SegmentArcRatio::Make(
		int					display_port_index,
		Vector2DOf<int>		position,
		Entity				* /*entity -- unused*/,
		GaugeRenderer		*gauge_renderer
	)
{
	ParameterDescription *p = methodDescription.parameterList;

	SegmentArcRatio *gauge = (SegmentArcRatio *)operator new(0xcc);	// FUN_00402298(0xcc)
	if (gauge != NULL)
	{
		new (gauge) SegmentArcRatio(				// FUN_004c6394
			p[0].data.rate, p[1].data.modeMask,
			(L4GaugeRenderer *)gauge_renderer,
			display_port_index,						// graphics_port_number
			position.x, position.y,					// centre
			p[2].data.scalar, p[3].data.scalar,		// inner, outer radius
			p[4].data.scalar, p[5].data.scalar,		// start, end angle
			p[6].data.integer,						// segment count (+dir)
			p[7].data.color, p[8].data.color,		// bg, fg
			(Scalar *)p[9].data.attributePointer,	// numerator source
			(Scalar *)p[10].data.attributePointer,	// denominator source
			"SegmentArcRatio");
	}
	return True;									// binary returns 1 unconditionally
}

//
// @004c6394 -- ctor.  Engine SegmentArc base (inner0==inner1, outer0==outer1 --
// the Make duplicates them, so the arc has a constant radius); precompute the
// span factor and wire the two Scalar connections.
//
SegmentArcRatio::SegmentArcRatio(
	GaugeRate			rate,
	ModeMask			mode_mask,
	L4GaugeRenderer		*renderer_in,
	int					graphics_port_number,
	int					center_x,
	int					center_y,
	Scalar				inner,
	Scalar				outer,
	Scalar				deg0,
	Scalar				deg1,
	int					number_of_segs,
	int					background_color,
	int					foreground_color,
	Scalar				*numerator_pointer,
	Scalar				*denominator_pointer,
	const char			*identification_string
):
	SegmentArc(rate, mode_mask, renderer_in, 0,					// FUN_00473f44 (owner_ID 0)
			   graphics_port_number, center_x, center_y,
			   inner, outer, inner, outer,						// inner0/outer0 == inner1/outer1
			   deg0, deg1, number_of_segs,
			   background_color, foreground_color,
			   True,											// use_thick_lines (binary param_20 = 1)
			   identification_string)
{
	// segmentSpan = (Scalar)(|n| / (|n|-1)) * 0.75f  -- |n|/(|n|-1) is INTEGER
	// division (== 1 for n>=2), so this is 0.75 for the 36-segment speed arc.
	int n = (number_of_segs < 0) ? -number_of_segs : number_of_segs;	// @0xC8
	segmentSpan = (Scalar)(n / (n - 1)) * 0.75f;					// _DAT_004c6484 = 0.75f

	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &numerator,   numerator_pointer));	// @0xC0
	AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &denominator, denominator_pointer));	// @0xC4
}

//
// @004c668d -- deleting-dtor thunk.  Connections + arc base are released by the
// implicit base-dtor chain (FUN_00474094); no own teardown.
//
SegmentArcRatio::~SegmentArcRatio()
{
}

//
// @004c6488 -- Execute.  currentValue = clamp(|numerator/denominator * span|,0,1);
// then delegate to the engine SegmentArc::Execute (0x474300) which lights that
// fraction of the segments.  (x87 recovered by disassembly: FUN_004dcd00 = fabs.)
//
void
	SegmentArcRatio::Execute()
{
	if (denominator < 1.0f)								// guard div-by-tiny / unpowered
	{
		currentValue = 0.0f;
	}
	else
	{
		currentValue = numerator / denominator * segmentSpan;
		if (currentValue < 0.0f) currentValue = -currentValue;	// FUN_004dcd00 = fabs
		if (currentValue < 0.0f)
			currentValue = 0.0f;
		else if (currentValue > 1.0f)
			currentValue = 1.0f;
	}
	SegmentArc::Execute();								// 0x474300 -- the base draw
}

// === btl4gau2.cpp begins at @004c6798 (SeekVoltage gauge) -- not part of this TU.
