Commit Graph
306 Commits
Author SHA1 Message Date
CydandClaude Fable 5 230fc50f58 Cockpit: green MFD phosphor tint + 4-above/4-below button strips
- MFD tint 0xFFFF (white) -> BT_MFD_GREEN (0x07E0, pure green in R5G6B5)
  for the five mono MFDs, matching the pod's green monochrome phosphor
  screens and RP412's FillSplitMFD (green<<8 == 0x00FF00).  Applied to
  both the cockpit (kBTCockpitSurfaces) and the dev composite
  (kBTGaugeSurfaces); the radar/secondary stays palette-colour (it is
  authentically multi-colour, not mono).
- Button strips are now 4 ABOVE + 4 BELOW each MFD (the authentic pod
  layout), not 4+4 stacked to one side.  The MFD surfaces are inset by
  kBtnStripH (top MFDs down, bottom MFDs up) so both strips land
  on-canvas; the same constant drives the surface inset and the button
  builder so they stay aligned.

Verified: L4MFDSPLIT cockpit renders green MFDs with 4+4 button strips;
BT_PLATFORM=pod renders identically on a dev box (the pod's true
multi-surface path needs >=2 adapters and stays guarded off).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:53:22 -05:00
CydandClaude Fable 5 99ea3f07e9 Cockpit: on-screen vRIO button banks around the MFDs (clickable, lamp-lit)
Each MFD carries its 4+4 physical button strip and the radar its 6+6
amber side columns, exactly as mounted on the pod (RP412 MFDSplitView
geometry + the shared Tesla-board RIO addresses; anchors 0x2F/0x27/0x37/
0x0F/0x07 for the five MFDs, 0x10/0x18 for the radar).

- L4VB16.cpp: BTBuildCockpitButtons computes the button rects from the
  same cockpit surface layout (strips toward the screen interior to stay
  on-canvas); BTDrawCockpitButtons draws them as coloured quads, lit from
  PadRIO::GetLampState (bright red/amber) or dim; state-block wrapped so
  the button pass doesn't leak render state into the next world frame.
  BTCockpitButtonAt hit-tests; BTPadRIOScreenButton bridges to PadRIO.
- btl4main.cpp WndProc: WM_LBUTTONDOWN/UP hit-test the buttons and inject
  press/release into PadRIO (SetCapture so the release always fires).

Verified: the button banks render around all six surfaces in the
L4MFDSPLIT cockpit; mouse plumbing wired through the already-active
PadRIO screen-button path (Phase 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:46:57 -05:00
CydandClaude Fable 5 0c964c0ee7 Cockpit: 1920x1080 single-window MFD arrangement (L4MFDSPLIT=1, RP412 layout)
The RP412 single-window cockpit, adapted to reuse BT's working D3D
surface compositor (DrawDevSurface) instead of RP's GDI child-window
panes -- same visual outcome, far lower risk (no L4VIDEO present-routing
rewrite):

- L4VB16.cpp: kBTCockpitSurfaces places the six instrument surfaces at
  RP412's exact 1920x1080 fractional positions (Heat=UL, Mfd2=UC,
  Comm=UR, Mfd1=LL, Mfd3=LR, radar=center-bottom portrait);
  BTDrawGaugeSurfaces parameterized by surface table; BTDrawCockpitPanel
  composites them over the full-window 3D as the pod bezels frame the
  viewscreen. Hooked in L4VIDEO before EndScene (L4MFDSPLIT-gated).
- btl4main.cpp: L4MFDSPLIT sizes the window to the 1920x1080 canvas
  (scaled to fit the work area; -res overrides) and wakes the gauge
  renderer (L4GAUGE) + PAD controls.

Fixed two more pre-existing latent gauge crashes that only fire when the
gauge renderer is woken off-pod (would hit the real pod too), previously
gated only on BT_DEV_GAUGES -- extended the safety nets to L4MFDSPLIT:
  * gauge.h: NULL gauge-attribute-source binds a static zero instead of
    AV'ing in GaugeConnectionDirectOf ctor (crashed building HeatSink
    cluster's VertTwoPartBar at ConfigureForModel).
  * GAUGE.cpp: SEH-guarded gauge Execute (disable a faulting gauge, not
    crash). GAUGREND.cpp: skip unresolved primitives + activate all MFD
    pages so every surface renders.

Verified: L4MFDSPLIT boots the cockpit; all six surfaces render framing
the 3D viewscreen; survives coolant-loop + gauge activation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:39:41 -05:00
CydandClaude Fable 5 2bb9b602bb KB: record the CoolingLoopConnection databinding-trap AV fix (gauges-hud)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:46:50 -05:00
CydandClaude Fable 5 cefbc3a686 Gauge: FIX CoolingLoopConnection AV -- guard the databinding-trap raw reads
BT_DEV_GAUGES crashed a few ticks in: CoolingLoopConnection::Update
(btl4gau2.cpp) walks the 1995 binary's plug->link->subsystem chain via
raw +8 offsets, but our reconstructed subsystem layout is not
byte-identical, so an intermediate 'link' comes out non-null-but-garbage
and *(link+8) AVs.  Fires the moment a mech's coolant loop goes active
(source+0x134==1) -- so it would crash the REAL POD too, not just the
dev composite; the brief earlier gauge runs just never activated the
loop.  Pre-existing (byte-identical across the BT411 merge), not a merge
regression.

