Commit Graph
142 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 e9db161404 Issue #20: wire Mech BalanceCoolant (id 0x16) -- the auto-cooling-balance button
Third instance of the silent-swallow pattern (unregistered handler id ->
Receiver NullHandler).  The Mech handler table @0x50BDF8 binds
{0x16, "BalanceCoolant" -> @0049f728}; the reconstruction never registered it.

Disasm-verified body (@0049f728): press-only guard (msg+0xc > 0; the binary
has NO novice lockout on this one), set EVERY condenser's valveState@0x1D0
to 1 (equal weights), then the shared redistribute @0049f788 -- which the
port ALREADY had faithfully as BTRecomputeCondenserValves (flow =
valve/total + condenserAlarm pulse; MoveValve calls it).  So the fix is:
- mech.hpp/mech.cpp: BalanceCoolantMessageID=0x16 + handler + MESSAGE_ENTRY
- heatfamily_reslice.cpp: BTBalanceCondenserValves bridge (Condenser is a
  complete type there; sets valves to 1 + redistributes)
- mech4.cpp: BT_BALTEST scripted harness (MoveValve press alternating with
  a Balance press)

VERIFIED headless (BT_BALTEST=1 BT_VALVE_LOG=1):
  boot     -> all valves 1, flows 0.1667 each
  MoveValve-> condenser#1 valve 5, flows 0.5 / 0.1x5 (the priority boost)
  BALANCE  -> all valves 1, flows 0.1667 each (equalized)
Awaiting the tester's live ADV-mode confirmation.

KB: decomp-reference.md Mech-table row marked wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:31:46 -05:00
arcattackandClaude Opus 4.8 4aab10ba89 Issue #19: wire ToggleSeekVoltage (Myomers id 9 + energy weapons id 0xb)
Root cause -- NOT issue #2 (the experience mis-seed pins everyone to VETERAN,
which PASSES the novice lockout; the gates were never the blocker).  Two
unwired handlers, the same silent-swallow pattern ToggleCooling had
(Receiver::Receive -> NullHandler for an unregistered id):

- Myomers (handler table @0x51158C {9,"ToggleSeekVoltage"@004b8a48}): the
  reconstruction had an EMPTY stub (the address falls in the untagged decomp
  gap 0x4b8837..0x4b8a8c) and never registered the id.  Body reconstructed
  faithfully from the raw disassembly (tools/disas2.py 0x4b8a48): novice
  lockout (@4ac9c8) -> heat-model gate (@4ad7d4) -> press-only (msg+0xc>0) ->
  currentSeekVoltageIndex@0x320 = (idx+1) % (maxSeekVoltageIndex@0x32C + 1).
  Modulo from zero, min index not consulted, no ResetFiringState.

- Emitter (energy-weapon table @0x511DB8 {0xb,"ToggleSeekVoltage"@004ba478}):
  the faithful body already existed as the orphaned "AdvanceSeekVoltage"
  (ZERO callers) -- converted to the registered message handler (same guards;
  cycle + ResetFiringState when not wrapping, disasm re-verified).  PPC and
  GaussRifle copy-inherit the set; the ammo branch keeps its own id 0xb ==
  EjectAmmo (still unwired, tracked separately).

Both registered via MESSAGE_ENTRY on top of the inherited chains.  Permanent
diagnostics: [seek] gear log (BT_SEEK_LOG) + BT_SEEKTEST scripted dispatch
harness (mech4.cpp, the cooltoggle pattern).

VERIFIED headless: [seek] Myomers -> 3,0,1,2,3... and [seek] PPC_1 ->
3,0,1,2,3... (modulo-4 wrap, matching the binary's arithmetic).  Awaiting
the tester's live ADV-mode confirmation for the real MFD button route (the
routing layer is the same streamed-EventMapping path ToggleCooling verified).

KB: decomp-reference.md message-table rows marked wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 12:57:18 -05:00
arcattackandClaude Opus 4.8 cbc5ff9532 Wire ToggleCooling (msg 3): restore the coolant on/off button + handler chain
Coolant priority (Lynx: weapon MFD -> Display -> Coolant) was unreachable: the
ToggleCooling handler (@004ad6f8, HeatableSubsystem table @0x50E41C id 3) was
undefined AND the handler chain skipped it -- PoweredSubsystem::GetMessageHandlers
chained straight to the Receiver root, bypassing HeatSink/HeatableSubsystem, so a
weapon's dispatch never saw id 3.

Reconstructed ToggleCooling from the disassembly (tools/disas2.py 0x4ad6f8): a
per-subsystem coolant on/off TOGGLE -- novice-locked (owner->BTPlayer->
roleClassIndex+0x274==0, the same guard as MoveValve), press-only, then flip
coolantAvailable(+0x134) 0<->1 and coolantFlowScale(+0x15C) 0.0f<->1.0f.  Cutting a
system's cooling frees the shared loop for the rest -- the emergent "coolant
priority" (the mechanism is a toggle, not a multi-level cycle).

