LoadLocomotionClips + ResolveAnimationClip + MeasureClipStride + LoadClipSlot
reconstructed and WIRED into the ctor's GameModel block. animationClips[] is
no longer an empty array: every gait slot resolves by the model's animation
prefix, and the locomotion constants are now MEASURED from the authored clips
instead of asserted as bring-up defaults. First live run, arena mission:
[mech] clips 'mad': standSpeed=5.23 walkStride=18.51 revStride=56.05
revSpeedMax=26.26 gimpSpeedMax=-4.23 gimpStride=-20.26
limpSet=1
That line carries three verifications at once: the prefix printing as text
proves the Mech__ModelResource layout is right at +0x40; the reverse figures
come out NEGATIVE exactly as the transition machines expect; and the mission
ran clean to live driving afterwards (703 log lines, no fault).
DRIVING FEEL CHANGED, deliberately: speedDemand at 0.6 throttle went 14.4 ->
26.9, because the placeholder top speed (30) gave way to the measured 56.05.
The Mad Cat is simply faster than the bring-up guess. Authenticity arriving,
not a regression.
TWO BINARY BEHAVIOURS REPRODUCED ON PURPOSE, both documented at the function:
The speed caps read keyframeData[keyframeCount] -- one entry PAST the last
frame. Fencepost is the binary's own (0x690 + 8 + [0x670]*0xc); whether the
authored table has count+1 entries is unestablished, but the clips were
authored against this read and the measured values are sane.
The reverse-cycle stride is computed from STALE locals. The decomp is
unambiguous: bbr and bbl are both measured into local_8/local_c, and the
divide's second terms come from local_10/local_14 -- still holding the
RUN-LEFT figures. gimpStrideLength = -((bbl + rrl_stale)/(...)). A 1995
copy-paste bug, shipped in every pod for thirty years, reproduced here with
a comment pointing at the wwr/wwl block that shows the intended pattern.
(And an earlier scare resolved: the negation IS in the binary -- the very
next instruction is 0x350 = -0x350. My first decomp window cut one line
short and briefly indicted the donor's minus sign.)
ONE DELIBERATE DIVERGENCE, tagged [T3]: the binary dereferences every resolve
result unguarded -- a model missing a mandatory clip crashes on load. Here a
miss stores NullResourceID (SelectSequence resolves it to an inert controller)
and the dependent measurement is skipped. Keeps the boot alive on unverified
clip sets; revisit when the fleet's models are known-good. Measurement binds
pass a NULL finished-callback (measurement parses, never plays -- the binary's
live pointers can never fire there).
Ctor additionally zero-initializes the whole gait channel -- globalTimeScale
defaulting to 1 specifically, because zero would silence every clip advance --
and fills the clip array with NullResourceID before the loader runs, so the
uninitialized-member class of bug (see 5.3.83) is closed here BEFORE the
consumers arrive.
MECH.HPP: the optional limp set carved out (hasGimpClips + 4 measured limp
figures + gyroRumbleTimer); reservedState 150 -> 140.
Next: the four Advance* entry points -- the last link before the legs move.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
747 lines
23 KiB
C++
747 lines
23 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;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @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();
|
|
}
|