Guard each raw deref (BTPtrReadable via VirtualQuery) so an invalid
chain resolves to the authentic 'no source' branch (0) instead of
faulting. [T3] -- proper fix is named Plug/Link accessors in a
complete-type TU; logged in the KB.

Verified: BT_DEV_GAUGES now survives coolant-loop activation and the
full cockpit MFD suite renders (Heat / weapon-status MFDs / radar /
Comm pilotList).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:46:29 -05:00
CydandClaude Fable 5 503536f1b3 Merge BT411 audio-fidelity + combat work (4e72f0c..abed41e) into BT412
Brings the post-fork BT411 line forward via a local-path merge (never
touches the BT411 gitea remote): the full audio-fidelity system (engine
AUD*/L4AUD* + audiopresets.cpp + ~600 content wavs + AUDIO_FIDELITY.md),
missiles/rear-fire/HUD/gyro/gait tasks (#66-68), FOGDAY.EGG, and
refreshed context docs -- 91 commits, ~688 files clean.

Only 5 files overlapped the steamification; resolved keeping BOTH:
- L4NET.CPP: took BT411's task-#50 fix (don't close the game listener on
  console loss) over the seam's adaptation of the old buggy close; sends
  stay on NetTransport_Get().
- L4NETTRANSPORT.cpp: folded BT411's TCP_NODELAY latency fix into
  WinsockNetTransport::Connect (the seam already had retry + nonblocking).
- mechmppr.cpp: combined the device_owns_input gating with BT411's
  task-#68 look-behind, gating the lookBehind write too.
- .gitignore / CMakeLists.txt / mech4.cpp: trivial / auto-merged
  (deviceOwnsInput gating preserved).

