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>
182 lines
9.1 KiB
Markdown
182 lines
9.1 KiB
Markdown
# 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.
|