diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index 8a0e6b03..3369846b 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -186,9 +186,34 @@ FallLeftAnimation = 0x1a, FallRightAnimation = 0x1b, CrashAnimation = 0x1c, - AnimationCount = 0x1d + AnimationCount = 0x1d, + + // + // The clip ARRAY is larger than the name table. Slot 0x20 holds the + // bump/crash clip (mech+0x64c) bound on a hard wall impact, so the + // array runs to 0x21 entries even though only 0x1d are named. + // + AnimationSlotCount = 0x21 }; +// +// A WARNING ABOUT THE NAMES ABOVE, which are verbatim from the binary's table +// and therefore authoritative as NAMES -- but are NOT a reliable guide to what +// each slot actually plays. +// +// The clip loader's slot assignments (LoadLocomotionClips) disagree with them. +// Slot 0x0e takes the "rwr" run-to-walk clip while the table calls 0x0e +// RightReverseAnimation; the reverse gait actually lives at 0x10-0x15 +// (sbr/sbl entry, bbr/bbl cycle, bsr/bsl exit); and the states the gait +// machine alternates between while walking forward are 6 and 7, not the +// pair the "WalkForward" names suggest. +// +// Read the slot map in MECH2.NOTES.md when the question is "what does this +// state play"; read the enum when the question is "what did the original call +// this index". Conflating the two is how BT411 shipped a reverse gait that +// played its coming-to-a-halt frames mid-cycle. +// + //########################################################################### //############################## Mech ############################### //########################################################################### @@ -587,10 +612,14 @@ // fold the sign) Scalar globalTimeScale; // multiplies every increment // - // Clip handle per MechAnimationState (binary mech+0x5cc, indexed by - // the state enum). Resolved from the mech's model resource. + // Clip handle per gait SLOT (binary mech+0x5cc, indexed by state). // - int animationClips[AnimationCount]; + // SIZED 0x21, NOT AnimationCount. 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. Sizing this by the enum + // leaves that slot off the end. + // + int animationClips[AnimationSlotCount]; AverageOf telemetryFilter[5]; CString resourceNameA; CString resourceNameB; diff --git a/restoration/source410/BT/MECH2.CPP b/restoration/source410/BT/MECH2.CPP index 58b260b8..abea1fb1 100644 --- a/restoration/source410/BT/MECH2.CPP +++ b/restoration/source410/BT/MECH2.CPP @@ -63,7 +63,11 @@ void Mech::SetLegAnimation(int state) { Check(this); - Verify(state >= 0 && state < AnimationCount); + // + // Bounded by the SLOT count, not the name count: slot 0x20 is the + // bump/crash clip and is legitimately bound on a wall impact. + // + Verify(state >= 0 && state < AnimationSlotCount); legAnimation.SelectSequence( animationClips[state], @@ -86,7 +90,7 @@ void Mech::SetBodyAnimation(int state) { Check(this); - Verify(state >= 0 && state < AnimationCount); + Verify(state >= 0 && state < AnimationSlotCount); bodyAnimation.SelectSequence( animationClips[state], diff --git a/restoration/source410/BT/MECH2.NOTES.md b/restoration/source410/BT/MECH2.NOTES.md index a67485a7..43b4c87e 100644 --- a/restoration/source410/BT/MECH2.NOTES.md +++ b/restoration/source410/BT/MECH2.NOTES.md @@ -1,120 +1,179 @@ -# 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 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 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[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. +# 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 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 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[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. + +## 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.