Commit Graph
5 Commits
Author SHA1 Message Date
CydandClaude Fable 5 9a74f2aa09 BT410 5.3.94: THE LEGS WALK -- AdvanceLeg/BodyAnimation live, 16 of 22 joints animating in paired strides
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>
2026-08-03 09:25:56 -05:00
CydandClaude Fable 5 a87c49c158 BT410 5.3.93: the clip loader runs LIVE -- the Mad Cat measures itself, and a 1995 copy-paste bug ships again on purpose
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>
2026-08-02 12:23:04 -05:00
CydandClaude Fable 5 64f99ffcc8 BT410 5.3.92a: reconcile two stale claims in MECH2.NOTES.md
The slot-map section added in 5.3.92 corrected two things the doc still
asserted higher up: that gimpStrideLength is authored negative (the sign is
applied at measurement) and that animationClips is sized AnimationCount (it is
AnimationSlotCount, 0x21). Both now agree with the correction rather than
leaving a reader to hit the stale version first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 08:15:52 -05:00
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