Files
BT411/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

11 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
locomotion Locomotion — gait, the ground model, collision, shadow established PROGRESS_LOG.md §7, §10 (gait/ground/collision/shadow); docs/P3_LOCOMOTION.md
asset-formats
decomp-reference
reconstruction-gotchas
combat-damage
rendering
RootTranslation
ground-model
SequenceController
MechControlsMapper
SLD
joint
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. GetCurrentCollisionsProcessCollisionListMech::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 GetMessages 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