Commit Graph
2 Commits
Author SHA1 Message Date
CydandClaude Fable 5 3629755d90 BT410 5.3.92: the gait slot map -- the clip array is bigger than the name table, and the "gimp" members are the REVERSE figures
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>
2026-08-02 08:15:17 -05:00
CydandClaude Fable 5 d8aea8d871 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>
2026-08-02 01:14:02 -05:00