- heat.hpp / heatfamily_reslice.cpp: HeatSink::ToggleCoolingMessageHandler (id 3)
  + HeatSink::GetMessageHandlers.  Registered at HeatSink (the abstract
  HeatableSubsystem never instantiates; every concrete heatable subsystem is a
  HeatSink, where the coolant fields live) -- identical coverage to the binary's
  base-table registration, no downcast.
- powersub.cpp + Condenser/Reservoir: chain their handler sets through
  HeatSink::GetMessageHandlers so id 3 reaches every heatable subsystem.
- mech4.cpp: BT_COOLTOGGLE_TEST scripted inject (dispatch id 3 to the first weapon)
  for verification.

Verified (BT_COOLTOGGLE_TEST + BT_COOL_LOG, expert egg): "[cool] PPC_1 ToggleCooling
reached" then coolant OFF(flow 0.0) <-> ON(flow 1.0) each press -- chain routes,
handler toggles, novice guard honored.  Both build/ + build-glass/ compile clean;
existing ids 4-8 unaffected (found before id 3).  NEXT: the #2 MFD button routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:04:04 -05:00
arcattackandClaude Fable 5 70eea6c1a4 Boresight parallax FIXED + the #4/#5 verdict instrumentation (Gitea #16, #4, #5)
PARALLAX (#16): the pick/fire ray was anchored at mech.y+5.0 (a port
improvisation) while the sight line ran from the eyepoint (y=6.23) --
two parallel rays whose constant offset grew into the reported low-miss
as range closed (measured ry +0.072 @50u -> +1.54 @2.7u).  The decomp's
sight and pick share the eye origin (HudSimulation @4b7830 chain).  Fix:
the viewpoint mech's cockpit eye owns the aim-camera publish in BOTH
views, origin = its own eye translation; leveling + deliberate elevation
untouched; chase view now converges to cockpit ballistics (V cannot
change where shots land).  After: pick pinned to the crosshair (ry <=
5e-6) from 50u to point-blank; 26 laser + 8 missile center-mass hits at
3/4-screen.  Awaiting the reporters' approach-test.

