BT410 Phase 5.3.27: the attribute wave -- 48/50 cockpit bindings go LIVE

The gauges now read the real simulation: heat/power/sensor/mech attribute
tables published under the 1995 L4GAUGE.CFG spellings the widgets already
bind by name.  Temperatures, coolant mass/capacity/leak rate, condenser
valve settings, generator voltages and numbers, radar percent, and the
mech's radar/speed rows all resolve to live members.

THE NUMBERING IS PINNED: the chain publishes 2..0x0E so
PoweredSubsystem::NextAttributeID lands on the authentic 0x0F -- the
surviving SENSOR.HPP numbers RadarPercent off it and MechWeapon's
binary-pinned table starts at 0x12, so its pads shrank 16 -> 3
(0x0F..0x11) exactly as the 5.3.x header comment predicted.  Sensor's
authentic enum finally has its table defined; MechWeapon/Sensor rechain to
PoweredSubsystem and Reservoir/AggregateHeatSink to HeatSink so the whole
family is visible where the cfg expects it.

Verified with BT_GAUGE_ATTR_LOG: 48 OK / 2 NULL (was 33/17, and 0/50 at
the block's birth).  The two remaining have no member to bind --
HeatSink/AmbientTemperature (a sim constant) and Searchlight/LightOn (a
memberless subclass) -- and fall to the documented zero cell.  Gauge
fight (15/15 rounds, 127 zone hits), smoke and novice all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-24 20:09:08 -05:00
co-authored by Claude Fable 5
parent 64e208f121
commit d67f8d5eda
11 changed files with 691 additions and 406 deletions
+56 -4
View File
@@ -51,11 +51,34 @@ Derivation
"HeatableSubsystem"
);
//
//#############################################################################
// Attribute tables -- the cockpit binds these by NAME (1995 L4GAUGE.CFG
// spellings). Contiguous IDs; each set chains its parent so a weapon or
// sensor sees the whole heat/power family.
//#############################################################################
//
const HeatableSubsystem::IndexEntry
HeatableSubsystem::AttributePointers[]=
{
ATTRIBUTE_ENTRY(HeatableSubsystem, CurrentTemperature, currentTemperature),
ATTRIBUTE_ENTRY(HeatableSubsystem, HeatLoad, heatLoad),
ATTRIBUTE_ENTRY(HeatableSubsystem, DegradationTemperature, degradationTemperature),
ATTRIBUTE_ENTRY(HeatableSubsystem, FailureTemperature, failureTemperature)
};
HeatableSubsystem::AttributeIndexSet
HeatableSubsystem::AttributeIndex(
ELEMENTS(HeatableSubsystem::AttributePointers),
HeatableSubsystem::AttributePointers,
Subsystem::AttributeIndex
);
HeatableSubsystem::SharedData
HeatableSubsystem::DefaultData(
HeatableSubsystem::ClassDerivations,
Subsystem::MessageHandlers,
Subsystem::AttributeIndex,
HeatableSubsystem::AttributeIndex,
Subsystem::StateCount
);
@@ -170,11 +193,27 @@ HeatSink::MessageHandlerSet
Subsystem::MessageHandlers
);
const HeatSink::IndexEntry
HeatSink::AttributePointers[]=
{
ATTRIBUTE_ENTRY(HeatSink, CoolantMass, coolantLevel),
ATTRIBUTE_ENTRY(HeatSink, CoolantCapacity, thermalCapacity),
ATTRIBUTE_ENTRY(HeatSink, CoolantMassLeakRate, coolantDraw),
ATTRIBUTE_ENTRY(HeatSink, CoolantAvailable, coolantAvailable)
};
HeatSink::AttributeIndexSet
HeatSink::AttributeIndex(
ELEMENTS(HeatSink::AttributePointers),
HeatSink::AttributePointers,
HeatableSubsystem::AttributeIndex
);
HeatSink::SharedData
HeatSink::DefaultData(
HeatSink::ClassDerivations,
HeatSink::MessageHandlers,
Subsystem::AttributeIndex,
HeatSink::AttributeIndex,
Subsystem::StateCount
);
@@ -800,11 +839,24 @@ Condenser::MessageHandlerSet
HeatSink::MessageHandlers
);
const Condenser::IndexEntry
Condenser::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Condenser, ValveSetting, coolantFlowScale)
};
Condenser::AttributeIndexSet
Condenser::AttributeIndex(
ELEMENTS(Condenser::AttributePointers),
Condenser::AttributePointers,
HeatSink::AttributeIndex
);
Condenser::SharedData
Condenser::DefaultData(
Condenser::ClassDerivations,
Condenser::MessageHandlers,
Subsystem::AttributeIndex,
Condenser::AttributeIndex,
Subsystem::StateCount
);
@@ -993,7 +1045,7 @@ AggregateHeatSink::SharedData
AggregateHeatSink::DefaultData(
AggregateHeatSink::ClassDerivations,
HeatSink::MessageHandlers,
Subsystem::AttributeIndex,
HeatSink::AttributeIndex,
Subsystem::StateCount
);
+52
View File
@@ -51,6 +51,29 @@
// Shared Data Support
//
public:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support. The cockpit gauges bind these by NAME through the
// interpreter (ParseAttribute -> GetAttributePointer): the 1995
// L4GAUGE.CFG spellings are the enum names. The whole heat/power chain
// publishes IDs 2..0x0E so PoweredSubsystem::NextAttributeID lands on the
// authentic 0x0F -- Sensor's table (surviving SENSOR.HPP) and
// MechWeapon's pinned 0x12 pads are both numbered off it. CONTIGUITY IS
// LOAD-BEARING: AttributeIndexSet::Build leaves uncovered gap slots
// uninitialized.
//
public:
enum {
CurrentTemperatureAttributeID = Subsystem::NextAttributeID, // 2
HeatLoadAttributeID, // 3
DegradationTemperatureAttributeID, // 4
FailureTemperatureAttributeID, // 5
NextAttributeID // 6
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet AttributeIndex;
static Derivation ClassDerivations;
static SharedData DefaultData;
@@ -212,6 +235,22 @@
// Shared Data Support
//
public:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support (the coolant family the cockpit bars read).
//
public:
enum {
CoolantMassAttributeID = HeatableSubsystem::NextAttributeID, // 6
CoolantCapacityAttributeID, // 7
CoolantMassLeakRateAttributeID, // 8
CoolantAvailableAttributeID, // 9
NextAttributeID // 0x0A
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet AttributeIndex;
static Derivation ClassDerivations;
static SharedData DefaultData;
@@ -367,6 +406,19 @@
public HeatSink
{
public:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support (the valve slider on the Eng page).
//
public:
enum {
ValveSettingAttributeID = HeatSink::NextAttributeID, // 0x0A
NextAttributeID
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet AttributeIndex;
static Derivation ClassDerivations;
static SharedData DefaultData;
+31 -1
View File
@@ -87,11 +87,34 @@ Mech::MessageHandlerSet
JointedMover::MessageHandlers
);
//
//#############################################################################
// The entity-level attribute table (cockpit binding by name).
//#############################################################################
//
const Mech::IndexEntry
Mech::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Mech, RadarRange, radarRange),
ATTRIBUTE_ENTRY(Mech, RadarLinearPosition, radarLinearPosition),
ATTRIBUTE_ENTRY(Mech, RadarAngularPosition, radarAngularPosition),
ATTRIBUTE_ENTRY(Mech, LinearSpeed, currentBodySpeed),
ATTRIBUTE_ENTRY(Mech, MaxRunSpeed, reverseStrideLength),
ATTRIBUTE_ENTRY(Mech, DuckState, duckState)
};
Mech::AttributeIndexSet
Mech::AttributeIndex(
ELEMENTS(Mech::AttributePointers),
Mech::AttributePointers,
JointedMover::AttributeIndex
);
Mech::SharedData
Mech::DefaultData(
Mech::ClassDerivations,
Mech::MessageHandlers,
JointedMover::AttributeIndex,
Mech::AttributeIndex,
33,
(Entity::MakeHandler)Mech::Make
);
@@ -193,6 +216,13 @@ Mech::Mech(
lastInflictingID = EntityID::Null;
damageLookupTable = NULL;
deathTransitionDone = 0;
//
// Cockpit-published state: the radar follows our own live transform.
//
radarRange = 4000.0f; // the authored map() max range
radarLinearPosition = &localOrigin.linearPosition;
radarAngularPosition = &localOrigin.angularPosition;
duckState = 0;
lastInflictingDamage = 0.0f;
//
+32 -1
View File
@@ -166,6 +166,26 @@
static MessageHandlerSet
MessageHandlers;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support -- the ENTITY-level rows the cockpit binds without a
// subsystem prefix (the radar trio the map() primitive reads, the speed
// pair the speedometer arc reads, and the duck/crouch state selector).
// Chained onto JointedMover's index.
//
public:
enum {
RadarRangeAttributeID = JointedMover::NextAttributeID,
RadarLinearPositionAttributeID,
RadarAngularPositionAttributeID,
LinearSpeedAttributeID,
MaxRunSpeedAttributeID,
DuckStateAttributeID,
NextAttributeID
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet AttributeIndex;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
@@ -462,6 +482,17 @@
DamageLookupTable *damageLookupTable; // binary mech+0x444 -- the
// cylinder hit-location table
//
// Cockpit-published state (bound by name from L4GAUGE.CFG): the radar
// scale + the pointers the map gauge follows, and the duck selector.
// The position/angle pointers are the 1995 shape -- the gauge holds a
// pointer TO the pointer and re-reads the live transform every frame.
//
Scalar radarRange;
Point3D *radarLinearPosition;
Quaternion *radarAngularPosition;
int duckState;
//
// The model's authored look-view angles (rad; deg in the resource).
//
@@ -475,7 +506,7 @@
// mech2/mech3/mech4 simulation. Reserved until that path is
// reconstructed with named fields.
//
int reservedState[196];
int reservedState[191];
};
#endif
+4 -17
View File
@@ -83,22 +83,9 @@ MechWeapon::MessageHandlerSet
const MechWeapon::IndexEntry
MechWeapon::AttributePointers[]=
{
MECHWEAP_PAD( 0, "MechWeaponPad02"),
MECHWEAP_PAD( 1, "MechWeaponPad03"),
MECHWEAP_PAD( 2, "MechWeaponPad04"),
MECHWEAP_PAD( 3, "MechWeaponPad05"),
MECHWEAP_PAD( 4, "MechWeaponPad06"),
MECHWEAP_PAD( 5, "MechWeaponPad07"),
MECHWEAP_PAD( 6, "MechWeaponPad08"),
MECHWEAP_PAD( 7, "MechWeaponPad09"),
MECHWEAP_PAD( 8, "MechWeaponPad0A"),
MECHWEAP_PAD( 9, "MechWeaponPad0B"),
MECHWEAP_PAD(10, "MechWeaponPad0C"),
MECHWEAP_PAD(11, "MechWeaponPad0D"),
MECHWEAP_PAD(12, "MechWeaponPad0E"),
MECHWEAP_PAD(13, "MechWeaponPad0F"),
MECHWEAP_PAD(14, "MechWeaponPad10"),
MECHWEAP_PAD(15, "MechWeaponPad11"),
MECHWEAP_PAD( 0, "MechWeaponPad0F"),
MECHWEAP_PAD( 1, "MechWeaponPad10"),
MECHWEAP_PAD( 2, "MechWeaponPad11"),
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12
ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13
ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14
@@ -119,7 +106,7 @@ MechWeapon::AttributeIndexSet
MechWeapon::AttributeIndex(
ELEMENTS(MechWeapon::AttributePointers),
MechWeapon::AttributePointers,
Subsystem::AttributeIndex
PoweredSubsystem::AttributeIndex
);
MechWeapon::SharedData
+1 -1
View File
@@ -99,7 +99,7 @@
//
public:
enum {
MechWeaponPadFirstAttributeID = Subsystem::NextAttributeID, // 2
MechWeaponPadFirstAttributeID = PoweredSubsystem::NextAttributeID, // 0x0F
PercentDoneAttributeID = 0x12,
TriggerStateAttributeID, // 0x13 -- the streamed fire-button binding
DistanceToTargetAttributeID, // 0x14
+39 -2
View File
@@ -55,11 +55,33 @@ PoweredSubsystem::MessageHandlerSet
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,
Subsystem::AttributeIndex,
PoweredSubsystem::AttributeIndex,
Subsystem::StateCount
);
@@ -557,11 +579,26 @@ Derivation
"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,
Subsystem::AttributeIndex,
Generator::AttributeIndex,
Subsystem::StateCount
);
+38
View File
@@ -49,6 +49,27 @@
// Shared Data Support
//
public:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support. The electrical family; NextAttributeID lands on
// the AUTHENTIC 0x0F -- the surviving SENSOR.HPP numbers RadarPercent
// off it and MechWeapon's pinned table starts its real IDs at 0x12
// (three pads bridge 0x0F..0x11). Do not renumber without re-pinning
// both.
//
public:
enum {
InputVoltageAttributeID = HeatSink::NextAttributeID, // 0x0A
OutputVoltageAttributeID, // 0x0B
RatedVoltageAttributeID, // 0x0C
VoltageStateAttributeID, // 0x0D
ConnectModeAttributeID, // 0x0E
NextAttributeID // 0x0F
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet AttributeIndex;
static Derivation ClassDerivations;
static SharedData DefaultData;
@@ -301,6 +322,23 @@
// Shared Data Support
//
public:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support (the generator panel reads these by name). A
// SIBLING branch of PoweredSubsystem off HeatSink -- the shared 0x0A
// base is correct, each branch owns its own index set.
//
public:
enum {
OutputVoltageAttributeID = HeatSink::NextAttributeID, // 0x0A
RatedVoltageAttributeID, // 0x0B
GeneratorNumberAttributeID, // 0x0C
NextAttributeID
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet AttributeIndex;
static Derivation ClassDerivations;
static SharedData DefaultData;
+379 -379
View File
@@ -1,379 +1,379 @@
//===========================================================================//
// File: reservr.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: Reservoir -- the coolant store //
//---------------------------------------------------------------------------//
// 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(RESERVR_HPP)
# include <reservr.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp>
#endif
#include <math.h>
Derivation
Reservoir::ClassDerivations(
HeatSink::ClassDerivations,
"Reservoir"
);
//
// The cockpit coolant-flush button (id 4, "InjectCoolant"); ToggleCooling
// (id 3) is inherited from the HeatSink chain.
//
const Reservoir::HandlerEntry
Reservoir::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(Reservoir, InjectCoolant)
};
Reservoir::MessageHandlerSet
Reservoir::MessageHandlers(
ELEMENTS(Reservoir::MessageHandlerEntries),
Reservoir::MessageHandlerEntries,
HeatSink::MessageHandlers
);
Reservoir::SharedData
Reservoir::DefaultData(
Reservoir::ClassDerivations,
Reservoir::MessageHandlers,
Subsystem::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
// The coolant store (binary ctor @4af408): CoolantCapacity overlays the
// inherited HeatSink thermalCapacity slot; the charge starts full; the flush
// flow scale is zero (a reservoir never conducts like an ordinary sink). A
// master reservoir registers the CoolantSimulation performance.
//
// Deferred with the AggregateHeatSink wave: the authentic master path also
// attaches the reservoir into the central bank's radiator loop and rescales
// the capacity by 0.05 x the bank's heat-sink count (the central sink here is
// still a plain HeatSink, which has no count) -- until then the streamed
// capacity is used as-is (a larger tank than authentic; noted).
//#############################################################################
//
Reservoir::Reservoir(
Mech *owner,
int subsystem_ID,
SubsystemResource *r,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, r, shared_data)
{
Check(owner);
Check_Pointer(r);
reservoirAlarm.Initialize(2);
squirtEfficiency = 0.5f;
thermalCapacity = r->coolantCapacity;
coolantSquirtMass = r->coolantSquirtMass;
coolantLevel = thermalCapacity;
coolantFlowScale = 0.0f;
injectAccumulator = 0.0f;
reservoirAlarm.SetLevel(0);
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[resv] '" << GetName()
<< "' capacity=" << thermalCapacity
<< " squirtMass=" << coolantSquirtMass << endl << flush;
}
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Reservoir::CoolantSimulation);
//
// The authentic master path scales the tank by the bank's aggregate
// heat-sink count: capacity = 0.05 x count x streamed (binary
// @4af408, CoolantCapacityScale byte-verified 0.05). The linked sink
// (from the resource) IS the bank. (The bank-side Attach that routes
// its DrawCoolant top-ups here joins the attach wave.)
//
HeatSink *link = (HeatSink *)linkedSinks.Resolve();
if (link != NULL
&& link->IsDerivedFrom(AggregateHeatSink::ClassDerivations))
{
Scalar masterScale =
(Scalar)((AggregateHeatSink *)link)->GetHeatSinkCount();
thermalCapacity = 0.05f * masterScale * thermalCapacity;
coolantLevel = thermalCapacity;
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[resv] capacity rescaled by bank count -> "
<< thermalCapacity << endl << flush;
}
}
}
Check_Fpu();
}
Reservoir::~Reservoir()
{
}
Logical
Reservoir::TestClass(Mech &)
{
return True;
}
Logical
Reservoir::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// DrawCoolant -- the SOURCE (binary @4af3b0): hand out up to coolantLevel of
// the requested amount and deduct it from the charge.
//#############################################################################
//
Scalar
Reservoir::DrawCoolant(Scalar requested)
{
Check(this);
Scalar supplied = 0.0f;
if (requested >= 0.0f)
{
supplied = requested;
if (coolantLevel < requested)
{
supplied = coolantLevel;
}
}
coolantLevel -= supplied;
return supplied;
}
//
//#############################################################################
// InjectCoolantMessageHandler -- the cockpit coolant-flush button (binary
// @4aee70, id 4): novice-locked. Release drops the inject alarm (flush OFF);
// press raises it when the tank holds charge (the flush-cloud effect joins
// the psfx wave) and zeroes the elapsed accumulator.
//#############################################################################
//
void
Reservoir::InjectCoolantMessageHandler(ReceiverDataMessageOf<int> *message)
{
Check(this);
Check(message);
if (NoviceLockout())
{
return;
}
if (message->dataContents < 1)
{
reservoirAlarm.SetLevel(0); // release -> flush OFF
}
else
{
if (reservoirAlarm.GetLevel() != 1 && coolantLevel > 0.0f)
{
reservoirAlarm.SetLevel(1); // flush ON
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[resv] FLUSH ON via button (charge="
<< coolantLevel << ")" << endl << flush;
}
}
injectAccumulator = 0.0f;
}
ForceUpdate();
}
//
//#############################################################################
// CoolantSimulation -- the registered Performance (binary @4aef78): while
// injection is active, accumulate elapsed time and run the coolant
// distribution. The InjectCoolant COCKPIT BUTTON (the id-4 message handler
// that raises the alarm) joins with the cockpit-button message wave; the DEV
// hook BT_FORCE_FLUSH=1 raises it here so the flush machinery is exercisable
// headlessly (one press ~4 s after the sim starts ticking).
//#############################################################################
//
void
Reservoir::CoolantSimulation(Scalar time_slice)
{
Check(this);
{
static int forceFlush = -1;
if (forceFlush < 0)
{
forceFlush = (getenv("BT_FORCE_FLUSH") != NULL) ? 1 : 0;
}
if (forceFlush == 1)
{
static Scalar armAccum = 0.0f;
armAccum += time_slice;
if (armAccum >= 4.0f && reservoirAlarm.GetLevel() != 1
&& coolantLevel > 0.0f)
{
forceFlush = 2;
reservoirAlarm.SetLevel(1);
injectAccumulator = 0.0f;
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[resv] FLUSH ON (charge="
<< coolantLevel << ")" << endl << flush;
}
}
}
}
if (reservoirAlarm.GetLevel() == 1)
{
injectAccumulator += time_slice;
InjectCoolant(time_slice);
}
}
//
//#############################################################################
// InjectCoolant -- the coolant-flush distribution (binary @4aefa4). Walk the
// roster gathering the flush targets in the authentic pass order -- the
// condensers, then the weapons, then every heat sink, then the linked master
// (the duplicate visits are intentional weighting) -- and squirt
// (coolantSquirtMass x the target's coolantFlowScale x dt) into each,
// crediting a negative pending-heat chill for the moved mass. Squirts only
// ever leave the tank; the chill only ever cools.
//#############################################################################
//
void
Reservoir::InjectCoolant(Scalar time_slice)
{
Check(this);
if (fabs(coolantLevel) <= 1.0e-4f) // the tank is empty
{
return;
}
enum { kMaxWork = 96 };
HeatSink *work[kMaxWork];
int workCount = 0;
Entity *own = (Entity *)owner;
int count = own->GetSubsystemCount();
int i;
for (i = 2; i < count && workCount < kMaxWork; ++i)
{
Subsystem *s = own->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations))
{
work[workCount++] = (HeatSink *)s;
}
}
for (i = 2; i < count && workCount < kMaxWork; ++i)
{
Subsystem *s = own->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(MechWeapon::ClassDerivations))
{
work[workCount++] = (HeatSink *)s;
}
}
for (i = 2; i < count && workCount < kMaxWork; ++i)
{
Subsystem *s = own->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(HeatSink::ClassDerivations))
{
work[workCount++] = (HeatSink *)s;
}
}
{
HeatSink *link = (HeatSink *)linkedSinks.Resolve();
if (link != NULL && workCount < kMaxWork)
{
work[workCount++] = link;
}
}
for (int w = 0; w < workCount; ++w)
{
if (fabs(coolantLevel) <= 1.0e-4f) // ran dry mid-pass
{
return;
}
HeatSink *sink = work[w];
if (sink->coolantFlowScale == 0.0f)
{
continue;
}
//
// squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0].
//
Scalar move = -coolantSquirtMass
* sink->coolantFlowScale
* time_slice;
Scalar lo = -coolantLevel;
if (move < lo) move = lo;
if (move > 0.0f) move = 0.0f;
//
// The heat delta riding the moved mass (computed BEFORE the level
// updates, as in the binary).
//
Scalar den = (fabs(sink->coolantLevel) > 1.0e-4f)
? sink->coolantLevel
: sink->thermalCapacity;
Scalar moved = (move < 0.0f) ? -move : move;
Scalar fracSink = moved / den;
Scalar fracRes = moved / coolantLevel;
Scalar heatDelta =
sink->heatEnergy * fracSink
- heatEnergy * fracRes;
coolantLevel += move; // the reservoir drains (move <= 0)
sink->coolantLevel -= move; // the sink gains
if (sink->coolantLevel >= 0.0f)
{
if (sink->coolantLevel > sink->thermalCapacity)
{
sink->coolantLevel = sink->thermalCapacity;
}
}
else
{
sink->coolantLevel = 0.0f;
}
Scalar cap = sink->thermalMass * startingTemperature;
if (heatDelta > cap)
{
heatDelta = cap;
}
Scalar chill = -heatDelta;
if (chill > 0.0f)
{
chill = 0.0f; // the flush only ever COOLS
}
sink->pendingHeat += chill;
}
}
//===========================================================================//
// File: reservr.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: Reservoir -- the coolant store //
//---------------------------------------------------------------------------//
// 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(RESERVR_HPP)
# include <reservr.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(MECHWEAP_HPP)
# include <mechweap.hpp>
#endif
#include <math.h>
Derivation
Reservoir::ClassDerivations(
HeatSink::ClassDerivations,
"Reservoir"
);
//
// The cockpit coolant-flush button (id 4, "InjectCoolant"); ToggleCooling
// (id 3) is inherited from the HeatSink chain.
//
const Reservoir::HandlerEntry
Reservoir::MessageHandlerEntries[]=
{
MESSAGE_ENTRY(Reservoir, InjectCoolant)
};
Reservoir::MessageHandlerSet
Reservoir::MessageHandlers(
ELEMENTS(Reservoir::MessageHandlerEntries),
Reservoir::MessageHandlerEntries,
HeatSink::MessageHandlers
);
Reservoir::SharedData
Reservoir::DefaultData(
Reservoir::ClassDerivations,
Reservoir::MessageHandlers,
HeatSink::AttributeIndex,
Subsystem::StateCount
);
//
//#############################################################################
// The coolant store (binary ctor @4af408): CoolantCapacity overlays the
// inherited HeatSink thermalCapacity slot; the charge starts full; the flush
// flow scale is zero (a reservoir never conducts like an ordinary sink). A
// master reservoir registers the CoolantSimulation performance.
//
// Deferred with the AggregateHeatSink wave: the authentic master path also
// attaches the reservoir into the central bank's radiator loop and rescales
// the capacity by 0.05 x the bank's heat-sink count (the central sink here is
// still a plain HeatSink, which has no count) -- until then the streamed
// capacity is used as-is (a larger tank than authentic; noted).
//#############################################################################
//
Reservoir::Reservoir(
Mech *owner,
int subsystem_ID,
SubsystemResource *r,
SharedData &shared_data
):
HeatSink(owner, subsystem_ID, r, shared_data)
{
Check(owner);
Check_Pointer(r);
reservoirAlarm.Initialize(2);
squirtEfficiency = 0.5f;
thermalCapacity = r->coolantCapacity;
coolantSquirtMass = r->coolantSquirtMass;
coolantLevel = thermalCapacity;
coolantFlowScale = 0.0f;
injectAccumulator = 0.0f;
reservoirAlarm.SetLevel(0);
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[resv] '" << GetName()
<< "' capacity=" << thermalCapacity
<< " squirtMass=" << coolantSquirtMass << endl << flush;
}
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Reservoir::CoolantSimulation);
//
// The authentic master path scales the tank by the bank's aggregate
// heat-sink count: capacity = 0.05 x count x streamed (binary
// @4af408, CoolantCapacityScale byte-verified 0.05). The linked sink
// (from the resource) IS the bank. (The bank-side Attach that routes
// its DrawCoolant top-ups here joins the attach wave.)
//
HeatSink *link = (HeatSink *)linkedSinks.Resolve();
if (link != NULL
&& link->IsDerivedFrom(AggregateHeatSink::ClassDerivations))
{
Scalar masterScale =
(Scalar)((AggregateHeatSink *)link)->GetHeatSinkCount();
thermalCapacity = 0.05f * masterScale * thermalCapacity;
coolantLevel = thermalCapacity;
if (getenv("BT_POWER_LOG"))
{
DEBUG_STREAM << "[resv] capacity rescaled by bank count -> "
<< thermalCapacity << endl << flush;
}
}
}
Check_Fpu();
}
Reservoir::~Reservoir()
{
}
Logical
Reservoir::TestClass(Mech &)
{
return True;
}
Logical
Reservoir::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// DrawCoolant -- the SOURCE (binary @4af3b0): hand out up to coolantLevel of
// the requested amount and deduct it from the charge.
//#############################################################################
//
Scalar
Reservoir::DrawCoolant(Scalar requested)
{
Check(this);
Scalar supplied = 0.0f;
if (requested >= 0.0f)
{
supplied = requested;
if (coolantLevel < requested)
{
supplied = coolantLevel;
}
}
coolantLevel -= supplied;
return supplied;
}
//
//#############################################################################
// InjectCoolantMessageHandler -- the cockpit coolant-flush button (binary
// @4aee70, id 4): novice-locked. Release drops the inject alarm (flush OFF);
// press raises it when the tank holds charge (the flush-cloud effect joins
// the psfx wave) and zeroes the elapsed accumulator.
//#############################################################################
//
void
Reservoir::InjectCoolantMessageHandler(ReceiverDataMessageOf<int> *message)
{
Check(this);
Check(message);
if (NoviceLockout())
{
return;
}
if (message->dataContents < 1)
{
reservoirAlarm.SetLevel(0); // release -> flush OFF
}
else
{
if (reservoirAlarm.GetLevel() != 1 && coolantLevel > 0.0f)
{
reservoirAlarm.SetLevel(1); // flush ON
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[resv] FLUSH ON via button (charge="
<< coolantLevel << ")" << endl << flush;
}
}
injectAccumulator = 0.0f;
}
ForceUpdate();
}
//
//#############################################################################
// CoolantSimulation -- the registered Performance (binary @4aef78): while
// injection is active, accumulate elapsed time and run the coolant
// distribution. The InjectCoolant COCKPIT BUTTON (the id-4 message handler
// that raises the alarm) joins with the cockpit-button message wave; the DEV
// hook BT_FORCE_FLUSH=1 raises it here so the flush machinery is exercisable
// headlessly (one press ~4 s after the sim starts ticking).
//#############################################################################
//
void
Reservoir::CoolantSimulation(Scalar time_slice)
{
Check(this);
{
static int forceFlush = -1;
if (forceFlush < 0)
{
forceFlush = (getenv("BT_FORCE_FLUSH") != NULL) ? 1 : 0;
}
if (forceFlush == 1)
{
static Scalar armAccum = 0.0f;
armAccum += time_slice;
if (armAccum >= 4.0f && reservoirAlarm.GetLevel() != 1
&& coolantLevel > 0.0f)
{
forceFlush = 2;
reservoirAlarm.SetLevel(1);
injectAccumulator = 0.0f;
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[resv] FLUSH ON (charge="
<< coolantLevel << ")" << endl << flush;
}
}
}
}
if (reservoirAlarm.GetLevel() == 1)
{
injectAccumulator += time_slice;
InjectCoolant(time_slice);
}
}
//
//#############################################################################
// InjectCoolant -- the coolant-flush distribution (binary @4aefa4). Walk the
// roster gathering the flush targets in the authentic pass order -- the
// condensers, then the weapons, then every heat sink, then the linked master
// (the duplicate visits are intentional weighting) -- and squirt
// (coolantSquirtMass x the target's coolantFlowScale x dt) into each,
// crediting a negative pending-heat chill for the moved mass. Squirts only
// ever leave the tank; the chill only ever cools.
//#############################################################################
//
void
Reservoir::InjectCoolant(Scalar time_slice)
{
Check(this);
if (fabs(coolantLevel) <= 1.0e-4f) // the tank is empty
{
return;
}
enum { kMaxWork = 96 };
HeatSink *work[kMaxWork];
int workCount = 0;
Entity *own = (Entity *)owner;
int count = own->GetSubsystemCount();
int i;
for (i = 2; i < count && workCount < kMaxWork; ++i)
{
Subsystem *s = own->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations))
{
work[workCount++] = (HeatSink *)s;
}
}
for (i = 2; i < count && workCount < kMaxWork; ++i)
{
Subsystem *s = own->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(MechWeapon::ClassDerivations))
{
work[workCount++] = (HeatSink *)s;
}
}
for (i = 2; i < count && workCount < kMaxWork; ++i)
{
Subsystem *s = own->GetSubsystem(i);
if (s != NULL && s->IsDerivedFrom(HeatSink::ClassDerivations))
{
work[workCount++] = (HeatSink *)s;
}
}
{
HeatSink *link = (HeatSink *)linkedSinks.Resolve();
if (link != NULL && workCount < kMaxWork)
{
work[workCount++] = link;
}
}
for (int w = 0; w < workCount; ++w)
{
if (fabs(coolantLevel) <= 1.0e-4f) // ran dry mid-pass
{
return;
}
HeatSink *sink = work[w];
if (sink->coolantFlowScale == 0.0f)
{
continue;
}
//
// squirt = -(squirtMass x flowScale x dt), clamped to [-coolantLevel, 0].
//
Scalar move = -coolantSquirtMass
* sink->coolantFlowScale
* time_slice;
Scalar lo = -coolantLevel;
if (move < lo) move = lo;
if (move > 0.0f) move = 0.0f;
//
// The heat delta riding the moved mass (computed BEFORE the level
// updates, as in the binary).
//
Scalar den = (fabs(sink->coolantLevel) > 1.0e-4f)
? sink->coolantLevel
: sink->thermalCapacity;
Scalar moved = (move < 0.0f) ? -move : move;
Scalar fracSink = moved / den;
Scalar fracRes = moved / coolantLevel;
Scalar heatDelta =
sink->heatEnergy * fracSink
- heatEnergy * fracRes;
coolantLevel += move; // the reservoir drains (move <= 0)
sink->coolantLevel -= move; // the sink gains
if (sink->coolantLevel >= 0.0f)
{
if (sink->coolantLevel > sink->thermalCapacity)
{
sink->coolantLevel = sink->thermalCapacity;
}
}
else
{
sink->coolantLevel = 0.0f;
}
Scalar cap = sink->thermalMass * startingTemperature;
if (heatDelta > cap)
{
heatDelta = cap;
}
Scalar chill = -heatDelta;
if (chill > 0.0f)
{
chill = 0.0f; // the flush only ever COOLS
}
sink->pendingHeat += chill;
}
}
+23 -1
View File
@@ -30,11 +30,33 @@ Derivation
"Sensor"
);
//
//#############################################################################
// Attribute table -- the AUTHENTIC surviving SENSOR.HPP enum
// (RadarPercent/SelfTest/BadVoltage off PoweredSubsystem::NextAttributeID =
// 0x0F). The cockpit map() binds "Avionics/RadarPercent".
//#############################################################################
//
const Sensor::IndexEntry
Sensor::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Sensor, RadarPercent, radarPercent),
ATTRIBUTE_ENTRY(Sensor, SelfTest, selfTest),
ATTRIBUTE_ENTRY(Sensor, BadVoltage, badVoltage)
};
Sensor::AttributeIndexSet
Sensor::AttributeIndex(
ELEMENTS(Sensor::AttributePointers),
Sensor::AttributePointers,
PoweredSubsystem::AttributeIndex
);
Sensor::SharedData
Sensor::DefaultData(
Sensor::ClassDerivations,
Subsystem::MessageHandlers,
Subsystem::AttributeIndex,
Sensor::AttributeIndex,
Subsystem::StateCount
);
@@ -439,3 +439,39 @@ tables -- lights half the panels), add BTPlayer::GetKillCount() and the
ammoBinLink accessor, then the VISUAL pass on the rig (emulator VPXLOG=1 +
VDB head windows / pod-launch --layout explode) diffing against the shipped
exe on the same GAUGE content.
## 5.3.27 -- THE ATTRIBUTE WAVE (48/50 cockpit bindings LIVE)
The heat/power/sensor/mech attribute rows are published, so the gauges read
the real simulation instead of the zero cell. THE NUMBERING IS PINNED:
the chain publishes IDs 2..0x0E so `PoweredSubsystem::NextAttributeID`
lands on the AUTHENTIC 0x0F -- the surviving SENSOR.HPP numbers
RadarPercent off it, and MechWeapon's binary-pinned table starts its real
IDs at 0x12 (its pads therefore shrank 16 -> 3, bridging 0x0F..0x11 exactly
as the header comment predicted). Do not renumber without re-pinning both.
HeatableSubsystem 2..5 CurrentTemperature / HeatLoad /
DegradationTemperature / FailureTemperature
HeatSink 6..9 CoolantMass / CoolantCapacity /
CoolantMassLeakRate / CoolantAvailable
Condenser 0x0A ValveSetting (sibling branch)
PoweredSubsystem 0x0A-0E InputVoltage / OutputVoltage / RatedVoltage /
VoltageState / ConnectMode -> Next = 0x0F
Generator 0x0A-0C OutputVoltage / RatedVoltage / GeneratorNumber
(sibling branch off HeatSink)
Sensor 0x0F-11 RadarPercent / SelfTest / BadVoltage
(the AUTHENTIC surviving enum, now defined)
Mech (entity) Radar{Range,LinearPosition,AngularPosition} /
LinearSpeed / MaxRunSpeed / DuckState
Also rechained so the whole family is visible where the cfg expects it:
MechWeapon -> PoweredSubsystem::AttributeIndex (weapons expose temps),
Sensor -> PoweredSubsystem, Reservoir + AggregateHeatSink -> HeatSink.
VERIFIED (BT_GAUGE_ATTR_LOG=1 on gaugeattr.conf): 48 OK / 2 NULL, up from
33/17 and 0/50 at the block's birth. The two remaining have NO member to
bind: HeatSink/AmbientTemperature (the radiator's ambient is a sim constant,
not state) and Searchlight/LightOn (Searchlight is a memberless
PowerWatcher subclass) -- both fall to the zero cell and draw static, which
is correct until those bricks exist. Gauge fight + smoke + novice all
clean.