Files
TeslaRel410/restoration/source410/BT/MECH2.NOTES.md
T
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

5.8 KiB
Raw Blame History

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:

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 1619 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.