VERDICT INSTRUMENTATION (#4 closed authentic, #5 verdict posted):
BT_RANGE_LOG per-frame pick tracing + BTGroundRayHitExact analytic
cross-check (0/8000 arena fall-throughs; cavern 6/8400 single-frame
grazes -- the 'crazy sliding' is the authentic world-pick + 500 m/s
slide over depth discontinuities); BT_AUD_TAIL StopNote/fade timing +
BT_FIRE_PULSE single-shot driver (the energy 'buzz' is the AUTHORED
charging loop: crescendo through recharge, 1.309s authored release,
measured within one frame).  CAVERN.EGG: solo cavern test egg.

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:57:34 -05:00
arcattackandClaude Fable 5 2edde29671 Fix 1 (Gitea #12): disasm-exact ProjectileWeaponSimulation @004bbd04 + the dirty-bit sweep
Replace the RivetGun-modeled ProjectileWeaponSimulation body with the fully
recovered binary machine (capstone disasm, every branch address-cited):

- gate 1 @4bbd36: simulationState@0x40==1 (destroyed) || heatAlarm==FailureHeat
  || owner disabled -> recoil=rechargeRate + alarm 7 (was: simulationFlags==1)
- gate 2 @4bbd71: bin alarm 2/3 or bin destroyed -> alarm 7 re-pinned per frame
- Loaded @4bbec2: DENIED shots (viewFireEnable off / no owner target) blip
  SetLevel(4);SetLevel(2) and STAY LOADED, no ammo pull (the old FireWeapon
  early-returns under an unconditionally-cycling caller faked a full firing
  pip cycle -- the "missiles cycle but never launch" incident mechanic);
  the ammo pull is bin->FeedAmmo @4bbee6 in the CALLER; both updateModel|=1
  marks (@4bbf05/@4bbf4c = ForceUpdate) bracket FireWeapon; recoil set @4bbf51
- Loading @4bbdd2: recoil bleeds ONLY here at electrical Ready and CLAMPS at 0
  (it ran to -114 before); bin Loaded -> weapon Loaded
- 7 @4bbe4d: binary-faithful roach motel (re-asserted unconditionally;
  recovery = ResetToInitialState only)
- slot 17 @004b9c9c reconstructed as MechWeapon::ComputeOutputVoltage
  (rechargeLevel=(rechargeRate-recoil)/rechargeRate, Emitter overrides with
  @4ba738) and called from Loading/7 -- the launcher recharge dial ANIMATES
  (the old "authentically static" claim was wrong)

FireWeapon bodies stripped to heat+spawn only per @004bcc60 (no view gate, no
ConsumeRound, no recoil); ConsumeRound retired (not a binary method).

Gotcha #20 sweep (all sites disasm-verified): simulationFlags|=0x1 == engine
DelayWatchersFlag (audio watchers muted forever) removed everywhere --
projweap/mislanch fire marks, ProjectileWeapon::ResetToInitialState (@4bbb47
= updateModel|=1); Emitter::SetDirty retired and split per binary site into
ForceUpdate() (@4bafaa/@4ba55d) vs ExecuteOnUpdate() (@4ba99a/@4ba943);
Emitter fault gate GetFlags()==1 -> simulationState==1 (@4baab9).

AmmoBin::GetAmmoState() accessor added (named-member read of ammoAlarm@0x1A8).
BT_LOOK_TEST=<frame> scripted verify added (mech4.cpp): holds the rear-view
button 300f on/300f off to drive viewFireEnable headlessly.

Verified solo ARENA1 (bhk1, autofire): 54-60 [projectile] PUSH + impact smoke;
look-back denial window = trigger pulses, state stays 2, zero launches,
resumes on release; enemy (disabled) mech launchers pinned 7 + full recoil;
STREAK-6 recharge dial sweeps across BT_SHOT frames; lasers fire throughout;
zero gauge faults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 23:26:11 -05:00
arcattackandClaude Fable 5 63b168cb92 SeekVoltageGraph: full reconstruction -- the eng-page POWER graph + top-box eraser (Gitea #11)
The #10 audit's one WRONG: the port Execute was a bring-up no-op, so the
emitter/myomer engineering pages never erased their top data box -> stale
sibling-page ghosts on the shared Eng bit-plane (SYSTEM 10 PPC showing the
Streak ammo box, etc).  Full faithful widget landed:

- btl4gau2: ctor/BecameActive/Execute/clear/ticks/cursor recovered from the
  capstone disasm of @004c6798/@004c6920/@004c6934/@004c6be4/@004c6c30/
  @004c6c6c (Ghidra dropped every x87 arg).  Plot: v=0..12000 step 1200,
  x=Round(response(v)*230), y=Round(v*(1/12000)*187) (ld80 @004c6bd0/@004c6d74
  = exactly 1/12000); change-test samples the response at 12000V vs the 9999
  activation sentinel; XOR op for tick/cursor move-by-redraw; destroyed branch
  centres edestryd.pcc and revives via own vtbl+0xC (BecameActive, slot 3 of
  PTR_0051a1fc -- vtable-dump verified).
- The vtbl+0x3C sampler identified from the binary vtables: Emitter slot 15
  @004bb42c = sqrt(P(v)/2.0e8), P @004bb3f4 = damageFraction*v^2*0.5*
  energyCoefficient; Myomers slot 15 @004b8f94 = sqrt(AvailableOutput(v)*3.6/
  350).  FUN_004dd138 == sqrt (part_015.c:4026): myomers' fabs reading
  corrected, GetSpeedReading renamed SeekVoltageResponse.  Port dispatch via
  complete-type bridges BTSeekVoltageSample/BTMyomersSeekSample +
  BTSubsystemDestroyed (databinding rule).
- Emitter's AUTHENTIC attribute table recovered (binary @0x511dd4, ids
  0x1D-0x25) and published: Laser*/Seek*/OutputVoltage@0x414 (currentLevel,
  RAW volts -- the live-cursor feed).  Field renames per the table:
  0x3F8 minSeekVoltageIndex / 0x3FC maxSeekVoltageIndex (static_assert-locked).
  The MechWeapon 0x1D OutputVoltage PORT ALIAS retired (binary table ends
  0x1C; Find walks lowest-id-first, the alias shadowed the authentic row).
  PPC::DefaultData -> Emitter::GetAttributeIndex().
- mech4: BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses) for
  steady-state pixel verification; BT_SEEK_LOG diag.

Verified live (BLH, autofire): both #10 repro pairs held ghost-free for
minutes (SYS09->SYS10 PPC, SYS02->SYS04 ERMed, SYS05->SYS06 Myomers); curves
draw with live cursors; one replot per activation; quad panels/J-K-L/sec
panel un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:27:39 -05:00
arcattackandClaude Fable 5 6783619069 Gitea #9: the upper-MFD PRESET pages (3 MFDs x 5) live -- SetPresetMode
table re-decoded to the ModeMFD bits + desktop J/K/L page cycle

The preset system was unwired by ONE defect in the message layer: the
SetPresetMode @004d1b24 table @0051dbf0 had been transcribed from the
section dump as BIG-endian dwords ({0x1e,0x01000000} instead of
{0x1e,0x01}), so a preset press set a garbage high bit -- and for group 1
items 3-4 / group 2 items 0-2 stomped the LIVE NonMapping / Intercom /
ModeSecondary* bits -- while the real page bits never moved.  Ground
truth (section_dump.txt:72901-72908, little-endian + BTL4MODE.HPP [T0]):
set = ModeMFD{1,2,3}{Quad,Eng1-4} = 1<<(group*5+item), bits 0-14, fully
disjoint from the #6 secondary trio (bits 18-20); group 2 is MFD3, NOT a
duplicate of the secondary views.

What the 15 presets show (l4gauge.cfg): group = the MFD (Mfd1 lower left
/ Mfd2 upper center / Mfd3 lower right), item 0 = the btquad.pcx Quad
overview (up to 4 vehicleSubSystems cluster panels), items 1-4 = the
full-screen engineering-detail pages (bteng.pcx + prepEngr screens
group*4+1..4 + the cluster eng children: GENERATOR SELECT A-D, POWER
graph, COOLING loop, DAMAGE, ammo).  Empty screens are authored per mech
(Blackhawk: 3/11/12 empty).

Authentic dispatch (streamed "L4" .CTL EventMappings, BT_CTRLMAP_LOG
dump): each MFD owns the 8-button RIO bank around it, MODE-MASK-gated --
Mfd1 = 0x08-0x0F, Mfd2 = 0x20-0x27, Mfd3 = 0x00-0x07.  Quad page ->
direct-select buttons for the POPULATED eng pages (mapper msgs
Aux1Eng1-4 0x4-0x7 / Aux2* 0x9-0xC / Aux3* 0xE-0x11); eng page -> one
back-to-Quad button (0x3/0x8/0xD) + per-subsystem controls.  These
records already install and fire on desktop (btinput passes the live
manager mask), so the NUMPAD profile's 0x20-0x27 keys page MFD2
authentically.

Port wiring (the #6 pattern): keys J/K/L -> actions Mfd1/2/3Cycle ->
gBTPresetCycle -> L4MechControlsMapper::CyclePresetModeNow(group) -- a
documented desktop shim (24 mode-dependent pod buttons don't fit a
keyboard) that cycles Quad -> populated Eng pages -> Quad, visiting
exactly the pod-reachable set; the body is the authentic SetPresetMode.

Dev-composite: BTDrawGaugeSurfaces now draws the Eng1-3 planes at their
sibling cells and skips any mono plane whose channel is BlankColor,
honoring the mode-driven reconfigure (RemapGraphicsPort) -- each dev
cell shows the ACTIVE page like the pod monitor.  This supersedes and
removes the 2026-07-12 GAUGREND "frozen-dial" scaffold (forced all 15
page bits active under BT_DEV_GAUGES; it pinned the shared Eng plane on
the highest screen and ate the page flips).

Pixel-verified (BT_PRESET_TEST + BT_DEV_GAUGES_DOCK + BT_SHOT,
Blackhawk): all three MFDs page Quad -> SYSTEM NN eng details -> back to
Quad in lockstep with the [mode] preset mask log; group 0 skips the
authored-empty screen 3, group 2 skips 11/12.  Un-regressed: N display
cycle (0x450421->0x490421->0x510421, page bits intact), M control mode,
CONTROLS.MAP 52 bindings parse clean.

Diags: BT_MODE_LOG ([mode] preset), BT_PRESET_TEST=<frame>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:30:35 -05:00
arcattackandClaude Fable 5 1d6339b226 Gitea #6: secondary MFD Damage/Critical/Heat cycling -- reconstruct the
NotifyOfDisplayModeChange override (vtbl+0x4C @4d1ae4) + wire desktop 'N'

The secondary screen's schematic selector was mislabeled: @004d1ae4 (the
bits-18..20 ModeSecondary* mask swap) was reconstructed as a non-virtual
"SetControlMode" that nothing called, so the desktop stayed pinned on the
Damage view.  The binary's L4 vtable @0051e440 pins the truth:

  +0x48 = @004d1acc  <- CycleControlModeMessageHandler (FUN_004afbe0):
          forwards to the base RET no-op @004b048c.  A BAS/MID/ADV
          control-mode change never touches the secondary view
          (empirically confirmed: BT_MODECYCLE_TEST cycles the CONTROL
          MODE lamp, mask bits 18-20 unchanged, schematic stays ARMOR).
  +0x4C = @004d1ae4  <- CycleDisplayModeMessageHandler (FUN_004afcac):
          THE Damage/Critical/Heat selector, indexed by displayMode
          (table @0051dbe4 = ModeSecondaryDamage/Critical/Heat).

Authentic pod inputs (streamed type-6 .CTL EventMappings, dumped via the
new BT_CTRLMAP_LOG EVENT records): secondary-panel button 0x15 -> msg
0x15 CycleDisplayMode (manual p13, the "'Mech status Info center" bottom
left of the secondary screen), button 0x18 -> msg 0x14 CycleControlMode
(manual p6, top right), 0x10/0x11 -> ZoomIn/Out.  The DOS keyboard
fallbacks (Keypress 0x13d/0x13e = extended F3/F4) are dead under the
WinTesla VK map, hence the desktop pin.

Port wiring (the M/ModeCycle pattern): key N / pad RightThumb -> action
DisplayCycle -> gBTDisplayCycle -> CycleDisplayModeNow() -- the same body
the pod console button message drives.  Both .MAP profiles + the
compiled-in default updated.

Verified live (docked gauges + BT_SHOT, BT_VIEWCYCLE_TEST): the sec
panel cycles ARMOR DAMAGE silhouette -> CRITICAL DAMAGE subsystem list
-> HEAT DAMAGE colored list, mask 0x450421 -> 0x490421 -> 0x510421; M
control-mode cycling un-regressed (BAS/MID/ADV lamp cycles, view pinned).

Diags: BT_MODE_LOG, BT_VIEWCYCLE_TEST=<frame>, BT_MODECYCLE_TEST=<frame>,
BT_CTRLMAP_LOG now dumps EVENT records.  KB: gauges-hud secondary-view
section rewritten, CLASSMAP +0x48/+0x4C slots, decomp-reference env
gates, GAUGE_COMPOSITE phase-4 entry resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:28:38 -05:00
arcattackandClaude Fable 5 2ae9bd43ae Coolant Flush end-to-end (Gitea #7): InjectCoolant id-4 handler + drain + FLUSH.PFX cloud + 'H' key
The playtest report (sound plays, no gauge drop, no cloud) traced to the
Reservoir's InjectCoolant chain being dead in three places:

- The handler was never REGISTERED: the binary's Reservoir handler table
  @0x50e680 has one entry {4, "InjectCoolant", @4aee70}; added
  Reservoir::GetMessageHandlers + the press/release handler (press starts
  the flush gated on coolantLevel@0x12C > 0 -- the old body misread +0x12C
  as currentTemperature; release stops it; novice lockout via FUN_004ac9c8).

- Reservoir::InjectCoolant (@4aefa4, 1019 bytes) was an empty stub -- the
  drain the coolant gauge reads.  Reconstructed in full: work list =
  condenser/weapon/heatable chains (+0x7cc/+0x7bc/+0x7ac, roster-walk
  emulation with the binary's duplicate-visit weighting; HeatSink-filtered
  [T2 guarded]) + the linked master sink; per sink with flowScale != 0 move
  squirtMass x flowScale x dt (clamped to the tank / sink capacity) and
  credit pendingHeat with the negative carried-heat delta capped at
  sinkMass x reservoir startingTemperature -- the set%-biased flush of the
  manual (p24), riding the existing heat model.

- Two latent ctor decode bugs surfaced and fixed: the master gate @4af408
  (and @4aeb40 HeatWatcher, swept) reads the OWNER MECH's simulationFlags
  (*(param_2+0x28)), not the resource's subsystemFlags (the misread left
  the CoolantSimulation Performance unregistered); and the capacity scale
  FILDs the bank's INTEGER HeatSinkCount ((float10)*(int*)(link+0x1d0)) --
  the float reinterpret gave ~1e-44 -> a permanently empty tank.
  Capacity = 0.05 x heatSinkCount x streamed CoolantCapacity (BLH: 6.0).

Visual: the binary's mode-1 coolant-effect renderable (FUN_00456a68, built
for classID 0xBC0 on "ReservoirState", part_014.c:5439; tick @part_007.c:
8780) starts psfx 19 = FLUSH.PFX ("Coolant flush", BTDPL.INI) when the
state changes to 1 -- BTSpawnFlushCloud (mech4.cpp) spawns it on the same
alarm edge as an attached emitter at torso height.

