Files
BT412/game/reconstructed/btl4gaug.cpp
T
arcattackandClaude Opus 4.8 d68284ede2 gauge wave P2a: build the condenser LeakGauge + valve slider (heat panel)
The gauge-widget-decode workflow reconstructed the unbuilt cockpit widgets from
the binary.  First two (the heat MFD's missing SET/LEAK columns):

- LeakGauge (BitMapInverseWipe, keyword "LeakGauge"): the class body already
  existed + is byte-faithful; it was just UNREGISTERED so every LeakGauge(...)
  config line was parse-skipped.  Added the Make factory + methodDescription +
  the BTL4MethodDescription[] registration.  Value @6 = Condenser/CoolantMassLeakRate
  (already published P1); 0 undamaged -> the leak wipe is authentically empty.

- VertNormalSlider (keyword "vertNormalSlider"): the condenser VALVE slider @2 was
  PROSE-ONLY.  Reconstructed all 7 functions (Make/ctor/dtor/TestInstance/
  BecameActive/Execute/Draw) from part_013.c:14051-14175 (Make @004c4b08 by
  disassembly) -- an XOR indicator row=Round(span*value) over the track; fixed the
  jumbled header member layout to the byte-exact order (sizeof 0xB0).  Published
  ValveSetting -> coolantFlowScale@0x15C on the HeatSink table so Condenser/ValveSetting
  resolves (verified: all 6 condensers OK).  The valve indicator renders (condenser 1
  shows it near the top = coolantFlowScale 1.0 = valve open).

Both /FORCE-safe (every vtable slot has a real body; link log grep clean, no
unresolved VertNormalSlider/BitMapInverseWipe).  Verified DBASE+dev gauges: the
heat panel now shows all 3 columns per condenser (TEMP scales, SET valve indicator,
LEAK track), coolant bar stays full, combat un-regressed (TARGET DESTROYED), 0 crashes.
The player-valve-toggle -> coolantFlowScale drive (SetValveSetting vtable+0x48) is
the remaining Phase-3 valve-mechanism work; the slider shows the static valve value today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:19:04 -05:00

2412 lines
87 KiB
C++

//===========================================================================//
// 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;
}
}
//###########################################################################
// VertNormalSlider (config keyword "vertNormalSlider" -- the condenser VALVE
// slider @2 in GenericHeatGauges1/2; vtable PTR_FUN_00518c34). Reconstructed
// from part_013.c:14051-14175 (Make @004c4b08 by disassembly). One
// GaugeConnectionDirectOf<Scalar> drives a normalised [0,1] value; Execute maps
// it to row = Round(span*value) and XOR-toggles a width x baseline indicator.
//###########################################################################
// CFG shape (L4GAUGE.CFG:4839): vertNormalSlider(rate,mode,(w,h),fgColor,bgColor,thickness,valveAttr)
MethodDescription
VertNormalSlider::methodDescription =
{
"vertNormalSlider",
VertNormalSlider::Make,
{
{ ParameterDescription::typeRate, NULL }, // rate ID
{ ParameterDescription::typeModeMask, NULL }, // mode mask
{ ParameterDescription::typeVector, NULL }, // (width,height)
{ ParameterDescription::typeColor, NULL }, // fill / foreground colour
{ ParameterDescription::typeColor, NULL }, // background colour
{ ParameterDescription::typeInteger, NULL }, // baseline (indicator thickness)
{ ParameterDescription::typeAttribute, NULL }, // value source (Condenser/ValveSetting, Scalar [0,1])
PARAMETER_DESCRIPTION_END
}
};
//
// @004c4b08 -- Make. Allocate 0xb0 + placement-construct. No owned bitmap, so no
// image-exists check; always returns True (the binary returns eax=1 even on alloc
// failure). `entity` unused (binds by attribute pointer already resolved into
// parameterList[6]).
//
Logical
VertNormalSlider::Make(
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
VertNormalSlider *gauge = (VertNormalSlider *)operator new(0xb0); // FUN_00402298(0xb0)
if (gauge != NULL)
{
new (gauge) VertNormalSlider( // FUN_004c4b84
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.color, // fill / foreground colour
p[4].data.color, // background colour
p[5].data.integer, // baseline (indicator thickness)
(Scalar *)p[6].data.attributePointer, // value source (Condenser/ValveSetting)
"VertNormalSlider");
}
return True;
}
//
// @004c4b84 -- ctor (vtable PTR_FUN_00518c34). GraphicGauge base; set the port
// extent (bottom->top), latch the raster op to XOR + the pen to the fill colour
// ONCE (so every Draw toggles the indicator in place), cache geometry, wire one
// Scalar connection. previousFill is NOT init here -- BecameActive sets it -1.
//
VertNormalSlider::VertNormalSlider(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int left,
int bottom,
int right,
int top,
int fill_color,
int background_color,
int baseline_in,
Scalar *value_pointer,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner_ID 0)
graphics_port_number, identification_string)
{
localView.SetPositionWithinPort(left, bottom, right, top); // vtbl+0x08
localView.SetOperation(GraphicsDisplay::Xor); // vtbl+0x0c (op == 3)
localView.SetColor(fill_color); // vtbl+0x18
fillColor = fill_color; // @0xA4
backgroundColor = background_color; // @0xA8
baseline = baseline_in; // @0x9C
width = (right - left) - 1; // @0x94
height = top - bottom; // @0x98
span = (top - bottom) - baseline_in; // @0xA0
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &value, value_pointer)); // -> @0xAC
}
//
// @004c4c68 -- dtor. Owns no resource; the connection + localView are released by
// the base teardown, so the body is empty (the base-dtor chain runs implicitly).
//
VertNormalSlider::~VertNormalSlider()
{
}
//
// @004c4c94 -- TestInstance. Non-virtual out-of-line forward to the base (its body
// MUST exist because the header declares it -- else /FORCE stubs it to an AV).
//
Logical
VertNormalSlider::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004c4cac -- BecameActive. Invalidate the cached row so the first Execute repaints.
//
void
VertNormalSlider::BecameActive()
{
previousFill = -1; // @0x90
}
//
// @004c4cc0 -- Execute. Clamp value to [0,1], map to row = Round(span*value), and
// on a change XOR-erase the old indicator + XOR-draw the new one.
//
void
VertNormalSlider::Execute()
{
if (value < 0.0f)
value = 0.0f;
else if (value > 1.0f)
value = 1.0f;
int pixel = HeatRound((Scalar)span * value); // row
if (pixel != previousFill)
{
Draw(); // erase old (XOR at old previousFill)
previousFill = pixel;
Draw(); // draw new (XOR at new previousFill)
}
}
//
// @004c4d48 -- Draw (file-private, non-virtual). Move the pen to the track's left
// edge at the current row, draw a width x baseline filled rectangle (XOR op + fill
// colour latched in the ctor, so a re-Draw at the same row erases).
//
void
VertNormalSlider::Draw()
{
if (previousFill >= 0)
{
localView.MoveToAbsolute(0, previousFill); // vtbl+0x24
localView.DrawFilledRectangleToRelative(width, baseline); // vtbl+0x4c
}
}
//###########################################################################
//###########################################################################
// 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.
//
//
// @004c4d88 -- OneOfSeveral base ctor (vtable PTR_FUN_00518bf0). Intern the
// image name, hold a ref, read the source strip size from the appropriate cache
// (PixMap8Cache if !fromImageStrip, else BitMapCache), divide by columns/rows to
// get the per-frame size, and set the graphics-port origin.
//
OneOfSeveral::OneOfSeveral(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int x,
int y,
Logical from_image_strip,
const char *image_name,
int background_color,
int foreground_color,
int columns_in,
int rows_in,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0, // FUN_00444818 (owner_ID 0)
graphics_port_number, identification_string)
{
backgroundColor = background_color; // @0x98
foregroundColor = foreground_color; // @0x9C
columns = columns_in; // @0xA0
fromImageStrip = from_image_strip; // @0x90
imageName = new char[strlen(image_name) + 1]; // @0x94 FUN_004700ac
strcpy(imageName, image_name);
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
if (fromImageStrip == 0) // PixMap8 strip
{
PixelMap8 *pm = warehouse->pixelMap8Bin.Get(imageName); // FUN_00442d2b (held)
frameWidth = (pm != NULL ? pm->Data.Size.x : 0) / columns_in; // @0xA4
frameHeight = (pm != NULL ? pm->Data.Size.y : 0) / rows_in; // @0xA8
}
else // BitMap strip
{
BitMap *bm = warehouse->bitMapBin.Get(imageName); // FUN_00442aec (held)
frameWidth = (bm != NULL ? bm->Data.Size.x : 0) / columns_in;
frameHeight = (bm != NULL ? bm->Data.Size.y : 0) / rows_in;
}
localView.SetOrigin(x, y); // this+0x48 vtbl+0x10
}
//
// @004c4e7c -- dtor. Release the image from the matching cache (keyed on
// imageName, before the free); base chain implicit.
//
OneOfSeveral::~OneOfSeveral()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
if (fromImageStrip == 0)
warehouse->pixelMap8Bin.Release(imageName); // FUN_00442e51
else
warehouse->bitMapBin.Release(imageName); // FUN_00442c12
delete[] imageName; // FUN_004022e8
imageName = NULL;
}
Logical
OneOfSeveral::TestInstance() const
{
return GraphicGauge::TestInstance();
}
//
// @004c4f14 -- BecameActive: previousSelected = -1 (force the first redraw).
//
void
OneOfSeveral::BecameActive()
{
previousSelected = -1; // @0xB0
}
//
// @004c4f28 -- Execute. On a change of `selected`, compute the frame's (col,row)
// within the strip and blit that sub-rectangle: DrawPixelMap8 (opaque) for a
// pixmap strip, or SetColor(bg)+DrawBitMapOpaque(fg,..) for a bitmap strip.
//
void
OneOfSeveral::Execute()
{
if (selected == previousSelected)
return;
previousSelected = selected;
int col, row;
if (columns == 1)
{
col = 0;
row = selected;
}
else
{
col = selected % columns;
row = selected / columns;
}
int sx0 = col * frameWidth;
int sx1 = frameWidth + sx0 - 1;
int sy0 = row * frameHeight;
int sy1 = frameHeight + sy0 - 1;
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
if (fromImageStrip == 0) // PixMap8: own palette, no SetColor
{
PixelMap8 *pm = warehouse->pixelMap8Bin.Get(imageName); // FUN_00442d2b
localView.DrawPixelMap8(True, 0, pm, sx0, sy0, sx1, sy1); // vtbl+0x5C (opaque)
warehouse->pixelMap8Bin.Release(imageName); // FUN_00442e51
}
else // BitMap strip
{
localView.SetColor(backgroundColor); // vtbl+0x18
BitMap *bm = warehouse->bitMapBin.Get(imageName); // FUN_00442aec
localView.DrawBitMapOpaque(foregroundColor, 0, bm, sx0, sy0, sx1, sy1); // vtbl+0x58
warehouse->bitMapBin.Release(imageName); // FUN_00442c12
}
}
//###########################################################################
// OneOfSeveralPixInt @004c5204 Make / @004c52d8 ctor (vtable PTR_FUN_00518b68)
//
// The cockpit button-state lamps (config "oneOfSeveralPixInt"): a PixMap8 strip
// of N frames selected by an integer game attribute (DuckState / Searchlight/
// LightOn / ControlsMapper/DisplayMode). Drawing is inherited from OneOfSeveral;
// this subclass just forces the PixMap path and wires the int connection.
//###########################################################################
MethodDescription
OneOfSeveralPixInt::methodDescription =
{
"oneOfSeveralPixInt",
OneOfSeveralPixInt::Make,
{
//
// CFG shape (L4GAUGE.CFG:5001):
// oneOfSeveralPixInt( rate, mode, strip.pcc, columns, rows, stateAttr )
// e.g. oneOfSeveralPixInt(E,ModeAlwaysActive,bduck.pcc,3,1,DuckState);
//
{ ParameterDescription::typeRate, NULL }, // rate ID
{ ParameterDescription::typeModeMask, NULL }, // mode mask
{ ParameterDescription::typeString, NULL }, // pixmap strip name
{ ParameterDescription::typeInteger, NULL }, // columns (frame count)
{ ParameterDescription::typeInteger, NULL }, // rows
{ ParameterDescription::typeAttribute, NULL }, // integer state selector
PARAMETER_DESCRIPTION_END
}
};
//
// @004c5204 -- Make. Allocate + construct, then verify the pixmap strip exists.
//
Logical
OneOfSeveralPixInt::Make(
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
OneOfSeveralPixInt *gauge = (OneOfSeveralPixInt *)operator new(0xb4); // FUN_00402298(0xb4)
if (gauge != NULL)
{
new (gauge) OneOfSeveralPixInt( // FUN_004c52d8
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
display_port_index, // graphics_port_number
position.x, position.y,
p[2].data.string, // pixmap strip
p[3].data.integer, p[4].data.integer, // columns, rows
(int *)p[5].data.attributePointer, // integer state source
"OneOfSeveralPixInt");
}
L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
if (warehouse->pixelMap8Bin.Get(p[2].data.string) == NULL) // FUN_00442d2b
{
DebugStream << "OneOfSeveralPixInt: Missing image '" << p[2].data.string << "'\n";
return False;
}
warehouse->pixelMap8Bin.Release(p[2].data.string); // FUN_00442e51
return True;
}
//
// @004c52d8 -- ctor. OneOfSeveral base with fromImageStrip=0 (PixMap8) and
// bg/fg=0 (the pixmap carries its own palette); one int connection -> selected.
//
OneOfSeveralPixInt::OneOfSeveralPixInt(
GaugeRate rate,
ModeMask mode_mask,
L4GaugeRenderer *renderer_in,
int graphics_port_number,
int x,
int y,
const char *image,
int columns_in,
int rows_in,
int *value_pointer,
const char *identification_string
):
OneOfSeveral(rate, mode_mask, renderer_in, graphics_port_number, x, y,
False, // fromImageStrip = 0 (PixMap)
image, 0, 0, // bg, fg = 0
columns_in, rows_in, identification_string)
{
selected = 0; // @0xAC this[0x2B]
AddConnection(new GaugeConnectionDirectOf<int>(0, &selected, value_pointer)); // FUN_004749de
}
//
// @004c5364 -- deleting-dtor thunk. All teardown is ~OneOfSeveral; implicit.
//
OneOfSeveralPixInt::~OneOfSeveralPixInt()
{
}
//###########################################################################
// Additional composite-cluster child gauges (used by btl4gau2's
// SubsystemCluster family). HorizTwoPartBar mirrors VertTwoPartBar
// (horizontal); OneOfSeveralInt/States are OneOfSeveral subclasses; the
// LeakGauge base BitMapInverseWipe reveals a bitmap up to a level.
//###########################################################################
//
// StateConnection -- @004c3324 ctor / @004c3390 Transfer. Copies a Subsystem's
// state word (@0x14) into the destination each frame (drives OneOfSeveralStates).
//
class StateConnection : public GaugeConnection
{
public:
StateConnection(int *destination, Entity *source)
: GaugeConnection(0), source(source), destination(destination) {} // FUN_004c3324
void Update() // @004c3390
{
*destination = *(int *)((char *)source + 0x14); // Subsystem state @0x14
}
protected:
Entity *source; // @0x10
int *destination; // @0x14
};
//
// @004c4170 -- HorizTwoPartBar ctor (vtable 0x518cbc). GraphicGauge base, intern
// the tile bitmap, SetExtent, store fill/bg + width/height, wire the value/low/
// high Scalar connections. Grows left->right (mirror of VertTwoPartBar).
//
HorizTwoPartBar::HorizTwoPartBar(
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 fill_color, int background_color,
Scalar *value_pointer, Scalar *low_pointer, Scalar *high_pointer,
const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0, graphics_port_number, // FUN_00444818
identification_string)
{
tileImage = new char[strlen(tile_image) + 1];
strcpy(tileImage, tile_image);
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
warehouse->bitMapBin.Get(tileImage);
localView.SetPositionWithinPort(left, bottom, right, top); // vtbl+0x08
fillColor = fill_color;
backgroundColor = background_color;
width = right - left;
height = (top - bottom) - 1;
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &value, value_pointer));
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &low, low_pointer));
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &high, high_pointer));
}
HorizTwoPartBar::~HorizTwoPartBar()
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(tileImage);
delete[] tileImage;
tileImage = NULL;
}
Logical HorizTwoPartBar::TestInstance() const { return GraphicGauge::TestInstance(); }
void HorizTwoPartBar::BecameActive() // @004c4324
{
previousFull = 0;
previousFill = width;
}
//
// @004c4340 -- Execute: clamp value to [0,high], map value/warn to bar pixels
// (half-up round of width*x/high), repaint on change (mirror of VertTwoPartBar
// but along X).
//
void HorizTwoPartBar::Execute()
{
if (value < 0.0f) value = 0.0f;
else if (value > high) value = high;
int warnPix = (int)((Scalar)width * low / high + 0.5f);
int valPix = (int)((Scalar)width * value / high + 0.5f);
if (warnPix < 0) warnPix = 0; else if (warnPix > width) warnPix = width;
if (valPix < 0) valPix = 0; else if (valPix > width) valPix = width;
if (warnPix != previousFull || valPix != previousFill)
{
if (warnPix < valPix)
{
localView.SetColor(fillColor);
localView.MoveToAbsolute(warnPix, 0);
localView.DrawFilledRectangleToAbsolute(valPix - 1, height);
}
if (valPix < width)
{
localView.SetColor(backgroundColor);
localView.MoveToAbsolute(valPix, 0);
localView.DrawFilledRectangleToAbsolute(width, height);
}
previousFull = warnPix;
previousFill = valPix;
}
}
//
// @004c5148 -- OneOfSeveralInt ctor (vtable 0x518bac): OneOfSeveral(fromStrip=1) +
// a GaugeConnectionDirectOf<int> feeding the selected frame. dtor @004c51d8.
//
OneOfSeveralInt::OneOfSeveralInt(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
int graphics_port_number, int x, int y, const char *image,
int columns, int rows, int *value_pointer, 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 GaugeConnectionDirectOf<int>(0, &selected, value_pointer)); // FUN_004749de
}
OneOfSeveralInt::~OneOfSeveralInt() {} // @004c51d8
//
// @004c5470 -- OneOfSeveralStates ctor (vtable 0x518b24): OneOfSeveral + a
// StateConnection reading the subsystem state word @0x14. BecameActive @004c552c
// clamps the state >= 0 then chains OneOfSeveral::Execute. dtor @004c5500.
//
OneOfSeveralStates::OneOfSeveralStates(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
int graphics_port_number, int x, int y, const char *image,
int columns, int rows, Entity *subsystem_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 StateConnection(&selected, subsystem_source)); // FUN_004c3324
}
OneOfSeveralStates::~OneOfSeveralStates() {} // @004c5500
void OneOfSeveralStates::BecameActive() // @004c552c
{
if (selected < 0)
selected = 0;
OneOfSeveral::BecameActive();
}
//
// gauge wave P2 -- the "LeakGauge" config factory + methodDescription. The class
// body (ctor/dtor/BecameActive/Execute) already existed + is byte-faithful; it was
// just UNREGISTERED, so every `LeakGauge(...)` config line (GenericHeatGauges1/2)
// was parse-skipped and the coolant-leak inverse-wipe never built. CFG shape
// (L4GAUGE.CFG:4858): LeakGauge(rate,mode,image.pcc,colorA,colorB,frames,third,levelAttr).
// value source @6 = Condenser/CoolantMassLeakRate -- already published (heat.cpp:345).
//
MethodDescription
BitMapInverseWipe::methodDescription =
{
"LeakGauge",
BitMapInverseWipe::Make,
{
{ ParameterDescription::typeRate, NULL }, // rate ID
{ ParameterDescription::typeModeMask, NULL }, // mode mask
{ ParameterDescription::typeString, NULL }, // leak strip bitmap -> @0x90
{ ParameterDescription::typeColor, NULL }, // colorA empty-cell -> @0x94
{ ParameterDescription::typeColor, NULL }, // colorB leak-cell -> @0x98
{ ParameterDescription::typeInteger, NULL }, // frames (=3) -> frames/fullWidth
{ ParameterDescription::typeScalar, NULL }, // third (.15; raw @0xB0, inert in base Execute)
{ ParameterDescription::typeAttribute, NULL }, // level source (Scalar CoolantMassLeakRate)
PARAMETER_DESCRIPTION_END
}
};
Logical
BitMapInverseWipe::Make(
int display_port_index,
Vector2DOf<int> position,
Entity * /*entity -- unused*/,
GaugeRenderer *gauge_renderer
)
{
ParameterDescription *p = methodDescription.parameterList;
BitMapInverseWipe *gauge = (BitMapInverseWipe *)operator new(0xb8); // FUN_00402298(0xb8)
if (gauge != NULL)
{
// NOTE the ctor lists `third` BEFORE `frames`, so map p[6]->third, p[5]->frames BY NAME.
new (gauge) BitMapInverseWipe( // FUN_004c5b7c
p[0].data.rate, p[1].data.modeMask,
(L4GaugeRenderer *)gauge_renderer,
display_port_index, // graphics_port_number
position.x, position.y, // -> localView.SetOrigin(x,y)
p[2].data.string, // image (eleak.pcc)
p[3].data.color, // color_a
p[4].data.color, // color_b
p[6].data.integer, // third (.15 raw bits; inert) -> @0xB0
p[5].data.integer, // frames (3)
(Scalar *)p[7].data.attributePointer, // level source (Condenser/CoolantMassLeakRate)
"LeakGauge");
}
L4Warehouse *warehouse = (L4Warehouse *)gauge_renderer->warehousePointer;
if (warehouse->bitMapBin.Get(p[2].data.string) == NULL) // FUN_00442aec
{
DebugStream << "LeakGauge: Missing image '" << p[2].data.string << "'\n";
return False;
}
warehouse->bitMapBin.Release(p[2].data.string); // FUN_00442c12
return True;
}
//
// @004c5b7c -- BitMapInverseWipe ctor (vtable 0x518a9c): GraphicGauge base;
// SetOrigin(x,y), intern the image, store colours + frame geometry (frameWidth =
// imageWidth/3, fullWidth = frames*2), wire a Scalar level connection. Used as
// the coolant LeakGauge.
//
BitMapInverseWipe::BitMapInverseWipe(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in,
int graphics_port_number, int x, int y, const char *image,
int color_a, int color_b, int third, int frames,
Scalar *value_pointer, const char *identification_string
):
GraphicGauge(rate, mode_mask, renderer_in, 0, graphics_port_number,
identification_string)
{
localView.SetOrigin(x, y); // vtbl+0x10
imageName = new char[strlen(image) + 1];
strcpy(imageName, image);
colorA = color_a;
colorB = color_b;
this->frames = frames;
fullWidth = frames * 2;
this->third = third;
L4Warehouse *warehouse = (L4Warehouse *)renderer_in->warehousePointer;
BitMap *bmp = warehouse->bitMapBin.Get(imageName);
if (bmp == NULL)
{
frameWidth = 0;
frameHeight = 0;
}
else
{
frameWidth = bmp->Data.Size.x / 3;
frameHeight = bmp->Data.Size.y;
}
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &value, value_pointer)); // FUN_00474855
}
BitMapInverseWipe::~BitMapInverseWipe() // @004c5c80
{
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
warehouse->bitMapBin.Release(imageName);
delete[] imageName;
imageName = NULL;
}
Logical BitMapInverseWipe::TestInstance() const { return GraphicGauge::TestInstance(); }
void BitMapInverseWipe::BecameActive() // @004c5cf4
{
previousLevel = -1;
}
//
// @004c5d08 -- Execute. Round the level to [0, fullWidth] (a tiny non-zero value
// forces at least 1); on a change repaint `frames` segments stacked vertically,
// each picking its strip frame from the level: <1 = empty (colorA, frame 0), ==1 =
// half (colorB, middle frame), >1 = full (colorB, last frame), decrementing the
// level by 2 per segment. (The coolant LeakGauge; value = CoolantMassLeakRate.)
//
void BitMapInverseWipe::Execute()
{
int level = (int)(value + (value < 0.0f ? -0.5f : 0.5f)); // FUN_004dcd94 round
if (level < 0) level = 0;
if (level > fullWidth) level = fullWidth;
if (value > 0.0025f && level < 1) level = 1; // _DAT_0050e3d8
if (level != previousLevel)
{
previousLevel = level;
L4Warehouse *warehouse = (L4Warehouse *)renderer->warehousePointer;
BitMap *bmp = warehouse->bitMapBin.Get(imageName);
if (bmp != NULL)
{
int y = -frameHeight;
for (int seg = frames; seg > 0; seg--)
{
localView.MoveToAbsolute(0, y); // vtbl+0x24
int frameIndex;
if (level < 1) { localView.SetColor(colorA); frameIndex = 0; }
else if (level == 1) { localView.SetColor(colorB); frameIndex = frameWidth; }
else { localView.SetColor(colorB); frameIndex = frameWidth * 2; }
localView.DrawBitMap(0, bmp, frameIndex, 0, // vtbl+0x54
frameWidth + frameIndex, frameHeight);
level -= 2;
y -= frameHeight;
}
}
warehouse->bitMapBin.Release(imageName);
}
}
//###########################################################################
//###########################################################################
// 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
}
//
// @004c6244 -- SegmentArc270 ctor (vtable 0x5189d0). Engine SegmentArc base + one
// Scalar connection driving currentValue (a 0..1 fraction, e.g. a weapon's
// PercentDone recharge) + the segment-span factor (|n|/(|n|-1) * 0.75, integer
// division == 0.75 for n>=2). Inherits SegmentArc::Execute (draws the lit segments
// up to currentValue). Used as the WeaponCluster recharge dial.
//
SegmentArc270::SegmentArc270(
GaugeRate rate, ModeMask mode_mask, L4GaugeRenderer *renderer_in, int owner_ID,
int graphics_port_number, int center_x, int center_y,
Scalar inner0, Scalar outer0, Scalar inner1, Scalar outer1,
Scalar deg0, Scalar deg1, int number_of_segs,
int background_color, int foreground_color,
Scalar *value_pointer, Logical use_thick_lines, const char *identification_string
):
SegmentArc(rate, mode_mask, renderer_in, owner_ID, graphics_port_number,
center_x, center_y, inner0, outer0, inner1, outer1, deg0, deg1,
number_of_segs, background_color, foreground_color, use_thick_lines,
identification_string)
{
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &currentValue, value_pointer));
int n = (number_of_segs < 0) ? -number_of_segs : number_of_segs;
segmentSpan = (Scalar)(n / (n - 1)) * 0.75f; // _DAT_004c62d4 = 0.75
}
SegmentArc270::~SegmentArc270() {} // @004c66b3 (base chain)
// === btl4gau2.cpp begins at @004c6798 (SeekVoltage gauge) -- not part of this TU.