Files
BT412/game/reconstructed/powersub.cpp
T
arcattackandClaude Opus 4.8 6fceb58439 vehicleSubSystems Phase 2: authentic aux-screen assignment -> panels build
The engineering-screen cluster panels now build for each subsystem on its real
aux-screen position.  Two pieces:

1. Aux-screen bridge (powersub.cpp BTGetSubsystemAuxScreen): the assignment lives
   on PoweredSubsystem (auxScreenNumber/Placement/Label, resource +0x104/108/10C)
   which our ctor already populates -- but the reconstructed heat-leaf branch is
   NOT byte-exact, so the Make's raw sub[0x1dc]/[0x1e0]/[0x1e4] reads were garbage
   (0xCDCDCDCD).  The bridge casts through the real PoweredSubsystem type + returns
   the NAMED fields (and is the FUN_0041a1a4/0x50f4bc type filter).  Make + Subsystem
   Cluster now read via the bridge.

2. Reconstruct the 4 child gauges the clusters build that were declared-but-undefined
   (only VertTwoPartBar/OneOfSeveral/PixInt existed): HorizTwoPartBar (full, mirrors
   Vert), OneOfSeveralInt (ctor), OneOfSeveralStates (ctor + BecameActive + State
   Connection @004c3324), BitMapInverseWipe/LeakGauge (full) + SeekVoltageGraph's
   Execute/BecameActive/dtor -- their /FORCE-stubbed ctors were segfaulting when a
   cluster built them.

VERIFIED (BT_DEV_GAUGES + BT_VSS_LOG): 7 authentic panels build -- Sensor(scr5,
qsensors) HeatSinkCluster, Myomers(scr6,qmyomers) MyomerCluster, PPC(scr1,10) +
Emitter(scr4,8,7) Energy/Ballistic clusters -- with real labels + placements, 0
crashes, combat un-regressed.

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

1284 lines
39 KiB
C++

