Files
BT411/game/reconstructed/myomers.cpp
T
Joe DiPrimaandClaude Opus 5 c28555bdc9 #96: TRACE THE CLOCK ORIGIN -- the pod ran at 28 fps (byte-proven); myomer heat re-bracketed to 14 Hz effective
epilectrik: "the issue seems to be related to a tick rate that advances the
heat accumulation... i need to trace out what the clock origin was."  Traced.

THE CLOCK ORIGIN [T1]: the DOS binary keeps its engine frame rate in the
global DAT_0052140c, set once at startup:
    0x401ace:  mov [0x52140c], 0x41E00000   = 28.0f     (nominal)
    0x401ada:  mov [0x52140c], 0x4191A6E0   = 18.2065f  (BIOS-tick fallback)
The DOS main pushes it straight into the ApplicationManager ctor (0x401189:
push [0x52140c]), and ~90 sites across the image fmul/fdiv by it -- it is THE
per-frame<->per-second conversion scalar of the whole 1995 engine.  The engine
executes every interesting master every frame and every executable subsystem
every entity tick (UPDATE.cpp / ENTITY.cpp, T0 -- no throttle at any layer),
so subsystem Performance cadence == this rate: the pod's myomer tick was
NOMINALLY 28 Hz.  My previous 30 Hz reference (the i860 BOARD frame) was
wrong by 7%, not the 2x the veterans hear.

THE RESIDUAL IS EFFECTIVE RATE, NOT NOMINAL: Oracle, on the halved build,
still asks "If you could please halve the myomer heat rate again.  We will
bracket to something reasonable."  A 486 host missing beats halves the real
Performance cadence without changing any constant -- and the 18.2 fallback
existing at all says slow paths were expected.  Statics cannot settle the
in-pod effective rate, so his bracket is the right instrument:

    effective default = 28 [T1 nominal] x 0.5 [T3 testimony calib] = 14 Hz
    BT_MYO_HZ=<hz> still overrides absolutely for bracketing
    the resolved rate now logs under BT_MYO_LOG, tier tags inline

Measured (Owens, seek 4, flat out): peak T 1007 -> ~640, degradation never
reached -- the halving Oracle requested, delivered as one auditable knob
instead of a silent constant edit.

KB: decomp-reference.md gains the DAT_0052140c section (~90 consumer sites --
whenever the decomp shows an unexplained mul/div by _DAT_0052140c it is
per-second<->per-frame at 28); btl4main.cpp's "30 = pod authentic" pacer note
corrected (30 kept deliberately for display smoothness, the divergence now
documented).  Candidate follow-up recorded: the frame-rate-dependent particle
trail density (rendering.md) should normalize against the same 28, which may
bear on Ronin's smoke-density report (#114).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:39:00 -05:00

