The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1450 lines
48 KiB
C++
1450 lines
48 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 BTPlayerRoleLocksAdvanced() (owner mech+0x190 player ->
|
|
// roleClassIndex+0x274 == 0: the ROOKIE role locks the
|
|
// advanced cockpit systems -- NOT "IsDamaged"; task #12)
|
|
// 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 = 0.01f; // _DAT_004b1924 -- a 10-byte x87 literal
|
|
// (0a d7 a3 70 3d 0a d7 a3 f8 3f = 0.01,
|
|
// byte-read from the exe, task #57): converts
|
|
// the authored MinVoltagePercent to a fraction.
|
|
// Was 1.0f, which made minVoltage 100x too big
|
|
// and latched the watchdog brownout (level 1).
|
|
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// PoweredSubsystem
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Attribute Support (gauge data-binding wave)
|
|
//
|
|
// InputVoltage -> voltageSource (the 0xC connection to the powering Generator;
|
|
// the cluster's power-branch gate + the source of GeneratorVoltageConnection).
|
|
// Chained to HeatSink's dense index; a function-local static so the parent
|
|
// index is safely constructed on first use regardless of static-init order.
|
|
//
|
|
const PoweredSubsystem::IndexEntry
|
|
PoweredSubsystem::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(PoweredSubsystem, InputVoltage, voltageSource) // @0x1D0
|
|
};
|
|
|
|
PoweredSubsystem::AttributeIndexSet&
|
|
PoweredSubsystem::GetAttributeIndex()
|
|
{
|
|
static PoweredSubsystem::AttributeIndexSet attributeIndex(
|
|
ELEMENTS(PoweredSubsystem::AttributePointers),
|
|
PoweredSubsystem::AttributePointers,
|
|
HeatSink::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
// task #12 -- the binary handler table @0x50F4EC (section_dump: ids 4-8,
|
|
// names "SelectGeneratorA".."ToggleGeneratorMode"). Chained onto the engine
|
|
// Receiver root set.
|
|
//
|
|
// STATIC-INIT ORDER (the silent-drop bug): the table lives INSIDE the accessor
|
|
// as a function-local static. As a namespace-scope array it was read by
|
|
// Emitter::DefaultData's cross-TU static-init chain BEFORE this TU's dynamic
|
|
// initializers ran -- the Build saw zeros, so weapons carried ids 9/10 but the
|
|
// 4-8 slots stayed empty (observed: [gensel-tx] logged, handler never ran).
|
|
// Function-local statics initialize on first call -- order-proof; the same
|
|
// idiom the T0 engine uses (APP.cpp GetMessageHandlers).
|
|
Receiver::MessageHandlerSet&
|
|
PoweredSubsystem::GetMessageHandlers()
|
|
{
|
|
static const Receiver::HandlerEntry entries[]=
|
|
{
|
|
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorA), // id 4 @004b099c
|
|
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorB), // id 5 @004b09e4
|
|
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorC), // id 6 @004b0a2c
|
|
MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorD), // id 7 @004b0a74
|
|
MESSAGE_ENTRY(PoweredSubsystem, ToggleGeneratorMode) // id 8 @004b0abc
|
|
};
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(entries), entries,
|
|
Receiver::GetMessageHandlers()
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
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
|
|
)
|
|
{
|
|
// task #12 ROSTER CORRECTION: the raw @004b0bd0 loop walks owner+0x124/
|
|
// +0x128 -- the SUBSYSTEM ROSTER, not the skeleton segment table (the
|
|
// same +0x128 gotcha as the heat/voltage index resolves). The old
|
|
// GetSegment() read cast EntitySegments to Subsystems.
|
|
Mech *mech = (Mech *)owner;
|
|
int count = mech->GetSubsystemCount(); // owner+0x124
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *sub = mech->GetSubsystem(i); // owner+0x128[i]
|
|
if (sub != 0
|
|
&& sub->GetClassID() == RegisteredClass::GeneratorClassID // +4 == 0xbc1
|
|
&& GetStatusFlags() != 0
|
|
&& AttachToVoltageSource(sub) != -1 // FUN_004b0dd8
|
|
)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
//
|
|
// @004b0d50 -- the charge time-scale (task #10, the heat/firepower feedback):
|
|
// rise = max(0, srcTemperature - srcStartingTemperature) (src+0x114 - src+0x13C)
|
|
// scale = max(voltageScale,
|
|
// (thermalResistivityCoefficient * rise + 1.0) * voltageScale)
|
|
// (_DAT_004b0dd0 = 0.0f, _DAT_004b0dd4 = 1.0f, both byte-verified.) The binary
|
|
// dereferences the resolved source UNGUARDED (the master ctor Verify guarantees
|
|
// the link); the port falls back to the unstretched voltageScale instead.
|
|
//
|
|
Scalar
|
|
PoweredSubsystem::ChargeTimeScale()
|
|
{
|
|
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1D0)
|
|
|
|
Scalar rise = 0.0f;
|
|
if (source != 0)
|
|
{
|
|
rise = source->currentTemperature // src+0x114
|
|
- source->startingTemperature; // src+0x13C
|
|
}
|
|
if (rise < 0.0f) // _DAT_004b0dd0
|
|
{
|
|
rise = 0.0f;
|
|
}
|
|
|
|
Scalar scale = (thermalResistivityCoefficient * rise + 1.0f) // this+0x30C, _DAT_004b0dd4
|
|
* voltageScale; // this+0x310
|
|
if (scale < voltageScale)
|
|
{
|
|
scale = voltageScale;
|
|
}
|
|
return scale;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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
|
|
|
|
// Raw @004b0efc reads *(int*)(*(owner+0x190) + 0x25c) -- the message-manager's
|
|
// short-event flag (NOT a Generator field). The manager is null in bring-up
|
|
// (BT_GetMessageManager stub), so guard the deref; the real read resumes once
|
|
// the SubsystemMessageManager (0xBD3) roster lookup is wired.
|
|
SubsystemMessageManager *msgMgr = BT_GetMessageManager((Mech *)owner); // *(this[0x34]+0x190)
|
|
|
|
if (!IsDamaged() // FUN_004ac9c8 == 0
|
|
&& msgMgr != 0
|
|
&& *(LWord *)((char *)msgMgr + 0x25c) != 0 // message-manager short-event flag
|
|
&& 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;
|
|
}
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// The cockpit POWER-ROUTING message handlers (task #12)
|
|
//
|
|
// @004b099c/@004b09e4/@004b0a2c/@004b0a74 -- one per generator; identical
|
|
// bodies (press only, undamaged only): find Generator N on the owner's
|
|
// SUBSYSTEM ROSTER, re-tap onto it, and drop the connect mode back to
|
|
// Connected (manual selection ends any auto-hunt).
|
|
//
|
|
void
|
|
PoweredSubsystem::SelectGenerator(int generator_number)
|
|
{
|
|
Subsystem *gen = FindGeneratorByNumber(generator_number); // FUN_004b0b18
|
|
if (gen == 0)
|
|
{
|
|
// the binary dereferences the result unguarded (authored mechs always
|
|
// carry generators A-D); fail loud instead of crashing.
|
|
Verify(False, "SelectGenerator: no such generator", __FILE__, __LINE__);
|
|
return;
|
|
}
|
|
int tap = AttachToVoltageSource(gen); // FUN_004b0dd8
|
|
modeAlarm.SetLevel(Connected); // FUN_0041bbd8(this+0x2b8, 1)
|
|
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
|
DEBUG_STREAM << "[gensel] " << GetName() << " -> " << gen->GetName()
|
|
<< (tap >= 0 ? " (tapped)" : " (REFUSED: no spare tap)") << std::endl;
|
|
}
|
|
|
|
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
|
|
|
|
void
|
|
PoweredSubsystem::SelectGeneratorAMessageHandler(ReceiverDataMessageOf<int> *message)
|
|
{
|
|
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0) // FUN_004ac9c8 == 0 && press
|
|
SelectGenerator(1);
|
|
}
|
|
void
|
|
PoweredSubsystem::SelectGeneratorBMessageHandler(ReceiverDataMessageOf<int> *message)
|
|
{
|
|
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0)
|
|
SelectGenerator(2);
|
|
}
|
|
void
|
|
PoweredSubsystem::SelectGeneratorCMessageHandler(ReceiverDataMessageOf<int> *message)
|
|
{
|
|
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0)
|
|
SelectGenerator(3);
|
|
}
|
|
void
|
|
PoweredSubsystem::SelectGeneratorDMessageHandler(ReceiverDataMessageOf<int> *message)
|
|
{
|
|
if (getenv("BT_FIRE_LOG"))
|
|
DEBUG_STREAM << "[gensel-rx] D on " << GetName()
|
|
<< " locked=" << BTPlayerRoleLocksAdvanced(owner)
|
|
<< " v=" << message->dataContents << std::endl;
|
|
if (!BTPlayerRoleLocksAdvanced(owner) && message->dataContents > 0)
|
|
SelectGenerator(4);
|
|
}
|
|
|
|
// diag (task #12): registration probe -- is the handler live in the set?
|
|
// (id-based Find only: the NAME Find strcmp-walks every slot including the
|
|
// uninitialized id-3 GAP slot between the root set and ours -- garbage
|
|
// entryName pointer -> AV. The dense-table gap is engine T0 behavior; the
|
|
// 1995 binary's own table has the same hole.)
|
|
void BTGenSelProbe()
|
|
{
|
|
Receiver::MessageHandlerSet &set = PoweredSubsystem::GetMessageHandlers();
|
|
Receiver::Handler byId = set.Find((Receiver::MessageID)7);
|
|
DEBUG_STREAM << "[gensel-reg] byId=" << (int)(byId != Receiver::NullHandler)
|
|
<< std::endl;
|
|
}
|
|
|
|
//
|
|
// @004b0abc -- Manual/Auto connect-mode toggle (press only, undamaged only):
|
|
// below AutoConnect -> arm the auto-hunt; at AutoConnect -> drop the current
|
|
// tap and go fully Manual (the pilot takes over routing).
|
|
//
|
|
void
|
|
PoweredSubsystem::ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf<int> *message)
|
|
{
|
|
if (BTPlayerRoleLocksAdvanced(owner) || message->dataContents <= 0)
|
|
return;
|
|
if ((unsigned)modeAlarm.GetLevel() < (unsigned)AutoConnect) // this+0x2cc < 2
|
|
{
|
|
modeAlarm.SetLevel(AutoConnect); // 2
|
|
}
|
|
else if (modeAlarm.GetLevel() == AutoConnect)
|
|
{
|
|
DetachFromVoltageSource(); // FUN_004b0e30
|
|
modeAlarm.SetLevel(ManualConnect); // 0
|
|
}
|
|
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
|
DEBUG_STREAM << "[gensel] " << GetName() << " mode -> "
|
|
<< modeAlarm.GetLevel() << std::endl;
|
|
}
|
|
|
|
//
|
|
// @004b0b18 -- walk the owner's SUBSYSTEM ROSTER (mech+0x128, count +0x124)
|
|
// for the Generator segment whose generatorNumber (+0x1E0, from the name
|
|
// suffix 'A'..'D') matches. NULL when absent.
|
|
//
|
|
Subsystem *
|
|
PoweredSubsystem::FindGeneratorByNumber(int generator_number)
|
|
{
|
|
Mech *mech = (Mech *)owner; // this+0xD0
|
|
int count = mech->GetSubsystemCount(); // +0x124
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
Subsystem *sub = mech->GetSubsystem(i); // +0x128[i]
|
|
if (sub != 0
|
|
&& sub->GetClassID() == RegisteredClass::GeneratorClassID // +4 == 0xbc1
|
|
&& ((Generator *)sub)->generatorNumber == generator_number) // +0x1E0
|
|
{
|
|
return sub;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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)
|
|
}
|
|
electricalStateAlarm.SetLevel(NoVoltage); // FUN_0041bbd8(this+0x264, 1) -- 0x264 IS electricalStateAlarm (was wrongly modeAlarm@0x2B8)
|
|
}
|
|
|
|
//
|
|
// @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 mislabel -- RETIRED, task #14)
|
|
//###########################################################################
|
|
//###########################################################################
|
|
//
|
|
// The "Myomers" block that lived here (ctor @4b1d18, classID 0xBC3,
|
|
// outputVoltage/powered/voltageAvailable) was a MISLABEL of Sensor (real
|
|
// reconstruction: sensor.cpp). Its Myomers::DefaultData / ctor / dtor /
|
|
// TestInstance / MyomersSimulation / ResetToInitialState / HandleMessage /
|
|
// CreateStreamedSubsystem definitions ODR-DUPLICATED the REAL Myomers
|
|
// (myomers.cpp, classID 0xBC6) -- /FORCE picked the winner by link order,
|
|
// so which SharedData live Myomers instances got was link-order-dependent
|
|
// (the same archetype as the fixed Condenser stub dedup). Retired whole.
|
|
//
|
|
|
|
//###########################################################################
|
|
//###########################################################################
|
|
// Generator
|
|
//###########################################################################
|
|
//###########################################################################
|
|
|
|
//#############################################################################
|
|
// Attribute Support (gauge data-binding wave)
|
|
//
|
|
// OutputVoltage -> outputVoltage@0x1DC (== MeasuredVoltage(); GeneratorSimulation
|
|
// drives it). Chained to HeatSink's dense index so the temps/coolant the
|
|
// GeneratorCluster also reads stay reachable. DefaultData below already calls
|
|
// GetAttributeIndex() (was resolving to the inherited HeatSink one).
|
|
//
|
|
const Generator::IndexEntry
|
|
Generator::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage) // @0x1DC
|
|
};
|
|
|
|
Generator::AttributeIndexSet&
|
|
Generator::GetAttributeIndex()
|
|
{
|
|
static Generator::AttributeIndexSet attributeIndex(
|
|
ELEMENTS(Generator::AttributePointers),
|
|
Generator::AttributePointers,
|
|
HeatSink::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
//#############################################################################
|
|
// 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 (heatAlarm.GetLevel() == HeatSink::FailureHeat) // this+0x184 == 2
|
|
{
|
|
// The THERMAL BREAKER: output collapses to 0 until the generator cools
|
|
// past NormalHeat and re-spins (task #11 diag below shows the duty cycle).
|
|
if (stateAlarm.GetLevel() != GeneratorRecovered && getenv("BT_HEAT_LOG"))
|
|
DEBUG_STREAM << "[gen] " << GetName() << " TRIPPED (T="
|
|
<< currentTemperature << ")" << std::endl;
|
|
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 (heatAlarm.GetLevel() == HeatSink::NormalHeat) // this+0x184 == 0
|
|
{
|
|
if (generatorOn == 0) // this[0x75]
|
|
{
|
|
stateAlarm.SetLevel(GeneratorIdle); // 1
|
|
}
|
|
else
|
|
{
|
|
if (getenv("BT_HEAT_LOG"))
|
|
DEBUG_STREAM << "[gen] " << GetName() << " RESTARTING (T="
|
|
<< currentTemperature << ")" << std::endl;
|
|
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()
|
|
{
|
|
// Chains to the REAL base HeatWatcher (@004aeb40) -- task #57: the old
|
|
// HeatableSubsystem stand-in predated the base-chain re-base and broke
|
|
// IsDerivedFrom(HeatWatcher) for every PowerWatcher descendant, which
|
|
// silently skipped the Torso/Searchlight/ThermalSight in the factory
|
|
// watcher-CONNECT pass (their watchdogs stayed at 0 = never Ready).
|
|
static Derivation classDerivations(HeatWatcher::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 (slot 10) -- ResetToInitialState: chain to the HeatWatcher base.
|
|
// (This body was previously MISLABELED as the Simulation. The vtable
|
|
// @0050f99c slot 10 (offset 0x28) is 004b1804, exactly where every other
|
|
// subsystem carries ResetToInitialState; the registered Performance PTR
|
|
// @0050f5fc points at 004b181c -- UpdateWatch below.)
|
|
//
|
|
void
|
|
PowerWatcher::ResetToInitialState(Logical /*powered*/)
|
|
{
|
|
HeatWatcher::ResetToInitialState(True); // FUN_004aea9c
|
|
}
|
|
|
|
//
|
|
// @004b181c -- the per-tick watch update: THE registered Performance
|
|
// (PTR @0050f5fc -> 004b181c), and called directly by the Torso master/copy
|
|
// simulations (@004b5cf0/@004b65f8 first line). Ghidra missed the function
|
|
// start; recovered from the raw pseudocode at part_013.c:1645:
|
|
//
|
|
// FUN_004aeac4(this); // heat mirror
|
|
// watched = watchedLink.Resolve(); // +0x114
|
|
// gen = watched->voltageSource.Resolve(); // +0x1D0
|
|
// watchdogAlarm.SetLevel(watched->elecLevel); // mirror +0x278
|
|
// if (elecLevel == 4 && gen && gen->outputVoltage(+0x1DC)
|
|
// <= minVoltage(+0x180) * gen->ratedVoltage(+0x1D8))
|
|
// watchdogAlarm.SetLevel(1); // brownout
|
|
//
|
|
// The watchdog MIRRORS the watched subsystem's electrical state -- this is
|
|
// what drives ElectricalStateLevel()==Ready on the Torso (the twist-rate
|
|
// power gate) and WatchedVoltageLevel on Searchlight/ThermalSight.
|
|
//
|
|
void
|
|
PowerWatcher::UpdateWatch()
|
|
{
|
|
WatchSimulation(0.0f); // FUN_004aeac4 -- heat-alarm mirror
|
|
|
|
PoweredSubsystem *watched = (PoweredSubsystem *)watchedLink.Resolve(); // FUN_00417ab4(this+0x114)
|
|
if (watched == 0)
|
|
{
|
|
// The binary derefs unconditionally (the link always binds on the
|
|
// master node). Null only on replicants / bring-up: hold Not-Ready.
|
|
watchdogAlarm.SetLevel(0);
|
|
return;
|
|
}
|
|
|
|
int level = watched->electricalStateAlarm.GetLevel(); // *(watched+0x278)
|
|
watchdogAlarm.SetLevel(level); // FUN_0041bbd8(this+0x184, level)
|
|
|
|
Generator *source = (Generator *)watched->ResolveVoltageSource(); // FUN_00417ab4(watched+0x1d0)
|
|
if (level == PoweredSubsystem::Ready && source != 0
|
|
&& source->MeasuredVoltage() <= minVoltage * source->RatedVoltageOf())
|
|
{
|
|
watchdogAlarm.SetLevel(1); // brownout: Ready but sagging source
|
|
}
|
|
}
|
|
|
|
//
|
|
// The Performance wrapper (PTR @0050f5fc -> @004b181c takes no dt).
|
|
//
|
|
void
|
|
PowerWatcher::Simulation(Scalar /*time_slice*/)
|
|
{
|
|
UpdateWatch();
|
|
}
|
|
|
|
//
|
|
// @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 raw offsets on our compiled
|
|
// objects are the databinding trap (2026-07-12: the "heat-leaf not byte-exact"
|
|
// wording is stale -- the leaf IS byte-exact since the sensor re-base,
|
|
// sensor.cpp:407-413 -- but the bridge stays: named members are the rule)
|
|
// -- 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).
|
|
//
|
|
// FUN_0041a1a4(sub->classDerivations, 0x50f4bc) -- is the subsystem PoweredSubsystem-
|
|
// derived? (The vehicleSubSystems type filter + the generator stateLamp gate.)
|
|
bool BTIsPoweredSubsystem(Subsystem *sub)
|
|
{
|
|
return sub != NULL && sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations());
|
|
}
|
|
|
|
bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64)
|
|
{
|
|
if (!BTIsPoweredSubsystem(sub)) // 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;
|
|
}
|