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>
User report: the shadow broke MID-SESSION -- suddenly solid opaque black and
buried on any slope. The session log pinned the trigger: pressing V. The
view toggle (SetViewInside) RELOADS every segment mesh through the object
loader and swaps them into the render tree -- including blh_tshd.bgf -- and
the reload path never called SetIsShadow(1) (only the original tree-build
call site tagged it). Untagged, the entire shadow pipeline disengages: no
translucent TFACTOR state block (solid black), no depth bias (buried), wrong
pass. The damage-swap reloads had the same hole.
Fix: tag *tshd* filenames in LoadObjectBGF itself, next to the existing
all-shadow-material detector (which misses the mech proxy -- plain black
material, not shadow_mtl). Every load path -- build, damage swap, view
toggle -- now yields a tagged object; the drawOps alphaTest routing reads
the flag downstream. User-verified live: the shadow survives repeated V
toggles, translucent and slope-correct.
This also likely explains the RECURRING "shadow vanishes on inclines"
reports: any V toggle silently broke it; every relaunch "fixed" it. KB
(locomotion.md): loader-level tag recorded as co-dependent part 5 of the
shadow arrangement; the vanish-diagnostic note now lists all three causes
(reload untag / gait desync / genuine burial) with check-first guidance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: the ground shadow painted OVER the mech's feet. Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.
Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):
- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
jointshadow ("apply terrain angle to pitch and roll") -- 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 the prior tilt attempts "dug into the hillside"; the engine conversion
is convention-proof. ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
lift varies across a slope; world->local rotation by the TRUE yaw from
localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
class). BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
decal order. The mech, drawn after, z-passes over the shadow: the bias
can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
BT_SHADOW_TILT=0 flat fallback -> the old -0.004. BT_SHADOW_BIAS overrides.
KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shadow report was a desync symptom, not a bias/tilt bug: the ~2-4 unit
depth-bias margin is tight enough that v4's out-of-phase leg pose showing
through could push the flat quad under a slope. No shadow code changed;
both symptoms cleared together. Noted as a diagnostic hint (check
[sync]/BT_SYNC_LOG before touching bias/tilt for this report class).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-reported recurrence of "legs stutter and lose sync". Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking. Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:
- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
frames; a demand change landing between them picks DIFFERENT next states
(one channel winds down, the other keeps walking) -> opposite-phase churn
(observed bs=6 ls=7 -- body on walk-R, leg on walk-L).
Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.
DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
- master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
-> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
(writing last) the displayed pose;
- IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
the BODY is the dead-reckoning PROJECTOR -- locally invisible.
v4's reading of FUN_004ab430 as the travel source was wrong.
FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split. Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).
Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes. KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry). New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>