Commit Graph
24 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Fable 5 14ff3519ac #83: the COLLISION DIVERT -- type-0 damage prices as internal rattle, never armor
The crash self-damage reconstruction (9c83a46) exposed its missing second
half: the binary's TakeDamage hub DIVERTS damageType 0 (@0x4a0368) into
FUN_0049ffcc -- an export-gap distributor recovered by raw disasm -- so
collision damage NEVER reaches the zone/armor loop.  Without the divert the
mover's raw kinetic-energy figure (60-ton mech: ~1000+ per wall tap, 90k+ on
a hard slam) fell into the WEAPON loop: 'tapped a wall and died instantly'.

The distributor, byte-anchored: gate on the owning player's advancedDamage
copy (+0x268 -- the manual's 'splash/collision damage' technician setting);
scale = (2000/moverMass) / (100 km/h in u/s)^2 / (1 - elasticity^2)
(tbyte 1/3.6 @0x4a0148; mass @+0x20c; elasticity @+0x244); scaled < 0.5 is
FREE; else Round(2x) sub-hits of 0.5 land on random INTERNAL subsystems
(HeatSink family / Gyroscope / Torso, drawn by collisionCriticalHitWeight
@0x10C) via ApplyDamageAndMeasure (@0x4ac07c).  Bridges: family derivations
(heat/gyro/torso.cpp), BTPlayerAdvancedDamageOn (btplayer.cpp).

Verified: bhk1 wall test -- 94k slam = 4.2 rattle pts in 8 sub-hits, taps
free (0.049 < 0.5), armor untouched.  A/B: arena wall-grind now 0 deaths,
27 rattle events, the limper STAYS GIMPED and peers' replicants re-enter the
gimp cycle between bounded staggers; grass circling unchanged (198 replicant
gimp entries).  [crashdmg]/[colldmg] probes document the pricing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:04:24 -05:00
Joe DiPrimaandClaude Fable 5 9c83a468db #82 ROOT FIX: reconstruct the crash SELF-DAMAGE -- wall-grinding now hurts,
the knockdown storm self-limits, peers keep the limp

The 'peers see a limping mech skating' report decomposed into a wall-grind
KNOCKDOWN STORM: a gimped mech held against a blocking solid re-crashes each
time its gg cycle rebuilds speed (the gg ceiling is authentically the clip's
natural speed -- bhk1 ~14.9 u/s, ceiling+divisor @0x544/0x548, loader formula
byte-matched -- so iv2 ~222 >> the 40.0 threshold @0x4ab184), and every
type-5 KNOCKDOWN broadcast floors all peers' replicants into knockdown/trn
churn: gliding + leg-lifts = skating.  Turning and crushable cars were
A/B-exonerated (grass circling: 198 replicant gimp entries, 17 crunches,
zero broadcasts).

What the binary has that the port lacked -- recovered from the EXPORT GAP by
raw disasm (scratchpad/night6/gap_4a9770.txt, @4aa89f-4aaab4) -- is the crash
branch's tail: fallDirection = worldToLocal(damageForce) (FUN_0040879c, M^T v,
un-normalized), fallScalar = -(fallDirection . localVelocity.linearMotion),
and a self-dispatched TakeDamageMessage{0x64, zone=-1, the engine-computed
collision Damage verbatim}.  A wall crash COSTS ARMOR, so the grind degrades
the mech instead of looping forever.  That dispatch was the long-standing
'DEFERRED' note in the response policy; now reconstructed.

Falsified en route (comments + KB corrected, do not revive): the stagger's
FUN_004a4c54(1)/(0x20) 'action-request flags' feed NO drive suppressor --
image-wide sweep + full gap disasm show word[this+0x18] is read only by the
net record emitter; the bmp clip applies NO root motion (adv=0 measured); the
'gimp cap 5.8' figure was the BACK-cycle stride printed under a misleading
label (now ggCapL/R; @0x52c has no binary consumer).

