# 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 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 cycle plays backwards. (The sign is applied at MEASUREMENT, not authored into the data — see the slot map below.) **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[AnimationSlotCount]` (0x21 — see the correction below). 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. ## The slot map (LoadLocomotionClips) — and why the enum is not it Recovered from the clip loader. **`animationClips[slot]` semantics, which do NOT match the name-table enum**, plus what each measured constant is taken from. Suffixes are the 3-char codes the loader appends to the model's animation prefix: | slot | clip | meaning | measures | |---|---|---|---| | 5 | `swr` | stand → walk R | `standSpeed` = final-keyframe stride | | 6 / 7 | `wwr` / `wwl` | the forward walk CYCLE | `walkStrideLength` = (s6+s7)/(d6+d7) | | 8 / 9 | `wsr` / `wsl` | walk → stand | | | 10 / 11 | `wrr` / `wrl` | walk → run | `reverseSpeedMax` from slot 10 | | 12 / 13 | `rrr` / `rrl` | the run CYCLE | `reverseStrideLength` = (s12+s13)/(d12+d13) | | 14 / 15 | `rwr` / `rwl` | run → walk | | | 16 / 17 | `sbr` / `sbl` | stand → back (reverse entry) | `gimpSpeedMax` from slot 16 | | 18 / 19 | `bbr` / `bbl` | the reverse CYCLE | `gimpStrideLength` = **−**(s+s)/(d+d) | | 20 / 21 | `bsr` / `bsl` | back → stand (reverse exit) | | | 22 / 23 | `wgl` / `wgr` | walk → limp | `gimpLeft/RightSpeedMax` | | 24 / 25 | `ggr` / `ggl` | the limp CYCLE | `gimpLeft/RightStrideLength` | | 26 / 27 | `gsl` / `gsr` | limp → stand | | | **0x20** | `bmp` | bump / crash stagger | — | Three things fall out of this table that are easy to get wrong: **The `gimp*`-named members are the REVERSE figures, not the limp ones.** The names are historical. `gimpSpeedMax` / `gimpStrideLength` are measured from `sbr` and `bbr`/`bbl` — the reverse gait. The actual limp has its own `gimpLeft*` / `gimpRight*` pair. This is the same trap as the states-16–19 misreading recorded above, from the same bad naming. **`gimpStrideLength` is negated at the point of measurement** — that is where the negative sign the transition machines fold comes from, not from the authored data being odd. **The limp clips are OPTIONAL.** The loader probes for `wgl`; if the model lacks it, `hasGimpClips` stays 0 and slots 22–27 are never filled. So a limping mech on a model without limp clips must fall through to the normal machine — which is, conveniently, exactly what the deferred gimp branch will have to check. ### Corrected after the fact `animationClips` was first sized `[AnimationCount]` (0x1d) from the enum. That is wrong — slot 0x20 is the bump clip, so the array is `[AnimationSlotCount]` (0x21) and `Set*Animation`'s `Verify` bounds against that. Sizing a real array off a name table that stops earlier is the sort of thing that reads fine and corrupts the object next door; caught by reading the loader, not by the compiler. ### Still to source Filling the array needs `Mech::ResolveAnimationClip` (@004a7f50) and `Mech::MeasureClipStride` (@004a8054) plus `LoadLocomotionClips` (@004a80d4) and `LoadLocomotionClipsExt` (@004a86c8, the 4-char-code variant). Note the manifest attributes all four to **mech2.cpp** while BT411 files them under mech3 — the manifest's attribution comes from the binary's own file tagging, so they belong here.