Files
TeslaRel410/restoration/source410/BT/MECH2.NOTES.md
T
CydandClaude Fable 5 1294aff371 BT410 5.3.104a: the duck's driver is unfound -- recorded, not invented
The squat clips and states exist end to end, but the binary's DuckRequest
handler only lights the cockpit lamp (duckState -> the gauge databinding),
and no decompiled code arms states 1/2/3 or calls SetDuckedCollisionTemplate.
The mechanization lives in the un-decompiled master perf. Noted in
MECH2.NOTES.md so nobody re-derives the dead end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:57:32 -05:00

391 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MECH2.CPP — reconstruction notes
**Status: ALL TWELVE FUNCTIONS RECONSTRUCTED (2026-08-03) — the gait AND the
limp are live-verified. The walk shows 16 of 22 joints in paired strides; the
forced limp breaks the pair symmetry exactly as it should (drag-leg joint at
18 poses vs its partner's 55) with the hull lurching at 1023 under a 26.9
demand.**
`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 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[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-1619
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 2227 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.
## The clip loader is in (2026-08-02) — and it ran live
`ResolveAnimationClip` / `MeasureClipStride` / `LoadClipSlot` /
`LoadLocomotionClips` are reconstructed and WIRED: the ctor's GameModel block
calls the loader while the model is locked, replacing the Phase 5.3 bring-up
locomotion defaults with values measured from the actual clips. First live
run (arena mission, MAD):
```
[mech] clips 'mad': standSpeed=5.23 walkStride=18.51 revStride=56.05
revSpeedMax=26.26 gimpSpeedMax=-4.23 gimpStride=-20.26 limpSet=1
```
The 'mad' prefix printing as text is itself evidence the `Mech__ModelResource`
layout is right at +0x40. The reverse figures come out negative, as the
transition machines expect. **Driving feel changed with this**: speedDemand at
0.6 throttle went 14.4 → 26.9, because the placeholder top speed (30) gave way
to the measured 56.05. That is authenticity arriving, not a regression.
### Two binary behaviours reproduced on purpose
**The speed caps read `keyframeData[keyframeCount]`** — one entry past the
last frame (`0x690 + 8 + [0x670]*0xc`). Whether the authored table carries
count+1 entries or the read lands on adjacent resource bytes is not yet
established; it is what the binary does, the clips were authored against it,
and the measured values above look sane.
**The reverse-cycle stride is computed from STALE data.** The decomp is
unambiguous: bbr and bbl are both measured into `local_8/local_c`, then the
divide takes its second terms from `local_10/local_14` — still holding the
run-left (rrl) figures. `gimpStrideLength = -((bbl + rrl_stale)/(bbl_t +
rrl_t_stale))`. A 1995 copy-paste bug, shipped in every pod, reproduced here
with a comment. The wwr/wwl and rrr/rrl blocks above it show the intended
pattern. (Also settled: the negation IS in the binary — `0x350 = -0x350` on
the very next instruction — an earlier decomp window cut just before it and
briefly suggested otherwise.)
### One deliberate divergence
The binary dereferences every `ResolveAnimationClip` result unguarded — a
model missing a mandatory clip crashes on load. Here a miss stores
`NullResourceID` (SelectSequence resolves that to an empty, inert controller)
and the dependent measurement is skipped, keeping the bring-up default.
Tagged [T3] in the source; revisit once every fleet mech's clip set is
known-good. The measurement binds also pass a NULL finished-callback where
the binary passes live pointers — measurement only parses, never plays, so
the callback cannot fire; NULL avoids arming a transition machine mid-load.
### What "next" looks like now
The array is filled and every constant is measured. The remaining half of
this TU is the four `Advance*` entry points (wired into `Mech::Simulate`) and
the two `Gimp*ClipFinished` machines. When `AdvanceLegAnimation` lands, the
gait will select clips and SEQCTL will write joints — the first frame where
the legs actually move.
## The entry points are in — and the legs walk (2026-08-03)
`AdvanceLegAnimation` (@004a5028) and `AdvanceBodyAnimation` (@004a5678)
reconstructed **from the raw decomp, not the BT411 donor** — the donor's
versions carry port-era replicant accommodations and a turn-in-place
dispatcher relocated from mech4's master performance, none of which is 1995
code. In the binary the mapper cell replicates, so the leg version reads the
mapper unconditionally, and what ARMS state 4 is mech4's dispatcher (not yet
reconstructed — a mech can hold a turn clip if something else arms it, but
nothing arms it yet).
Wired into `Mech::Simulate`: leg channel with joints, body channel as pure
measurement (`move_joints` 0). The body's measured distance is dropped for
now — consuming it as the forward step is `IntegrateMotion`'s job (mech4).
The seam is marked STAGED at the call site.
**Live verification** (arena mission, auto-throttle, `BT_JOINTS=1`):
```
before 22 handles on the wire, 1 animating (the root)
after 22 handles on the wire, 16 animating, 0 two-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 pairs is six joints per leg cycling in alternating strides —
the pose-count symmetry is itself evidence the handed clip alternation is
running correctly. No fault, mission drove itself clean.
### Structure notes for the next reader
* **The channels differ more than the ClipFinished twins do.** The leg
version has the wind-down block (cycle decayed → drop to standing), the
turn-in-place case 4, and the "Standing Not Supported" guard; the body
version has NONE of those — its case 4 sits in the plain group, and its
reset passes `move_joints` through to `Reset`. All binary-verified.
* **Case 0 falls THROUGH.** Arming stand→walk (or stand→reverse) drops into
the plain-advance group so the new clip advances the same frame. The
Standing guard inside that group is unreachable via the fall-through
(arming rewrote the level) — it catches direct entry only.
* **A slow walk is the walk clip played slow.** Each cycle advances its clip
at `cycle/strideLength` of the authored rate, with the cycle speed slewed
toward the demand inside per-gait caps. The reverse cycle's caps are all
NEGATIVE and its advance ratio is folded positive — the clip is authored
backward, not played backward.
* **Two members remain UNSOURCED**: `idleStrideScale` (+0x5ac, defaults 1)
and `runSpeedMax` (+0x7a0, the run cycle's upward cap — not set by
LoadLocomotionClips; defaulted huge so it never binds). Finding their real
writers is open work (model resource? mech3/mech4?).
* `ForceUpdate(8)` (the leg-state update-record request, binary @004a4c54
inline) is a STAGED no-op — our replication emitter is not reconstructed.
## The stride became the speed (2026-08-03)
The STAGED seam in `Simulate` is half-closed: the body channel's measured
distance is now the mech's speed (`currentBodySpeed = stride / dt`), which is
the binary's own relationship (`IntegrateMotion` @004ab1c8 sets local velocity
z = distance/dt). The explicit acceleration model survives only as a [T3]
fallback for a model with no gait clips at all.
Two findings made this safe rather than speculative:
**The gait slew rate IS the authored acceleration.** The binary's ctor sets
`forwardCycleRate`, `gimpCycleRate` and `groundCycleRate` all from the model's
maxAcceleration (rec+0x44; 30 for the Mad Cat), and `airborneCycleRate` from
superStopAcceleration. So the stride-driven speed slews at exactly the rate
the old model used — feel preserved, source authentic. Our ctor now sets
`forwardCycleRate`/`gimpCycleRate` from the same guarded model read. (This
also retires the ctor default of 1.0, which would have taken ~27 s to reach
speed.)
**Live curve, arena run:** first sample 7.32 (stepping off through the
stand→walk clip), then 2230 oscillating around the demanded 26.9 — the
per-stride speed variation of a real gait, where the old model pinned
26.9022 flat. Cadence in the hull motion is the authentic behaviour.
Still staged from IntegrateMotion: the airborne flavour pick
(`movementMode 3/4 && +0x580`), the dead-reckon latency fold (+0x778/+0x77c),
the local-velocity/orientation integration proper, and the turn-in-place
dispatcher. One correction queued by the raw decomp: `+0x598` is written by a
VECTOR op in IntegrateMotion's replicant branch (FUN_00408644 on floats), so
`motionEventName` is likely a Point3D, not a CString — revisit when that
branch is reconstructed.
## The limp, settled and live (2026-08-03)
The "gimp" family had two conflicting donor readings — limp vs jump-jet. The
binary settles it: **limp**. The mode test in every gimp function is
`mech+0x40` ∈ {3, 4}, the same values the damage model documents as "limp
gait graphic (left 3 / right 4)"; there is no jump-jet control anywhere in
the cockpit; and the donor's own port routing (graphic-alarm 3/4) was reading
the same field under a different name. The donor's "run-jump clip"
annotations in its mech3 member map were its misreading, which is why its
mech2 header said "airborne/jump-jet" — recorded so nobody re-litigates it.
**How the limp actually works** (from @004a7970/@004a6344 + the advancers
@004a71f4/@004a5bf8, all four now reconstructed):
* The limp replaces ONE stride. Limping left, the right stride (6) hands off
to the left limp figure (0x16 → the self-cycling 0x18); the other leg's
clips keep their normal alternation. That asymmetry is what reads as a
limp rather than a different gait.
* Both machines CLAMP THEIR DEMAND while in a cycle — the leg machine writes
the mapper's own speedDemand cell down to the damaged side's cap (hence
`MechControlsMapper::SetSpeedDemand`), the body machine clamps
bodyTargetSpeed, and both floor at zero. A limping mech cannot command
more speed than its figure carries, nor reverse out of a forward cycle.
* The limp flavours of Advance* keep states 0x160x1b LIVE (the normal
flavours treat them as the reset group — which is precisely why the limp
flavours must be selected while limping, or the figure gets neutralized
mid-cycle). They carry no death latch and no wind-down: movement modes are
exclusive, a limping mech is not falling.
* All movement-mode reads go through `Mech::MovementMode()` (mech+0x40 ==
the simulation state), which also honours the `BT_FORCE_LIMP=3|4` dev hook
(set once in the ctor) so the machinery can be exercised before the damage
model's limp hook exists.
**Verification, two runs:**
* No-regression: the normal mission's speed sequence is BIT-IDENTICAL to
5.3.95 (7.31972, 26.6726, 22.1601, …) — the branch costs nothing when not
limping.
* Forced left limp (`pod_render_limp.conf`): demand stays 26.9, hull lurches
at 1023; and on the wire the healthy run's tight pose-count pairs
(650/649 … 434/421) break to a 3× asymmetry — the drag-leg joint at 18
poses against its partner's 55. No fault, no "Unsupported mech animation".
**Still open in this family:** the damage hook itself (leg zone ≥ 0.5 →
mode 3/4 — a MECHDMG increment; nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces (orientation/velocity integration, the
dead-reckon fold, turn-in-place arming).
## The duck: clips found, driver unfound (2026-08-03)
5.3.103 put the squat clips in slots 3 (`squ`) and 2 (`sqd`) with
`squatCapable`, and the machines have always carried the matching states —
2 plays the squat-down and holds at 1 (Advance returns 0: the crouch hold),
3 stands back up. But **nothing arms them**. The binary's `DuckRequest`
handler (@0049fa00, the RIO crouch button, id 0x1a) is one line —
`duckState = 1` — and BT411's search found no reader of `duckState` in any
decompiled code: its consumer is the cockpit DATABINDING
(`oneOfSeveralPixInt(...DuckState)` — the duck lamp), and the mech reset is
the only other writer. `SetDuckedCollisionTemplate` (@004ac04c) exists to
shrink the collision cylinder, caller likewise unfound (likely the
un-decompiled master perf).
So the crouch's mechanization lives in code nobody has decompiled yet.
Writing an arming path would be invention; the states stay dormant until
the master-perf disasm names their driver. Recorded so the next reader
doesn't burn a session rediscovering this.