Files
TeslaRel410/restoration/source410/BT/MECH2.CPP
T
CydandClaude Fable 5 7c2363d89f BT410 5.3.96: the LIMP is real -- all twelve mech2 functions reconstructed, and the drag leg shows up on the wire
The "gimp" family had two conflicting donor readings -- limp vs jump-jet --
flagged in 5.3.94 rather than guessed at.  The binary settles it: LIMP.  The
mode test in every gimp function is mech+0x40 in {3,4}, the same values the
damage model documents as "limp gait graphic (left 3 / right 4)"; there is no
jump-jet control anywhere in the pod cockpit; and the donor's port routing
("graphicAlarm 3/4") was the same field under a different name.  Its
"run-jump clip" mech3 annotations were the misreading that started the murk.

RECONSTRUCTED, completing all 12 of mech2's census functions:

  GimpLegClipFinished  @004a7970    GimpBodyClipFinished @004a6344
  AdvanceLegAnimationGimp @004a71f4 AdvanceBodyAnimationGimp @004a5bf8
  + the limp branch atop both normal *ClipFinished
  + the limp pick in Simulate (replaces the normal advancers while limping)

HOW A LIMP WORKS, now from the bytes rather than description:

  It replaces ONE stride.  Limping left, the right stride (6) hands off to
  the left limp figure (0x16 -> the self-cycling 0x18) while the other leg
  keeps its normal clips.  The asymmetry IS the limp.

  Both machines CLAMP THEIR DEMAND while in a cycle -- the leg machine
  writes the mapper's own speedDemand cell down to the damaged side's cap
  (new MechControlsMapper::SetSpeedDemand, matching the binary's direct
  mapper+0x128 write), the body machine clamps bodyTargetSpeed, both floor
  at zero.  A limping mech cannot command more than its figure carries, nor
  reverse out of a forward cycle.

  The limp advancers keep states 0x16-0x1b LIVE -- the normal advancers
  treat those as the reset group, which is exactly why the limp flavours
  must be selected while limping or the figure is neutralized mid-cycle.
  No death latch, no wind-down: the movement modes are exclusive.

  ALSO EXPLAINED IN PASSING: Ghidra's 3760-byte FUN_004a6344 -- the census's
  largest function -- is really THREE functions.  The two normal ClipFinished
  callbacks (@004a6928/@004a6d8c) are reached only via data pointers, so the
  decompiler folded them into the gimp-body machine's extent.

All movement-mode reads route through Mech::MovementMode() (mech+0x40 == the
simulation state), which honours a BT_FORCE_LIMP=3|4 dev hook so the gait
could be verified before the damage model's limp hook exists.

VERIFIED, two runs on the rig:

  NO-REGRESSION: the normal mission's speed sequence is BIT-IDENTICAL to
  5.3.95 (7.31972, 26.6726, 22.1601, ...).  The branch costs nothing.

  FORCED LEFT LIMP (new pod_render_limp.conf): the mapper still demands
  26.9; the hull lurches at 10-23.  And on the wire the healthy walk's tight
  pose-count pairs (650/649 ... 434/421) BREAK to a 3x asymmetry -- the
  drag-leg joint at 18 poses against its partner's 55.  The limp is visible
  in the data exactly the way it will be visible on screen.

