TWO missing pieces, both now in place.
1. Generator had NO handler set of its own, so every message aimed at it hit a
default-constructed blackhole. Proved empirically with the new
unhandled-message trace by pressing button 0x1A:
[msg] UNHANDLED: Generator has no handler for message id 4 -- silently dropped
Added Generator::GetMessageHandlers() chained to **HeatSink**, deliberately NOT
PoweredSubsystem: id 4 there is SelectGeneratorA (the weapon-side generator
SELECTION that already works), so chaining to it would have made these four
buttons invoke the WRONG function instead of doing nothing. HeatSink owns id 3
(ToggleCooling), leaving id 4 free.
2. ToggleGeneratorOnOff had no body at all (0 references in game/). Transcribed
instruction-for-instruction from @004b1ed0:
- call @004ac9c8 -> BTPlayerRoleLocksAdvanced: a NOVICE pilot cannot use it
- [msg+0xc] <= 0 -> press only, ignore the release
- ON : startTimer = 0; if heatAlarm(@0x184) < 1 also stateAlarm -> 0
(Starting, so it spins up -- a heat-faulted generator keeps its fault
level); generatorOn = 1, coolantAvailable = 1, coolantFlowScale = 1.0
- OFF : stateAlarm -> 1 (GeneratorIdle); generatorOn = 0,
coolantAvailable = 0, coolantFlowScale = 0 (its coolant share is
released back to the loop)
So taking a generator off line idles it, drops its coolant draw and stops it
feeding its taps -- the authentic heat-management trade from the manual.
VERIFIED LIVE (BT_BTNTEST=0x1A,400,900 through the real click seam):
[btntest] PRESS 0x1a at poll 400
[gensel] GeneratorA generator OFF LINE (stateAlarm 1, coolantFlowScale 0)
and the UNHANDLED warning for Generator is gone.
Answers Cyd's report from two playtests ('still cannot turn of gennies'). Note
this is the ON/OFF control -- generator SELECTION (which generator a subsystem
draws from) is a different message and already worked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
1716 lines
60 KiB
C++
1716 lines
60 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
|
|
};
|
|
// Chain through HeatSink (id 3 ToggleCooling) so a powered subsystem's
|
|
// dispatch -- weapons/avionics -- reaches the Eng-page "Coolant" button. The
|
|
// old chain went straight to the Receiver root, SKIPPING HeatSink/
|
|
// HeatableSubsystem, which is why msg 3 was unreachable (decomp-reference.md
|
|
// "ToggleCooling ... UNWIRED cause"). HeatSink then chains to the root.
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(entries), entries,
|
|
HeatSink::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::DeathReset(int /*reset_command*/)
|
|
{
|
|
ResetToInitialState(True); // @004b0e6c
|
|
}
|
|
|
|
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.
|
|
//
|
|
extern int BTPlayerExperienceSimLive(void *owner_mech); // btplayer.cpp (player+0x25c; issue #2)
|
|
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8: player+0x274 == 0)
|
|
|
|
Logical
|
|
PoweredSubsystem::HandleMessage(int message)
|
|
{
|
|
HeatSink::HandleMessage(message); // FUN_004ad748
|
|
|
|
// CORRECTED (issue #2 [T1]): @004b0efc reads *(int*)(*(owner+0x190)+0x25c)
|
|
// -- owner mech+0x190 is the PLAYER LINK, and +0x25c is the BTPlayer's
|
|
// "sim live" EXPERIENCE flag (0 only for novice), NOT a message-manager
|
|
// field (the old misattribution left this path dead behind the null
|
|
// BT_GetMessageManager stub). The leading FUN_004ac9c8==0 term is the
|
|
// not-novice predicate (BTPlayerRoleLocksAdvanced), not a damage query.
|
|
if (!BTPlayerRoleLocksAdvanced(owner) // FUN_004ac9c8 == 0 (not novice)
|
|
&& BTPlayerExperienceSimLive(owner) != 0 // *(player+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
|
|
{
|
|
// CORRECTED (issue #2 [T1]): both FUN_004ac9c8 calls here are the
|
|
// not-novice EXPERIENCE predicate on the owning player (each subsystem
|
|
// hops sub->owner mech->playerLink; this part and its source share the
|
|
// same mech, hence the same player) -- NOT damage-state queries (the old
|
|
// HeatSink::IsDamaged stand-in read simulationState).
|
|
if (BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8(this)
|
|
return;
|
|
Generator *source = (Generator *)voltageSource.Resolve(); // FUN_00417ab4(this+0x1d0)
|
|
if (source != 0 && !BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8(source): same mech -> same player
|
|
{
|
|
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;
|
|
}
|
|
|
|
|
|
//
|
|
// BTPowerSourceFrame -- complete-type bridge for the AnimatedSourceLamp
|
|
// (btpbus/btebus generator-letter lamp). The binary's PowerSourceConnection
|
|
// sampler (@004c3258) resolves the subsystem's InputVoltage link and reads
|
|
// resolved+0x1E0 == Generator::generatorNumber (1..4 -> btpbus frames A..D; 0
|
|
// == OFF).
|
|
//
|
|
// The port cannot reproduce the binary's path -- it walked
|
|
// ResolveLink( AttributePointerOf(subsystem,"InputVoltage") )
|
|
// but AttributePointerOf now returns the WRONG slot: the BT_DEV_GAUGES audio
|
|
// attribute rows shifted the chained attribute ids, so the name->pointer lookup
|
|
// no longer lands on voltageSource@0x1D0 (the same regression that broke the
|
|
// CoolingLoop lamp -- see btl4gau2.cpp). The link therefore always resolved to
|
|
// 0 and every generator lamp drew frame 0 ("OFF") even though the master
|
|
// PoweredSubsystem ctor DID bind voltageSource to its Generator.
|
|
//
|
|
// This bridge takes the subsystem directly and reads the NAMED member via
|
|
// PoweredSubsystem::ResolveVoltageSource() (voltageSource.Resolve()), bypassing
|
|
// the attribute table entirely -- exactly the BTCoolingLoopFrame pattern.
|
|
//
|
|
int BTPowerSourceFrame(void *subsystem_v)
|
|
{
|
|
static int s_log = -1;
|
|
if (s_log < 0) s_log = getenv("BT_LOOP_LOG") ? 1 : 0;
|
|
static int s_n = 0;
|
|
int trace = s_log && (s_n++ < 40);
|
|
|
|
Subsystem *sub = (Subsystem *)subsystem_v;
|
|
if (sub == 0 || !sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations()))
|
|
{
|
|
if (trace) DEBUG_STREAM << "[busfeed] " << (sub ? sub->GetName() : "(null)")
|
|
<< " -> 0 (not a PoweredSubsystem)\n" << std::flush;
|
|
return 0;
|
|
}
|
|
Generator *gen = (Generator *)((PoweredSubsystem *)sub)->ResolveVoltageSource();
|
|
if (gen == 0)
|
|
{
|
|
if (trace) DEBUG_STREAM << "[busfeed] " << sub->GetName()
|
|
<< " -> 0 (voltageSource unbound)\n" << std::flush;
|
|
return 0;
|
|
}
|
|
if (gen->GetClassID() != RegisteredClass::GeneratorClassID)
|
|
{
|
|
if (trace) DEBUG_STREAM << "[busfeed] " << sub->GetName()
|
|
<< " -> 0 (source not a Generator)\n" << std::flush;
|
|
return 0;
|
|
}
|
|
int frame = gen->generatorNumber; // @0x1E0 (A..D)
|
|
if (trace) DEBUG_STREAM << "[busfeed] " << sub->GetName() << " -> frame " << frame
|
|
<< " (generator '" << gen->GetName() << "')\n" << std::flush;
|
|
return frame;
|
|
}
|
|
|
|
//
|
|
// BTGeneratorVoltage -- complete-type bridge for the GeneratorVoltageConnection
|
|
// (@004c3288), which drives the eng-page generator-voltage bar (evolt.pcc,
|
|
// ScalarBarGauge @004c721c) and the MyomerCluster seek-voltage graph. The
|
|
// binary resolved the subsystem's InputVoltage link and read resolved+0x1DC ==
|
|
// Generator::outputVoltage (0..12000, == MeasuredVoltage()). Same databinding
|
|
// trap as BTPowerSourceFrame: the port's ResolveLink(AttributePointerOf(...,
|
|
// "InputVoltage")) path is dead after the BT_DEV_GAUGES audio attribute rows
|
|
// shifted the chained ids, so the bar read 0. This reads the NAMED member via
|
|
// PoweredSubsystem::ResolveVoltageSource(), bypassing the attribute table.
|
|
//
|
|
Scalar BTGeneratorVoltage(void *subsystem_v)
|
|
{
|
|
static int s_log = -1;
|
|
if (s_log < 0) s_log = getenv("BT_LOOP_LOG") ? 1 : 0;
|
|
static int s_n = 0;
|
|
int trace = s_log && (s_n++ < 40);
|
|
|
|
Subsystem *sub = (Subsystem *)subsystem_v;
|
|
if (sub == 0 || !sub->IsDerivedFrom(*PoweredSubsystem::GetClassDerivations()))
|
|
{
|
|
if (trace) DEBUG_STREAM << "[voltfeed] " << (sub ? sub->GetName() : "(null)")
|
|
<< " -> 0 (not a PoweredSubsystem)\n" << std::flush;
|
|
return 0.0f;
|
|
}
|
|
Generator *gen = (Generator *)((PoweredSubsystem *)sub)->ResolveVoltageSource();
|
|
if (gen == 0 || gen->GetClassID() != RegisteredClass::GeneratorClassID)
|
|
{
|
|
if (trace) DEBUG_STREAM << "[voltfeed] " << sub->GetName()
|
|
<< " -> 0 (no Generator)\n" << std::flush;
|
|
return 0.0f;
|
|
}
|
|
Scalar volts = gen->MeasuredVoltage(); // outputVoltage @0x1DC
|
|
if (trace) DEBUG_STREAM << "[voltfeed] " << sub->GetName() << " -> " << volts
|
|
<< "V (generator '" << gen->GetName() << "')\n" << std::flush;
|
|
return volts;
|
|
}
|
|
|
|
|
|
//#############################################################################
|
|
// 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
|
|
ATTRIBUTE_ENTRY(Generator, GeneratorState, stateAlarm), // @0x1FC (0x54 StateIndicator-compatible alarm; SetLevel fires audio)
|
|
ATTRIBUTE_ENTRY(Generator, GeneratorOn, generatorOn) // @0x1D4 (AudioLogicalTrigger; init 1 -- static until a shutdown writer drives it to 0)
|
|
};
|
|
|
|
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()); }
|
|
|
|
//
|
|
Receiver::MessageHandlerSet&
|
|
Generator::GetMessageHandlers()
|
|
{
|
|
//
|
|
// Gitea #53. Chains **HeatSink**, deliberately NOT PoweredSubsystem: id 4 on
|
|
// PoweredSubsystem is `SelectGeneratorA` (the weapon-side generator SELECTION,
|
|
// which already works), so chaining there would make the four ON/OFF buttons
|
|
// invoke the wrong function instead of doing nothing. HeatSink owns id 3
|
|
// (ToggleCooling) and chains onward to the root, leaving id 4 free for us.
|
|
//
|
|
static const Receiver::HandlerEntry entries[]=
|
|
{
|
|
MESSAGE_ENTRY(Generator, ToggleGeneratorOnOff) // id 4 @004b1ed0
|
|
};
|
|
static Receiver::MessageHandlerSet messageHandlers(
|
|
ELEMENTS(entries), entries,
|
|
HeatSink::GetMessageHandlers()
|
|
);
|
|
return messageHandlers;
|
|
}
|
|
|
|
//
|
|
// @004b1ed0 -- ToggleGeneratorOnOff: the pod's Generator A-D ON/OFF buttons (RIO
|
|
// addresses 0x1A-0x1D on the radar rail, one per generator). Transcribed
|
|
// instruction-for-instruction from the binary; the body did not exist before
|
|
// (Gitea #53 -- 0 references in game/).
|
|
//
|
|
// call 0x4ac9c8(this); if (ret) return; -> BTPlayerRoleLocksAdvanced: a
|
|
// NOVICE pilot cannot touch it
|
|
// if ([msg+0xc] <= 0) return; -> press only, ignore the release
|
|
// if ([this+0x1d4] != 0) goto OFF -> generatorOn -> we are turning it off
|
|
//
|
|
// ON: eax = [this+0x184] -> heatAlarm level
|
|
// if (eax >= 1) { [this+0x1f0] = 0; } -> heat degraded/failed: clear the
|
|
// start timer but LEAVE the state
|
|
// alarm in its fault level
|
|
// else { [this+0x1f0] = 0;
|
|
// Set_Alarm_Level(this+0x1fc, 0); }
|
|
// -> healthy: also drop the state alarm
|
|
// to Starting so it spins up
|
|
// [this+0x1d4] = 1 -> generatorOn = 1
|
|
// [this+0x134] = 1 -> coolantAvailable = 1
|
|
// [this+0x15c] = 1.0f -> coolantFlowScale = 1.0
|
|
//
|
|
// OFF: Set_Alarm_Level(this+0x1fc, 1) -> stateAlarm = GeneratorIdle
|
|
// [this+0x1d4] = 0 -> generatorOn = 0
|
|
// [this+0x134] = 0 -> coolantAvailable = 0
|
|
// [this+0x15c] = 0.0f -> coolantFlowScale = 0 (its coolant
|
|
// share is released to the loop)
|
|
//
|
|
// So taking a generator off line idles it, drops its coolant draw, and -- via the
|
|
// state machine reading generatorOn -- stops it supplying voltage to its taps.
|
|
// That is the authentic heat-management trade the manual describes.
|
|
//
|
|
void
|
|
Generator::ToggleGeneratorOnOffMessageHandler(ReceiverDataMessageOf<int> *message)
|
|
{
|
|
if (BTPlayerRoleLocksAdvanced(owner)) // @004ac9c8 -- NOVICE lock
|
|
{
|
|
return;
|
|
}
|
|
if (message->dataContents <= 0) // press only
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (generatorOn == 0)
|
|
{
|
|
// ---- turning it ON ----
|
|
startTimer = 0.0f; // @0x1F0
|
|
if ((unsigned)heatAlarm.GetLevel() < 1) // @0x184 -- healthy
|
|
{
|
|
stateAlarm.SetLevel(0); // @0x1FC -> Starting (spin up)
|
|
}
|
|
generatorOn = 1; // @0x1D4
|
|
coolantAvailable = 1; // @0x134
|
|
coolantFlowScale = 1.0f; // @0x15C
|
|
}
|
|
else
|
|
{
|
|
// ---- taking it OFF LINE ----
|
|
stateAlarm.SetLevel(1); // @0x1FC -> GeneratorIdle
|
|
generatorOn = 0; // @0x1D4
|
|
coolantAvailable = 0; // @0x134
|
|
coolantFlowScale = 0.0f; // @0x15C
|
|
}
|
|
|
|
if (getenv("BT_FIRE_LOG") || getenv("BT_HEAT_LOG"))
|
|
DEBUG_STREAM << "[gensel] " << GetName() << " generator "
|
|
<< (generatorOn ? "ON LINE" : "OFF LINE")
|
|
<< " (stateAlarm " << stateAlarm.GetLevel()
|
|
<< ", coolantFlowScale " << coolantFlowScale << ")" << std::endl;
|
|
}
|
|
|
|
//
|
|
void
|
|
Generator::ResetToInitialState(Logical powered)
|
|
{
|
|
//
|
|
// RE-TRANSCRIBED FROM THE BINARY (Gitea #55). The old body chained
|
|
// `HeatableSubsystem::ResetToInitialState` and set `outputVoltage = 0` -- which
|
|
// is what GNRATOR.TCP says (see gnrator.hpp), but **BT's binary diverges from
|
|
// the TCP source here**, and the binary is what shipped in the pod.
|
|
//
|
|
// Disassembled @004b215c, instruction for instruction:
|
|
// call 0x4ad760 (this, powered) -> HeatSink::ResetToInitialState (NOT
|
|
// HeatableSubsystem -- so the coolant
|
|
// refill DOES run for a generator)
|
|
// [this+0x1d4] = 1 -> generatorOn = 1
|
|
// [this+0x1f0] = 0 -> startTimer = 0
|
|
// Set_Alarm_Level(this+0x1fc, 0) -> stateAlarm 0
|
|
// Set_Alarm_Level(this+0x1fc, 2) -> stateAlarm 2 (on line)
|
|
// eax = [this+0x1d8]; [this+0x1dc] = eax
|
|
// -> outputVoltage = ratedVoltage
|
|
// [this+0x134] = 1 -> coolantAvailable = 1
|
|
// [this+0x15c] = 0x3f800000 -> coolantFlowScale = 1.0f
|
|
// ecx = [this+0x1ec]; [this+0x1f0] = ecx
|
|
// -> startTimer = startTime
|
|
//
|
|
// Why this matters: the old version brought a generator back from a respawn at
|
|
// **ZERO output voltage** with its state alarm untouched. Energy weapons charge
|
|
// from the generator's voltage, so a respawned pilot's lasers could never reach
|
|
// ready -- no recharge ring, no fire-ready dot, nothing damaged (a strong
|
|
// candidate for Gitea #54, David's three dead energy weapons).
|
|
//
|
|
HeatSink::ResetToInitialState(powered); // @004ad760 (was HeatableSubsystem)
|
|
|
|
generatorOn = 1; // @0x1D4
|
|
startTimer = 0.0f; // @0x1F0 (cleared, then seeded below)
|
|
stateAlarm.SetLevel(0); // @0x1FC
|
|
stateAlarm.SetLevel(2); // ... to GeneratorOnLine
|
|
outputVoltage = ratedVoltage; // @0x1DC = @0x1D8 -- back ON LINE
|
|
coolantAvailable = 1; // @0x134
|
|
coolantFlowScale = 1.0f; // @0x15C
|
|
startTimer = startTime; // @0x1F0 = @0x1EC
|
|
}
|
|
|
|
//
|
|
// DeathReset (Gitea #55): the respawn sweep's entry for a Generator. Without it
|
|
// the sweep hit the empty `Subsystem::DeathReset` base and a respawned mech kept
|
|
// its previous life's generator state.
|
|
//
|
|
void
|
|
Generator::DeathReset(int /*reset_command*/)
|
|
{
|
|
ResetToInitialState(True); // @004b215c
|
|
}
|
|
|
|
//
|
|
// @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) // this[0x10]
|
|
&& !BTPlayerRoleLocksAdvanced(owner)) // FUN_004ac9c8 == 0 (not novice; issue #2 -- was the state-based IsDamaged stand-in, which wrongly blocked state 2)
|
|
{
|
|
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;
|
|
// @004b179c's FUN_004ac9c8(source) [T1] = the not-novice EXPERIENCE
|
|
// predicate on the source's owner (same mech as ours; issue #2 -- was
|
|
// the state-based IsDamaged stand-in).
|
|
if (source != 0 && !BTPlayerRoleLocksAdvanced(owner))
|
|
{
|
|
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;
|
|
}
|
|
|
|
//
|
|
// issue #11 bridge -- the SeekVoltageGraph's destroyed test:
|
|
// *(subsystem+0x40) == 1 in the binary (@004c6934) == MechSubsystem::
|
|
// simulationState == 1 (Destroyed). Named-member read through the real type
|
|
// (databinding rule); every subsystem that owns a graph is MechSubsystem-
|
|
// derived (Emitter/PPC/Myomers), so the PoweredSubsystem gate is sufficient
|
|
// and a non-match reads "not destroyed".
|
|
//
|
|
int BTSubsystemDestroyed(Subsystem *sub)
|
|
{
|
|
if (!BTIsPoweredSubsystem(sub))
|
|
return 0;
|
|
return (((PoweredSubsystem *)sub)->GetSimulationState() == 1) ? 1 : 0; // +0x40 == 1
|
|
}
|