Verified (mp_gimpAB rigs): arena1 wall-grind now 4 bounded strikes, limper
dies of its crashes and respawns cleanly twice (START->RESET, no strand);
grass circling unchanged-clean (198 replicant gimp states, 0 knocks).
Rig: pid capture race fixed (a missed winpid left a stale node holding the
-net port -> null runs), per-config sweep added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:18:28 -05:00
Joe DiPrimaandClaude Fable 5 19fe1e5fa2 KB: the wall-grind knockdown storm IS the surviving #82 skating -- A/B-isolated
Four-config matrix (mp_gimpAB.sh, [animind] replicant-state verdict):
grass straight/circle = clean replicant limp (198 gimp entries; 17 car
crunches during it -- crushable path broadcasts nothing, cars exonerated);
arena1 straight = one wall strike, replicant recovers into the limp;
arena1 circle = wall GRIND -- the bmp stagger clip's ~6.5 u/s root motion
re-strikes the wall (6.5^2 > iv2 threshold 40), each rebind re-broadcasts
the type-5 knockdown (26x in the user's run), and every peer's replicant
lives in knockdown/recovery churn: gliding + trn leg-lifts = "skating".
Turning while gimped is innocent; single strikes are benign.

Authentic fix filed in open-questions: reconstruct the consumer of the
stagger's action-request flags (FUN_004a4c54 bits 1/0x20 -- the drive
suppressor while the bmp clip plays); the rebind guard at mech4.cpp:6861
is already marked bring-up-pending exactly that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:41:56 -05:00
Joe DiPrimaandClaude Fable 5 e1c15eb806 #82 peers see a limping mech SKATING: the mech erased its own gimp cell every frame
The binary's one mech+0x40 IS Simulation::simulationState, which rides EVERY
update record header -- so in 1995 a peer's replicant learned the gimp level on
every packet and its gait limped with no gimp-specific replication anywhere.
The port's Mech::PerformAndWatch wrote SetMovementMode(1) every frame ("ground,
non-death, non-airborne"), which erased the level once it was mirrored in for
the warning voice (#78): the wire carried 1, bystanders walked while sliding at
limp speed, and the voice sequence restarted on every damage event (1->4 edge
per tick) instead of announcing once.

- mech4: that per-frame write now writes the AUTHORITATIVE level
  (gimped ? 3/4 : 1) via a new alarm-only bridge BTMechGimpAlarmLevel -- which
  deliberately never consults the cell it feeds, so a respawn-cleared alarm
  cannot re-latch stale gimp out of it.
- BTMechGimpLevel: falls back to the replicated cell, with ONE-CELL precedence
  (a fall/death/limbo state wins, so the normal drivers and their death latch
  run -- what the binary's single cell enforced structurally).
- Reverted the #78 record guard: on a replicant the master's records are
  authoritative, so pinning 3/4 against them would keep a peer limping through
  a respawn.  Fixed at the writer instead.
- [simstomp] trap now scoped to the watched mech with a module-relative return
  address (symcrash-able) -- that is what named the writer.
- Also learned + recorded: zone damage levels replicate only when the EXPLOSION
  TABLE's tier is crossed (peer measured at 0.428 vs master 0.857), so peer-side
  damage state must never be inferred from them.

Verified two-node (scratchpad/night6/mp_skate.sh): the observer's replicant gets
sim=4 and its gait runs 23 -> 25 (wgr entry -> ggl limp cycle).  Field clue that
cracked it: "after respawning a peer DID see the limp" (epilectrik/SAURON).
KB: locomotion.md + gotcha #25.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 22:29:03 -05:00
Joe DiPrimaandClaude Fable 5 18766a167b locomotion.md: correct the gimp-audio verdict -- the reverse-disabled voice is real and wired
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:23:22 -05:00
Joe DiPrimaandClaude Fable 5 7838df2924 locomotion.md: the gimp audio verdict (servo loop, no voice line) + rebind watch item
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 17:41:33 -05:00
Joe DiPrimaandClaude Fable 5 0fd33531a4 locomotion.md: record the gimped turn-step relocation find
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:45:51 -05:00
Joe DiPrimaandClaude Fable 5 36f68718c2 the visible limp (#78): there was never a jump-jet clip set -- the 'Airborne' drivers ARE the gimp gait machines
The port had FUN_004a5bf8/71f4 fully reconstructed as AdvanceBody/Leg-
AnimationAirborne, gated on (MovementMode()==3||4) && jumpCapable@0x580 and
believed dead ("the test mech never jumps").  The binary says otherwise:
mech+0x40 in that gate is the graphicAlarm LEVEL (3=left-leg gimp, 4=right)
and +0x580 is hasGimpClips, set by the conditional loader block that probes
'wgl' and fills clip slots 22-27 (wgl/wgr/ggr/ggl/gsl/gsr) plus the four
measurements at 0x53c-0x548 (wg entry strides = speed caps, gg cycle strides).
Renamed the five jump* members + both drivers accordingly.

New: GimpBodyClipFinished @004a6344 / GimpLegClipFinished @004a7970 -- the
gimp transition machines, branched from the normal finished-callbacks.  Phase-
correct limp entry (left-gimp enters 0x16/wgl only from a RIGHT step, right-
gimp 0x17/wgr from a LEFT step), gg cycles at gimp cadence, gs exits, and the
demand clamp to the gimped side's speed cap (leg cb writes it back into the
mapper -- the binary's authentic slowdown; the T3 x0.5 stand-in in mechmppr is
retired, BT_GIMP_SPEED now defaults 1.0).  The binary's gimp machines have no
reverse entry -- the "reverse disabled" behavior is now binary-proven.

Reviving the dead drivers replayed two port-glue bugs (gotcha #24): the raw
*(controlSource) mapper read (null -> crash at first engagement) and the
missing alarm->member state re-sync (machine pinned in one run state).  Both
fixed; bench harness gained BT_SELF_DAMAGE_TICKS=<n> to hold a zone past
LegHalfStructure without destroying it.

Bench-verified (madcat, novice, zone 16): crossing -> alarm 4 -> wgr entry
from a left step -> 8k+ frames stable in the ggl limp cycle at cadence 14.77
(vs 18.5 walk / 22+ run) with raw demand still 50.  [T2]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 15:49:39 -05:00
Joe DiPrimaandClaude Fable 5 520f6eecd3 myomer damage reaches the wheels, legs gimp at half structure, reverse refuses -- and an ODR trap unmasked (#75/#78)
The speed-demand site (MechControlsMapper::InterpretControls) now applies the
drive scale the mover's feed roster applied in 1995: speedDemand multiplies by
the myomers' live speedEffect (gear ratio, thermal curve, 1 - zone damage, via
the BTMyomersDriveOf bridge) and, while GIMPED, by 0.5 [T3: VGL Lynx's
"roughly 50%", BT_GIMP_SPEED overrides]. The gimp states were already being
raised -- mechdmg sets graphicAlarm 3 (left) / 4 (right) when a LegDamageZone-
flagged zone crosses half structure -- but nothing downstream ever saw them.
While gimped, reverse input is refused ("reverse disabled"), matching the
old-timers' account; the pod's audio cue rides the alarm's watchers.

Bench, one trajectory, arithmetically exact: a deterministic right-leg ramp
(the new BT_SELF_DAMAGE_ZONE harness) crosses 0.5 and the demand goes
44.837 -> 6.726 = 44.837 x 0.5 (gimp) x 0.3 (a crit-chewed myomers from the
same ramp -- the #80 crits composing with #75's scale, unprompted).

The reason "nothing downstream ever saw them" is the real find of the night,
now gotcha #23: AlarmIndicator is typedef'd to DIFFERENT TYPES per header
family -- mech.hpp says ReconAlarm (4 bytes), heat.hpp says GaugeAlarm (0x54)
-- so Mech::graphicAlarm and EVERY member after it sit at different offsets
depending on a TU's include order. mechdmg wrote level 4 and read it back;
mechmppr read 0 from the same object, same expression. No compiler error can
catch it: each TU only ever sees one definition. Until the split is audited,
cross-TU reads of the gimp level go through BTMechGimpLevel (compiled in
mechdmg's TU) -- and the same split-brain explains why the port carries the
binary's ONE movementMode cell as two live members (engine simulationState vs
graphicAlarm level) that never meet.

Also landed en route: the Myomers un-powered self-repair observed healing in
the field logs at exactly 0.011 x the authored Explosive scale per tick --
the 2026-07-29 reversal confirmed live; and Mech message 0x15 "RealMaxSpeed"
raw-decoded (@0x49f604: sets mech+0x7a0 from the message unless the +0x7a4
latch holds -- a console-tunable top speed).

Open on #78, documented in locomotion.md: the Gimp animation clips (authored
keys in the binary's model-record parser; mech2's state enum vs mech3's
reverse-fix disagree about slots 0x12-0x17 -- reconcile before wiring) and
the audio-cue binding.

Diags: BT_SELF_DAMAGE_ZONE, BT_DRIVE_LOG, BT_GIMP_SPEED.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:29:36 -05:00
arcattackandClaude Fable 5 6ad1e074cc Glass lever: port the pod-build GAIT DETENT to the PadRIO throttle slew
Human re-test on the fresh build: zero-stop OK, but the lever could PARK inside
the walk/run hysteresis band (walkStrideLength@0x534 .. reverseSpeedMax@0x538)
where the gait SM has no stable state -- walk<->run hunt, EngineShiftFwd/Rev
retriggering ("mixed walk/run mode + repeated acceleration sample").  The
pod-build keyboard lever already snaps out of the band to the NEARER edge at
key-rest (mech4.cpp GAIT DETENT); the PadRIO slew channel lacked it.

Faithful port, not an invention: mech4.cpp publishes the band in lever units
every driven frame (gBTGaitDetentLo/Hi seam, same guards + the same +0.002
engage margin); L4PADRIO Poll() applies the identical nearer-edge snap to the
Throttle channel when the slew rests (no slew key held, no pad slew axis
deflected).  Sweeping through the band while held stays continuous (the
authentic moving lever, one authentic shift).

Verified (DEV.EGG glass, graduated parks each ~0.10 of the sweep,
BT_GAIT_TRACE+BT_MPPR_TRACE): band [0.358026,0.50388) = demand 22.02-30.87;
in-band parks snap both directions (0.4137->lo, 0.465126->hi); no at-rest
sample in the band; exactly ONE walk->run clip (state 10) up and ONE run->walk
(state 14) down across the whole sweep; zero-stop unaffected; pod build
rebuilt + BT_AUTODRIVE smoke clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:57:34 -05:00
arcattackandClaude Opus 4.8 aa004eb7b8 Gait: keyboard detent snaps the lever out of the walk/run DEAD BAND + KB
Byte-decoded finding [T1]: the gait SM has no stable state for a demand in
(walkStrideLength@0x534, reverseSpeedMax@0x538).  Cap semantics settled from
LoadLocomotionClips @0x4a80d4: 0x538 is the walk->run TRANSITION CLIP's exit
speed (the run ENGAGE threshold -- not a reverse max) and 0x34c (run-cycle avg
root speed) is the mapper's continuous demand multiplier (FUN_004afd10).  The
finished-callbacks up-shift when tgt > 0x534 but enter/sustain run only when
tgt >= 0x538, so a demand parked between them (Blackhawk: 22.02-30.87 =
throttle 36-50%) hunts walk -> shift-up -> shift-down forever, firing the
authored EngineShiftFwd/Rev sounds each swing -- the user's repro, faithfully
reproduced by all-authentic logic + data.  Likely masked on the pod by
MECHANICAL throttle-quadrant detents ("5 speeds" lore; the software path is
notch-free) [T4] -- added to get-from-Nick.

Accommodation (keyboard = our stand-in lever): at key REST, snap the lever to
the nearer dead-band edge (walk cap, or run engage + margin since the cont
check is >=).  Sweeping THROUGH the band while held stays continuous -- an
authentic moving lever; the single shift it fires is the authentic shift.
BT_GAIT_TRACE logs [gaitdetent] snaps.

KB: locomotion.md gait-dead-band section, pod-hardware.md mechanical-notch
hypothesis, open-questions.md Nick item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:34:18 -05:00
arcattackandClaude Opus 4.8 82e6bb306a KB: sweep STALE "unfinished" claims -> match landed commits (context-system audit)
4-agent audit cross-checked every open/pending/stub/deferred claim in the context
system against git + code. Fixed ~13 stale spots (currency only, no code change),
each contradicted by its own file body + a landed commit:

- 0xBD3 SubsystemMessageManager (task #7, afefaee): subsystems.md frontmatter+body
  line 57; decomp-reference.md 0xBD3 row + +0x434 row -- all still said
  "MISLABELS controlsMapper / WAVE 8 untangle" (done; mech.hpp names it messageManager).
- Cylinder hit-location STEP 6 (d07ac7d): combat-damage.md frontmatter;
  decomp-reference.md "not reconstructed" line.
- Per-mech turn-rate wiring (task #64b, a83995a): multiplayer.md + locomotion.md
  frontmatter (still "bring-up constant / not yet wired").
- Mech-level update records (task #1, c9f0c2a): open-questions.md "dead Wword
  branches mech.cpp:1511/1613" (those branches are gone; named fields now).
- Collision damage + DeathShutdown (2026-07-08/12): combat-damage.md frontmatter.
- Myomers un-stub (8b36440): decomp-reference.md "INERT un-stub" row.
- Day/night BT_MATPRI (shipped, default-on): rendering.md frontmatter (LOD half kept).
- Fire-trigger path (task #5, 8ed6184): open-questions.md "still gBTWeaponTrigger
  bring-up" (globals retired/never-read).
- HEAT entry FACTUAL fix: "heatLoad -> effectiveRange 0 = overheat cutout" was false
  per aab7a8a -- effectiveRange reads host-zone damage; the real cutout is the
  FailureHeat charge-hold.

Frontmatter open_questions re-pointed at genuinely-open items. VERIFIED NOT stale
(left as-is): multiplayer body-channel-projection notes (a real open [T3] residual
per the coupled-motion section's ~2.9u leg-vs-body snap). Genuinely-open items
(factory roster loops 2-4, subsystem-panel gate, missile cluster, console-death
freeze, gyro tails, etc.) untouched. checkctx CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:35:41 -05:00
arcattackandClaude Fable 5 a83995a755 Locomotion: wire the AUTHENTIC per-mech turn-rate lerp (task #64b)
The master-perf disasm (0x4aa3d3-0x4aa4ff) computes the yaw rate as
lerp(walkingTurnRate@0x574, runningTurnRate@0x578) by ground speed, not a
constant. Wired it into the drive, replacing the bring-up constant kDriveTurnRate:

- walkingTurnRate/runningTurnRate are now REAL named members. The ctor's
  `Wword(0x15d)/(0x15e) = model->...TurnRate * DegreesToRadians` writes were
  NO-OPS -- Wword() returns a shared static scratch cell, so the rates were
  silently discarded. Now stored + read by name.
- The drive (mech4.cpp) computes authTurnRate: base = walkingTurnRate; above
  reverseSpeedMax(0x538) lerp walk->run across [walkStride 0x534 .. topSpeed
  0x34c] with an over-run falloff runningTurnRate/t^2; clamp >=0; zeroed in the
  death/limbo/airborne leg states (1/2/3) or when deathAnimationLatched(0x650) is
  set. Falls back to kDriveTurnRate if the model gave no rates.

The lerp is a RUN-speed refinement -- below reverseSpeedMax (walk / turn-in-place)
the rate is the constant walkTR base; runningTurnRate < walkingTurnRate (a running
mech is less maneuverable). Verified headless (DEV mech, BT_TURN_LOG): walkTR
1.309 rad/s (75 deg/s), runTR 0.873 (50 deg/s), turn-in-place + walk yaw at walkTR,
no crash. [T1 logic / T2 runtime]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 05:48:51 -05:00
arcattackandClaude Fable 5 35c30bda67 Locomotion: replace task-#64 trn stand-ins with the AUTHENTIC dispatcher (disasm-decoded)
The turn-in-place dispatcher lives in the master-perf gap 0x4a9b5c-0x4ab188 that
Ghidra never lifted. Disassembled it with objdump (scratchpad/masterperf.asm) and
hand-decoded the real logic, replacing the two [T3] stand-ins I had invented:

- Entry gate was `commandedSpeed < 0.25*standSpeed` (a near-zero guess). AUTHENTIC
  (0x4aa505): arm trn when TURNING (|angVel|>1e-4, port proxy = turnDemand deadband)
  AND speed in the FULL [0, standSpeed] sub-walk range AND turnCapable. trn is gated
  on TURNING, not on being slow.
- Case-4 exit had a ×4 fast-forward + SetLegAnimation(5) invention. Restored the
  VERBATIM decompiled leg-SM exits (part_012.c:12013): standSpeed<spd or reverse ->
  Standing, else advance the pivot. Added the master-perf turn-STOP exit (0x4aa5c6):
  turn stopped -> Standing + legResetLatch=1 (folded into case 4 since the port has
  no separate master-perf frame).
- Body case-4 twin mirrors the authentic exits (mj=0, tracks state for lockstep).

The fast-forward was invented to dodge a turn->walk stutter that was ACTUALLY the
body channel leaking joints (mj=1) -- already root-caused + fixed by mj=0. The
trn->walk seam (legFrm7 cycle-end -> swr legFrm1) is the authentic pose-matched
boundary and the same seam the fast-forward version hit, just at the correct release
speed. Also decoded (documented, not yet wired) the authentic turn-RATE lerp
(walkingTurnRate@0x574 -> runningTurnRate@0x578 by speed; angVel = turnDemand*rate);
the bring-up drive still yaws at the constant kDriveTurnRate.

Headless-verified (BT_AUTODRIVE/BT_FORCE_TURN/BT_WALK_DELAY/BT_GAIT_TRACE): turn-in-
place enters state 4, ramp past standSpeed -> 4->stand->swr->walk, both channels in
lockstep. Feel re-verification pending. [T1 logic / T2 runtime]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:31:56 -05:00
arcattackandClaude Opus 4.8 bfdd41bb9d Gait: turn-then-walk stutter ROOT-CAUSED + FIXED -- one skeleton writer (task #64)
User repro (100%): turn in place, push forward before the turn stops -> the gait
skips/stutters + visibly reduced bob; standstill starts always clean.  Three-layer
fix, user-verified:

1) THE DISPLAY BUG (the actual visible artifact): v5's "body advances first with
mj=1, leg overwrites last, so body drift can't show" was FALSE.  Whenever the two
gait channels phase-split, the BODY channel's out-of-phase joint writes leaked
into the rendered skeleton (rhythmic leg skips, averaged-down bob) while every
leg-channel trace read clean -- the leg DATA was fine, the RENDERED pose wasn't
the leg's.  v6: AdvanceBodyAnimation(dt, mj=0) -- the body still advances +
projects for replication (records/cycle speeds unchanged) but no longer touches
the skeleton.  One writer, structurally; matches the binary's own observable
("body phase drift is locally INVISIBLE in the binary").  BT_BODY_MJ=1 = old A/B.

2) THE SPLIT SEED: the bring-up trn trigger armed only the LEG channel, so a
turn-in-place entry guaranteed the channels re-entered walk frames apart and the
walk cycles ran permanently out of phase.  Lockstep: the Standing trn entry arms
BOTH channels the same frame (body case-4 twin added, same rate/keying), gated on
both Standing.  The authentic dispatcher (un-decompiled master-perf gap 0x4a9b5c-
0x4ab188, the sole reader of turnDemand/turnCapable) armed both -- the body's
case-4 machinery is dead code otherwise [T1].

3) THE POSE-MATCH INVARIANT: the engine has NO pose blending; transitions avoid
pops purely by authored pose-matched boundaries.  The old trn exits cut the pivot
clip MID-STEP (legFrm 7->1 teleport).  Now: entry gated on near-zero speed
(turn-IN-place); on forward command the pivot FAST-FORWARDS to completion (x4)
and the authentic finish callback lands Standing at the stand pose -> the normal
pose-matched stand->walk runs.  Decompiled reverse abort kept; the standSpeed
mid-clip abort subsumed (it WAS the pose cut).  [T3: 0.25*standSpeed threshold +
4x rate stand in for the gap's constants.]

Harness: BT_FORCE_TURN now reaches gBTDrive.turn (was silently inert for the
gait), BT_WALK_DELAY=<s> holds forced throttle then ramps (the turn-first repro),
BT_GAIT_TRACE=1 per-frame gait trace.  Regressions: standstill start, turn-entry,
pure pivot loops, run cycle -- all clean, bob full amplitude (1.33/1.32).
KB: locomotion.md v6 section + trn reconstruction + symptom-family closure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:52:35 -05:00
arcattackandClaude Fable 5 2e9c78d604 Gait: the AUTHENTIC walk lean -- interior/exterior clip sets, level cockpit + leaning mechs (task #59)
The MadCat (and every leg-mech) leans -8deg into a walk / -11deg into a run --
an authored jointhip (hingex) pose in the EXTERIOR gait clips that the walk/run
CYCLE clips never rebind, so it HOLDS.  The 1995 game keeps a separate INTERIOR
('i'-suffix) clip set for the mech you PILOT: those shake jointshakey (cockpit
rattle) and OMIT jointhip, so your own view stays level while everyone else sees
you lean.  The port had this entirely dead -- the local cockpit mech pitched
-8deg into the ground (user-reported "staring at the ground").

ROOT CAUSE (two stacked bugs):
- The authentic ctor clip-set gate (@part_012.c:10308-10320) was reconstructed
  as no-op stubs: LoadLowDetailBody/LoadHighDetailBody MISLABELED the
  FUN_004a80d4/86c8 GAIT-CLIP loader addresses as a "body LOD" pair, so the gate
  did nothing and a separate unconditional LoadLocomotionClips always loaded
  EXTERIOR clips.
- LoadLocomotionClipsExt (the interior loader) was a stub aliased to the
  exterior loader.

FIX:
- Reconstructed the real 4-char INTERIOR loader (byte-exact vs @004a86c8; the
  'i'-suffix table dumped from the exe @0x10d74d: swri/wwri/wwli/...; all 18
  base clips confirmed present in BTL4.RES for mad/blh/ava).
- Fixed the ctor gate: getenv("L4VIEWEXT") || (instanceFlags&0xC)==4 -> exterior
  (replicant / forced external view); else -> interior (local cockpit master).
  Removed the mislabeling no-op stubs.
- PORT ADAPTATION (MaintainViewClipSet, per-frame in PerformAndWatch): the port
  never sets the replicant COPY bit (all MP mechs build as local masters --
  heat-sim/scoring/torso-watcher-connect all run on both, by design), so the
  ctor gate lands everyone on interior.  Pick the set by VIEWPOINT instead: the
  mech you pilot keeps interior (level cockpit), every other mech flips once to
  exterior (the lean).  Reloads only on a viewpoint-status change; the sets
  share stride data so the swap is seamless.  Model pointers stashed at ctor.

VERIFIED live (BT_HIP_LOG probe): your walking mech writes jointhip=0 (level);
the peer's replica in the other pod's view leans at exactly -8.0/-11.1deg --
the authored clip values.  A/B control: L4VIEWEXT=1 forces the walking master
back to 74 sustained -8/-11 writes.

KB: locomotion.md ("NO walk lean" audit conclusion CORRECTED -- the lean is
authored + sustained + clip-set-specific), asset-formats.md (.ANI parses
pitch/yaw/roll for all joint types; KeyJointPos translation; cycle clips carry
no pitch), open-questions.md (item closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:46:22 -05:00
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
arcattackandClaude Fable 5 4554ea543a KB: locomotion turn-rate note -- authored rates stream correctly post task #4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:36:39 -05:00
arcattackandClaude Fable 5 3c34ae6de6 KB: full staleness audit + sweep -- 35 verified corrections across 16 topics, 15 new glossary terms
Adversarially-verified audit (18 agents) against the task #46-#56 landings:
MP/current-state understatements, superseded punch readings, retired env
gates, dead paths, line-cite drift; env-gate hub table completed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:56:51 -05:00
arcattackandClaude Fable 5 47aede3094 Shadow: tag tshd proxies in the LOADER (V toggle broke the pipeline)
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>
2026-07-09 17:10:33 -05:00
arcattackandClaude Fable 5 b6fe686b22 Shadow: authentic terrain tilt + decal draw order (feet layer correctly)
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>
2026-07-09 16:59:48 -05:00
arcattackandClaude Fable 5 099cad998d KB: incline shadow-vanish co-resolved with gait v5 (user-confirmed live)
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>
2026-07-09 16:34:56 -05:00
arcattackandClaude Fable 5 d4e139a0d9 Gait v5: display+travel from the LEG channel (kills the stutter class)
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>
2026-07-09 16:25:49 -05:00
arcattackandClaude Opus 4.8 f914fc040a context-system: complete migration -> CLAUDE.md is now a 160-line router (zero context lost)
Full migration of the 2236-line monolithic CLAUDE.md into the progressive-context
knowledge graph (per spark-lesson / expert-seed.md), so the deep RE knowledge loads
on-demand instead of every session.

ZERO CONTEXT LOST:
- docs/PROGRESS_LOG.md = the complete old CLAUDE.md, VERBATIM (byte-identical) -- the
  lossless safety net + the "full detail" quick-lookup fallback.
- 18 context/*.md topic files (1343 lines) digest every section (§1-3 -> project-overview,
  §4 -> content-archives, §5 -> asset-formats/bgf-format, §5a -> source-completeness,
  §5b/§8 -> wintesla-port, §7/§10 -> locomotion, §10a -> build-and-run, §10b ->
  reconstruction-method, §10c -> combat-damage + reconstruction-gotchas, §10d -> subsystems,
  render notes -> rendering, gauges -> gauges-hud, MP -> multiplayer, §9 -> open-questions).
- reference/glossary.yaml (53 terms). decomp-reference.md = the offsets/ClassIDs/addresses hub.

CLAUDE.md (160 lines) = router: identity, answer/reason protocols, quick-lookup table,
evidence tiers (T0 engine-truth / T1 decompiled+verified / T2 reconstructed+runtime /
T3 guarded / T4 hypothesis), conventions + DO-NOT (the systemic bug classes), structure.
Retains the load-bearing work directives (build recipe pointer, "keep current" mandate).

Knowledge graph validates CLEAN (scratchpad/checkctx.py -- all [[links]] + quick-lookup +
docs refs resolve; [[name]] -> topic file or glossary term). docs/*.md ledgers stay as the
detailed logs; context/*.md are the curated digests that route into them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:19:50 -05:00