//===========================================================================// // 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::ctor // FUN_004749de GaugeConnectionDirectOf::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 class-derivation tags (HeatableSubsystem / // HEATWATCHER -- task #57 relabel; was "HeatSink") // // 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 #pragma hdrstop #if !defined(BTL4GAUG_HPP) # include #endif #if !defined(APP_HPP) # include #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 HeatableSubsystem) vs. the // alternate (0x50e604 = HEATWATCHER, task #57 relabel); destination@0x18. // Transfer: no subsystem -> 100 ; subsystem operational (+0x40 == 1) -> 100 ; // else Round( Resolve(subsys+0x114) ) -- NB for a Heatable that offset is // currentTemperature, for a WATCHER it is watchedLink (the flag@0x14 likely // picks the read; the watcher path is unverified [T4]). // 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 &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 &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 &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 /*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(¤tColorIndex, 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 /*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(¤tColorIndex, 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 /*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(¤tColorIndex, 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 /*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. // extern int BTIsHeatWatcher(Subsystem *sub); // heatfamily_reslice.cpp bridge if (!subsystem->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()) && // FUN_0041a1a4 (0x50e3ec) !BTIsHeatWatcher(subsystem)) // (0x50e604 = HEATWATCHER -- task #57 relabel: // the old "HeatSink" reading was redundant after // the HeatableSubsystem test; the binary's // alternate branch is the disjoint WATCHER family // (ammo bins / torso carry a mirrored heatAlarm)) { 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 ¤tColorIndex, 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 // 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 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 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(0, &value, value_pointer)); // @0xB0 AddConnection(new GaugeConnectionDirectOf(0, &low, low_pointer)); // @0xB4 AddConnection(new GaugeConnectionDirectOf(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 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 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(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 // (FUN_004749de) feeding the frame index. dtor @004c51d8. // // @004c5204 Make / @004c52d8 ctor -- OneOfSeveralPixInt (vtable PTR_FUN_00518b68): // OneOfSeveral(fromImageStrip=0) + GaugeConnectionDirectOf. 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 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(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(0, &value, value_pointer)); AddConnection(new GaugeConnectionDirectOf(0, &low, low_pointer)); AddConnection(new GaugeConnectionDirectOf(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 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(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 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(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 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 // (&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 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(0, &numerator, numerator_pointer)); // @0xC0 AddConnection(new GaugeConnectionDirectOf(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(0, ¤tValue, 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 diagName = 0; } SegmentArc270::~SegmentArc270() {} // @004c66b3 (base chain) // // PORT diagnostic wrapper (frozen-dial hunt, 2026-07-12): log the exact value // and segment memory the arc runs with, then chain the engine draw untouched. // void SegmentArc270::Execute() { if (getenv("BT_PANEL_LOG") && diagName != 0) { static int s_al = 0; if ((s_al++ % 60) == 0) DEBUG_STREAM << "[arc] " << diagName << " val=" << (float)currentValue << " prevSeg=" << previousSegment << " segs=" << numberOfSegments << "\n" << std::flush; } SegmentArc::Execute(); } // === btl4gau2.cpp begins at @004c6798 (SeekVoltage gauge) -- not part of this TU.