The "gimp" family had two conflicting donor readings -- limp vs jump-jet --
flagged in 5.3.94 rather than guessed at. The binary settles it: LIMP. The
mode test in every gimp function is mech+0x40 in {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 pod cockpit; and the donor's port routing
("graphicAlarm 3/4") was the same field under a different name. Its
"run-jump clip" mech3 annotations were the misreading that started the murk.
RECONSTRUCTED, completing all 12 of mech2's census functions:
GimpLegClipFinished @004a7970 GimpBodyClipFinished @004a6344
AdvanceLegAnimationGimp @004a71f4 AdvanceBodyAnimationGimp @004a5bf8
+ the limp branch atop both normal *ClipFinished
+ the limp pick in Simulate (replaces the normal advancers while limping)
HOW A LIMP WORKS, now from the bytes rather than description:
It replaces ONE stride. Limping left, the right stride (6) hands off to
the left limp figure (0x16 -> the self-cycling 0x18) while the other leg
keeps its normal clips. The asymmetry IS the limp.
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
(new MechControlsMapper::SetSpeedDemand, matching the binary's direct
mapper+0x128 write), the body machine clamps bodyTargetSpeed, both floor
at zero. A limping mech cannot command more than its figure carries, nor
reverse out of a forward cycle.
The limp advancers keep states 0x16-0x1b LIVE -- the normal advancers
treat those as the reset group, which is exactly why the limp flavours
must be selected while limping or the figure is neutralized mid-cycle.
No death latch, no wind-down: the movement modes are exclusive.
ALSO EXPLAINED IN PASSING: Ghidra's 3760-byte FUN_004a6344 -- the census's
largest function -- is really THREE functions. The two normal ClipFinished
callbacks (@004a6928/@004a6d8c) are reached only via data pointers, so the
decompiler folded them into the gimp-body machine's extent.
All movement-mode reads route through Mech::MovementMode() (mech+0x40 == the
simulation state), which honours a BT_FORCE_LIMP=3|4 dev hook so the gait
could be verified before the damage model's limp hook exists.
VERIFIED, two runs on the rig:
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.
FORCED LEFT LIMP (new pod_render_limp.conf): the mapper still demands
26.9; the hull lurches at 10-23. And on the wire the healthy walk's tight
pose-count pairs (650/649 ... 434/421) BREAK to a 3x asymmetry -- the
drag-leg joint at 18 poses against its partner's 55. The limp is visible
in the data exactly the way it will be visible on screen.
STILL OPEN in this family: the damage hook (leg zone >= 0.5 -> mode 3/4, a
MECHDMG increment -- nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
372 lines
20 KiB
Markdown
372 lines
20 KiB
Markdown
# 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 10–23 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 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.
|
||
|
||
## 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 22–30 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 0x16–0x1b 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 10–23; 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).
|