BT410 5.3.91: the gait transition machine -- two channels, one table, and a reverse cycle that has been misread twice
mech2.cpp exists. Six of its twelve functions are reconstructed: the two
Set*Animation binders, the two *Transition tails, and both *ClipFinished jump
tables (@0x4a69aa leg / @0x4a6e0a body). Compile-verified, BT 51/51, links
clean.
TWO CHANNELS, DELIBERATELY NEAR-DUPLICATE. A mech runs two clip channels over
the same states and the same clips; only the speed they consult differs:
LEG reads the LIVE mapper GetSpeedDemand() -- responds to the stick at once
BODY reads bodyTargetSpeed, a snapshot -- which is what lets a dead-reckoned
or networked mech walk with no controls mapper of its own
So LegClipFinished and BodyClipFinished are twins rather than one shared
routine, exactly as the binary has them. Kept that way on purpose: where the
two jump tables agree, a divergence in this file is a bug, and that mutual
check is worth more than the duplication costs.
EVERY 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 29-state enum is
VERBATIM from the 0x3c-stride name table at .data:0050cfe8, the table the
"Unsupported mech animation" assert indexes, so the names and their order are
the original's rather than inferred from behaviour.
THE COMMIT TEST. Both exits that leave a walk cycle test the demand AND the
current cycle speed slewed by one carryover:
if (demand < standSpeed && (cycle - rate*carryover) < standSpeed) -> stop
if (demand > walkStride && (cycle + rate*carryover) > walkStride) -> run
Requiring both is what stops a momentary flick of the stick yanking the mech
out of a stride it has already committed to. Drop either conjunct and you get
a mech that stutters between gaits on noisy input.
TWO THINGS THAT READ WRONG AND ARE NOT:
gimpStrideLength is authored NEGATIVE. The cycle time from it comes out
negative and is folded positive before being spent (@0x4a6c6e / @0x4a6d3d).
That fold is not defensive coding -- remove it and the limp plays backwards.
States 16-19 on the body channel are the REVERSE gait, not a limp, despite
sharing the gimp caps. BT411 records misreading these as "gimp, fall back to
standing" TWICE; that makes the body loop stand -> reverse-entry forever, a
slow reverse with a wrong-footed exit. Read here by structural symmetry with
the leg table, where every previously-decoded body case mirrors its leg twin.
DEFERRED, and named in the sidecar: the four Advance* per-frame entry points
and the two Gimp*ClipFinished limp machines -- with them the gimp-level branch
at the top of both *ClipFinished, so a limping mech currently runs the normal
machine. That branch needs a TU-safe read of the graphic alarm level (BT411
routes it through a mechdmg bridge to dodge an AlarmIndicator ODR split), worth
reproducing carefully rather than reaching for the alarm directly.
NOTHING CALLS ANY OF THIS YET. The Advance* functions are the entry points and
they are the deferred half, so no gait state is ever selected and a run behaves
exactly as before. Same caveat as 5.3.90: a blocker removed, not a behaviour
delivered.
HEADER: MECH.HPP gains the enum, six declarations, and the channel state --
legStateAlarm/bodyStateAlarm (read via GetLevel; the binary's +0x3b0/+0x728 are
mirrors of the alarm level, so no separate int is kept), legCycleSpeed,
bodyCycleSpeed, forwardCycleRate, gimpCycleRate, standSpeed, gimpSpeedMax,
gimpStrideLength, globalTimeScale, animationClips[0x1d]. 41 ints carved from
reservedState, 191 -> 150. walkStrideLength/reverseStrideLength/
reverseSpeedMax/bodyTargetSpeed already existed from the Phase 5.3 locomotion
work and are reused.
STILL UNSOURCED: animationClips[] is declared but nothing fills it. The clip
handles come from the mech's model resource and must be resolved before the
Advance* increment, or SetLegAnimation hands SelectSequence a garbage ID. It
fails soft (SelectSequence tolerates a missing resource with an inert
controller) but it is a hard prerequisite for the gait doing anything.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,54 @@
|
||||
}
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//####################### MechAnimationState ############################
|
||||
//###########################################################################
|
||||
//
|
||||
// The gait clip a mech is currently playing. Recovered VERBATIM from the
|
||||
// 0x3c-byte-stride name table at .data:0050cfe8 -- the table the binary's
|
||||
// "Unsupported mech animation" assert indexes -- so the names and their order
|
||||
// are the original's, not a reading of behaviour.
|
||||
//
|
||||
// The cycle alternates LEFT and RIGHT because each clip is one stride: a
|
||||
// walk is Right -> Left -> Right forever, and the transitions in and out of
|
||||
// stand / run / reverse each have their own handed pair so the mech always
|
||||
// leaves a cycle on the correct foot.
|
||||
//
|
||||
enum MechAnimationState
|
||||
{
|
||||
StandingAnimation = 0x00,
|
||||
RightStandToWalkAnimation = 0x01,
|
||||
RightWalkForwardAnimation = 0x02,
|
||||
LeftWalkForwardAnimation = 0x03,
|
||||
RightWalkToStandAnimation = 0x04,
|
||||
LeftWalkToStandAnimation = 0x05,
|
||||
RightWalkToRunAnimation = 0x06,
|
||||
LeftWalkToRunAnimation = 0x07,
|
||||
RightRunAnimation = 0x08,
|
||||
LeftRunAnimation = 0x09,
|
||||
RightRunToWalkAnimation = 0x0a,
|
||||
LeftRunToWalkAnimation = 0x0b,
|
||||
RightStandToReverseAnimation = 0x0c,
|
||||
LeftStandToReverseAnimation = 0x0d,
|
||||
RightReverseAnimation = 0x0e,
|
||||
LeftReverseAnimation = 0x0f,
|
||||
RightReverseToStandAnimation = 0x10,
|
||||
LeftReverseToStandAnimation = 0x11,
|
||||
LeftWalkToGimpAnimation = 0x12,
|
||||
RightWalkToGimpAnimation = 0x13,
|
||||
LeftGimpAnimation = 0x14,
|
||||
RightGimpAnimation = 0x15,
|
||||
LeftGimpToStandAnimation = 0x16,
|
||||
RightGimpToStandAnimation = 0x17,
|
||||
FallForwardAnimation = 0x18,
|
||||
FallBackwardAnimation = 0x19,
|
||||
FallLeftAnimation = 0x1a,
|
||||
FallRightAnimation = 0x1b,
|
||||
CrashAnimation = 0x1c,
|
||||
AnimationCount = 0x1d
|
||||
};
|
||||
|
||||
//###########################################################################
|
||||
//############################## Mech ###############################
|
||||
//###########################################################################
|
||||
@@ -274,6 +322,44 @@
|
||||
void
|
||||
Simulate(Scalar time_slice);
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------
|
||||
// The gait transition machine (mech2.cpp).
|
||||
//
|
||||
// Set*Animation binds a channel's clip player to the clip for a state
|
||||
// and records the state; *Transition is the shared tail every handler
|
||||
// ends in (bind the next state, then spend the leftover time in it).
|
||||
//
|
||||
// *ClipFinished are the callbacks the clip players invoke at end of
|
||||
// clip -- hence static, taking the mech explicitly. They are
|
||||
// RE-ENTRANT BY CONTRACT: each re-arms its channel and advances the
|
||||
// carryover itself, returning the distance that carryover covered, so
|
||||
// SequenceController::Advance folds the result into its own return.
|
||||
//--------------------------------------------------------------------
|
||||
//
|
||||
void
|
||||
SetLegAnimation(int state);
|
||||
void
|
||||
SetBodyAnimation(int state);
|
||||
|
||||
Scalar
|
||||
LegTransition(int next_state, Scalar advance_time, int move_joints);
|
||||
Scalar
|
||||
BodyTransition(int next_state, Scalar advance_time, int move_joints);
|
||||
|
||||
static Scalar
|
||||
LegClipFinished(
|
||||
Mech *mech,
|
||||
unsigned callback_arg,
|
||||
Scalar carryover,
|
||||
int move_joints);
|
||||
static Scalar
|
||||
BodyClipFinished(
|
||||
Mech *mech,
|
||||
unsigned callback_arg,
|
||||
Scalar carryover,
|
||||
int move_joints);
|
||||
|
||||
//
|
||||
// The respawn heal-and-move (binary @0049fb74): reposition the SAME
|
||||
// entity at the drop-zone origin, kill all motion, clear the death
|
||||
@@ -472,6 +558,39 @@
|
||||
StateIndicator collisionState;
|
||||
SequenceController legAnimation;
|
||||
SequenceController bodyAnimation;
|
||||
|
||||
//
|
||||
// GAIT ANIMATION (mech2.cpp). Two parallel channels, each a clip
|
||||
// player plus a state alarm holding the current MechAnimationState:
|
||||
//
|
||||
// LEG channel -- the locally-simulated gait. Its transitions read
|
||||
// the LIVE commanded speed from the controls mapper.
|
||||
// BODY channel -- the displayed-motion gait, whose advanced distance
|
||||
// is what feeds the mech's forward motion. Its
|
||||
// transitions read bodyTargetSpeed instead, so a
|
||||
// dead-reckoned or networked mech walks correctly
|
||||
// without a local controls mapper.
|
||||
//
|
||||
// The alarms ARE the state (binary mech+0x3b0 / +0x728 mirror the
|
||||
// alarm level); read them with GetLevel rather than keeping a copy.
|
||||
//
|
||||
AlarmIndicator legStateAlarm;
|
||||
AlarmIndicator bodyStateAlarm;
|
||||
Scalar legCycleSpeed; // channel-A cycle speed
|
||||
Scalar bodyCycleSpeed; // channel-B cycle speed
|
||||
Scalar forwardCycleRate; // cycle-speed slew rate
|
||||
Scalar gimpCycleRate; // slew rate while limping
|
||||
Scalar standSpeed; // "moving at all" threshold
|
||||
Scalar gimpSpeedMax; // limp cycle speed cap
|
||||
Scalar gimpStrideLength; // limp clip length (NEGATIVE
|
||||
// as authored -- the machines
|
||||
// fold the sign)
|
||||
Scalar globalTimeScale; // multiplies every increment
|
||||
//
|
||||
// Clip handle per MechAnimationState (binary mech+0x5cc, indexed by
|
||||
// the state enum). Resolved from the mech's model resource.
|
||||
//
|
||||
int animationClips[AnimationCount];
|
||||
AverageOf<Scalar> telemetryFilter[5];
|
||||
CString resourceNameA;
|
||||
CString resourceNameB;
|
||||
@@ -570,7 +689,11 @@
|
||||
// mech2/mech3/mech4 simulation. Reserved until that path is
|
||||
// reconstructed with named fields.
|
||||
//
|
||||
int reservedState[191];
|
||||
//
|
||||
// 41 ints carved out for the gait channel above (2 AlarmIndicators = 4,
|
||||
// 8 Scalars, animationClips[0x1d] = 29); 191 -> 150.
|
||||
//
|
||||
int reservedState[150];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
//===========================================================================//
|
||||
// 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);
|
||||
Verify(state >= 0 && state < AnimationCount);
|
||||
|
||||
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 < AnimationCount);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# MECH2.CPP — reconstruction notes
|
||||
|
||||
**Status: the TRANSITION MACHINE reconstructed (compile-verified 2026-08-02,
|
||||
BT 51/51, links clean). The four per-frame `Advance*` entry points and the two
|
||||
`Gimp*ClipFinished` machines are NOT yet written — see "What is deferred".**
|
||||
|
||||
`mech2.cpp` is the mech's gait: which walk clip is playing, when it changes,
|
||||
and to what. It sits between the locomotion demand and the clip player:
|
||||
|
||||
```
|
||||
Mech::Simulate speed/turn demand
|
||||
-> Mech::AdvanceLegAnimation DEFERRED -- the per-frame entry point
|
||||
-> SequenceController::Advance [[SEQCTL]] -- keyframes -> joint writes
|
||||
-> Mech::LegClipFinished THIS FILE -- end of clip, pick the next
|
||||
-> Mech::LegTransition THIS FILE -- bind it, spend the leftover
|
||||
```
|
||||
|
||||
## The two channels, and why they are near-duplicates
|
||||
|
||||
A mech runs two parallel clip channels over the same state machine and the
|
||||
same clips. The only difference is which speed the transitions consult:
|
||||
|
||||
| | reads | why |
|
||||
|---|---|---|
|
||||
| **LEG** (`legAnimation`, `legStateAlarm`) | the LIVE mapper `GetSpeedDemand()` | responds to the stick immediately |
|
||||
| **BODY** (`bodyAnimation`, `bodyStateAlarm`) | `bodyTargetSpeed`, a snapshot | lets a dead-reckoned or networked mech walk with no mapper of its own |
|
||||
|
||||
That is why `LegClipFinished` and `BodyClipFinished` are near-twins rather
|
||||
than one shared routine — it is how the binary has it (two separate jump
|
||||
tables, @0x4a69aa and @0x4a6e0a). **Keep them twins.** Where the two tables
|
||||
agree, a divergence in this file is a bug, and that mutual check is worth more
|
||||
than the duplication costs.
|
||||
|
||||
## Every 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 a cycle on the correct foot. The `MechAnimationState` enum (now in
|
||||
[[MECH]]'s header) is **verbatim** from the 0x3c-stride name table at
|
||||
`.data:0050cfe8` — the table the "Unsupported mech animation" assert indexes —
|
||||
so the names and their order are the original's, not inferred from behaviour.
|
||||
|
||||
## The commit test, in both machines
|
||||
|
||||
Every walk handler has three exits, and the two that leave the cycle test
|
||||
**both** the demand and the current cycle speed slewed by one carryover:
|
||||
|
||||
```c
|
||||
if (demand < standSpeed && (cycle - cycleRate * carryover) < standSpeed)
|
||||
-> walk-to-stand
|
||||
if (demand > walkStrideLength && (cycle + cycleRate * carryover) > walkStrideLength)
|
||||
-> toward the run cycle
|
||||
else
|
||||
-> the next stride, other foot
|
||||
```
|
||||
|
||||
Requiring both means a momentary flick of the stick cannot yank the mech out
|
||||
of a stride it has already committed to. Dropping either half of those
|
||||
conjunctions would give a mech that stutters between gaits on noisy input.
|
||||
|
||||
## Two things that read wrong and are not
|
||||
|
||||
**`gimpStrideLength` is authored NEGATIVE.** The cycle time computed from it
|
||||
comes out negative and is folded positive before being spent (binary
|
||||
@0x4a6c6e / @0x4a6d3d). The fold is not defensive coding — remove it and the
|
||||
limp plays backwards.
|
||||
|
||||
**States 16–19 on the body channel are the REVERSE gait, not a limp**, despite
|
||||
sharing the `gimpSpeedMax` / `gimpCycleRate` caps. While the demand stays
|
||||
below the cap the cycle alternates 0x12 ↔ 0x13; a forward demand exits through
|
||||
the back-to-stand pair. BT411 records having read these as "gimp, not decoded,
|
||||
fall back to standing" twice, which makes the body loop stand → reverse-entry
|
||||
forever — a slow reverse with a wrong-footed exit. The reading here is by
|
||||
structural symmetry with the leg table, where every previously-decoded body
|
||||
case mirrors its leg twin.
|
||||
|
||||
## What is deferred
|
||||
|
||||
Six of the twelve functions the manifest attributes to this TU:
|
||||
|
||||
| | why it is not here yet |
|
||||
|---|---|
|
||||
| `AdvanceLegAnimation` @004a5028 | the per-frame entry points — the next increment |
|
||||
| `AdvanceBodyAnimation` @004a5678 | |
|
||||
| `AdvanceBodyAnimationGimp` @004a5bf8 | the airborne/jump-jet flavours |
|
||||
| `AdvanceLegAnimationGimp` @004a71f4 | |
|
||||
| `GimpBodyClipFinished` @004a6344 | the limp transition machines, entered from the |
|
||||
| `GimpLegClipFinished` @004a7970 | top of the two `*ClipFinished` above on gimp level 3/4 |
|
||||
|
||||
The gimp-level branch at the top of both `*ClipFinished` is therefore also
|
||||
absent: a limping mech currently runs the normal machine. That branch needs a
|
||||
TU-safe read of the graphic alarm level (BT411 routes it through a
|
||||
`mechdmg.cpp` bridge to avoid an `AlarmIndicator` ODR split) — worth
|
||||
reproducing carefully rather than reaching for the alarm directly.
|
||||
|
||||
**Nothing calls any of this yet.** The `Advance*` functions are the entry
|
||||
points and they are the deferred half, so no gait state is ever selected and a
|
||||
run behaves exactly as before. Same honest caveat as [[SEQCTL]]: this is a
|
||||
blocker removed, not a behaviour delivered.
|
||||
|
||||
## Header changes
|
||||
|
||||
`MECH.HPP` gained the enum, the six method declarations, and the channel
|
||||
state: `legStateAlarm` / `bodyStateAlarm` (`AlarmIndicator` — read the state
|
||||
with `GetLevel`, the binary's mech+0x3b0 / +0x728 are mirrors of the alarm
|
||||
level, so no separate int is kept), `legCycleSpeed`, `bodyCycleSpeed`,
|
||||
`forwardCycleRate`, `gimpCycleRate`, `standSpeed`, `gimpSpeedMax`,
|
||||
`gimpStrideLength`, `globalTimeScale`, and `animationClips[AnimationCount]`.
|
||||
41 ints carved from `reservedState`, 191 → 150.
|
||||
|
||||
`walkStrideLength`, `reverseStrideLength`, `reverseSpeedMax` and
|
||||
`bodyTargetSpeed` were already present from the Phase 5.3 locomotion work and
|
||||
are reused, not duplicated.
|
||||
|
||||
**Still unsourced: `animationClips[]`.** The array is declared but nothing
|
||||
fills it. The clip handles come from the mech's model resource, and resolving
|
||||
them is a prerequisite for the `Advance*` increment — `SetLegAnimation` would
|
||||
otherwise hand `SelectSequence` a garbage ID. `SelectSequence` tolerates a
|
||||
missing resource (empty controller, inert playback), so this fails soft rather
|
||||
than crashing, but it must be wired before the gait can do anything.
|
||||
Reference in New Issue
Block a user