Commit Graph
122 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 ebdfa40d95 HUD (task #68 follow-up): authentic reticle pip GROUPS -- front vs rear
The binary reticle registration resolves each weapon's RearFiring attribute
and registers its pip in group 1 (front) or 2 (rear) (part_014.c:5429-5434)
[T1]; the reticle draws the group selected by the mech's reticleElementMask
low bits (= binary mech+0x390, driven by the weapon-update view switch
part_013.c:5588-5595: forward |1&~2, look-back |2&~1).

Port: BTBuildReticle passes the real group (was hardcoded 1 on the disproven
"no BLH weapon is rear" belief); BTCommitLookState drives the mech's
reticleElementMask low bits per view (mech4's HUD tick already publishes
them as gBTHudGroupMask, and the reticle Draw already filters on it).

Resolves the user-reported "only 2 ERM dots but the mech has 3": ERMLaser_1
(front) and ERMLaser_2 (rear) both author pip position 1 -- with every pip
drawn in one group the two red dots sat exactly on top of each other.  Now:
forward view = 5 pips (2 PPC + ERM_1 + 2 SRM), look-back = the 2 rear
lasers' own group layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:45:24 -05:00
arcattackandClaude Opus 4.8 38febae36b Rear-fire + look views (task #68): the pod's rear arsenal reconstructed
The user's tip ("some mechs actually do fire backward") checks out end to
end.  Binary ground truth [T1]:
- weapon+0x334 (attr 0x1B "RearFiring"): the ctor tests the MOUNT SEGMENT's
  page name for the marker 'b' (@0x511aa2, part_013.c:6913-6930) -- the back
  gun ports sitelbgunport/siterbgunport.  The old reading ("EXT in the
  weapon model name") was a double misread (wrong string, wrong name).
- mech+0x410 (attr id 50 "RearFiring"; the old `stateFlags` label): the ctor
  ORs every weapon's flag (part_012.c:10220) -- "carries a rear arsenal".
- The mapper's five-state LOOK machine (part_013.c:396-459): on a state
  change it re-aims the eyepoint (mech+0x360 = EyepointRotation -- consumed
  by DPLEyeRenderable, already live in the port) and re-arms each weapon's
  viewFireEnable(+0x3E0): FORWARD view = the non-rear weapons, LOOK-BACK
  (yaw pi + lookBackAngle pitch) = the REAR-mounted ones, side/down = none.
  +0x3E0 is the same flag the emitter's Loaded->Firing gate reads (the old
  `useConfiguredPip` label) -- pips and fire permission both follow the view.

Port: rearFiring derived from the mount segment name (BTWeaponMountIsRear);
mech rearFiring ORed in the roster pass (the old SubProxy::IsDerivedFrom
stub returned 0 -- that loop never ran; now bridged through
BTWeaponIsRearFiring, which also fixes the weaponRoster fill); the look
commit is LIVE (BTCommitLookState: eyepoint EulerAngles from the authored
per-mech look angles -- now real members, were Wword scratch parks -- +
per-weapon view enables); MissileLauncher/ProjectileWeapon FireWeapon gate
on viewFireEnable like the emitter; RearFiring attrs (mech + weapon) bind
real members.  Keyboard: HOLD 'V' = the pod's rear-view button.

Live-verified on the default blackhawk: ERMLaser_2 -> siterbgunport rear=1,
ERMLaser_3 -> sitelbgunport rear=1, PPCs/SRMs/torso mounts forward -- the
blackhawk authors TWO REAR LASERS (owens also has back ports).  [rearfire]
trace under BT_PROJ_LOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:10:40 -05:00
arcattackandClaude Opus 4.8 ab6ea83e3b Missiles (task #67): launch through the WEAPON MOUNT frame -- the
backward-firing rack

User report (madcat): missiles sometimes leave the mech BACKWARD; lasers
always fine.  Root cause: only missiles fly the authored MuzzleVelocity
vector, and BTPushProjectile rotated it through the mech BODY basis
(localToWorld) -- but the madcat's racks ride the TORSO.  The muzzle POINT
was segment-resolved (tracked the twist) while the launch DIRECTION followed
the LEGS: twist the torso far enough and rounds left the back.  Lasers/ACs
aim straight at the designated pick (no launch vector) -- unaffected, exactly
as observed.

Binary ground truth [T1]: the fire builder FUN_004bcc60 spawns the missile
with the FULL muzzle-segment frame (the real GetMuzzlePoint fills an
AffineMatrix into the descriptor, :8762-8764), composes the authored MV with
the z-NEGATION (:8759-8761 -- confirming the 2026-07-12 telemetry finding)
onto the mech's own localVelocity (FUN_004b9cbc = owner+0x1c4), and the dumb
seeker re-aims 100u ahead of the MISSILE's own frame each frame (0x4be9a0
disasm) -- the mount orientation IS the launch direction.

Port: BTPushProjectile takes muzzle_seg and rotates the launch vector through
the mount segment's world frame (segment-to-entity x localToWorld, the same
matrix the muzzle point already used), keeping the z-negation convention, and
inherits the shooter's world velocity; body-basis fallback for callers
without a segment.  All four call sites pass GetSegmentIndex().

Bonus decode banked for the full Missile-entity revival (KB-worthy): the
three authentic performances -- Seeker 0x4be9a0 (aim = target-frame offset /
100u-ahead dumb + loft/lead 0x4beae4), Thruster 0x4be474 (quaternion-slerp
BODY TURN toward the aim at turnRate deg/s), MoveAndCollide 0x4bef78
(velocity aerodynamically aligned to thrust via signed-square per-axis gains,
ballistic droop as fuel burns, PROXIMITY FUSE on seeker rangeToTarget,
max-range + altitude-floor retire).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:46:56 -05:00
arcattackandClaude Opus 4.8 c51d64a6b8 Gyro tail + audio (task #66): the INSTABILITY MODEL -- mech+0x3F0 reconstructed
Byte-decoded the un-exported master-perf block @0x4aad3d-0x4aaf14 (capstone;
the same gap that held the F5 footstep code) [T1]:

  instab  = (min(|AccelerationLastFrame| / maxUnstableAcceleration, 1)
             x unstableAccelerationEffect)^2
  instab += clamp((demand - legCycleSpeed)/demand, <=1)      # "gun the engine"
             x unstableGunTheEngineEffect                    # (demand clamped to
                                                             #  [gimpStride(neg), runMax2])
  if (legAnimationState == 4 /*turn-in-place*/) instab += unstableStopedTurnEffect
  clamp to 1  ->  mech+0x3F0  ->  gyro->swayBias (gyro+0x3A8)

Every piece resolves to already-reconstructed structure: the model-record
tuning block rec+0x80..0x94 (ctor copy @0x4a2593 -- the six authored
Unstable* fields, previously parked in the Wword scratch bank, now real
members); AccelerationLastFrame@0x82c = the snapshot of the ring-derived
localAcceleration (copied at the perf tail @0x4ab142); CurrentSpeed@0x348 =
legCycleSpeed; the trn state 4 gate; and the gyro feed lands in the
EXISTING swayBias member + GyroscopeSimulation consumer (task #56) --
GyroFrameJointWrite's "0.0f model TBD" argument is now the live value.

UnstablePercentage (binary id 52 @0x3F0) binds the real member: the LAST
dead audio attribute is live -- the authored instability alarm (start
thresh 0.01 + volume = the fraction) sounds under hard maneuvers, and the
cockpit ambient sway now scales with reckless driving.

Live verification (30s, 0.6-throttle run): instab 0.38 during the walk->run
chase (cyc 22.2 vs demand 33.6), settling to 0.002-0.12 per-stride ripple at
steady run; attribute binds with live float values; [instab] trace under
BT_GYRO_TRACE.  unstableSuperStopEffect/unstableHighVelocityEffect writers
remain unlocated (plausibly the airborne perf variant) -- noted on members.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:51:10 -05:00
arcattackandClaude Opus 4.8 a11a697824 Audio: footsteps arrive on the FIRST stride -- the 10-20 s warm-up bug
User report: footfalls silent for the first 10-20 s of every mission (all
mechs), then solid.  Root cause was three interlocking layers, each measured
with timestamped traces:

1. The authored footstep volume chain (LocalAcceleration [0,10]->ctl100 +
   LocalVelocity [0,0.6]->ctl101 through authored N=30/N=15
   AudioControlSmoothers, fill 0) hangs off the SOURCE's watcher chain
   (scale watches smoother watches mixer watches source), and an idle
   source's chain executes only at Start attempts -- one smoother sample
   per stride.
2. Each hop is frame-gated (AudioComponent::ExecuteWatchers,
   DefaultAudioFrameDelay), so any burst collapses to one execution.
3. The transient drop gate (vol < 0.3, AUDREND) rejected every Start while
   the smoother average crawled up 1/30th per attempt -> ~25 dropped strides
   before the first audible step, then per-frame execution while playing
   kept it warm forever ("solid after that").

Fixes (engine-level, each documented in place):
- AudioScaleOf<T>::Execute now sends EVERY poll (scales are continuous
  value-feeders; the base bitwise change-gate -- Motion::operator== is
  memcmp -- froze on our deterministic gait math, where the original's
  noisy physics floats never bit-repeated.  Triggers/matchers keep the
  change gate: their semantics are edge-based).
- Component/AudioComponent::PrimeWatchers(passes): recursive, GATE-FREE
  watcher pump; AUDREND runs 30 passes on every transient Start request so
  the authored smoothers evaluate at their true steady state before the
  drop gate reads the volume.
- localAcceleration derives via the binary's exact structure: 15-sample
  ring buffers of the raw position derivative + dt (ctor part_012.c:9836,
  derive :15169-15195), in the PerformAndWatch tail so it runs every frame.
- AttributeWatcherOf::GrabCurrentValue private -> protected (the scale
  override calls it).

Verified (30 s walk from cold start): drops 25 -> 3 (the survivors are
authentic quiet-stride gating: first gentle strides at vol ~0.28 vs the 0.3
gate), footfalls deliver from the first stride, 43 delivered with live
per-stride gain variation.  Diag traces added: [accwatch]/[fsscale]/
[smooth]/[smoothcfg]/[motionscalecfg] + timestamps on DROP/volset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:11:17 -05:00
arcattackandClaude Opus 4.8 0e2401fb52 Audio Phase 4d (AUDIO_FIDELITY F5): footStep is the authored CONTACT LEVEL
The binary's per-frame perf (disasm @0x4a9e80-0x4a9eb6 leg / @0x4aba86-
0x4abab9 body) computes footStep@0x394 = (jointlocal.y <= *footStepThreshold)
with the threshold pointer = SequenceController+0x20 = &ANI hdr[2] -- the
header word the engine's own AnimationInstance captures (JMOVER.cpp:1415)
and our SelectSequence skipped.  States 0/1 retain the value.

Port: SelectSequence captures hdr[2]; PerformAndWatch evaluates the contact
level per frame from the cached jointlocal root joint (leg channel drives
the pose); the 150 ms clip-transition pulse + decay are RETIRED (steps fired
at clip boundaries with a fixed width; clips whose root crosses twice or
never counted wrong).

Live verification (30s walk): the authored threshold decodes real
(-0.232 root height); rootY oscillates across it per stride (-0.26 contact /
-0.19 swing) with clean 0->1->0 transitions per leg state (5/6/7); 34
footfall deliveries at stride rate with per-stride varying gains.

Also closes F23(2): the 17 authored AnimationState trigger states fire
empirically -- the EngineShiftFwd/Rev heard during the gait dead-band hunt
WERE states 10/11/14/15; the runtime clip numbering is correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:16:14 -05:00
arcattackandClaude Opus 4.8 9dcb4752de Audio Phase 4c (AUDIO_FIDELITY F17/F19): impact scaling + the authored footstep feed
F17 CollisionSpeed (binary id 24 @0x4B4): real member captured as
|worldLinearVelocity| when the contact accumulator arms (0->1) -- the authored
AttackVolume [0.9,1] / Brightness [0.7,1] scales over impact speed [0,25] now
make harder hits sound louder and brighter.  ReduceButton (id 46 @0x340): real
watchable member (the keyboard rig never presses it).  UnstablePercentage
stays deferred: its sway/overspeed model @0x3F0 is the known gyro-ledger gap
(live writer unexported); binding without the model would be a stand-in.

F19 footstep feed (the invention is dead, long live the authored chain):
new [motionscalecfg/motiontrigcfg] traces recovered the authored configs --
EVERY motion watcher extracts |linearMotion| (motionValue=3); the footstep
volume mixer is fed by LocalAcceleration [0,10] -> ctl100 (per-stride kick)
+ LocalVelocity [0,0.6] -> ctl101 (0.4 base while moving).  The port never
wrote Mover::localAcceleration, so ctl100 read 0 and the old mech2.cpp
step-intensity broadcast (patch-sniffing, invented curve) fought the live
authored scale.  Now: localAcceleration.linear = d(published velocity)/dt --
EXACTLY the binary's derivation ((avgVel - prev)/avgDt into +0x1e4,
part_012.c:15186-15195) -- and the broadcast is REMOVED.

Regression (30s, walk throttle): stable; footfalls deliver through the
wholly-authored chain with per-stride VARYING gains (0.61/0.72/0.62 -- real
step dynamics, impossible under the old constant-curve invention).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:11:36 -05:00
arcattackandClaude Opus 4.8 cc2b109cc8 Audio Phase 4a (AUDIO_FIDELITY F6/F7): coolant-leak warning + missile alarm
F6 ReportLeak (the largest dead authored block -- 38 of 54 match watchers):
recovered the binary HeatSink attribute table (16-byte {id,name,off+1} rows
@0x50e438..0x50e4c8, ids 3..12) [T1] -- it confirms EVERY existing heat
binding (CurrentTemperature@0x114 .. CoolantMassLeakRate@0x130, HeatSink@
0x164) and adds the one row we never published: ReportLeak (id 12) ->
+0x138 = coolantActive, the INT leak hysteresis flag UpdateCoolant
(@004adbf8) drives 1/0 around draw 0.003/0.0025.  PoweredSubsystem derives
from HeatSink, so the single row serves all 19 authored leak watchers
through the chained index, exactly like the binary.  MechWeapon's pinned-id
pad absorbed the +1 chain shift (0x0E -> 0x0F, tripwire fired as designed).

F7 IncomingLock/DistanceToMissile (missile alarm): binary Mech table walked
in full (ids 21..56 [T1] -- also settles FootStep@0x394, CollisionSpeed@
0x4B4, UnstablePercentage@0x3F0, ReduceButton@0x340 for the next findings).
IncomingLock id 54 @0x3fc, DistanceToMissile id 56 @0x400; the old
"maxSpeed @0x400 = FLT_MAX" member was a MISREAD of the far default and is
retired (its 1000.0f "override" was RadarRange id 47 @0x404, already
published).  Real members + accumulators: Missile::MoveAndCollide reports
target + range each tick (BTReportIncomingMissile bridge); PerformAndWatch
latches per frame.  The authored beeper (match 1/0) + range->TEMPO scale
(100..800 -> 600..10, accelerating as the missile closes) read them
unchanged.  Drive is intent-level [T3]; init 0/FLT_MAX matches the binary
reset (part_012.c:9446-9447).

Regression (30s): stable; ReportLeak binds real on every subsystem (0 pad
redirects); DistanceToMissile binds with FLT_MAX live; attrnull 41 -> 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:58:08 -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 537749cae3 KB: audio fidelity Phase 1+2 settled facts -> context/wintesla-port.md
wPreset patch numbering, SF2 v1.0 pitch generators + EMU8000 44100 base rate +
the WAV bake formula, inverted SBK attenuation, full-zone/key-select/loop-
region/release-fade architecture, distance/gain/doppler model, the audio-clock
origin (FUN_0044e19c = ApplicationManager::GetFrameRate; frames were clock
ticks), and the ConfigureActivePress base-attribute resolution.  Annotated the
mech4.cpp velocity smoothing as AUTHENTIC [T1] (binary Mech::Execute AverageOf
filters, part_012.c:15169-15179).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:07:09 -05:00
arcattackandClaude Opus 4.8 c68307ad53 Audio: kill the per-footfall static -- smooth the PUBLISHED velocity (task #50)
User pinpointed it: the per-stride 'static' was NOT the footstep sample -- it
was the ambient engine hum being interrupted in time with the footfalls.
Diagnosis (live tape): EnginePower01 was caught in a rapid Play->Stop cycle
(80x/session).  The idle-hum is gated by authored LocalVelocity triggers (hum
ON in speed [1,20], OFF above 20) -- and the port's localVelocity is a RAW
per-frame position derivative that pulses with every stride (gait acceleration
+ ground-snap bob).  At walking speed |v| straddles the 20 band edge, so the
trigger Start/Stop-flapped the hum on every foot plant; at run |v| >> 20 keeps
it off (matching 'goes away at higher speeds').

Fix: ~0.25s exponential filter on the PUBLISHED localVelocity.linearMotion
(fwd + vertical) -- kills the stride ripple, tracks real speed changes within
a couple strides.  The physics worldLinearVelocity (StaticBounce, collision
damage pricing) is untouched; update records + the impact gate get the
smoother value (both benefit).

VERIFIED: EnginePower 1 play / 0 stops for the whole run (was 80 stop-cycles);
FootFall cycles normally per stride; gear shifts/windup lifecycle intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 09:07:36 -05:00
arcattackandClaude Opus 4.8 1d019e8109 Audio: footstep pulse VERIFIED end-to-end to the matcher; volume chain traced (task #50)
Footstep chain progress (each step empirically verified):
- The FootStep watcher is an AudioMatchOf<Logical> (AudioLogicalTrigger extends
  AudioMatchOf, NOT AudioTriggerOf): authored config match=1 -> Start(2.0) on a
  DirectPatchSource.  [trigcfg]/[matchcfg] ctor dumps added.
- The pulse was pinned at 1: the decay lived in IntegrateMotion, which the
  current gait path NEVER CALLS.  Moved to Mech::PerformAndWatch (provably
  per-frame) with a time-based 150ms window sized to the ~10Hz watcher poll.
  The matcher now fires per stride (14/run, was 1).
- Every footstep Start is DROPPED at the renderer: the source's volumeScale is
  0 (five VolumeAudioHandler sends of exactly 0 = inert/one-shot primes).  The
  volume arrives through the authored control chain (AudioControlMultiplier
  inputs ctl 100+; one 0 input pins the product).  Chain scales identified on
  this entity: LocalVelocity (live), LocalAcceleration (live), Myomers.
  SpeedEffect (=1.0 healthy; one authored scale maps [0,1]->[1,0] INVERTED),
  UnstablePercentage (INERT PAD -- always 0!), HeatSink.CurrentTemperature.
  PRIME SUSPECT: an UnstablePercentage-fed multiplier input primes 0 once and
  never updates (the pad never changes), pinning footstep volume at 0.
- SF2 numbering fact recorded: 38 presets in AUDIO1 / 5 in AUDIO2 have
  wPreset != file index (gap at preset 77); my table keys by wPreset.  The
  184x ProgramButton01 (bank1 patch83) storm at ~7/s needs an identity check
  against index-numbering (it may be the wrong sample for that patch id).

Diagnostics added (BT_AUDIO_SPATIAL/BT_ATTRBIND_LOG): [trigcfg]/[matchcfg]
ctor dumps, [matchfire] with component ptr+class, [volset] VolumeAudioHandler,
[mult0] multiplier zero-products, scale sends with authored boundaries,
[fswatch] dedicated footstep watcher tracer (g_btFootStepAddr), SetupPatch
ENTRY with bank/patch/state, Simulation::DebugAudioWatcherCount.

NEXT: back UnstablePercentage with a real (live) member -- it is the mech's
stability 0..1 (the stabilityAlarm/gyro family) -- or confirm via a one-run
chain dump which multiplier input pins the FS source; then the footstep
transient should clear the 0.3 drop gate at walking speed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:52:06 -05:00
arcattackandClaude Opus 4.8 ba3040ea09 Audio: footstep-chain diagnostics -- every link verified except the final trigger fire (task #50)
Instrumentation (all BT_AUDIO_LOG/BT_AUDIO_SPATIAL-gated): mech watcher-poll
probe (delayed flag + socket count via new Simulation::DebugAudioWatcherCount),
per-sim audio-socket size in ExecuteWatchers, trigger notifications (val>0),
watcher poll-rate/change probes, footstep pulse address trace.

VERIFIED working: FootStep binds to the real member (pulse addr == bound ptr,
same run); pulses fire per stride (40/run); the watcher registers on the mech
(audioWatchers=20 after audio-object creation); the mech's poll runs
(delayed=0, simFlags=0x1110); Logical == int (STYLE.H:132) matches the member
type.  The idle<->moving engine crossfade (AudioMotionScale on LocalVelocity
through the restored watcher poll) is CONFIRMED audible in play.

REMAINING: the FootFall transient still doesn't reach SetupPatch -- next probe
is the trigger's streamed threshold/inverse config and the speed-scaled
transient volume at the renderer drop gate (LowAudioVolumeThreshold=0.3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:16:27 -05:00
arcattackandClaude Opus 4.8 744ef8cc16 Audio: restore the dropped ExecuteWatchers poll -- polled audio watchers were ALL dead (task #50)
User: "when I start moving all the sounds fade away, no footsteps."  Diagnosis
chain (all empirical, BT_AUDIO_SPATIAL/BT_ATTRBIND_LOG traces):
- The audio head DOES track the mech (listener-relative positioning verified
  while driving) -- not a spatial bug.
- The idle sounds correctly STOP on leaving the standing state; the MOVING
  sounds never started because every POLLED audio watcher was dead:
  the reconstructed Mech::PerformAndWatch replaced the engine performance but
  dropped the ExecuteWatchers() step from the engine tail (Simulation::
  PerformAndWatch = Perform -> ExecuteWatchers -> WriteSimulationUpdate).
  Only PUSHED StateIndicator watchers (SetState->Execute) ever fired -- which
  is exactly why state sounds worked but footsteps/motion-scaled audio didn't.
  FIX: poll ExecuteWatchers() (AreWatchersDelayed-gated) before the update
  write.  Immediately unlocks the polled family: MissileLoaded01/LaserLoaded01
  ready dings, ProgramButton01, motion scales (PlayNote 15 -> 30 per run).

- FootStep (0x1e) backed REAL: was the inert attrPad (an AudioLogicalTrigger
  polls it -- threshold-crossing pulse per foot plant).  New footStep member,
  pulsed by SetBodyAnimation on entering any locomotion clip 1..0x17 (runtime
  stride alternation MEASURED as body states 12<->13 -- the enum-name numbering
  does not match runtime clip ids), decayed by IntegrateMotion (~1/3 s).
- Footstep volume is speed-scaled (AudioMotionScale on LocalVelocity): at
  standstill the transient start computes volume 0 and the renderer drops it
  (LowAudioVolumeThreshold) -- so footfalls are audible at real walking speed.

Diagnostics kept (BT_AUDIO_SPATIAL): spatial dist/vol per source, CLIPPED/DROP/
START at the request gate, vol=0 factor breakdown, scale->0 sends, trigger
notifications, watcher poll-rate probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:03:27 -05:00
arcattackandClaude Opus 4.8 7b1e469ffa Audio: in-game TRIGGERING works -- real StateIndicator attrs + audio-watcher databinding (task #50)
Enabling audio unlocked L4AudioRenderer::CreateEntityAudioObjects, which binds
AudioStateWatchers to entity/subsystem state attrs BY NAME (attribute-pointer
databinding) and calls AddAudioWatcher on them.  The reconstruction pointed the
Mech's state attrs at the scalar read-pad `attrPad` (fine for gauges that READ a
value, fatal for a state watcher that calls a METHOD) -> AV in SChainOf::Add on
0xCDCDCDCD.  Root-caused via cdb + a BT_ATTRBIND_LOG trace in AttributeWatcher.

Mech-level state audio now REAL + driven (verified: walking fires SetupPatch +
PlayNote/alSourcePlay, patches 78/80/116):
- AnimationState (0x1f) / ReplicantAnimationState (0x20): real StateIndicators
  (0x21 states covering MechAnimationState 0..0x20), driven from SetBodyAnimation
  so footstep/gait/transition sounds fire on animation change.
- CollisionState (0x16): real 4-state StateIndicator driven from ProcessCollision
  via collisionTemporaryState (the deferred @4aa741 per-frame zero + the
  part_012.c:15406-15413 contact tail, InitialHit accumulation; the 1-vs-2 Slide
  split awaits the 0x240/0x244 floats still stubbed by fieldAt()).
- @0x44c CORRECTED: "ammoState (0/1 leaking/2 dry)" was a mislabel -> it is
  collisionTemporaryState (never read as ammo).

Bring-up guards [T3, temporary, self-clearing] so audio-on is STABLE while the
subsystem state models are still stubs (Generators/Condensers/Myomers/Torso/
Avionics/Reservoir/ControlsMapper -- ~15 subsystems, ~40 attrs: GeneratorState,
CondenserState, ReportLeak, ...): a NULL subsystem attr redirects to an inert pad
(was a fatal Fail); an AudioStateWatcher on an unconstructed StateIndicator (null/
0xCDCDCDCD) skips instead of AV.  Both pass automatically once the subsystem is
reconstructed.  Those subsystem sounds stay silent until then (their audio wave).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:40:08 -05:00
arcattackandClaude Opus 4.8 3224009dc1 MP: peer accel/decel "snap" MEASURED ~0.64u (not ~2.9u) -- effectively resolved (task #50)
Went after the flagged ~2.9u peer-motion snap. Added a BT_SNAPLOG diagnostic
(mech4.cpp: logs the re-anchor drift eMag = |authority - dead-reckon| absorbed per
frame) and ran a controlled accel/decel soak: node B BT_AUTODRIVE + BT_DRIVE_SWEEP0
sweeping the throttle through STOP, node A observing, affinity-pinned.

Result: peer drift maxes at ~0.64u (median 0.54u, 18 events >0.5u over 48s), gently
absorbed at k~0.24/frame -- SUB-UNIT, at the noise floor. The ~2.9u figure was stale:
it predated the peer body-channel swap (96a896a, which put the peer on the same
channel the master's mirror predicts) AND was measured without the CPU-affinity fix
(packet-jitter-sparse records inflate the drift). The old "run the master mirror on a
leg-channel prediction" plan is moot now the peer is on the body channel.

No motion-code change -- just the measurement + the BT_SNAPLOG diagnostic (retained).
KB (multiplayer.md, open-questions.md) corrected to the measured value. checkctx CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:15:56 -05:00
arcattackandClaude Opus 4.8 aab7a8a137 Combat: FIX weapon effectiveRange -- it degraded with heatLoad, not host-zone damage (task #50)
MechWeapon::UpdateTargeting computed effectiveRange = (1 - heatLoad) * weaponRange,
reading the weapon's own inherited HeatableSubsystem heatLoad. The authentic decomp
(@004b9bdc:6983) reads *(weapon+0xE0)+0x158 = Subsystem::damageZone->damageLevel --
i.e. effectiveRange = (1 - HOST-ZONE DAMAGE) * weaponRange. Same @0xE0-DamageZone-vs-
heat misattribution already corrected in HeatSink::UpdateCoolant (heat.cpp:803).

Impact: for a charge/discharge weapon (ER laser) the weapon's OWN heatLoad swings
0..1 every fire cycle, so effectiveRange collapsed toward 0 and the weapon was
perpetually "out of range" -> Emitter::FireWeapon's `if (dist <= effectiveRange)`
gate skipped SendDamageMessage -> NO damage submission and hence NO impact explosion.
The beam still rendered (beamFlag/beamEndpoint set before the gate), so the shot LOOKED
like a hit but did nothing -- the user-reported "lackluster/absent laser hits, esp.
the ER medium, on mechs AND buildings". PPCs mostly worked only because their heatLoad
happened to sit low/stable.

Fix: read the QUALIFIED this->Subsystem::damageZone->damageLevel (the MechSubsystem
shadow is a shim -- heat.cpp:812) so an UNDAMAGED weapon holds its full, STABLE
weaponRange, and range shortens only as the weapon's host zone takes battle damage.

Verified (parked in range of a building, autofire): laser effRange 500 STABLE
(was fluctuating 0/59/340/424 -> mostly out of range); impact explosions 13 in 22s
(11 laser id=16 + 2 PPC), up from ~2. Lasers now consistently damage + spawn FX.

Also adds env-gated diagnostics used to root-cause this: [fireW] range trace +
per-weapon explID (emitter.cpp), and BT_FIRE_AT_STRUCT (mech4.cpp) which designates
the nearest world structure so weapon-vs-structure fire can be tested without the
screen aim ray.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:31:47 -05:00
arcattackandClaude Opus 4.8 de8f6d02c1 MP: world structures (garages/walls) TARGETABLE -- boresight ray-tests the static collision tree (task #50)
The boresight's non-mech pick only sampled the VISUAL heightfield (BTGroundRayHit),
which on arena1 is a single flat 'sky'-named ground mesh (and class-42 BuildTables
likewise holds only 'sky') -- so shots passed THROUGH the garages/walls and only mechs
moved the HUD range axis (user-reported regression: "buildings used to move the range
axis; now only mechs do, and you can only fire at a peer").

Real fix: the boresight now ALSO ray-tests the ZONE'S STATIC COLLISION SOLID TREE --
the same geometry that already blocks the mech's walk. Authentic engine mechanism:
Mover::FindBoxedSolidHitBy already tests the static world via
zone->GetCollisionRoot()->FindBoundingBoxHitBy(line). Factored its "static world" tail
into Mover::FindStaticSolidHitBy(Line*) (static solids only, no movers), wrapped by
Mech::WorldStructurePick(start,dir,range,&hit) (builds the world-space Line, reads the
entry point back via line->FindEnd since HitByBounded clips line->length=enter).

Boresight pick order is now: closest MECH (PickRayHit, damage zones + lock) -> closest
STRUCTURE (WorldStructurePick; occludes a mech BEHIND it; designates the gBTTerrainEntity
sentinel + entry point, so the range caret reads the structure distance and NO lock ring
draws, mech4.cpp:4529) -> flat ground (BTGroundRayHit) -> sky (fire-at-nothing). Also
un-skips arena1's misnamed-'sky' flat ground so the ground tier works (btvisgnd
geometry-aware skip + [mapent]/[rendent] census).

Verified headless: a BT_WSWEEP horizontal ray-fan on arena1 tracks position (3/24 hits
near the boundary -> 17-21/24 inside the interior garage cluster -> 3/24 past it =
DISCRETE interior solids, not an enclosing box), no crash/assert/AV. Interactive aim
(BTGetAimRay) can't run headless (no window -> noRay), so the sweep is the headless proof;
interactive aim is user-verified.

Note: FindBoundingBoxUnder (the ground/containedByNode BoundingBoxTree) is DOWNWARD-only
(gravity/ground-snap: *height = FindDistanceBelowBounded), useless for a horizontal
boresight; the static SOLID tree's FindBoundingBoxHitBy is the only ray-vs-world query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:08:45 -05:00
arcattackandClaude Opus 4.8 ee6e19e86e Combat: RESTORE authentic fire-at-nothing + non-mech range axis (task #50 / Discord report)
Determined the authentic behavior from the decomp (HudSimulation + Emitter,
part_013.c:5619-5670 / 7689-7778) and fixed two regressions the players hit:
 (A) You could only fire when a MECH was locked (couldn't fire at nothing).
 (B) Buildings/structures/ground stopped moving the range axis; only mechs did.

AUTHENTIC (T1): a weapon discharges iff the target slot mech+0x388 != 0
(Emitter::FireWeapon FUN_004bace8:7727), and 0x388 is whatever the BORESIGHT
designates -- mech OR world geometry -- NOT a manual mech lock.  The RANGE readout
(HUD+0x1ec) moves for ANY designated target (mech OR structure/ground); the LOCK
ring is mech-only (target has a damage-zone table, HudSim :5619).  So the pod
'fired freely at nothing' and buildings moved the range caret.

ROOT CAUSES (both content-triggered, not a targeting-code change):
 1. arena1's ONLY class-42 world entity is the 10000x10000 FLAT GROUND plane at
    y=0, MISNAMED 'sky' -- SkippedName filtered it out by name, so BuildTables
    ingested 0 geometry and gBTTerrainEntity stayed null -> nothing groundable ->
    mech-only targeting.  (LAST.EGG rewrite ~bb795e2 lost whatever terrain the
    working scene had.)
 2. A boresight that hit nothing set MECH_TARGET_ENTITY=0 -> no discharge.

FIXES:
 - btvisgnd.cpp: geometry-aware skip -- keep a wide, near-flat, near-ground plane
   even if name-skipped (it is the arena floor); still skip true domes/backdrops.
   Adds mesh y-bounds.  arena1 now ingests 1 ground instance (was 0).
 - btl4vid.cpp: capture ANY world-geometry entity as the non-mech pick sentinel
   (the default render case is world-only), not only Terrain-derived ones.
 - mech4.cpp: FIRE-AT-NOTHING -- when nothing pickable is hit, designate a
   max-range point (1200, the HUD default 0x44960000) along the boresight using
   the world sentinel, so 0x388 != 0 and the weapon discharges (no zone damage /
   no lock ring, since the sentinel has no damage-zone table -- mechs only).

Verified autonomous: arena1 ground ingested; [target] shows 'fire-at-nothing
(max-range designate)'; weapon discharges at empty space ([fire] explode
resolved); range axis (sShownRange->BTSetHudTargetRange) slides to the designated
distance.  Diagnostic: BT_GROUND_LOG (world-geometry ingest + skip reasons).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:10:48 -05:00
arcattackandClaude Opus 4.8 23f1532fb0 MP: peer true-mirror cadence -- drive the body clip from actual replicated motion (task #50)
Toward a truer mirror (user: peer 'hesitates/skips on accel/decel, master has
smoother transitions').  The body channel FOLLOWS the master's replicated STATE
(96a896a) but its CADENCE (bodyCycleSpeed) is still slewed LOCALLY toward the
replicated commanded speed -- which drifts from the master's actual cadence, so
the animation phase wanders until a record snaps it (the hesitation).

FIX (replicant-gated, single-player untouched): peerMirrorSpeed member = the
actual replicated ground speed (|updateVelocity|, set in the peer branch); the
body channel walk (6/7) + reverse (0xc/0xd) cases override bodyCycleSpeed with
it (clamped to the state's band) right before the clip advance, so the peer's
clip advances at the master's REAL rate instead of a locally-drifting slew.
peerMirrorSpeed = -1 on the master/single-player -> the authentic slew runs.
BT_NO_MIRROR_CAD / BT_PEER_LEGCH revert.

Verified autonomous (through-zero sweep): no crash, body channel advances forward
(back-steps ~0), feet track (position ratio 1.05, maxStep 1.68u).  Perceived
smoothness is the visual test (harness saturates).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:58:11 -05:00
arcattackandClaude Opus 4.8 f094d78df1 MP: restore peer turn-step on the body channel -- arm turn state 4 from replicated turn (task #50)
The body-channel swap (96a896a) fixed the stop-slide but regressed turn-in-place
to a rotating statue: the body Standing case only enters walk/reverse/stand, never
the turn clip (state 4).  On the master that arming comes from the LEG Standing
case cross-arming both channels (mech2.cpp:937) -- which the peer no longer runs.

FIX (peer branch): arm/exit the body turn state from the replicated turn
(replMppr->turnDemand, already derived from the replicated yaw rate with
hysteresis): standing + turning -> SetBodyAnimation(4) so the body case 4 advances
the trn clip (peer STEPS through the pivot); turn stops -> back to Standing.
Walk/reverse transitions still own their own exits.

Verified autonomous (forced spin): peer body channel holds state 4, trn clip
advances (frmAvg 0.2-0.3, frmMax 1-2), back-steps 0, no crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:38:33 -05:00
arcattackandClaude Opus 4.8 96a896ae43 MP: peer poses from the BODY channel following replicated state -- the authentic single-channel design (task #50)
Answers 'is this different from the original?' -- NO, it RESTORES it.  Decomp
(workflows wh1h5gnmc/w1s9ou02o): the 1995 game had ONE live skeletal channel,
the BODY channel FUN_004a5678 via IntegrateMotion; the LEG channel FUN_004a5028
is DEAD CODE (zero call sites, SetLegAnimation never runs).  Both master and peer
posed the whole skeleton from the body channel, the PEER FOLLOWING the master's
replicated body-anim state (bodyStateAlarm@0x728, set by the type-3 reader's
SetBodyAnimation, mech.cpp:1913) + replicated bodyTargetSpeed@0x6b4.

Our port RESURRECTED the dead leg SM as the peer's poser (mech4.cpp AdvanceLeg-
Animation) fed a LOCALLY re-derived commanded speed.  The leg SM's phase-
independent pre-switch wind-down (mech2.cpp:560-568: force-jump {6,7,8,9}->stand
when legCycleSpeed<=0) made the peer SKIP the master's decel state 8 and snap to
stand while the body coasted -- the 'slid forward after legs stopped' slide, the
reverse 'slippy sliding in place', and the whole re-derivation desync class.

FIX (peer branch, gated BT_PEER_LEGCH=1 to revert): pose+travel from
AdvanceBodyAnimation(dt, mj=1) -- the body channel, which follows the replicated
state and has NO early wind-down, playing the master's exact clips (walk/decel-8/
reverse/stand).  ZERO new netcode -- the state is already on the wire and already
decoded into the body channel; we were simply posing from the wrong (resurrected,
dead) channel.  Single-player untouched (replicant-gated).

Verified autonomous (through-zero sweep): no crash; body channel advances
(frmAvg 0.3-0.75, states 5/6/7/10 following master); slide-in-stand events
519 -> 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:50:43 -05:00
arcattackandClaude Opus 4.8 3792a04661 Fix throttle-detent Abs() macro bug + KB analog-throttle + slide diagnostics (task #50)
Uncommitted work from the speed-model + peer-motion investigation:
 - btl4mppr.cpp: the L4MechControlsMapper full-throttle detent used the
   unparenthesized Abs() macro (STYLE.H:118) on an expression -- Abs(throttlePos
   - 1.0f) mis-expands to -(throttlePos + 1.0f), always <= 0.05, so the detent
   snapped throttle to full EVERY frame.  Diff into a temp first (same class as
   the 7615ecd angular-resync Abs fix).
 - context/pod-hardware.md: document the decomp-verified analog-continuous pod
   throttle path (RIO Ranger 0-800 counts, 0.05 deadband, no notching; '5 speeds'
   is false) from workflow w0odszxro.
 - mech4.cpp: BT_SLIDE/[mslide] per-frame slide diagnostics (peer position moving
   while legs in stand; master decel profile) -- used to prove the stop-slide is a
   peer leg-SM-winds-down-early desync, not master momentum.  Env-gated.

The 'pretty good' coupled-motion gameplay state is already shipped (a9ab3db).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:13:56 -05:00
arcattackandClaude Opus 4.8 a9ab3db952 MP: FIX peer gliding-stops -- snap the residual offset when legs are at rest (task #50)
User-reported 'gliding stops': when the master halts, the peer's legs wind down
to standing but the error-absorption kept sliding the body forward to close a
small residual offset -- visible precisely because no leg motion masks it.  With
the feet planted a sub-unit correction is imperceptible, so resolve it in one
frame (k=1) when replLegAdv~0 and the offset is small, instead of gliding it in.
Large offsets at rest (a real teleport) still ease.

Measured (stop-emphasis sweep): maxStep 1.77u->0.98u (now sub-unit), peer drift
2.1u->1.0u mean.  The remaining <=1u snaps are the leg-vs-body channel-shape
mismatch -- likely at/near the in-spec floor for this test rig (the Python
console relay batches packets, unlike the real dedicated pod network).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:51:14 -05:00
arcattackandClaude Opus 4.8 38fed81d8a MP: tighten peer re-anchor -- error-proportional absorption (residual snap, task #50)
Chasing the ~2.9u residual snap: instrumented BT_MIRDIV and isolated it to the
PEER side (master send-mirror error is accurate, mean 0.39u < 0.55u threshold;
peer drifted 3-11u from received authority).  Root: the master models the peer
with the BODY channel but the peer moves with the LEG channel -- slightly
different travel/frame -- so the master deadband does not fire exactly when the
peer drifts, and the fixed ~1/3s offset decay bled slower than accel/decel drift
accumulated.  A hard ground-snap (authentic FUN_004ab1c8:14985) popped instead
(records not perfectly dense on the one-box relay).

FIX: error-proportional absorption -- bleed the offset to updateOrigin at a rate
that scales with the error (k 0.15..0.6/frame): small steady offset absorbed
gently (no foot-pop), large speed-change drift caught in a few frames.

Measured (through-zero sweep): peer drift 11u->2.1u, maxStep 2.9u->1.77u, ratio
1.043->1.036.  Remaining <=1.8u snaps are the leg-vs-body channel-shape mismatch;
fully closing needs a leg-channel send-mirror (second instance) -- deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:41:30 -05:00
arcattackandClaude Opus 4.8 b0137427a6 MP: PROMOTE coupled peer motion to DEFAULT -- user + metrics confirm (task #50)
User confirms 'way better'; single-player un-regressed (master walks correctly,
the bodyTargetSpeed change only affects the invisible mj=0 body channel).
Flip the authentic-coupled path ON by default:
 - peer position: gait-coupled linear (was velocity dead-reckon); BT_DR_POS=1 reverts
 - master send-mirror: gait projection of projectedOrigin; BT_NO_MASTER_GAITMIRROR reverts
The two-source split (position from velocity + animation from commanded speed)
that mismatched during speed changes is retired.  Residual: occasional ~2.9u
snap from the leg(peer)-vs-body(mirror) channel mismatch -- second-order,
tracked for follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:29:48 -05:00
arcattackandClaude Opus 4.8 c52a1ad79a MP: COUPLED peer motion -- authentic gait-driven position + master gait send-mirror (task #50, env-gated)
Finishes the coupled IntegrateMotion path the decomp workflow (wh1h5gnmc, 3
make-or-break claims CONFIRMED by adversarial verify) proved authentic:
 - Peer LINEAR position is SINGLE-SOURCE gait: IntegrateMotion integrates the
   body-channel cycleDistance into projectedOrigin.linear@0x260, copied verbatim
   to localOrigin -- NOT velocity dead-reckon (that is angular/heading ONLY).
   0x260 'motionDelta' and 'projectedOrigin' are literally the same field; the
   'contradiction' was our reconstruction's two misnamed shadows.
 - Master and peer run the SAME predictor; the master's SEND-mirror must run it
   too (binary FUN_004a9b5c @0x4aab9c) so a gait-driven peer stays anchored. Our
   mirror used the constant-velocity deadReckoner + overwrote bodyTargetSpeed
   live every frame -> could not model a gait peer (the tug-of-war).
 - T4 CONFIRMED: both channels pose the full skeleton (same JointedMover); keep
   the peer LEG channel (our body channel is unbound on the peer).

CHANGES (all behind BT_MASTER_GAITMIRROR / BT_ROOT_POS, DEFAULT OFF -- zero
change to shipped behavior until visually confirmed):
 - mech4.cpp: send-mirror advances projectedOrigin by the mj=0 body channel
   travel (mirrorBodyAdv) rotated by heading + last-sent angular vel, re-seeded
   to localOrigin on each send, instead of the deadReckoner; and stops the
   per-frame live bodyTargetSpeed overwrite so the mirror slews toward last-sent.
 - mech.hpp/mech.cpp: mirrorBodyAdv member.

Measured A/B (autonomous through-zero sweep circle, the speed-change regime):
   velocity two-source (current default): ratio 1.0022 BUT user-visible glitch
   coupled, NO mirror (dense):            ratio 1.86  (tug-of-war)
   coupled + gait mirror, dense OFF:      ratio 1.14, back-steps 0.1%
   coupled + gait mirror, dense ON:       ratio 1.043, back-steps 0.1%, maxStep 2.9u
The single-source coupling is proven (backward-stepping 0.1% vs the split's
churn). Residual 2.9u occasional snap = leg(peer)-vs-body(mirror) channel
mismatch, second-order. Awaiting visual confirmation before default promotion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:22:02 -05:00
arcattackandClaude Opus 4.8 702822f9c5 MP: root-motion peer-position EXPERIMENT (env-gated BT_ROOT_POS; default unchanged) + KB notes (task #50)
The user's architectural question -- 'should animation and velocity even be
allowed to be uncoupled?' -- is decomp-CONFIRMED correct: the original peer
(FUN_004ab430 -> FUN_004ab1c8) drove POSITION FROM THE CLIP'S ROOT TRAVEL
between records (feet<->ground locked by construction) with a pose-sync
offset decay absorbing record corrections.  Our port dead-reckons position
from velocity while the legs run on commanded speed -- the two only agree at
steady state, mismatching exactly during speed changes (the reported glitch).

This commit lands the peer half of the coupled architecture, env-gated:
 - BT_ROOT_POS=1: peer position += clip travel rotated by heading (mirror of
   the master world-step @3325, == IntegrateMotion tail @004ab1c8); pose
   records absorbed via the authentic offset-decay (motionEventVector
   mechanism) instead of snapping.
 - Measured A/B (through-zero sweep circle, harshest speed-change regime):
     velocity-lerp (default): step ratio 1.0022 (clean)
     root-motion + sparse records: 11-17u anchor snaps (master's velocity
       mirror no longer models a gait-driven peer -> under-sends)
     root-motion + dense + offset-decay: evenness OK but ratio 1.86 --
       authority tug-of-war (double-authoring, exactly the D5 risk).
   CONCLUSION: the coupled peer requires the MASTER side of the original
   architecture too (gait-driven send-gate mirror / channel-B IntegrateMotion
   projection) -- a coherent rebuild for a fresh session, not a peer-only
   patch.  Default therefore stays velocity dead-reckon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:59:22 -05:00
arcattackandClaude Opus 4.8 2c6db6a2de Diag: speed-change investigation -- authentic analog throttle CONFIRMED, harnesses + gait probes (task #50)
Decomp workflow w0odszxro settled the speed model: the pod throttle was ANALOG-
CONTINUOUS (RIO Ranger, 800 ADC counts, 0.05 deadband; sole detent = snap-to-1.0
within 0.05 @004d196c).  '5 speeds' is FALSE -- the 1-5 keys are MFD mode pages;
the 0..5 stepper is HUD/radar zoom.  BONUS: throttleState@0x4a4 is a MISNOMER --
binary census proves it is the fall-contact surface material cache (0..7, init
2=Concrete), written only at knockdown; rename pending.

Empirical rule-outs (autonomous drive-sweep harness, this commit):
 - Record density: with the clock guard + incremental heading in, a per-frame
   continuous demand sweep measures IDENTICAL to constant throttle on position
   evenness ([repljit]), render heading ([rendhdg]), and gait cadence ([gaitev]).
 - Type-3 stomps: ~0 fire in sustained sweeps (only at launch) -- not the driver.
 - The 0.05-grid keyboard publish experiment is retained env-gated OFF
   (BT_GRID_LEVER) -- the ADC was ~continuous, so the grid is NOT authentic and
   gains nothing measurable; default publish stays continuous (authentic).

New tooling: BT_DRIVE_SWEEP[0] (forced-drive triangle sweep, optional through-
zero), BT_FORCE_STEP (0.05-grid variant), BT_GAITEV (per-frame leg-clip advance
+ state-flip + demand-change stats), [t3rx] (type-3 stomp trace).

STATUS: user still reports visible speed-change glitches in interactive play;
all harness metrics saturate at baseline -- next step is probes ON the user's
interactive session (their eyes + instruments on the same run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:47:03 -05:00
arcattackandClaude Opus 4.8 d78e77bf84 MP: FIX walk+turn peer skip -- incremental heading integration + scalar yaw mirror (task #50)
The user-keyboard regime (steering WHILE walking) fired BOTH dense record streams
at once and exposed the last divergence: our peer heading used the engine
Mover::DeadReckon slerp-toward-projection, whose angular projection reads the
SHARED lastUpdate/nextUpdate timebase.  The dense type-0 pose stream resets that
timebase every frame while walking while RESTORING a stale orientation (the
authentic case-0 strip, verified against FUN_004a1232 case 0) -- so the angular
target barely advances from a stale base and the slerp DRAGS the heading back
every frame.  Measured: peer yaw advancing at ~40% rate with half the frames
stepping BACKWARD.  Pure-spin and pure-walk tests never showed it (single
stream) -- why autonomous looked smooth while keyboard play skipped.

AUTHENTIC FIX (decomp FUN_004ab1c8 -> FUN_004ab188/FUN_00409f58): the original
replicant integrates its heading INCREMENTALLY from the CURRENT pose -- exact
rotation of (replicated yaw rate * dt) composed on each frame -- and re-anchors
on type-4 receipt.  It never slerps toward a projected angular target.
 - mech4.cpp peer branch: save heading, let DeadReckon own LINEAR only, then
   integrate heading incrementally (ReconQuatIntegrate); on angSyncLatch (new
   type-4) re-anchor to updateOrigin.
 - mech.hpp/mech.cpp: angSyncLatch member (angular analog of poseSyncLatch),
   armed by ReadUpdateRecord case 4.
 - SCALAR peer-yaw mirror (angMirrorYaw/Rate/Time, re-based in the type-4
   writer): replaces the quaternion projectedOrigin mirror for the ANGLE
   deadband -- the old one was recomputed each frame by the master's own
   reckoner from timing it does not control and false-fired in pi-waves
   (measured maxAng~=pi bursts -> periodic resync floods).
 - Dense-rot type-4 send REMOVED (was masking the old crude projection; not
   authentic; churned the shared horizon).  Orientation now rides the sparse
   angle/velocity deadband resyncs exactly as the binary's.

Verified live-autonomous:
 - pure spin: 59/59 perfectly regular peer yaw steps; master resyncs 0/s with
   mirror drift ~5e-7 (records near-silent, authentic sparse model).
 - walk+turn circle (the user regime): peer sim yaw monotonic at exactly the
   master's rate (0.00556/frame @ 0.327 rad/s), no backward steps, no stalls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:52:15 -05:00
arcattackandClaude Opus 4.8 b7be95b584 Diag: BT_TRNRATE probe -- turn-in-place body-rate vs trn-clip cadence (task #50)
Confirmed via BT_TRNRATE + direct disasm of the un-exported perf drive (0x4aa3d3)
that turn-in-place rate == walkingTurnRate (speed=0 collapses the lerp) and the trn
clip cadence is authentically FIXED -- so the reconstruction is faithful and the
residual legs-vs-body slip is authentic (decoupled by design; scaling the clip would
deviate from the decomp).  Probe left env-gated for future asset-tuning checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:26:55 -05:00
arcattackandClaude Opus 4.8 a8eb8a427f MP: FIX peer spin hang/divergence -- exact quaternion integration + frequent orientation (task #50)
Answers 'how did the original handle this?' from the decomp (subagent hunt):
the 1995 binary's replicant reckoner (FUN_004ab1c8 -> FUN_00409f58, part_000.c:9359)
integrates heading EXACTLY: build a unit axis-angle rotation quaternion from
angularVelocity*dt ({axis*sin(t/2), cos(t/2)}) and Hamilton-multiply it onto the
heading (FUN_00409d9c) -- exact for any timestep, stays on the unit sphere.  It
further carries the full orientation quaternion in the FREQUENT pose record
(FUN_0040a938, 7-float pose), so the dead-reckon gap stays tiny.

Our reconstruction diverged two ways, both fixed:
 1. ReconQuatIntegrate (mechrecon.hpp) -- the reconstruction of FUN_00409f58 -- was
    STUBBED as , a crude small-angle VECTOR add.  Restored to
    the real exact axis-angle composition.  (A 'no stand-ins' violation: the comment
    even wrongly claimed Quaternion::Add == FUN_00409f58.)
 2. The engine Mover reckoner (MOVER.cpp AcceleratedDeadReckoner/LinearDeadReckoner)
    also did the vector Add on projectedOrigin.angularPosition -> over a long peer
    record gap it diverged to ~180deg then snapped (the reported spin HANG/hesitation).
    Routed both through a new ExactAngularProject() helper (same exact math).
 3. Orientation only rode the sparse type-4 resync; during a PURE spin the linear
    dense-send never fires (not translating), so the gap ballooned (~1.6s) and the now-
    exact projection sat far ahead -> the slerp jumped.  Added an ANGULAR dense-send
    (resync every frame while |yawRate|>0.1), mirroring the original's frequent-
    orientation model -> gap stays tiny -> smooth.

Verified live-autonomous (BT_AUTODRIVE+BT_FORCE_TURN + BT_RENDHDG render-rate probe):
the ~180deg divergence + multi-radian snaps are GONE (rendered maxStep 0.05-0.10 rad,
no jumps).  User confirms: no frame hang/hesitation.  MOVER.cpp change is strictly
more correct (exact==crude for the small per-frame master case; only large-gap peer
extrapolation changes), so walking is unaffected.

KNOWN REMAINING (separate, smaller): the turn-STEP leg animation (trn clip, mech2.cpp
advance_normally) runs at a FIXED idleStrideScale cadence that does not scale with the
rotation rate, so the legs lag/skip vs the (now-correct) body rotation.  Next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:05:16 -05:00
arcattackandClaude Opus 4.8 9eff043973 Diag: render-rate rotation-evenness probe + angular-drift instrumentation (task #50 spin residual)
Autonomous headless spin harness (BT_AUTODRIVE+BT_FORCE_TURN) + probes that
finally measure what the eye sees, not the sim heading:
 - BT_RENDHDG (L4VIDEO render loop): per-RENDERED-frame peer heading step
   evenness -- avgStep/maxStep/max-avg ratio.  Confirmed the residual spin
   'hesitation' is UNEVEN rendered rotation (max/avg ~2.8x), correlated with the
   byAngle resync-flood bursts (maxAng ~= pi), NOT a render-vs-sim rate stall
   (render redraws a fresh heading every frame).
 - BT_SPIN / BT_ANGSIGN: resync trigger breakdown (byAngle/byVel/byRest) + the
   frame-level local/update/projected angular-Y that exposed the Abs() macro bug.
 - gBTReplRenderYaw: peer heading published from the sim to the render probe.
 - Normalize projectedOrigin.angularPosition after the peer-mirror advance
   (adding a scaled ang-vel VECTOR to a quaternion denormalizes it) -- correct,
   but NOT sufficient: the pi divergence is REAL, from the crude large-angle
   quaternion projection over the sparse angular-record interval during a PURE
   spin (linear dense-send doesn't fire when not translating; type-4 is the only
   orientation carrier and resets the horizon, so dense type-4 -> half-rate).

Residual ROOT (scoped, not yet fixed): the reckoner (MOVER.cpp:457-466) projects
rotation by ADDING vector*t to the heading quaternion -- a small-angle approx the
original kept valid via dense records; our pure-spin case has 1.6s record gaps so
it diverges to ~180deg -> uneven render.  Fix path = proper quaternion integration
in the reckoner, or refresh the angular origin without the type-4 horizon reset.
Both touch the shared engine dead-reckon -> aligning before the change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:27:04 -05:00
arcattackandClaude Opus 4.8 7615ecd316 MP: FIX peer spin freeze/half-rate -- unparenthesized Abs() macro floods angular resync (task #50)
Root cause found via autonomous headless spin (BT_AUTODRIVE+BT_FORCE_TURN) +
frame-level [angsign] probe: the STYLE.H:118 macro

    #define Abs(value) ((value>0) ? value : -value)

has NO parens around value, so Abs(a-b) mis-expands to (a-b>0 ? a-b : -a-b) ==
-(a+b) on the false branch, NOT |a-b|.  The angular resync velocity gate passed
an EXPRESSION: Abs(localVelocity.angular.y - updateVelocity.angular.y).  For a
steady spin the two are equal, so a-b==0 takes the false branch and yields
-(2*rate): on a REVERSE spin that is +2*rate > velDb, firing a type-4 resync
EVERY frame.  The resync flood reset the peer's dead-reckon horizon every frame,
pinning its angular slerp at ~half rate and (as the drift periodically ran to
180deg) freezing the rotation for seconds while the leg turn-clip kept stomping.

Confirmed live-autonomous: velDrift 0 (was +2.618), byVel 0 (was 55/55), peer
rendered-rotation median dyaw/expected 1.00 (was 0.49), no multi-second dt gaps.

Fix (mech4.cpp resync gate, reconstruction side -- the engine macro is original,
the original master-perf avoided it by diffing into a temp):
 - velDrift: diff into a temp, explicit (d<0?-d:d) so the macro never sees an
   expression.
 - angDrift: already reworked to a single-variable wrap-safe yaw delta (also
   dodges the macro) -- and it corrects a prior units/wrap bug (was comparing a
   raw quaternion-Y component against a radian deadband).
 - velocity gate now diffs vs updateVelocity (the value the peer extrapolates
   with), same scalar representation as localVelocity.
 - BT_SPIN / BT_ANGSIGN / updYaw diagnostic probes (env-gated).

KNOWN RESIDUAL (smaller, follow-up): the ANGLE gate still bursts when the crude
large-angle quaternion projection in the reckoner runs updateOrigin stale between
writes and drifts ~180deg; causes occasional single-frame hitches (peer median is
still 1.0), not the freeze.  Separate from this fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:04:32 -05:00
arcattackandClaude Opus 4.8 6374efc26a MP: FIX peer gait jitter -- feed the leg channel the REPLICATED commanded speed (task #50)
Decomp-verified via multi-agent investigation (workflow wv1km7lvc, adversarially
checked against reference/decomp/all/part_012.c + engine T0).

ROOT CAUSE (D3 CONFIRMED): the peer's leg channel FUN_004a5028 reads its speed
demand from mapper->speedDemand (**(mech+0x128)+0x128, part_012.c:11947/11975/
12028). The reconstruction FABRICATED that input from the noisy dead-reckoned
velocity magnitude and PINNED it flat with a standSpeed*1.05 floor across the
walk threshold -- the leg stutter/skip + cadence-vs-travel desync, worst on
accel/decel. The authentic input is the replicated commanded speed
bodyTargetSpeed@0x6b4 -- the master's own throttle-commanded speedDemand
(mechmppr.cpp:749), stamped into every record (mech.cpp:2036) and read back on
the peer (mech.cpp:1833/1841/1871). It is the IDENTICAL value the master's leg
channel consumes, so the peer clears the stand->walk gate exactly when the master
does and ramps cadence continuously through accel/decel.

Confirms the 07-13/07-14 regression: bb795e2 (smooth, 07-12) fed raw derived
speed with NO floor; the floor added in the task-#64/#50 spiral is what pinned
the cadence.

CHANGES (mech4.cpp replicant branch):
 - Change 1 (D3): replMppr->speedDemand = bodyTargetSpeed (was derived velocity
   + standSpeed*1.05 floor). Old path kept behind BT_REPL_VEL for A/B.
 - Turning: authentic peer turning is net-driven (angular slerp in DeadReckon,
   MOVER.cpp:521-525, D8). Removed the flickery bare +-0.02 yawRate->turnDemand.
   The visible turn-STEP (authentically from the body channel replaying the
   type-3 turn state; we run the leg channel per D2) is reproduced by arming the
   trn clip from the replicated yaw rate with HYSTERESIS (enter >0.08, hold until
   <0.03) so the clip no longer chatters. BT_REPL_NOTURN = decomp-strict, no step.

REFUTED (kept as-is): D1/D5 -- peer POSITION authentically comes from the engine
velocity dead-reckoner (Mover::DeadReckon, T0), NOT the gait travel; the
body-channel switch DEVIATES (double-authors position + T4 leg-bone risk) and was
NOT taken. D4 -- bodyTargetSpeed is non-zero tracking throttle (0 only on coast,
which authentically winds down to stand).

Verified live: walking + accel/decel + in-place pivot all smooth (user-confirmed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:08:22 -05:00
arcattackandClaude Opus 4.8 d5d512e087 WIP checkpoint: TCP_NODELAY + dense-send + BT_JIT/BT_ANIM probes + FOGDAY test egg (task #50 investigation)
Session-in-progress peer-motion work, checkpointed before bisecting the
07-13/07-14 gait regression. Contains: TCP_NODELAY on game sockets (L4NET),
every-frame dense position send while moving + came-to-rest/REST send +
per-frame BT_JIT/BT_ANIM smoothness probes (mech4). bodyTargetSpeed feed
reverted to derived-velocity default. MOVER.cpp spline experiment already
reverted. NOT a fix -- a checkpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:43:35 -05:00
arcattackandClaude Opus 4.8 94647dd378 MP: FIX phantom walk-in-place -- master sends a pose record on came-to-rest (task #50)
The linear pose send-gate watched only POSITION drift + the 2s heartbeat.
When a mech stops, its localOrigin is pinned and the master's own dead-reckon
projection re-bases to it each frame, so error->0 and NO pose record fires --
leaving the peer holding the last WALKING velocity (updateVelocity). The
replicant then dead-reckons + animates a phantom walk-in-place until the 2s
heartbeat (stretched to ~30s when the master window is OS-throttled to ~8fps
while backgrounded during solo two-window testing).

Add the symmetric linear 'came-to-rest' trigger -- the exact analog of the
angular (live yaw-rate==0 && replicated yaw-rate!=0) resync trigger already
present in the type-4 gate: fire a pose record the frame the live horizontal
speed collapses while the last-SENT speed was still non-zero, so the replicant
receives velocity=0 immediately instead of waiting for the heartbeat.

Diagnosis established via BT_WIRE tx0/rx0 probes: records deliver 1:1
(239 tx =~ 240 rx), velocities match -- NOT a replication stall; the master
simply never transmitted the stop. Also confirmed the dominant choppiness is
OS window-throttling (master 8fps backgrounded vs observer 63fps), a
two-windows-on-one-box test artifact absent on real pod hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:30:18 -05:00
arcattackandClaude Opus 4.8 2aba89f962 MP: FIX replicant walk-entry floor -- gate on standSpeed not walkStrideLength (task #50)
The prior floor gated on walkStrideLength*0.5, but walkStrideLength is a
stride METRIC (~22.0), not the walk velocity (~6.1) -- so the condition
sd > 11 was never true for a walking peer and the floor never fired. The
replicant's derived speedDemand (== actual velocity 6.13) sits BELOW
standSpeed (6.83), so the leg SM's stand->walk gate (standSpeed < demand)
never tripped and the peer slid forward in the Standing pose.

Gate on standSpeed instead: floor the demand to standSpeed*1.05 when the
peer is clearly walking (demand in (standSpeed*0.5, standSpeed*1.05)).
Verified live: uvel=6.13 -> spd=7.169 floored, legState enters 5 (walk)
on frame 1 with no Standing-pose slide. standSpeed=6.8277 confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:07:45 -05:00
arcattack 936c34c997 MP: FIX replicant 'slides before it steps' -- walk-entry floor on derived demand
Root-caused from a live per-frame [replmov] trace (user's session, where replication
works -- the headless rig won't spawn a replicant): the replicant's leg SM sat in
Standing (legState 0, legFrm pinned at 19) while dead-reckoning forward at spd=6.13,
until the master's speed rose to 6.99, then it snapped to StandToWalk.

Cause: the replicant derives speedDemand from the master's ACTUAL velocity, but a
mech's forward walk velocity == walkStrideLength, which sits BELOW standSpeed (bhk1:
walk 6.13 < stand ~6.8). The leg SM's stand->walk gate is ,
so a steadily-walking peer's derived demand NEVER crosses it -> the peer slides in the
standing pose until the master happens to accelerate past standSpeed. The master never
hits this: it feeds the leg SM its COMMANDED throttle speed (>> standSpeed), not the
actual velocity.

Fix: when the peer is clearly moving forward, floor the derived demand just past
standSpeed so it enters the walk cycle immediately; the walk case clamps legCycleSpeed
back to walkStrideLength, so cadence still matches travel (no foot-slip). Reverse
needs no floor (its gate is commandedSpeed < ZeroSpeed, tripped by any negative).

Diag: BT_REPL_MOV (per-frame replicant pos/vel/speed/legState/legFrm + master twin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EOF
2026-07-14 10:50:39 -05:00
arcattackandClaude Fable 5 a3b735d5da MP: replicant motion chop ROOT-CAUSED + FIXED -- stale peer-mirror spammed re-base
records; also the 2007 call-counter clock stub + two crash fixes

User-reported: peer mechs turn/move choppy ("missing frames"). Measured per-frame
(BT_REPL_HDG): 13% of frames the replicant's heading STALLED, 11% it JUMPED 2-6x.

THREE layered defects found:

1) THE CLOCK STUB (engine substrate): TIMESTUB.cpp's GetRTC/GetHiRes were 2007
   `return time++` call-counters -- the "clock" advanced per CALL, not per ms --
   and TIMESTUB won the /FORCE duplicate-symbol race over the REAL QPC clock in
   L4TIME.cpp (LNK4006). Every Now()-domain consumer (dead-reckon above all) ran
   on call-count pseudo-time. Removed TIMESTUB from the build; L4TIME covers every
   symbol. [The real clock alone did NOT cure the chop -- but it was objectively
   broken and un-gated the two latent bugs below.]

2) TWO CRASH FIXES the real clock exposed:
   - legAnimationState@0x3b0 never ctor-initialized (the task-#56 0xCDCDCDCD
     family): the type-3 writer re-dispatches SetBodyAnimation(legAnimationState)
     on the WRITER; a record emitted before the leg SM's first tick passed raw
     0xCDCDCDCD as a clip index -> AV (cdb-pinned, mech2.cpp:233). Init 0.
   - Replicants were SERIALIZING update records: the tail WriteSimulationUpdate ran
     for every instance, and the port's replicant leg-SM accommodation (task #50)
     calls ForceUpdate -> replicants emitted derived/uninitialized state into the
     stream. Master-gated; replicant marks discarded (master-authoritative).

3) THE CHOP ITSELF: the master's resync send-gate compares localOrigin vs
   projectedOrigin -- the PEER-ESTIMATE mirror -- but the port master never
   maintained projectedOrigin (the bring-up drive replaced Mover::Perform; the
   engine only updates the projection inside replicant-only DeadReckon). Stale
   mirror -> |local-projected| > deadband EVERY frame -> a type-4 re-base record
   EVERY frame -> the replicant hard-copied the master's deadband-quantized
   heading each frame (the engine lerp never engaged; nextUpdate always behind
   till) -> stall/snap beat = the chop. FIX: advance the mirror each frame by the
   last-SENT angular velocity (what the peer is extrapolating) and re-base it in
   the type-4 writer. Records now flow only on TRUE drift (~1 per 5 frames in a
   steady spin), the replicant extrapolates smoothly between them, and the lerp
   horizon finally engages. Measured: STALLS 13%->1%, JUMPS 11%->1% (residue: tiny
   sub-degree backward corrections on record arrival -- inherent dead-reckon
   overshoot the lerp absorbs).

Diag probes: BT_REPL_HDG (per-frame replicant heading + dead-reckon internals),
BT_REPL_TRN (replicant turn/leg state). scratchpad/clockcrash_bp.txt (cdb).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:08:47 -05:00
arcattackandClaude Fable 5 992aedece8 Gait: FIX replicant trn regression -- gate master-perf turn-stop exit to masters
Today's authentic trn dispatcher folded the master-perf turn-STOP exit into leg/body
case 4.  But the master perf (FUN_004a9b5c) runs on MasterInstance mechs, NOT
replicants -- and a replicant derives turnDemand from the noisy REPLICATED yaw rate
(mech4.cpp:1968), which dips into the +-0.05 deadband between dead-reckon updates.  So
the exit kicked the peer out of trn every few frames -> the peer 'rotated as a statue'
+ jerky (user-reported regression from yesterday).  Gate both turn-stop exits to
GetInstance() != ReplicantInstance; the replicant advances the trn clip from its
replicated turn signal exactly as before the exit was added.  Also BT_REPL_TRN probe
(mech4.cpp) logs the replicant yawRate/turnDemand/legState.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 09:11:22 -05:00
arcattackandClaude Fable 5 530bbeb959 Fix BT_SPAWN_AT terrain embed + add BT_COOL_LOG probe
BT_SPAWN_AT teleport set only X/Z, keeping the old (dropzone) Y -- so teleporting to
a new spot embedded the mech in sloped terrain (user-reported).  Lift the teleport
+500 above the old elevation so the authentic per-frame ground SNAP (BoxTree
FindBoundingBoxUnder, no gravity) settles it onto the surface next frame (the snap
only lowers, so it must start above the surface).  Diagnostic only.

Also BT_COOL_LOG (heat.cpp): logs a heat subsystem's damageLevel/heatLoad/coolantDraw
when it carries damage -- for the 'do damaged mechs leak coolant' investigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:46:00 -05:00
arcattackandClaude Fable 5 fcaea1862a Diag: BT_AF_PERIOD=<sec> autofire cadence throttle + afc_dump.py (raw RES verify)
Closes the coolant/jam investigation. BT_AF_PERIOD pulses the trigger every <sec>s so
a slower-than-max fire rate can be tested; afc_dump.py reads the AFC100 record raw from
BTL4.RES. Both confirm the QA report is FAITHFULLY reproduced (see KB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 07:26:12 -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 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 Opus 4.8 a9c0d2c854 Combat: FIX splash mis-routed onto the direct victim via the msgmgr (task #62)
User: struck a mech ~4x, still standing -- but the [dmghit] trace showed the
DIRECT victim taking 16 applications for 8 impacts while the intended splash
bystander took 0.  Root cause: BTApplySplashDamage delivered splash through the
shooter's SubsystemMessageManager::AddDamageMessage, which CONSOLIDATES every
damage message of a frame onto the FIRST hit entity (commonDamageInformation.
entityHit, messmgr.cpp:279).  The direct hit registered the primary as the common
entity, so the bystander's splash was consolidated onto the primary too -- the
excluded direct victim took 8 direct + 8 mis-routed splash = 2x, and the real
bystander took nothing.

Fix: deliver splash with a DIRECT e->Dispatch to each victim, matching the T0
source (EXPLODE.cpp:246 target_entity->Dispatch); Dispatch reroutes cross-pod for
a replicant on its own.  Verified (BT_DMG_LOG): 8 impacts -> 8 dmghit on the
direct victim + 8 on the bystander (was 16/0); no death; per-hit 5.83.

KB: combat-damage.md warns not to route splash through the consolidating msgmgr.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:10:51 -05:00
arcattackandClaude Opus 4.8 fa0b825b60 Combat: FIX missile direct damage 6x over-application -- damage once per salvo (task #62)
User live regression: missiles killed a mech in ~2 shots ("takes way more than 2
normally"). Root cause -- DamageZone::TakeDamage (arcade @0041e4e0 == WinTesla
DAMAGE.cpp:379) is `damageLevel += amount*scale` and IGNORES burstCount. So the
arcade's ONE cluster Missile per trigger lands its damageAmount EXACTLY ONCE; the
missileCount/burstCount split is cosmetic for zone damage (only gyro-bounce +
splash-falloff read burstCount). The port re-expresses that one cluster as N
flying rounds and was damaging on EVERY round = ~missileCount x too lethal on the
direct hit (mirrors the splash over-application fixed in 145a69f).

Fix (mislanch.cpp FireWeapon): only the salvo-LEAD round (i==0) carries damage
(damageData.damageAmount) AND the cluster splash; the other N-1 rounds are VISUAL
(damage 0) -- the tracer ripple, no extra damage. AC unaffected (one round/shot,
not a cluster).

Verified (BT_DMG_LOG [dmghit] trace): per-hit missile damage = clean 5.83, burst
1, once. Missiles-ONLY (BT_AF_MISSILE now independent of BT_AUTOFIRE), single
enemy, 45s: 8 damaging hits, top zone reaches only 0.18, NO death (was death in
~3 pulls) -- "way more than 2" restored. Earlier test deaths were BT_AUTOFIRE
firing laser+PPC+missiles combined, not missiles alone.

KB: combat-damage.md documents burstCount-ignored + the N-round cluster trap
(direct + splash both once-per-salvo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:01:44 -05:00
arcattackandClaude Opus 4.8 145a69f865 Combat: FIX missile splash over-application -- once per salvo, not per round (task #62)
Live regression: a clustered bystander died in ~2 missile salvos ("suddenly
lethal"). Root cause, not authentic: the arcade fires ONE cluster Missile per
trigger (burstCount=missileCount) doing ONE SplashDamage event with baseBurst =
missileCount, floored at 1 ONCE. The port re-expresses that cluster as N flying
BTProjectile rounds, and task #62 fired splash PER ROUND -- each baseBurst=1,
each floored at 1 -- so the distance floor was applied N times = ~missileCount x
too much splash.

Fix: BTProjectile.splashBurst tags ONLY the salvo-lead round. MissileLauncher::
FireWeapon passes nmiss (the cluster count) on i==0 and 0 on every other round;
the contact + world-impact splash hooks fire only when splashBurst>0, using it as
baseBurst. One splash event per salvo with baseBurst=missileCount -- matches the
single arcade cluster missile. Replicant mirror rounds carry 0 (damage 0 too) so
the master's cross-pod splash isn't doubled. AC unaffected (not a MissileLauncher;
splash_burst defaults 0).

Verified headless (BT_SPAWN_ENEMY=2 clustered rig, 45s): bystander now takes 6
splash events (1/salvo, baseBurst=6) and SURVIVES (was ~2 shots); primary dies to
direct hits in ~3 trigger pulls (36 missiles == single-enemy TTK); AC does not
splash; no crash. Also: BT_SPAWN_ENEMY read as a COUNT (=2 clusters a bystander
within SplashRadius) for eyeballing splash; entity-id logging on [enemy]/[splash].
KB: combat-damage.md documents the N-round cluster trap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:43:50 -05:00
arcattackandClaude Opus 4.8 12fbc023a8 Combat: MISSILE SPLASH DAMAGE -- Explosion::SplashDamage reconstructed (task #62)
Area-of-effect blast on missile detonation, from the T0 source EXPLODE.cpp:50-254
(@0042fad0) + the arcade decomp. Fires on ANY missile impact (world or mech) --
the +0x360 gate sits in the Missile::Perform collision branch (part_013.c:10097).
Only missiles splash; the AC's tracer is not a Missile.

Radius source CORRECTED: it is the ROUND's own GameModel (type-0xf) +0x50, seeded
from the launcher's linked AmmoBin ammoModelFile @0x1e8 (part_013.c:8778) -- NOT
the launcher's ExplosionModelFile (which resolved to 0). Live-resolved radius = 30
for both MP missile launchers; 0 for AC/Emitters.

Burst falloff = baseBurst / dist^exp floored at 1 (arcade 1.25 = decomp 0x3ff40000;
WinTesla EXPLODE.cpp:209 drifted to 1.2f). damageType/amount pass through unchanged;
only burstCount, radial damageForce, and impactPoint are set. Excludes shooter +
direct victim; dist>radius gated; delivered via msgmgr (cross-pod) or Dispatch.

New: BTResolveSplashRadius / BTApplySplashDamage (mech4.cpp), hooked at both the
world-impact and contact detonation paths; BTAmmoRoundModelResource bridge
(ammobin). Env: BT_SPLASH_LOG, BT_SPLASH_TEST (synthetic near-miss), BT_AF_MISSILE
(missile autofire). Verified: near-miss dist=15 -> 1 burst to a bystander; live
missiles detonate + exclude the direct victim; AC does not splash; no crash.

Deferred (T3): per-player enable sub-gate missile+0x360 = BTPlayer+0x264 (writers
read as a per-frame toggle, not a config flag) -- port treats SplashRadius>0 as the
enable. KB: combat-damage.md + open-questions.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:06:24 -05:00
arcattackandClaude Fable 5 4c54f7ef0c Ballistic FX: DAFC muzzle flash + autocannon fire replication (task #61)
The autocannon gains its authentic muzzle effect AND becomes visible when an
enemy fires it -- the ballistic-FX gap behind "I only see the AC from my own
view."

MUZZLE FLASH (the genuine shipped effect, not the cut card):
- MUZFLASH.BGF is an orphaned/cut asset (nothing references it) -- so it is NOT
  rendered.  The shipped projectile-gun muzzle effect is DAFC.PFX, which
  BTDPL.INI documents as "the effect used on all projectile guns" (psfx 6
  external / 14 internal): an orange fire-smoke blast (btfx:firesmoke1),
  maxIssue 25 over ~0.2s so the emitter auto-expires = one burst per shot.
- BTFlashMuzzle (mech4.cpp) spawns it on the gun-port SEGMENT via the existing
  BTStartPfxAttached path; the segment frame sprays -Z out the barrel.  Hooked
  at ProjectileWeapon::FireWeapon (the fire edge).  Default ON (BT_MUZZLE=0
  disables).  AC only -- lasers show their beam, missiles their launch.

ENEMY AC FIRE NOW REPLICATES (the real find):
- ROOT CAUSE: the subsystem-record replication channel EXISTS and works (mech
  ticks subsystem->PerformAndWatch(update_stream); Entity::UpdateMessageHandler
  routes incoming records to GetSimulation(subsystemID-1)->ReadUpdateRecord).
  The emitter (beam) and MissileLauncher (salvo mirror) both call ForceUpdate()
  so their fire serializes -> enemy lasers + missiles ARE visible on the peer.
  The AUTOCANNON set only `simulationFlags |= 0x1` (the +0x28 instance flag,
  NOT the updateModel bit WriteSimulationUpdate walks) and had NO fire record,
  so its shot never crossed the wire -- the enemy's cannon was invisible.
- FIX (the AC twin of the missile salvo mirror): ProjectileWeapon::
  WriteUpdateRecord/ReadUpdateRecord (fire counter + aim) + ForceUpdate() in
  FireWeapon.  The replicant edge-detects the counter and mirrors ONE visual
  round + DAFC muzzle flash from its own resolved muzzle; a null/untargeted aim
  streaks straight out the barrel (launchVelocity) instead of toward origin.
- Verified live 2-node: the watching node logs REPLICANT AC shots + DAFC
  flashes at the enemy's gun-port (seg 7), aimed shots fly to the real target;
  no crashes.

TRACER: all ACs author TracerInterval=1 (every round is a tracer); the existing
amber streak is acceptable-authentic, so no tracer change was needed.

KB: open-questions.md -- CORRECTED the (wrong) earlier note that claimed no
subsystem-record channel exists; it does, the AC just wasn't using it.  Logged
the methodology lesson: the coverage audit finds UNWRITTEN functions, not
"reconstructed but inert" ones (a function present but never called -- the AC
record + the missile mirror both looked done); a LIVENESS audit would catch
that class.

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