Files
TeslaRel410/restoration/source410/BT/MYOMERS.CPP
T
CydandClaude Fable 5 02b78eadc7 BT410 5.3.111: the myomers drive -- and runSpeedMax's writer is found in the muscles
Myomers::MyomersSimulation (@004b8d18) reconstructed, five blocks in the
binary's order, and it answers a question left open since 5.3.94:

  runSpeedMax (+0x7a0) IS WRITTEN BY THE MYOMERS.  Each myomer's
  RegisterMaxOutput (@004b8ef0) raises the mech's run-cycle cap to its
  full-throttle AvailableOutput -- so the mech's true top speed is a
  property of its MUSCLE ASSEMBLY, and it degrades live as heat crosses the
  degradation band and as the myomer's own zone accumulates damage.  The
  1e9 ctor sentinel now stands only until the first myomer registers.

The drive computation (@004b8ac0): base = the mech base speed
(reverseStrideLength, mech+0x34C) scaled by the gear ratio -- input voltage
over the RECOMMENDED gear -- with a quadratic thermal bleed across the
degradation band, zero past failure, and the subsystem's wear taken off the
top.  speedEffect republishes per frame as a 0..1 fraction of full drive:
input clamped to the SELECTED gear then the top gear, through
AvailableOutput, normalised by the base speed.

AND THE MUSCLES HEAL: while a myomer has NO VOLTAGE and its zone is not
destroyed, it hands its own zone a NEGATIVE explosive tick (-0.011,
0xbc343958) -- unloaded muscle knits itself back together.  The donor's
history records this block as the fix for "torso twist dead after respawn"
(the Torso mirrors this subsystem's electrical level; with the base machine
never running, a myomer that lost power in the death window sat at
NoVoltage forever) -- and our runs respawn now, so the fix is live where it
matters.

Registered on the master (the sibling gate); the base electrical machine
runs FIRST and unconditionally, per the donor's hard-learned ordering note.
Not carried over: the port's BT_FORCE_SEEK / repair-test bench scaffolding.

Live run clean; the gait's speed sequence is unchanged (7.31972, 26.6726,
...) -- the muscle cap sits above the demanded 26.9 on a healthy Mad Cat,
exactly as it should.

Stub census: 18 across 12 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 22:27:00 -05:00

275 lines
8.1 KiB
C++

