Went to source animationClips[] and found two things: a bug in what 5.3.91 committed, and a naming trap that has already cost BT411 a shipped defect. THE BUG, MINE. I sized animationClips[AnimationCount] -- 0x1d, from the enum. Wrong. The name table stops at 0x1d but the ARRAY does not: slot 0x20 (mech+0x64c) is the bump/crash clip the mech binds on a hard wall impact. So the array is [AnimationSlotCount] = 0x21 and Set*Animation's Verify bounds against that instead. Sizing a real array off a name table that stops earlier reads fine and corrupts whatever sits next door; caught by reading the clip loader, not by the compiler. THE SLOT MAP, recovered from LoadLocomotionClips and now written down in full (suffix, meaning, and which measured constant each clip yields): 5 swr stand -> walk standSpeed 6/7 wwr/wwl forward walk CYCLE walkStrideLength = (s6+s7)/(d6+d7) 8/9 wsr/wsl walk -> stand 10/11 wrr/wrl walk -> run reverseSpeedMax 12/13 rrr/rrl run CYCLE reverseStrideLength 14/15 rwr/rwl run -> walk 16/17 sbr/sbl stand -> back gimpSpeedMax 18/19 bbr/bbl reverse CYCLE gimpStrideLength (NEGATED here) 20/21 bsr/bsl back -> stand 22/23 wgl/wgr walk -> limp gimpLeft/RightSpeedMax 24/25 ggr/ggl limp CYCLE gimpLeft/RightStrideLength 26/27 gsl/gsr limp -> stand 0x20 bmp bump / crash stagger THE TRAP: THE "gimp*" MEMBERS ARE THE REVERSE FIGURES, NOT THE LIMP ONES. gimpSpeedMax and gimpStrideLength are measured from sbr and bbr/bbl -- the reverse gait. The real limp has its own gimpLeft*/gimpRight* pair. This is the same bad naming that produced the states-16-19 misreading recorded in 5.3.91, and it has now caused the same error twice from two directions. Also settled: gimpStrideLength's negative sign is applied AT MEASUREMENT, not authored into the data -- which is where the fold in the transition machines comes from. And the limp clips are OPTIONAL: the loader probes for wgl and leaves hasGimpClips 0 with slots 22-27 unfilled if the model lacks it, so the deferred gimp branch must check that before routing into the limp machine. THE ENUM IS NOT THE SLOT MAP, and MECH.HPP now says so at the enum itself. The names are verbatim from the binary and authoritative AS NAMES, but slot 0x0e takes the run-to-walk clip while the table calls it RightReverseAnimation, and the forward walk alternates 6/7 rather than the pair the WalkForward names suggest. Read the slot map for "what does this play"; read the enum for "what did the original call this index". ATTRIBUTION NOTE: the four clip helpers (ResolveAnimationClip @004a7f50, MeasureClipStride @004a8054, LoadLocomotionClips @004a80d4, LoadLocomotionClipsExt @004a86c8) are exactly the four addresses the manifest lists under mech2.cpp that BT411 files under mech3. The manifest's attribution comes from the binary's own file tagging, so they belong here -- which also accounts for all 12 of mech2's functions. BT 51/51. Still nothing calls the gait. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
481 lines
14 KiB
C++
481 lines
14 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
|
|
|
|
//
|
|
//#############################################################################
|
|
// 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;
|
|
}
|