BT410 5.3.110: the gyroscope lives -- the cockpit's springs, the hit bounce, and the crunch, all wired

The feel wave's cornerstone, transcribed from the task-#56 byte-exact donor
per the GYRO.NOTES.md plan:

  THE SPRINGS.  IntegrateEyeJoint (@004b2ec0): the eye TRANSLATION spring
  whose equilibrium (posSpring+negSpring)/2 IS the authentic steady eye
  offset, with the byte-verified quirks intact -- the damping step
  OVERWRITES the force accumulator (it carries last frame's damping and
  the damage impulses by design) and the position step has NO dt.
  IntegrateBody (@004b30ec): the body ROTATION spring, X/Z-crossed on the
  spring force and crossed AGAIN on the orientation step.

  THE SIM (@004b275c): chain the power watch (5.3.109's mirrors), slew the
  sway angle between the powered and impaired percentages -- impaired reads
  the gyro's OWN mirrored alarms: watchdog != Ready or heat == Failure --
  band-clamp, run both integrators.  The Performance ends there.

  THE JOINT WRITES (@004b33e0/@004b34ec) live where the binary calls them:
  the MECH master-perf tail after the gait pass, death-gated -- the idle
  sway onto the EyeJoint, and TRANSLATION <- eyePosition + ROTATION <-
  bodyOrientation onto 'jointeye', the BallTranslation joint the cockpit
  eyepoint rides.  Every write QuantiseEps-gated.

  THE HIT BOUNCE (@004b2980): every non-collision hit shakes the cockpit.
  Direction from the damage force (random horizontal when ~zero), rotated
  into the yaw-only torso frame, per-type priced amount/multiplier*response
  (Explosive alone reads burstCount), clamped 1.3 UPPER only, then the four
  kicks -- impulse, torque (which zeroes .y and negates itself), the fixed
  up-axis vibration, and the vertical mirror.  Fed from the damage hub's
  step 1, BEFORE zone resolution -- an invalid-zone hit still shakes you.

  THE CRUNCH: driving through a crushable prop now kicks the gyro (torque
  0.4 along the contact normal, upward impulse 0.2) -- the 5.3.102 staged
  site, closed.

Resource: the five DamageResponse quads {trans,pitchRoll,yaw,vibration}
appended after the multipliers (record 0x21C, binary-verified layout) --
they were always in the stream; we simply never read them.  The ctor now
zeroes every accumulator (the donor's history records a NaN poison from
exactly that omission), copies the clamp pairs (body doubled), self-points
externalPitchPtr at spare0 until a torso binds it, and REGISTERS the
Performance on the master -- it ticks live, no fault.

Stub census: 19 across 13 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-03 16:42:56 -05:00
co-authored by Claude Fable 5
parent 38cce95641
commit 41fca98f85
5 changed files with 998 additions and 315 deletions
@@ -0,0 +1,88 @@
# pod_render_norio.conf -- pod_render_rec with the RIO serial port OFF.
#
# The emulator log shows a steady stream of serial1 RX OVERRUN errors on
# the RIO pipe, and controls post their events at HighEventPriority
# UNCONDITIONALLY (CONTROLS.HPP:250) -- so a chattering RIO port would
# flood a priority the background pump always serves first, starving the
# priority-0 renderer events the load gate waits on. This conf tests that
# by removing the port entirely. Everything else is identical to the rec
# conf, so a launch here and a hang there isolates the RIO.
#
[sdl]
output=opengl
# higher,higher not highest: HIGH_PRIORITY_CLASS starved the host desktop;
# with the retry patches a rare dropout self-recovers (see gauge_rio.conf).
priority=higher,higher
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
# match the EMU8000s' native rate (no resample) and buffer ~60ms so brief
# emulation-thread stalls (RIO retry recovery) don't audibly chop
rate=44100
blocksize=1024
prebuffer=60
[serial]
# RIO on COM1 with the low-latency options (rxpollus/rxburst) so the board's
# few-ms ACK deadline is met; plasma display on COM2 (real pod has both).
# VWE fork namedpipe backend (com0com/realport retired -- COM1/COM2 gone):
# DOSBox = pipe client (retry), vRIO/vPLASMA apps = servers; an unconnected
# pipe behaves as an unplugged cable so the mission still runs. serialnamedpipe.h
serial1=disabled
serial2=namedpipe pipe:vplasma
# live UNBUFFERED game output: DOS char devices are not buffered, so
# redirecting stdout to COM3 lands every line immediately. A normal
# '> file' redirect stays 0 bytes until the process exits, which hides
# all progress on a run that does NOT crash.
serial3=file file:C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
c:
cd \REL410\BT
set VIDEOFORMAT=svga
rem production pod card init (PARAMETR.BAT:181-186): DIAGNOSE + AWEUTIL per
rem card -- AWEUTIL /S does the EMU8000 bring-up and DRAM detect the HMI SOS
rem driver relies on; skipping it left the cards uninitialized (silent).
rem aweutil /s SKIPPED for now: it verifies the AWE32 GM ROM, which the
rem emulated cards lack (hangs in a retry loop) -- restore once the ROM is
rem dumped from a real card. diagnose /s kept (passes, sets mixer config).
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
rem arena1 city mission (TESTARN.EGG: map=arena1, time=day) with the RIO
rem attached; stdout redirected so mission-load progress survives kills.
set BT_JOINTS=1
set L4VIEWEXT=1
set BT_MECH_LOG=1
set BT_GYRO_LOG=1
set BT_MAP_LOG=1
set BT_MER_LOG=1
set BT_VID_LOG=1
set BT_LAUNCH_LOG=1
set BT_STACK_LOG=1
set BT_FORCE_THROTTLE=0.6
set BT_FORCE_TURN=0.25
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s n p
32rtm.exe -x
BTL4REC.EXE -egg testarn.egg > COM3
echo GAME-RC=%errorlevel% >> RC.TXT
32rtm.exe -u
echo ALPHA1-RUN-DONE
pause
+603 -117
View File
@@ -1,117 +1,603 @@
//===========================================================================// //===========================================================================//
// File: gyro.cpp // // File: gyro.cpp //
// Project: BattleTech Brick: Mech subsystems // // Project: BattleTech Brick: Mech subsystems //
// Contents: Gyroscope -- eye/body stabilisation // // Contents: Gyroscope -- eye/body stabilisation //
//---------------------------------------------------------------------------// //---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. // // Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide // // All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================// //===========================================================================//
#include <bt.hpp> #include <bt.hpp>
#pragma hdrstop #pragma hdrstop
#if !defined(GYRO_HPP) #if !defined(GYRO_HPP)
# include <gyro.hpp> # include <gyro.hpp>
#endif #endif
#if !defined(MECH_HPP) #if !defined(MECH_HPP)
# include <mech.hpp> # include <mech.hpp>
#endif #endif
Derivation #if !defined(RANDOM_HPP)
Gyroscope::ClassDerivations( # include <random.hpp>
PowerWatcher::ClassDerivations, #endif
"Gyroscope"
); Derivation
Gyroscope::ClassDerivations(
Gyroscope::SharedData PowerWatcher::ClassDerivations,
Gyroscope::DefaultData( "Gyroscope"
Gyroscope::ClassDerivations, );
Subsystem::MessageHandlers,
Subsystem::AttributeIndex, Gyroscope::SharedData
Subsystem::StateCount Gyroscope::DefaultData(
); Gyroscope::ClassDerivations,
Subsystem::MessageHandlers,
Gyroscope::Gyroscope( Subsystem::AttributeIndex,
Mech *owner, Subsystem::StateCount
int subsystem_ID, );
SubsystemResource *r,
SharedData &shared_data Gyroscope::Gyroscope(
): Mech *owner,
PowerWatcher(owner, subsystem_ID, r, shared_data) int subsystem_ID,
{ SubsystemResource *r,
Check(owner); SharedData &shared_data
Check_Pointer(r); ):
PowerWatcher(owner, subsystem_ID, r, shared_data)
exageration = r->exageration; {
maxAnimationNoise = r->maxAnimationNoise; Check(owner);
minAnimationNoise = r->minAnimationNoise; Check_Pointer(r);
rotationPerSecond = r->rotationPerSecond;
percentageOnNormal = r->percentageOnNormal; exageration = r->exageration;
percentageOnDestruction = r->percentageOnDestruction; maxAnimationNoise = r->maxAnimationNoise;
percentageOnDegradation = r->percentageOnDegradation; minAnimationNoise = r->minAnimationNoise;
percentageOnFailure = r->percentageOnFailure; rotationPerSecond = r->rotationPerSecond;
percentageOnNormal = r->percentageOnNormal;
springConstant = r->springConstant; percentageOnDestruction = r->percentageOnDestruction;
dampingConstant = r->dampingConstant; percentageOnDegradation = r->percentageOnDegradation;
posSpring = r->posSpring; percentageOnFailure = r->percentageOnFailure;
negSpring = r->negSpring;
rotationSpringConstant = r->rotationSpringConstant; springConstant = r->springConstant;
rotationDampingConstant = r->rotationDampingConstant; dampingConstant = r->dampingConstant;
rotationPosSpring = r->rotationPosSpring; posSpring = r->posSpring;
rotationNegSpring = r->rotationNegSpring; negSpring = r->negSpring;
rotationSpringConstant = r->rotationSpringConstant;
damageMultiplier[0] = r->collisionDamageMultiplier; rotationDampingConstant = r->rotationDampingConstant;
damageMultiplier[1] = r->ballisticDamageMultiplier; rotationPosSpring = r->rotationPosSpring;
damageMultiplier[2] = r->explosiveDamageMultiplier; rotationNegSpring = r->rotationNegSpring;
damageMultiplier[3] = r->laserDamageMultiplier;
damageMultiplier[4] = r->energyDamageMultiplier; damageMultiplier[0] = r->collisionDamageMultiplier;
damageMultiplier[1] = r->ballisticDamageMultiplier;
// damageMultiplier[2] = r->explosiveDamageMultiplier;
// Zero the eye/body dynamics accumulators (the binary zeroes each; the damageMultiplier[3] = r->laserDamageMultiplier;
// per-frame sim seeds them from the springs above). damageMultiplier[4] = r->energyDamageMultiplier;
//
for (int i = 0; i < 64; ++i) damageResponse[0] = r->collisionDamageResponse;
{ damageResponse[1] = r->ballisticDamageResponse;
dynamicsState[i] = 0.0f; damageResponse[2] = r->explosiveDamageResponse;
} damageResponse[3] = r->laserDamageResponse;
damageResponse[4] = r->energyDamageResponse;
Check_Fpu();
} //
// The clamps are ctor-time COPIES of the spring pairs; the body pair is
Gyroscope::~Gyroscope() // doubled. Every accumulator zeroes -- the binary leaves nothing to
{ // allocator fill (the donor's history records a NaN poison from exactly
} // that omission).
//
Logical eyeClampUpper = posSpring;
Gyroscope::TestClass(Mech &) eyeClampLower = negSpring;
{ bodyClampUpper.Multiply(rotationPosSpring, 2.0f);
return True; bodyClampLower.Multiply(rotationNegSpring, 2.0f);
}
{
Logical Vector3D
Gyroscope::TestInstance() const vzero(0.0f, 0.0f, 0.0f);
{ eyePosition = eyeForce = eyeVelocity = eyeWork = vzero;
return IsDerivedFrom(ClassDerivations); bodyOrientation = bodyForce = bodyVelocity = bodyWork = vzero;
} placePos = placeRot = vzero;
placeQuat[0] = placeQuat[1] = placeQuat[2] = 0.0f;
void placeQuat[3] = 1.0f;
Gyroscope::ResetToInitialState()
{ int i;
Check(this); for (i = 0; i < 12; ++i)
for (int i = 0; i < 64; ++i) {
{ workMatrix[i] = 0.0f;
dynamicsState[i] = 0.0f; }
} workMatrix[0] = workMatrix[5] = workMatrix[10] = 1.0f;
}
spare0 = 0.0f;
// externalPitchPtr = &spare0;
// Per-frame eye/body stabilisation (spring/damper integration driving the }
// cockpit eye joint). Not yet reconstructed. vibrationDirection = Vector3D(0.0f, 1.0f, 0.0f);
// swayAngle = 0.0f;
void swayVelocity = 0.0f;
Gyroscope::GyroscopeSimulation(Scalar) swayActive = 0;
{ swayBias = 0.0f;
Fail("Gyroscope::GyroscopeSimulation -- gyro.cpp not yet reconstructed");
} eyeJointNode = owner->ResolveJoint(r->eyeJoint);
mechJointNode = owner->ResolveJoint(r->mechJoint);
//
// The master instance runs the gyro per-frame.
//
if (owner->GetInstance() != Entity::ReplicantInstance)
{
SetPerformance(&Gyroscope::GyroscopeSimulation);
}
Check_Fpu();
}
Gyroscope::~Gyroscope()
{
}
Logical
Gyroscope::TestClass(Mech &)
{
return True;
}
Logical
Gyroscope::TestInstance() const
{
return IsDerivedFrom(ClassDerivations);
}
void
Gyroscope::ResetToInitialState()
{
Check(this);
{
Vector3D
vzero(0.0f, 0.0f, 0.0f);
eyePosition = eyeForce = eyeVelocity = eyeWork = vzero;
bodyOrientation = bodyForce = bodyVelocity = bodyWork = vzero;
swayAngle = 0.0f;
}
}
//
//#############################################################################
// @004b275c -- the per-frame gyro. Chain the power watch, slew the sway
// angle toward the powered or impaired percentage, and run both spring
// integrators. THE PERFORMANCE ENDS THERE: the joint writes are called from
// the MECH master performance tail after the gait, not from here.
//#############################################################################
//
void
Gyroscope::GyroscopeSimulation(Scalar time_slice)
{
Check(this);
PowerWatcher::Simulation(time_slice);
//
// The impaired test reads the gyro's OWN mirrored alarms -- the two
// UpdateWatch just drove: the voltage watchdog and the heat watch.
//
Logical
impaired =
watchdogAlarm.GetLevel() != PoweredSubsystem::Ready ||
heatAlarm.GetLevel() == HeatSink::FailureHeat;
Scalar
target =
(impaired ? percentageOnDestruction : percentageOnNormal)
+ swayBias;
Scalar
step = rotationPerSecond * time_slice;
if (target < swayAngle)
{
step = -step;
}
swayAngle += step;
if (step <= 0.0f)
{
if (step < 0.0f && swayAngle < target)
{
swayAngle = target;
}
}
else
{
if (swayAngle > target)
{
swayAngle = target;
}
}
if (swayAngle > maxAnimationNoise)
{
swayAngle = maxAnimationNoise;
}
if (swayAngle < minAnimationNoise)
{
swayAngle = minAnimationNoise;
}
IntegrateEyeJoint(time_slice);
IntegrateBody(time_slice);
Check_Fpu();
}
//
//#############################################################################
// @004b2ec0 -- the eye TRANSLATION spring, byte-exact quirks preserved: both
// spring terms use springConstant; the damping step OVERWRITES the force
// accumulator (it carries last frame's damping + damage impulses by design);
// and the position step has NO dt. Equilibrium is (posSpring+negSpring)/2
// per axis -- the authentic steady eye offset.
//#############################################################################
//
void
Gyroscope::IntegrateEyeJoint(Scalar time_slice)
{
Vector3D
toward_negative,
toward_positive,
force;
toward_negative.Subtract(eyePosition, negSpring);
toward_positive.Subtract(eyePosition, posSpring);
force.Multiply(springConstant, toward_negative);
eyeForce += force;
force.Multiply(springConstant, toward_positive);
eyeForce += force;
eyeWork = eyeForce;
force.Multiply(eyeWork, time_slice);
eyeVelocity += force;
eyeForce.Multiply(dampingConstant, eyeVelocity);
eyeWork = eyeForce;
force.Multiply(eyeWork, time_slice);
eyeVelocity += force;
eyePosition += eyeVelocity;
if (eyePosition.x > eyeClampUpper.x) eyePosition.x = eyeClampUpper.x;
if (eyePosition.y > eyeClampUpper.y) eyePosition.y = eyeClampUpper.y;
if (eyePosition.z > eyeClampUpper.z) eyePosition.z = eyeClampUpper.z;
if (eyePosition.x < eyeClampLower.x) eyePosition.x = eyeClampLower.x;
if (eyePosition.y < eyeClampLower.y) eyePosition.y = eyeClampLower.y;
if (eyePosition.z < eyeClampLower.z) eyePosition.z = eyeClampLower.z;
}
//
//#############################################################################
// @004b30ec -- the body ROTATION spring. Same shape with three verified
// quirks: the spring force is X/Z-CROSSED (k.x*d.z, k.y*d.y, k.z*d.x); the
// damping is componentwise UNcrossed; and the orientation step crosses AGAIN
// (x += v.z, z += v.x).
//#############################################################################
//
void
Gyroscope::IntegrateBody(Scalar time_slice)
{
Vector3D
toward_negative,
toward_positive,
force;
toward_negative.Subtract(bodyOrientation, rotationNegSpring);
toward_positive.Subtract(bodyOrientation, rotationPosSpring);
force.x = rotationSpringConstant.x * toward_negative.z;
force.y = rotationSpringConstant.y * toward_negative.y;
force.z = rotationSpringConstant.z * toward_negative.x;
bodyForce += force;
force.x = rotationSpringConstant.x * toward_positive.z;
force.y = rotationSpringConstant.y * toward_positive.y;
force.z = rotationSpringConstant.z * toward_positive.x;
bodyForce += force;
bodyWork = bodyForce;
force.Multiply(bodyWork, time_slice);
bodyVelocity += force;
bodyForce.Multiply(rotationDampingConstant, bodyVelocity);
bodyWork = bodyForce;
force.Multiply(bodyWork, time_slice);
bodyVelocity += force;
bodyOrientation.x += bodyVelocity.z;
bodyOrientation.y += bodyVelocity.y;
bodyOrientation.z += bodyVelocity.x;
if (bodyOrientation.y > bodyClampUpper.y) bodyOrientation.y = bodyClampUpper.y;
if (bodyOrientation.x > bodyClampUpper.x) bodyOrientation.x = bodyClampUpper.x;
if (bodyOrientation.z > bodyClampUpper.z) bodyOrientation.z = bodyClampUpper.z;
if (bodyOrientation.y < bodyClampLower.y) bodyOrientation.y = bodyClampLower.y;
if (bodyOrientation.x < bodyClampLower.x) bodyOrientation.x = bodyClampLower.x;
if (bodyOrientation.z < bodyClampLower.z) bodyOrientation.z = bodyClampLower.z;
}
//
//#############################################################################
// @004b33e0 / @004b34ec -- the joint writes, called from the MECH master
// performance tail after the gait pass.
//
// WriteEyeJoint scales the EyeJoint's own channel by swayAngle -- the idle
// wander. WriteMechJoint pushes the integrated state onto 'jointeye'
// (BallTranslation ONLY): TRANSLATION <- eyePosition, ROTATION <-
// bodyOrientation, each write gated on a real change.
//#############################################################################
//
static const Scalar
GyroQuantiseEps = 0.0001f;
void
Gyroscope::WriteEyeJoint()
{
Check(this);
if (eyeJointNode == NULL)
{
return;
}
switch (eyeJointNode->GetJointType())
{
case Joint::HingeXJointType:
case Joint::HingeYJointType:
case Joint::HingeZJointType:
{
Scalar
base = (Scalar)eyeJointNode->GetRadians(),
swayed = swayAngle * base;
if (
base - swayed > GyroQuantiseEps ||
swayed - base > GyroQuantiseEps
)
{
eyeJointNode->SetRotation(Radian(swayed));
}
}
break;
case Joint::BallJointType:
case Joint::BallTranslationJointType:
{
EulerAngles
base = eyeJointNode->GetEulerAngles();
EulerAngles
swayed(
Radian((Scalar)base.pitch * swayAngle),
Radian((Scalar)base.yaw * swayAngle),
Radian((Scalar)base.roll * swayAngle));
eyeJointNode->SetRotation(swayed);
}
break;
default:
break;
}
}
void
Gyroscope::WriteMechJoint()
{
Check(this);
if (
mechJointNode == NULL ||
mechJointNode->GetJointType() != Joint::BallTranslationJointType
)
{
return;
}
{
Point3D
current = mechJointNode->GetTranslation();
if (
current.x - eyePosition.x > GyroQuantiseEps ||
eyePosition.x - current.x > GyroQuantiseEps ||
current.y - eyePosition.y > GyroQuantiseEps ||
eyePosition.y - current.y > GyroQuantiseEps ||
current.z - eyePosition.z > GyroQuantiseEps ||
eyePosition.z - current.z > GyroQuantiseEps
)
{
mechJointNode->SetTranslation(
Point3D(eyePosition.x, eyePosition.y, eyePosition.z));
}
}
{
EulerAngles
current = mechJointNode->GetEulerAngles();
if (
(Scalar)current.pitch - bodyOrientation.x > GyroQuantiseEps ||
bodyOrientation.x - (Scalar)current.pitch > GyroQuantiseEps ||
(Scalar)current.yaw - bodyOrientation.y > GyroQuantiseEps ||
bodyOrientation.y - (Scalar)current.yaw > GyroQuantiseEps ||
(Scalar)current.roll - bodyOrientation.z > GyroQuantiseEps ||
bodyOrientation.z - (Scalar)current.roll > GyroQuantiseEps
)
{
mechJointNode->SetRotation(
EulerAngles(
Radian(bodyOrientation.x),
Radian(bodyOrientation.y),
Radian(bodyOrientation.z)));
}
}
}
//
//#############################################################################
// The hit-bounce kicks (@004b2d8c / @004b2de4 / @004b2e50): negate the hit
// direction, scale by exageration, land in the force accumulators. The
// torque zeroes its .y then negates the WHOLE vector; the vertical kick
// keeps only the pitch component and mirrors it into .y.
//#############################################################################
//
void
Gyroscope::ApplyDamageImpulse(Scalar x, Scalar y, Scalar z, Scalar magnitude)
{
Check(this);
Vector3D
direction(-x, -y, -z);
magnitude *= exageration;
direction *= magnitude;
eyeForce += direction;
}
void
Gyroscope::ApplyDamageTorque(Scalar x, Scalar y, Scalar z, Scalar magnitude)
{
Check(this);
Vector3D
direction(-x, -y, -z);
magnitude *= exageration;
direction *= magnitude;
bodyForce += direction;
bodyForce.y = 0.0f;
bodyForce.Negate(bodyForce);
}
void
Gyroscope::ApplyVerticalImpulse(Scalar pitch, Scalar magnitude)
{
Check(this);
Vector3D
direction(pitch, 0.0f, 0.0f);
magnitude *= exageration;
direction *= magnitude;
bodyForce += direction;
bodyForce.y = bodyForce.x;
}
//
//#############################################################################
// @004b2980 -- the damage fan-out: every non-collision hit shakes the
// cockpit. Direction = the record's damageForce, or a random horizontal
// when it is ~zero; rotated into the yaw-only torso frame and re-normalized;
// per-type scaled amount / multiplier * response (Explosive alone reads
// burstCount); each channel clamped at 1.3 UPPER only; then the four kicks.
//#############################################################################
//
void
Gyroscope::ApplyDamageResponse(const Damage &damage)
{
Check(this);
if (damage.damageAmount <= 0.0f)
{
return;
}
if (damage.damageType == Damage::CollisionDamageType)
{
return;
}
Scalar
trans = 0.0f,
pitch_roll = 0.0f,
yaw = 0.0f,
vibration = 0.0f;
Vector3D
direction;
if (
damage.damageForce.x < 1.0e-4f && damage.damageForce.x > -1.0e-4f &&
damage.damageForce.y < 1.0e-4f && damage.damageForce.y > -1.0e-4f &&
damage.damageForce.z < 1.0e-4f && damage.damageForce.z > -1.0e-4f
)
{
direction.x = ((Scalar)Random >= 0.5f) ? (Scalar)Random : -(Scalar)Random;
direction.z = ((Scalar)Random >= 0.5f) ? (Scalar)Random : -(Scalar)Random;
direction.y = 0.0f;
}
else
{
direction = damage.damageForce;
}
direction.Normalize(direction);
//
// Rotate the WORLD hit direction into the yaw-only body frame (the
// torso twist), then re-normalize.
//
placeRot = Vector3D(0.0f, *externalPitchPtr, 0.0f);
placePos = Vector3D(0.0f, 0.0f, 0.0f);
{
AffineMatrix
frame;
frame = EulerAngles(
Radian(placeRot.x), Radian(placeRot.y), Radian(placeRot.z));
int
i;
for (i = 0; i < 12; ++i)
{
workMatrix[i] = frame.entries[i];
}
Vector3D
world = direction;
direction.x =
world.x * workMatrix[0] + world.y * workMatrix[1] +
world.z * workMatrix[2];
direction.y =
world.x * workMatrix[4] + world.y * workMatrix[5] +
world.z * workMatrix[6];
direction.z =
world.x * workMatrix[8] + world.y * workMatrix[9] +
world.z * workMatrix[10];
}
direction.Normalize(direction);
switch (damage.damageType)
{
case Damage::BallisticDamageType:
trans = damage.damageAmount / damageMultiplier[1] * damageResponse[1].trans;
pitch_roll = damage.damageAmount / damageMultiplier[1] * damageResponse[1].pitchRoll;
yaw = damage.damageAmount / damageMultiplier[1] * damageResponse[1].yaw;
vibration = damage.damageAmount / damageMultiplier[1] * damageResponse[1].vibration;
break;
case Damage::ExplosiveDamageType:
trans = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].trans;
pitch_roll = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].pitchRoll;
yaw = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].yaw;
vibration = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].vibration;
break;
case Damage::LaserDamageType:
trans = damage.damageAmount / damageMultiplier[3] * damageResponse[3].trans;
pitch_roll = damage.damageAmount / damageMultiplier[3] * damageResponse[3].pitchRoll;
yaw = damage.damageAmount / damageMultiplier[3] * damageResponse[3].yaw;
vibration = damage.damageAmount / damageMultiplier[3] * damageResponse[3].vibration;
break;
case Damage::EnergyDamageType:
trans = damage.damageAmount / damageMultiplier[4] * damageResponse[4].trans;
pitch_roll = damage.damageAmount / damageMultiplier[4] * damageResponse[4].pitchRoll;
yaw = damage.damageAmount / damageMultiplier[4] * damageResponse[4].yaw;
vibration = damage.damageAmount / damageMultiplier[4] * damageResponse[4].vibration;
break;
default:
break;
}
if (trans > 1.3f) trans = 1.3f;
if (pitch_roll > 1.3f) pitch_roll = 1.3f;
if (yaw > 1.3f) yaw = 1.3f;
if (vibration > 1.3f) vibration = 1.3f;
ApplyDamageImpulse(direction.x, direction.y, direction.z, trans);
ApplyDamageTorque(direction.x, direction.y, direction.z, pitch_roll);
ApplyDamageImpulse(
vibrationDirection.x, vibrationDirection.y, vibrationDirection.z,
vibration);
ApplyVerticalImpulse(yaw, yaw);
}
+201 -126
View File
@@ -1,126 +1,201 @@
//===========================================================================// //===========================================================================//
// File: gyro.hpp // // File: gyro.hpp //
// Project: BattleTech Brick: Mech subsystems // // Project: BattleTech Brick: Mech subsystems //
// Contents: Gyroscope -- the eye/body stabilisation subsystem // // Contents: Gyroscope -- the eye/body stabilisation subsystem //
//---------------------------------------------------------------------------// //---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. // // Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide // // All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================// //===========================================================================//
#if !defined(GYRO_HPP) #if !defined(GYRO_HPP)
# define GYRO_HPP # define GYRO_HPP
# if !defined(POWERSUB_HPP) # if !defined(POWERSUB_HPP)
# include <powersub.hpp> # include <powersub.hpp>
# endif # endif
//##################### Forward Class Declarations ####################### //##################### Forward Class Declarations #######################
class Mech; class Mech;
//########################################################################### //###########################################################################
//################### Gyroscope Model Resource ######################### //################### Gyroscope Model Resource #########################
//########################################################################### //###########################################################################
struct Gyroscope__SubsystemResource: //
public PowerWatcher::SubsystemResource // One per-damage-type response row: how hard a hit of that type kicks
{ // the four gyro channels.
Scalar reservedF4; //
Scalar exageration; struct Gyroscope__DamageResponse
Scalar maxAnimationNoise; {
Scalar minAnimationNoise; Scalar trans;
Scalar rotationPerSecond; Scalar pitchRoll;
Scalar percentageOnNormal; Scalar yaw;
Scalar percentageOnDestruction; Scalar vibration;
Scalar percentageOnDegradation; };
Scalar percentageOnFailure;
Vector3D springConstant; struct Gyroscope__SubsystemResource:
Vector3D dampingConstant; public PowerWatcher::SubsystemResource
Vector3D posSpring; {
Vector3D negSpring; Scalar reservedF4;
Vector3D rotationSpringConstant; Scalar exageration;
Vector3D rotationDampingConstant; Scalar maxAnimationNoise;
Vector3D rotationPosSpring; Scalar minAnimationNoise;
Vector3D rotationNegSpring; Scalar rotationPerSecond;
char eyeJoint[32]; Scalar percentageOnNormal;
char mechJoint[32]; Scalar percentageOnDestruction;
Scalar collisionDamageMultiplier; Scalar percentageOnDegradation;
Scalar ballisticDamageMultiplier; Scalar percentageOnFailure;
Scalar explosiveDamageMultiplier; Vector3D springConstant;
Scalar laserDamageMultiplier; Vector3D dampingConstant;
Scalar energyDamageMultiplier; Vector3D posSpring;
}; Vector3D negSpring;
Vector3D rotationSpringConstant;
//########################################################################### Vector3D rotationDampingConstant;
//############################## Gyroscope ############################# Vector3D rotationPosSpring;
//########################################################################### Vector3D rotationNegSpring;
// char eyeJoint[32];
// The gyro drives the cockpit eye joint (view stabilisation) and the body char mechJoint[32];
// lean. The tuning constants are read from the resource; the eye/body Scalar collisionDamageMultiplier;
// dynamics state (springs, forces, velocities, work matrices) is advanced by Scalar ballisticDamageMultiplier;
// the per-frame GyroscopeSimulation and held in dynamicsState until that Scalar explosiveDamageMultiplier;
// (staged) method is reconstructed with the named fields. Scalar laserDamageMultiplier;
// Scalar energyDamageMultiplier;
class Gyroscope:
public PowerWatcher //
{ // The five response quads follow the multipliers in the stream
public: // (record size 0x21C -- binary-verified layout).
static Derivation ClassDerivations; //
static SharedData DefaultData; Gyroscope__DamageResponse collisionDamageResponse;
Gyroscope__DamageResponse ballisticDamageResponse;
typedef void Gyroscope__DamageResponse explosiveDamageResponse;
(Gyroscope::*Performance)(Scalar time_slice); Gyroscope__DamageResponse laserDamageResponse;
void Gyroscope__DamageResponse energyDamageResponse;
SetPerformance(Performance performance) };
{
Check(this); //###########################################################################
activePerformance = (Simulation::Performance)performance; //############################## Gyroscope #############################
} //###########################################################################
//
static Logical // The gyro drives the cockpit eye joint (view stabilisation) and the body
TestClass(Mech &); // lean. The tuning constants are read from the resource; the eye/body
Logical // dynamics state (springs, forces, velocities, work matrices) is advanced by
TestInstance() const; // the per-frame GyroscopeSimulation and held in dynamicsState until that
void // (staged) method is reconstructed with the named fields.
ResetToInitialState(); //
void class Gyroscope:
GyroscopeSimulation(Scalar time_slice); public PowerWatcher
{
public: public:
typedef Gyroscope__SubsystemResource SubsystemResource; static Derivation ClassDerivations;
static SharedData DefaultData;
Gyroscope(
Mech *owner, typedef void
int subsystem_ID, (Gyroscope::*Performance)(Scalar time_slice);
SubsystemResource *subsystem_resource, void
SharedData &shared_data = DefaultData SetPerformance(Performance performance)
); {
~Gyroscope(); Check(this);
activePerformance = (Simulation::Performance)performance;
protected: }
Scalar exageration;
Scalar maxAnimationNoise; static Logical
Scalar minAnimationNoise; TestClass(Mech &);
Scalar rotationPerSecond; Logical
Scalar percentageOnNormal; TestInstance() const;
Scalar percentageOnDestruction; void
Scalar percentageOnDegradation; ResetToInitialState();
Scalar percentageOnFailure; void
Vector3D springConstant; GyroscopeSimulation(Scalar time_slice);
Vector3D dampingConstant;
Vector3D posSpring; //
Vector3D negSpring; // The two integrators (called by the sim) and the two joint writes
Vector3D rotationSpringConstant; // (called by the MECH master performance tail, after the gait --
Vector3D rotationDampingConstant; // NOT by the sim; binary @0x4aaf74/83).
Vector3D rotationPosSpring; //
Vector3D rotationNegSpring; void
Scalar damageMultiplier[5]; IntegrateEyeJoint(Scalar time_slice);
// void
// Eye/body dynamics accumulators advanced by GyroscopeSimulation IntegrateBody(Scalar time_slice);
// (positions, velocities, forces, clamps, work matrices, sway). void
// Reserved until the per-frame sim is reconstructed (phase 5). WriteEyeJoint();
// void
Scalar dynamicsState[64]; WriteMechJoint();
};
//
#endif // The hit-bounce feeds. ApplyDamageResponse is the damage hub's
// fan-out (every non-collision hit shakes the cockpit); the three
// primitive kicks are also fed directly by the collision CRUNCH.
//
void
ApplyDamageResponse(const Damage &damage);
void
ApplyDamageImpulse(Scalar x, Scalar y, Scalar z, Scalar magnitude);
void
ApplyDamageTorque(Scalar x, Scalar y, Scalar z, Scalar magnitude);
void
ApplyVerticalImpulse(Scalar pitch, Scalar magnitude);
public:
typedef Gyroscope__SubsystemResource SubsystemResource;
Gyroscope(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~Gyroscope();
protected:
Scalar exageration;
Scalar maxAnimationNoise;
Scalar minAnimationNoise;
Scalar rotationPerSecond;
Scalar percentageOnNormal;
Scalar percentageOnDestruction;
Scalar percentageOnDegradation;
Scalar percentageOnFailure;
Vector3D springConstant;
Vector3D dampingConstant;
Vector3D posSpring;
Vector3D negSpring;
Vector3D rotationSpringConstant;
Vector3D rotationDampingConstant;
Vector3D rotationPosSpring;
Vector3D rotationNegSpring;
Scalar damageMultiplier[5];
Gyroscope__DamageResponse
damageResponse[5];
//
// The eye TRANSLATION spring state (the steady eye offset + hit
// bounce) and the body ROTATION spring state (the hull tip). The
// clamps are COPIES of the spring pairs, taken at ctor time (the
// body pair scaled by 2).
//
Vector3D eyePosition, eyeVelocity, eyeForce, eyeWork;
Vector3D eyeClampUpper, eyeClampLower;
Vector3D bodyOrientation, bodyVelocity, bodyForce, bodyWork;
Vector3D bodyClampUpper, bodyClampLower;
//
// Sway (the animation-noise wander the sim slews between the
// powered/impaired percentages) and the placement scratch the
// damage fan-out rotates hit directions through.
//
Scalar swayAngle, swayBias, swayVelocity;
int swayActive;
Vector3D placeRot, placePos;
Scalar placeQuat[4];
Scalar workMatrix[12];
Scalar spare0;
Scalar *externalPitchPtr; // -> the torso twist (yaw-only hit
// frame); self-points at spare0
// until a torso binds it
Vector3D vibrationDirection; // the fixed up axis
Joint *eyeJointNode;
Joint *mechJointNode;
};
#endif
+70 -67
View File
@@ -1,67 +1,70 @@
# GYRO.CPP — reconstruction notes # GYRO.CPP — reconstruction notes
**Status: ctor + streaming RECONSTRUCTED; the per-frame simulation is the **Status: FULLY RECONSTRUCTED AND WIRED (2026-08-03). The simulation,
in-flight increment (plan below). Donor: BT411 gyro.cpp (1106 lines, task both integrators, both joint writes, all four damage hooks, and the three
#56 BYTE-EXACT [T1] on the integrators and the damage fan-out).** feeds (damage hub, collision crunch, master-perf joint-write dispatch) are
live; the registered Performance ticks without fault. Deep verification --
## Why this TU is the feel wave's cornerstone the visible cockpit bounce under fire -- is an operator/rig item. Donor:
BT411 gyro.cpp (task #56 byte-exact).**
The gyro owns the cockpit's physical feel: the eye TRANSLATION spring (the
steady eye offset + hit bounce, written to the 'jointeye' BallTranslation ## Why this TU is the feel wave's cornerstone
joint that `siteeyepoint` rides) and the body ROTATION spring (the hull tip).
The 5.3.99 collision block's staged "gyro crunch" feed and the damage hub's The gyro owns the cockpit's physical feel: the eye TRANSLATION spring (the
staged hit-bounce both land here. steady eye offset + hit bounce, written to the 'jointeye' BallTranslation
joint that `siteeyepoint` rides) and the body ROTATION spring (the hull tip).
## The transcription plan (all donor line refs gathered 2026-08-03) The 5.3.99 collision block's staged "gyro crunch" feed and the damage hub's
staged hit-bounce both land here.
1. **Members** (carve `dynamicsState[64]`): eyePosition/eyeVelocity/eyeForce/
eyeWork (Vector3D), bodyOrientation/bodyVelocity/bodyForce/bodyWork, ## The transcription plan (all donor line refs gathered 2026-08-03)
swayAngle/swayBias/swayVelocity/swayActive, placeRot/placePos,
workMatrix[12], externalPitchPtr (the torso-twist Scalar*), eyeJointNode/ 1. **Members** (carve `dynamicsState[64]`): eyePosition/eyeVelocity/eyeForce/
mechJointNode (Joint*), damageResponse[5] {trans,pitchRoll,yaw,vibration}, eyeWork (Vector3D), bodyOrientation/bodyVelocity/bodyForce/bodyWork,
vibrationDirection (fixed up axis). Clamps are ALIASES: eyeClampUpper == swayAngle/swayBias/swayVelocity/swayActive, placeRot/placePos,
posSpring, eyeClampLower == negSpring; body likewise on the rotation pair. workMatrix[12], externalPitchPtr (the torso-twist Scalar*), eyeJointNode/
2. **GyroscopeSimulation** (@004b275c): PowerWatcher::Simulation(dt); the mechJointNode (Joint*), damageResponse[5] {trans,pitchRoll,yaw,vibration},
impaired pick (HeatModelOff || electrical != Ready || heat == FailureHeat) vibrationDirection (fixed up axis). Clamps are ALIASES: eyeClampUpper ==
selects percentageOnDestruction vs Normal + swayBias as the sway target; posSpring, eyeClampLower == negSpring; body likewise on the rotation pair.
slew by rotationPerSecond*dt with no-overshoot clamps, band-clamp into 2. **GyroscopeSimulation** (@004b275c): PowerWatcher::Simulation(dt); the
[minAnimationNoise, maxAnimationNoise]; IntegrateEyeJoint; IntegrateBody. impaired pick (HeatModelOff || electrical != Ready || heat == FailureHeat)
The performance ENDS there — WriteEyeJoint/WriteMechJoint are called from selects percentageOnDestruction vs Normal + swayBias as the sway target;
the MECH master perf tail (@0x4aaf74/83), i.e. our Simulate gait seam, slew by rotationPerSecond*dt with no-overshoot clamps, band-clamp into
with the death/leg-anim gates. [minAnimationNoise, maxAnimationNoise]; IntegrateEyeJoint; IntegrateBody.
3. **IntegrateEyeJoint** (@004b2ec0, byte-exact): both spring terms use The performance ENDS there — WriteEyeJoint/WriteMechJoint are called from
springConstant; damping OVERWRITES the force accumulator (carries into the MECH master perf tail (@0x4aaf74/83), i.e. our Simulate gait seam,
next frame by design); position += velocity with NO dt; per-axis clamp with the death/leg-anim gates.
min-vs-posSpring then max-vs-negSpring. 3. **IntegrateEyeJoint** (@004b2ec0, byte-exact): both spring terms use
4. **IntegrateBody** (@004b30ec): same shape, three quirks — spring force springConstant; damping OVERWRITES the force accumulator (carries into
X/Z-CROSSED (k.x*d.z, k.y*d.y, k.z*d.x); damping componentwise uncrossed; next frame by design); position += velocity with NO dt; per-axis clamp
orientation += velocity CROSSED again (x+=v.z, z+=v.x). Clamp vs the min-vs-posSpring then max-vs-negSpring.
rotation spring pair. 4. **IntegrateBody** (@004b30ec): same shape, three quirks — spring force
5. **WriteEyeJoint** (@004b33e0): swayAngle onto the EyeJoint node — scalar X/Z-CROSSED (k.x*d.z, k.y*d.y, k.z*d.x); damping componentwise uncrossed;
channel scaled for hinge types <3, vector channel scaled for 4/5, write orientation += velocity CROSSED again (x+=v.z, z+=v.x). Clamp vs the
only past QuantiseEps. **WriteMechJoint** (@004b34ec): 'jointeye' type-5 rotation spring pair.
ONLY: SetTranslation(eyePosition) + SetRotation(EulerAngles from 5. **WriteEyeJoint** (@004b33e0): swayAngle onto the EyeJoint node — scalar
bodyOrientation), each QuantiseEps-gated. channel scaled for hinge types <3, vector channel scaled for 4/5, write
6. **Damage hooks**: ApplyDamageImpulse (negate dir, *exageration, += only past QuantiseEps. **WriteMechJoint** (@004b34ec): 'jointeye' type-5
eyeForce); ApplyDamageTorque (same into bodyForce, then bodyForce.y = 0, ONLY: SetTranslation(eyePosition) + SetRotation(EulerAngles from
then whole-vector NEGATE); ApplyVerticalImpulse (x-only kick, .y mirrors bodyOrientation), each QuantiseEps-gated.
.x). **ApplyDamageResponse** (@004b2980, byte-exact): no-op on Collision 6. **Damage hooks**: ApplyDamageImpulse (negate dir, *exageration, +=
or zero amount; direction = damageForce or random-horizontal when ~0; eyeForce); ApplyDamageTorque (same into bodyForce, then bodyForce.y = 0,
rotate into the yaw-only torso frame (externalPitchPtr) and re-normalize; then whole-vector NEGATE); ApplyVerticalImpulse (x-only kick, .y mirrors
per-type scale amount/multiplier[type]*response[type].{4 terms} .x). **ApplyDamageResponse** (@004b2980, byte-exact): no-op on Collision
(Explosive alone multiplies burstCount); clamp each at 1.3 UPPER only; or zero amount; direction = damageForce or random-horizontal when ~0;
then the four kicks: impulse(dir,trans), torque(dir,pitchRoll), rotate into the yaw-only torso frame (externalPitchPtr) and re-normalize;
vibration along vibrationDirection, vertical(yaw term). per-type scale amount/multiplier[type]*response[type].{4 terms}
7. **Wiring**: the damage hub's staged gyro feed (MECH.CPP (Explosive alone multiplies burstCount); clamp each at 1.3 UPPER only;
TakeDamageMessageHandler step 1) calls ApplyDamageResponse; the crushable then the four kicks: impulse(dir,trans), torque(dir,pitchRoll),
CRUNCH branch calls Torque(n,0.4)+Impulse(up,0.2) (already-shaped site); vibration along vibrationDirection, vertical(yaw term).
Simulate's gait seam calls WriteEyeJoint+WriteMechJoint on the master 7. **Wiring**: the damage hub's staged gyro feed (MECH.CPP
after the gait advances (death/leg gates per the binary tail). TakeDamageMessageHandler step 1) calls ApplyDamageResponse; the crushable
8. **Family bridge**: Mech::DistributeCollisionDamage already references CRUNCH branch calls Torque(n,0.4)+Impulse(up,0.2) (already-shaped site);
Gyroscope::ClassDerivations directly — no ODR bridge needed in our tree. Simulate's gait seam calls WriteEyeJoint+WriteMechJoint on the master
after the gait advances (death/leg gates per the binary tail).
## Verification plan 8. **Family bridge**: Mech::DistributeCollisionDamage already references
Gyroscope::ClassDerivations directly — no ODR bridge needed in our tree.
BT_GYRO_LOG prints the joint writes + NaN sentinel; a live arena run with
enemy fire should show [gyro-dmg] fan-outs and 'jointeye' translation motion ## Verification plan
on the wire (the cockpit eye rides it — visible as view bounce on hits).
BT_GYRO_LOG prints the joint writes + NaN sentinel; a live arena run with
enemy fire should show [gyro-dmg] fan-outs and 'jointeye' translation motion
on the wire (the cockpit eye rides it — visible as view bounce on hits).
+36 -5
View File
@@ -1078,12 +1078,13 @@ void
Check(this); Check(this);
Check(message); Check(message);
if (gyroSubsystem != NULL && getenv("BT_MECH_LOG")) //
// The RAW record feeds the gyro FIRST -- even an invalid-zone hit shakes
// the cockpit (binary hub order step 1).
//
if (gyroSubsystem != NULL)
{ {
DEBUG_STREAM << "[gyro] hit feed staged (type=" ((Gyroscope *)gyroSubsystem)->ApplyDamageResponse(message->damageData);
<< (int)message->damageData.damageType
<< " amt=" << message->damageData.damageAmount << ")"
<< endl << flush;
} }
lastInflictingID = message->inflictingEntity; lastInflictingID = message->inflictingEntity;
@@ -1613,6 +1614,19 @@ void
AdvanceLegAnimation(time_slice); AdvanceLegAnimation(time_slice);
} }
//
// The gyro's joint writes (binary master-perf tail @0x4aaf74/83, AFTER
// the animation pass): the idle sway onto the EyeJoint, and the
// integrated eye offset + body tip onto 'jointeye' -- the joint the
// cockpit eyepoint rides. Gated off during the death clips exactly as
// the tail is.
//
if (gyroSubsystem != NULL && MovementMode() < 5)
{
((Gyroscope *)gyroSubsystem)->WriteEyeJoint();
((Gyroscope *)gyroSubsystem)->WriteMechJoint();
}
// //
// THE TURN-IN-PLACE DISPATCHER (master perf @0x4aa505-0x4aa588, decoded // THE TURN-IN-PLACE DISPATCHER (master perf @0x4aa505-0x4aa588, decoded
// from raw disasm -- Ghidra never decompiled the function). From // from raw disasm -- Ghidra never decompiled the function). From
@@ -2009,6 +2023,23 @@ void
localOrigin.linearPosition = post_snap_position; localOrigin.linearPosition = post_snap_position;
localToWorld = localOrigin; localToWorld = localOrigin;
MoveCollisionVolume(); MoveCollisionVolume();
//
// The gyro CRUNCH (binary @4aa7ce-4aa871): torque 0.4 along the
// normalized contact force, upward impulse 0.2. The Normalize
// is unguarded in the binary too.
//
if (gyroSubsystem != NULL)
{
Vector3D
crunch_normal;
crunch_normal.Normalize(collision_damage.damageForce);
((Gyroscope *)gyroSubsystem)->ApplyDamageTorque(
crunch_normal.x, crunch_normal.y, crunch_normal.z, 0.4f);
((Gyroscope *)gyroSubsystem)->ApplyDamageImpulse(
0.0f, 1.0f, 0.0f, 0.2f);
}
collision_damage.damageAmount = 0.0f; collision_damage.damageAmount = 0.0f;
if (getenv("BT_MECH_LOG")) if (getenv("BT_MECH_LOG"))