Files
BT412/context/locomotion.md
T
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00

142 lines
11 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.
---
id: locomotion
title: "Locomotion — gait, the ground model, collision, shadow"
status: established
source_sections: "PROGRESS_LOG.md §7, §10 (gait/ground/collision/shadow); docs/P3_LOCOMOTION.md"
related_topics: [asset-formats, decomp-reference, reconstruction-gotchas, combat-damage, rendering]
key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint]
open_questions:
- "Authentic per-mech turn-rate constant (currently bring-up constant; the authored WalkingTurnRate/RunningTurnRate now STREAM correctly since the task #4 record-layout fix -- rec words 0x18/0x19 deg->rad -> mech+0x574/0x578, Wword-absorbed -- wiring them in retires the bring-up rate)"
- "Airborne clip callbacks (FUN_004a7970) deferred"
---
# Locomotion
Walking, the gait controller, the ground/collision model, and the shadow. All DEFAULT-ON now
(`BT_GAIT_CUTOVER`, `BT_GAIT_SM`, `BT_COLLISION`, `BT_REAL_CONTROLS` — set `=0` to fall back). Full
detail: `docs/P3_LOCOMOTION.md` + `docs/PROGRESS_LOG.md §7, §10`.
## Walk speed is animation-driven (not physics)
Each `.ANI` has a `[RootTranslation]` where **`.z` = forward root SPEED** (world units/s). The
engine `AnimationInstance::Animate(dt, move_joints)` integrates `movement += Δt·rootTrans.z` and
moves the body that far → feet plant by construction (no stride constant; k=1). Idle anims have
z=0. The `.MOD` physics governed thrust VEHICLES, NOT walk gait. [T1]
## The MARCHING-GHOST desync — ROOT-CAUSED + FIXED (2026-07-13, live diagnosis) [T2]
The long-parked "replicant walks in place while its master stands still" reproduced live with
telemetry running: the master's `[gaitSM]` showed `state=0 cycleSpeed=-2.50725 kfCur=19` — BOTH
channels stuck at a REVERSE cadence inside Standing. Mechanism: the walk-family stop gate
(`cycleSpeed <= ZeroSpeed`, mech2.cpp) passes trivially for NEGATIVE (reverse) cadences, and
several stand-entry paths (turn exit, terminal poses) never touch the cycle — so a reverse-stop
enters Standing at full reverse cycle speed. The master looks still (case-0 never advances the
clip; `kfCur` freezes) but the stale cycle REPLICATES, and the peer's replicant animates it =
the marching ghost. A clean FORWARD stop decays the cycle first (`legSum ~3e-8` on the healthy
node) — which is why the bug was intermittent and direction-dependent. FIX: all four
Advance*Animation Standing cases zero their channel's cycle on entry + ForceUpdate(8) so the
peer hears the stop immediately. (The signed-demand change (task #15) is what let negative
cycles reach machinery written for forward decel.) The earlier "bob reduction + stutter"
symptom family likely shares this root — re-observe after this fix before chasing separately.
## The two-channel gait (real controls)
The BT `SequenceController` (`seqctl.cpp`, from `SelectSequence@004277a8`+`Advance@0042790c`) drives
locomotion. Under `BT_REAL_CONTROLS` two channels run — **roles corrected 2026-07-09 (task #49) from
raw disasm** (both Advance fns had NO static decomp caller; found by byte-scanning for `e8` calls):
- **LEG channel = the LOCAL sim.** The master perf (the `0x4a9b5c` gap region) calls
`AdvanceLegAnimation` @0x4aa399 (airborne @0x4aa388) and stores `-dist/dt` into **localVelocity
(+0x1cc)** — the leg drives the local mech's TRAVEL and, advancing LAST (mj=1), the DISPLAYED pose.
It reads the LIVE `speedDemand` and owns trn/turn-in-place + the early-stand wind-down guard. [T1]
- **BODY channel = the replication PROJECTOR.** `IntegrateMotion` (0x4ab1c8; body advance @0x4ab312,
from `FUN_004ab430`, the projected-origin/dead-reckon updater) stores `-dist/dt` into
**projectedVelocity (+0x2a0)** — the dead-reckoning feed for update records. Its phase drift is
locally INVISIBLE in the binary. The literal projectedVelocity store is unwired, but the
update-record velocity feed is live via the localVelocity/worldLinearVelocity maintenance
(mech4.cpp VELOCITY STORAGE block) — replicant gait (task #50) runs off it. [T1]
**v5 (current): display == travel BY CONSTRUCTION through the LEG** (body advances first, leg last).
History: v3 (travel=leg, display=body) foot-slipped — two channels, different consumers; v4 unified
BOTH on the body (mis-reading 0x4ab430 as the travel source) — feet planted, but whenever the two
state machines phase-split, the out-of-phase LEG pose showed through on frames the body didn't write
(Standing/wind-down) = the RECURRING "legs stutter / lose sync" report. The split mechanism [T2,
reproduced via `BT_FORCE_OSC` + `BT_FORCE_TURN` + `BT_SYNC_LOG`]: (a) SEED — the bring-up trn trigger
arms only the leg (authentic trn dispatcher still unlocated), so the walk re-entry lands 1 frame
apart; (b) AMPLIFIER — with offset clips the end-of-clip callbacks fire on different frames, so a
demand change landing between them picks DIFFERENT next states (one winds down, one keeps walking)
→ opposite-phase churn. Under v5 both effects still exist but only skew the projection channel. [T2]
The gait STATE MACHINE (`AdvanceBodyAnimation`, mech2.cpp) slews `bodyCycleSpeed` toward the demand
through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkStride`/`standSpeed` via
`legAnimation.SelectSequence`). Transition callbacks (`Mech::BodyClipFinished @0x4a6d8c`,
`LegClipFinished @0x4a6928` — PE-parsed jump tables) alternate walk 6↔7 / run 12↔13 + walk→run. [T2]
**Aliasing bug class (namedClip):** `namedClip[]`@0x5e0 == `animationClips[]`@0x5cc+0x14 (ONE array);
`gimpBaseClip`@0x64c == `animationClips[0x20]`; `bodyAnimationState`@0x728 == `bodyStateAlarm.level`.
Declared-separate members don't see each other's writes — alias them.
Replicants (task #50): travel = engine DeadReckon; leg channel driven joints-only from the replicated
worldLinearVelocity -> speedDemand (mech4.cpp REPLICANT MOTION) — detail in [[multiplayer]]. [T2]
## The AUTHENTIC 1995 ground model (default-on, `BT_GROUND_REAL`)
Decoded by the 10-agent `ground-model-decode` workflow (binary `FUN_004a9b5c` = the master perf).
**NO GRAVITY anywhere** — accelerations zeroed every frame. Vertical position = an ABSOLUTE per-frame
ground SNAP:
1. ctor lifts `collisionTemplate->minY` by 5% of volume X-width (the probe height + wall-vs-floor separation).
2. per frame `MoveCollisionVolume()` re-finds the containing node in the zone's **BoundingBoxTree**
(BOXTREE.cpp — NOT a heightfield).
3. probe `q = origin + (0, lift, 0)``FindBoundingBoxUnder(q, &h)` (h = distance down to the highest
solid top; 1 = miss).
4. snap gate `h > 1e-4`: `origin.y -= (h lift)` ⇒ y = surfaceY exactly. **MISS ⇒ NOTHING** (y holds,
no runaway). Up-slope bounded by the lift window (implicit step allowance); walk-offs drop instantly.
5. `GetCurrentCollisions``ProcessCollisionList``Mech::ProcessCollision` (@0x4abb40, vtbl+0x3c):
BoxedSolid resolver → StaticBounce → owner classification. Crushable icon (flags&0x8000) = damage
0.00123f sentinel = move stands; damage>0 = **FULL FRAME REJECTION** (walls block, never slide);
impactVel²>40 = crash-anim (SetLegAnimation(0x20)). [T2]
KEY: `FindSmallestNodeContainingColumn`/`FindBoundingBoxUnder` (`FUN_0040e36c`/`0040e5f0`, BOXTREE.CPP)
are ALIVE in the 2007 engine — the "heightfield" labels were wrong (caused two reverted attempts). A
master mech needs a **CollisionAssistant** (GetCurrentCollisions iterates it unchecked). BT solids
ship ZERO tiles (h never negative). [T2]
## Knockdown / crash + the desync/stutter fixes
Wall impacts iv²>40 (~6.3 u/s) bind the `bmp` stagger clip. Two documented bug fixes: (a) the
knockdown must stagger BOTH channels (`SetBodyAnimation(0x20)` + `SetLegAnimation(0x20)`) or display
+ travel split permanently (foot-slip); (b) a contact-hysteresis gate (0.4 s `gBlockCooldown`)
stops the knockdown re-firing on sustained/glancing contact. `localVelocity` is zeroed while crashed
(spec-faithful) so a knocked mech can't keep advancing. [T2]
## Controls (`BT_REAL_CONTROLS`, default-on)
`MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand`
/ `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s
them out) → poll `GetAsyncKeyState` per frame, gated on foreground. Virtual controls: W/S sweep a
persistent throttle LEVER with a zero detent; A/D a momentary auto-centering stick. [T2]
## Shadow (default-on, `BT_SHADOW_TILT`)
BT 4.10's shadow is the `*_tshd.bgf` proxy, posed by `jointshadow` (terrain tilt — the SKL's own
contract: "apply terrain angle to pitch and roll") + `jointtshadow` (torso twist). The working 2026-07-09
arrangement (user-verified live: feet layer over it, opaque, survives inclines/declines) has FOUR
co-dependent parts — change one, re-check the others:
1. **TILT** (mech.cpp `UpdateShadowJoint`): quad up aligned to the surface normal via an orthonormal
basis fed to the ENGINE's own LinearMatrix→EulerAngles conversion — hand-derived Euler signs are
exactly how two prior tilt attempts "dug into the hillside"/vanished. ~35° cliff-guard cap.
2. **SAMPLER** (mech4.cpp): gradient from the collision probes PLUS `BTVisualGroundLift` per probe
(the quad hugs the VISIBLE terrain, whose lift varies across a slope); world→local by the TRUE yaw
from `localToWorld` (NOT the `gDriveHeading` scalar mirror — the task-#48 drift bug class).
`BT_SHADOW_LOG` traces the normals.
3. **DRAW ORDER** (`PASS_SHADOW`, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw between the STATIC terrain
and the DYNAMIC opaque bodies (classic decal order), so the mech z-passes over the shadow — the
bias can never paint the shadow OVER the feet, structurally.
4. **BIAS pairing** (L4D3D.cpp): tilt on (default) → decal epsilon 0.0008; `BT_SHADOW_TILT=0`
(flat A/B fallback) → the old 0.004 it needs to survive slope burial. `BT_SHADOW_BIAS` overrides.
5. **LOADER-LEVEL TAG** (L4D3D.cpp `LoadObjectBGF`): `*tshd*` filenames get `SetIsShadow(1)` in the
LOADER, not at call sites. The V VIEW TOGGLE (`SetViewInside`) and the damage swaps RELOAD segment
meshes; an untagged reload disengages the whole pipeline — solid opaque black, no bias, buried on
any slope ("shadow broke mid-session after pressing V", user-confirmed fixed). The all-shadow-
material detector misses the mech proxy (plain black material, not shadow_mtl). [T2]
Polar maps have STUB `*_tshd` (shadow authored-off on snow). The visual-ground conform (`btvisgnd.cpp`)
stays render-only (never touches localOrigin). [T2]
**"Shadow vanishes on inclines" now has THREE confirmed/suspected causes — diagnose before fixing:**
(a) the V-toggle/reload UNTAG above (solid black + buried; broke mid-session; the likeliest cause of
the recurring reports — a relaunch always "fixed" it because the fresh build re-tagged); (b) gait
desync popping the displayed root pose past the bias margin (the first 2026-07-09 recurrence co-
resolved with gait v5, no shadow code changed — though (a) may have contributed) [T2 obs; mechanism
T4]; (c) genuine slope burial when the quad is flat/mis-tilted (the depth-bias margin is finite).
Check `[shadowobj]` tag lines and `[sync]`/`BT_SYNC_LOG` before touching bias/tilt.
## Key Relationships
- Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets).
- Feeds: [[combat-damage]] (collision→damage), [[rendering]] (shadow/visual-conform).