//===========================================================================//
// File: myomers.cpp //
// Project: BattleTech Brick: Mech subsystems //
// Contents: Myomers -- artificial-muscle locomotion power //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MYOMERS_HPP)
# include <myomers.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
Derivation
Myomers::ClassDerivations(
PoweredSubsystem::ClassDerivations,
"Myomers"
);
const Myomers::IndexEntry
Myomers::AttributePointers[]=
{
ATTRIBUTE_ENTRY(Myomers, SpeedEffect, speedEffect),
ATTRIBUTE_ENTRY(Myomers, CurrentSeekVoltageIndex, currentSeekVoltageIndex),
ATTRIBUTE_ENTRY(Myomers, RecommendedSeekVoltageIndex,
recommendedSeekVoltageIndex),
ATTRIBUTE_ENTRY(Myomers, MinSeekVoltageIndex, minSeekVoltageIndex),
ATTRIBUTE_ENTRY(Myomers, MaxSeekVoltageIndex, maxSeekVoltageIndex),
{ (int)Myomers::SeekVoltageAttributeID, "SeekVoltage",
(Simulation::AttributePointer)&Myomers::seekVoltage }
};
Myomers::AttributeIndexSet
Myomers::AttributeIndex(
ELEMENTS(Myomers::AttributePointers),
Myomers::AttributePointers,
PoweredSubsystem::AttributeIndex
);
//
// The shared data must carry the POWERED tables, not Subsystem's -- the
// same miswiring found on MissileLauncher (5.3.33). Myomers derives from
// PoweredSubsystem, so wiring it to Subsystem's dropped every powered
// attribute AND the powered message handlers (ToggleSeekVoltage and the
// generator-select family) on the way past.
//
Myomers::SharedData
Myomers::DefaultData(
Myomers::ClassDerivations,
PoweredSubsystem::MessageHandlers,
Myomers::AttributeIndex,
Subsystem::StateCount
);
Myomers::Myomers(
Mech *owner,
int subsystem_ID,
SubsystemResource *resource,
SharedData &shared_data
):
PoweredSubsystem(owner, subsystem_ID, resource, shared_data)
{
Check(owner);
Check_Pointer(resource);
speedEffect = 1.0f;
heatRange = failureTemperature - degradationTemperature;
heatRangeSquared = heatRange * heatRange;
velocityEfficiency = resource->velocityEfficiency;
accelerationEfficiency = resource->accelerationEfficiency;
//
// Build the seek-voltage drive table: each resource fraction scaled by the
// powering generator's rated voltage. The voltage source is resolved once
// AttachToVoltageSource runs (phase-4 wiring); until then it is null and the
// table scales to zero.
//
Generator *source = (Generator *)ResolveVoltageSource();
Scalar srcRated = (source != NULL) ? source->ratedVoltage : 0.0f;
maxSeekVoltageIndex = -1;
int count = 0;
while (count < 5)
{
if (resource->seekVoltage[count] == -1.0f)
{
maxSeekVoltageIndex = count - 1;
break;
}
seekVoltage[count] = resource->seekVoltage[count] * srcRated;
++count;
}
if (maxSeekVoltageIndex < 0)
{
maxSeekVoltageIndex = count - 1;
}
minSeekVoltageIndex = 0;
recommendedSeekVoltageIndex = resource->seekVoltageRecommendedIndex;
currentSeekVoltageIndex = recommendedSeekVoltageIndex;
//
// The master instance runs the myomer per-frame (the same gate every
// sibling uses; the binary's is the segment-copy/master flag pair).
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Myomers::MyomersSimulation);
}
Check_Fpu();
}
Myomers::~Myomers()
{
}
Logical
Myomers::TestClass(Mech &)
{
return True;
}
Logical
Myomers::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
//
//#############################################################################
// @004b8ac0 -- the drive computation. base = the mech base speed scaled by
// the gear ratio (input over the RECOMMENDED gear's voltage); a heat factor
// bleeds it off quadratically across the degradation band and kills it past
// failure; and the subsystem's own accumulated zone damage takes its cut.
//#############################################################################
//
Scalar
Myomers::AvailableOutput(Scalar input_voltage)
{
Check(this);
Scalar
base =
((Mech *)GetEntity())->BaseSpeedOf() *
(input_voltage / seekVoltage[recommendedSeekVoltageIndex]);
Scalar
heat_factor;
if (degradationTemperature <= currentTemperature)
{
if (failureTemperature <= currentTemperature)
{
heat_factor = 0.0f;
}
else
{
Scalar
band = currentTemperature - degradationTemperature;
heat_factor =
(base == 0.0f)
? 1.0f
: (base - band * band * (base / heatRangeSquared)) / base;
}
}
else
{
heat_factor = 1.0f;
}
return (1.0f - GetSubsystemDamageLevel()) * base * heat_factor;
}
//
//#############################################################################
// @004b8ef0 -- raise the owner's run-speed cap to this myomer's
// full-throttle output. An idempotent max: repeat calls never lower it,
// and the best available output only falls as heat and damage degrade --
// so re-registering per tick converges on the binary's once-at-assembly
// value while staying live to degradation.
//#############################################################################
//
void
Myomers::RegisterMaxOutput()
{
Check(this);
Scalar
best = AvailableOutput(seekVoltage[maxSeekVoltageIndex]);
((Mech *)GetEntity())->RaiseRunSpeedMax(best);
}
//
//#############################################################################
// @004b8d18 -- the per-frame myomer. Five blocks, in the binary's order:
//
// 0. RegisterMaxOutput -- the assembly-time top-speed cap (see above).
// 1. The base electrical state machine. FIRST, unconditionally -- gating
// it behind anything denies power handling to most pilots.
// 2. The un-powered SELF-REPAIR: while this myomer has NO VOLTAGE and its
// zone is not destroyed, hand the zone a NEGATIVE explosive tick --
// muscle knits itself back together while unloaded.
// 3. Republish the input voltage from the resolved source.
// 4. Republish speedEffect, the live drive handed to the mover: the input
// clamped to the SELECTED gear then the top gear, through
// AvailableOutput, normalised by the mech base speed -- a 0..1
// fraction carrying the gear ratio, the thermal curve and the wear.
//#############################################################################
//
static const Scalar
MyomerRecoveryPerTick = -0.011f; // 0xbc343958 -- the heal is a
// negative damage amount
void
Myomers::MyomersSimulation(Scalar time_slice)
{
Check(this);
RegisterMaxOutput();
PoweredSubsystemSimulation(time_slice);
if (
GetVoltageState() == PoweredSubsystem::NoVoltage &&
GetSubsystemDamageLevel() < 1.0f
)
{
Damage
repair;
repair.damageType = Damage::ExplosiveDamageType;
repair.damageAmount = MyomerRecoveryPerTick;
repair.impactPoint = ((Mech *)GetEntity())->localOrigin.linearPosition;
repair.burstCount = 1;
TakeDamage(repair);
}
Generator
*source = (Generator *)ResolveVoltageSource();
outputVoltage =
(GetVoltageState() == PoweredSubsystem::Ready && source != NULL)
? source->MeasuredVoltage()
: 0.0f;
if (GetVoltageState() != PoweredSubsystem::Ready)
{
speedEffect = 0.0f;
}
else
{
Scalar
drive = outputVoltage;
if (drive >= seekVoltage[currentSeekVoltageIndex])
{
drive = seekVoltage[currentSeekVoltageIndex];
}
if (drive > seekVoltage[maxSeekVoltageIndex])
{
drive = seekVoltage[maxSeekVoltageIndex];
}
speedEffect =
AvailableOutput(drive) / ((Mech *)GetEntity())->BaseSpeedOf();
}
Check_Fpu();
}