934 lines
45 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); [slot 15 @004b8f94 RESOLVED
// issue #11: SeekVoltageResponse -- the SeekVoltageGraph
// sampler, sqrt not fabs]; 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()
{
// issue #19 (2026-07-21): register the binary's Myomers handler table
// @0x51158C {9, "ToggleSeekVoltage" -> @004b8a48} on top of the inherited
// powered ids -- the id was unregistered, so the ADV-mode seek-gear press
// was silently swallowed (Receiver::Receive NullHandler).
static const Receiver::HandlerEntry entries[]=
{
MESSAGE_ENTRY(Myomers, ToggleSeekVoltage) // id 9 @004b8a48
};
static Receiver::MessageHandlerSet messageHandlers(
ELEMENTS(entries), entries,
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. NB [0x511620] resolves to @004b8b9c,
// the WRAPPER (which calls PoweredSubsystemSimulation @0x4b0bd0 first and
// then the drive-heat integrator @004b8d18) -- our MyomersSimulation plays
// the wrapper's role and chains the base itself; see the note on its body.
// The binary ctor @004b8fec stores that
// 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
// #96 -- "each mech has a unique heating profile". Dump what THIS mech's
// myomer record actually streamed, so per-chassis variation is a measured
// fact rather than an assumption. One line per myomer built.
if (getenv("BT_MYO_LOG"))
{
DEBUG_STREAM << "[myoparm] " << (GetName() ? GetName() : "?")
<< " velEff=" << velocityEfficiency
<< " accEff=" << accelerationEfficiency
<< " gears=[";
for (int g = 0; g <= maxSeekVoltageIndex && g < 5; ++g)
DEBUG_STREAM << (g ? "," : "") << seekVoltage[g];
DEBUG_STREAM << "] rec=" << recommendedSeekVoltageIndex
<< " max=" << maxSeekVoltageIndex
<< " degradeT=" << degradationTemperature
<< " failT=" << failureTemperature
<< " thermalMass=" << thermalMass
<< "\n" << std::flush;
}
}
//***************************************************************************
// 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 SeekVoltageResponse.)
//
// 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::SeekVoltageResponse slot 15 @004b8f94
//***************************************************************************
//
// PoweredSubsystem virtual slot 15 (base @004b1780) == the shared subsystem
// vtbl+0x3C sampler the SeekVoltageGraph's Execute (@004c6934) plots -- THIS
// is what draws the myomer engineering page's POWER curve (issue #11; the
// old "drives a cockpit speed gauge" guess and the GetSpeedReading name are
// retired). Converts AvailableOutput(input_voltage) into the graph's 0..1
// x-response: scaled by 3.6 (m/s -> km/h), normalised to a 350 km/h full
// scale, then FUN_004dd138 == SQRT (part_015.c:4026 -- the old "fp magnitude
// / fabs" reading was wrong; sqrt is also what Emitter's analog @004bb42c
// applies to its power ratio). (Decompiled body discards the result ->
// returned via FPU.)
//
Scalar Myomers::SeekVoltageResponse(Scalar input_voltage)
{
Scalar output = AvailableOutput(input_voltage); // @004b8ac0
Scalar r = (output * 3.6f - 0.0f) / 350.0f;
// (FUN_004dd138 routes a negative operand to the matherr handler; output
// >= 0 in practice -- guarded for port safety.)
return (r > 0.0f) ? (Scalar)sqrt((double)r) : 0.0f; // FUN_004dd138 (sqrt)
}
//***************************************************************************
// issue #11 bridge -- the Myomers leg of the SeekVoltageGraph sample dispatch
// (see emitter.cpp BTSeekVoltageSample). Guarded: a non-Myomers subsystem
// samples 0 (only emitter/myomer pages own a graph in the shipped config).
//***************************************************************************
//***************************************************************************
// BTMyomersSpeedEffectOf -- the myomer drive fraction for the master perf's
// demand scale (mechmppr.cpp). RESTORED 2026-07-31 (the seek audit): the
// morning's deletion of the predecessor (BTMyomersDriveOf) rested on an
// export-gap-blind sweep -- the REAL consumer lives in the un-exported
// master-perf region @0x4a9cf2-0x4a9da4: MAX speedEffect over the myomer
// chain -> mech+0x79C -> `mapper->speedDemand *= it` (+ turn freeze at
// |drive| <= 1e-4). UNCLAMPED (the old bridge's f>1 -> 1 clamp ate the
// gear-4 overdrive): speedEffect legitimately rides to ~1.43 at seek 4.
// Returns -1 for a non-Myomers subsystem. [T1 disasm]
//***************************************************************************
Scalar BTMyomersSpeedEffectOf(void *subsystem)
{
Entity *entity = (Entity *)subsystem;
if (entity == 0 || !entity->IsDerivedFrom(Myomers::ClassDerivations))
return -1.0f;
Scalar f = ((Myomers *)entity)->SpeedEffect();
return (f > 0.0f) ? f : 0.0f;
}
Scalar BTMyomersSeekSample(void *subsystem, Scalar voltage)
{
Entity *entity = (Entity *)subsystem;
if (entity == 0 || !entity->IsDerivedFrom(Myomers::ClassDerivations))
return 0.0f;
return ((Myomers *)entity)->SeekVoltageResponse(voltage); // @004b8f94
}
//***************************************************************************
// Myomers::HasVoltage slot 16 @004b8f3c
//***************************************************************************
//
// Override of PoweredSubsystem::HasVoltage (slot 16, base @004b0b5c) -- Gitea
// #62 rename: the base was misnamed IsSourceShorted, asserting the inverse of
// what it computes. Myomers tightens it: adequate = at least the SELECTED SEEK
// voltage, not merely non-zero.
// 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::HasVoltage(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;
}
// The un-powered recovery rate the Performance hands its own damage zone each
// tick, as a NEGATIVE damageAmount: the literal 0xbc343958 stored at @004b8bf0.
static const Scalar MyomerRecoveryPerTick = -0.010999999940395355f; // 0xbc343958
//***************************************************************************
// Myomers::MyomersSimulation Performance @004b8b9c
//***************************************************************************
//
// THE REGISTERED PERFORMANCE -- the outer wrapper, decoded in full 2026-07-28.
// The ctor's [0x511620] resolves to 0x4b8b9c, NOT 0x4b8d18; the port had
// registered the inner drive-heat integrator and dropped this whole wrapper.
// Five blocks, in binary order:
//
// @004b8bab chain PoweredSubsystem::PoweredSubsystemSimulation (0x4b0bd0)
// @004b8bb9 un-powered self-repair of this myomer's own zone (INERT -- see
// the block comment; the original is dead code here too)
// @004b8c1e republish outputVoltage@0x344 from the resolved source
// @004b8c5a republish speedEffect@0x31C, the drive handed to the mover
// @004b8ceb run the inner integrator -- ONLY when outputVoltage > 0
//
// Restoring the first block alone fixed Gitea #70 (torso twist dead after a
// respawn): the Torso is a PowerWatcher that MIRRORS this subsystem's
// electrical level, and with the base machine never running, a Myomers that
// lost power in the death/reset window sat at NoVoltage forever. The other
// four are the rest of that same dropped wrapper.
//
void Myomers::MyomersSimulation(Scalar time_slice)
{
// 0. RegisterMaxOutput (@004b8ef0) -- the assembly-time top-speed cap.
// The binary calls it from the mech-assembly loop over the myomer chain
// (part_012.c:10322), after the model record is streamed:
// mech[0x7A0] = max(mech[0x7A0], AvailableOutput(seekVoltage[maxIdx]))
// = base x (topGear/recGear) = base x ~1.4284 on a healthy assembly --
// the gait rise-clamp both channels bound against (part_012:12103/12334).
// The port's capability-chain fill is the SubProxy stub (same situation as
// the flush's chain walk), so each myomer registers ITSELF here. Called
// per tick instead of once (the 0x358 layout lock leaves no room for a
// latch member): RegisterMaxOutput is an idempotent max() -- best falls as
// heat/damage degrade AvailableOutput, and max() never lowers the cap --
// so the resulting state is identical to the binary's once-at-assembly.
RegisterMaxOutput(); // @004b8ef0
// BENCH (BT_FORCE_SEEK=<idx 0..max>): pin the seek gear without the eng-page
// button -- the seek-4 supercharge/overheat loop is otherwise unreachable
// on a headless rig. Env-gated, off by default; the toggle handler stays
// the authentic path.
{
static int s_forceSeek = -2;
if (s_forceSeek == -2)
{
const char *e = getenv("BT_FORCE_SEEK");
s_forceSeek = (e != 0 && *e != '\0') ? atoi(e) : -1;
}
if (s_forceSeek >= 0 && s_forceSeek <= maxSeekVoltageIndex)
currentSeekVoltageIndex = s_forceSeek;
}
// 1. @004b8bab -- the base electrical state machine. MUST come first, and
// must precede any heat-model gate: OwnerAdvancedDamage() is off below
// veteran, so gating ahead of this denied the machine to most pilots.
PoweredSubsystemSimulation(time_slice); // call 0x4b0bd0
// 2. @004b8bb9-0x4b8c1b -- the INTENDED un-powered self-repair. While this
// myomer has NO VOLTAGE and its own zone is not yet destroyed, hand that
// zone a NEGATIVE explosive Damage every tick. DamageZone::TakeDamage is
// T0 (engine/MUNGA/DAMAGE.cpp:374) and reads only damageAmount/damageType:
// damageLevel += damageAmount * damageScale[damageType];
// Clamp(damageLevel, 0.0f, 1.0f);
// so the READING is "a myomer you power down slowly heals, and the `< 1.0`
// test stops a destroyed zone being resurrected".
//
// ⚠ CORRECTION (2026-07-29, #80): the earlier "inert in the original too"
// note here was WRONG about the original. The 2026-07-28 experiment that
// froze at 0.6 was measuring a PORT bug: the subsystem ctor wrote the
// zone's armour/scales through the ReconDamageZone PROXY (struct offsets
// +4/+8) instead of the engine members at +0x140/+0x144, so the real
// damageScale[] stayed zero. The binary's ctor (@0x4ac7bb) initializes
// them from the resource ("WeaponDamagePoints" + the five per-type
// "...DamagePoints" keys), and the port now does the same -- so this
// branch is LIVE, in 1995 and here: an un-powered, not-yet-destroyed
// myomer heals at 0.011 * damageScale[Explosive] per tick.
// (impactPoint/burstCount are set for fidelity; the callee ignores both.)
if (electricalStateAlarm.GetLevel() == PoweredSubsystem::NoVoltage // this+0x278 == 1
&& DamageStructureLevel() < 1.0f) // zone+0x158 < _DAT_004b8d10
{
extern int BTMechEntityPosition(void *entity, float *out_xyz); // mech4.cpp
float ownerXYZ[3] = { 0.0f, 0.0f, 0.0f };
BTMechEntityPosition(owner, ownerXYZ); // owner+0x100
Damage repair; // FUN_0041db7c (Damage::Damage)
repair.damageType = Damage::ExplosiveDamageType; // +0x00 = 2
repair.damageAmount = MyomerRecoveryPerTick; // +0x04 = 0xbc343958
repair.impactPoint.x = ownerXYZ[0]; // +0x20 <- FUN_00408440
repair.impactPoint.y = ownerXYZ[1];
repair.impactPoint.z = ownerXYZ[2];
repair.burstCount = 1; // +0x2C
ApplyZoneDamage(repair); // zone vtable +0x18
}
// 3. @004b8c1e-0x4b8c5a -- republish the input voltage. (The binary also
// zeroes +0x344 when simulationState == Destroyed just above this, then
// overwrites it unconditionally here; that store is dead in the original
// too, so it is not reproduced.)
Generator *source = (Generator *)ResolveVoltageSource(); // FUN_00417ab4(this+0x1D0)
outputVoltage = (electricalStateAlarm.GetLevel() == PoweredSubsystem::Ready
&& source != 0)
? source->MeasuredVoltage() // source+0x1DC
: 0.0f;
// 4. @004b8c5a-0x4b8ce8 -- republish speedEffect, the live drive fed to the
// mover. Clamp the input voltage to the SELECTED gear and then to the top
// gear, then normalise AvailableOutput by the same Mech base speed it was
// scaled by -- so speedEffect is a 0..1 FRACTION of full drive carrying the
// gear ratio, the thermal-degradation curve and the accumulated zone damage.
if (!HasVoltage(0)) // vtable +0x40 (slot 16)
{
speedEffect = 0.0f;
}
else
{
Scalar drive = outputVoltage; // @0x344
if (drive >= seekVoltage[currentSeekVoltageIndex]) // @0x330[@0x320]
drive = seekVoltage[currentSeekVoltageIndex];
if (drive > seekVoltage[maxSeekVoltageIndex]) // @0x330[@0x32C]
drive = seekVoltage[maxSeekVoltageIndex];
speedEffect = AvailableOutput(drive) / OwnerBaseSpeed(); // @004b8ac0 / (owner+0x34C)
}
// BRING-UP SCAFFOLDING (temporary, env-gated, off by default).
//
// BT_MYOMERS_REPAIR_TEST=<level> seeds this myomer's OWN zone to <level> once,
// at frame 300, so the un-powered self-repair above has something to walk
// back. Nothing else can damage that zone on a bench: unaimed self-damage
// goes through the mech's hit-location table and never crits a myomer, so
// block 2 of the Performance would otherwise stay unexercised. Pair it with
// BT_POWER_DETACH_TEST=Myomers to open the NoVoltage window.
// Self-contained: it also cuts the voltage link and drops the subsystem into
// MANUAL, so the AutoConnect hunt does NOT put it straight back (which is
// what BT_POWER_DETACH_TEST does -- that one re-attaches within a frame or
// two, far too short a window to watch a repair).
if (getenv("BT_MYOMERS_REPAIR_TEST"))
{
static int s_rtFrame = 0;
if (++s_rtFrame == 300)
{
Scalar seed = (Scalar)atof(getenv("BT_MYOMERS_REPAIR_TEST"));
if (seed <= 0.0f) seed = 0.5f;
SetSubsystemDamageLevel(seed); // mechsub.cpp bridge
DetachFromVoltageSource(); // @004b0e30
modeAlarm.SetLevel(ManualConnect); // stay dark (no auto-hunt)
DEBUG_STREAM << "[myo] SEED zone damage = " << seed
<< ", detached + forced Manual" << std::endl << std::flush;
}
}
// PROBE (BT_MYOMERS_LOG): the wrapper's outputs. Sample every frame while
// the electrical state is anything but Ready -- the NoVoltage window is the
// self-repair window and it only lasts about a second, so a once-a-second
// probe would step right over the transient.
if (getenv("BT_MYOMERS_LOG"))
{
static int s_ml = 0;
int notReady = (electricalStateAlarm.GetLevel() != PoweredSubsystem::Ready);
if (notReady || (s_ml % 120) == 0)
{
DEBUG_STREAM << "[myo] " << (GetName() ? GetName() : "?")
<< " elec=" << (int)electricalStateAlarm.GetLevel()
<< " outV=" << outputVoltage
<< " speed=" << speedEffect
<< " dmg=" << DamageStructureLevel()
<< " gear=" << currentSeekVoltageIndex
<< std::endl << std::flush;
}
++s_ml;
}
// 5. @004b8ceb -- the drive-heat integrator runs ONLY with voltage present.
if (outputVoltage > 0.0f) // vs _DAT_004b8d14 (0.0f)
MyomersDriveHeat(time_slice); // call 0x4b8d18
}
//***************************************************************************
// Myomers::MyomersDriveHeat inner @004b8d18
//***************************************************************************
//
// Per-tick drive/heat integration. Reached only from the wrapper above, and
// only while outputVoltage > 0.
//
// 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::MyomersDriveHeat(Scalar time_slice)
{
if (!OwnerAdvancedDamage()) // FUN_004ad7d4 gate (binary
return; // gates the ACCUMULATE; same outcome)
// #85 fix (2026-07-31): the five Mech motion operands were `return 0.0f`
// cross-family shims, so this integrator faithfully accumulated ratio^2 *
// damageGain * ZERO every tick -- the myomers stayed ice cold at any seek
// (Oracle's night-7 panel-confirmed report). Now sampled for real via the
// complete-Mech-TU bridge (mech4.cpp), operands re-grounded against the
// raw disasm of @004b8d18 + the authored resource (BTL4.RES 0x0BC6 records:
// VelocityEfficiency=0.995, AccelerationEfficiency=0.8, gears
// 0.3/0.5/0.7/0.9999 rec idx 2 -- one shared tuning across all 18 mechs).
float velMag, accMag, vy, mass, gravity;
{
extern void BTMechMyomerMotionSample(void *mech,
float *vel_mag, float *acc_mag, float *vel_y,
float *mass, float *gravity);
BTMechMyomerMotionSample(owner, &velMag, &accMag, &vy, &mass, &gravity);
}
Scalar velComplement = 1.0f - accelerationEfficiency; // @0x354 (= 0.2 authored)
Scalar workComplement = 1.0f - velocityEfficiency; // @0x350 (= 0.005 authored)
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;
// The binary fabs()es all three motion operands (the jbe/fchs pairs
// @4b8dd5/@4b8df0/@4b8e16); |v| and |a| are sqrt-positive already, but
// v.y is signed -- descending must heat like climbing, not cool.
if (vy < 0.0f) vy = -vy;
// Physics (operands nailed 2026-07-31): mass@0x20C, gravity=**(0x250) --
// work = 1/2 m v^2 (kinetic energy; NO dt in the
// binary -- the accumulate is per-TICK, an authentic 1995
// fixed-frame assumption, so heat/s scales with frame rate;
// kept verbatim, field-calibrate if the pod veterans object)
// velTerm = m g |vy| dt (climb power)
// accTerm = m |v| |a| dt (acceleration power)
Scalar work = mass * (velMag * velMag) * 0.5f; // dot(v,v)*0.5 (_DAT_004b8ee4)
// The three terms (#96 "myomers run too hot"). Two are POWER (x dt); the
// kinetic one is an ENERGY the binary adds once per TICK with NO dt
// (FUN_004b8d18: param_2 multiplies the climb and accel terms only).
//
// FRAME-RATE NORMALISATION. A per-tick energy makes heat/SECOND scale with
// the tick rate, which on a 1995 fixed-frame machine is a constant and on a
// modern PC is not: two players on different hardware, or the same player in
// a heavy scene, get different heat from identical throttle. Measured here
// at ~59Hz the myomers cross their degradation threshold in ~27s of seek-4
// cruising; at the pod's documented 30Hz board frame that is ~54s.
//
// So the term is rescaled to the reference frame rate: multiply by
// dt * hz, making the heat identical on every machine. This is the ONE
// deliberate divergence from a verbatim transcription of @004b8d18, and it
// exists because the binary's per-tick value depends on the pod's frame
// rate, which our port does not run at.
//
// THE CLOCK ORIGIN, traced 2026-08-02 (was the open question here):
// the DOS binary keeps its engine frame rate in the global DAT_0052140c,
// set once at startup --
// 0x401ace: mov [0x52140c], 0x41E00000 = 28.0f (nominal)
// 0x401ada: mov [0x52140c], 0x4191A6E0 = 18.2065f (BIOS-tick fallback)
// -- and consumed by ~90 fmul/fdiv sites across the image as THE
// per-frame<->per-second conversion. The DOS main passes it straight into
// the ApplicationManager ctor (0x401189: push [0x52140c]). So the pod's
// nominal Performance cadence was 28 Hz, byte-proven. [T1]
//
// CALIBRATION FACTOR 0.5, testimony (Oracle, 2026-08-02): at the byte rate
// the veterans still report ~2x too much myomer heat -- "If you could
// please halve the myomer heat rate again. We will bracket to something
// reasonable." The plausible physical story is the pod's EFFECTIVE rate
// under load (a 486 host missing beats; the 18.2 fallback existing at all
// says slow paths were expected), which statics cannot settle. Effective
// default therefore 28 * 0.5 = 14 Hz, explicitly marked testimony-
// calibrated [T3]; BT_MYO_HZ=<hz> overrides absolutely for bracketing.
static const Scalar kPodFrameHz = 28.0f; // byte-proven nominal [T1]
static const Scalar kSeekHeatCalib = 0.5f; // Oracle bracket 2026-08-02 [T3]
static Scalar s_hz = -1.0f;
if (s_hz < 0.0f)
{
const char *hv = getenv("BT_MYO_HZ");
s_hz = (hv && *hv) ? (Scalar)atof(hv) : (kPodFrameHz * kSeekHeatCalib);
if (s_hz <= 0.0f) s_hz = kPodFrameHz * kSeekHeatCalib;
if (getenv("BT_MYO_LOG"))
DEBUG_STREAM << "[myoheat] kinetic-term reference rate: " << s_hz
<< " Hz (pod nominal 28 [T1] x calib " << kSeekHeatCalib
<< " [T3]" << (hv ? ", ENV OVERRIDE" : "") << ")" << std::endl;
}
Scalar termClimb = velComplement * vy * mass * gravity * time_slice;
Scalar termKinetic = workComplement * work * (time_slice * s_hz); // rate-normalised
Scalar termAccel = velComplement * velMag * accMag * mass * time_slice;
Scalar gain = ratio * ratio * damageGain;
pendingHeat /* @0x1C8 */ += gain * (termClimb + termKinetic + termAccel);
if (getenv("BT_MYO_LOG"))
{
static int s_n = 0;
static Scalar s_elapsed = 0.0f;
s_elapsed += time_slice; // wall clock since the sim started
if ((s_n++ % 120) == 0)
{
Scalar sum = termClimb + termKinetic + termAccel;
// T and the clock: how long does seek 4 actually take to cook the
// myomers? (#96 -- the players' claim is "under a minute".)
DEBUG_STREAM << "[myotemp] t=" << s_elapsed
<< " T=" << currentTemperature
<< " degradeT=" << degradationTemperature
<< " failT=" << failureTemperature << "\n" << std::flush;
DEBUG_STREAM << "[myoheat] v=" << velMag << " a=" << accMag
<< " m=" << mass << " g=" << gravity
<< " ratio=" << ratio << " dmgGain=" << damageGain
<< " dt=" << time_slice
<< " | climb=" << (gain * termClimb)
<< " kinetic=" << (gain * termKinetic)
<< " accel=" << (gain * termAccel)
<< " kineticShare=" << (sum != 0.0f ? (termKinetic / sum) : 0.0f)
<< " pending=" << pendingHeat << "\n" << std::flush;
}
}
}
//***************************************************************************
// 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
}
//***************************************************************************
// (ConnectToMover / DisconnectFromMover are DELETED -- myomer-system
// completion, 2026-07-31. Their source functions @004b9550/@004b95b8 are
// MechWeapon::ConfigureMappables / ChooseButton (the +0x31C there is the
// weapon TriggerState and **(mech+0x128) is the CONTROLS MAPPER's button
// roster -- proven by the MECHWEAP.CPP assert string and the trigger-edge
// detector @004b9608 adjacent in the image). The binary has NO dynamic
// myomer->mover feed; see the RegisterMaxOutput latch in MyomersSimulation
// for the real (assembly-time cap) locomotion coupling.)
//***************************************************************************
// Myomers::ToggleSeekVoltageMessageHandler @004b8a48
//***************************************************************************
// Handler table @0x51158C: {9, "ToggleSeekVoltage"}. Reconstructed FAITHFULLY
// from the raw disassembly (tools/disas2.py 0x4b8a48; the decomp shards missed
// the untagged gap 0x4b8837..0x4b8a8c) -- issue #19 fix, 2026-07-21:
// 0x4b8a50 call 0x4ac9c8 novice lockout (BTPlayerRoleLocksAdvanced) -> ret
// 0x4b8a5b call 0x4ad7d4 heat-model experience gate -> ret if OFF
// 0x4b8a68 msg+0xc <= 0 press only (release is negative)
// 0x4b8a6f idx@0x320 = (idx + 1) % (maxSeekVoltageIndex@0x32C + 1)
// Note: modulo (max+1) from ZERO -- minSeekVoltageIndex is NOT consulted, and
// there is no ResetFiringState (both unlike the Emitter sibling @004ba478).
void Myomers::ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf<int> *message)
{
if (IsDamaged()) // FUN_004ac9c8 -- NOVICE lockout bridge
{
return;
}
if (!HeatModelActive() || message->dataContents <= 0) // FUN_004ad7d4; msg+0xc
{
return;
}
currentSeekVoltageIndex = // @0x320
(currentSeekVoltageIndex + 1) % (maxSeekVoltageIndex + 1); // @0x32C
if (getenv("BT_SEEK_LOG"))
DEBUG_STREAM << "[seek] " << GetName() << " -> gear "
<< currentSeekVoltageIndex << std::endl;
}
//===========================================================================//
// 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. DRIVE HEAT IS LIVE (#85 fix
// 2026-07-31: the motion operands are sampled from the real Mech). Still
// inert: the LOCOMOTION coupling (no-op mover feed -- speedEffect is computed
// but nothing consumes it; WAVE 6 cutover) and the climb heat term (the
// environment gravity is unwired in the port, so g reads 0).
//===========================================================================//
Subsystem *CreateMyomersSubsystem(Mech *owner, int id, void *seg)
{
return (Subsystem *) new (Memory::Allocate(0x358))
Myomers(owner, id, (Myomers::SubsystemResource *)seg);
}
//
// DeathReset (#55 step 4) -- the respawn sweep's entry for the Myomers.
//
void Myomers::DeathReset(int reset_command)
{
ResetToInitialState(reset_command != 0); // @004b8aa4
}