Files
BT412/game/reconstructed/myomers.hpp
T
arcattackandClaude Opus 4.8 1356870e56 P7: byte-exact re-base of the CORE heat leaf (HeatSink/Condenser/Reservoir/Generator/Myomers)
The reconstruction modeled the binary's shared alarm/connection types with undersized
stand-ins, sliding every field above them low (the 72-byte auxScreenNumber gap). Fix the
foundational heat-leaf classes byte-exact + static_assert-lock them, from the ctor decomp:

Shared types corrected:
  * SubsystemConnection 4 -> 0xC  (binary link node FUN_004af9cf; FUN_00417ab4 derefs +8)
  * GaugeAlarm54 = 0x54           (real AlarmIndicator FUN_0041b9ec; STATUS level at +0x14,
                                   so subsystem+0x184 == heatAlarm+0x14 == GetLevel())
    WatcherGaugeAlarm now typedefs GaugeAlarm54 (Watcher branch locks stay valid).

Byte-exact + locked (ctor-verified):
  * HeatSink   heatEnergy@0x158 linkedSinks@0x164 heatAlarm@0x170 resource@0x1C4
               pendingHeat@0x1C8, sizeof 0x1D0  (@004adda0)
  * Condenser  valveState@0x1D0 condenserAlarm@0x1DC  (@004ae568)
  * Reservoir  reservoirAlarm@0x1D0 ... squirtEfficiency@0x22C, sizeof 0x230  (@4aef78)
  * Generator  stateAlarm@0x1FC, sizeof 0x250  (@004b225c)
  * Myomers    phantom moverConnection tail removed (fits 0x358)

Three systemic bug classes fixed (added to the checklist in CLAUDE.md / HARD_PROBLEMS.md):
  * alias field    - a subclass member re-declaring an inherited slot the ctor reuses
                     (Condenser refrigerationOutput==massScale@0x160; Reservoir
                     coolantCapacity==thermalCapacity@0x128) -> use the inherited name
  * alarm-interior - a value read at alarm+0x14 modeled as a separate member
                     (HeatSink heatState@0x184, Reservoir injectActive@0x1e4)
                     -> route to alarm.GetLevel()
  * phantom field  - a member past the object end (Generator shortFlag@0x25C is really
                     *(owner+0x190)+0x25c the msg-manager, @004b0efc; Myomers
                     moverConnection@0x110 a write-only base slot) -> remove it

Heat conduction now reads the REAL heatEnergy=1.34e7 (not garbage); combat DESTROYED-in-8,
0 crashes, heapcheck-clean through construction.

REMAINING (measured; a distinct larger task): making PoweredSubsystem byte-exact grows it
+0x98 and cascades into MechWeapon/Emitter/PPC/Sensor/Myomers -- all model the 0x54
AlarmIndicator with 4-byte ReconAlarm / 8-byte HeatAlarm stand-ins and are short +
phantom-tailed; retyping without byte-exacting them overflows the Emitter alloc (heap
corruption). PoweredSubsystem kept on HeatAlarm(8) stand-ins (marked) pending a
subsystem-tree ALARM UNIFICATION. See docs/HARD_PROBLEMS.md P7.

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

345 lines
16 KiB
C++

