//===========================================================================// // 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 // engine aggregate: NotationFile / NameList / Generator / ... #pragma hdrstop #if !defined(APP_HPP) # include #endif #if !defined(TESTBT_HPP) # include #endif #include // 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 } //*************************************************************************** // 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: " Missing VelocityEfficiency!" (0051171c) return False; } if (!subsystem_file->GetEntry(model_name, "AccelerationEfficiency", &resource->accelerationEfficiency) // s_AccelerationEfficiency_00511739 && resource->accelerationEfficiency == -1.0f) { // Warning: " 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: " 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: " 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). //*************************************************************************** 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) { // 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". // // ⚠ BUT IT IS INERT -- IN THE ORIGINAL TOO. [T0, measured 2026-07-28] // A subsystem's private zone is built by the 2-arg trivial ctor // `new DamageZone(this, 0)`, and DAMAGE.cpp:187-190 zeroes ALL FIVE // damageScale[] entries; DamageZone::Reset never touches them, and the only // other writer in the codebase is Mech__DamageZone (the MECH's streamed // zones, a different class -- mechdmg.cpp:246-253). So damageScale stays // {0,0,0,0,0} for the life of the zone and the sum above is always // `damageLevel += amount * 0.0f` == no change. Confirmed live: seeded to // 0.6, held at NoVoltage for ~1500 ticks, damageLevel never moved off 0.6 // (BT_MYOMERS_REPAIR_TEST below). The crit path reaches subsystem damage a // different way -- it writes damageLevel DIRECTLY (mechsub.cpp // DistributeCriticalHit pins *(this[0x38]+0x158) = 1.0f) -- which is why // subsystems can still be destroyed even though this route cannot. // // Reconstructed anyway, and deliberately NOT "fixed": this is what the 1995 // binary does, and inventing a working repair here would be a behavior we // made up. Kept so the dead branch is visible rather than silently missing. // (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= seeds this myomer's OWN zone to 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 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::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 *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, 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); } // // DeathReset (#55 step 4) -- the respawn sweep's entry for the Myomers. // void Myomers::DeathReset(int reset_command) { ResetToInitialState(reset_command != 0); // @004b8aa4 }