Files
TeslaRel410/restoration/source410/BT/POWERSUB.CPP
T
CydandClaude Fable 5 24ea06574c BT410 Phase 5.3.28: the visual A/B pass -- 93% pixel-identical to the shipped cockpit
Ran the reconstructed exe and the shipped ALPHA_1 binary against the SAME
GAUGE content, same mount, gauges on, and diffed the 640x480x16
framebuffers.  OUR EXE DRAWS THE COCKPIT: 90.1% pixel-identical on the
first run, and every difference traced to a single root cause.

The authored aux-screen block (screen number / background placement /
label) lives in the subsystem resource -- which is freed with the Mech
ctor's stream buffer (the 5.3.24 use-after-free landmine).  Both this tree
and the BT411 port had stubbed it, which is why BT411's displays show the
same artifacting.  PoweredSubsystem now caches the block at ctor time with
accessors, and btl4gau2 uses the AUTHORED screen instead of a roster-order
stand-in and builds the placement strip art.  Panels snapped onto their
authored screens -- weapon dials now land on the same panels as the
shipped exe, mech icons appear on the sensor/myomer panels, generator
letters correct -- taking the match to 93.2% identical / 85% coverage.

Remaining differences are donor-inherited stubs: the panel title art (now
proven to come from the q-strip statusImage path, not auxScreenLabel --
the label blit is wired and guarded regardless), the pilot name bitmap,
and some lamp frames.

The A/B rig is banked at emulator/render-bridge/gauge-ab/ (both confs +
the window-grab script) so the comparison is repeatable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:29:01 -05:00

827 lines
23 KiB
C++

