Files
BT411/game/reconstructed/myomers.cpp
arcattackandClaude Fable 5 dd27238ceb Gauges: the CLOSEOUT wave -- radar pips, Myomers dedup, attribute parity, ledger sweep (tasks #14-#17)
TASK #17 -- AUTHENTIC RADAR SYMBOLOGY (the cross-blip stand-in retired).
The recon proved the 'missing pip raster set' never existed as BT game code:
pips are the ENGINE's L4GaugeImage vector-stroke system (T0 source in tree,
L4GAUIMA.cpp == FUN_0046f0c0 line-for-line), the 'pip table' is L4Warehouse::
gaugeImageBin keyed by Entity::resourceID, and BTL4.RES ships 110 type-0x12
shapes (every mech/vehicle/building/tree).  The six btl4rdr stubs are wired
to the real facilities: contacts draw their authentic model silhouettes with
LOD selection, the target gets the binary's 4px-inflated box highlight, and
player-name labels resolve via Mission::GetSmallNameBitmap (the prebuilt
64x16 egg rasters keyed by the player's bitmapindex).  DECODED: the invented
'VideoObject' was Entity::owningPlayer all along (+0x190; nameID =
playerBitmapIndex@0x1E0, target = BTPlayer::objectiveMech@0x284 -- new
bridge BTPlayerObjectiveMechOf); the 'LabelledEntity' class is Landmark
(cultural.h; label path dormant -- no landmark content ships, no runtime
landmarkID writer).  The L4GREND BT_DEV_GAUGES warehouse guard is removed
(its AV had a different culprit, below); resource type 18 corrected to
GaugeImageStream in decomp-reference (was mislabeled 'ModelList').

TASK #14 -- the Myomers ODR duplicate ELIMINATED: the powersub.cpp/hpp
'Myomers' (classID 0xBC3 -- actually Sensor) is retired whole; it duplicated
?DefaultData@Myomers@@ against the real class (dumpbin-verified) and /FORCE
picked the winner by link order.  The real Myomers (0xBC6) now chains
PoweredSubsystem's handler set (ids 4-8) and publishes its SEVEN binary
attributes (@00511588: SpeedEffect/Current/Recommended/Min/MaxSeekVoltage-
Index/SeekVoltage/OutputVoltage) -- the old empty unchained index starved
the Myomer engineering panel of every resolve.

TASK #16 -- attribute parity: MechWeapon publishes the FULL binary table
@0x511890 (11 entries; ids renumbered to binary truth -- the port aliases
had squatted the binary's DistanceToTarget/TargetWithinRange ids; the
streamed TriggerState 0x13 binding unchanged).  Binary names resolved two
TODO members: pipState -> estimatedReadyTime (attr 0x1A), and the EXT-model
flag is the binary's RearFiring (0x1B).  ThermalSight LightState published.
HUD (offset conflicts) + missile-side tables (id encoding suspect)
documented for a re-dump instead of publishing blind.
  THE CRASH THIS EXPOSED [T2, cdb-verified]: gotcha #11's dense-table gap
is a LATENT AV, not a guaranteed one -- the old table's 0x0D..0x12 gap
(task #5) survived on heap luck; the renumber reshuffled allocations and
Find() AV'd on a garbage entryName in WeaponCluster's PercentDone resolve.
Fixed with five named PAD entries (the mech.cpp attrPad idiom) + a
static_assert locking the pad base to PoweredSubsystem::NextAttributeID.
Gotcha #11 amended with the proof.

TASK #15 -- stale-ledger sweep: GAUGE_COMPOSITE ('composite not yet built',
Reservoir shadow, PlayerStatus/vehicleSubSystems 'remaining', valve-dormant-
until-0xBD3, sensor guard, the superseded 'Heat MFD near-static' reframe --
all banner-corrected), gauges-hud frontmatter, L4VB16 + powersub comments.

Verified live: 50/50 config attribute bindings resolve, 0 NULLs, 0 parse
skips, mech spawns and simulates 31/31 subsystems, no cross-blip fallbacks,
the pip cache fills through entity registration without the old guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:57:45 -05:00

590 lines
28 KiB
C++

//===========================================================================//
// File: myomers.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Myomers subsystem implementation (mech artificial-muscle drive). //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from BTL4OPT.EXE. No source or header survived; this file is
// rebuilt from the decompiled cluster @0x4b8a48 .. @0x4b95b8 (Ghidra
// pseudo-C in recovered/all/part_013.c), the vtable @005117dc, the attribute
// IndexEntry table @00511588 and the string pool @00511660. Each method
// carries its @ADDR and the binary evidence used.
//
// --------------------------------------------------------------------------
// LOCATION / IDENTITY (all proven):
// Class name "Myomers" string @0051166d
// ClassID 0x0BC6 stamped by CSS @004b9140 (resource +0x20)
// model size 0x1B0 stamped by CSS @004b9140 (resource +0x24)
// object size 0x358 Mech factory case 0xBC6 (part_012.c:10069)
// vtable @005117dc set by ctor @004b8fec
// ctor @004b8fec dtor @004b9114
// CSS @004b9140 DefaultData @0051154c
// base PoweredSubsystem (ctor @004b0f74 called with &DAT_0051154c)
//
// CORRECTION vs powersub.cpp: powersub's "Myomers" (vtable 0050fb0c / ctor
// 4b1d18 / classID 0xBC3, members outputVoltage/powered/voltageAvailable)
// was a MISLABEL of **Sensor** (now sensor.cpp). Retire that naming. The
// real Myomers is here, classID 0xBC6. See CLASSMAP.md.
// --------------------------------------------------------------------------
//
// CONFIDENCE
// confident : class identity, base, ClassID/size, vtable slot mapping,
// member layout (every member is backed by the @00511588
// attribute table or by a ctor store), resource layout
// (every field is backed by a CSS parse @004b9140),
// ctor/dtor/CSS/HandleMessage/ResetToInitialState/
// AvailableOutput/RegisterMaxOutput/HasAdequateVoltage/
// ConnectToMover/DisconnectFromMover bodies.
// best-effort : MyomersSimulation (@004b8d18) is the per-tick Performance
// (only reachable via function pointer; SetPerformance call
// site not captured -- see note); GetSpeedReading (@004b8f94,
// slot 15) purpose; ToggleSeekVoltage (@004b8a48) not present
// in the decompiled shards; the heat-base field @0x150 and
// graphic-state block (this[7..9]) inherit-pattern stores.
// excluded : @0x4b7f94..@0x4b881c (gimbal / torso-horizon display, base
// 4b18a4, DAT_00511228) -- adjacent cluster, NOT Myomers.
// @0x4b95ec MechWeapon abstract-sim trap -- next class.
// All inherited PoweredSubsystem/HeatSink/Subsystem vtable
// slots (1-8, 11-14) are inherited, not redefined here.
//
#include "myomers.hpp" // FIRST: neutralizes powersub.hpp's stale 'Myomers'
#include <bt.hpp> // engine aggregate: NotationFile / NameList / Generator / ...
#pragma hdrstop
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
#include <math.h> // fabs (speed-gauge magnitude)
//===========================================================================
// Shared Data Support (DefaultData @0051154c, ClassDerivations near it)
//
// Standard 4-arg Subsystem::SharedData boilerplate (cf. sensor.cpp):
// ClassDerivations / MessageHandlers / AttributeIndex are defined BEFORE
// DefaultData, which takes their address/reference.
//===========================================================================
Derivation
Myomers::ClassDerivations(
PoweredSubsystem::GetClassDerivations(), // returns Derivation* (no &)
"Myomers"
);
// task #14: the REAL shared data. The old empty static
// MessageHandlers/AttributeIndex meant (a) no inherited message handlers
// (the powered ids 4-8 among them), (b) an EMPTY UNCHAINED attribute index --
// even CurrentTemperature/InputVoltage resolved NULL on Myomers instances, so
// the Myomer engineering panel's cluster children were silently blank. Both
// now chain PoweredSubsystem's sets, and the SEVEN binary attributes
// (IndexEntry table @00511588) are published. Function-local statics per the
// static-init-order rule (reconstruction-gotchas #9).
Receiver::MessageHandlerSet&
Myomers::GetMessageHandlers()
{
static Receiver::MessageHandlerSet messageHandlers(
PoweredSubsystem::GetMessageHandlers()); // copy-inherit (ids 4-8)
return messageHandlers;
}
Myomers::AttributeIndexSet&
Myomers::GetAttributeIndex()
{
static const Myomers::IndexEntry entries[]=
{
ATTRIBUTE_ENTRY(Myomers, SpeedEffect, speedEffect), // 0x12 @0x31C
ATTRIBUTE_ENTRY(Myomers, CurrentSeekVoltageIndex, currentSeekVoltageIndex), // 0x13 @0x320
ATTRIBUTE_ENTRY(Myomers, RecommendedSeekVoltageIndex, recommendedSeekVoltageIndex), // 0x14 @0x324
ATTRIBUTE_ENTRY(Myomers, MinSeekVoltageIndex, minSeekVoltageIndex), // 0x15 @0x328
ATTRIBUTE_ENTRY(Myomers, MaxSeekVoltageIndex, maxSeekVoltageIndex), // 0x16 @0x32C
ATTRIBUTE_ENTRY(Myomers, SeekVoltage, seekVoltage), // 0x17 @0x330 (array base)
ATTRIBUTE_ENTRY(Myomers, OutputVoltage, outputVoltage) // 0x18 @0x344
};
static Myomers::AttributeIndexSet attributeIndex(
ELEMENTS(entries), entries,
PoweredSubsystem::GetAttributeIndex()
);
return attributeIndex;
}
Myomers::SharedData
Myomers::DefaultData(
&Myomers::ClassDerivations,
Myomers::GetMessageHandlers(),
Myomers::GetAttributeIndex(),
Myomers::StateCount
);
//===========================================================================
// Recovered float constants (section_dump.txt)
// _DAT_004b8b94 = 0.0f _DAT_004b8b98 = 1.0f
// _DAT_004b8ee4 = 0.5f _DAT_004b8ee8 = 0.0f _DAT_004b8eec = 1.0f
// _DAT_004b9110 = -1.0f (SeekVoltage list sentinel)
// _DAT_004b9470 = -1.0f (efficiency "missing" sentinel)
//===========================================================================
//***************************************************************************
// Myomers::Myomers @004b8fec
//***************************************************************************
//
// Chains to PoweredSubsystem::PoweredSubsystem (@004b0f74) with the Myomers
// DefaultData (&DAT_0051154c), then installs vtable @005117dc and builds the
// drive table. Field-store evidence (this[idx] -> byte offset):
//
// this[199]=0x3f800000 speedEffect@0x31C = 1.0f
// this[0xd2]=fail-degrade heatRange@0x348 = failureTemp(@0x11C) - degradationTemp(@0x118)
// this[0xd3]=range*range heatRangeSquared@0x34C = heatRange * heatRange
// this[0xd4]=res+0x190 velocityEfficiency@0x350
// this[0xd5]=res+0x194 accelerationEfficiency@0x354
// this[0x54]=0x3e19999a heat-base field @0x150 = 0.15f (best-effort; inherited HeatSink coeff)
// this[10]|=8 Subsystem flags @0x28 |= 8
// (cond) this[7..9] = graphic-state PTR_LAB_00511620 / DAT_00511624 / DAT_00511628
// when (flags@0x28 & 0xC)==0 && (flags & 0x100)!=0 (inherited render hook)
//
// seek-voltage table: source = FUN_00417ab4(this+0x1D0) resolves the
// VoltageSource connection to its Generator; the resource SeekVoltage[]
// fractions (res+0x198) are multiplied by the Generator's ratedVoltage
// (source+0x1D8) and stored into seekVoltage[]@0x330, stopping at the -1.0
// sentinel. maxSeekVoltageIndex@0x32C = (count-1).
// recommendedSeekVoltageIndex@0x324 = res+0x1AC; currentSeekVoltageIndex@0x320
// is seeded to the same; minSeekVoltageIndex@0x328 = 0.
//
Myomers::Myomers(
Mech *owner,
int subsystem_ID,
SubsystemResource *resource,
SharedData &shared_data)
: PoweredSubsystem(owner, subsystem_ID, resource, shared_data) // @004b0f74, &DAT_0051154c
{
// vtable @005117dc installed by the compiler here.
speedEffect = 1.0f; // @0x31C
heatRange = failureTemperature - degradationTemperature; // @0x348 (this[0x11C]-this[0x118])
heatRangeSquared = heatRange * heatRange; // @0x34C
velocityEfficiency = resource->velocityEfficiency; // @0x350 res+0x190
accelerationEfficiency = resource->accelerationEfficiency; // @0x354 res+0x194
// (heat-base default coefficient @0x150 = 0.15f; inherited HeatSink slot)
// (Subsystem flags @0x28 |= 8; optional graphic-state hook -- see header note)
// (WAVE 6 de-shim) the owner*/damageStructure shim backing fields are gone --
// their accessors return neutral defaults directly. The mover handle lives at
// base+0x110 (not a Myomers own field); the coupling is inert, so no init here.
// Register the per-tick Performance. The binary ctor @004b8fec stores the
// MyomersSimulation pointer-to-member into activePerformance (this[7..9] =
// PTR_LAB_00511620 / DAT_00511624 / DAT_00511628) under the live-master
// guard (owner segment flags & 0xC)==0 && (flags & 0x100) -- the exact same
// shape as the Gyroscope/Torso ctors (gyro this[7..9]=PTR_FUN_0050fe08,
// torso this[7..9]=PTR_FUN_00510c10). The earlier reconstruction mislabeled
// this store a "graphic-state hook" and omitted the registration, leaving
// activePerformance at the DoNothingOnce default (drops out after frame 1).
// (WAVE 6) the segmentFlags shim is removed; the gate reads OWNER
// simulationFlags (param_2+0x28) directly -- the oracle-verified source.
if ((owner->simulationFlags & SegmentCopyMask) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 (binary @004b8fec)
{
SetPerformance(&Myomers::MyomersSimulation); // this[7..9] = &MyomersSimulation
}
// Resolve the powering Generator (PoweredSubsystem::ResolveVoltageSource,
// FUN_00417ab4(this+0x1D0)) and build the seek-voltage drive table by
// scaling each resource fraction by the Generator's rated voltage.
Generator *source = (Generator *)ResolveVoltageSource(); // this+0x1D0 -> Generator
Scalar srcRated = (source != 0) ? source->ratedVoltage : 0.0f; // source+0x1D8
maxSeekVoltageIndex = -1;
int count = 0;
while (count < 5) {
if (resource->seekVoltage[count] == -1.0f /*_DAT_004b9110*/) {
maxSeekVoltageIndex = count - 1; // @0x32C
break;
}
seekVoltage[count] = resource->seekVoltage[count]
* srcRated; // @0x330[count] (source+0x1D8)
++count;
}
if (maxSeekVoltageIndex < 0)
maxSeekVoltageIndex = count - 1; // list filled all 5 slots
recommendedSeekVoltageIndex = resource->seekVoltageRecommendedIndex; // @0x324 res+0x1AC
currentSeekVoltageIndex = recommendedSeekVoltageIndex; // @0x320
minSeekVoltageIndex = 0; // @0x328
}
//***************************************************************************
// Myomers::~Myomers @004b9114
//***************************************************************************
// vtable slot 0. Thin; chains to the PoweredSubsystem chain.
Myomers::~Myomers()
{
}
//***************************************************************************
// Myomers::CreateStreamedSubsystem @004b9140
//***************************************************************************
//
// Chains to PoweredSubsystem::CreateStreamedSubsystem (@004b13ac) for the
// shared electrical/thermal fields, then:
// * stamps resource+0x20 = 0x0BC6 (MyomersClassID),
// resource+0x24 = 0x1B0 (streamed model size).
// * on first pass (passes==1) initialises VelocityEfficiency / Acceleration-
// Efficiency (res+0x190/+0x194) to -1.0, the 5 SeekVoltage slots
// (res+0x198..) to -1.0, and SeekVoltageRecommendedIndex (res+0x1AC) to -1.
// * parses "VelocityEfficiency" -> res+0x190 (missing/-1.0 -> error)
// * parses "AccelerationEfficiency" -> res+0x194 (missing/-1.0 -> error)
// * parses the "SeekVoltage" notation list: each child either sets
// "SeekVoltageRecommendedIndex" (res+0x1AC) or appends a SeekVoltage
// fraction (res+0x198..); a missing list or missing recommended index is
// an error.
//
int Myomers::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes)
{
if (!PoweredSubsystem::CreateStreamedSubsystem(model_file, model_name,
subsystem_name, resource, subsystem_file, directories, passes))
return False; // @004b13ac
resource->subsystemModelSize = sizeof(*resource); // resource +0x24 (0x1B0)
resource->classID = (RegisteredClass::ClassID)0x0BC6; // resource +0x20 (MyomersClassID)
if (passes == 1) {
resource->velocityEfficiency = -1.0f; // +0x190
resource->accelerationEfficiency = -1.0f; // +0x194
for (int i = 0; i < 5; ++i)
resource->seekVoltage[i] = -1.0f; // +0x198..
resource->seekVoltageRecommendedIndex = -1; // +0x1AC
}
// "VelocityEfficiency" / "AccelerationEfficiency" -- required scalars.
if (!subsystem_file->GetEntry(model_name, "VelocityEfficiency",
&resource->velocityEfficiency) // s_VelocityEfficiency_00511709
&& resource->velocityEfficiency == -1.0f) {
// Warning: "<subsystem> Missing VelocityEfficiency!" (0051171c)
return False;
}
if (!subsystem_file->GetEntry(model_name, "AccelerationEfficiency",
&resource->accelerationEfficiency) // s_AccelerationEfficiency_00511739
&& resource->accelerationEfficiency == -1.0f) {
// Warning: "<subsystem> Missing AccelerationEfficiency!" (00511750)
return False;
}
// "SeekVoltage" notation list: each child either sets the recommended index
// or appends a seek-voltage fraction (up to 5). (00511771)
NameList *list = subsystem_file->MakeEntryList(model_name, "SeekVoltage");
if (list == 0 && resource->seekVoltageRecommendedIndex == -1) {
// Warning: "<model> missing SeekVoltage " (0051177d)
} else if (list != 0) {
Scalar *out = resource->seekVoltage; // +0x198
Scalar *end = resource->seekVoltage + 5;
for (NameList::Entry *node = list->GetFirstEntry();
node != 0;
node = node->GetNextEntry())
{
if (strcmp(node->GetName(), "SeekVoltageRecommendedIndex") == 0) // 00511793
resource->seekVoltageRecommendedIndex = node->GetAtoi(); // -> +0x1AC
else if (out < end)
*out++ = (Scalar)node->GetAtof(); // append fraction
}
delete list;
if (resource->seekVoltageRecommendedIndex == -1) {
// Warning: "<model> missing SeekVoltageRecommendedIndex!" (005117af)
return False;
}
}
return True;
}
//***************************************************************************
// Myomers::AvailableOutput @004b8ac0
//***************************************************************************
//
// Core drive computation: returns the locomotion drive available for a given
// input voltage. (helper; called by RegisterMaxOutput and GetSpeedReading.)
//
// base = ownerMech[0x34C] * (input_voltage / seekVoltage[recommendedIndex])
// ^ Mech base-speed scalar ^ normalise to the recommended gear
//
// heatFactor (thermal degradation, mirrors the other PoweredSubsystem leaves):
// if currentTemp >= degradationTemp:
// if currentTemp >= failureTemp -> 0.0 (burned out)
// else d = currentTemp - degradationTemp;
// heatFactor = (base==0)? 1.0 : 1.0 - d*d/heatRangeSquared
// else -> 1.0 (cold, full drive)
//
// return (1.0 - damage[0x158]) * base * heatFactor;
// ^ accumulated damage/wear (this[0xE0]->structureLevel@0x158) bleeds off output.
//
Scalar Myomers::AvailableOutput(Scalar input_voltage)
{
Scalar base = OwnerBaseSpeed() // *(Mech@this[0xD0] + 0x34C)
* (input_voltage / seekVoltage[recommendedSeekVoltageIndex]); // @0x330[@0x324]
Scalar heatFactor;
if (degradationTemperature <= currentTemperature) { // 0x118 <= 0x114
if (failureTemperature <= currentTemperature) { // 0x11C <= 0x114
heatFactor = 0.0f;
} else {
Scalar d = currentTemperature - degradationTemperature;
heatFactor = (base == 0.0f)
? 1.0f
: (base - d * d * (base / heatRangeSquared)) / base; // == 1 - d*d/heatRangeSquared
}
} else {
heatFactor = 1.0f;
}
Scalar damage = DamageStructureLevel(); // *(this[0xE0] + 0x158)
return (1.0f - damage) * base * heatFactor; // _DAT_004b8b98 = 1.0
}
//***************************************************************************
// Myomers::RegisterMaxOutput @004b8ef0
//***************************************************************************
//
// Computes this myomer's full-throttle drive (AvailableOutput at the top gear
// seekVoltage[maxSeekVoltageIndex]) and raises the owner Mech's top-speed
// field (Mech[0x7A0]) to it. Called once per myomer while the Mech is being
// assembled (loop @part_012.c:10322 over the Mech's myomer list @0x1EB).
//
void Myomers::RegisterMaxOutput()
{
Scalar best = AvailableOutput(seekVoltage[maxSeekVoltageIndex]); // @0x330[@0x32C]
if (best > OwnerMaxSpeed()) // Mech[0x7A0]
SetOwnerMaxSpeed(best);
}
//***************************************************************************
// Myomers::GetSpeedReading slot 15 @004b8f94
//***************************************************************************
//
// PoweredSubsystem virtual slot 15 (base @004b1780). Best-effort: converts
// AvailableOutput(input_voltage) into a normalised gauge reading -- output is
// scaled by 3.6 (m/s -> km/h) and divided by 350 (gauge full scale), then run
// through FUN_004dd138 (fp magnitude helper / sqrt). Drives a cockpit
// speed gauge. (Decompiled body discards the result -> returned via FPU.)
//
Scalar Myomers::GetSpeedReading(Scalar input_voltage)
{
Scalar output = AvailableOutput(input_voltage); // @004b8ac0
return (Scalar)fabs((double)((output * 3.6f - 0.0f) / 350.0f)); // FUN_004dd138 (fp magnitude)
}
//***************************************************************************
// Myomers::HasAdequateVoltage slot 16 @004b8f3c
//***************************************************************************
//
// Override of PoweredSubsystem::IsSourceShorted (slot 16, base @004b0b5c).
// Returns True only when the (resolved) voltage source can supply at least
// the currently-selected seek voltage AND is in the Ready state (state==2).
//
// if (source == 0) source = ResolveVoltageSource(); // FUN_00417ab4(this+0x1D0)
// if (source == 0) return False;
// return seekVoltage[currentSeekVoltageIndex] <= source->outputVoltage
// && source->state == 2; // source+0x1DC, source+0x210
//
Logical Myomers::HasAdequateVoltage(Subsystem *source)
{
Generator *gen = source ? (Generator *)source
: (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1D0)
if (gen == 0)
return False;
if (seekVoltage[currentSeekVoltageIndex] <= gen->MeasuredVoltage() // @0x330[@0x320] <= src+0x1DC
&& gen->GeneratorStateOf() == Generator::GeneratorReady) // src+0x210 == 2
return True;
return False;
}
//***************************************************************************
// Myomers::MyomersSimulation Performance @004b8d18
//***************************************************************************
//
// Per-tick drive/heat integration. BEST-EFFORT (only reachable via the
// activePerformance function pointer; the SetPerformance call site was not
// captured in the shards -- registration is presumed in the un-decompiled
// ToggleSeekVoltage/activation path).
//
// Reads the owner Mech's motion state and the seek-voltage ratio, then -- when
// the sim-control "advanced damage" gate is on (FUN_004ad7d4: *(Mech[0xD0]+0x190)
// +0x260) -- accumulates mechanical-work heat into the HeatSink heat
// accumulator @0x1C8:
//
// v = |Mech velocity vector| (Mech +0x1C4/0x1C8/0x1CC, magnitude)
// a = |Mech accel/desired vector| (Mech +0x82C/0x830/0x834, magnitude)
// vDot= Mech[0x20C] * (dot of the velocity vector with itself) * _DAT_004b8ee4(0.5)
// ratio = seekVoltage[current]/seekVoltage[recommended], clamped >= 1.0
// damageGain = 1.0 + damage[0x158]
// velTerm = (1 - accelerationEfficiency) * |Vy| * Mech[0x20C] * *Mech[0x250] * dt
// accTerm = (1 - accelerationEfficiency) * |v| * |a| * Mech[0x20C] * dt
// work = (1 - velocityEfficiency) * vDot
// heat[0x1C8] += ratio*ratio * damageGain * (velTerm + work + accTerm)
//
// (Field roles in the work expression are best-effort; the structure -- heat
// rises with the square of the gear ratio, with damage, and with the
// inefficiency complements of VelocityEfficiency/AccelerationEfficiency -- is
// faithful to the decompiled arithmetic.)
//
void Myomers::MyomersSimulation(Scalar time_slice)
{
if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate
return;
Scalar velMag = OwnerVelocityMag(); // |Mech velocity| (+0x1C4/0x1C8/0x1CC)
Scalar accMag = OwnerDriveMag(); // |Mech drive vector| (+0x82C/0x830/0x834)
Scalar vy = OwnerVelocityY(); // Mech +0x1C8
Scalar k = OwnerMotionGain(); // Mech +0x20C
Scalar m = OwnerMass(); // *Mech[0x250]
Scalar velComplement = 1.0f - accelerationEfficiency; // @0x354
Scalar workComplement = 1.0f - velocityEfficiency; // @0x350
Scalar damageGain = 1.0f + DamageStructureLevel(); // this[0xE0]+0x158
Scalar ratio = seekVoltage[currentSeekVoltageIndex] // @0x330[@0x320]
/ seekVoltage[recommendedSeekVoltageIndex];// @0x330[@0x324]
if (ratio < 1.0f) ratio = 1.0f;
Scalar work = k * (velMag * velMag) * 0.5f; // dot(v,v)*0.5 == |v|^2*0.5 (_DAT_004b8ee4)
pendingHeat /* @0x1C8 */ +=
ratio * ratio * damageGain *
( velComplement * vy * k * m * time_slice
+ workComplement * work
+ velComplement * velMag * accMag * k * time_slice );
}
//***************************************************************************
// Myomers::HandleMessage slot 9 @004b8a8c
//***************************************************************************
// Thin override; forwards to PoweredSubsystem::HandleMessage (@004b0efc).
Logical Myomers::HandleMessage(int message)
{
return PoweredSubsystem::HandleMessage(message); // @004b0efc
}
//***************************************************************************
// Myomers::ResetToInitialState slot 10 @004b8aa4
//***************************************************************************
// Thin override; forwards to PoweredSubsystem::ResetToInitialState (@004b0e6c).
void Myomers::ResetToInitialState(Logical powered)
{
PoweredSubsystem::ResetToInitialState(powered); // @004b0e6c
}
//***************************************************************************
// Myomers::ConnectToMover (mislabeled)
//***************************************************************************
//
// ⚠ MISATTRIBUTION (task #5 correction): @004b9550/@004b95b8 are NOT Myomers
// functions -- the weapon message-table @0x511860 proves they are
// MechWeapon::ConfigureMappables (id 9) / ChooseButton (id 10), the weapon->
// button grouping handlers (the +0x31C coincidence: Myomers speedEffect vs
// MechWeapon TriggerState). The bodies below are retained as the port's
// Myomers stand-ins but carry no binary address.
//
// Attaches the live SpeedEffect output (@0x31C) into the Mech's JointedMover
// roster (the mover at **(Mech[0xD0]+0x128)). Driven by a connect message
// whose payload index is at message+0xC:
// index = abs(message->value) - 1;
// if (message->value < 1) { mover->Detach(index); this[0x110] = -1; }
// else { this[0x110] = 0; mover->Attach(index, &speedEffect, this, 10, 9, 0); }
// (mover vtable +0x38 = Attach, +0x3C = Detach.)
//
void Myomers::ConnectToMover(SubsystemMessage &message)
{
int index = abs(message.value) - 1; // message+0xC
if (message.value < 1) {
MoverDetach(index); // owner mover vtable +0x3C
/* base+0x110 = -1 (detached) -- inert, no base accessor yet */
} else {
/* base+0x110 = 0 (attached) -- inert, no base accessor yet */
MoverAttach(index, &speedEffect); // owner mover vtable +0x38, feed @0x31C
// (original args: index,&speedEffect,this,10,9,0)
}
}
//***************************************************************************
// Myomers::DisconnectFromMover (stand-in; no binary address)
//***************************************************************************
// Counterpart of ConnectToMover: detaches the SpeedEffect feed.
// if (message->value > 0)
// mover->Release(message->value, &speedEffect); // mover vtable +0x44
void Myomers::DisconnectFromMover(SubsystemMessage &message)
{
if (message.value > 0) { // message+0xC
MoverRelease(message.value, &speedEffect); // owner mover vtable +0x44, feed @0x31C
}
}
//***************************************************************************
// Myomers::ToggleSeekVoltage @004b8a48
//***************************************************************************
// Mappable command (attribute id 0x09 "ToggleSeekVoltage", bound in the
// @00511588 table to handler @004b8a48). Cycles currentSeekVoltageIndex
// @0x320 between minSeekVoltageIndex@0x328 and maxSeekVoltageIndex@0x32C.
// BEST-EFFORT: present in the binary but not captured in the decompiled
// shards (the address falls in the untagged gap 0x4b8837..0x4b8a8c).
void Myomers::ToggleSeekVoltage()
{
// cycle currentSeekVoltageIndex within [minSeekVoltageIndex, maxSeekVoltageIndex]
}
//===========================================================================//
// WAVE 6 -- compile-time OVERFLOW lock (the real class must fit the 0x358
// factory alloc). Like Sensor, the heat-leaf base (PoweredSubsystem:HeatSink)
// is NOT byte-exact to the binary, so we lock only the overflow bound, not the
// exact field offsets -- nothing reads Myomers at a raw offset today (the mover
// feed + gauge readouts are inert) and MyomersSimulation uses named members.
// The de-shim dropped the owner*/segmentFlags backing fields to make room.
//===========================================================================//
struct MyomersLayoutCheck
{
// BYTE-EXACT: with PoweredSubsystem byte-exact (ends 0x31C) and the phantom
// moverConnection tail removed, Myomers' own fields (ctor @004b8fec: speedEffect@0x31C
// .. accelerationEfficiency@0x354) land exactly at 0x31C..0x358.
static_assert(offsetof(Myomers, speedEffect) == 0x31C, "Myomers::speedEffect @0x31C (attr 0x12)");
static_assert(offsetof(Myomers, seekVoltage) == 0x330, "Myomers::seekVoltage @0x330 (attr 0x17)");
static_assert(sizeof(Myomers) == 0x358, "sizeof(Myomers) 0x358 (factory alloc, byte-exact)");
};
//===========================================================================//
// WAVE 6 factory bridge -- Myomers (factory case 0xBC6, "Actuator" stub).
// The real class at 0xBC6 (ctor @004b8fec) is Myomers (the mech's artificial-
// muscle drive); the factory built an Actuator RECON_SUBSYS stub. alloc 0x358.
// The subsystem constructs (resolves its Generator + builds the seek-voltage
// gear table) and ticks its real Performance, but is currently INERT w.r.t.
// locomotion/heat (no-op mover feed + the OwnerAdvancedDamage()==False sim gate)
// -- so it cannot regress the gait/drive. Authentic coupling = a follow-up.
//===========================================================================//
Subsystem *CreateMyomersSubsystem(Mech *owner, int id, void *seg)
{
return (Subsystem *) new (Memory::Allocate(0x358))
Myomers(owner, id, (Myomers::SubsystemResource *)seg);
}