STILL OPEN in this family: the damage hook (leg zone >= 0.5 -> mode 3/4, a
MECHDMG increment -- nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces.

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

2051 lines
53 KiB
C++

//===========================================================================//
// File: mech2.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Mech gait animation -- the transition machine //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp>
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#endif
//
//#############################################################################
// A mech walks on two parallel clip channels.
//
// The LEG channel is the locally-simulated gait. Its transitions read the
// LIVE commanded speed out of the controls mapper, so it responds to the
// stick the instant it moves.
//
// The BODY channel is the displayed motion, and the distance IT advances is
// what carries the mech forward. Its transitions read bodyTargetSpeed --
// a snapshot -- which is what lets a dead-reckoned or networked mech walk
// properly with no controls mapper of its own.
//
// Both channels run the same state machine over the same clips; only the
// speed they consult differs. That is the whole reason the two ClipFinished
// functions below are near-twins rather than one shared routine, and the
// symmetry is load-bearing: where the binary's two jump tables agree, a
// disagreement in this file is a bug.
//
// Each clip is ONE STRIDE, which is why every state is handed. A walk is
// Right, Left, Right, ... and each entry to and exit from a cycle has its own
// handed pair so the mech always leaves on the correct foot.
//
// The machine only ever runs at END OF CLIP. SequenceController::Advance
// calls the channel's finished callback, which picks the next state, re-arms
// the channel, and spends the leftover time in the new clip -- returning the
// distance that leftover covered so Advance can fold it in. Getting that
// contract wrong double-counts the mech's forward motion.
//#############################################################################
//
//
//#############################################################################
// @004a7fc4 -- bind the leg channel to a state's clip and record the state.
//#############################################################################
//
void
Mech::SetLegAnimation(int state)
{
Check(this);
//
// Bounded by the SLOT count, not the name count: slot 0x20 is the
// bump/crash clip and is legitimately bound on a wall impact.
//
Verify(state >= 0 && state < AnimationSlotCount);
legAnimation.SelectSequence(
animationClips[state],
(void *)Mech::LegClipFinished,
0,
0);
legStateAlarm.SetLevel((unsigned)state);
}
//
//#############################################################################
// @004a800c -- the body channel's equivalent.
//
// This one also drives the animation StateIndicators, which is how the audio
// subsystem's watchers learn a gait changed. Guarded to the constructed
// range so an out-of-range clip cannot trip StateIndicator's own Verify.
//#############################################################################
//
void
Mech::SetBodyAnimation(int state)
{
Check(this);
Verify(state >= 0 && state < AnimationSlotCount);
bodyAnimation.SelectSequence(
animationClips[state],
(void *)Mech::BodyClipFinished,
0,
0);
bodyStateAlarm.SetLevel((unsigned)state);
animationState.SetState(state);
replicantAnimationState.SetState(state);
}
//
//#############################################################################
// The shared tails (@0x4a6a06 leg / @0x4a6e66 body) every handler ends in:
// bind the next state, then spend the carryover inside it.
//#############################################################################
//
Scalar
Mech::LegTransition(int next_state, Scalar advance_time, int move_joints)
{
Check(this);
SetLegAnimation(next_state);
return legAnimation.Advance(advance_time, move_joints);
}
Scalar
Mech::BodyTransition(int next_state, Scalar advance_time, int move_joints)
{
Check(this);
SetBodyAnimation(next_state);
return bodyAnimation.Advance(advance_time, move_joints);
}
//
//#############################################################################
// @004a6928 -- the LEG channel's end-of-clip machine (jump table @0x4a69aa).
//
// Reads the live commanded speed from the controls mapper. A mech with no
// mapper reads zero and simply idles, which is the correct behaviour for a
// replicant.
//#############################################################################
//
Scalar
Mech::LegClipFinished(
Mech *mech,
unsigned /* callback_arg */,
Scalar carryover,
int move_joints
)
{
Check(mech);
//
// A LIMPING mech's transitions run the limp machine instead (movement
// mode 3 = left-leg limp, 4 = right; the damage model's own "limp gait
// graphic (left 3 / right 4)"). Guarded on the clip set existing --
// LoadLocomotionClips leaves hasGimpClips 0 for a model without it.
//
{
int
mode = mech->MovementMode();
if ((mode == 3 || mode == 4) && mech->hasGimpClips)
{
return mech->GimpLegClipFinished(carryover);
}
}
//
// The binary reads subsystemArray[0] -- the roster's controls-mapper slot.
// A mech without one (a replicant) reads zero and idles, which is right.
//
Scalar
demand = 0.0f;
if (mech->subsystemArray != NULL && mech->subsystemArray[0] != NULL)
{
demand =
((MechControlsMapper *)mech->subsystemArray[0])->GetSpeedDemand();
}
Scalar
cycle_rate = mech->forwardCycleRate,
time_scale = mech->globalTimeScale,
cycle = mech->legCycleSpeed,
tail_time = carryover * time_scale;
switch (mech->legStateAlarm.GetLevel())
{
//
// Standing and the idle group -- nothing to transition to.
//
case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27:
return 0.0f;
case 2:
mech->legStateAlarm.SetLevel(1);
return 0.0f;
//
// The transition-END clips: having arrived, fall back to standing.
//
case 3: case 4: case 8: case 9: case 20: case 21:
case 28: case 29: case 30: case 31: case 32:
mech->legStateAlarm.SetLevel(0);
return 0.0f;
//
// Walking, right foot down (@0x4a6aad). Three ways out: stop if the
// demand has fallen below the "moving at all" threshold, step up toward
// the run cycle if it is over the walk cap, otherwise take the next
// stride on the other foot.
//
// The stop and step-up tests each check the DEMAND and the CURRENT CYCLE
// SPEED slewed by one carryover -- so a momentary flick of the stick
// cannot yank the mech out of a stride it has already committed to.
//
case 5: case 6: case 14:
if (
demand < mech->standSpeed &&
(cycle - cycle_rate * carryover) < mech->standSpeed
)
{
return mech->LegTransition(9, tail_time, move_joints);
}
if (
demand > mech->walkStrideLength &&
(cycle + cycle_rate * carryover) > mech->walkStrideLength
)
{
return mech->LegTransition(0xb, tail_time, move_joints);
}
return mech->LegTransition(
7,
carryover * cycle * time_scale / mech->walkStrideLength,
move_joints);
//
// Walking, left foot down (@0x4a69d6) -- the mirror.
//
case 7: case 15:
if (
demand < mech->standSpeed &&
(cycle - cycle_rate * carryover) < mech->standSpeed
)
{
return mech->LegTransition(8, tail_time, move_joints);
}
if (
demand > mech->walkStrideLength &&
(cycle + cycle_rate * carryover) > mech->walkStrideLength
)
{
return mech->LegTransition(0xa, tail_time, move_joints);
}
return mech->LegTransition(
6,
carryover * cycle * time_scale / mech->walkStrideLength,
move_joints);
//
// Running / reversing (@0x4a6bdb and @0x4a6b63): drop back to the walk
// cycle when the demand decays, else alternate feet.
//
case 10: case 12:
if (
demand < mech->reverseSpeedMax &&
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
)
{
return mech->LegTransition(0xf, tail_time, move_joints);
}
return mech->LegTransition(
0xd,
carryover * cycle * time_scale / mech->reverseStrideLength,
move_joints);
case 11: case 13:
if (
demand < mech->reverseSpeedMax &&
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
)
{
return mech->LegTransition(0xe, tail_time, move_joints);
}
return mech->LegTransition(
0xc,
carryover * cycle * time_scale / mech->reverseStrideLength,
move_joints);
//
// Limping (@0x4a6c17 and @0x4a6cc4). gimpStrideLength is authored
// NEGATIVE, so the cycle time comes out negative and has to be folded
// positive before it can be spent -- the binary does exactly this at
// @0x4a6c6e / @0x4a6d3d.
//
case 16: case 18:
if (
demand > mech->gimpSpeedMax &&
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
)
{
return mech->LegTransition(0x15, tail_time, move_joints);
}
{
Scalar
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return mech->LegTransition(0x13, cycle_time, move_joints);
}
case 17: case 19:
if (
demand > mech->gimpSpeedMax &&
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
)
{
return mech->LegTransition(0x14, tail_time, move_joints);
}
{
Scalar
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return mech->LegTransition(0x12, cycle_time, move_joints);
}
}
//
// Falls, crashes and the death clips play out and stop here.
//
return 0.0f;
}
//
//#############################################################################
// @004a6d8c -- the BODY channel's end-of-clip machine (jump table @0x4a6e0a).
//
// Structurally identical to the leg machine above, reading bodyTargetSpeed
// instead of the live mapper demand. Kept as its own routine because that is
// how the binary has it, and because the two tables are each other's check.
//#############################################################################
//
Scalar
Mech::BodyClipFinished(
Mech *mech,
unsigned /* callback_arg */,
Scalar carryover,
int move_joints
)
{
Check(mech);
{
int
mode = mech->MovementMode();
if ((mode == 3 || mode == 4) && mech->hasGimpClips)
{
return mech->GimpBodyClipFinished(carryover, move_joints);
}
}
Scalar
cycle_rate = mech->forwardCycleRate,
time_scale = mech->globalTimeScale,
cycle = mech->bodyCycleSpeed,
demand = mech->bodyTargetSpeed,
tail_time = carryover * time_scale;
switch (mech->bodyStateAlarm.GetLevel())
{
case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27:
return 0.0f;
case 2:
mech->bodyStateAlarm.SetLevel(1);
return 0.0f;
case 3: case 4: case 8: case 9: case 20: case 21:
case 28: case 29: case 30: case 31: case 32:
mech->bodyStateAlarm.SetLevel(0);
return 0.0f;
//
// Walking, right foot down (@0x4a6f11).
//
case 5: case 6: case 14:
if (
demand < mech->standSpeed &&
(cycle - cycle_rate * carryover) < mech->standSpeed
)
{
return mech->BodyTransition(9, tail_time, move_joints);
}
if (
demand > mech->walkStrideLength &&
(cycle + cycle_rate * carryover) > mech->walkStrideLength
)
{
return mech->BodyTransition(0xb, tail_time, move_joints);
}
return mech->BodyTransition(
7,
carryover * cycle * time_scale / mech->walkStrideLength,
move_joints);
//
// Walking, left foot down (@0x4a6e36).
//
case 7: case 15:
if (
demand < mech->standSpeed &&
(cycle - cycle_rate * carryover) < mech->standSpeed
)
{
return mech->BodyTransition(8, tail_time, move_joints);
}
if (
demand > mech->walkStrideLength &&
(cycle + cycle_rate * carryover) > mech->walkStrideLength
)
{
return mech->BodyTransition(0xa, tail_time, move_joints);
}
return mech->BodyTransition(
6,
carryover * cycle * time_scale / mech->walkStrideLength,
move_joints);
//
// Running / reversing (@0x4a7041 and @0x4a6fc7).
//
case 10: case 12:
if (
demand < mech->reverseSpeedMax &&
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
)
{
return mech->BodyTransition(0xf, tail_time, move_joints);
}
return mech->BodyTransition(
0xd,
carryover * cycle * time_scale / mech->reverseStrideLength,
move_joints);
case 11: case 13:
if (
demand < mech->reverseSpeedMax &&
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
)
{
return mech->BodyTransition(0xe, tail_time, move_joints);
}
return mech->BodyTransition(
0xc,
carryover * cycle * time_scale / mech->reverseStrideLength,
move_joints);
//
// The reverse cycle (@0x4a707d and @0x4a712c). Note these are the BACK
// gait, not a limp, despite sharing the gimp caps: while the demand stays
// below gimpSpeedMax the cycle alternates 0x12 <-> 0x13, and a forward
// demand leaves through the back-to-stand pair. Reading them as "gimp,
// fall back to standing" makes the body loop stand -> reverse-entry
// forever, which is a slow reverse with a wrong-footed exit.
//
case 16: case 18:
if (
demand > mech->gimpSpeedMax &&
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
)
{
return mech->BodyTransition(0x15, tail_time, move_joints);
}
{
Scalar
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return mech->BodyTransition(0x13, cycle_time, move_joints);
}
case 17: case 19:
if (
demand > mech->gimpSpeedMax &&
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
)
{
return mech->BodyTransition(0x14, tail_time, move_joints);
}
{
Scalar
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return mech->BodyTransition(0x12, cycle_time, move_joints);
}
}
return 0.0f;
}
//
//#############################################################################
// @004a5028 -- the LEG channel's per-frame update (ground flavour).
//
// Reads the LIVE demand from the controls mapper, arms the death clips off
// the movement mode, slews legCycleSpeed toward the demand inside each
// cycle's caps, and advances the clip -- which is what writes the leg
// joints. Returns the cycle distance covered this frame.
//
// Reconstructed from the RAW decomp rather than the BT411 donor: the donor
// carries port-era replicant accommodations and a relocated turn-in-place
// dispatcher that belong to ITS network model, not to the binary. In the
// binary the trn dispatcher lives in the master performance (mech4), and a
// replicant's mapper cell replicates -- so this function reads the mapper
// unconditionally, exactly as decompiled.
//#############################################################################
//
Scalar
Mech::AdvanceLegAnimation(Scalar time_slice)
{
Check(this);
//
// The binary reads the roster's slot 0 with no null check -- a mech
// always has its controls mapper by the time it ticks.
//
MechControlsMapper
*mapper = (MechControlsMapper *)subsystemArray[0];
Check_Pointer(mapper);
Scalar
demand = mapper->GetSpeedDemand(),
distance = 0.0f;
//
// One-shot: movement modes 5..8 are the falls/deaths; latch the matching
// crash clip exactly once.
//
if (!deathAnimationLatched)
{
switch (MovementMode())
{
case 5: SetLegAnimation(0x1c); deathAnimationLatched = 1; break;
case 6: SetLegAnimation(0x1d); deathAnimationLatched = 1; break;
case 7: SetLegAnimation(0x1e); deathAnimationLatched = 1; break;
case 8: SetLegAnimation(0x1f); deathAnimationLatched = 1; break;
}
}
//
// Wind-down: once the cycle speed has decayed to nothing during a
// walk-transition state, drop straight to standing.
//
{
int
state = (int)legStateAlarm.GetLevel();
if (
legCycleSpeed <= 0.0f &&
(state == 6 || state == 7 || state == 8 || state == 9)
)
{
legStateAlarm.SetLevel(0);
legResetLatch = 1;
}
}
switch (legStateAlarm.GetLevel())
{
case 0:
//
// Standing. A demand above standSpeed begins the walk; a NEGATIVE
// demand backs up; anything in between stays put. Arming a state
// FALLS THROUGH so the new clip advances this same frame.
//
if (standSpeed < demand)
{
SetLegAnimation(5);
}
else
{
distance = 0.0f;
if (demand >= 0.0f)
{
break;
}
SetLegAnimation(0x10);
}
// fall through
case 2: case 3: case 5: case 8: case 9: case 10: case 0xb:
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
advance_clip:
//
// The plain-advance group: transitions, falls and deaths play at the
// global rate scaled by the idle/transition stride scale. The
// Standing guard is the binary's own (MECH2.CPP:0xd3) -- unreachable
// through the fall-through above (arming rewrote the level), it
// catches a DIRECT entry with the alarm still at 0.
//
if (legStateAlarm.GetLevel() == 0)
{
Fail("Standing Not Supported");
}
distance = legAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale, 1);
legCycleSpeed = distance / time_slice;
break;
case 1:
distance = 0.0f;
break;
case 4:
//
// Turn-in-place. A demand outside [0, standSpeed] abandons the turn
// -- drop to standing and request the leg-state update record --
// otherwise the turn clip advances like any transition. (What ARMS
// state 4 is the master performance's dispatcher, mech4 -- not here.)
//
if (standSpeed < demand)
{
legStateAlarm.SetLevel(0);
ForceUpdate(8);
break;
}
distance = 0.0f;
if (demand < 0.0f)
{
legStateAlarm.SetLevel(0);
ForceUpdate(8);
break;
}
goto advance_clip;
case 6: case 7:
//
// The walk cycle. Slew the cycle speed toward the demand at
// forwardCycleRate: upward capped by the demand then the walk
// stride, downward floored by the demand then standSpeed. The clip
// advances at (cycle / walkStride) of its authored rate -- a slow
// walk IS the walk clip played slow.
//
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > walkStrideLength)
{
legCycleSpeed = walkStrideLength;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < standSpeed)
{
legCycleSpeed = standSpeed;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale,
1);
break;
case 0xc: case 0xd:
//
// The run cycle -- same slew, its own caps: up to runSpeedMax, down
// no further than reverseSpeedMax (the drop-out threshold the
// ClipFinished handler tests).
//
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > runSpeedMax)
{
legCycleSpeed = runSpeedMax;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < reverseSpeedMax)
{
legCycleSpeed = reverseSpeedMax;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale,
1);
break;
case 0x12: case 0x13:
//
// The reverse cycle. Everything is NEGATIVE here -- the demand, the
// cycle speed, and both caps (gimpSpeedMax ~ -4, gimpStrideLength
// ~ -20 on the Mad Cat), so "up" slews toward zero and "down" toward
// full reverse, at the reverse's own gimpCycleRate. The advance
// ratio is folded positive: a reverse clip is authored backward, it
// is not played backward.
//
if (demand > legCycleSpeed)
{
legCycleSpeed += gimpCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > gimpSpeedMax)
{
legCycleSpeed = gimpSpeedMax;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= gimpCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < gimpStrideLength)
{
legCycleSpeed = gimpStrideLength;
}
}
{
Scalar
ratio = legCycleSpeed / gimpStrideLength;
if (ratio <= 0.0f)
{
ratio = -ratio;
}
distance = legAnimation.Advance(
ratio * time_slice * globalTimeScale, 1);
}
break;
case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b:
//
// The reset group -- limp exits and the four falls. Clear the
// motion-event state and every one-shot, drop to standing, and put
// the skeleton back to its neutral pose.
//
motionEventName = "";
motionEventArmed = 0;
legResetLatch = 0;
deathAnimationLatched = 0;
legStateAlarm.SetLevel(0);
legAnimation.Reset(1);
break;
default:
Fail("Unsupported mech animation");
}
return distance;
}
//
//#############################################################################
// @004a5678 -- the BODY channel's per-frame update (ground flavour).
//
// The displayed-motion twin. Differences from the leg version, all
// binary-verified: the demand is bodyTargetSpeed (the snapshot, no mapper
// access); there is NO wind-down block and NO turn-in-place case (state 4
// sits in the plain group); move_joints arrives as a parameter and reaches
// every Advance AND the reset's Reset call -- so the caller decides whether
// this channel poses the skeleton or only measures the stride.
//#############################################################################
//
Scalar
Mech::AdvanceBodyAnimation(Scalar time_slice, int move_joints)
{
Check(this);
Scalar
demand = bodyTargetSpeed,
distance = 0.0f;
if (!deathAnimationLatched)
{
switch (MovementMode())
{
case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break;
case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break;
case 7: SetBodyAnimation(0x1e); deathAnimationLatched = 1; break;
case 8: SetBodyAnimation(0x1f); deathAnimationLatched = 1; break;
}
}
switch (bodyStateAlarm.GetLevel())
{
case 0:
distance = 0.0f;
if (standSpeed < demand)
{
SetBodyAnimation(5);
}
else
{
if (demand >= 0.0f)
{
break;
}
SetBodyAnimation(0x10);
}
// fall through
case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0xb:
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
distance = bodyAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale, move_joints);
bodyCycleSpeed = distance / time_slice;
break;
case 1:
distance = 0.0f;
break;
case 6: case 7:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > walkStrideLength)
{
bodyCycleSpeed = walkStrideLength;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < standSpeed)
{
bodyCycleSpeed = standSpeed;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale,
move_joints);
break;
case 0xc: case 0xd:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > runSpeedMax)
{
bodyCycleSpeed = runSpeedMax;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < reverseSpeedMax)
{
bodyCycleSpeed = reverseSpeedMax;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale,
move_joints);
break;
case 0x12: case 0x13:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += gimpCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > gimpSpeedMax)
{
bodyCycleSpeed = gimpSpeedMax;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= gimpCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < gimpStrideLength)
{
bodyCycleSpeed = gimpStrideLength;
}
}
{
Scalar
ratio = bodyCycleSpeed / gimpStrideLength;
if (ratio <= 0.0f)
{
ratio = -ratio;
}
distance = bodyAnimation.Advance(
ratio * time_slice * globalTimeScale, move_joints);
}
break;
case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b:
motionEventName = "";
motionEventArmed = 0;
bodyResetLatch = 0;
deathAnimationLatched = 0;
bodyStateAlarm.SetLevel(0);
bodyAnimation.Reset(move_joints);
break;
default:
Fail("Unsupported mech animation");
}
return distance;
}
//
//#############################################################################
// THE LIMP MACHINES (@004a7970 leg / @004a6344 body) -- the transition
// tables that run while movement mode is 3 (left-leg limp) or 4 (right).
//
// The limp replaces ONE stride: limping left, the right stride (6) hands off
// to the left limp figure (0x16 -> the 0x18 cycle); limping right, the left
// stride (7) hands off to the right figure (0x17 -> 0x19). The other leg's
// clips keep their normal alternation, which is what makes it read as a limp
// rather than a different gait.
//
// Both machines CLAMP THEIR DEMAND while in any cycle -- the leg machine
// writes the mapper's own speedDemand cell down to the damaged side's cap,
// the body machine clamps bodyTargetSpeed -- so a limping mech cannot
// command more speed than its limp figure carries, and cannot command a
// reverse out of a forward cycle (the clamp floors at zero).
//#############################################################################
//
Scalar
Mech::GimpLegClipFinished(Scalar carryover)
{
Check(this);
MechControlsMapper
*mapper = (MechControlsMapper *)subsystemArray[0];
Check_Pointer(mapper);
int
mode = MovementMode(),
state = (int)legStateAlarm.GetLevel();
//
// The demand clamp (see the block comment above).
//
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
demand = mapper->GetSpeedDemand();
if (demand > cap)
{
demand = cap;
}
if (demand < 0.0f)
{
demand = 0.0f;
}
mapper->SetSpeedDemand(demand);
}
Scalar
demand = mapper->GetSpeedDemand(),
cycle_rate = forwardCycleRate,
time_scale = globalTimeScale,
cycle = legCycleSpeed;
int
plain_next;
switch (state)
{
case 2:
legStateAlarm.SetLevel(1);
return 0.0f;
case 3: case 8: case 9: case 0x14: case 0x15: case 0x1a: case 0x1b:
case 4:
case 0x20:
legStateAlarm.SetLevel(0);
return 0.0f;
//
// The right-stride walk family. Continuing hands off to the LEFT
// stride when the RIGHT leg is the good one (mode 4), and to the left
// limp figure when the left leg is the bad one (mode 3).
//
case 5: case 6: case 0xe:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 9;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 0xb;
break;
}
SetLegAnimation((mode == 4) ? 7 : 0x16);
return legAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, 1);
case 7: case 0xf:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 8;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 10;
break;
}
SetLegAnimation((mode == 3) ? 6 : 0x17);
return legAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, 1);
case 10: case 0xc:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xf;
break;
}
SetLegAnimation(0xd);
return legAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, 1);
case 0xb: case 0xd:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xe;
break;
}
SetLegAnimation(0xc);
return legAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, 1);
case 0x10: case 0x12:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x15;
break;
}
SetLegAnimation(0x13);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return legAnimation.Advance(cycle_time, 1);
}
case 0x11: case 0x13:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x14;
break;
}
SetLegAnimation(0x12);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return legAnimation.Advance(cycle_time, 1);
}
//
// The limp figures themselves: keep cycling while the demand holds,
// exit toward standing when it drops.
//
case 0x16: case 0x18:
if (
demand < gimpLeftSpeedMax &&
(cycle - cycle_rate * carryover) < gimpLeftSpeedMax
)
{
plain_next = 0x1a;
break;
}
SetLegAnimation(0x18);
return legAnimation.Advance(
carryover * cycle * time_scale / gimpLeftStrideLength, 1);
case 0x17: case 0x19:
if (
demand < gimpRightSpeedMax &&
(cycle - cycle_rate * carryover) < gimpRightSpeedMax
)
{
plain_next = 0x1b;
break;
}
SetLegAnimation(0x19);
return legAnimation.Advance(
carryover * cycle * time_scale / gimpRightStrideLength, 1);
default:
return 0.0f;
}
//
// The plain tail every non-cycle exit lands in.
//
SetLegAnimation(plain_next);
return legAnimation.Advance(carryover * time_scale, 1);
}
Scalar
Mech::GimpBodyClipFinished(Scalar carryover, int move_joints)
{
Check(this);
int
mode = MovementMode(),
state = (int)bodyStateAlarm.GetLevel();
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax;
if (bodyTargetSpeed > cap)
{
bodyTargetSpeed = cap;
}
if (bodyTargetSpeed < 0.0f)
{
bodyTargetSpeed = 0.0f;
}
}
Scalar
demand = bodyTargetSpeed,
cycle_rate = forwardCycleRate,
time_scale = globalTimeScale,
cycle = bodyCycleSpeed;
int
plain_next;
switch (state)
{
case 2:
bodyStateAlarm.SetLevel(1);
return 0.0f;
case 3: case 8: case 9: case 0x14: case 0x15: case 0x1a: case 0x1b:
case 4:
case 0x20:
bodyStateAlarm.SetLevel(0);
return 0.0f;
case 5: case 6: case 0xe:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 9;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 0xb;
break;
}
SetBodyAnimation((mode == 4) ? 7 : 0x16);
return bodyAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, move_joints);
case 7: case 0xf:
if (
demand < standSpeed &&
(cycle - cycle_rate * carryover) < standSpeed
)
{
plain_next = 8;
break;
}
if (
demand > walkStrideLength &&
(cycle + cycle_rate * carryover) > walkStrideLength
)
{
plain_next = 10;
break;
}
SetBodyAnimation((mode == 3) ? 6 : 0x17);
return bodyAnimation.Advance(
carryover * cycle * time_scale / walkStrideLength, move_joints);
case 10: case 0xc:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xf;
break;
}
SetBodyAnimation(0xd);
return bodyAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, move_joints);
case 0xb: case 0xd:
if (
demand < reverseSpeedMax &&
(cycle - cycle_rate * carryover) < reverseSpeedMax
)
{
plain_next = 0xe;
break;
}
SetBodyAnimation(0xc);
return bodyAnimation.Advance(
carryover * cycle * time_scale / reverseStrideLength, move_joints);
case 0x10: case 0x12:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x15;
break;
}
SetBodyAnimation(0x13);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return bodyAnimation.Advance(cycle_time, move_joints);
}
case 0x11: case 0x13:
if (
demand > gimpSpeedMax &&
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
)
{
plain_next = 0x14;
break;
}
SetBodyAnimation(0x12);
{
Scalar
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
if (cycle_time <= 0.0f)
{
cycle_time = -cycle_time;
}
return bodyAnimation.Advance(cycle_time, move_joints);
}
case 0x16: case 0x18:
if (
demand < gimpLeftSpeedMax &&
(cycle - cycle_rate * carryover) < gimpLeftSpeedMax
)
{
plain_next = 0x1a;
break;
}
SetBodyAnimation(0x18);
return bodyAnimation.Advance(
carryover * cycle * time_scale / gimpLeftStrideLength, move_joints);
case 0x17: case 0x19:
if (
demand < gimpRightSpeedMax &&
(cycle - cycle_rate * carryover) < gimpRightSpeedMax
)
{
plain_next = 0x1b;
break;
}
SetBodyAnimation(0x19);
return bodyAnimation.Advance(
carryover * cycle * time_scale / gimpRightStrideLength, move_joints);
default:
return 0.0f;
}
SetBodyAnimation(plain_next);
return bodyAnimation.Advance(carryover * time_scale, move_joints);
}
//
//#############################################################################
// THE LIMP ADVANCERS (@004a71f4 leg / @004a5bf8 body). Selected instead of
// the normal pair while limping, and structurally different in exactly the
// ways a limp needs:
//
// * States 0x16-0x1b are LIVE here -- the limp entries advance in the
// plain group and the 0x18/0x19 figures get their own slewed cycles with
// the damaged side's caps. (The NORMAL advancers treat those states as
// the reset group, which is why these flavours must be selected while
// limping -- the normal one would neutralize the figure mid-cycle.)
//
// * No death latch and no wind-down: the movement modes are exclusive, so
// a limping mech is by definition not falling.
//
// * The same demand clamp as the transition machines, applied per frame.
//#############################################################################
//
Scalar
Mech::AdvanceLegAnimationGimp(Scalar time_slice)
{
Check(this);
MechControlsMapper
*mapper = (MechControlsMapper *)subsystemArray[0];
Check_Pointer(mapper);
int
mode = MovementMode(),
state = (int)legStateAlarm.GetLevel();
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
clamped = mapper->GetSpeedDemand();
if (clamped > cap)
{
clamped = cap;
}
if (clamped < 0.0f)
{
clamped = 0.0f;
}
mapper->SetSpeedDemand(clamped);
}
Scalar
demand = mapper->GetSpeedDemand(),
distance = 0.0f;
switch (legStateAlarm.GetLevel())
{
case 0:
if (demand <= standSpeed)
{
break;
}
SetLegAnimation(5);
// fall through
case 2: case 3: case 5: case 8: case 9: case 10: case 0xb:
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
advance_clip:
distance = legAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale, 1);
legCycleSpeed = distance / time_slice;
break;
case 1:
break;
case 4:
if (standSpeed < demand)
{
legStateAlarm.SetLevel(0);
ForceUpdate(8);
break;
}
goto advance_clip;
case 6: case 7:
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > walkStrideLength)
{
legCycleSpeed = walkStrideLength;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < standSpeed)
{
legCycleSpeed = standSpeed;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale,
1);
break;
case 0xc: case 0xd:
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > runSpeedMax)
{
legCycleSpeed = runSpeedMax;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < reverseSpeedMax)
{
legCycleSpeed = reverseSpeedMax;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale,
1);
break;
case 0x12: case 0x13:
if (demand > legCycleSpeed)
{
legCycleSpeed += gimpCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > gimpSpeedMax)
{
legCycleSpeed = gimpSpeedMax;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= gimpCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < gimpStrideLength)
{
legCycleSpeed = gimpStrideLength;
}
}
{
Scalar
ratio = legCycleSpeed / gimpStrideLength;
if (ratio <= 0.0f)
{
ratio = -ratio;
}
distance = legAnimation.Advance(
ratio * time_slice * globalTimeScale, 1);
}
break;
//
// The limp figures: slewed like a walk, inside the damaged side's caps.
//
case 0x18: case 0x19:
{
Scalar
speed_cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
stride = (mode == 3) ? gimpLeftStrideLength : gimpRightStrideLength;
if (demand > legCycleSpeed)
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed > stride)
{
legCycleSpeed = stride;
}
}
else if (demand < legCycleSpeed)
{
legCycleSpeed -= forwardCycleRate * time_slice;
if (legCycleSpeed < demand)
{
legCycleSpeed = demand;
}
if (legCycleSpeed < speed_cap)
{
legCycleSpeed = speed_cap;
}
}
distance = legAnimation.Advance(
time_slice * (legCycleSpeed / stride) * globalTimeScale, 1);
}
break;
default:
Fail("Unsupported mech animation");
}
return distance;
}
Scalar
Mech::AdvanceBodyAnimationGimp(Scalar time_slice, int move_joints)
{
Check(this);
int
mode = MovementMode(),
state = (int)bodyStateAlarm.GetLevel();
if (
state == 6 || state == 7 || state == 0xc || state == 0xd ||
state == 0x12 || state == 0x13
)
{
Scalar
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax;
if (bodyTargetSpeed > cap)
{
bodyTargetSpeed = cap;
}
if (bodyTargetSpeed < 0.0f)
{
bodyTargetSpeed = 0.0f;
}
}
Scalar
demand = bodyTargetSpeed,
distance = 0.0f;
switch (bodyStateAlarm.GetLevel())
{
case 0:
if (demand <= standSpeed)
{
break;
}
SetBodyAnimation(5);
// fall through
case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0xb:
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
distance = bodyAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale, move_joints);
bodyCycleSpeed = distance / time_slice;
break;
case 1:
break;
case 6: case 7:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > walkStrideLength)
{
bodyCycleSpeed = walkStrideLength;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < standSpeed)
{
bodyCycleSpeed = standSpeed;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale,
move_joints);
break;
case 0xc: case 0xd:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > runSpeedMax)
{
bodyCycleSpeed = runSpeedMax;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < reverseSpeedMax)
{
bodyCycleSpeed = reverseSpeedMax;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale,
move_joints);
break;
case 0x12: case 0x13:
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += gimpCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > gimpSpeedMax)
{
bodyCycleSpeed = gimpSpeedMax;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= gimpCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < gimpStrideLength)
{
bodyCycleSpeed = gimpStrideLength;
}
}
{
Scalar
ratio = bodyCycleSpeed / gimpStrideLength;
if (ratio <= 0.0f)
{
ratio = -ratio;
}
distance = bodyAnimation.Advance(
ratio * time_slice * globalTimeScale, move_joints);
}
break;
case 0x18: case 0x19:
{
Scalar
speed_cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
stride = (mode == 3) ? gimpLeftStrideLength : gimpRightStrideLength;
if (demand > bodyCycleSpeed)
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed > stride)
{
bodyCycleSpeed = stride;
}
}
else if (demand < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < demand)
{
bodyCycleSpeed = demand;
}
if (bodyCycleSpeed < speed_cap)
{
bodyCycleSpeed = speed_cap;
}
}
distance = bodyAnimation.Advance(
time_slice * (bodyCycleSpeed / stride) * globalTimeScale,
move_joints);
}
break;
default:
Fail("Unsupported mech animation");
}
return distance;
}
//
//#############################################################################
// @004a7f50 -- prefix + suffix -> the clip's resource ID.
//
// The clip names are the model's 3-char animation prefix with a 3-char gait
// suffix appended ("mad" + "wwr" = madwwr), resolved by name over the
// animation resources. Returns a pointer to the found description's
// resourceID; NULL when the model has no such clip -- which is a REAL case
// (the limp set is optional), so callers must tolerate it.
//#############################################################################
//
ResourceDescription::ResourceID *
Mech::ResolveAnimationClip(const char *prefix, const char *suffix)
{
Check(this);
Check_Pointer(prefix);
Check_Pointer(suffix);
char
clip_name[12];
strcpy(clip_name, prefix);
strcat(clip_name, suffix);
ResourceDescription
*description = application->GetResourceFile()->FindResourceDescription(
clip_name,
ResourceDescription::AnimationResourceType,
ResourceDescription::NullResourceID);
return (description != NULL) ? &description->resourceID : NULL;
}
//
//#############################################################################
// @004a8054 -- bind the clip at animationClips[slot] into the leg channel and
// integrate its keyframe strides. Returns (via the out parameters) the total
// cycle distance and the final keyframe time; the loader divides total by
// time to recover a cycle speed.
//
// The callback is NULL on purpose: measurement only ever PARSES the clip
// (SelectSequence), it never plays it, so the finished callback can never
// fire. The binary passes a live pointer here; NULL is behaviourally
// identical and avoids arming a transition machine mid-load. [T3]
//#############################################################################
//
void
Mech::MeasureClipStride(int slot, Scalar *total, Scalar *last_key)
{
Check(this);
Verify(slot >= 0 && slot < AnimationSlotCount);
legAnimation.SelectSequence(animationClips[slot], NULL, 0, 0);
*total = 0.0f;
*last_key = 0.0f;
int
frame;
for (frame = 0; frame < legAnimation.keyframeCount; ++frame)
{
Scalar
frame_time = legAnimation.keyframeTimes[frame];
*total += (frame_time - *last_key) *
legAnimation.keyframeData[frame].stride;
*last_key = frame_time;
}
}
//
//#############################################################################
// @004a80d4 -- resolve and cache every gait clip, measuring the gait
// constants from the clips themselves as it goes. This is where standSpeed,
// walkStrideLength, reverseSpeedMax, reverseStrideLength, gimpSpeedMax and
// gimpStrideLength actually COME FROM -- they are properties of the authored
// animations, not authored numbers.
//
// Two binary behaviours reproduced deliberately; neither is a transcription
// slip. See the sidecar before "fixing" either:
//
// * The speed caps read keyframeData[keyframeCount] -- one entry PAST the
// last frame (the binary reads 0x690 + 8 + [0x670]*0xc).
//
// * The reverse-cycle stride divides the bbl measurement by STALE data:
// both bbr and bbl are measured into the same pair, so the divide takes
// its second terms from whatever the run cycle left behind. A 1995
// copy-paste bug, shipped, and therefore reproduced -- the walk and run
// cycles above it show what was obviously intended.
//
// DIVERGENCE FROM THE BINARY, on purpose: the binary dereferences every
// ResolveAnimationClip result unguarded -- a mech whose model lacks a
// MANDATORY clip crashes on load. Here a miss stores NullResourceID (which
// SelectSequence resolves to an empty, inert controller) and the dependent
// measurement is skipped, leaving the bring-up default in place. [T3: keeps
// the current boot alive on models whose clip sets have not been verified;
// revisit once every fleet mech is known-good.]
//#############################################################################
//
//
// Resolve one slot: store the clip ID or NullResourceID. Returns whether the
// clip exists, so dependent measurements can be skipped on a miss.
//
int
Mech::LoadClipSlot(int slot, const char *prefix, const char *suffix)
{
ResourceDescription::ResourceID
*clip_ID = ResolveAnimationClip(prefix, suffix);
animationClips[slot] =
(clip_ID != NULL) ? *clip_ID : ResourceDescription::NullResourceID;
return clip_ID != NULL;
}
void
Mech::LoadLocomotionClips(ModelResource *model)
{
Check(this);
Check_Pointer(model);
const char
*prefix = model->animationPrefix;
//
// Zero-initialized because the guarded skips below can reach the reverse
// divide with the run pair unmeasured -- a path the (unguarded) binary
// does not have, so the stale-pair reproduction must not become an
// uninitialized read on top of it.
//
Scalar
total_a = 0.0f, last_a = 0.0f,
total_b = 0.0f, last_b = 0.0f;
gyroRumbleTimer = 0.0f;
//
// Stand -> walk. standSpeed is the clip's final-entry stride.
//
if (LoadClipSlot(5, prefix, "swr"))
{
legAnimation.SelectSequence(animationClips[5], NULL, 0, 0);
standSpeed =
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
}
//
// The forward walk cycle: stride = (s6 + s7) / (d6 + d7).
//
if (
LoadClipSlot(6, prefix, "wwr") &&
LoadClipSlot(7, prefix, "wwl")
)
{
MeasureClipStride(6, &total_a, &last_a);
MeasureClipStride(7, &total_b, &last_b);
walkStrideLength = (total_a + total_b) / (last_a + last_b);
}
LoadClipSlot(8, prefix, "wsr");
LoadClipSlot(9, prefix, "wsl");
//
// Walk -> run. reverseSpeedMax is measured from wrr the same way
// standSpeed is from swr.
//
if (LoadClipSlot(10, prefix, "wrr"))
{
legAnimation.SelectSequence(animationClips[10], NULL, 0, 0);
reverseSpeedMax =
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
}
LoadClipSlot(11, prefix, "wrl");
//
// The run cycle.
//
if (
LoadClipSlot(12, prefix, "rrr") &&
LoadClipSlot(13, prefix, "rrl")
)
{
MeasureClipStride(12, &total_a, &last_a);
MeasureClipStride(13, &total_b, &last_b);
reverseStrideLength = (total_a + total_b) / (last_a + last_b);
}
LoadClipSlot(14, prefix, "rwr");
LoadClipSlot(15, prefix, "rwl");
//
// The bump/crash stagger clip, slot 0x20 -- the reason the clip array is
// bigger than the state-name table.
//
LoadClipSlot(0x20, prefix, "bmp");
//
// The reverse set. gimpSpeedMax is measured from the entry clip; the
// cycle stride divide below reproduces the binary's stale-pair bug (see
// the header comment) and is negated exactly where the binary negates.
//
if (LoadClipSlot(16, prefix, "sbr"))
{
legAnimation.SelectSequence(animationClips[16], NULL, 0, 0);
gimpSpeedMax =
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
}
LoadClipSlot(17, prefix, "sbl");
LoadClipSlot(20, prefix, "bsr");
LoadClipSlot(21, prefix, "bsl");
if (
LoadClipSlot(18, prefix, "bbr") &&
LoadClipSlot(19, prefix, "bbl")
)
{
MeasureClipStride(18, &total_a, &last_a);
MeasureClipStride(19, &total_a, &last_a); // the binary's stale pair:
// total_b/last_b still hold
// the run-cycle figures
gimpStrideLength = (total_a + total_b) / (last_a + last_b);
gimpStrideLength = -gimpStrideLength;
}
//
// The OPTIONAL limp set. Probe for wgl; a model without it has no limp
// clips at all, and the limp machine must never be entered for it.
//
hasGimpClips = 0;
if (ResolveAnimationClip(prefix, "wgl") != NULL)
{
hasGimpClips = 1;
if (LoadClipSlot(22, prefix, "wgl"))
{
legAnimation.SelectSequence(animationClips[22], NULL, 0, 0);
gimpLeftSpeedMax =
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
}
if (LoadClipSlot(23, prefix, "wgr"))
{
legAnimation.SelectSequence(animationClips[23], NULL, 0, 0);
gimpRightSpeedMax =
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
}
if (LoadClipSlot(24, prefix, "ggr"))
{
MeasureClipStride(24, &total_a, &last_a);
gimpLeftStrideLength = total_a / last_a;
}
if (LoadClipSlot(25, prefix, "ggl"))
{
MeasureClipStride(25, &total_a, &last_a);
gimpRightStrideLength = total_a / last_a;
}
LoadClipSlot(26, prefix, "gsl");
LoadClipSlot(27, prefix, "gsr");
}
Check_Fpu();
}