Input: new CONTROLS.MAP action "Flush" on 'H' (HELD; press+release both
dispatch, the held-button payload).  Diags: BT_FLUSH_LOG, BT_FLUSH_TEST.

Verified live (FOGDAY): [flush] Reservoir level 6 -> 0.13 -> 0 over ~0.6 s
held (= the manual's 3-4 punches to empty), the coolant vertBar source
Reservoir/CoolantMass drains, and the bluish condensation cloud rises from
the mech (scratchpad/flush_cloud.png vs flush_before.png).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:15:44 -05:00
arcattackandClaude Fable 5 d9ceddb12a Aim ray follows the torso elevation (the boresight leveling erased it)
Follow-through on the pitch fix: the view pitched but shots still flew
level (user report).  Cause: the task-#48 AIM BORESIGHT leveling in the
eye's aim-camera publish (L4VIDRND) strips ALL pitch from the pick-ray
direction -- correct for the TERRAIN body pitch it was built against, but
it also erased the pilot's deliberate R/F elevation.

Fix: mech4's per-frame eye compose publishes the torso elevation
(gBTEyeElev); the publisher re-applies it onto the LEVELED forward
(fwd = (cos e * level, sin e)) before building the basis -- terrain pitch
stays stripped, the deliberate aim survives into BTGetAimRay.  Sign
matches the pixel-calibrated view compose (+ = up).

Harness evidence: pinned-down elevation visibly pitches the chase eye
into the terrain (the ray basis moves); a full headless aimed-kill could
not be driven (the autofire gate needs a designated target and the
random spawns wedged short of the truck row) -- aim-at-truck kill needs
the human pass.  Awaiting human verification (issue #8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:44:49 -05:00
arcattackandClaude Fable 5 b85afed925 Torso-elevation aim LIVE: R/F now pitches the view + aim ray (pitch was inert)
User + tester reports: pitch does not work.  Root cause: the Torso sim
integrated the R/F / stick-Y axis into currentElevation (authored limits
and rates all correct) but NOTHING consumed it -- eyepointRotation's only
writer was the look-state switch, so neither the cockpit view nor the aim
ray (camera basis) ever pitched.  Shots always flew level; low targets
(trucks) were unhittable.

Fix (faithful: the pod's stick-Y pitches the EYE; the torso geometry does
not tilt): per-frame compose in the HUD tick --
eyepointRotation = EulerAngles(lookPitch + torsoElevation, lookYaw, 0);
BTCommitLookState now stores its look component in gBTLookPitch/Yaw; new
complete-type bridge BTGetTorsoElevation (torso.cpp, mirrors
BTGetTorsoTwist).  DPLEyeRenderable consumes it every frame, so the view
and the boresight aim pitch together (crosshair stays screen-centred).

Sign PIXEL-CALIBRATED via new diag BT_FORCE_ELEV=<-1..1> (pins the axis
headless): screenshots confirm axis +1 (key R) = aim UP, -1 (F) = down.
X still recenters.  Awaiting human verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:54:39 -05:00
arcattackandClaude Fable 5 6f6a39b8c9 Issue #3 (b)+(c): weapon damage reaches cultural icons -> full trkdead explosion + authored burn fire
(b) The 'tiny explosion' on the ram kill is AUTHORED: crunch res 31 = 'stephit'
(one video object, effect 1008 = ddam5 damage smoke) -- a step/ram squash is
small by design.  The FULL explosion res 32 = 'trkdead' = psfx 15 dtrkboom
(fiery omni burst) + psfx 16 dtrkburn (the burning-wreck fire), reached by
WEAPON kills -- which the 1995 binary dispatches [T1]:
  - MechWeapon::SendDamageMessage @004b9728 (part_013.c:6765) gates only
    "target NOT derived from Mech@0x50bdb4 OR aimed zone set": a non-Mech
    boresight target takes the zone=-1 damage UNCONDITIONALLY;
  - Missile contact @004be078 dispatches at the struck solid's OWNER entity
    with no class test at all.
Port wiring: Mech::WorldStructurePick returns the struck solid's owning entity
(BoxedSolid::GetOwningSimulation; TERRAIN.cpp:107/246 build every static solid
with its Terrain/CulturalIcon/Door entity as owner); the mech4 pick block
designates IT instead of the gBTTerrainEntity sentinel (sentinel = fallback);
the projectile contact path grew the non-mech damage-zoned else-branch
(@004be078 mirror, direct Dispatch).  Plain terrain ignores the damage
(ENTITY.cpp:885 zone==-1 guard); an icon's handler maps -1 -> 0 and dies.
Truck armor is WeaponDamagePoints=1 (TRK.DMG): one laser = one dead truck.

(c) The burning fire is dtrkburn.pfx, authored INTO the death package (every
icon family carries 1016: trkdead/bigdead/meddead/msldead/twrdead).  There is
NO looping BurningState fire in the binary: the damage-zone effect watcher
ctor @0042a984 has exactly one call site (the Mech ctor, part_012.c:10405), so
icon ExplosionTables are inert, and CulturalIcon has no Performance.
Documented as authentic -- nothing invented.

Verified live (BT_SHOT pixel capture): 26 laser kills, [cult] TakeDamage
type=3 -> DYING res=32 -> DPLIndependantEffect 1015+1016 at the icon origin;
frames show the orange dtrkboom fireball, dtrkburn flames on the fresh wreck,
and the rubble aftermath; ram harness re-run still fires crunch res 31.
Diags: BT_FIRE_AT_ICON (designate nearest ahead icon; live-icon census in
CULTURAL.cpp), BT_FX_TEST="1015,1016".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:02:15 -05:00
CydandClaude Fable 5 122fb7bccb Fix the backtick crash (a /FORCE-hidden linkage bug) + backtick = view toggle
The crash (any typed key in a glass session; found via the backtick report):
the merge-reconciliation stand-down declared extern BTRIODevicePresent at
BLOCK scope inside the extern-C BTInputSuppressKey -- the declaration
inherited C linkage, _BTRIODevicePresent went UNRESOLVED, and /FORCE bound
the call to garbage (cdb: wild call into Sensor::DefaultData from
LBE4ControlsManager::Execute's suppression check).  The tolerated-LNK2019
batch hid the new unresolved external -- the exact CLAUDE.md /FORCE trap.
Fix: the extern moved to file scope (C++ linkage); link verified clean of
_BTRIODevicePresent.

Backtick feature (per Cyd: backtick = 1st/3rd person): PadRIO edge-detects
VK_OEM_3 in its poll (focus-guarded, message-path-free) and the mech4 view
block consumes it beside the V action (which stands down with the binding
engine in glass mode).  Verified live: two real presses -> [view] COCKPIT
eyepoint -> [view] external chase, game alive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:52:36 -05:00
CydandClaude Fable 5 f889e24ce0 Merge origin/master: the D1 relay/operator line + input remap meet the glass layer
33 master commits in (relay TCP/UDP + PySide6 operator console, CONTROLS.MAP
+XInput binding engine, camera seats, torso pitch aim, sign fixes, the 1995
manual, version stamping, 18-mech certification).  Conflicts: .gitignore +
CLAUDE.md router rows (combined).

SEMANTIC RECONCILIATION (the one real overlap): masters btinput binding
engine (ungated, CONTROLS.MAP) and the glass PadRIO (gated, bindings.txt)
would both read the keyboard/pad in a glass+PAD session.  btinput now joins
the stand-down convention: BTInputPoll yields (and BTInputSuppressKey claims
NOTHING, so authentic hotkeys flow) when an operational cockpit device owns
the input path -- BTRIODevicePresent, BT_KEY_BRIDGE force-override honored,
forced harness exempt.  One input system per mode: btinput on pod/dev
desktops, PadRIO on glass.  The mechmppr/mech4 bridge merges composed clean
(masters negate-once sign fix inside our device-gated bridge).  The D1 relay
keeps its own raw sockets by design (an alternative LAN wire; Steam and
relay are separate modes).

Verified post-merge: all 3 configs build; glass boots with [input] binding
engine standing down + PadRIO owning input (30 ticks); pod forced-walk
speedDemand=61.501 with btinput ACTIVE; 2-node loopback MP full 31/31
mission, 76/76 ticks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:46:41 -05:00
arcattackandClaude Fable 5 ff5260ce98 Coolant-loop cross-check vs the 1995 manual: structure strongly faithful
Dumped each mech's subsystem->coolant-loop (BT_SPEC_LOG in mech4: walks
the roster once, resolves each subsystem's linked-condenser number via
BTCoolingLoopFrame) and diffed all 6 manual mechs' COOLANT LOOPS tables.

RESULT -- the structure survives the 4.0->4.10 gap remarkably well:
- BACKBONE identical on ALL 6: Generator A/B/C/D on loops 1/2/3/5,
  Sensors(our Avionics) on loop 2, Myomers on loop 5, LRMs on 1&3,
  autocannon on 4, big energy weapon on 6 -- exact match
- weapon LOADOUT identical on 5 of 6 (Thor/Vulture/MadCat/Owens/
  Blackhawk); Loki is the one full rework (4.0 PPCx2/AFC100/SRM6 ->
  4.10 AFC50x2/ER Medium x2/SRM4)
- the consistent 4.0->4.10 change is a small-laser REDISTRIBUTION: the
  2 ER Small Lasers moved off the sensor/heavy loops onto the energy
  loops 4&6 (Thor/MadCat/Vulture); Owens near-perfect (loops 1&3 exact)
- Loop 0 = correctly UNCOOLED infrastructure (condensers, reservoir,
  gyro, torso, HUD, ammo bins, ...) -- not a coolant loop

CONCLUSION: the coolant-loop reconstruction is faithful; every diff is
4.0->4.10 balance tuning, not a bug.  BT_SPEC_LOG retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:37:14 -05:00
arcattackandClaude Fable 5 1271d3bc76 Torso elevation (pitch aim) wired: the pod stick's Y axis lives
The mechs could always tilt their aim up/down -- Torso models the full
vertical axis (currentElevation, rate, VerticalLimitTop/Bottom,
recenter) and EVERY 1995 control mode routes stickPosition.y into
Torso::SetAnalogElevationAxis -- but the desktop bridge hard-zeroed
stick Y, so the axis was dead on a keyboard rig.  The one pod control
the remap left unwired.

- btinput: JoystickY axis -> elevTarget/elevActive/elevAbsolute
- mech4 shim: sElev integrator (same walk/spring model as the twist;
  X recenters pitch too via gBTElevRecenter)
- mechmppr bridge: feeds stickPosition.y every bridged frame (the old
  unconditional zero removed); both mode branches covered
- CONTROLS.MAP (+ numpad profile + compiled default): R/F = aim
  up/down, pad LeftStickY = elevation (was unused)
- torso.hpp: CurrentElevation()/ElevationVelocity() accessors (diag)
- [mppr] trace gains stickY (note: the trace reads AFTER the next
  frame's device push re-zeroes the stick -- input flows regardless)

Verified live: R held -> torso elevation climbs at the authored rate
and clamps at 0.349066 rad = exactly 20.0 deg (the Blackhawk's
VerticalLimitTop); release holds the aim.  The eyepoint correctly
stays level -- pitch aims the GUNS and reads on the HUD's vertical
elevation tape, as in the pod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:44:22 -05:00
arcattackandClaude Fable 5 17176058cc First 4-pod session VERIFIED + the EntityID-cast diagnostic trap
4 pods, relay-assigned seats, 4-pilot MP4.EGG (eggmodel-generated incl.
callsign bitmaps): 4 distinct seats, full ladder, every node replicates
all 3 foreign mechs (host-qualified [repl] sets are exact complements),
4 distinct spawn positions (no dropzone stacking), UDP at 4-pod fan-out
(relay tx=3x rx, zero drops), all pods alive through the session.
Per-host [net-rx] census symmetric: 36 NewDynamicEntity makes (mech +
subsystem roster) from every host to every node.

DIAGNOSTIC TRAP that cost a false bug-hunt: EntityID::operator int()
returns localID ONLY, so the [repl] log's (long)GetEntityID() collapsed
different hosts' mechs onto one number and read as 'missing
replicants'.  The log now streams the EntityID object (host:local via
its operator<<).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:42:38 -05:00
arcattackandClaude Fable 5 980c9cd7e5 Input remap: CONTROLS.MAP binding engine + XInput gamepad support
The pod's controls reach the game through the RIO serial board; on a
desktop that hardware is a keyboard shim of hardcoded GetAsyncKeyState
reads.  New binding engine (game/reconstructed/btinput.cpp) maps PC keys
and an XInput pad onto the authentic channels through a user-editable
file (content/CONTROLS.MAP, community-suggested grammar, corrected):

  key/pad -> button <addr>   real buttonGroup emissions, mirroring the
                             RIO convention exactly (press a+1 w/ mode
                             mask saved, release -a-1 w/ saved mask;
                             L4CTRL.cpp:2470-2520) -- aux/preset banks,
                             hotbox, panic, reverse thrust all reachable
  key/pad -> axis            Throttle rate (lever), pedals/JoystickX
                             deflect (turn/twist) into the existing
                             virtual-controls integrators (feel, gait
                             detent, spring-centering all unchanged)
  keypad pilot|external <n>  keyboardGroup key values ('0'-'F')
  pckey <char>               any authentic 1995 typed hotkey
  action <name>              port dev controls (view, all-stop, ...)

Keys claimed by a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP
feed -- ends the historic double-dispatch ('w' drove AND selected pilot
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups fed
the developer fake-event dispatcher).  Unbound keys keep their authentic
meaning.  The dual-use 'V' is split: V = view toggle, B = look behind.

Default profile = WASD classic (compiled-in twin; delete the file to
restore).  CONTROLS_NUMPAD.MAP ships the corrected community layout
(keypads are NOT buttons 0x50-0x6F -- that space doesn't exist; no
clickable cockpit exists so everything needs a binding; keyboard fire
buttons added; 0x36/0x37 are hotbox not 'config'; missiles moved off
Ctrl).  XInput loads dynamically (1_4 -> 9_1_0), disconnected-pad
probing rate-limited; pad sticks write the axes as absolute positions
(the spring is physical), triggers/buttons per the file.

Verified live: bindings load (43), W and NumPad8 drive (speedDemand 0 ->
61.5), X all-stop (spd -> 0), aux-bank emission (D1 -> [input] 0x2f
PRESS/release under the numpad profile), suppression both directions
(posted 'r' reaches [keych], posted 'w' swallowed), full-keyspace
WM_CHAR+WM_KEYUP fuzz x3 survived with sim advancing, XInput graceful
with no pad, 2-node relay session un-regressed (full ladder + UDP).
Pad-in-hand testing still needs a physical controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:09:36 -05:00
CydandClaude Fable 5 9f35a8034a Controls: the keyboard bridges STAND DOWN for a live device (step 2c)
New engine query BTRIODevicePresent() (L4CTRL.cpp, ungated -- pod-correct for
the serial RIO too): device exists AND IsOperational().  Both dev input
bridges -- the mech4.cpp mapper-attr writes and the mechmppr.cpp BT_KEY_BRIDGE
block -- now gate their WRITES on it: BT_KEY_BRIDGE unset = auto (bridge only
when no device), 0 = force off (the documented pod setting, honored), else
force on.  The BT_FORCE_THROTTLE headless harness always rides the bridge.
The mapper demand READ (turn = turnDemand) always runs.

Verified live (glass build, PAD): OS-injected LSHIFT hold slews the PadRIO
throttle 0 -> 0.33 -> 1.0 with pre==thr every frame -- the value arrives via
the ENGINE push through the streamed .CTL binding, not the bridge -- and
speedDemand=61.501 comes out of the authentic InterpretControls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:14:37 -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 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