//===========================================================================//
// File: myomers.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Myomers subsystem -- the mech's artificial-muscle drive. //
// Converts available voltage into locomotion "speed effect" and //
// the mechanical work that feeds the heat model. //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BTL4OPT.EXE). NO header survived;
// every declaration below is recovered from the decompiled cluster
// @0x4b8a48 .. @0x4b95b8 (just before MechWeapon @0x4b95ec), the object
// vtable @005117dc, the Myomers attribute IndexEntry table @00511588, and
// the embedded string pool @00511660 ("Myomers", "ToggleSeekVoltage",
// "SpeedEffect", "CurrentSeekVoltageIndex", "RecommendedSeekVoltageIndex",
// "MinSeekVoltageIndex", "MaxSeekVoltageIndex", "SeekVoltage",
// "OutputVoltage", "VelocityEfficiency", "AccelerationEfficiency",
// "SeekVoltageRecommendedIndex"). See myomers.cpp for per-method @ADDR
// evidence. Field offsets in comments are byte offsets in the shipped
// object (runtime size 0x358, alloc'd by Mech factory case 0xBC6 @part_012).
//
// ===========================================================================
// IMPORTANT CORRECTION (supersedes the earlier powersub.cpp / powersub.hpp
// "Myomers" block):
//
// powersub.cpp/.hpp described a class it CALLED "Myomers" at vtable
// @0050fb0c / ctor @004b1d18 / classID 0x0BC3, with inferred members
// outputVoltage/powered/voltageAvailable. That was a MISLABEL. @004b1d18
// is **Sensor** (proven by the @0050fae0 string pool "Sensor / RadarPercent
// / SelfTest / BadVoltage" and the surviving SENSOR.HPP); it is now in
// sensor.cpp with classID 0x0BC3.
//
// The REAL Myomers is THIS class:
// vtable @005117dc (NB: PTR_LAB_005117dc)
// ctor @004b8fec
// dtor @004b9114
// CSS @004b9140 (stamps classID 0x0BC6, streamed model size 0x1B0)
// DefaultData @0051154c
// classID 0x0BC6 (Mech factory case 0xBC6, alloc 0x358)
// The "outputVoltage/powered/voltageAvailable" triad invented in powersub
// has NO string backing and must be retired. Myomers' real members are
// the SpeedEffect / SeekVoltage[] drive table below.
// ===========================================================================
//
// A Myomers IS-A PoweredSubsystem (powersub.hpp): it is driven from a
// Generator segment, spins up over StartTime, and tracks the same electrical
// state machine. On top of that it owns a discrete table of "seek voltages"
// (drive gears), selectable at run time, and reports the resulting locomotion
// "speed effect" both to the Mech's mover and to the heat model.
//
#if !defined(MYOMERS_HPP)
# define MYOMERS_HPP
#if !defined(POWERSUB_HPP)
//
// powersub.hpp STILL declares a stale class it calls "Myomers" (the mislabeled
// Sensor -- vtable 0050fb0c / ctor 4b1d18 / classID 0xBC3, resolved to Sensor in
// sensor.hpp + CLASSMAP.md, with inferred members outputVoltage/powered/
// voltageAvailable that have no string backing). The AUTHENTIC Myomers is
// classID 0xBC6, declared below. We may not edit powersub.hpp, so the stale
// token is neutralized for the duration of the include, then the real class is
// declared. CROSS-FAMILY: powersub.hpp should DELETE its stale 'Myomers'
// (it duplicates the Sensor now owned by sensor.hpp).
//
# define Myomers Myomers__powersub_stale
# include "powersub.hpp"
# undef Myomers
#endif
//##################### Forward Class Declarations #######################
class Mech;
//###########################################################################
// SubsystemMessage -- minimal local stand-in.
//
// CROSS-FAMILY: the engine subsystem-message type is not present in the
// reconstructed/engine headers available here. The mover connect/disconnect
// handlers only read the signed attachment index/handle at message+0xC, so a
// one-field stand-in lets this module compile. Real need: the BT
// SubsystemMessage class.
//
#if !defined(BT_SUBSYSTEM_MESSAGE_SHIM)
# define BT_SUBSYSTEM_MESSAGE_SHIM
struct SubsystemMessage
{
int value; // message+0xC : signed mover-attachment index/handle
};
#endif
//###########################################################################
//############################# Myomers #################################
//###########################################################################
//
// vtable @005117dc, ctor @004b8fec, dtor @004b9114, shared data @0051154c.
// RegisteredClass::MyomersClassID == 0x0BC6 (stamped by CreateStreamedSubsystem
// @004b9140 into resource +0x20; streamed model size 0x1B0 into resource +0x24).
//
class Myomers:
public PoweredSubsystem
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation ClassDerivations;
static SharedData DefaultData; // @0051154c
static Receiver::MessageHandlerSet MessageHandlers;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Streamed model resource.
//
// Extends the PoweredSubsystem resource (which ends at startTime @+0x18C).
// The myomer-specific fields begin at +0x190. Parsed by
// CreateStreamedSubsystem @004b9140; streamed model size is 0x1B0.
//
struct Myomers__SubsystemResource:
public PoweredSubsystem::SubsystemResource
{
Scalar velocityEfficiency; // +0x190 "VelocityEfficiency" (-1.0 == missing -> error)
Scalar accelerationEfficiency; // +0x194 "AccelerationEfficiency" (-1.0 == missing -> error)
Scalar seekVoltage[5]; // +0x198 "SeekVoltage" (fraction of rated; list ends at -1.0 sentinel)
int seekVoltageRecommendedIndex;// +0x1AC "SeekVoltageRecommendedIndex" (-1 == missing -> error)
};
typedef Myomers__SubsystemResource SubsystemResource;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support
//
// Recovered IndexEntry table @00511588 (each record {attribID, nameptr,
// byte_offset|1, 0}). ToggleSeekVoltage (id 0x09) is a mappable COMMAND
// bound to the toggle handler @004b8a48; the rest are data attributes.
// (PoweredSubsystem::NextAttributeID == 0x12, so SpeedEffect resolves to
// 0x12 and the enum below is contiguous.)
//
// ToggleSeekVoltage -> id 0x09 -> handler @004b8a48 (command)
// SpeedEffect -> id 0x12 -> @0x31C
// CurrentSeekVoltageIndex -> id 0x13 -> @0x320
// RecommendedSeekVoltageIndex -> id 0x14 -> @0x324
// MinSeekVoltageIndex -> id 0x15 -> @0x328
// MaxSeekVoltageIndex -> id 0x16 -> @0x32C
// SeekVoltage -> id 0x17 -> @0x330 (array base)
// OutputVoltage -> id 0x18 -> @0x344
//
public:
enum {
ToggleSeekVoltageCommandID = 0x09, // mappable command (not in the attribute id run)
SpeedEffectAttributeID = PoweredSubsystem::NextAttributeID, // 0x12
CurrentSeekVoltageIndexAttributeID, // 0x13
RecommendedSeekVoltageIndexAttributeID, // 0x14
MinSeekVoltageIndexAttributeID, // 0x15
MaxSeekVoltageIndexAttributeID, // 0x16
SeekVoltageAttributeID, // 0x17
OutputVoltageAttributeID, // 0x18
NextAttributeID // 0x19
};
private:
static const IndexEntry AttributePointers[]; // @00511588
protected:
static AttributeIndexSet AttributeIndex;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
Myomers(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
); // @004b8fec
~Myomers(); // @004b9114
static int
CreateStreamedSubsystem( // @004b9140
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Drive model.
//
// AvailableOutput() is the shared core: it converts an input voltage into
// the locomotion drive available, scaled by the Mech's base-speed factor,
// throttled by the heat-degradation curve, and attenuated by accumulated
// damage. RegisterMaxOutput() pushes the strongest myomer's full-throttle
// output to the Mech so the cockpit can show top speed.
//
public:
Scalar
AvailableOutput(Scalar input_voltage); // @004b8ac0 (helper, returns drive)
void
RegisterMaxOutput(); // @004b8ef0 (-> owner Mech max speed)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
// MyomersSimulation is the per-tick Performance: it integrates the work
// done driving the Mech's velocity/acceleration and accumulates the
// resulting heat (gated by the sim-control "advanced damage" flag).
//
public:
typedef void
(Myomers::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
MyomersSimulation(Scalar time_slice); // @004b8d18 (Performance)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (vtable @005117dc)
//
public:
Logical
HandleMessage(int message); // slot 9, @004b8a8c (-> PoweredSubsystem @004b0efc)
void
ResetToInitialState(Logical powered = True); // slot 10, @004b8aa4 (-> PoweredSubsystem @004b0e6c)
// slot 15 (PoweredSubsystem base @004b1780) -- normalised speed/gauge readout
// (output km/h scaled to 350): GetSpeedReading @004b8f94
Scalar
GetSpeedReading(Scalar input_voltage); // slot 15, @004b8f94 (name best-effort)
// slot 16 (PoweredSubsystem::IsSourceShorted base @004b0b5c) -- Myomers
// requires the source to supply at least the selected seek voltage
// AND be in the Ready (==2) state.
Logical
HasAdequateVoltage(Subsystem *source = 0); // slot 16, @004b8f3c (overrides IsSourceShorted)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Mover-connection message handlers (bound via the simulation message map;
// route the live SpeedEffect @0x31C into / out of the Mech's JointedMover
// roster at owner Mech[0x128]).
//
protected:
void
ConnectToMover(SubsystemMessage &message); // @004b9550 (attach SpeedEffect feed)
void
DisconnectFromMover(SubsystemMessage &message); // @004b95b8 (detach SpeedEffect feed)
// ToggleSeekVoltage command handler @004b8a48 (cycles currentSeekVoltageIndex;
// present in the binary, not captured in the decompiled shards -- best-effort).
void
ToggleSeekVoltage(); // @004b8a48 (best-effort)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CROSS-FAMILY compile shims (owning Mech / JointedMover roster / DamageZone).
//
// The drive and heat arithmetic below reads values that logically live on the
// owning Mech (base-speed, top-speed, velocity/acceleration, motion gain,
// mass, the sim-control "advanced damage" gate), on the Mech's JointedMover
// roster (Attach/Detach/Release of the SpeedEffect feed), and on this
// segment's DamageZone (structureLevel). None of those are reachable from a
// PoweredSubsystem here (owner is an incomplete Mech*, JointedMover exposes no
// roster API, and DamageZone is not in this hierarchy), so they are backed
// locally to keep the formulas intact and compiling. The real accessors
// belong on Mech / JointedMover / DamageZone -- see report CROSS-FAMILY NEEDS.
//
protected:
// WAVE 6 un-stub -- CROSS-FAMILY Mech/Mover/DamageZone couplings. These
// logically read the owning Mech's motion state, its JointedMover roster,
// and this segment's DamageZone -- none reachable from a PoweredSubsystem
// here (owner is an incomplete Mech*, JointedMover exposes no roster API,
// DamageZone is not in this hierarchy). To let the real class FIT its 0x358
// factory alloc, the shim BACKING FIELDS are dropped and the accessors
// return the neutral defaults the ctor used to prime, so the subsystem
// CONSTRUCTS + TICKS but is INERT w.r.t. locomotion/heat: the mover feed is
// a no-op and MyomersSimulation early-returns on OwnerAdvancedDamage()==False.
// ⚠ FAITHFUL FOLLOW-UP: redirect these to real Mech/JointedMover/DamageZone
// accessors + the advanced-damage gate (SubsystemMessageManager 0xBD3) so the
// mech actually slows when damaged/overheated and motion generates heat --
// that path DOES drive the live mover, so reconcile it with the gait cutover
// FIRST (both would feed the mover).
Scalar OwnerBaseSpeed() const { return 1.0f; } // Mech +0x34C (neutral)
Scalar OwnerMaxSpeed() const { return 0.0f; } // Mech +0x7A0
void SetOwnerMaxSpeed(Scalar) { } // no-op (real: write Mech top speed)
Scalar OwnerVelocityMag() const { return 0.0f; } // |Mech velocity|
Scalar OwnerDriveMag() const { return 0.0f; } // |Mech drive/accel|
Scalar OwnerVelocityY() const { return 0.0f; } // Mech velocity.y
Scalar OwnerMotionGain() const { return 0.0f; } // Mech +0x20C
Scalar OwnerMass() const { return 0.0f; } // *Mech +0x250
Logical OwnerAdvancedDamage() const { return False; } // advanced-damage gate (keeps sim INERT)
Scalar DamageStructureLevel() const{ return 0.0f; } // DamageZone +0x158
void MoverAttach(int index, Scalar *feed) { (void)index; (void)feed; } // JointedMover vtable +0x38 (no-op)
void MoverDetach(int index) { (void)index; } // JointedMover vtable +0x3C (no-op)
void MoverRelease(int handle, Scalar *feed) { (void)handle; (void)feed; } // JointedMover vtable +0x44 (no-op)
// Live-master gate constants (the ctor reads owner->simulationFlags).
enum {
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy
MasterHeatSinkFlag = 0x0100 // live-master segment bit
};
// The mover attachment handle lives at BASE+0x110 (a MechSubsystem slot), NOT a
// Myomers own field -- declaring it here appended a 4-byte tail that overflowed the
// 0x358 alloc once the PoweredSubsystem base became byte-exact. The coupling is
// inert (ConnectToMover -> no-op MoverAttach), so the handle is write-only today;
// when the real mover feed is revived, write base+0x110 via a base accessor.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data. Offsets are byte offsets into the shipped object.
//
protected:
Scalar speedEffect; // @0x31C attr 0x12; ctor 1.0f -- live drive fed to the mover
int currentSeekVoltageIndex; // @0x320 attr 0x13; ctor = recommended -- selected drive "gear"
int recommendedSeekVoltageIndex;// @0x324 attr 0x14; ctor = resource +0x1AC
int minSeekVoltageIndex; // @0x328 attr 0x15; ctor 0
int maxSeekVoltageIndex; // @0x32C attr 0x16; ctor = (seekVoltageCount - 1)
Scalar seekVoltage[5]; // @0x330 attr 0x17; ctor = resource fraction * source ratedVoltage
Scalar outputVoltage; // @0x344 attr 0x18; runtime output voltage
Scalar heatRange; // @0x348 ctor = failureTemperature - degradationTemperature
Scalar heatRangeSquared; // @0x34C ctor = heatRange*heatRange (degradation-curve denominator)
Scalar velocityEfficiency; // @0x350 resource +0x190
Scalar accelerationEfficiency; // @0x354 resource +0x194
friend struct MyomersLayoutCheck;
};
#endif