//===========================================================================//
// File: powersub.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: PoweredSubsystem -- a HeatSink drawing electrical power //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(POWERSUB_HPP)
# include <powersub.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
//
//#############################################################################
// Shared data support
//#############################################################################
//
Derivation
PoweredSubsystem::ClassDerivations(
HeatSink::ClassDerivations,
"PoweredSubsystem"
);
//
//#############################################################################
// The cockpit generator buttons (binary handler table @0x50F4EC, ids 4-8),
// chained onto the HeatSink set (id 3 ToggleCooling) so every powered
// subsystem's dispatch reaches the Eng-page Coolant button too. Static-init
// order is safe under the authentic .MAK lib order (heat precedes powersub).
//#############################################################################
//
const PoweredSubsystem::HandlerEntry
PoweredSubsystem::MessageHandlerEntries[] =
{
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
};
PoweredSubsystem::MessageHandlerSet
PoweredSubsystem::MessageHandlers(
ELEMENTS(PoweredSubsystem::MessageHandlerEntries),
PoweredSubsystem::MessageHandlerEntries,
HeatSink::MessageHandlers
);
//
//#############################################################################
// Attribute tables (cockpit binding by name).
//#############################################################################
//
const PoweredSubsystem::IndexEntry
PoweredSubsystem::AttributePointers[]=
{
ATTRIBUTE_ENTRY(PoweredSubsystem, InputVoltage, inputVoltage),
ATTRIBUTE_ENTRY(PoweredSubsystem, OutputVoltage, outputVoltage),
ATTRIBUTE_ENTRY(PoweredSubsystem, RatedVoltage, ratedVoltage),
ATTRIBUTE_ENTRY(PoweredSubsystem, VoltageState, electricalStateAlarm),
ATTRIBUTE_ENTRY(PoweredSubsystem, ConnectMode, modeAlarm)
};
PoweredSubsystem::AttributeIndexSet
PoweredSubsystem::AttributeIndex(
ELEMENTS(PoweredSubsystem::AttributePointers),
PoweredSubsystem::AttributePointers,
HeatSink::AttributeIndex
);
PoweredSubsystem::SharedData
PoweredSubsystem::DefaultData(
PoweredSubsystem::ClassDerivations,
PoweredSubsystem::MessageHandlers,
PoweredSubsystem::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
// A HeatSink that draws electrical power from a generator (binary ctor
// @004b0f74). Resolves the "VoltageSource" roster index to the powering
// generator, attaches the tap, and primes the electrical state machine. The
// voltageSourceIndex indexes the owner mech's SUBSYSTEM ROSTER (the same
// index space the AmmoBin link uses) -- the roster slots ahead of this
// subsystem are already constructed by the segment walk, and the shipped
// stream orders the generators first.
//#############################################################################
//
PoweredSubsystem::PoweredSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data),
voltageSource(),
electricalStateAlarm(5),
modeAlarm(3)
{
Check(owner);
Check_Pointer(subsystem_resource);
inputVoltage = 0.0f;
outputVoltage = 0.0f;
ratedVoltage = 0.0f;
thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient;
startTime = subsystem_resource->startTime;
//
// Cache the authored aux-screen block NOW -- the resource memory dies
// with the Mech ctor's stream buffer.
//
auxScreenNumber = subsystem_resource->auxScreenNumber;
auxScreenPlacement = subsystem_resource->auxScreenPlacement;
Str_Copy(auxScreenLabel, subsystem_resource->auxScreenLabel,
sizeof(auxScreenLabel));
Str_Copy(engScreenLabel, subsystem_resource->engScreenLabel,
sizeof(engScreenLabel));
startTimer = startTime;
voltageScale = 1.0f;
//
// Resolve the voltage source from the roster and attach the tap.
//
Subsystem *source = NULL;
if (subsystem_resource->voltageSourceIndex >= 0
&& subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount())
{
source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex);
}
if (source != NULL)
{
AttachToVoltageSource(source);
}
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[power] '" << GetName()
<< "' srcIdx=" << subsystem_resource->voltageSourceIndex << " -> ";
if (source != NULL)
{
DEBUG_STREAM << source->GetName();
}
else
{
DEBUG_STREAM << "<none>";
}
DEBUG_STREAM << " startTime=" << startTime << endl << flush;
}
electricalStateAlarm.SetLevel(Ready);
modeAlarm.SetLevel(Connected);
//
// A master (non-replicant) instance runs the per-frame electrical
// simulation. Derived subsystems (the weapons, Sensor, ...) override with
// their own Performance in their ctors, each of which chains this step.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation);
}
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
PoweredSubsystem::~PoweredSubsystem()
{
}
//
//#############################################################################
//#############################################################################
//
void
PoweredSubsystem::ResetToInitialState(Logical powered)
{
Check(this);
HeatSink::ResetToInitialState(powered);
inputVoltage = 0.0f;
outputVoltage = 0.0f;
electricalStateAlarm.SetLevel(0);
modeAlarm.SetLevel(0);
}
//
//#############################################################################
//#############################################################################
//
Logical
PoweredSubsystem::TestClass(Mech &)
{
return True;
}
Logical
PoweredSubsystem::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// AttachToVoltageSource Link this subsystem to its powering generator
// (binary @004b0dd8): take a tap on the generator (-1 when every tap is
// taken) and hold the live connection.
//#############################################################################
//
int
PoweredSubsystem::AttachToVoltageSource(Subsystem *source)
{
Check(this);
Check(source);
Generator *generator = (Generator *)source;
if (generator->TapVoltageSource() != 0)
{
return -1;
}
voltageSource.Add(source);
inputVoltage = generator->MeasuredVoltage();
return 0;
}
//
//#############################################################################
// DetachFromVoltageSource (binary @004b0e30): release the tap on the current
// source and clear the connection.
//#############################################################################
//
void
PoweredSubsystem::DetachFromVoltageSource()
{
Check(this);
Generator *source = (Generator *)voltageSource.Resolve();
if (source != NULL)
{
source->UntapVoltageSource();
voltageSource.Clear();
}
}
//
//#############################################################################
// FindGeneratorByNumber (binary @004b0b18): walk the owner's roster for the
// Generator whose authored generatorNumber matches (1=A .. 4=D).
//#############################################################################
//
Subsystem*
PoweredSubsystem::FindGeneratorByNumber(int generator_number)
{
Check(this);
for (int slot = 2; slot < owner->GetSubsystemCount(); ++slot)
{
Subsystem *sub = owner->GetSubsystem(slot);
if (sub != NULL
&& sub->IsDerivedFrom(Generator::ClassDerivations)
&& ((Generator *)sub)->GetGeneratorNumber() == generator_number)
{
return sub;
}
}
return NULL;
}
//
//#############################################################################
// SelectGenerator -- the manual re-tap: release the current source, tap
// generator N, drop the connect mode back to Connected (a manual selection
// ends any auto-hunt). The binary dereferences the find unguarded (authored
// mechs always carry A-D); we skip loud instead.
//#############################################################################
//
void
PoweredSubsystem::SelectGenerator(int generator_number)
{
Check(this);
Subsystem *generator = FindGeneratorByNumber(generator_number);
if (generator == NULL)
{
if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[gensel] '" << GetName()
<< "' -> generator " << generator_number
<< " NOT FOUND" << endl << flush;
}
return;
}
DetachFromVoltageSource();
int tap = AttachToVoltageSource(generator);
modeAlarm.SetLevel(Connected);
if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[gensel] '" << GetName()
<< "' -> '" << generator->GetName() << "'"
<< ((tap >= 0) ? " (tapped)" : " (REFUSED: no spare tap)")
<< endl << flush;
}
}
//
//#############################################################################
// The cockpit button handlers (ids 4-8). Press-only (dataContents > 0),
// novice-locked -- a novice cockpit's generator panel is inert.
//#############################################################################
//
void
PoweredSubsystem::SelectGeneratorAMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
if (!NoviceLockout() && message->dataContents > 0)
{
SelectGenerator(1);
}
}
void
PoweredSubsystem::SelectGeneratorBMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
if (!NoviceLockout() && message->dataContents > 0)
{
SelectGenerator(2);
}
}
void
PoweredSubsystem::SelectGeneratorCMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
if (!NoviceLockout() && message->dataContents > 0)
{
SelectGenerator(3);
}
}
void
PoweredSubsystem::SelectGeneratorDMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
if (!NoviceLockout() && message->dataContents > 0)
{
SelectGenerator(4);
}
}
//
//#############################################################################
// ToggleGeneratorMode (id 8, binary @004b0abc): cycle Manual -> AutoConnect
// -> (detach +) Manual. The auto-hunt itself (a shorted/dead source makes
// the subsystem walk for a live generator) lives in
// PoweredSubsystemSimulation -- a later brick.
//#############################################################################
//
void
PoweredSubsystem::ToggleGeneratorModeMessageHandler(
ReceiverDataMessageOf<int> *message)
{
Check(this);
if (NoviceLockout() || message->dataContents <= 0)
{
return;
}
if ((unsigned)modeAlarm.GetLevel() < (unsigned)AutoConnect)
{
modeAlarm.SetLevel(AutoConnect);
}
else if (modeAlarm.GetLevel() == AutoConnect)
{
DetachFromVoltageSource();
modeAlarm.SetLevel(ManualConnect);
}
if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[gensel] '" << GetName()
<< "' mode -> " << modeAlarm.GetLevel() << endl << flush;
}
}
//
//#############################################################################
// ChargeTimeScale -- the heat/firepower feedback (binary @004b0d50):
// rise = max(0, sourceTemperature - sourceStartingTemperature)
// scale = max(voltageScale,
// (thermalResistivityCoefficient * rise + 1) * voltageScale)
// A hot generator stretches the exponential charge constant, so recharging
// slows exactly when the electrical plant is cooking.
//#############################################################################
//
Scalar
PoweredSubsystem::ChargeTimeScale()
{
Check(this);
Generator *source = (Generator *)voltageSource.Resolve();
if (source == NULL)
{
return voltageScale;
}
Scalar rise = source->CurrentTemperatureOf()
- source->StartingTemperatureOf();
if (rise < 0.0f)
{
rise = 0.0f;
}
Scalar stretched =
(thermalResistivityCoefficient * rise + 1.0f) * voltageScale;
return (stretched > voltageScale) ? stretched : voltageScale;
}
//
//#############################################################################
// DeathReset -- respawn restore for the powered chain and the generators.
//#############################################################################
//
void
PoweredSubsystem::DeathReset(Logical full_reset)
{
Check(this);
MechSubsystem::DeathReset(full_reset);
ResetToInitialState(True);
//
// ResetToInitialState drops the connect-mode indicator to Manual; the
// respawned subsystem keeps its live tap, so the mode is Connected
// (the ctor's spawn state).
//
modeAlarm.SetLevel(Connected);
}
void
Generator::DeathReset(Logical full_reset)
{
Check(this);
MechSubsystem::DeathReset(full_reset);
//
// PRESERVE the tap accounting across the reset: consumers keep their
// voltageSource links through respawn (nothing detaches), so zeroing
// currentTapCount here would desync the maxTapCount invariant and let
// post-respawn SelectGenerator presses oversubscribe the generator
// (caught by the 5.3.24 adversarial review).
//
int live_taps = currentTapCount;
ResetToInitialState();
currentTapCount = live_taps;
}
//
//#############################################################################
// ForceShortRecovery (binary @004b11bc): on a short event, drive the powering
// generator to Shorted and clear its output; GeneratorSimulation then runs
// the short-recovery timer back to Ready. Both @4ac9c8 calls in the binary
// are the not-novice experience predicate (this part and its source share
// the same mech, hence the same player) -- novice cockpits never see
// electrical shorts.
//#############################################################################
//
void
PoweredSubsystem::ForceShortRecovery()
{
Check(this);
if (NoviceLockout())
{
return;
}
Generator *source = (Generator *)voltageSource.Resolve();
if (source != NULL)
{
source->ForceShort();
if (getenv("BT_POWER_LOG") || getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[short] '" << source->GetName()
<< "' SHORTED by special damage" << endl << flush;
}
}
}
//
//#############################################################################
// PoweredSubsystemSimulation -- the per-frame electrical step (binary
// @004b0bd0). Runs the HeatSink thermal step, then advances the electrical
// state machine from the state of the powering generator.
//
// PARTIAL: the AutoConnect replacement-generator hunt (modeAlarm AutoConnect +
// the status-flag gate) joins with the damage wave.
//#############################################################################
//
void
PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice)
{
Check(this);
HeatSink::HeatSinkSimulation(time_slice);
Generator *source = (Generator *)voltageSource.Resolve();
if (source == NULL)
{
electricalStateAlarm.SetLevel(NoVoltage);
}
else
{
if (source->GeneratorStateOf() == Generator::GeneratorShorted)
{
electricalStateAlarm.SetLevel(Shorted);
}
if (source->GeneratorStateOf() == Generator::GeneratorStarting
|| source->GeneratorStateOf() == Generator::GeneratorFailed)
{
electricalStateAlarm.SetLevel(GeneratorOff);
}
}
switch (electricalStateAlarm.GetLevel())
{
case Starting:
startTimer += time_slice;
if (startTime <= startTimer)
{
electricalStateAlarm.SetLevel(Ready);
}
break;
case NoVoltage:
if (source != NULL)
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
case Shorted:
case GeneratorOff:
if (source != NULL
&& source->GeneratorStateOf() == Generator::GeneratorReady)
{
electricalStateAlarm.SetLevel(Starting);
startTimer = 0.0f;
}
break;
}
if (source != NULL)
{
inputVoltage = source->MeasuredVoltage();
}
Check_Fpu();
}
//###########################################################################
//############################## Generator #############################
//###########################################################################
//
//#############################################################################
// Shared data support
//#############################################################################
//
Derivation
Generator::ClassDerivations(
HeatSink::ClassDerivations,
"Generator"
);
const Generator::IndexEntry
Generator::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage),
ATTRIBUTE_ENTRY(Generator, RatedVoltage, ratedVoltage),
ATTRIBUTE_ENTRY(Generator, GeneratorNumber, generatorNumber)
};
Generator::AttributeIndexSet
Generator::AttributeIndex(
ELEMENTS(Generator::AttributePointers),
Generator::AttributePointers,
HeatSink::AttributeIndex
);
Generator::SharedData
Generator::DefaultData(
Generator::ClassDerivations,
Subsystem::MessageHandlers,
Generator::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
// The generator -- the voltage source loads tap.
//#############################################################################
//
Generator::Generator(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, subsystem_resource, shared_data),
stateAlarm(5)
{
Check(owner);
Check_Pointer(subsystem_resource);
ratedVoltage = subsystem_resource->ratedVoltage;
outputVoltage = ratedVoltage;
maxTapCount = subsystem_resource->maxTapCount;
currentTapCount = 0;
percentVoltageAvailable = 1.0f;
startTime = subsystem_resource->startTime;
startTimer = startTime;
stateAlarm.SetLevel(GeneratorReady);
generatorOn = 1;
shortRecoveryTime = subsystem_resource->shortRecoveryTime;
shortTimer = shortRecoveryTime;
//
// Generator number from the last character of the segment name
// ('A' -> 1, 'B' -> 2, ...).
//
const char *name = GetName();
generatorNumber = name[strlen(name) - 1] - 0x40;
//
// Install the generator's per-frame electrical Performance.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Generator::GeneratorSimulation);
}
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
Generator::~Generator()
{
}
//
//#############################################################################
//#############################################################################
//
Logical
Generator::TestClass(Mech &)
{
return True;
}
Logical
Generator::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
//#############################################################################
//
void
Generator::ResetToInitialState()
{
Check(this);
HeatSink::ResetToInitialState(True);
outputVoltage = ratedVoltage;
currentTapCount = 0;
percentVoltageAvailable = 1.0f;
startTimer = startTime;
shortTimer = shortRecoveryTime;
generatorOn = 1;
stateAlarm.SetLevel(GeneratorReady);
}
//
//#############################################################################
// GeneratorSimulation -- the generator's per-frame step (PARTIAL). Runs the
// HeatSink thermal step and the start/short-recovery timers. The authentic
// load model (output voltage sag under tap load / I^2R self-heat feeding the
// charge integration) joins with the electrical-charge wave
// (TrackSeekVoltage). A healthy generator holds GeneratorReady at its rated
// voltage.
//#############################################################################
//
void
Generator::GeneratorSimulation(Scalar time_slice)
{
Check(this);
HeatSink::HeatSinkSimulation(time_slice);
switch (stateAlarm.GetLevel())
{
case GeneratorStarting:
startTimer += time_slice;
if (startTime <= startTimer)
{
stateAlarm.SetLevel(GeneratorReady);
outputVoltage = ratedVoltage;
}
break;
case GeneratorShorted:
shortTimer -= time_slice;
if (shortTimer <= 0.0f)
{
shortTimer = shortRecoveryTime;
stateAlarm.SetLevel(GeneratorStarting);
startTimer = 0.0f;
}
break;
default:
break;
}
Check_Fpu();
}
//###########################################################################
//############################ PowerWatcher ############################
//###########################################################################
Derivation
PowerWatcher::ClassDerivations(
HeatWatcher::ClassDerivations,
"PowerWatcher"
);
PowerWatcher::SharedData
PowerWatcher::DefaultData(
PowerWatcher::ClassDerivations,
Subsystem::MessageHandlers,
Subsystem::AttributeIndex,
Subsystem::StateCount
);
PowerWatcher::PowerWatcher(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
HeatWatcher(owner, subsystem_ID, subsystem_resource, shared_data),
watchdogAlarm(5)
{
Check(owner);
Check_Pointer(subsystem_resource);
//
// minVoltage is a scaled fraction of the watched supply; the exact scale
// constant is a tuning value (stored 1:1 here until located).
//
minVoltage = subsystem_resource->minVoltagePercent;
Check_Fpu();
}
PowerWatcher::~PowerWatcher()
{
}
Logical
PowerWatcher::TestClass(Mech &)
{
return True;
}
Logical
PowerWatcher::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
void
PowerWatcher::ResetToInitialState(Logical powered)
{
Check(this);
HeatWatcher::ResetToInitialState(powered);
watchdogAlarm.SetLevel(0);
}
void
PowerWatcher::DeathReset(Logical full_reset)
{
Check(this);
MechSubsystem::DeathReset(full_reset);
ResetToInitialState(True);
}
//
// Per-frame supply-voltage watchdog. Not yet reconstructed.
//
void
PowerWatcher::Simulation(Scalar)
{
Fail("PowerWatcher::Simulation -- powersub.cpp not yet reconstructed");
}