Commit Graph
132 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 c75abca857 KB: audio backend was STUB DLLs (not just gated) -- now real OpenAL + in-tree WAV loader (task #50)
Correct the audio digest in wintesla-port.md + project-overview.md: the "no
sound ever" root cause was that BOTH engine/lib backend DLLs were no-op stubs
(libsndfile ordinal-only sf_open->NULL; OpenAL32 imports only KERNEL32). Now
real OpenAL Soft + LoadWavPCM; soundbank cracked (241 samples). Only remaining
gap = game triggering (AudioEntities).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:56:24 -05:00
arcattackandClaude Opus 4.8 52440e13b0 Audio: ENABLE the engine (relax dead AWE-card gate) + diagnose the real silence (task #50)
User reported never hearing any audio. Root-caused: the KB "Audio: DONE" was wrong.
The OpenAL playback chain IS fully implemented (device/buffers/sources; PlayNote
really calls alSourcePlay), but three gaps kept it silent:

1. GATED OFF (the "no sound" root cause): BTL4Application::MakeAudioRenderer returned
   NULL unless the pod's AWE_FRONT/AWE_REAR AWE32-card env vars were set -- authentic
   1995 pod behavior, dead on modern hardware, so the renderer was NEVER created.
   FIXED: default audio ON (BT_NO_AUDIO=1 restores silence; AWE vars still force-on).
   Verified: the OpenAL device now opens with no env vars ([audio] device OPENED).
2. Soundbank STUB: allPresets[2][100] (btstubs.cpp) is zero-init -> PRESET_isImplemented
   false -> 0 buffers load. Sample data exists (AUDIO1/2.RES: EnginePower/LaserAFire/...)
   but the event->sample map is gone (not in the decomp). STILL OPEN.
3. No triggering: the reconstructed game never creates AudioEntities on events. STILL OPEN.

So the device opens but nothing loads/plays yet -- enabling real sound needs the
soundbank reconstructed + the game triggers wired (a proper audio wave). Added a
BT_AUDIO_LOG trace harness across L4AUDRND/L4AUDRES/L4AUDLVL. KB corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:02:31 -05:00
arcattackandClaude Opus 4.8 d624b9b783 KB: pod-LAN real-IP path VALIDATED -- not localhost-locked (task #50)
Tested the real-IP config path (not just 127.0.0.1): an egg with this machine's
real LAN IP (10.0.0.46) in [pilots] connected + replicated + moved end-to-end
between two nodes (Connected to GameMachineHost at 10.0.0.46:1602 / All
connections completed! / peer telemetry flowing). Confirms gethostbyname
local-address matching + WSAStringToAddressA IP:PORT parse work with real
addresses. Recorded the LAN recipe + noted the one remaining unknown: an actual
two-physical-machine run (firewall) -- validated single-box via the real IP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:24:12 -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 bc4d6e5597 MP: console-death freeze NOT REPRODUCIBLE + fix the latent game-listener-close bug (task #50)
Investigated the "console death froze peer replication" open question (2026-07-14)
with a direct 2-node repro (BT_REPL_LOG on a circling peer, affinity-pinned):

- Clean relay kill -> replicant kept circling smoothly through the kill, NO
  disconnect logged, node survived. Replication did NOT freeze.
- Deliberately STUCK console (scratchpad/btconsole_stuck.py: connect, start
  mission, then stop recv() while holding the socket OPEN = the exact
  "receive pad full -> close never seen" mode hypothesized) -> replicant kept
  moving the whole run. Replication did NOT freeze.

Conclusion: peer replication is INDEPENDENT of the console (pods replicate
peer-to-peer over the GAME socket; the console is a separate egg/mission/status
channel). The original freeze was a transient -- plausibly the same single-box
packet-jitter/CPU-contention artifact root-caused in 49d73dc -- or was fixed by
later MP work.

FIX (the one real bug found): L4NetworkManager::HostDisconnectedMessageHandler's
ConsoleHostType branch closed gameListenerSocket (the GAME listener) on a CONSOLE
disconnect -- a naming bug (the comment + commented-out OpenConnection intended a
CONSOLE re-listen, which CreateConsoleHost already does). Removed. Harmless to
established peer sockets (why a live match survives console loss) but it would
have blocked a NEW peer from joining after a console cycle. Verified: 2-node
still connects (All connections completed!) + the peer circles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:52:35 -05:00
arcattackandClaude Opus 4.8 d1ce99402f KB: code-first sweep of stub/stand-in claims -> 2 more STALE fixed
Targeted pass on "stand-in / not-wired / stub / no-op" claims, verified against
CODE + runtime (not KB wording -- the trap that hid the peer-warp staleness):

- combat-damage.md STEP-6 "remaining = fix ResourceFindByName no-op -> empty
  table" (reads present-tense above the "STEP 6 COMPLETE" header): STALE. The
  table LOADS live ([cyl] table 'bhk1'/'madcat'/'ava1' layers=7); the name-load
  BYPASSED the no-op ResourceFindByName via SearchList(type=0x14) (mech.cpp:1631).
  d07ac7d. ResolveHit resolves unaimed hits.
- multiplayer.md "Mech::Reset full subsystem-reset sweep is still a bring-up TODO":
  STALE. mech4.cpp:1616 loops every subsystem -> DeathReset(mode) (heat/power/ammo/
  charge) + heals zones + ForceUpdate(0x1f). Per-subsystem DeathReset bodies are
  authentically trivial for some classes -- not a missing sweep.

Verified ACCURATE (not stale, left as-is): GaussRifle FireWeapon no-op (a decomp
fact -- inert in the 1995 binary; btl4gau2 faithfully marks it "not yet supported");
StatusMessagePool NULL stub (kill ticker live, non-kill status messages genuinely
deferred); Myomers coupling inert (genuinely open); searchlight fog-swap (open by
decision). checkctx CLEAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:01:17 -05:00
arcattackandClaude Opus 4.8 1b5c3bbbd8 KB: correct STALE "peer warp not wired" claim -> it IS wired + visible (user-caught)
User observed peer warp spheres in 2-player mode; the KB (translocation-warp.md
frontmatter+body, multiplayer.md frontmatter+body) still called the peer path a
"local-player sphere stand-in until SimulationState/DropZoneLocation replication."
STALE: mechdmg.cpp:1074 fires BTStartWarpEffect at the peer's position gated to
ReplicantInstance, and simulationState rides every update-record header -- so an
observer DOES see a peer's un-wreck/respawn warp. Committed 160b78e ("observer
sees peer un-wreck"). The only genuine remainder is a [T3] fidelity nuance: the
peer sphere is anchored to the peer's WORLD position rather than the peer's
authentic (un-replicated) DropZoneLocation. (The staleness audit itself missed
this -- it trusted the file's own stale wording over its body + the code.)

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:35:41 -05:00
arcattackandClaude Opus 4.8 ac599c0d2a KB: reconcile STALE peer coupled-motion "COMPLETION PENDING" entry -> DONE (task #50)
The open-questions.md block was the old completion PLAN (ea39af1) and was never
updated after the work actually landed + the residual was root-caused. It
contradicted multiplayer.md (which correctly documents both "Authentic coupled
peer motion -- DONE" and the packet-jitter resolution). Reconciled to DONE:
coupled single-source gait pipeline default-on (c52a1ad/b013742/96a896a/f094d78/
23f1532); the "needs master half" conclusion superseded by 49d73dc (residual
shakiness = single-box packet jitter, a test-rig artifact, fixed with CPU
affinity, user-confirmed -- NOT a game bug). Minor follow-ups (2.9u channel-snap,
decay constant) tracked in multiplayer.md. No code change -- KB currency only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:19:43 -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 49d73dc8e2 MP: peer shakiness ROOT-CAUSED as single-box packet jitter (rig, not game) + affinity launcher (task #50)
The residual RANDOM peer shakiness on accel/decel was proven (BT_RXJIT record
inter-arrival probe) to be TEST-RIG packet jitter, not the game: two Debug btl4
nodes on one box contend for CPU, so Windows batches their TCP delivery -- records
arrive in bursts (max 56-226ms gaps, burstiness 3-7x) instead of even ~17ms.  The
peer dead-reckons across the gaps then snaps -> random shake.  Pinning the nodes to
disjoint cores restored even ~17ms delivery (burstiness ~1.0) and the shakiness
vanished (user-confirmed 'that was it').  Real pods = dedicated machines, no
contention -> never see it.  So NO un-authentic jitter buffer -- the coupled
body-channel peer (96a896a/f094d78/23f1532) is the authentic + correct finish.

 - mech.cpp: BT_RXJIT record-arrival-jitter probe (env-gated).
 - tools/mp_launch.sh: 2-node launcher that pins nodes to disjoint cores (bakes in
   the fair-delivery condition; documents why).
 - context/multiplayer.md: the finding, so it is not re-litigated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:14:22 -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 47771ba256 KB: authentic coupled peer motion DONE -- the single-source gait pipeline (task #50)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:30:16 -05:00
arcattackandClaude Opus 4.8 ea39af1ff6 KB: peer coupled-motion architecture -- completion plan + session landings (task #50)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:00:25 -05:00
arcattackandClaude Fable 5 e6ad29d648 KB: log the console-death replication-freeze open question (task pending recurrence)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 09:31:21 -05:00
CydandClaude Fable 5 0faf6a0072 Front end: local mission-egg builder + front-end mode (Workstream B foundation)
The core 'single-player without the operator console' capability: a
zero-argument launch now builds its own mission and plays.

- game/reconstructed/btl4fe.{hpp,cpp}: the BattleTech catalog (8 maps,
  8 mechs + variants, colors) and BTFeMission_WriteEgg -- emits the exact
  console egg format (verified against content/MP.EGG): [mission],
  [ordinals] + the 4 static rank-place plasma bitmaps, [pilots],
  per-pilot loadout sections, and [largebitmap]/[smallbitmap] with
  GDI-rendered 128x32 / 64x16 pilot-name plasma bitmaps.
- L4APP.H: SetEggNotationFileName / SetNetworkCommonFlatAddress setters
  (from RP412) so the front end feeds the standard -egg load path.
- btl4main.cpp WinMain: front-end mode -- no -egg and no -net builds
  frontend.egg locally (BTFrontEnd_Run) and boots it. -egg/-net runs
  are untouched.

Verified: no-args launch -> '[frontend] built frontend.egg' -> mission
loads and runs; the generated egg is structurally identical to MP.EGG
(32x32/16x16 name bitmaps with real glyph pixels, all sections present).

Remaining in Phase 5 (deferred): the interactive on-screen catalog menu,
the in-process LocalConsole marshal (mission clock + StopMission at
expiry + single-binary loop), and the score-intake/results screen (new
for BT -- no mission-end flow exists yet). (Phase 5 of the roadmap)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:44:37 -05:00
CydandClaude Fable 5 5ebb9a5906 Net: NetTransport seam + Steam transport (Workstream C.1)
The wire moves behind NetTransport (L4NETTRANSPORT): L4NET.CPP taken
from RP412 post-seam -- all ~24 Winsock call sites route through
NetTransport_Get() -- with BT's 3 BT_NET_TRACE blocks re-sited onto
their code anchors (they read message/packet metadata, not sockets, so
no collision). Default WinsockNetTransport = the arcade/LAN TCP wire.

SteamNetTransport (L4STEAMTRANSPORT, ISteamNetworkingSockets + FakeIP/
SDR) compiles under option(BT412_STEAM) (default OFF); Steamworks SDK
1.64 vendored at extern/steamworks_sdk_164. steam_appid.txt gitignored
(Spacewar 480 by hand until a real AppID). Ported gConsoleLossEndsMission
from RP412's APPMGR (default False = arcade re-listen).

Verified: default TCP build passes full loopback MP through the seam
(console -> egg msgID-3 chunks -> mesh complete -> both instances tick,
net-tx/net-rx traces fire through NetTransport_Get()); BT412_STEAM=ON
compiles + links against the SDK + boots solo. Live Steam session
deferred to Phase 6. (Phase 4 of docs/BT412-ROADMAP.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:33:34 -05:00
CydandClaude Fable 5 5285117d3b Cockpit 3a: PlasmaScreen on the desktop (L4PLASMA=SCREEN)
Renders BT's 128x32 plasma display into a desktop window instead of
streaming to serial hardware. L4PLASMASCREEN.* copied verbatim from
RP412; the L4GREND.cpp selector diff applied (BT's only divergence
there is a comment -- no collision).

Verified: a 'Plasma Display' window opens and the game ticks normally
beside it (L4GAUGE=640x480x16 L4PLASMA=SCREEN).

Phase 3b (single-window MFD-split cockpit) is DEFERRED -- blocked on
the missing BT cockpit-layout geometry and BT's unfinished upstream MFD
dev-composite; the L4VB16 hand-merge is HIGH-risk (BT's dev-gauge
docking overlaps RP412's split rewrite). See context/steamification.md
and docs/BT412-ROADMAP.md. (Phase 3 of the roadmap)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:23:43 -05:00
CydandClaude Fable 5 fe97746a51 Input: PadRIO -- play without the pod (L4CONTROLS=PAD), Workstream A.1
Ported from RP412: RIOBase split out of the serial RIO (L4RIO.h),
rioPointer is RIOBase* (L4CTRL.h), PAD token -> new PadRIO() speaking
the RIO surface from an XInput pad + keyboard (L4PADRIO/L4PADBINDINGS,
vRIO bindings.txt grammar, hot-plug), KeyLight RGB mirror TU
(BT412KEYLIGHT, /std:c++17 per-file).

BT-side fixes PadRIO forced into the open:
- Both keyboard input bridges (mech4.cpp, mechmppr.cpp BT_KEY_BRIDGE)
  stand down when a RIO device exists -- they overwrote the engine
  controls push every frame. M/X conveniences stay live.
- Mapper attribute chain OFF BY ONE (latent real-pod bug): the DOS
  chain below MechControlsMapper carried two base attributes, WinTesla
  carries one, and AttributeIndexSet::Find is positional -- the .CTL
  stick mapping wrote throttlePosition. Pad slot + binary-locked enum;
  gotcha ledgered (reconstruction-gotchas #11).

Verified: PAD throttle lever ramps + sticks, stick turns with the
authentic speed-vs-turn clamp (61.5 -> 22.0 u/s), mech drives; keyboard
fallback intact (BT_FORCE_THROTTLE harness). New diags: BT_CTRLMAP_LOG,
BT_STICK_LOG. (Phase 2 of docs/BT412-ROADMAP.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 08:16:33 -05:00
CydandClaude Fable 5 55e4295dc3 Toolchain: VS2022 (v143) is the build of record -- /FORCE:UNRESOLVED retired
The v143 linker refuses to emit an image with unresolved externals even
under /FORCE, so the dead offline-tool ladders in mech3.cpp
(Mech::CreateSubsystemStream / SubsystemDefaultData -- never called at
runtime, resources ship prebuilt in BTL4.RES) are compiled out behind
BT412_OFFLINE_TOOLS (default off). The exe links /FORCE:MULTIPLE only:
a genuinely unresolved symbol is now a hard link error instead of a
hidden runtime AV. No other source changes needed for v143.

Verified: clean build; solo DEV.EGG runs; two-instance loopback MP via
btconsole.py -- mesh forms, mission runs both sides, master + replicant
tick in each world. (Phase 1 of docs/BT412-ROADMAP.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 07:36:19 -05:00
arcattackandClaude Fable 5 da80727467 KB: autocannon jam/coolant -- SOLVED from decomp, QA behavior CONFIRMED faithful
Reproduced the QA report end-to-end from the decomp + raw-binary-verified constants.
The coolant-priority effect is real but fire-rate-dependent: at the ~11s combat
cadence, not-boosted jams (T=1058>1000) while boosting the AFC's condenser keeps it
below the line (T=953, no jam) -- exactly as the QA described. Max-spam (8s) overheats
regardless (why the earlier 'weak boost' read was wrong); 16s never jams. All AFC100
constants verified authentic vs BTL4.RES raw bytes (afc_dump.py). No bug, no stand-in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 07:27:01 -05:00
CydandClaude Fable 5 d8dd512843 BT412: fork from BT411 -- Steam + internet-multiplayer line
Identity pass (README/CLAUDE.md/CMake project bt412), the steamification
roadmap (docs/BT412-ROADMAP.md), and the living context topic
(context/steamification.md). Forked at BT411 4e72f0c; origin is
VWE/BT412.git only -- this repo never touches the BT411 remote.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 07:24:05 -05:00
arcattackandClaude Fable 5 21abe2212e KB: autocannon jam/coolant-priority -- mechanism is FAITHFUL + COMPLETE (decomp hunt)
Two-agent decomp hunt on the QA report ('AFCs jam on 3rd shot if coolant priority
not boosted'). Conclusion: the whole coolant->jam chain is reconstructed end-to-end
and FAITHFUL -- the weapon's constant coolantFlowScale=1.0 is authentic (only
condensers' 0x15C is written from the valve, via RecomputeCondenserValves over the
condenser chain mech+0x7cc). Authentic mechanism is second-order-in-equation but
first-order-in-outcome: weapon -> its condenser -> bank; boosting a condenser's valve
share (~5x conductance swing) keeps its weapon below degradationTemp -> no jam.
Distinguished the sticky probabilistic jam (degrad 1000, JammedState 5) from the
self-clearing overheat lockout (failure 2000, weaponAlarm 7). Empirically the port is
directionally correct (~4th-shot probabilistic) but not the QA's deterministic 3rd
shot -- a calibration/scenario question pending QA clarification, NOT a stubbed path.
Do not tune the heat economy to force it without ground-truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:56:34 -05:00
arcattackandClaude Fable 5 844edbd2aa KB: FX per-zone chain LIVENESS-VERIFIED (BT_CRIT_PROBE solo)
Closed the loop: BT_CRIT_PROBE=8 on solo DEV.EGG drove the full per-zone FX chain
end-to-end -- [zonefx] entity 1:321 seg 12 psfx 12/13 -- proving TakeDamage ->
mechdmg effect loop -> BTStartZoneEffect -> StartEntityEffectImplementation (segment
world-pos resolve) -> BTStartPfxAttached fires live. The 2-node fight harness didn't
engage (mechPicks=0, an MP-harness spawn/connect issue, not FX); BT_CRIT_PROBE is the
reliable solo liveness path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:12:12 -05:00
arcattackandClaude Fable 5 a198e5fb4a KB: fix my botched FX entry -- the impact/destruction FX chain IS reconstructed
Reverting a wrong 'correction' I just made: I claimed the six L4VIDRND explosion
renderables were STUBBED and 'never draw'. WRONG -- ScalingExplosionRenderable has a
real ctor/Execute body, and the impact/fire/detonation FX were reconstructed in the
Fire VISUALS (48c9c84) + Impact-FX FORENSICS (065c114) waves. The '//STUBBED: DPL RB'
markers I grepped are benign 2007 empty ctor/dtor notes on unrelated renderables
(InnerProjectile/DPLObjectWrapper/ChildLight), not the explosion FX. Only genuine
open item: the chain is not yet liveness-verified headlessly (no solo enemy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:06:45 -05:00
arcattackandClaude Fable 5 31d338b6ff KB: correct stale StartEntityEffectImplementation entry (dispatcher DONE 2026-07-13)
The per-zone effect dispatcher was reconstructed + wired the same day the audit
flagged it as 'RECOMMENDED NEXT TARGET' -- the entry was never updated. Correct it:
dispatcher done (btl4vid.cpp:889, wired at mechdmg.cpp:1120); the real remaining
FX gap is the six still-STUBBED L4VIDRND scaling-explosion renderable bodies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:01:53 -05:00
arcattackandClaude Fable 5 8f7b304069 KB: mark authentic per-mech turn-rate DONE (task #64b)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 05:56:48 -05:00
arcattackandClaude Fable 5 a83995a755 Locomotion: wire the AUTHENTIC per-mech turn-rate lerp (task #64b)
The master-perf disasm (0x4aa3d3-0x4aa4ff) computes the yaw rate as
lerp(walkingTurnRate@0x574, runningTurnRate@0x578) by ground speed, not a
constant. Wired it into the drive, replacing the bring-up constant kDriveTurnRate:

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:52:35 -05:00
arcattackandClaude Opus 4.8 c0a0ec5e69 KB: document the searchlight fog-swap latent bug (task #63, left as-is by decision)
Investigation (workflow) found the searchlight-driven fog swap never worked in the
ORIGINAL 1995 binary either -- it's a latent bug, not a port regression:
SearchlightSimulation (@004b841c) reads requestedOn@0x1E0 (never written) while
ToggleLamp (@004b860c) toggles commandedOn@0x1DC; no bridge exists, so lightState
@0x1D8 is perpetually 0 (lamp never lights). The self-consistent sibling
ThermalSight reads the field it toggles (0x1DC) -- the tell. The port reproduces
the bug faithfully (searchlight.cpp:189). Additionally the port never constructs
PullFogRenderable, so even absent the bug the swap wouldn't fire.

Decision: LEAVE AS-IS + document (faithful to the buggy original); the port keeps
the static lights-ON fog= values. A working swap would DEVIATE from the shipped
binary (repair the sim to read commandedOn + construct PullFogRenderable at the
btl4vid MakeMechRenderables inside pass + a toggle input) -- fully scoped in the KB
if ever wanted. No code change this commit.

Documented in rendering.md (fog section), open-questions.md (deferred subsystems),
subsystems.md (Searchlight). Core per-map/time/weather fog (task #63) is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:04:29 -05:00
arcattackandClaude Opus 4.8 225fa5951b Render: fog searchlight-swap verified INERT + BT_FOG_LOG diag (task #63)
Verifying the searchlight/nosearchlightfog swap (user request): it does NOT work.
The arcade swaps active fog between fog= (headlight on) and nosearchlightfog=
(off, darker/tighter) via PullFogRenderable::Execute (edge-triggered on the mech's
light attrs -> SetFogStyle searchLightOn/Off).  But PullFogRenderable is NEVER
CONSTRUCTED in the port (grep: only the .h decl + .cpp def; no `new`), and
searchLightOn/Off is called from nowhere else.  Empirical confirmation via the new
BT_FOG_LOG hook: over a 22s run SetFogStyle fired only style 0, zero style 2/3.

So fog is static at the searchlight-ON (fog=) values -- normal lights-on play, so
core per-map/time/weather fog is unaffected -- but the nosearchlightfog variant is
dead.  Corrects my earlier "the swap is wired end-to-end" claim (code paths exist,
trigger never instantiated).  KB rendering.md updated; wiring it back is a scoped
reconstruction (bind the Searchlight subsys light state to a constructed
PullFogRenderable at viewpoint setup).  No behavior change this commit -- diag only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:36:48 -05:00
arcattackandClaude Opus 4.8 083b148bb0 Render: FOG restored -- authentic per-map/time/weather fog now renders (task #63)
User: "aren't some levels supposed to have fog?" -- they were, and none showed.
BTDPL.INI (the DPL env INI, L4DPLCFG) authors fog=near far r g b (+ often
nosearchlightfog=) on EVERY map/time/WEATHER leaf; weather is a fixed egg field
(clear/fog/soup). Shipped eggs pin cavern/night/clear -> page dsnitclear
(near 90, far 1100, dark blue). The env pipeline resolved + pushed it to D3D, but
it rendered INVISIBLE: D3DRS_FOGTABLEMODE=D3DFOG_LINEAR (table fog) derives its
factor from the perspective-NONLINEAR z-buffer, so without WFOG the 90..1100 range
collapsed to fog-factor ~1 (no fog everywhere).

Fix (L4VIDEO.cpp world pass): auto-detect D3DPRASTERCAPS_WFOG -> per-pixel W-fog
(smooth, == the arcade dpl_fog_type_pixel_lin); else VERTEX fog (eye-space but
per-vertex -> splotchy on coarse terrain tris, so only a fallback). FOGENABLE + the
fog mode are re-asserted each world-pass frame (the old one-time set got clobbered
per-pass, which is why plain table fog showed nothing). Verified on this GPU:
WFOG present -> per-pixel table fog, user-confirmed smooth.

Also corrected task #20's false "shipped maps define no fog" comment (it checked
.MAP/.RES, not BTDPL.INI). Env hooks: BT_FOGMODE=table|vertex|off, BT_WEATHER=
clear|fog|soup, BT_FOG="near far r g b" (now honored on the authored branch too);
[fog] resolved/model diagnostics. KB: rendering.md fog section added.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:23:59 -05:00
arcattackandClaude Fable 5 267059ab88 Damage economy RECONCILED: 3 stand-ins fixed, 3 paths confirmed authentic (task #60)
Full 5-path audit of the damage economy vs the decomp (5-finder +
adversarial-verify workflow), resolving the KB self-contradiction the
binary-coverage audit flagged.

CONFIRMED AUTHENTIC as-is (no change needed):
- Energy beam (emitter.cpp): damagePortion = authored DamageAmount x
  (charge/seekV)^2; the ctor x1e7 and fire x1e-7 cancel (_DAT_004bafbc
  dumped from the exe = x87 80-bit 1e-7 exactly).
- Autocannon (projweap.cpp): full authored DamageAmount from resource
  +0x19C delivered unmodified; the 0.0625 at :667 is the shooter's own
  gyro recoil, not the round.
- Zone-armor BASE model: damageLevel += amount x damageScale[type]
  (engine DAMAGE.cpp:379, called mechdmg.cpp:427), legs x0.5, 1.0=dead.

3 STAND-INS FIXED (all byte-verified against the decomp):
- A. mechdmg.cpp:451 -- read the phantom `stance` member (no binary
  offset, zero writers -> perma-0), so the leg-shot-out -> fall/death
  branch was DEAD. Now MovementMode() (mech+0x40, @part_012.c:6910).
- B. mechdmg.cpp:458 -- guarded the leg partial-failure graphic on the
  always-0 IsAirborne() stub where the binary calls IsDisabled()
  (@0049fb54 = movementMode 2||9). On a wreck the binary SUPPRESSES the
  write; the stub let it corrupt graphicAlarm 9->4/3 -- the task-#52
  wreck-graphic bug, now fixed AT SOURCE (was only masked by the
  IsMechDestroyed latch).
- C. mech4.cpp:1551 -- flat kShotDamage=12 fed as the kill-score
  damageAmount (the KB self-contradiction: task #8 claimed it retired,
  but it was live). The score handler @0x4c02e4 derives the whole kill
  award from it, so every kill scored identically regardless of weapon.
  Now lastInflictingDamage -- the real killing-blow magnitude, latched
  in TakeDamageMessageHandler (mech.cpp:624), mirroring the per-hit path
  (mech4.cpp:1207). The phantom `int stance` slot is reused for the new
  Scalar member (size-neutral, no layout shift); init 0 in the ctor.

DEFERRED (task #60-D, documented): the missile CLUSTER model -- the port
fires N flying rounds (net armor total authentic) vs the binary's ONE
missile with a random burstCount cluster roll (loses cluster variance +
single-zone concentration). Blocked on an OPEN decomp semantic (does
burstCount multiply armor or only the gyro kick? settle at FUN_004bef78
-> FUN_004be078 -> EXPLODE.cpp:209-210).

VERIFIED live: clean build; 2-node fight -> clean center-mass kill (no
crash, kills 0->1); [zone-armor] dump confirms per-zone armor 50-140 +
legs x0.5. NB the displayed POINTS score still reads 0 -- a SEPARATE
open gap (scoreAward + role/team/tonnage multipliers unwired); fix C
corrected the damage INPUT to that formula.

KB swept: open-questions.md (self-contradiction resolved + task #60
summary + deferred missile item), combat-damage.md (damageScale is
type-indexed not even/odd; task-#52 source fix; kill-score section),
RECONCILE.md (missile = ONE spawn not N), stale comments in mechweap.cpp
(SendDamageMessage is LIVE), mislanch.hpp, mechdmg.cpp (FUN_0049fb54 =
IsDisabled).

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:46:22 -05:00