The last link is in. AdvanceLegAnimation (@004a5028) and AdvanceBodyAnimation
(@004a5678) reconstructed and wired into Mech::Simulate, and the first live
run put a walking gait on the wire:
before 22 handles, 1 animating (the vehicle root)
after 22 handles, 16 animating, ZERO 2-float records
root 812 poses; six PAIRS at 650/649, 599/596, 563/563,
542/540, 531/529, 434/421; three slow joints at 25
Six left/right pose-count pairs is six joints per leg cycling in alternating
strides -- the symmetry is itself evidence that the handed clip alternation
(Right, Left, Right, each clip one stride) is running correctly. Chain, end
to end, every stage previously verified in isolation and now live together:
mapper demand -> AdvanceLegAnimation (state machine) -> SelectSequence
-> SequenceController::Advance (keyframes -> Joint::SetHinge/SetRotation)
-> BTL4HingeRenderable (5.3.88 matrix transport) -> 12-float flush
-> render bridge applies. Mission drove itself clean, no fault.
RECONSTRUCTED FROM THE RAW DECOMP, NOT THE DONOR -- and the sidecar says why:
BT411's versions carry port-era replicant accommodations (its mapper cell
does not replicate; the binary's does) and a turn-in-place dispatcher it
relocated INTO the leg machine from mech4's master performance. None of that
is 1995 code. Here the leg version reads the mapper unconditionally, exactly
as decompiled, and state 4's ARMING stays where the binary has it -- in
mech4, not yet reconstructed.
THE TWO CHANNELS DIFFER MORE THAN THEIR ClipFinished TWINS DO, all
binary-verified: the leg version has the wind-down block, the turn-in-place
case and the "Standing Not Supported" guard; the body version has none of
those, its case 4 sits in the plain-advance group, and move_joints reaches
every body Advance AND its reset's Reset -- the caller decides whether the
body channel poses joints or only measures stride. Wired accordingly: leg
poses, body measures (move_joints 0), body distance dropped at a seam marked
STAGED -- consuming it as the forward step is IntegrateMotion's job (mech4).
Also faithful: case 0 FALLS THROUGH so a freshly armed clip advances the same
frame it was selected; the plain group's Standing guard is unreachable via
that fall-through and catches direct entry only; each cycle plays its clip at
cycle/stride of the authored rate (a slow walk IS the walk clip played slow);
and the reverse cycle's caps are all negative with the advance ratio folded
positive at the end.
UNSOURCED, named in the sidecar rather than invented: idleStrideScale
(+0x5ac, defaults 1) and runSpeedMax (+0x7a0, the run cycle's upward cap --
LoadLocomotionClips does not set it; defaulted huge so it never binds until
its real writer is found). ForceUpdate(8) is a STAGED no-op pending the
replication emitter.
Still deferred: the airborne Advance* flavours (@004a5bf8/@004a71f4, jump
jets) and the Gimp*ClipFinished limp machines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1194 lines
34 KiB
C++
1194 lines
34 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);
|
|
|
|
//
|
|
// 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);
|
|
|
|
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 (GetSimulationState())
|
|
{
|
|
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 (GetSimulationState())
|
|
{
|
|
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;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @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();
|
|
}
|