Verified: clean build (default + implicitly the Steam TU untouched);
solo front-end mode; loopback MP through the seam (mesh completes, both
tick, replication works, no NODELAY warnings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:34:54 -05:00
arcattackandClaude Opus 4.8 abed41e711 gitignore: __pycache__
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:07:37 -05:00
arcattackandClaude Opus 4.8 224092aaff KB: reconcile the context system with the day's work
- decomp-reference.md: the binary ATTRIBUTE-TABLE section -- 16-byte
  {id,name,off+1,0} row format (alignment warning), the walker technique,
  and the three recovered tables (Mech ids 21-56 incl the corrected
  EyepointRotation@0x360 / RearFiring@0x410 / DistanceToMissile@0x400
  labels; HeatSink 3-12; Torso 3-15; the weapon RearFiring 'b' marker).
- combat-damage.md: the REAR-FIRE + look-view system (roster survey: the
  Blackhawk's ERMLaser_2/3 are the game's only rear weapons; every missile
  rack forward), the missile mount-frame launch truth, and the BANKED
  full Missile flight-model decode (three performances, proximity fuse).
- wintesla-port.md: post-Phase-4 closures (instability model live, F14
  static filter baked, the footstep warm-up bug + its trace-cap lesson);
  deferred list trimmed to F21/HRTF.
- AUDIO_FIDELITY.md: status block -- F14 FIXED, warm-up bug noted.
- open-questions.md: "which mechs used rear fire" answered from data;
  remaining look-view key bindings noted.
checkctx: CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 18:07:15 -05:00
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 c581553a6c Audio (AUDIO_FIDELITY F14 residue): bake the authored static resonant
low-pass into the zone WAVs

232 zones author initialFilterFc(8)/initialFilterQ(9) (SBK 0..127) -- the
fixed resonant low-pass the EMU8000 applied in hardware; the port played
them unfiltered (flagship: the LaserLoaded charge hum, fc=57 Q=87, much
brighter/harsher than the arcade).  The extractor now applies an RBJ 2-pole
low-pass per zone: cutoff = the AWE NRPN curve (100 + fc*7900/127 Hz),
resonance = SBK Q -> 0..+12 dB peak [T3 curve, endpoints exact], designed at
the zone's baked rate so note-60 playback reproduces the hardware's absolute
cutoff (pitch-shifted notes carry the filter -- same limitation as the
rate-baked tuning).  Synthetic sweep verified: flat lows, +5.4 dB at the
authored 3.6 kHz cutoff, -22 dB @10 kHz, -46 dB @14 kHz.

Whole bake pipeline (filter -> attenuation) now runs in float with ONE int16
conversion + peak normalization: an int-per-stage draft hard-clipped 165 of
232 zones (resonance overshoot, worst 3% of samples); now only pre-existing
source-material clipping remains (14 files).  160 WAVs re-baked; the preset
table is unchanged (no engine rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:36:44 -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 a8f14e1c24 KB: audio Phase 3+4 status -> AUDIO_FIDELITY.md + wintesla-port.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:17:21 -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 21378ec132 Audio Phase 4b (AUDIO_FIDELITY F16): the torso-twist servo whir -- Torso
publishes its real attribute table

Recovered the binary Torso attribute table [T1]: dense ids 3..15 from
MechSubsystem::NextAttributeID (HeatWatcher/PowerWatcher publish nothing --
binary parity), all 13 rows land on already-reconstructed members:
RotationOfTorsoVertical/Horizontal @0x1E4/0x1D8, HorizontalLimitRight/Left
@0x1DC/0x1E0, SpeedOfTorsoVertical/Horizontal @0x1EC/0x1E8 (the |rate| the
binary abs's -- our derive already did), StickPosition @0x1F0, TorsoUp/Down/
Left/Right/Center @0x1F8..0x208, MotionState @0x20C (statusFlags; the binary
writes 2 on the limit-hit frame -- the authored ==2 matcher is the twist-stop
clunk, settling the audit's [T4] guess).

Torso's AttributeIndex was default-constructed EMPTY -- TorsoTwistInt01/
Ext01/Stop01 were unreachable.  Now the authored chain (pitch -200..+200
cents over twist speed 0.5..0.9, start/stop gate at 0.25, stop clunk on
MotionState==2) drives them unchanged.

Regression (25s): stable; all three bind real; **attrnull count = 0 -- every
authored audio attribute in the game now binds a real member.**

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:03:38 -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 0ca7d269d2 Audio Phase 3 (AUDIO_FIDELITY F9/F11/F12): EFX lowpass + reverb split + cockpit placement
New L4AUDEFX bridge (OpenAL Soft ALC_EXT_EFX): one EAXReverb aux slot at the
authentic AUDIO.INI global_reverb_scale (0.3) + a scratch AL_FILTER_LOWPASS
(params copied at attach).

F9 filters (was: computed then thrown away -- everything full-bright at all
distances): Dynamic3D ExecuteModel drives GAINHF from highFreqCutoffScale x
brightnessScale x maxMIDIFilterCutoff, UNGATED (decomp part_008.c:7496,
7589-7604 -- every moving 3D sound dulls with distance per the AUDIO.INI
knee-60/exp-2.0 model); Static3D from brightness x max (:7831-7884); Direct
inside its existing gated NRPN-rate block.  AWE 100-8000 Hz curve -> EFX
5 kHz-reference gainhf via a 2-pole approximation [T3 curve, endpoints exact].

F11 reverb (was: bone-dry everywhere): 3D patch sources attach an aux send at
Start, exactly where the original sent CC91 = global_reverb_scale
(part_008.c:7278-7394); Direct cockpit sources keep CC91=0 -- dry.  The
wet-exterior vs dry-cockpit contrast is back.

F12 placement (was: every cockpit sound dead-center): DirectPatchSource
Start places sources by the authored 6-value position enum (front/rear card
+ pan CC10, decomp @00463848/@004638a8) as listener-relative directions,
composed with the zone L/C/R pan.  New PatchResource GetBankID/GetPatchID
pass-throughs (the LOD accessor is protected).

Regression (35s drive+fire): EFX READY, stable, deliveries unregressed, no AL
errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:59:39 -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 691e88569a Audio Phase 2 (AUDIO_FIDELITY F1/F2/F13/F14): full-zone soundbank -- key-splits,
layers, baked tuning, loop regions, release fades

Extractor rewrite (tools/sf2extract.py): every sample-bearing instrument zone
becomes a SAMPLEINFO slot -- 603 zones / 241 presets (68/115 + 94/126 multi-
zone, matching the audit exactly).  Per zone: keyRange(43), sampleModes(54),
SBK samplePitch(55) + rootKey(58) + coarse/fineTune(51/52), attenuation(48,
INVERTED SBK scale, 0.375 dB/step [T3], baked into the PCM), releaseVolEnv(38),
pan(17), shdr loop region.

F2 pitch (algebraically exact): WAV rate = round(44100 * 2^(((6000 -
rootCents) + tune)/1200)) -- EMU8000 v1 base 44100.  Cross-checks: FootFall
17300 Hz (the audit's +4.2 st), MissileLoaded low zone 88200 (-24 st),
Warnings01 8-way klaxon split w/ 3 looped zones, Death01 162 Hz extremes.

F1 zone selection: SetupPatch/PlayNote take the authored note;
attach/play only zones whose [keyLo,keyHi] contains it (detach the rest so a
rewound source can't replay a stale buffer).  Live-verified: LaserLoaded/
MissileLoaded note 36 -> low clunk zone, note 84 -> high blip zone (pitch 4);
LaserCFire plays all 3 authored layers incl the looping sustain.  Stereo-pair
zones pan via listener-relative AL_POSITION (distance model is AL_NONE).
MAX_PRESET_SAMPLES=25 (AllExplosion); fixed PRESET_isImplemented's >=5 bound.

F13 loops + releases: authored [loopStart,loopEnd] applied via
AL_SOFT_loop_points at buffer load (0 rejections; kills the latent boom-loop
on MechExplosion's 1.5% sustain slice + the ~2.4 Hz LaserBSustain wrap tick);
StopNote now honors the authored releaseVolEnv (1.1-3.9 s on ~20 looping
presets) with a dB-linear fade serviced from AudioHead::Execute; restarts
reclaim fading sources.  One-shots keep the instant stop (faithful).

Regression (40s drive+fire): stable, loop points accepted, key-splits + layers
verified in the delivery trace, chirp still dead, footfalls fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:04:34 -05:00
arcattackandClaude Opus 4.8 fc7f311771 Audio Phase 1 (AUDIO_FIDELITY F3/F4/F10/F15/F18/F20 + KB): the quick wins
F3 distance: alDistanceModel(AL_NONE); restored the commented authored
  distance multiply in Dynamic3D::CalculateSourceVolumeScale, added the same
  override to Static3D, dropped the (now inert) AL_MAX_DISTANCE writes.  Far
  battle audio follows the authored knee/rolloff curve again, and the volume
  cull / voice steal / ducking chains are distance-aware.
F4 volume law: AL_GAIN = volume_scale^2 at all 3 per-frame sites (Direct/
  Dyn3D/Static3D) -- the GM CC7 squared curve; linear was ~+6 dB at mids.
F10 doppler: alDopplerFactor(0) (AL's model ran wrong constants + sign-
  inverted velocity: approaching sources pitched DOWN); the AUTHORED
  dopplerCents (AUDIO.INI range/speed-of-sound model, decomp-proven consumer
  part_008.c:7466) now adds into the Dynamic3D pitch chain.
F15 ConfigureActivePress: published at the MechSubsystem BASE (binary id 2,
  descriptor @0x50de5c -> +0x110); renamed the misnamed vitalSubsystemIndex
  member (the weapon ConfigureMappables handler already drives it 0/-1).
  Removed the invented Sensor/Myomers duplicates and RESTORED their byte-
  exact layouts (0x328/0x358 allocs + asserts).  MechWeapon's pinned-id pad
  absorbed the +1 chain shift -- which matches the binary's own numbering.
F18 cook-off warning: AmmoBin FireCountdownStarted -> the existing
  cookOffArmed @0x18C (binary table @0x512600); the countdown klaxon can fire.
F20 zoom blip: L4MechControlsMapper publishes TargetRangeExponent -> the live
  @0x1a4 zoom member (own table chained to MechControlsMapper).
KB: replaced the bogus divisionParameters+0x10 rate read with
  SystemClock::GetTicksPerSecond() (FUN_0044e19c is GetFrameRate -- original
  audio frames were CLOCK TICKS).

Regression (35s drive+fire): stable; ConfigureActivePress binds real on all 9
subsystems (idle -1, zero pad redirects); FireCountdownStarted +
TargetRangeExponent bind live members; attrnull 53 -> 41; chirp still dead;
footfalls still fire (gain now correctly squared).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:43:38 -05:00
arcattackandClaude Opus 4.8 05cb3f5549 Docs: AUDIO_FIDELITY.md -- multi-agent fidelity audit, 37 verified findings (task #50)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:14:06 -05:00
arcattackandClaude Opus 4.8 15d4278b6f Audio: run-speed footfall silence is AUTHORED -- the interior fade curve (task #50)
The 'footsteps disappear when running' report traced to the authored interior-
footfall volume chain: an AudioScalarScale on LocalVelocity with the INVERTED
curve aB=[0,20] -> cB=[1,0] (through a smoother) drives the FootFallInt source
volume -- full at standstill, fading with speed, ZERO at >=20 (run).  The
drop-gate evaluation (CalculateSourceVolumeScale -> ExecuteWatchers) pulls this
authored value and overrides the game-side mixer feed, so run-speed footfalls
are dropped BY DESIGN: at running speed the pod cockpit is engine roar; the
1995 sound design fades the interior footfall out.  Timeline evidence:
volset(1,1 from the mixer) -> matchfire Start -> volset(0 from the authored
chain during drop evaluation) -> DROP, exactly once per stride at speed >= 20.

No code change -- this commit records the finding.  User-verified state:
chirp gone, per-step static gone (velocity smoothing), footfalls audible at
walk, authentically absent at run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 09:12:56 -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 b8908b965e Audio: chirp ACTUALLY killed -- 9 configure-ticker triggers, typed -1 pad (task #50)
The previous 'chirp killed' verification was WRONG: it checked the 1/s playing
snapshot (which misses 0.12s pulses) instead of delivery counts -- the run had
186 ProgramButton deliveries.  Corrected metric + trigcfg now prints its target
component, revealing NINE thresh=-1 Start triggers on the configure-ticker
sequence: ConfigureActivePress on Avionics + Myomers (backed real, -1) AND
SEVEN more subsystems still on the inert pad (0 = "button 0 held" -> ticker on).

Fix: a TYPED pad in the missing-attribute redirect -- ConfigureActivePress
resolves to a shared static int = -1 (the authored none-held idle) instead of
the zero pad; every other missing attr keeps the zero pad.  Covers all
configurable subsystems at once with no layout changes; self-clears per
subsystem as each gets a real driven member.

VERIFIED BY DELIVERY COUNT: ProgramButton01 deliveries = 0 (was 31-186/run);
7 attrs took the -1 pad; the rest of the soundscape intact (weapon cycles,
loops, translocate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:55:17 -05:00
arcattackandClaude Opus 4.8 be626c71ce Audio: KILL THE CHIRP -- ConfigureActivePress backed real (idle -1) on Sensor+Myomers (task #50)
The obnoxious always-on clicking was the cockpit CONFIGURE-MODE ticker: an
authored looped sequence gated by an AudioIntegerTrigger with threshold -1 on
<subsystem>.ConfigureActivePress (the held-configure-button index; -1 = none).
The attribute was one of the inert-pad redirects -- the pad reads 0 = "button 0
held forever" -> the ticker Start-fired at load and pulsed 2.7/s eternally.
Found via [sendcfg]/[seqstart] traces: the first seq start immediately follows
the trigcfg(thresh=-1, onID=Start) construction on the pad address.

Fix: real int configureActivePress = -1 APPENDED to Sensor ("Avionics") and
Myomers, registered in their attribute tables.  Both classes are byte-exact
factory allocs, so the sizeof locks AND the placement-new alloc constants are
bumped TOGETHER (Sensor 0x328->0x32C, Myomers 0x358->0x35C) -- the tripwire
caught a mid-layout insert (seekVoltage@0x330 shift) and a fixed-alloc overrun
before they shipped; member moved to the true tail.  Wiring the live value
(EnterConfiguration/ExitConfiguration sets/clears the index) restores the
authored hold-to-configure tick later; idle -1 is the correct silent state.

VERIFIED: ProgramButton01 plays ZERO times (was the 2.7/s chirp);
ConfigureActivePress binds real (vtbl=FFFFFFFF = the -1); FootFallInt at 0.98
gain; stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:47:52 -05:00
arcattackandClaude Opus 4.8 847b9b85cd Audio: target the step-intensity send at FOOTSTEP sources only (task #50)
The output-filtered broadcast still fed EVERY volume/brightness mixer -- the
always-on ambience pulse's own volume mixer got cranked to step intensity on
every stride ('a stuck button', 3/s, loud).  The send now identifies footstep
sources by authored patch (FootFallInt/Ext: bank2 37-39, bank1 106/107) through
mixer->GetTargetComponent() -> AudioSource -> PatchResource LOD bank/patch
(new accessors: GetTargetComponent, GetBankID/GetPatchID,
AudioResource::PeekAudioLevelOfDetail).

Also: step intensity curve raised (speed/25, floor 0.55 -- a mech stomp is
never subtle; it was firing at ~0.4 gain and masked under 0.9-gain layers,
heard as an unrecognizable 'orchestral hit').

Verified: FootFallInt at 0.78 gain in the live tape; the ambience pulse back
to its brief authored form (absent from 1/s snapshots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:36:28 -05:00
arcattackandClaude Opus 4.8 37d4554440 Audio: implement NOTE->PITCH + control pitch -- the AL_PITCH was never applied (task #50)
The whole pitch system existed but was discarded: NoteAudioHandler captured the
MIDI note, ExecuteModel computed relativePitch from the control-chain cents --
and NOTHING ever called alSourcef(AL_PITCH).  Every sample played at root.

Consequences fixed:
- The load-time 'chirping' ambience: an authored looped sequence (div=120
  tempo=160, note 51 events on bank1:83) plays a rhythmic tick pitched ~9
  semitones DOWN; at root it was a high click ('chirp').  Identified live via
  the new BT_AUDIO_DUMP playing-source tape + user ear-match at cadence.
- Engine now revs: EngineMotor pitch rides the throttle (measured 1.0 -> 1.12).
- All sequence-authored patterns (alarms/klaxons/ambience) regain their pitch.

Implementation: BTNotePitchFactor(note) = 2^((note-60)/12); applied at all 3
patch-source StartImplementations (fresh attach) and all 3 ExecuteModels
(combined with the control-chain relativePitch, clamped 0.5..2.0 as before).
Uses the existing AudioSource::GetCurrentNoteValue().

Also: [seqcfg] sequence-config dump, g_bufferNames + BT_AUDIO_DUMP live tape
(1/s: every playing AL source with sample name/gain/pitch/loop),
tools/soundboard.py updated mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:20:42 -05:00
arcattackandClaude Opus 4.8 e017f257a5 Tools: soundboard.py -- click-to-play UI for the 241-sample soundbank (task #50)
Stdlib-only (tkinter + winsound) sample browser: filter box, per-button
[bank:patch] tags parsed from audiopresets.cpp so an ear-identified sound maps
straight back to the game's (bankID, patchID), Play-All sweep, STOP/ESC.
For verifying sample identity/pitch (the 22050 Hz extraction guess) by ear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:01:27 -05:00
arcattackandClaude Opus 4.8 e454e97617 Audio: fix step-broadcast misfire -- feed ONLY volume/brightness mix stages (task #50)
The blind foot-plant broadcast hit every entity-registered component; mixers
whose authored OUTPUT control is Start forwarded the send as a Start and fired
their sound on every stride (the per-step ProgramButton 'chirp').  Added
GetOutputControlID() to AudioControlMixer/Multiplier and the broadcast now
feeds only stages outputting Volume(3)/Brightness(5) -- the footstep loudness
inputs -- directly (mixers are entity-registered, no splitter hop needed).

Verified: FootFallInt01 delivers on BOTH gaits now (11/run, walk input included);
ProgramButton deliveries 188 -> 38 (the remainder are authored looped-sequence
cockpit ambience, not the broadcast); stable 28s drive+fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 07:54:36 -05:00
arcattackandClaude Opus 4.8 90f99fe4b5 Audio: FOOTSTEPS WORK -- reconstruct the lost game-side step-intensity broadcast (task #50)
The final link: the footstep source's volume + brightness mix from two
AudioControlMixer inputs (ctl 100 walk / 101 run) behind an entity-registered
AudioControlSplitter.  The object stream only ZERO-initializes those inputs
(AudioControlSend one-shot initializers; mixer ctor inits 0.0) -- the runtime
feed was BT game code (lost with the source), using the engine's game-facing
Entity::AudioSocketIterator broadcast (the only path to those anonymous
components; the splitter registers via entity->AddAudioComponent).

Reconstruction [T3]: on each foot plant (the SetBodyAnimation locomotion-clip
pulse), broadcast the step intensity (live ground speed on the authored
0..40 -> 0..1 velocity-curve family, floored at 0.35 to clear the 0.3
transient-drop gate) on the gait-appropriate input (run-family clips
0x0a..0x0f -> 101, walk family -> 100; the other input zeroed so gait
changes don't stack).  Patch-source components (VDATA 1001/1002/1005) are
skipped -- AudioSource::ReceiveControl Fail()s on input-range ids.

VERIFIED: FootFallInt01.wav (bank2 patch39) delivers + plays per stride;
mixer sums nonzero (vol=1 at run speed); 30s drive+fire regression clean --
weapon charge/fire/sustain cycles, ready dings, buttons, engine loops, state
audio (0 skips), gait advancing normally, no crashes, no stuck loops.

Also this session: [seqcfg] sequence config dump (decoded the authored event
streams: ctl 8=note/6=attack-vol/1=start/2=stop patterns), BT_AUDIO_NODROP
diagnostic (force low-volume transient delivery at a 0.7 floor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 07:47:48 -05:00
arcattackandClaude Opus 4.8 f4b2a7f87f Audio: fix POISONED calibration rate (raw-offset garbage) + calibrated audio clock (task #50)
Two real bugs found + fixed chasing the footstep volume:

1. BTL4Application::MakeAudioRenderer read sample_rate from a raw 1995-layout
   offset (divisionParameters+0x10) -- the databinding trap: it yielded
   -1.6e14 (measured), poisoning Renderer::calibrationRate for the ENTIRE
   audio system.  Every AudioTime conversion (sequence event scheduling,
   compression curve durations, Seconds_To_Frames) was garbage.  Sanity-clamp
   to the authored 1000 frames/sec default (BT_AUDIO_LOG logs the value).

2. AudioHead::Execute fed audioFrameCount raw OS ticks; now converts
   ticks -> calibrated frames via SystemClock::GetTicksPerSecond (1000ms
   fallback when the static isn't measured yet), so AudioTime consumers see
   the rate they were calibrated for.  With rate=1000 + ms ticks the units
   now align end-to-end.

Footstep status: chain verified through pulse -> matcher -> Start -> renderer;
the volume mixer's two inputs receive only value-0 AudioControlSequence events
even with correct timing -- the authored event VALUES need decoding next
(sequence tempo/divisions ctor dump; possibly Verify()-stripped tempo garbage
or the events are resets and the true volume rides another control id).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 00:31:55 -05:00
arcattackandClaude Opus 4.8 99757a947d Audio: footstep chain traced to the END -- gait VINDICATED; audio clock 18x unit mismatch found (task #50)
MAJOR RESULT -- THE GAIT IS CORRECT, the enum was mislabeled:
LoadLocomotionClips (binary-verified slot map) proves namedClip==&animationClips[5]:
clips 6/7 = wwr/wwl (WALK cycle), 12/13 = rrr/rrl (RUN cycle -- what autodrive
uses at full throttle), 10/11 = wrr/wrl, 14/15 = rwr/rwl, 16-21 = reverse set.
The mech2.cpp "MechAnimationState" enum (0x3c-stride name table) does NOT match
the runtime clip ids -- it is the mislabel (12/13 are NOT StandToReverse).
The authored AudioStateTriggers on AnimationState (statecfg dump: states
1,2,5,8,9,10,11,14,15,16,17,20,21,22,23,26,27 -> Start) align EXACTLY with the
TRANSITION clips under the true map; the CYCLING strides (6/7, 12/13) have no
state triggers because the FootStep pulse attribute serves them -- our FootStep
reconstruction is the authentic design.  DO NOT renumber the gait.

Footstep chain, fully mapped (all empirtical):
pulse -> AudioMatchOf(match=1) -> Start on the FootFall DirectPatchSource ->
DROPPED at volume 0.  Volume path: an AudioControlMixer (2 inputs, never fed)
<- an AudioControlSplitter <- AudioControlSequence timeline events.  The
sequences DO fire -- but only value-0 events (the tick-0 initializers); the
later (real-volume) events never land.

NEW ROOT-CAUSE CANDIDATE [T3]: audio clock unit mismatch.  The renderer is
constructed with DefaultRendererRate=30 (calibrationRate: Seconds_To_Frames =
s*30) but AudioHead::Execute sets audioFrameCount = Now().ticks which advances
~550/s (measured via the [audioclock] probe) -- sequence event scheduling runs
~18x off the authored timing.  Fix candidate: calibrate the audio renderer to
the actual Now().ticks rate (or drive audioFrameCount at the calibrated 30/s).

Also corrected en route: FUN_004b9550/95b8 are MechWeapon ConfigureMappables/
ChooseButton (mapper EnterConfiguration +0x38), NOT Myomers::ConnectToMover;
+0x31c there is fireImpulse.  Myomers::speedEffect has NO binary writer found
beyond the ctor 1.0f (its 'mover coupling' stand-in is spurious).

Diagnostics added: [statecfg] AudioStateTrigger ctor dump, [split] splitter
forwards, [seqev] sequence timeline events, [audioclock] frame-rate probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 00:25:18 -05:00
arcattackandClaude Opus 4.8 c4be8d3a07 Audio: footstep volume chain FULLY mapped -- blocked on gait-state id mismatch (task #50)
Chain (every hop verified live): FootStep pulse (SetBodyAnimation, decay in
PerformAndWatch) -> AudioMatchOf match=1 -> Start(2.0) on the FootFall
DirectPatchSource... which the renderer DROPS because its volume is 0:
an AudioControlMixer (outCtl=3/Volume + a second outCtl=5 mixer) feeds the
source, and BOTH mixer inputs are 0 -- nothing ever sends them.

Eliminated as senders (all traced): AudioScaleOf scales (29 total, none target
the mixer), AudioControlMultiplier (one instance, other target), Random/
AudioSampleAndHold (RNG verified WORKING -- varied samples; RANDOM.cpp is in
the build, the self-init ctor runs).

HYPOTHESIS [T4, next session]: the mixer inputs are fed by AudioStateTriggers
on AnimationState keyed to the AUTHORED gait-clip ids (walk 2/3, run 8/9 =
per-gait footstep volume).  Our runtime body SM walks in states 12<->13 --
which the recovered MechAnimationState enum names RightStandToReverse/
LeftStandToReverse.  If the authored audio expects 2/3 for forward walk, the
runtime clip NUMBERING in the reconstructed gait SM is offset from the
authentic ids -- a locomotion-layer mismatch with implications beyond audio
(the audio config is an independent witness to the true clip numbering).
Verify via the AudioStateTrigger configs on the AnimationState watcher
(trigcfg dump exists) and cross-check the SetBodyAnimation clip table.

New diagnostics: [snh] SampleAndHold sends, [mix] mixer forwards with input
index + sum, [scalecfg] authored scale bindings/boundaries, [volset].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 00:01:58 -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 ea8555480e Audio: fix stuck MissileLoading loop -- ProjectileWeapon never reached a ready state (task #50)
The runaway loop the user heard was MissileLoading01: the ProjectileWeapon
(SRM/ballistic) state machine set Firing(0)/Loading(3)/Jammed(5)/NoAmmo(7) but
NEVER a ready/Loaded level, so at idle the launcher parked in Loading(3) forever.
Its WeaponState audio (weaponAlarm, now StateIndicator-firing) therefore looped
MissileLoading indefinitely -- the loop stops only when the alarm LEAVES the
loading state.  (The laser sibling was fine: the Emitter DOES reach Loaded when
its charge tops off, so LaserACharge stopped.)

Fix: in ProjectileWeaponSimulation's default (idle/ready) branch, drive the state
from readiness -- Loaded(2, charge + ammo up = silent ready) once ReadyToFire,
else Loading(3, reloading).  SetLevel fires audio only on a real change, so this
yields the natural Firing -> Loading(reload) -> Loaded(ready) cycle and the
MissileLoading loop now stops when the launcher finishes reloading.  Verified:
weapon cycles Loading(3, recoil>0)->Loaded(2, recoil<=0); the only remaining
continuous loop is EnginePower (correct).  Fire path unchanged.

Also: master volume default kept; richer BT_AUDIO_LOG StopNote/SetupPatch traces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:29:05 -05:00
arcattackandClaude Opus 4.8 01261d72f3 Audio: add master volume (listener AL_GAIN); default 0.6, BT_AUDIO_VOLUME override (task #50)
The raw AWE32 samples are hot and everything plays at full per-source gain, so the
mix was loud.  Set alListenerf(AL_GAIN) once at device init -- it scales EVERY
source (master volume).  Default 0.6; override with BT_AUDIO_VOLUME=<0.0..1.0+>
(0 = mute, 1 = full, >1 = boost).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:18:21 -05:00
arcattackandClaude Opus 4.8 c27af4800b Audio: fix stuck weapon charge/sustain/loading loops -- maintain GaugeAlarm54 oldState (task #50)
WeaponState -> weaponAlarm (mechweap.cpp:149), and the weapon's looping audio
(LaserACharge/LaserASustain/MissileLoading, all SF2 sampleModes=1) is STARTED by an
AudioStateWatcher on the firing/charging state and STOPPED by an inverse
AudioStateTrigger that keys on old_state (== the state being LEFT).

The subsystem-state commit fired GaugeAlarm54 watchers on SetLevel but deliberately
left levelB (the oldState slot @0x10) untouched to avoid disturbing the weapon's
LevelCountB read.  Consequence: the audio watcher's StateChanged(oldState,newState)
saw a stale oldState, so the inverse/STOP triggers never matched -> the charge/
sustain/loading loops played forever.

Fix: SetLevel now sets levelB := level (oldState := currentState) before the change,
exactly like StateIndicator::SetState.  The stop triggers now match on leaving the
state and StopNote the loop.  This is also authentic: in the binary the 0x54 alarm
+0x10 IS oldState, and the weapon's LevelCountB()/weaponIdle read of +0x10 is that
same oldState (the old static-count reading was the reconstruction's accident).
weaponIdle is only consumed in the MP replicant record-apply path, so single-player
firing is unaffected; the value is now the authentic oldState-based one.

Subsystem state audio unaffected (0 skips). Diagnostics: BT_AUDIO_LOG now traces
SetupPatch src/file/loop + StopNote.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:11:49 -05:00
arcattackandClaude Opus 4.8 41c8ac120d Audio: fix runaway loops -- read SF2 sampleModes, not loop-point presence (task #50)
Weapon fire (and ~all one-shots) played FOREVER: sf2extract flagged a sample as
looping whenever the SF2 shdr had loop points -- but almost every SoundFont sample
has loop points; whether to actually loop is the instrument's sampleModes generator
(igen oper 54): 0/2 = one-shot, 1 = loop, 3 = loop-until-release.  The old heuristic
looped 239/241 samples, so SetupPatch set AL_LOOPING=1 on fire/explosion/step sounds.

Read sampleModes at the same igen zone as sampleID and loop only on 1/3.  Now
166 one-shot / 75 loop: LaserAFire/BigExplosion/BasicClick -> ForceStatic (one-shot),
EnginePower/EngineMotor/coolant -> LoopAtWill (correctly sustained).  Regenerated
audiopresets.cpp; the WAV PCM is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:56:38 -05:00
arcattackandClaude Opus 4.8 17a81010b4 KB: subsystem state audio done (0 skips); inert secondary attrs documented (task #50)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:45:14 -05:00
arcattackandClaude Opus 4.8 dcdd7dd0a7 Audio: register Generator.GeneratorOn -> generatorOn (@0x1D4) (task #50)
The last cleanly-backed subsystem audio attr: GeneratorOn (AudioLogicalTrigger)
now resolves to the real generatorOn member instead of the inert pad.  Static for
now (init 1, no shutdown writer drives it to 0 yet), but bound correctly.

Remaining inert attrs (ReportLeak/ConfigureActivePress/MotionState/
SpeedOfTorsoHorizontal/TargetRangeExponent) have no modeled backing member in
their size-locked subsystem layouts -- they read the inert pad (silent, no crash)
until those members are reconstructed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:44:42 -05:00
arcattackandClaude Opus 4.8 0e05a22a55 Audio: AmmoBin AmmoState + chain its attribute index -> ZERO state-watcher skips (task #50)
AmmoBin::AttributeIndex was a bare, default-constructed static with NO parent
chain, so nothing resolved on an AmmoBin -- even the inherited SimulationState
came back NULL (the audio AmmoState + SimulationState watchers were skipped).

- Chain AmmoBin::AttributeIndex to HeatWatcher::GetAttributeIndex() and give it a
  real AttributePointers[] with AmmoState -> ammoAlarm (@0x194, the 6-level 0x54
  StateIndicator-compatible feed alarm, already SetLevel'd Feeding/Loaded/Empty/
  Dumped by the sim) so reload/empty/feed audio fires on the ammo transition.

With this every StateIndicator audio attribute across the mech + all subsystems
binds to a real indicator: audiostate skips 85 -> 0.  Remaining audio gaps are the
inert Logical/Scalar/Enum subsystem attrs (ReportLeak/GeneratorOn/ConfigureActive
Press/MotionState/SpeedOfTorsoHorizontal/TargetRangeExponent), which read 0 (silent,
non-crashing) pending backing members in their size-locked layouts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:41:28 -05:00
arcattackandClaude Opus 4.8 5ba541ed56 Audio: subsystem STATE sounds -- GaugeAlarm54 IS a StateIndicator; register Generator/Condenser/Reservoir state (task #50)
The audio subsystem binds AudioStateWatchers (class 28) to per-subsystem state
attrs (GeneratorState/CondenserState/ReservoirState) BY NAME.  Recovered the
watcher TYPE per attribute via a MakeObjectImplementation ClassID trace.

Root cause of the subsystem-state crash: the binary's 0x54-byte subsystem alarm
(FUN_0041b9ec, reconstructed as GaugeAlarm54) IS a StateIndicator -- its "three
sub-indicators" @0x18/0x2c/0x40 are the audio/video/gauge watcher SChains, and
level@0x14 is currentState.  The reconstruction had modeled that tail as an opaque
`_tail`, so the sockets were never constructed -> AddAudioWatcher AV'd on 0xCDCDCDCD.

- GaugeAlarm54: give it the three REAL, constructed SChainOf<Component*> sockets +
  AddAudioWatcher/Video/Gauge; SetLevel now fires the watchers on a level CHANGE
  (empty sockets = no-op, so the ~all other alarms are unaffected).  levelB (weapon
  LevelCountB "reset source") is left untouched -- weapons unchanged.  sizeof stays
  0x54 (static_assert holds -> SChainOf<Component*> is 0x14, layout exact).
- Register the state attrs -> the subsystem's own alarm (own AttributePointers[]
  chained to the parent): Generator.GeneratorState->stateAlarm, Condenser.
  CondenserState->condenserAlarm, Reservoir.ReservoirState->reservoirAlarm.  Those
  alarms are already SetLevel'd by the sim, so the state audio fires on transition.
- AudioStateWatcher guard now validates the +0x18 SOCKET (what AddAudioWatcher
  touches), not the +0 vtable -- GaugeAlarm54 is non-polymorphic at +0 (raw header)
  but has a real socket at +0x18.

audiostate skips 85 -> 10 (only AmmoBin AmmoState/SimulationState left).  The
Logical/Scalar/Enum subsystem attrs (ReportLeak/GeneratorOn/ConfigureActivePress/
MotionState/SpeedOfTorsoHorizontal/TargetRangeExponent) stay inert (read 0, silent,
non-crashing) -- they need backing members in size-locked layouts (a polish wave).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:38:15 -05:00
arcattackandClaude Opus 4.8 03f0c21484 KB: audio in-game triggering works (mech-level); subsystem audio pending subsystem waves (task #50)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:40:44 -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