From 41fca98f8519446bfc1ecf01c906ad12d400a1ec Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 3 Aug 2026 16:42:56 -0500 Subject: [PATCH] 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 --- emulator/render-bridge/pod_render_gyro.conf | 88 +++ restoration/source410/BT/GYRO.CPP | 720 ++++++++++++++++---- restoration/source410/BT/GYRO.HPP | 327 +++++---- restoration/source410/BT/GYRO.NOTES.md | 137 ++-- restoration/source410/BT/MECH.CPP | 41 +- 5 files changed, 998 insertions(+), 315 deletions(-) create mode 100644 emulator/render-bridge/pod_render_gyro.conf diff --git a/emulator/render-bridge/pod_render_gyro.conf b/emulator/render-bridge/pod_render_gyro.conf new file mode 100644 index 00000000..cfd40ed5 --- /dev/null +++ b/emulator/render-bridge/pod_render_gyro.conf @@ -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 + diff --git a/restoration/source410/BT/GYRO.CPP b/restoration/source410/BT/GYRO.CPP index 864359bd..b2b8d62e 100644 --- a/restoration/source410/BT/GYRO.CPP +++ b/restoration/source410/BT/GYRO.CPP @@ -1,117 +1,603 @@ -//===========================================================================// -// File: gyro.cpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: Gyroscope -- eye/body stabilisation // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(GYRO_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -Derivation - Gyroscope::ClassDerivations( - PowerWatcher::ClassDerivations, - "Gyroscope" - ); - -Gyroscope::SharedData - Gyroscope::DefaultData( - Gyroscope::ClassDerivations, - Subsystem::MessageHandlers, - Subsystem::AttributeIndex, - Subsystem::StateCount - ); - -Gyroscope::Gyroscope( - Mech *owner, - int subsystem_ID, - SubsystemResource *r, - SharedData &shared_data -): - PowerWatcher(owner, subsystem_ID, r, shared_data) -{ - Check(owner); - Check_Pointer(r); - - exageration = r->exageration; - maxAnimationNoise = r->maxAnimationNoise; - minAnimationNoise = r->minAnimationNoise; - rotationPerSecond = r->rotationPerSecond; - percentageOnNormal = r->percentageOnNormal; - percentageOnDestruction = r->percentageOnDestruction; - percentageOnDegradation = r->percentageOnDegradation; - percentageOnFailure = r->percentageOnFailure; - - springConstant = r->springConstant; - dampingConstant = r->dampingConstant; - posSpring = r->posSpring; - negSpring = r->negSpring; - rotationSpringConstant = r->rotationSpringConstant; - rotationDampingConstant = r->rotationDampingConstant; - rotationPosSpring = r->rotationPosSpring; - rotationNegSpring = r->rotationNegSpring; - - damageMultiplier[0] = r->collisionDamageMultiplier; - damageMultiplier[1] = r->ballisticDamageMultiplier; - damageMultiplier[2] = r->explosiveDamageMultiplier; - damageMultiplier[3] = r->laserDamageMultiplier; - damageMultiplier[4] = r->energyDamageMultiplier; - - // - // Zero the eye/body dynamics accumulators (the binary zeroes each; the - // per-frame sim seeds them from the springs above). - // - for (int i = 0; i < 64; ++i) - { - dynamicsState[i] = 0.0f; - } - - Check_Fpu(); -} - -Gyroscope::~Gyroscope() -{ -} - -Logical - Gyroscope::TestClass(Mech &) -{ - return True; -} - -Logical - Gyroscope::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -void - Gyroscope::ResetToInitialState() -{ - Check(this); - for (int i = 0; i < 64; ++i) - { - dynamicsState[i] = 0.0f; - } -} - -// -// Per-frame eye/body stabilisation (spring/damper integration driving the -// cockpit eye joint). Not yet reconstructed. -// -void - Gyroscope::GyroscopeSimulation(Scalar) -{ - Fail("Gyroscope::GyroscopeSimulation -- gyro.cpp not yet reconstructed"); -} +//===========================================================================// +// File: gyro.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Gyroscope -- eye/body stabilisation // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(GYRO_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(RANDOM_HPP) +# include +#endif + +Derivation + Gyroscope::ClassDerivations( + PowerWatcher::ClassDerivations, + "Gyroscope" + ); + +Gyroscope::SharedData + Gyroscope::DefaultData( + Gyroscope::ClassDerivations, + Subsystem::MessageHandlers, + Subsystem::AttributeIndex, + Subsystem::StateCount + ); + +Gyroscope::Gyroscope( + Mech *owner, + int subsystem_ID, + SubsystemResource *r, + SharedData &shared_data +): + PowerWatcher(owner, subsystem_ID, r, shared_data) +{ + Check(owner); + Check_Pointer(r); + + exageration = r->exageration; + maxAnimationNoise = r->maxAnimationNoise; + minAnimationNoise = r->minAnimationNoise; + rotationPerSecond = r->rotationPerSecond; + percentageOnNormal = r->percentageOnNormal; + percentageOnDestruction = r->percentageOnDestruction; + percentageOnDegradation = r->percentageOnDegradation; + percentageOnFailure = r->percentageOnFailure; + + springConstant = r->springConstant; + dampingConstant = r->dampingConstant; + posSpring = r->posSpring; + negSpring = r->negSpring; + rotationSpringConstant = r->rotationSpringConstant; + rotationDampingConstant = r->rotationDampingConstant; + rotationPosSpring = r->rotationPosSpring; + rotationNegSpring = r->rotationNegSpring; + + damageMultiplier[0] = r->collisionDamageMultiplier; + damageMultiplier[1] = r->ballisticDamageMultiplier; + damageMultiplier[2] = r->explosiveDamageMultiplier; + damageMultiplier[3] = r->laserDamageMultiplier; + damageMultiplier[4] = r->energyDamageMultiplier; + + damageResponse[0] = r->collisionDamageResponse; + damageResponse[1] = r->ballisticDamageResponse; + damageResponse[2] = r->explosiveDamageResponse; + damageResponse[3] = r->laserDamageResponse; + damageResponse[4] = r->energyDamageResponse; + + // + // The clamps are ctor-time COPIES of the spring pairs; the body pair is + // doubled. Every accumulator zeroes -- the binary leaves nothing to + // allocator fill (the donor's history records a NaN poison from exactly + // that omission). + // + eyeClampUpper = posSpring; + eyeClampLower = negSpring; + bodyClampUpper.Multiply(rotationPosSpring, 2.0f); + bodyClampLower.Multiply(rotationNegSpring, 2.0f); + + { + Vector3D + vzero(0.0f, 0.0f, 0.0f); + eyePosition = eyeForce = eyeVelocity = eyeWork = vzero; + bodyOrientation = bodyForce = bodyVelocity = bodyWork = vzero; + placePos = placeRot = vzero; + placeQuat[0] = placeQuat[1] = placeQuat[2] = 0.0f; + placeQuat[3] = 1.0f; + + int i; + for (i = 0; i < 12; ++i) + { + workMatrix[i] = 0.0f; + } + workMatrix[0] = workMatrix[5] = workMatrix[10] = 1.0f; + + spare0 = 0.0f; + externalPitchPtr = &spare0; + } + vibrationDirection = Vector3D(0.0f, 1.0f, 0.0f); + swayAngle = 0.0f; + swayVelocity = 0.0f; + swayActive = 0; + swayBias = 0.0f; + + 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); +} diff --git a/restoration/source410/BT/GYRO.HPP b/restoration/source410/BT/GYRO.HPP index 1cbc0f54..73333daf 100644 --- a/restoration/source410/BT/GYRO.HPP +++ b/restoration/source410/BT/GYRO.HPP @@ -1,126 +1,201 @@ -//===========================================================================// -// File: gyro.hpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: Gyroscope -- the eye/body stabilisation subsystem // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(GYRO_HPP) -# define GYRO_HPP - -# if !defined(POWERSUB_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - -//########################################################################### -//################### Gyroscope Model Resource ######################### -//########################################################################### - - struct Gyroscope__SubsystemResource: - public PowerWatcher::SubsystemResource - { - Scalar reservedF4; - 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; - char eyeJoint[32]; - char mechJoint[32]; - Scalar collisionDamageMultiplier; - Scalar ballisticDamageMultiplier; - Scalar explosiveDamageMultiplier; - Scalar laserDamageMultiplier; - Scalar energyDamageMultiplier; - }; - -//########################################################################### -//############################## Gyroscope ############################# -//########################################################################### -// -// The gyro drives the cockpit eye joint (view stabilisation) and the body -// lean. The tuning constants are read from the resource; the eye/body -// dynamics state (springs, forces, velocities, work matrices) is advanced by -// the per-frame GyroscopeSimulation and held in dynamicsState until that -// (staged) method is reconstructed with the named fields. -// - class Gyroscope: - public PowerWatcher - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - typedef void - (Gyroscope::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - void - ResetToInitialState(); - void - GyroscopeSimulation(Scalar time_slice); - - 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]; - // - // Eye/body dynamics accumulators advanced by GyroscopeSimulation - // (positions, velocities, forces, clamps, work matrices, sway). - // Reserved until the per-frame sim is reconstructed (phase 5). - // - Scalar dynamicsState[64]; - }; - -#endif +//===========================================================================// +// File: gyro.hpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Gyroscope -- the eye/body stabilisation subsystem // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(GYRO_HPP) +# define GYRO_HPP + +# if !defined(POWERSUB_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + +//########################################################################### +//################### Gyroscope Model Resource ######################### +//########################################################################### + + // + // One per-damage-type response row: how hard a hit of that type kicks + // the four gyro channels. + // + struct Gyroscope__DamageResponse + { + Scalar trans; + Scalar pitchRoll; + Scalar yaw; + Scalar vibration; + }; + + struct Gyroscope__SubsystemResource: + public PowerWatcher::SubsystemResource + { + Scalar reservedF4; + 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; + char eyeJoint[32]; + char mechJoint[32]; + Scalar collisionDamageMultiplier; + Scalar ballisticDamageMultiplier; + Scalar explosiveDamageMultiplier; + Scalar laserDamageMultiplier; + Scalar energyDamageMultiplier; + + // + // The five response quads follow the multipliers in the stream + // (record size 0x21C -- binary-verified layout). + // + Gyroscope__DamageResponse collisionDamageResponse; + Gyroscope__DamageResponse ballisticDamageResponse; + Gyroscope__DamageResponse explosiveDamageResponse; + Gyroscope__DamageResponse laserDamageResponse; + Gyroscope__DamageResponse energyDamageResponse; + }; + +//########################################################################### +//############################## Gyroscope ############################# +//########################################################################### +// +// The gyro drives the cockpit eye joint (view stabilisation) and the body +// lean. The tuning constants are read from the resource; the eye/body +// dynamics state (springs, forces, velocities, work matrices) is advanced by +// the per-frame GyroscopeSimulation and held in dynamicsState until that +// (staged) method is reconstructed with the named fields. +// + class Gyroscope: + public PowerWatcher + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + typedef void + (Gyroscope::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + void + ResetToInitialState(); + void + GyroscopeSimulation(Scalar time_slice); + + // + // The two integrators (called by the sim) and the two joint writes + // (called by the MECH master performance tail, after the gait -- + // NOT by the sim; binary @0x4aaf74/83). + // + void + IntegrateEyeJoint(Scalar time_slice); + void + IntegrateBody(Scalar time_slice); + void + WriteEyeJoint(); + void + WriteMechJoint(); + + // + // 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 diff --git a/restoration/source410/BT/GYRO.NOTES.md b/restoration/source410/BT/GYRO.NOTES.md index 14b9f2cb..6eb5b7ce 100644 --- a/restoration/source410/BT/GYRO.NOTES.md +++ b/restoration/source410/BT/GYRO.NOTES.md @@ -1,67 +1,70 @@ -# GYRO.CPP — reconstruction notes - -**Status: ctor + streaming RECONSTRUCTED; the per-frame simulation is the -in-flight increment (plan below). Donor: BT411 gyro.cpp (1106 lines, task -#56 BYTE-EXACT [T1] on the integrators and the damage fan-out).** - -## Why this TU is the feel wave's cornerstone - -The gyro owns the cockpit's physical feel: the eye TRANSLATION spring (the -steady eye offset + hit bounce, written to the 'jointeye' BallTranslation -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 -staged hit-bounce both land here. - -## The transcription plan (all donor line refs gathered 2026-08-03) - -1. **Members** (carve `dynamicsState[64]`): eyePosition/eyeVelocity/eyeForce/ - eyeWork (Vector3D), bodyOrientation/bodyVelocity/bodyForce/bodyWork, - swayAngle/swayBias/swayVelocity/swayActive, placeRot/placePos, - workMatrix[12], externalPitchPtr (the torso-twist Scalar*), eyeJointNode/ - mechJointNode (Joint*), damageResponse[5] {trans,pitchRoll,yaw,vibration}, - vibrationDirection (fixed up axis). Clamps are ALIASES: eyeClampUpper == - posSpring, eyeClampLower == negSpring; body likewise on the rotation pair. -2. **GyroscopeSimulation** (@004b275c): PowerWatcher::Simulation(dt); the - impaired pick (HeatModelOff || electrical != Ready || heat == FailureHeat) - selects percentageOnDestruction vs Normal + swayBias as the sway target; - slew by rotationPerSecond*dt with no-overshoot clamps, band-clamp into - [minAnimationNoise, maxAnimationNoise]; IntegrateEyeJoint; IntegrateBody. - The performance ENDS there — WriteEyeJoint/WriteMechJoint are called from - the MECH master perf tail (@0x4aaf74/83), i.e. our Simulate gait seam, - with the death/leg-anim gates. -3. **IntegrateEyeJoint** (@004b2ec0, byte-exact): both spring terms use - springConstant; damping OVERWRITES the force accumulator (carries into - next frame by design); position += velocity with NO dt; per-axis clamp - min-vs-posSpring then max-vs-negSpring. -4. **IntegrateBody** (@004b30ec): same shape, three quirks — spring force - X/Z-CROSSED (k.x*d.z, k.y*d.y, k.z*d.x); damping componentwise uncrossed; - orientation += velocity CROSSED again (x+=v.z, z+=v.x). Clamp vs the - rotation spring pair. -5. **WriteEyeJoint** (@004b33e0): swayAngle onto the EyeJoint node — scalar - channel scaled for hinge types <3, vector channel scaled for 4/5, write - only past QuantiseEps. **WriteMechJoint** (@004b34ec): 'jointeye' type-5 - ONLY: SetTranslation(eyePosition) + SetRotation(EulerAngles from - bodyOrientation), each QuantiseEps-gated. -6. **Damage hooks**: ApplyDamageImpulse (negate dir, *exageration, += - eyeForce); ApplyDamageTorque (same into bodyForce, then bodyForce.y = 0, - then whole-vector NEGATE); ApplyVerticalImpulse (x-only kick, .y mirrors - .x). **ApplyDamageResponse** (@004b2980, byte-exact): no-op on Collision - or zero amount; direction = damageForce or random-horizontal when ~0; - rotate into the yaw-only torso frame (externalPitchPtr) and re-normalize; - per-type scale amount/multiplier[type]*response[type].{4 terms} - (Explosive alone multiplies burstCount); clamp each at 1.3 UPPER only; - then the four kicks: impulse(dir,trans), torque(dir,pitchRoll), - vibration along vibrationDirection, vertical(yaw term). -7. **Wiring**: the damage hub's staged gyro feed (MECH.CPP - TakeDamageMessageHandler step 1) calls ApplyDamageResponse; the crushable - CRUNCH branch calls Torque(n,0.4)+Impulse(up,0.2) (already-shaped site); - Simulate's gait seam calls WriteEyeJoint+WriteMechJoint on the master - after the gait advances (death/leg gates per the binary tail). -8. **Family bridge**: Mech::DistributeCollisionDamage already references - Gyroscope::ClassDerivations directly — no ODR bridge needed in our tree. - -## Verification plan - -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). +# GYRO.CPP — reconstruction notes + +**Status: FULLY RECONSTRUCTED AND WIRED (2026-08-03). The simulation, +both integrators, both joint writes, all four damage hooks, and the three +feeds (damage hub, collision crunch, master-perf joint-write dispatch) are +live; the registered Performance ticks without fault. Deep verification -- +the visible cockpit bounce under fire -- is an operator/rig item. Donor: +BT411 gyro.cpp (task #56 byte-exact).** + +## Why this TU is the feel wave's cornerstone + +The gyro owns the cockpit's physical feel: the eye TRANSLATION spring (the +steady eye offset + hit bounce, written to the 'jointeye' BallTranslation +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 +staged hit-bounce both land here. + +## The transcription plan (all donor line refs gathered 2026-08-03) + +1. **Members** (carve `dynamicsState[64]`): eyePosition/eyeVelocity/eyeForce/ + eyeWork (Vector3D), bodyOrientation/bodyVelocity/bodyForce/bodyWork, + swayAngle/swayBias/swayVelocity/swayActive, placeRot/placePos, + workMatrix[12], externalPitchPtr (the torso-twist Scalar*), eyeJointNode/ + mechJointNode (Joint*), damageResponse[5] {trans,pitchRoll,yaw,vibration}, + vibrationDirection (fixed up axis). Clamps are ALIASES: eyeClampUpper == + posSpring, eyeClampLower == negSpring; body likewise on the rotation pair. +2. **GyroscopeSimulation** (@004b275c): PowerWatcher::Simulation(dt); the + impaired pick (HeatModelOff || electrical != Ready || heat == FailureHeat) + selects percentageOnDestruction vs Normal + swayBias as the sway target; + slew by rotationPerSecond*dt with no-overshoot clamps, band-clamp into + [minAnimationNoise, maxAnimationNoise]; IntegrateEyeJoint; IntegrateBody. + The performance ENDS there — WriteEyeJoint/WriteMechJoint are called from + the MECH master perf tail (@0x4aaf74/83), i.e. our Simulate gait seam, + with the death/leg-anim gates. +3. **IntegrateEyeJoint** (@004b2ec0, byte-exact): both spring terms use + springConstant; damping OVERWRITES the force accumulator (carries into + next frame by design); position += velocity with NO dt; per-axis clamp + min-vs-posSpring then max-vs-negSpring. +4. **IntegrateBody** (@004b30ec): same shape, three quirks — spring force + X/Z-CROSSED (k.x*d.z, k.y*d.y, k.z*d.x); damping componentwise uncrossed; + orientation += velocity CROSSED again (x+=v.z, z+=v.x). Clamp vs the + rotation spring pair. +5. **WriteEyeJoint** (@004b33e0): swayAngle onto the EyeJoint node — scalar + channel scaled for hinge types <3, vector channel scaled for 4/5, write + only past QuantiseEps. **WriteMechJoint** (@004b34ec): 'jointeye' type-5 + ONLY: SetTranslation(eyePosition) + SetRotation(EulerAngles from + bodyOrientation), each QuantiseEps-gated. +6. **Damage hooks**: ApplyDamageImpulse (negate dir, *exageration, += + eyeForce); ApplyDamageTorque (same into bodyForce, then bodyForce.y = 0, + then whole-vector NEGATE); ApplyVerticalImpulse (x-only kick, .y mirrors + .x). **ApplyDamageResponse** (@004b2980, byte-exact): no-op on Collision + or zero amount; direction = damageForce or random-horizontal when ~0; + rotate into the yaw-only torso frame (externalPitchPtr) and re-normalize; + per-type scale amount/multiplier[type]*response[type].{4 terms} + (Explosive alone multiplies burstCount); clamp each at 1.3 UPPER only; + then the four kicks: impulse(dir,trans), torque(dir,pitchRoll), + vibration along vibrationDirection, vertical(yaw term). +7. **Wiring**: the damage hub's staged gyro feed (MECH.CPP + TakeDamageMessageHandler step 1) calls ApplyDamageResponse; the crushable + CRUNCH branch calls Torque(n,0.4)+Impulse(up,0.2) (already-shaped site); + Simulate's gait seam calls WriteEyeJoint+WriteMechJoint on the master + after the gait advances (death/leg gates per the binary tail). +8. **Family bridge**: Mech::DistributeCollisionDamage already references + Gyroscope::ClassDerivations directly — no ODR bridge needed in our tree. + +## Verification plan + +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). diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 36f657de..49d6f335 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -1078,12 +1078,13 @@ void Check(this); 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=" - << (int)message->damageData.damageType - << " amt=" << message->damageData.damageAmount << ")" - << endl << flush; + ((Gyroscope *)gyroSubsystem)->ApplyDamageResponse(message->damageData); } lastInflictingID = message->inflictingEntity; @@ -1613,6 +1614,19 @@ void 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 // from raw disasm -- Ghidra never decompiled the function). From @@ -2009,6 +2023,23 @@ void localOrigin.linearPosition = post_snap_position; localToWorld = localOrigin; 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; if (getenv("BT_MECH_LOG"))