//===========================================================================//
// File: powersub.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Powered (electrically driven) subsystems -- voltage routing, //
// generators, myomer drive and the low-voltage watchdog //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C for the module cluster @0x4b02f0..@0x4b2678. The only function the
// decomp tagged is the PoweredSubsystem ctor @004b0f74 (file=bt/powersub.cpp);
// the cluster was recovered via the call graph, the class vtables and the
// "d:\tesla_bt\bt\POWERSUB.CPP" assertion strings. Each non-trivial method
// cites the originating @ADDR.
//
// Hex/byte float constants converted to decimal:
// 0x3f800000 = 1.0f 0x3f000000 = 0.5f 0xbf800000 = -1.0f
// _DAT_004b0bcc = 1.0e-4f _DAT_004b297c = 0.0f
//
// Helper-function name mapping (engine internals referenced by the decomp;
// the heat.* names are carried over from the heat-subsystem reconstruction):
// FUN_004adda0 HeatSink::HeatSink (PoweredSubsystem / Generator base ctor)
// FUN_004adfd4 HeatSink::~HeatSink
// FUN_004ad924 HeatSink::HeatSinkSimulation (base Performance)
// FUN_004ad760 HeatSink::ResetToInitialState
// FUN_004ad748 HeatSink::HandleMessage (base message dispatch)
// FUN_004add30 HeatSink::GetStatusFlags
// FUN_004ac868 HeatableSubsystem::~HeatableSubsystem (chained from PowerController dtor)
// FUN_004ac530 Subsystem ctor (PowerController base)
// FUN_004ac9c8 Subsystem::IsDamaged() (returns True when the part is dead)
// FUN_0049fb54 Subsystem::IsDamaged()/IsDead variant used by PowerController
// FUN_00417ab4 SharedData::Resolve() (connection -> Subsystem*)
// FUN_004179d4 SharedData ctor FUN_004179f8 SharedData dtor FUN_00417a5c SharedData::Clear
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041baa4 ~AlarmIndicator
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_0040385c Verify()/assert(msg,file,line)
// FUN_004dcd00 fabsf()
// FUN_004dbb24 DebugStream::operator<<(char*) FUN_004d9c38 DebugStream::endl
// FUN_004d4a78 strlen()
// FUN_00404088 NotationFile::Read(name,key,char**)
// FUN_00404118 NotationFile::Read(name,key,float*)
// FUN_004040d8 NotationFile::Read(name,key,int*)
// FUN_004215b0 Get_Segment_Index(file,name)
// FUN_004ae150 HeatSink::CreateStreamedSubsystem
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(POWERSUB_HPP)
# include <powersub.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
//
// Tuning constants observed as read-only globals in the decomp.
//
static const Scalar VoltageEpsilon = 1.0e-4f; // _DAT_004b0bcc (short-detect threshold)
static const Scalar MinOutputVoltage = 0.0f; // _DAT_004b2158 / _DAT_004b297c
static const Scalar MaxOutputVoltage = 1.0f; // _DAT_004b2154 / _DAT_004b2258
static const Scalar RatedVoltageRef = 1.0f; // _DAT_004b1d10 (myomer rated reference)
static const Scalar DegradedDriveScale = 0.5f; // _DAT_004b1d14 (heat-degradation throttle)
static const Scalar ResourceUnset = -1.0f; // _DAT_004b177c / _DAT_004b1a3c / _DAT_004b2580
static const Scalar MinVoltageScale = 1.0f; // _DAT_004b1924 (PowerWatcher threshold scale)
//###########################################################################
//###########################################################################
// PoweredSubsystem
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
PoweredSubsystem::SharedData
PoweredSubsystem::DefaultData(
PoweredSubsystem::GetClassDerivations(),
PoweredSubsystem::GetMessageHandlers(),
PoweredSubsystem::GetAttributeIndex(),
PoweredSubsystem::StateCount
);
Derivation*
PoweredSubsystem::GetClassDerivations()
{
static Derivation classDerivations(HeatSink::GetClassDerivations(), "PoweredSubsystem");
return &classDerivations;
}
//#############################################################################
// Construction / Destruction
//
// @004b0f74 -- THE tagged powersub.cpp function (size 486). Builds on the
// HeatSink base, then resolves and attaches the voltage source named in the
// resource and primes the electrical state machine.
//
PoweredSubsystem::PoweredSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004adda0
voltageSource(0), // FUN_004b1bb1(this+0x74, 0)
electricalStateAlarm(5), // FUN_0041b9ec(this+0x99, 5)
modeAlarm(3) // FUN_0041b9ec(this+0xae, 3)
{
Check(owner);
Check_Pointer(subsystem_resource);
//
// Resolve the "VoltageSource" index to the generator subsystem.
//
// ⚠ ROOT-CAUSE FIX (sibling of the heat.cpp heap corruption): voltageSourceIndex
// indexes the owner's SUBSYSTEM ROSTER, not the skeleton segment table. Raw
// PoweredSubsystem ctor (part_013.c:1198):
// if (res->voltageSourceIndex /*+0xFC*/ < owner->subsystemCount /*+0x124*/)
// source = owner->subsystemArray[voltageSourceIndex] /*+0x128*/;
// The earlier GetSegment draft cast an EntitySegment (288B) to Generator*, so
// AttachToVoltageSource's `currentTapCount += 1` wrote 136 bytes past that heap
// block at every mech spawn. Roster slots are pre-zeroed -> NULL = warn path.
Subsystem *source = 0;
if (subsystem_resource->voltageSourceIndex >= 0
&& subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount())
{
source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex);
}
else
{
// @004b0f74: "Bad voltageSourceIndex to connect power to!" POWERSUB.CPP:0x264
Verify(False, "Bad voltageSourceIndex to connect power to!", __FILE__, 0x264);
}
//
// A "master" instance (segment flagged 0x100, not a sub-/damaged copy)
// runs the per-frame electrical simulation and owns the live link.
//
// INTEGRATION (gate reconcile): master-gate reads OWNER simulationFlags
// (param_2+0x28), the oracle-verified authoritative source — not the per-
// segment resource flags (which stream 0 here). See heat.cpp note.
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // (owner flags & 0xC) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0 // owner flags & 0x100
)
{
SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation); // this[7..9] = PTR @0050f5b0
if (source == 0)
{
// @004b0f74: POWERSUB.CPP:0x272
Verify(False, "Master PoweredSubsystem is missing required generator", __FILE__, 0x272);
}
else
{
AttachToVoltageSource(source); // FUN_004b0dd8
}
}
else if (
// gate on OWNER flags, matching raw part_013.c:1216 (param_2+0x28 & 0xc)==4
// (the earlier draft read the resource flags here -- always 0 -> dead branch)
(owner->simulationFlags & SegmentCopyMask) == 4
&& source != 0
)
{
AttachToVoltageSource(source);
}
thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient; // +0x100
startTime = subsystem_resource->startTime; // +0x18C
startTimer = startTime;
electricalStateAlarm.SetLevel(Ready); // FUN_0041bbd8(this+0x99, 4)
modeAlarm.SetLevel(Connected); // FUN_0041bbd8(this+0xae, 1)
voltageScale = 1.0f;
auxScreenNumber = subsystem_resource->auxScreenNumber; // +0x104
auxScreenPlacement = subsystem_resource->auxScreenPlacement; // +0x108
strcpy(auxScreenLabel, subsystem_resource->auxScreenLabel); // inline copy from +0x10C
strcpy(engScreenLabel, subsystem_resource->engScreenLabel); // inline copy from +0x14C
Check_Fpu();
}
//
// @004b115c -- tears down the alarms and the voltage-source connection, then
// chains to ~HeatSink.
//
PoweredSubsystem::~PoweredSubsystem()
{
Check(this);
// modeAlarm (@0xae) / electricalStateAlarm (@0x99) / voltageSource (@0x74)
// are released here; FUN_004adfd4 finishes the HeatSink chain.
Check_Fpu();
}
//###########################################################################
// TestClass / TestInstance -- PoweredSubsystem
//
Logical
PoweredSubsystem::TestClass(Mech &)
{
return True;
}
Logical
PoweredSubsystem::TestInstance() const // @004b1208
{
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x50f4bc)
}
//#############################################################################
// Per-frame simulation
//
//
// @004b0bd0 -- registered Performance for a master PoweredSubsystem. Runs the
// HeatSink simulation, then advances the electrical state machine from the
// state of the powering generator and (in AutoConnect mode) hunts for a
// replacement generator when the link is lost.
//
void
PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice)
{
HeatSink::HeatSinkSimulation(time_slice); // FUN_004ad924
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x74)
if (source == 0)
{
electricalStateAlarm.SetLevel(NoVoltage);
}
else
{
if (source->GeneratorStateOf() == 3) // source+0x210 == 3 (shorted)
{
electricalStateAlarm.SetLevel(Shorted);
}
if (source->GeneratorStateOf() == 1 || source->GeneratorStateOf() == 4)
{
electricalStateAlarm.SetLevel(GeneratorOff);
}
}
switch (electricalStateAlarm.GetLevel()) // this[0x9e] @0x278
{
case Starting:
startTimer += time_slice;
if (startTime <= startTimer)
{
electricalStateAlarm.SetLevel(Ready);
}
break;
case NoVoltage:
if (source != 0)
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
case Shorted:
case GeneratorOff:
if (source != 0 && source->GeneratorStateOf() == 2) // generator back to Ready
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
}
//
// AutoConnect: if the mode alarm is in the auto state and we are not
// damaged and currently unpowered, scan every GeneratorClassID segment in
// the mech and attach to the first one that will accept a tap.
//
if (
modeAlarm.GetLevel() == AutoConnect // this[0xb3] @0x2cc == 2
&& GetStatusFlags() == 0 // (*this[0x40])(this,0) == 0
)
{
Mech *mech = (Mech *)owner;
for (int i = 0; ; ++i) // owner+0x124 segment scan
{
EntitySegment *segment_node = mech->GetSegment(i); // owner+0x128[i]
if (segment_node == 0)
{
break;
}
Subsystem *segment = (Subsystem *)segment_node;
if (
segment->GetClassID() == RegisteredClass::GeneratorClassID // segment+4 == 0xbc1
&& GetStatusFlags() != 0
&& AttachToVoltageSource(segment) != -1 // FUN_004b0dd8
)
{
break;
}
}
}
Check_Fpu();
}
//#############################################################################
// Subsystem virtual overrides
//
//
// @004b0e6c -- powered/unpowered transition. When power is cut the electrical
// alarm goes idle; when restored it returns to "GeneratorOff" with the start
// timer reset, but only while the source is actually Ready (state 2).
//
void
PoweredSubsystem::ResetToInitialState(Logical powered)
{
HeatSink::ResetToInitialState(True); // FUN_004ad760
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x74)
if (!powered)
{
if (source != 0)
{
electricalStateAlarm.SetLevel(NoVoltage); // SetLevel 0 in decomp -> Starting/idle
startTimer = 0.0f; // this[0xc6] = 0
}
}
else
{
if (source != 0 && source->GeneratorStateOf() == 2)
{
electricalStateAlarm.SetLevel(Ready); // SetLevel 4
startTimer = startTime; // this[0xc6] = this[0xc5]
}
}
}
//
// @004b0efc -- pass the message to the HeatSink base; if the part is alive,
// the source short flag is set and message 4 arrives, force a short-recovery.
//
Logical
PoweredSubsystem::HandleMessage(int message)
{
HeatSink::HandleMessage(message); // FUN_004ad748
Generator *source = (Generator *)(/* owner subsystem-message manager */
BT_GetMessageManager((Mech *)owner)); // *(this[0x34]+0x190)
if (!IsDamaged() // FUN_004ac9c8 == 0
&& source->ShortFlag() != 0 // source+0x25c != 0
&& message == 4)
{
ForceShortRecovery(); // FUN_004b11bc(this)
}
return True;
}
//
// @004b11bc -- on a short event, if neither this part nor its live source is
// damaged, drive the source's alarm to "shorted" (3) and clear its output.
//
void
PoweredSubsystem::ForceShortRecovery() // internal helper for @004b0efc
{
if (IsDamaged()) // FUN_004ac9c8
return;
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
if (source != 0 && !source->IsDamaged())
{
source->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3
source->outputVoltage = 0.0f; // source+0x1f8 = 0
}
}
//
// @004b0f48 -- base HeatSink flags plus 0x40 when this part has no usable
// voltage (the slot-0x40 virtual returns 0).
//
LWord
PoweredSubsystem::GetStatusFlags()
{
LWord flags = HeatSink::GetStatusFlags(); // FUN_004add30
if (!HasVoltage()) // virtual "has voltage" query, slot @vtable+0x40
{
flags |= 0x40;
}
return flags;
}
//
// Virtual "has usable voltage" query (vtable+0x40). Base powered subsystem
// reports voltage present whenever the electrical state machine is Ready.
//
Logical
PoweredSubsystem::HasVoltage()
{
return (electricalStateAlarm.GetLevel() == Ready) ? True : False;
}
//
// @004b1224 -- prints "<name> = <electrical state>".
//
void
PoweredSubsystem::PrintState()
{
HeatSink::PrintState(); // FUN_004ae050
switch (electricalStateAlarm.GetLevel()) // this+0x278
{
case Starting: DebugStream << GetName() << " = Starting" << endl; break;
case NoVoltage: DebugStream << GetName() << " = NoVoltage" << endl; break;
case Shorted: DebugStream << GetName() << " = Shorted" << endl; break;
case GeneratorOff: DebugStream << GetName() << " = GeneratorOff" << endl; break;
case Ready: DebugStream << GetName() << " = Ready" << endl; break;
default: DebugStream << GetName() << " Unknown Electrical State!" << endl; break;
}
}
//#############################################################################
// Voltage-source linkage helpers
//
//
// @004b0dd8 -- claim a tap on 'source' if it has a spare one, drop any current
// link, then bind the connection object. Returns the new tap index, -1 on
// failure (source already fully loaded).
//
int
PoweredSubsystem::AttachToVoltageSource(Subsystem *source)
{
int tap;
Generator *gen = (Generator *)source; // the voltage source is a Generator
if (gen->currentTapCount < gen->maxTapCount) // source+0x1e8 < source+0x1e4
{
gen->currentTapCount += 1;
tap = gen->currentTapCount;
}
else
{
tap = -1;
}
if (tap >= 0)
{
DetachFromVoltageSource(); // FUN_004b0e30
voltageSource.Add(source); // (**(this[0x74]+4))(this+0x74, source)
}
return tap;
}
//
// @004b0e30 -- release our tap on the current source and raise the
// "disconnected" mode alarm.
//
void
PoweredSubsystem::DetachFromVoltageSource()
{
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
if (source != 0)
{
source->currentTapCount -= 1; // source+0x1e8 -= 1
voltageSource.Clear(); // FUN_00417a5c(this+0x1d0)
}
modeAlarm.SetLevel(1); // FUN_0041bbd8(this+0x264, 1) TODO: confirm which alarm
}
//
// @004b0b5c -- True when the (resolved or supplied) source is in the shorted
// state (state 2 in this query) and its measured voltage exceeds the short
// threshold.
//
Logical
PoweredSubsystem::IsSourceShorted(Subsystem *source)
{
if (source == 0)
{
source = voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
}
Generator *gen = (Generator *)source;
if (
gen != 0
&& gen->GeneratorStateOf() == 2 // source+0x210 == 2
&& fabsf(gen->MeasuredVoltage() - VoltageEpsilon) > VoltageEpsilon // source+0x1dc
)
{
return True;
}
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- PoweredSubsystem (@004b13ac)
//
// Parses the power-specific resource fields on top of the HeatSink record.
//
int
PoweredSubsystem::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!HeatSink::CreateStreamedSubsystem( // FUN_004ae150
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = 0x190; // resource+0x24
subsystem_resource->classID = RegisteredClass::PoweredSubsystemClassID; // 0x0bc2, resource+0x20
if (passes == 1)
{
// first pass: prime fields to "unset"
subsystem_resource->voltageSourceIndex = -1;
subsystem_resource->thermalResistivityCoefficient = ResourceUnset; // -1.0f
subsystem_resource->auxScreenPlacement = -1;
memset(subsystem_resource->auxScreenLabel, 0, 64); // resource+0x10C
memset(subsystem_resource->engScreenLabel, 0, 64); // resource+0x14C
subsystem_resource->startTime = ResourceUnset;
}
//
// "VoltageSource" -- segment name; resolve to a segment index (biased +2).
//
const char *voltageSourceName = "Unspecified";
int found = model_file->GetEntry(subsystem_name, "VoltageSource", &voltageSourceName);
if (!found && subsystem_resource->voltageSourceIndex == -1)
{
DebugStream << subsystem_name << " missing VoltageSource!";
return False;
}
if (strcmp(voltageSourceName, "Unspecified") != 0)
{
subsystem_resource->voltageSourceIndex =
Get_Segment_Index(model_file, model_name, directories, voltageSourceName); // FUN_004215b0
}
if (subsystem_resource->voltageSourceIndex < 0)
{
DebugStream << subsystem_name << " has an invalid voltage source!";
return False;
}
if (strcmp(voltageSourceName, "Unspecified") != 0)
{
subsystem_resource->voltageSourceIndex += 2;
}
if (
!model_file->GetEntry(subsystem_name, "ThermalResistivityCoefficient",
&subsystem_resource->thermalResistivityCoefficient)
&& subsystem_resource->thermalResistivityCoefficient == ResourceUnset
)
{
DebugStream << subsystem_name << " missing ThermalResistivityCoefficient!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "AuxScreenPlacement",
&subsystem_resource->auxScreenPlacement)
&& subsystem_resource->auxScreenPlacement == -1
)
{
subsystem_resource->auxScreenPlacement = -1; // left unset (optional)
}
if (
!model_file->GetEntry(subsystem_name, "AuxScreenNumber",
&subsystem_resource->auxScreenNumber)
&& subsystem_resource->auxScreenNumber == -1
)
{
DebugStream << subsystem_name << " missing AuxScreenNumber!";
return False;
}
const char *label;
if (model_file->GetEntry(subsystem_name, "AuxScreenLabel", &label))
{
strcpy(subsystem_resource->auxScreenLabel, label);
}
else if (subsystem_resource->auxScreenLabel[0] == '\0')
{
DebugStream << subsystem_name << " missing AuxScreenLabel!";
return False;
}
if (model_file->GetEntry(subsystem_name, "EngScreenLabel", &label))
{
strcpy(subsystem_resource->engScreenLabel, label);
}
else if (subsystem_resource->engScreenLabel[0] == '\0')
{
DebugStream << subsystem_name << " missing EngScreenLabel!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "StartTime", &subsystem_resource->startTime)
&& subsystem_resource->startTime == ResourceUnset
)
{
DebugStream << subsystem_name << " missing StartTime!";
return False;
}
Check_Fpu();
return True;
}
//###########################################################################
//###########################################################################
// Myomers
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
Myomers::SharedData
Myomers::DefaultData(
Myomers::GetClassDerivations(),
Myomers::GetMessageHandlers(),
Myomers::GetAttributeIndex(),
Myomers::StateCount
);
Derivation*
Myomers::GetClassDerivations()
{
static Derivation classDerivations(PoweredSubsystem::GetClassDerivations(), "Myomers");
return &classDerivations;
}
//
// @004b1d18 -- master myomer bundle registers a separate Performance and a
// status-flag bit (0x8); copies skip the live simulation.
//
Myomers::Myomers(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
PoweredSubsystem(owner, subsystem_ID, subsystem_resource, /*shared_data=*/DefaultData) // FUN_004b0f74(..,&DAT_0050fa1c,0,0)
{
outputVoltage = 1.0f; // this[0xc7]
powered = 0; // this[0xc8]
voltageAvailable = 0; // this[0xc9]
simulationFlags |= 0x8; // this[0xa] |= 8
if ((subsystem_resource->subsystemFlags & SegmentCopyMask) != 4) // (flags & 0xC) != 4
{
SetPerformance((Performance)&Myomers::MyomersSimulation); // PTR @0050fa94
}
}
//
// @004b1d90
//
Myomers::~Myomers()
{
Check(this);
BT_ClearMyomers((Mech *)owner); // *(this[0x34]+0x374) = 0
Check_Fpu();
}
Logical Myomers::TestClass(Mech &) { return True; }
Logical Myomers::TestInstance() const // @004b1e18
{
return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(..,0x50fa2c)
}
//
// @004b1c4c -- compute the actuator drive voltage available to the locomotion
// model from the powering generator, throttled by heat damage and gated by
// the electrical state.
//
void
Myomers::MyomersSimulation(Scalar time_slice)
{
PoweredSubsystem::PoweredSubsystemSimulation(time_slice); // FUN_004b0bd0
Generator *source = (Generator *)voltageSource.Resolve(); // this[0x38] linked source
outputVoltage = RatedVoltageRef - source->MeasuredVoltage(); // 1.0 - *(source+0x158)
if (simulationState == 1) // this[0x10] @0x40
{
outputVoltage = 0.0f;
powered = 0;
}
else
{
powered = 1;
}
if (electricalStateAlarm.GetLevel() == Ready) // this[0x9e] == 4
{
voltageAvailable = 0;
}
else
{
voltageAvailable = 1;
outputVoltage = 0.0f;
}
//
// Heat damage (HeatSink heat-state @0x184) throttles the drive.
//
switch (heatState) // this[0x61] @0x184
{
case HeatSink::NormalHeat: // 0
powered = 1;
break;
case HeatSink::DegradationHeat: // 1
outputVoltage *= DegradedDriveScale; // *= 0.5
powered = 1;
break;
case HeatSink::FailureHeat: // 2
outputVoltage = 0.0f;
powered = 0;
break;
}
}
//
// @004b1c18 -- reset clears the myomer output state, then chains to the base.
//
void
Myomers::ResetToInitialState(Logical powered_flag)
{
if (powered_flag)
{
outputVoltage = 1.0f; // this[0xc7]
powered = 0; // this[0xc8]
voltageAvailable = 0; // this[0xc9]
}
PoweredSubsystem::ResetToInitialState(powered_flag); // FUN_004b0e6c
}
//
// @004b1bfc -- straight delegation to the base.
//
Logical
Myomers::HandleMessage(int message)
{
return PoweredSubsystem::HandleMessage(message); // FUN_004b0efc
}
//
// @004b1dcc -- no extra resource fields; just tags the record as Myomers.
//
int
Myomers::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!PoweredSubsystem::CreateStreamedSubsystem( // FUN_004b13ac
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = 0x190;
subsystem_resource->classID = RegisteredClass::MyomersClassID; // 0x0bc3, resource+0x20
return True;
}
//###########################################################################
//###########################################################################
// Generator
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support
//
Generator::SharedData
Generator::DefaultData(
Generator::GetClassDerivations(),
Generator::GetMessageHandlers(),
Generator::GetAttributeIndex(),
Generator::StateCount
);
Derivation*
Generator::GetClassDerivations()
{
static Derivation classDerivations(HeatSink::GetClassDerivations(), "Generator");
return &classDerivations;
}
//
// @004b225c -- a generator is itself a HeatSink (it gets hot producing power).
// It starts on-line at rated voltage; the trailing letter of the segment name
// ("GeneratorA".."GeneratorD") becomes the generator number.
//
Generator::Generator(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, /*shared_data=*/DefaultData), // FUN_004adda0(..,&DAT_0050fb50,0,0)
stateAlarm(5) // FUN_0041b9ec(this+0x7f, 5)
{
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // owner+0x28 (gate reconcile)
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0
)
{
SetPerformance(&Generator::GeneratorSimulation); // PTR @0050fc14
}
ratedVoltage = subsystem_resource->ratedVoltage; // resource+0xFC
outputVoltage = ratedVoltage;
maxTapCount = subsystem_resource->maxTapCount; // resource+0x100
currentTapCount = 0;
percentVoltageAvailable = 1.0f;
startTime = subsystem_resource->startTime; // resource+0x104
startTimer = startTime;
stateAlarm.SetLevel(GeneratorReady); // SetLevel 2
generatorOn = 1;
shortRecoveryTime = subsystem_resource->shortRecoveryTime; // resource+0x108
shortTimer = shortRecoveryTime;
simulationFlags |= 0x8; // this[0xa] |= 8
// last character of the segment name -> generator number ('A'==0x41 -> 1)
const char *name = GetName(); // this[0x35]
generatorNumber = name[strlen(name) - 1] - 0x40; // FUN_004d4a78
Check_Fpu();
}
//
// @004b237c
//
Generator::~Generator()
{
Check(this);
// stateAlarm (@0x7f) released here; FUN_004adfd4 finishes the HeatSink chain.
Check_Fpu();
}
Logical Generator::TestClass(Mech &) { return True; }
Logical Generator::TestInstance() const { return IsDerivedFrom(*GetClassDerivations()); }
//
// Generator vtable slot 10 -- recovered VERBATIM from the surviving GNRATOR.TCP
// fragment (the one Generator method the @0x4b02f0.. cluster decomp omitted; see
// gnrator.cpp). Chains the HeatableSubsystem reset (NOT the HeatSink one --
// matching the inlined base chain seen across the HeatSink family) and forces
// the generator output cold.
//
void
Generator::ResetToInitialState(Logical /*powered*/)
{
HeatableSubsystem::ResetToInitialState(True);
outputVoltage = 0.0f;
}
//
// @004b1f7c -- generator state machine: spin-up, short recovery, and the
// resulting percent-of-rated voltage made available to attached loads.
//
void
Generator::GeneratorSimulation(Scalar time_slice)
{
HeatSink::HeatSinkSimulation(time_slice); // FUN_004ad924
if (simulationState == 1) // this[0x10] @0x40
{
stateAlarm.SetLevel(GeneratorIdle); // SetLevel 1
}
if (heatState == HeatSink::FailureHeat) // this[0x61] @0x184 == 2
{
stateAlarm.SetLevel(GeneratorRecovered); // SetLevel 4
}
switch (stateAlarm.GetLevel()) // this[0x84] @0x210
{
case GeneratorStarting: // 0
outputVoltage = 0.0f;
startTimer += time_slice;
if (startTime <= startTimer)
{
stateAlarm.SetLevel(GeneratorReady); // 2
outputVoltage = (MaxOutputVoltage - SourceLevel()) * ratedVoltage; // (1.0 - *(this[0x38]+0x158)) * this[0x76]
}
break;
case GeneratorIdle: // 1
outputVoltage = 0.0f;
break;
case GeneratorShorted: // 3
outputVoltage = 0.0f;
shortTimer += time_slice;
if (shortRecoveryTime <= shortTimer)
{
stateAlarm.SetLevel(GeneratorReady); // 2
outputVoltage = (MaxOutputVoltage - SourceLevel()) * ratedVoltage;
}
break;
case GeneratorRecovered: // 4
outputVoltage = 0.0f;
if (heatState == HeatSink::NormalHeat) // this[0x61] == 0
{
if (generatorOn == 0) // this[0x75]
{
stateAlarm.SetLevel(GeneratorIdle); // 1
}
else
{
startTimer = 0.0f;
stateAlarm.SetLevel(GeneratorStarting); // 0
}
}
break;
}
//
// percent of rated voltage actually available, clamped to [0, 1].
//
percentVoltageAvailable = outputVoltage / ratedVoltage; // this[0x74]
if (percentVoltageAvailable < MinOutputVoltage)
{
percentVoltageAvailable = MinOutputVoltage; // 0.0
}
else if (percentVoltageAvailable > MaxOutputVoltage)
{
percentVoltageAvailable = MaxOutputVoltage; // 1.0
}
}
//
// @004b21d0 -- when the mech's electrical bus is live: message 4 with the
// generator damaged/off drives it into the shorted-recovery state; any other
// message recomputes the output from the source level.
//
Logical
Generator::HandleMessage(int message)
{
HeatSink::HandleMessage(message); // FUN_004ad748
Mech *mech = (Mech *)owner;
if (BT_IsBusLive(mech)) // *(*(this[0x34]+0x190)+0x274) != 0
{
if (message == 4)
{
if ((simulationState == 2 || simulationState == 0) && !IsDamaged()) // this[0x10]; FUN_004ac9c8
{
stateAlarm.SetLevel(GeneratorShorted); // 3
shortTimer = 0.0f; // this[0x7e]
}
}
else
{
outputVoltage = (MaxOutputVoltage - SourceLevel()) * ratedVoltage;
}
}
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- Generator (@004b23bc)
//
int
Generator::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!HeatSink::CreateStreamedSubsystem( // FUN_004ae150
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = 0x10c;
subsystem_resource->classID = RegisteredClass::GeneratorClassID; // 0x0bc1, resource+0x20
if (passes == 1)
{
subsystem_resource->ratedVoltage = ResourceUnset; // -1.0f
subsystem_resource->startTime = ResourceUnset;
subsystem_resource->shortRecoveryTime = ResourceUnset;
}
if (
!model_file->GetEntry(subsystem_name, "RatedVoltage", &subsystem_resource->ratedVoltage)
&& subsystem_resource->ratedVoltage == ResourceUnset
)
{
DebugStream << subsystem_name << " missing RatedVoltage!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "MaxTapCount", &subsystem_resource->maxTapCount)
&& subsystem_resource->maxTapCount == -1
)
{
DebugStream << subsystem_name << " missing MaxTapCount!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "StartTime", &subsystem_resource->startTime)
&& subsystem_resource->startTime == ResourceUnset
)
{
DebugStream << subsystem_name << " missing StartTime!";
return False;
}
if (
!model_file->GetEntry(subsystem_name, "ShortRecoveryTime", &subsystem_resource->shortRecoveryTime)
&& subsystem_resource->shortRecoveryTime == ResourceUnset
)
{
DebugStream << subsystem_name << " missing ShortRecoveryTime!";
return False;
}
Check_Fpu();
return True;
}
//###########################################################################
//###########################################################################
// PowerWatcher (BEST-EFFORT)
//###########################################################################
//###########################################################################
//
// NOTE: PowerWatcher's base class lies just below the captured decomp window
// (base ctor @004aeb40, base streamer @004aec54, base message/sim @004aea84 /
// @004aea9c). Only the power-specific override bodies were recovered; the
// base relationship is reconstructed and flagged best-effort.
//
PowerWatcher::SharedData
PowerWatcher::DefaultData(
PowerWatcher::GetClassDerivations(),
PowerWatcher::GetMessageHandlers(),
PowerWatcher::GetAttributeIndex(),
PowerWatcher::StateCount
);
Derivation*
PowerWatcher::GetClassDerivations()
{
// Real base ctor is @004aeb40 (HeatWatcher); HeatableSubsystem stands in.
static Derivation classDerivations(HeatableSubsystem::GetClassDerivations(), "PowerWatcher");
return &classDerivations;
}
//
// @004b18a4 -- threshold = scale * resource->minVoltagePercent.
//
PowerWatcher::PowerWatcher(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatWatcher(owner, subsystem_ID, subsystem_resource, shared_data), // FUN_004aeb40 -- HeatWatcher base
watchdogAlarm(5) // FUN_0041b9ec(this+0x61, 5)
{
minVoltage = MinVoltageScale * subsystem_resource->minVoltagePercent; // _DAT_004b1924 * (resource+0xf0)
if (
(owner->simulationFlags & SegmentCopyMask) == 0 // owner+0x28 (gate reconcile)
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0
)
{
SetPerformance(&PowerWatcher::Simulation); // PTR @0050f5fc
}
}
//
// @004b1930
//
PowerWatcher::~PowerWatcher()
{
Check(this);
Check_Fpu();
}
Logical PowerWatcher::TestClass(Mech &) { return True; }
Logical PowerWatcher::TestInstance() const { return IsDerivedFrom(*GetClassDerivations()); } // @004b1970
//
// @004b179c -- on message 4, if the watched source's source is shorted (and
// alive), force the short-recovery alarm; then chain to the base handler.
//
Logical
PowerWatcher::HandleMessage(int message)
{
if (message == 4)
{
Subsystem *watched = watchedLink.Resolve(); // FUN_00417ab4(this+0x45)
Generator *source = (watched != 0) // FUN_00417ab4(watched+0x1d0)
? (Generator *)((PoweredSubsystem *)watched)->ResolveVoltageSource()
: 0;
if (source != 0 && !source->IsDamaged())
{
source->stateAlarm.SetLevel(Generator::GeneratorShorted); // source+0x1fc -> 3
source->outputVoltage = 0.0f; // source+0x1f8 = 0
}
}
return MechSubsystem::HandleMessage(message); // FUN_004aea84 -> FUN_004ac0bc = MechSubsystem::HandleMessage
}
//
// @004b1804 -- delegate to the base simulation.
//
void
PowerWatcher::Simulation(Scalar time_slice)
{
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c = HeatWatcher::ResetToInitialState
(void)time_slice;
}
//
// @004b198c -- reads "MinVoltagePercent".
//
int
PowerWatcher::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!HeatWatcher::CreateStreamedSubsystem( // FUN_004aec54 -- HeatWatcher parse
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories, passes
)
)
{
return False;
}
subsystem_resource->subsystemModelSize = 0xf4;
subsystem_resource->classID = RegisteredClass::PowerWatcherClassID; // resource+0x20
if (passes == 1)
{
subsystem_resource->minVoltagePercent = ResourceUnset; // -1.0f
}
if (
!model_file->GetEntry(subsystem_name, "MinVoltagePercent",
&subsystem_resource->minVoltagePercent)
&& subsystem_resource->minVoltagePercent == ResourceUnset
)
{
DebugStream << subsystem_name << " missing MinVoltagePercent!";
return False;
}
return True;
}
//###########################################################################
//###########################################################################
// VoltageSourceConnection
//###########################################################################
//###########################################################################
//
// Small SharedData specialisation that links a PoweredSubsystem to its
// Generator segment (vtable @0050f994).
//
//
// @004b1bb1
//
VoltageSourceConnection::VoltageSourceConnection(int initial):
SubsystemConnection(initial) // FUN_004179d4
{
// *this = &PTR_FUN_0050f994
}
//
// @004b1bd0
//
VoltageSourceConnection::~VoltageSourceConnection()
{
// *this = &PTR_FUN_0050f994; FUN_004179f8(this, 0)
}
//===========================================================================//
// WAVE 3a factory bridges -- the power bus (Generator 0xBC1, PoweredSubsystem
// 0xBC2). Energy weapons resolve a real VoltageSource off these to charge.
//===========================================================================//
Subsystem *CreateGeneratorSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(Generator) <= 0x250);
return (Subsystem *) new (Memory::Allocate(0x250))
Generator(owner, id, (Generator::SubsystemResource *)seg, Generator::DefaultData);
}
Subsystem *CreatePoweredSubsystem(Mech *owner, int id, void *seg)
{
Check(sizeof(PoweredSubsystem) <= 0x31c);
return (Subsystem *) new (Memory::Allocate(0x31c))
PoweredSubsystem(owner, id, (PoweredSubsystem::SubsystemResource *)seg, PoweredSubsystem::DefaultData);
}
//
// Bridge for the vehicleSubSystems gauge factory (btl4gau2.cpp). The engineering-
// screen assignment lives on PoweredSubsystem (auxScreenNumber / auxScreenPlacement
// / auxScreenLabel, resource +0x104/+0x108/+0x10C). Reading it from btl4gau2.cpp
// via a raw offset (sub+0x1dc) is wrong because the reconstructed heat-leaf branch
// is not byte-exact -- so the gauge factory calls this bridge, which casts through
// the real PoweredSubsystem type and returns the NAMED fields. Returns False for
// non-PoweredSubsystem-derived subsystems (== the FUN_0041a1a4 / 0x50f4bc type
// filter the vehicleSubSystems Make applies before dispatch).
//
bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64)
{
if (sub == NULL)
return false;
if (!sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations())) // FUN_0041a1a4(...,0x50f4bc)
return false;
PoweredSubsystem *ps = (PoweredSubsystem *)sub;
if (screen != NULL) *screen = ps->auxScreenNumber; // +0x104
if (placement != NULL) *placement = ps->auxScreenPlacement; // +0x108
if (label64 != NULL) strcpy(label64, ps->auxScreenLabel); // +0x10C
return true;
}