Commit Graph
134 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 83b3f31957 Whirlwind respawn: authentic death->drop-zone->recreate cycle (task #52)
Death transition (mech4) now dispatches VehicleDead(-1) to the owning
player; BTPlayer::VehicleDeadMessageHandler restructured to the authentic
@004c05c4 three-way branch: -1 = death bookkeeping + sever playerVehicle
(wreck stays) + 5s re-post; >=0 = engine drop-zone hunt -> DropZoneReply
-> CreatePlayerVehicle (new mech); -2 = the acquire probe. Guarded on a
DropZones group so a zone-less mission stays dead instead of aborting.

Three latent bugs the respawn path exposed, all fixed:
- IsMechDestroyed latched on graphicAlarm>=9 alone; a later leg hit on the
  wreck rewrites the alarm to 4/3, un-latching -> the death transition
  re-ran (double kill/score, abort). Now latches on movementMode 2||9.
- Score handlers dereferenced the severed playerVehicle during the dead
  window; guarded.
- The console score flush routed a NetworkClient::Message through the
  player's Entity::Dispatch, which stamps entityID past the smaller struct
  -> /RTC1 stack overflow. Sent via application->SendMessage instead.
- Renderer LoadMission re-entry (per viewpoint-make) re-read the env INI;
  its light block Fail'd on stale sceneLightCount. Reset it in
  DPLReadEnvironment so the respawn's second read is clean.

Verified 2-node self-drive: B killed repeatedly by A respawns each time
(wreck stays), ticks + reloads + takes fresh damage on the new mech, no
abort/hang across multiple death->respawn cycles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:26:55 -05:00
arcattackandClaude Fable 5 e430e474cb Death gate: a destroyed mech takes no pilot input (ghost-drive fix)
User report from 2-window play: after dying, the pilot could still steer the
wreck around invisibly.  Zero throttle/turn/fire and the forced harnesses at
the gBTDrive source when IsMechDestroyed() -- deliberately INPUT-only, so the
gait/animation machinery keeps running and the death collapse plays out.

Also recorded in the KB: the killer's screen shows the wreck SWAP but not the
death SINK (needs the Mech-level death/knockdown update records, disasm
preserved from task #51), and the respawn-cycle scouting (type-12 intro
scripts are binary ref-records; adrop; DropZone + Mech::Reset machinery) for
the recovery-whirlwind reconstruction (task #52).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:18:11 -05:00
arcattackandClaude Fable 5 29035028fd Cross-pod beams: replicate emitter discharge via subsystem update records
User report: lasers only visible on the window firing them.  The peer's
replicant emitters never learned the master fired.

THE AUTHENTIC PIPE (decomp-verified):
- FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call)
  does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c)
  on the app+0x34 manager -- our per-weapon render walk already plays that
  role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's
  updateModel / ForceUpdate().
- Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message:
  the roster walk already hands the entity's stream to every subsystem's
  PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested
  updateModel bit; Entity::UpdateMessageHandler routes received records by
  subsystemID to the subsystem's ReadUpdateRecord.  All engine machinery --
  the missing pieces were the Emitter's serialize/apply pair + the triggers.

CORRECTIONS to the dormant task-33-era transcriptions (never exercised --
nothing ever set updateModel -- so the latent misreads never surfaced):
- The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord,
  7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry +
  body semantics; @004ba568 resolves an EntityID at rec+0x30 through the
  entity index -- record semantics, not Damage).  Renamed across MechWeapon /
  Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is
  undecoded (inherits MechWeapon for now).
- Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not
  recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour --
  the old `CopyColor(targetEntity+0x184)` was also a databinding trap.
- OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing
  UpdateRecord typedef as the param type, silently NOT overriding the engine
  virtual (the base ran instead; nothing would ever have serialized).
  Base-typed params (Simulation__UpdateRecord*), casts inside.
- Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve
  (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields.
- ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one
  final record at beam end (turns the peer's beam off).
- Mech::DrawWeaponBeams extracted from the player-only drive block so the walk
  runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics
  would have served the player's segment pointers as the replicant's muzzles).

VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all
225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun
ports).  Solo un-regressed (150 beams, kill chain, no crash).

Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery
(reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record
types decoded (pose/alarm/leg-state+heat with the body-channel write-through
re-sync, knockdown, death, impact, movementMode) -- transcription deferred;
it replicates remote knockdown/death/heat and was not needed for beams.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:04:34 -05:00
arcattackandClaude Fable 5 a110041a70 Fix "can't turn in MP": pilotArray[1] overrun stomped controlMode
User report from the first live 2-window session: throttle worked, A/D turn
did nothing.  [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).

Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe).  Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.

ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it.  In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0.  MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode.  This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).

Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits).  Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).

VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:16:46 -05:00
arcattackandClaude Fable 5 d09cda6c36 MP: replicant gait animation -- peer mechs walk instead of sliding (task #50)
A peer's replicant mech slid around as a frozen statue: DeadReckon moved
localOrigin but nothing animated the legs.  Reconstruct the replicated-motion
display loop: after DeadReckon, derive a SIGNED speed demand from the
replicated velocity (the master publishes worldLinearVelocity in every update
record, Mover::WriteUpdateRecord MOVER.cpp:759; the replicant stores it in
updateVelocity.linearMotion :712; sign = forward dot against the -Z facing),
write it to controlsMapper->speedDemand (the LEG channel's live demand
source), and advance the LEG state machine for its JOINT writes only --
travel stays with DeadReckon, so position always follows the master and the
clip cadence matches the replicated speed.

Two replicant-only initialization holes found live (the master's equivalents
are primed per-frame in the PLAYER-only drive block):
- globalTimeScale/idleStrideScale read 0 -> clip advance time dt*0, the leg
  machine engaged (state 11) but the clip froze (legCycle stuck 0);
- reverseSpeedMax2 (the run-cycle rise-clamp @0x7a0; LoadLocomotionClips does
  NOT set it) read debug-heap garbage -> legCycleSpeed clamped to -4.3e8 on
  entering run state 13.  Primed both; legCycleSpeed sane-banded once.

VERIFIED 2-node (BT_GOTO driver + BT_REPL_LOG observer): the replicant runs
the full gait lifecycle -- stand -> walk(5,7) -> run(10,12), legCycle 7.5->34
tracking the master's speed, wind-down(8), Standing(0) when the master stops.
Solo un-regressed (goto->aim->kill chain intact, no crash).

projectedVelocity source-swap (the binary publishes the BODY channel's
smoothed projection instead of the actual travel velocity) evaluated and
DEFERRED: it risks a verified working replication feed for a smoothing
nuance; recorded in multiplayer.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 17:31:05 -05:00
arcattackandClaude Fable 5 47aede3094 Shadow: tag tshd proxies in the LOADER (V toggle broke the pipeline)
User report: the shadow broke MID-SESSION -- suddenly solid opaque black and
buried on any slope.  The session log pinned the trigger: pressing V.  The
view toggle (SetViewInside) RELOADS every segment mesh through the object
loader and swaps them into the render tree -- including blh_tshd.bgf -- and
the reload path never called SetIsShadow(1) (only the original tree-build
call site tagged it).  Untagged, the entire shadow pipeline disengages: no
translucent TFACTOR state block (solid black), no depth bias (buried), wrong
pass.  The damage-swap reloads had the same hole.

Fix: tag *tshd* filenames in LoadObjectBGF itself, next to the existing
all-shadow-material detector (which misses the mech proxy -- plain black
material, not shadow_mtl).  Every load path -- build, damage swap, view
toggle -- now yields a tagged object; the drawOps alphaTest routing reads
the flag downstream.  User-verified live: the shadow survives repeated V
toggles, translucent and slope-correct.

This also likely explains the RECURRING "shadow vanishes on inclines"
reports: any V toggle silently broke it; every relaunch "fixed" it.  KB
(locomotion.md): loader-level tag recorded as co-dependent part 5 of the
shadow arrangement; the vanish-diagnostic note now lists all three causes
(reload untag / gait desync / genuine burial) with check-first guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 17:10:33 -05:00
arcattackandClaude Fable 5 b6fe686b22 Shadow: authentic terrain tilt + decal draw order (feet layer correctly)
User report: the ground shadow painted OVER the mech's feet.  Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.

Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):

- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
  jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
  the surface normal via an orthonormal basis fed to the ENGINE's own
  LinearMatrix->EulerAngles conversion.  Hand-derived Euler signs are exactly
  how the prior tilt attempts "dug into the hillside"; the engine conversion
  is convention-proof.  ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
  BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
  lift varies across a slope; world->local rotation by the TRUE yaw from
  localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
  class).  BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
  between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
  decal order.  The mech, drawn after, z-passes over the shadow: the bias
  can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
  BT_SHADOW_TILT=0 flat fallback -> the old -0.004.  BT_SHADOW_BIAS overrides.

KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:59:48 -05:00
arcattackandClaude Fable 5 099cad998d KB: incline shadow-vanish co-resolved with gait v5 (user-confirmed live)
The shadow report was a desync symptom, not a bias/tilt bug: the ~2-4 unit
depth-bias margin is tight enough that v4's out-of-phase leg pose showing
through could push the flat quad under a slope. No shadow code changed;
both symptoms cleared together. Noted as a diagnostic hint (check
[sync]/BT_SYNC_LOG before touching bias/tilt for this report class).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:34:56 -05:00
arcattackandClaude Fable 5 d4e139a0d9 Gait v5: display+travel from the LEG channel (kills the stutter class)
User-reported recurrence of "legs stutter and lose sync".  Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking.  Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:

- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
  walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
  frames; a demand change landing between them picks DIFFERENT next states
  (one channel winds down, the other keeps walking) -> opposite-phase churn
  (observed bs=6 ls=7 -- body on walk-R, leg on walk-L).

Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.

DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
  - master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
    -> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
    (writing last) the displayed pose;
  - IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
    the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
    the BODY is the dead-reckoning PROJECTOR -- locally invisible.
  v4's reading of FUN_004ab430 as the travel source was wrong.

FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split.  Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).

Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes.  KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry).  New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:25:49 -05:00
arcattackandClaude Fable 5 9353d59eb9 Fix drive-to-range stall: BT_GOTO self-drives + steers correctly cross-net
The BT_GOTO beeline never actually closed on a target -- three separate faults:

1. It was gated behind `if(gBTDrive.forced)`, and `forced` was set ONLY by
   BT_AUTODRIVE -- a chicken-and-egg where the block that would drive was gated
   on the drive already being on.  So BT_GOTO alone just turned in place (the
   "stall").  Fix: BT_GOTO enables forced mode itself AND supplies its own
   forward throttle (a beeline must drive, not only steer), cutting the throttle
   inside a stop radius (weapon range for "enemy", so it holds + shoots).

2. Steering used the gDriveHeading SCALAR MIRROR (seeded to 0), but MP pilots
   spawn facing a non-zero heading -> it steered the WRONG way.  Fix: heading
   error from the mech's ACTUAL forward (localToWorld's -Z axis), signed angle
   to the target.

3. The goto turn was routed through controlsMapper->turnDemand, which zeroes out
   in -net mode (the mapper key-bridge only shapes the local viewpoint mech
   there) -- the heading froze and A drove straight past B.  Fix: apply the goto
   turn to the orientation integration DIRECTLY (if gBTGotoActive, turn=
   gBTGotoTurn), after the mapper read.

Verified 2-node one-box: A with BT_GOTO="enemy" ALONE (no autodrive) drives
dist 1673->231m, holds at weapon range, mechPicks=59, autofire -> B hdlr=192,
DESTROYED -- a fully automated human-style cross-pod KILL via the real beam
path (not the FORCE_DMG hook).  Solo un-regressed (plain autodrive still drives;
goto="enemy" resolves the enemy, mechPicks=59, no crash).  BT_GOTO_LOG traces
the beeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:02:51 -05:00
arcattackandClaude Fable 5 0dfbb97dcc Fix torso-pitch aim ray: gyro-stabilized boresight from the mech torso
The aim/pick ray was the active RENDER EYE's view direction, which made a
mech unable to aim at a distant enemy:
  (a) it inherited the mech BODY pitch -- on a slope the body tilts ~8 deg to
      conform to terrain, so the boresight pointed that far DOWN and the pick
      ray hit the ground ~50-100m short of the target (groundPicks, never
      mechPicks); and
  (b) in CHASE view (the default) it was the elevated behind-the-shoulder
      camera, so the ray started over the mech's shoulder and flew high.

Fix (L4VIDRND DPLEyeRenderable::Execute, the BTSetAimCamera publish):
- LEVEL the boresight: drop the view-direction pitch and rebuild an upright
  basis (world +Y up) -- the guns fire along the mech's gyro-stabilized
  HORIZONTAL heading, not the terrain-pitched body.  Reticle X still carries
  the torso twist (BTTwistToReticleX); reticle Y any elevation.
- ANCHOR the ray origin at the mech's TORSO (myEntity->localToWorld
  translation + ~5) instead of the render eye, so it is correct in BOTH the
  chase (default) and cockpit views.

Verified end-to-end: solo, enemy 120m ahead, BOTH chase and cockpit view ->
[target] "MECH under boresight ... mechPicks=59 groundPicks=0" -> beam ->
*** DESTROYED.  Also proved PickRayHit resolves a REPLICANT when aimed at it
(hit=1 at 757m, 2-node run).

mech4: BT_GOTO="enemy" test aid now chases the nearest peer of ANY instance
(solo dummy OR replicant), not just replicants.

Still open (locomotion, not aim/MP): BT_GOTO/BT_AUTODRIVE stall short of the
enemy, so an AUTOMATED 2-node kill via the real beam isn't demonstrated yet
(the FORCE_DMG hook still is).  A human can now aim correctly in the default
view.  Solo un-regressed; build clean; KB validator clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:11:54 -05:00
arcattackandClaude Fable 5 bac45851ad MP task #48 (partial): -net aim+drive already work; localize the real gap
Re-measured interactive -net combat on a 2-node run. The KB's "aim ray dead /
drive dead in -net" was STALE: A's aim ray is live (noRay=0, the active eye
publishes the aim camera) and A drives (BT_AUTODRIVE/BT_GOTO move it). The real
blocker to a human cross-pod kill via the beam path is NOT the netcode:

- Boresight inherits the mech BODY pitch. The aim ray = the active eye's view
  dir; on sloped terrain the body pitches to conform and the boresight points
  ~8 deg down ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y=-0.145 every frame),
  so at range the ray hits terrain ~50-100m short of the enemy (mechPicks=0).
- Drive/goto stalls ~700m short before closing to pick/weapon range.
- PickRayHit vs a replicant is thus unconfirmed, but the replicant localToWorld
  IS correct (lstart magnitude == true A->B distance; the DeadReckon +
  localToWorld=localOrigin block already tracks the replicated position).

Change: add a gated BT_GOTO="enemy" test mode (beeline toward the nearest live
replicant) -- MP spawn coords vary per run so a fixed "x z" can't reliably face
the peer. KB (multiplayer.md) updated with the corrected findings + the real
remaining work (torso-pitch/level-boresight aim ray + drive-to-range).

Solo un-regressed; btl4.exe builds; KB validator clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:13:41 -05:00
arcattackandClaude Fable 5 54b6663b4d MP task #47 SOLVED: cross-pod damage + kill (validate the master mech)
Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3)
dead over the network; B runs the death transition on its own screen. Verified
on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits
of 12 and is DESTROYED, with wreck swap + explosion.

ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*)
does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that
arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler
never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants
(the P6 bring-up hack for the partial MakeReady handshake); a locally-created
master never got validated.

FIX (one line): Mech::Make now sets ValidFlag for the master too -- the
reconstructed ctor builds the mech synchronously, so it's safe. Nothing else
changed: targeting, the virtual victim->Dispatch, the replicant reroute, the
wire, receive, and id resolution were ALL already correct.

Two prior root causes were BOTH mis-diagnoses from the WRONG messageID:
Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered
msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21
hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU
Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof
444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the
id is correct end-to-end (3:22 = B's master).

- game/reconstructed/mech.cpp: validate master + replicant at Make.
- game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated
  2-node cross-pod reproducer (real virtual Dispatch path).
- context/multiplayer.md, reconstruction-gotchas.md: corrected root cause +
  the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean.

Solo un-regressed (the solo enemy was already force-valid; the change only ADDS
ValidFlag). btl4.exe builds; full mission loop clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:52:26 -05:00
arcattackandClaude Fable 5 04a2049ce2 MP task #47: DISPROVE the Entity-layout root cause; correct the record
The prior commit (c78662a) filed cross-pod damage failure under a
"cross-TU Entity/EntityID layout divergence" (and the P5 base-region
audit). A compile-time offsetof probe injected into BOTH translation
units disproves that: game/reconstructed AND engine/MUNGA compute an
IDENTICAL Entity layout --

  sizeof(Entity)=444  offsetof(entityID)=380 (0x17C)
  offsetof(ownerID)=388  offsetof(simulationFlags)=32
  offsetof(Mech,entityID)=380  (Entity subobject at 0)

So Entity::Dispatch reads this->entityID at the SAME offset mech4's
GetEntityID() does -- there is no per-TU read difference, and the
logged 3:22 (mech4 candidate) vs 3:19 (engine Dispatch) cannot be one
object read two ways. Those lines were different objects/messages,
mis-correlated. The distinct, REAL P5 "base-region layout divergence"
(HARD_PROBLEMS.md) is about raw-offset stomps at this+0x2d4..0x2f0 --
far above entityID -- and is unrelated to #47.

Changes:
- Revert the committed BT_MP_NET engine diagnostics (ENTITY/EVENT/
  NTTMGR/RECEIVER/L4NET) to the clean a9c3e96 baseline -- those are
  the very instruments that produced the mis-correlated data.
- mech4 BT_MP_FORCE_DMG hook: dispatch via the real m->Dispatch(&td)
  path (no false id stamping); comment records the offsetof finding.
- context/multiplayer.md: layout-divergence RULED OUT; 3:22/3:19 marked
  unconfirmed; leading hypotheses reframed as H2 (wire host-relative
  (de)serialization) vs H3 (replicant id != master's registered key),
  to be distinguished by a 2-node run with per-message correlation.

Solo un-regressed (mech walks + targets, 0 faults). Cross-pod delivery
remains open, but the investigation is redirected off the wrong (large,
structural) layout-audit path onto the EntityID wire/id-assignment path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:09:50 -05:00
arcattackandClaude Fable 5 c78662a0f1 MP cross-pod damage ROOT-CAUSED: EntityID localID corrupted by hostID on the wire (task #47)
Traced the dispatched-message delivery end to end with BT_MP_NET /
BT_MP_FORCE_DMG. Everything works except one wire step:
- A's Entity::Dispatch reroutes the replicant's TakeDamage
  (application->SendMessage(ownerID=3, EntityManager, msg)); POST-Dispatch
  the message carries entityID=3:22 (host:local) -- the replicant's own
  ID, matching B's master (GetEntityID()==3:22). VERIFIED sent.
- On B the message ARRIVES, GetEntityPointer finds an entity, Posts it,
  the event drains (ProcessEventTask = ProcessOneEvent(0)), Event::Process
  runs, Receive finds+calls a handler. VERIFIED the full deliver chain.
- BUG: B receives entityID=3:19, NOT 3:22 -- the localID dropped by
  exactly the hostID (3) between A's send and B's receive. So
  GetEntityPointer(3:19) returns the WRONG entity (classID 48, not the
  mech 0xBB9), whose base handler ignores the unaimed hit -> 0 damage.
  Auto-replicated UPDATE records (msgID 18) arrive with the correct 3:22
  and find the mech, so the corruption is specific to the dispatched-
  message wire path/direction.

Next: the host-relative EntityID (de)serialization on the dispatched-msg
path (RoutePacket / packet EntityID encoding) -- diff vs the update path
which translates correctly. Diagnostics retained (all BT_MP_NET-gated,
off in solo -- verified: solo 22 hits, 0 probe noise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:03:32 -05:00
arcattackandClaude Fable 5 a9c3e96f20 MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry
(BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects
every mech -- player, dummy, and peer replicants. The boresight
world-pick walks BTGetTargetCandidates (all mechs != shooter, closest
ray hit) and the whole fire/damage block retargets from the solo
gEnemyMech to the picked hotTarget; missile/projectile validation
generalised via BTIsRegisteredMech. Verified one-box: instance A
enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3).
Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED).

STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and
invoked with a valid owner, but the dispatched message doesn't reach
the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's
msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A
dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6
damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute
FIRES; the break is downstream in the transport/delivery of a
dispatched entity-message (update records flow fine -- net-rx works).
Next MP task = that wire delivery. Diagnostics BT_MP_LOG /
BT_MP_FORCE_DMG retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 10:13:39 -05:00
arcattackandClaude Fable 5 48191d6fdf MP-front scout (task #45): P6 survives the combat rework; LAN-fight gap map
One-box smoke test re-run on the current build: console egg -> mesh ->
RunningMission both instances, 174+ x144-byte update records each side,
2 mech trees per instance, 0 crashes (~6 min). The movement-replication
milestone is intact after all the targeting/weapon/death changes.

Gap map to a first playable LAN fight recorded in
context/multiplayer.md (wiring order): (1) generalize the world-pick
from gEnemyMech to all peer mechs; (2) exercise the Dispatch reroute
for cross-pod TakeDamage; (3) victim state visuals on the shooter's
screen (zone replication or a death event); (4) cross-pod beam visuals
via the AUTHENTIC beam-keepalive messages (FUN_0041c350, templates
@0x511e6c/78 -- already stubbed in emitter.cpp); (5) 2-window driving +
DEATHS scoring; respawn deferred to the P5 teardown debt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:40:48 -05:00
arcattackandClaude Fable 5 fa88f74c68 HUD aspect correction: square reticle units at any window shape (task #44)
User: "it doesn't scale with our screen resolution?" -- correct. The
HUD draws into the fixed 800x600 backbuffer and the present stretches
it into the client area: on non-4:3 windows everything distorted
(circles -> ellipses, the bottom tape widened vs the ladder, positions
drifted outward).

- BTGetPresentAspect (L4VIDEO, from gWindowAspect; 4:3 fallback).
- dpl2d x-unit = (bbW/2)/presentAspect (== bbH/2 at 4:3 -- unchanged
  there), circles pre-squished in bb space so the stretch restores
  round.
- The reticle<->NDC conversions (BTGetAimRay / BTProjectToReticle /
  BTProjectHotBox / BTTwistToReticleX) use the PRESENT aspect; the
  mouse map (BTClientToReticle) is now pure client-relative.
- Placement itself is authentic and stays put: the binary clusters the
  instruments around the boresight (ladder x=0.35 half-heights, spans
  +-0.25) -- NOT at the display edges.
- Crash en route: the BTGetPresentAspect extern declared INSIDE dpl2d's
  anonymous namespace = a different, unresolved symbol -> /FORCE
  garbage call (landed in MissileLauncher::DefaultData). Moved to
  global scope; gotcha noted in the comment.

Verified 4:3 regression: aimed lock + zone hits + no crash; hud-geom
MapX/MapY unchanged at 4:3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:14:36 -05:00
arcattackandClaude Fable 5 e13e8af44b dpl2d OpenLines = SEPARATE SEGMENT PAIRS + pip spacing 0.03 (user screenshot)
User screenshot showed three artifacts, all one root cause: my dpl2d
executor drew OpenLines/CloseLines vertex runs as a connected LINESTRIP.
Every ctor use of OpenLines is SEGMENT PAIRS (4 crosshair arms, one pair
per ladder tick, the compass stem, the lock ring's 4 ticks, threat
marks) -- the strip joined ticks into zigzags ("squiggly lines" on both
ladders), hung diagonals off the crosshair arms, and drew chords across
the lock ring. Now D3DPT_LINELIST (one line per pair).

Also PIP_SPACING: _DAT_004cdce8 is a DOUBLE = 0.03 (read from the exe);
my 0.01 guess overlapped the 0.028-wide pips (user: "pips are
overlapping"). Reference-screenshot pairs sit side-by-side, matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:55:51 -05:00
arcattackandClaude Fable 5 3430f87305 HUD colors corrected vs the binary ctor (validated by a period screenshot)
A period pod screenshot from players who know the game flagged the
overlay. Stroke-by-stroke re-read of the ctor (@4546-4587) against it:
- The range caret triangle is GREEN width 1 (ctor @4550-4551 sets
  SetLineWidth(1)+SetColor(0,0.75,0) AFTER the yellow bar's CallList) --
  my transcription left it yellow/2. Same for the bottom bowtie carets
  (@4569-4570). Both fixed; the caret polylines also get the binary's
  explicit 4th closing point (@4558).
- The screenshot otherwise CONFIRMS our layout (yellow range bar, green
  caret, colored pip dots, tick tape + green bowtie, rotating compass,
  mid-ladder range with no lock = the world-pick terrain range).
- Its crosshair is ~2.5x taller with arrowhead arms -- NOT our binary's
  +-0.04..0.16 program: a different pod revision. Ours stays [T1] to
  BTL4OPT.EXE 4.10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:49:54 -05:00
arcattackandClaude Fable 5 2250b01c6b Fire-channel pulses moved out of the enemy block (post-kill firing froze)
User report: after killing the enemy, wandering around, weapons won't
fire. Not a lock requirement -- the trigger pulse generator (the
1,0,1,0 edge feed for CheckFireEdge) sat inside `if (gEnemyMech != 0)`.
Once the wreck buried and gEnemyMech nulled, the channels froze at
their last value -> no rising edges -> no fire, even with a valid
terrain pick in the target slot. Latent since the world-pick model made
no-enemy firing meaningful (task #41); the weapon-group split kept the
pulses in the same wrong place.

The pulses now run unconditionally (before the enemy block); the
weapon's own HasActiveTarget gate remains the only fire arbiter --
terrain downrange fires at the scenery, a true sky shot (nothing within
1200) authentically refuses.

Verified: BT_AIM=0.5,0.3 (terrain-only picks, enemy never aimed at) ->
sustained fire, 123 laser + 82 PPC beam samples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:37:05 -05:00
arcattackandClaude Fable 5 14c0dd06f5 Three-trigger keyboard weapon groups: 1=lasers 2=PPCs 3=missiles (task #43)
Interim pod-like grouping ahead of the authentic ConfigureMappables/
ChooseButton mapper channels (decomp-anchored in open-questions):
- Three fire channels like three pod buttons: gBTWeaponTrigger (lasers,
  key 1 or SPACE), gBTPPCTrigger (PPCs, key 2), gBTMissileTrigger
  (missiles, key 3 or CTRL). Keyboard only per user (mouse buttons
  removed).
- EmitterSimulation picks its channel by classID (PPC 3028 vs Emitter
  3016); BT_AUTOFIRE holds all three.
- The BT_BEAM_LOG sampler modulus 30 aliased to ONE weapon under the
  new synchronized 5-beam volleys (5 | 30) -> 31 (coprime).

Verified: all 5 energy weapons fire (31 samples each), aimed hits land,
kill completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:31:16 -05:00
arcattackandClaude Fable 5 ae8aca0f3f KB: weapon groups decomp-anchored (ConfigureMappables/ChooseButton)
Answering "do lasers and PPCs fire together?": in the pod, grouping was
a PILOT CHOICE. MechWeapon message handlers id 9 'ConfigureMappables'
(FUN_004b9550) and id 10 'ChooseButton' (FUN_004b95b8), handler table
@0x511860 (names read from the exe): ConfigureMappables binds the
weapon's fireImpulse@0x31C to controls-mapper fire-channel #N (roster
slot 0; vtable +0x38 register / +0x3c unregister / +0x44 choose),
ChooseButton = the in-cockpit re-binding. Weapons sharing a button fire
together. The port's SPACE=energy / CTRL=missiles split is a stand-in
default; the authentic per-button channels are a tracked follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:22:59 -05:00
arcattackandClaude Fable 5 4e7561714e HUD: per-weapon pip data dump + the real authored ranges recorded
[hud] pip diagnostic logs each registered pip's classID/PipPosition/
WeaponRange/PipExtendedRange/PipColor. The live dump corrects the
"everything is 500m" assumption: BLH lasers red @500 (x3, ext=1),
missiles amber @800 (x2), PPCs blue @900 (x2) -- the pips sit at their
weapons' max-range marks on the range ladder (caret below a pip = that
weapon reaches), so 7 weapons read as 3 weapon-system groups. KB +
comments corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:14:18 -05:00
arcattackandClaude Fable 5 5038d64382 Damage range gate: the authentic per-weapon effectiveRange (was a 100u stand-in)
User report: lock at spawn range (~120) but no damage until closing in.
The bring-up damage gate used kWeaponRange=100 -- a placeholder from
before the weapon reconstruction -- while the authored BLH weapon range
is 500 m. The binary's rule (FireWeapon @004bace8:7758 [T1]): damage
applies when dist <= the weapon's EFFECTIVE range = (1 - host-zone
damage) x authored WeaponRange -- the per-weapon targetWithinRange flag
UpdateTargeting computes each frame. The gate (and the [target] log's
IN WEAPON RANGE annotation) now reads that flag across the weapon
roster; kWeaponRange removed.

Verified: stationary at spawn range 120 -> IN WEAPON RANGE + 34 aimed
zone hits (was 0 until closing under 100).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:11:03 -05:00
arcattackandClaude Fable 5 cb110310ba Range caret: the authentic 500 m/s slide (HudSimulation :5652-5670)
The right-ladder caret reads range-to-pick, and with the world-pick
model the pick is usually the ground downrange -- so walking (cockpit
bob + turning sweeping the boresight across near/far terrain) made the
caret teleport frame to frame ("going all over the place", user
report). The binary rate-limits the DISPLAYED range: shown +=
clamp(true - shown, +-dt*500) -- a 500 m/s slide, applied in
HudSimulation, including toward the no-target 1200 default
(_DAT_004b7ed0 confirmed 0 = the abs() idiom). Implemented in mech4's
HUD feed where dt is in scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 07:47:28 -05:00
arcattackandClaude Fable 5 305c928923 Death effects dispatch at the victim's death transition (task #42)
User report: enemy takes hits, starts smoking, then beams pass through
-- no more damage, no death. Root cause: the death-effects dispatch
(blhdead explosion chain + wreck swap + KILL score) lived in the
SHOOTER's laser fire block kill check. A missile killing blow lands
frames later in BTUpdateProjectiles -- outside that block -- and after
task #41 the boresight pick skips a dead mech, so the fire block never
ran against it again: the death chain was silently skipped, leaving an
internally-dead, smoking, standing, invulnerable mech.

Fix: the dispatch moved to the VICTIM's own once-per-death transition
(UpdateDeathState's movementMode->9 moment) -- fires exactly once for
ANY kill source (laser, missile, collision). Killer id for the
Explosion message = lastInflictingID (the task-#31 bookkeeping). The
BT_ENABLE_TEARDOWN cdb harness was removed with the old block (findings
preserved in docs/HARD_PROBLEMS.md).

Verified headless (75s soak, missile-heavy kill): destroyed ->
'blhdead' id=22 -> wreck swap 'blhdbr.bgf' -> smoke re-arm -> sink ->
buried INERT; post-death picks fall through to terrain; 0 crashes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:21:03 -05:00
arcattackandClaude Fable 5 e30a61a62f World-pick targeting: 0x388 = whatever is downrange -- mech, terrain, or nothing (task #41)
User video evidence (lasers firing at nothing) + the colleague's
torso-locked report + the binary all reconcile into one model:

The boresight pick hits the WORLD, and the target slot mech+0x388 holds
whatever entity is downrange:
- enemy mech under the boresight -> aimed target (hull point -> STEP-6
  zone under the boresight, hotbox + lock ring, damage);
- else the TERRAIN downrange (BTGroundRayHit to 1200) -> the beam and
  missiles fire at the scenery ("firing at nothing", as in the pod
  videos), range caret reads the ground distance (authentic :5639), no
  damage;
- else (sky) -> no target, and the weapon's own double 0x388 gate
  refuses the discharge.

Binary evidence for non-mech targets: HudSimulation :5620 explicitly
handles a target WITHOUT damage zones (target->0x120 == 0) -- dead code
if only mechs were ever targeted. The pick is automatic every frame
(0x388: 11 reads / 0 direct stores in CODE).

Implementation: gBTTerrainEntity captured in MakeEntityRenderables
(Terrain::GetClassDerivations); mech4 pick = enemy PickRayHit else
ground ray -> terrain entity + ground point; damage/hotbox/lock only
for the mech pick; the task-40 enemy auto-converge REMOVED (facing away
now fires at the scenery, not magically at the enemy -- the user's
complaint). Verified all three states headless (BT_AIM 0,0 / .6,.5 /
0,-.8): aimed zone hits / terrain beams + 0 damage / no discharge.

Fourth (and evidence-complete) targeting model iteration; KB updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:06:00 -05:00
arcattackandClaude Fable 5 60ed54e008 Auto-target fire model: firing needs a target, NOT a manual lock/pinpoint (task #40)
Colleague: you fire without a lock, at "nothing." Double-checked the
binary:
- The weapon fire path is DOUBLY gated on mech+0x388 != 0:
  EmitterSimulation (FUN_004baa88:7689) only calls FireWeapon with a
  target, and FireWeapon (FUN_004bace8:7727) wraps its whole body --
  including beamFlag(+0x46c)=1 -- in the same check. So no target => no
  beam, literally (cannot fire into truly empty space). [T1]
- BUT a capstone scan of the entire CODE section finds 11 READS of
  +0x388 and ZERO direct stores: the target is written INDIRECTLY (a
  message/selector), i.e. AUTO-acquired -- there is no manual lock to
  fire. The spinning-ring LOCK (HudSimulation 5619-5634) is a separate,
  stricter state.

So the colleague is right that no lock is needed; my port was wrong to
gate firing on a pinpoint boresight-on-hull pick. Fix: mech+0x388 = the
enemy whenever it is ALIVE (auto-target); firing needs only that. A
pinpoint hull hit upgrades the shot to aimed-zone damage + the lock
ring; off-hull with the enemy present, the beam converges on centre
mass (body hits). Third correction from over-reading the RP-shared
Reticle struct (after sticky-lock and mouse-cursor).

Verified: BT_AIM="0.5 0.2" (off-hull) -> fires + lands damage (was 0
before); BT_AIM="0 0" (centred) -> HOT-aimed zone hits. KB swept;
checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:49:29 -05:00
arcattackandClaude Fable 5 a8a56c57c9 Targeting correction: reticle = TORSO BORESIGHT, not a free mouse cursor (task #39)
Colleague flagged that targeting is torso-locked with no fine cursor.
Correct: the pod stick's yaw drove the TORSO TWIST
(MechControlsMapper -> HUD::SetFreeAimSlew(stick_x) @cockpit+0x28C,
gated on torso-horizontal-enabled; hud.hpp:167) and reticlePosition
@HUD+0x1FC is COMPUTED by HudSimulation from the mech pose quaternion +
target geometry, zeroed to centre with no target (part_013.c:5680) --
never a free-floating cursor. The engine Reticle struct is general
(shared with Red Planet); BT drives it from the torso boresight.

Fix:
- Removed the mouse-cursor slew (a mis-sourced stand-in from the
  RP-shared struct). The crosshair is now the torso boresight:
  BTTwistToReticleX(torsoTwist) = tan(twist) projected through the live
  per-axis projection. Dead-centre on the fixed-torso BLH
  (TorsoHorizontalEnabled=0); you aim by steering the whole mech.
- BT_AIM="x y" retained as the headless test harness.

Verified: face-to-face spawn -> HOT lock + aimed zone hits (36);
BT_FORCE_TURN circling -> 31 no-target vs 1 HOT (steering off-target
drops the lock, as it must). KB swept (combat-damage / gauges-hud /
open-questions); checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:29:42 -05:00
arcattackandClaude Fable 5 24ea7f13af HUD ironing (task #38): authentic Lock producer + simple-X mode
- LOCK rules transcribed from the exported HudSimulation
  (part_013.c:5619-5634): lock requires a target AND your own HUD host
  zone damage < 0.75 (_DAT_004b7ec4 -- a shot-up targeting computer
  drops lock) AND the targeted zone damage < 1.0 (_DAT_004b7ec8;
  whole-mech target checks zone 0 -- a wreck's dead zone can't
  re-lock). Box and ring are now separate signals like the binary:
  gBTHudLockState 1 = target held (hotbox draws), 2 = LOCKED (+ the
  spinning ring).
- SIMPLE-X mode: the ctor's [0x99] minimal-reticle list transcribed
  (@4689-4705: green +-0.02..0.08 cross on the aim translate); Draw
  switches master <-> simple X on the PrimaryHudOn element bit (0x20),
  completing the recovered Execute's state-list logic.
- Canopy diagnosis sharpened (open-questions): blx_cop is the torso
  segment's inside-skeleton mesh enclosing the eye; the black box =
  the skeleton render branch missing its texture + PUNCH cutout (the
  canopy windows are punch texels). A rendering-branch task; stays
  hidden by default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:12:58 -05:00
arcattackandClaude Fable 5 18d49491b7 Reticle Execute @004cdcf0 RECOVERED: every HUD instrument now live (task #37)
The one un-exported gap in the reticle chain, read via capstone
(tools/disas2.py; the annotated disasm preserved at
reference/decomp/reticle_execute_004cdcf0.disasm.txt). Draw() is now a
transcription of the real per-frame logic, and the HUD attr-table names
(hud.hpp ids 4/5/6/8/A/B/C/D) are CONFIRMED by their Execute usage:

- Range ladder: BAR from ladder-top to the caret + caret translate.
- The bottom 21-tick tape is the TORSO-TWIST indicator (NOT heading):
  deflection = -/+(span/2) x (RotationOfTorsoHorizontal / twist limit),
  attrs 4/5/6. Fixed-torso BLH reads centred -- authentic static.
- The circle-with-stem is the COMPASS (attr 0xD, rad->deg rotation) at
  (botX, botY - 3*tickMajor - 0.03), with the THREAT trail (attr 0xC)
  in its rotated frame: 0.05-unit attack-direction marks, fresh < 2s
  red, expiring at 6s. Port feed: player TakeDamage pushes the impact
  direction (dormant vs the passive test dummy).
- Pips (composed into subB6): hidden when destroyed (attr 1 == 1), LIT
  when the fire cycle is LOADED (attr 0x1c == 2; port source
  rechargeLevel >= 1) else the dark charging ring; filtered by the
  weapon-GROUP bits (weaponMode & elementMask&0xF). Range plays NO part
  (the stored TargetWithinRange slots are never read by Execute).
- Lock ring: subB9 at frame centre SPINNING 4 deg/frame while locked
  ([0x9d] is the spin matrix, not a heading list).
- Target HOTBOX: a rectangle hugging the projected extents (x+-4 around
  the top-centre hotbox point, +1/-11.5 vertical; baked K=2.8145 -- the
  port projects through the live per-axis projection), switching to the
  edge arrows past +-1.6 or behind (BTProjectHotBox, L4VIDEO).
- Reticle state Off/On + PrimaryHudOn full-HUD/simple-X switch, aim
  translate on slew move, 3D marker + PNAME player-name mesh identified
  (chain still deferred).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:38:50 -05:00
arcattackandClaude Fable 5 cf9f56044e Aim coordinates: route all conversions through the true NDC (fixes the
sideways lock offset)

The windowed present stretches the fixed 800x600 backbuffer into the
client area while the projection's aspect follows the CLIENT (resize
rebuild) -- so the dpl2d/reticle frame, the client frame and NDC only
coincide for a backbuffer-shaped window. The mouse mapping and the
square-frame ray shortcut (cx = rx*tanHalfFov) were exact at screen
centre and drifted outward with a side-flipped sign: aiming left of the
enemy locked as if right of it (user report).

- BTSetAimProjection now publishes BOTH proj scales (P11 carries the
  aspect) + the backbuffer size (the dpl2d frame). NOTE: GetViewport at
  the loop top holds the PREVIOUS frame's last pass viewport (gauge
  passes shrink it) -> use the renderer's own GetWidth/GetHeight.
- BTGetAimRay / BTProjectToReticle convert reticle <-> NDC per axis
  (ndc_x = rx * vh/vw; camera x = ndc_x/P11) -- exact for any window
  shape and FOV.
- New BTClientToReticle undoes the present stretch for the mouse
  (client px -> viewport px -> reticle coords); mech4 uses it instead
  of the raw client-height mapping.
- 1Hz aim telemetry in the [target] log (hits/noRay/noPick): verified
  100% pick hit rate once the projection is live (the pre-visibility
  mission-load seconds report noRay -- that was the "flicker").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:13:45 -05:00
arcattackandClaude Fable 5 9666dc5b5e Targeting: lock mirrors the pick per frame (sticky designation removed)
The sticky lock made aim irrelevant after the first acquisition (the
crosshair starts on the face-to-face enemy, so it locked at spawn and
never let go -- played as auto-targeting). The binary treats no-target
as a frequent live state (fire path re-checks 0x388!=0 at every step,
part_013.c:7689/7727; HUD range feed has a permanent no-target default,
:5636), so the target slots now mirror the reticle pick each frame:
locked while the crosshair is ON the mech, no lock otherwise. Keeping
the crosshair on the enemy is the gunnery.

Verified headless: BT_AIM="0 0" -> HOT + aimed zone hits; BT_AIM="0.6
0.4" -> no lock, zero damage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 21:08:08 -05:00
arcattackandClaude Fable 5 d78bde066d Authentic target acquisition LIVE: reticle slew + pick-ray lock + aimed zone damage (task #36)
The engine Reticle model (MUNGA/RETICLE.h [T0]) reconstructed end to end:
- Mech::targetReticle is a real Reticle member bound to the TargetReticle
  attribute (0x1d), per the RP VTV analog (VTV.h targetReticle).
- Crosshair slew: mouse -> client rect -> reticle coords (the pod stick
  free-aim channel's dev-box stand-in); BT_AIM="x y" pins it headless.
  LMB fires lasers / RMB missiles (alongside SPACE/CTRL).
- Pick ray: the ACTIVE eye publishes pos + LookAtRH basis (BTSetAimCamera,
  L4VIDRND view-write site) + the render loop publishes proj._22;
  BTGetAimRay builds the world ray, Mech::PickRayHit slab-tests it against
  the collision template's ExtentBox via the engine's BoundingBox::HitBy
  (local frame; clips the Line at entry) -> world hull point.
- Designation: the mech under the crosshair designates (sticky; re-hover
  refreshes; cleared when the target leaves the roster at burial); the
  entity target slots 0x37c/0x388/0x38c feed the whole weapon path.
- Aimed fire: while HOT the impact point is the PICKED hull point -> the
  STEP-6 cylinder lookup resolves the zone under the crosshair (verified:
  center-aim -> head-band zone 13 dominant). Off-crosshair the sticky
  designation converges on center mass.
- HUD: the aim group draws at the slewed position ([0x9a] translate,
  contained by push/pop); the designator ring tracks the target's
  projected point (subB9 hot / subB8 designated, BTProjectToReticle);
  edge arrows when off-screen/behind.
- AUTHENTIC gating: no fire arc exists in the binary (FireWeapon fires
  whenever HasActiveTarget, part_013.c:7758) -> BT_FIRE_ARC is now an
  explicit OPT-IN presentation clamp; the hardwired gEnemyMech lock and
  the projectile path's gEnemyMech fallback are removed.
- Fixed en route: every renderable rebuild stomped mCamera back to the
  chase eye (start-inside silently lost the cockpit camera; the aim feed
  exposed it). BTL4VideoRenderer::mViewInside persists the chosen view.

Verified headless: BT_AIM="0 0" -> HOT lock, pick hits the hull face at
exact range, aimed zones resolve; BT_AIM="0.8 0.3" -> no lock, zero
damage, zero missile launches; kill chain completes to wreck + smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:57:11 -05:00
arcattackandClaude Fable 5 6988821525 KB: authentic targeting model recovered (Reticle pick-ray, RETICLE.h [T0])
The engine Reticle struct (the mech's "TargetReticle" attribute) is the
acquisition chain: slewable screen-space crosshair -> pick-ray -> the
entity + damage zone under it become mech+0x388/0x38c, the intersection
point mech+0x37c. Convergence-on-lock is authentic (FireWeapon has no
aim test); targetWithinRange = dist < (1-damage) x weaponRange
(FUN_004b9bdc). Port acquisition is a bring-up stand-in (hardwired lock
+ BT_FIRE_ARC cone) -> tracked as the next combat-fidelity step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:23:24 -05:00
arcattackandClaude Fable 5 48b17750e5 HUD reticle + weapon pips LIVE: dpl2d 2D display-list port (task #35)
- dpl2d API fully recovered from the binary recorders (@487f34-488630):
  opcode model (points/lines/polyline/circle/color/width/matrix/push-pop),
  CallList = INLINE include (state persists to caller), centered coordinate
  frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework.
- BTReticleRenderable ctor @004cc40c transcribed with the authentic
  calibration (originX .35, originY .25, scaleY .5, 0..1200m right range
  ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings,
  turn arrows); range caret slides from the live target range fed by the
  mech4 targeting step (BTSetHudTargetRange).
- Weapon pips: the binary gate is IsDerivedFrom(0x511830 =
  MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing
  weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL
  7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A
  (lit, authored PipColor) on TargetWithinRange, else dark ring B.
- AddWeapon @004cdac0 store map corrected to the verified order
  (part_014.c:4827-4837); both state attrs are literally named
  "SimulationState" (strings @51d526/51d577) -> weapon simulationState.
- Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster
  (0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag
  table added to context/decomp-reference.md.
- Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary
  Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked
  in context/open-questions.md with the blx_cop canopy + PNAME pip meshes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:05:44 -05:00
arcattackandClaude Fable 5 1cd8ca1a80 HUD phase 1: full dpl2d API recovered + opcode-faithful 2D layer rework
Every dpl2d_ recorder in the binary self-identifies via its debug-name string
(part_010.c 0x487f34..0x4888c0) -- the complete API + opcode map is recorded
in phases/phase-02-dpl2d-reticle.md: point sets (2/3), closed polylines (4/5),
open line strips (6/7), AddPoint (8), AddCircle (9), SetColor (0xF),
Set/ConcatMatrix (0x10/0x11, 2x3 affine), Push/PopState (0x12/0x13),
SetLineWidth (0x15), CallDisplayList (nested glyphs), FullScreenClipRegion.

dpl2d.cpp reworked to that model: command-stream recorder (open-primitive
vertex runs, state commands, nested list calls) + a recursive ExecuteList
rasteriser (XYZRHW points/strips/loops/circles, 2x3 transform + state stacks,
save/restore).  CORRECTION: the old "PushMatrix/MoveTo/PopMatrix" trio was a
misreading of OpenPolypoint/AddPoint/ClosePolypoint ("draw a point") -- kept
as aliases mapping to the true semantics.

Reticle findings recorded: the ctor (@004cc40c) builds ~15 display lists (the
dotted-cross reticle, tick ladders via FUN_004cd938, side arrows, arcs) plus a
3D marker chain and the PNAME1-8.bgf pip meshes (all ship).  Next: transcribe
ctor+Execute, wire the 1996 caller's AddWeapon arguments, hook the draw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 19:32:36 -05:00
arcattackandClaude Fable 5 56f02b2176 Cockpit view live: V toggle, camera gate, real EyepointRotation, inside skeleton
- V toggles the authentic cockpit eyepoint <-> the chase camera.  Both eyes
  coexist; DPLEyeRenderable::Execute now writes the VIEW only when it IS
  mCamera (unconditional writes let the last-executed eye stomp the toggle).
- EyepointRotation is a real zeroed EulerAngles member on Mech (was bound to
  the shared junk attrPad -- the eye composes this attribute into the view
  EVERY frame, so the cockpit camera was rotated by garbage: the canted
  horizon, then the black screen).  The eye-slew systems write it later.
- The cockpit eye mounts at the eyepoint's REST position with a clean upright
  forward basis on the tree root (the live joint chain fed it the site tilt +
  torso pose; authentic pitch/yaw is the deferred gyro eye chain).
- The inside view swaps the player to the INSIDE skeleton mesh set
  (SkeletonType_A): 19 body segments hide, exactly one mesh remains --
  blx_cop.bgf, the authentic cockpit canopy shell around the eyepoint.  It
  currently renders as a black enclosure (the black-screen report), so it is
  HIDDEN pending its interior/punch-material rendering (BT_INSIDE_COCKPIT=1
  shows it for that work).  Damage gstates respected in both directions;
  RemakeEntity keys off the DISPLAYED skeleton.
- Dev: BT_START_INSIDE=1 starts in the cockpit view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 19:23:07 -05:00
arcattackandClaude Fable 5 c2f70f6348 Beams CUT on a destroyed target (the real FUN_0049fb54, was a mislabeled stub)
Answers "can I keep targeting the dead mech?": the lock isn't cleared by
anything we've found, but the binary REFUSES to sustain energy beams on a
destroyed mech: ServiceDischarge (@004ba8d0) and ContinueDischarge (@004baa20)
check the beam's target EVERY discharge frame -- IsDerivedFrom(Mech) then
Mech::IsDestroyed (FUN_0049fb54 = movementMode 2||9) -- and kill the beam the
moment it dies.  EmitterSimulation (@004baa88) applies the same check to the
OWNER (a dead mech's own weapons drop everything).

Our recon had FUN_0049fb54 as a mislabeled "cockpit/HUD query" no-op stub
called with NULL, and never set the beam's target (0x474) at fire -- all three
authentic gates were dead.  Now: FireWeapon stashes the owner's target entity;
both discharge paths + the owner gate run the real check (BTMechDestroyed).

Verified: 40 beam samples before the kill, 0 after -- lasers flash-fail on the
wreck instead of cutting it like butter (heat is still spent per the binary's
FireWeapon, which fires blind; only the DISCHARGE checks the corpse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:46:08 -05:00
arcattackandClaude Fable 5 cba4c3098f scratchpad: bgfbounds pointed at the beam models (the natural-width evidence)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:38:32 -05:00
arcattackandClaude Fable 5 31626a3b97 Beams at the models' NATURAL width, per-weapon tube models (de-cartooned)
The "cartoonish" beams: the port drew the ermlaser tube INFLATED -- a 3.0x
glow + 0.9x core two-layer hack, 13x the authored radius.  Parsed the beam
models: ERMLASER/MLASER 0.22u radius, SLASER 0.11, LLASER 0.32, PPC 0.62 --
thin pencil beams with an authored per-class size progression; the PPC bolt
is genuinely ~3x fatter with its own 62-vert shape.

Now: ONE draw per beam at natural model scale (width multiplier 1.0), using
the weapon's OWN tube model (ermlaser for lasers, ppc.bgf for PPCs; the
s/m/llaser tubes load for other mechs' loadouts).  BTPushBeamKind carries the
model selector; BTPushBeam keeps its signature (kind 0).  Tint = 40+215x the
authored PipColor; the thin natural tubes stay under additive saturation so
the scrolling grit reads without a hand-dimmed core layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:33:20 -05:00
arcattackandClaude Fable 5 2036e7da51 Per-weapon beams from the REAL Emitter sim state (authentic per-mech fire)
The visible beams now come from each weapon's own live state instead of the
hardcoded single-look stagger: FireWeapon arms beamFlag + dischargeTimer (the
weapon's authored DischargeTime), ServiceDischarge ends the window, recharge
gates cadence -- so volley-vs-stagger patterns, cadence and colours all emerge
from each mech's real loadout (BLH: 3 lasers + 2 PPCs).  Per weapon: live
muzzle (its own mount; gun-port ordinal fallback when the mount doesn't
resolve -- GetMuzzlePoint returns the feet otherwise), the fire's stored world
endpoint, and the authored PipColor (lasers red 1,0,0 / PPC blue 0,0,1
straight from the .SUB data).  PPC draws as a thicker, brighter bolt.

Filter is by EXACT classID (Emitter 3016 / PPC 3028) -- the derivation check
matched shared recon-stub chains (a Sensor and the MissileLauncher passed and
drew garbage beams from misinterpreted offsets).

The target slot (MECH_TARGET_POS) now carries the TORSO aim point (all
consumers -- beam endpoints, missiles, reticle -- want center-mass, not the
ground between the feet); the projectile fallback lift moved to match.

Accessors: Emitter::BeamOn/BeamEndpoint, MechWeapon::MuzzlePoint/PipColor.
BT_BEAM_LOG=1 samples the live per-weapon beam state.

Verified: [beam] PPC mz=(...,4.49,...) end=(...,7,...) rgb=(0,0,1) -- gun-port
muzzles, torso endpoints, authored colours; kill chain intact (10 hits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:06:59 -05:00
arcattackandClaude Fable 5 a834866fcd Enemy spawn faces the player (was copying the player's pose -> back turned)
The dummy spawned 120u along the player's forward with the SAME orientation,
so both mechs faced the same way and the player stared at its back (also why
first shots read as rear hits).  Flip the enemy's yaw 180 deg about Y (engine
convention: Z basis = (sin y, 0, cos y) -> yaw = atan2(z.x, z.z) + pi) so the
two spawn face to face.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:39:15 -05:00
arcattackandClaude Fable 5 ebeadba78e Verified the BLH loadout (missiles ARE authentic) + split weapon controls
BT_ROSTER dump (the shipped subsystem stream is the authority): the pod
Blackhawk mounts 5 Emitters (two laser types) + 2 MissileLaunchers each with
its own AmmoBin -- the periodic missile volleys are the real loadout firing
on its own recharge, NOT a placeholder weapon.

What WASN'T authentic: one key firing everything (the bring-up collapse of
the controls mapper).  Interim split toward the real weapon groups:
  SPACE = energy weapons (lasers)
  CTRL  = missile launchers (the projectile-weapon channel)
New gBTMissileTrigger pulse; projweap's fireImpulse moves onto it; the
BT_AUTOFIRE harness drives both channels.  The full controls-mapper weapon
grouping stays the deferred authentic reconstruction.

Also: BT_ROSTER=1 one-shot loadout dump (class name + id per subsystem).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:35:44 -05:00
arcattackandClaude Fable 5 a2333de7c1 Buried wreck: PARK the collision volume (count-zero alone doesn't unblock)
The mech-vs-mech gather (Mover::GetCurrentCollisions) tests
mover->collisionVolume DIRECTLY -- it never consults collisionVolumeCount --
so the previous inert fix left the stale box blocking at the wreck spot.
The count-zero now serves only to stop MoveCollisionVolume from re-placing
the box, and the box itself is parked 100km underground where it intersects
nothing.  Verified: the autodrive harness, previously pinned at ~7m from the
enemy, drives straight through the spot after burial (341,155 -> 992,-971).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:19:35 -05:00
arcattackandClaude Fable 5 9b5004c39d Missiles: authentic dsrm smoke trail + torso aim (placeholder tracer removed)
The "white lines toward the feet" report: the LAUNCHES are authentic (the
mech's real MissileLauncher/ProjectileWeapon roster subsystems firing on
their own recharge cadence -- that's the every-Nth-shot rhythm), but the
visual was the bring-up 3-segment beam tracer, and the aim point was the
victim's ORIGIN (ground level between the feet).

- Trail: per-frame puffs of the AUTHENTIC dsrm effect (psfx 0, "the lrm
  smoke trail") along the flight path, frame-oriented so the smoke streams
  behind the round (its .PFX velocities are +Z = backward); the round itself
  is a short hot streak.  New BTPfxTrailPuff spawns slot particles directly
  at a moving point (no emitter instance).
- Aim: the resolved target point lifts to torso height (the beams already
  converged there) -- impacts now cyl-resolve to body zones instead of
  diving at the feet/legs.

Follow-up noted: the round MESHES (BULLET/LRMS.BGF ship) for a true
model-per-projectile look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:15:03 -05:00
arcattackandClaude Fable 5 bf87360c42 Buried wreck goes INERT: collision off + target lock dropped
Answers "it's still there but invisible": not normal -- the real game removes
the dead entity (death row).  Full entity teardown needs the mech render tree
unhooked from the renderer first (the remaining P5 follow-through), so until
then the burial transition makes the wreck behaviorally gone:
  - collisionVolumeCount = 0 (the collision gather skips volume-less movers;
    MoveCollisionVolume early-outs) -> no phantom blocking
  - the player's target lock drops -> beams stop converging on / hitting the
    empty spot, no phantom impact smoke
Verified: zero [damage] hits after burial; INERT fires one-shot at
"wreck buried".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:01:46 -05:00
arcattackandClaude Fable 5 5236d4263a Wreck sink (quadratic burial) + ldbr debris field -- the 1996 script completed
Answers "does the wreck fade away?": YES, by sinking.  FUN_00456410 (the 1996
sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is
-0.025 -> the ~7-unit hulk is fully underground ~17s after the kill.  The
script also pairs the standing hulk with the LDBR strewn-debris field (12x13u
flat scatter), parented together and sinking together.

Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact
torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble
sculpt.  The "just a leg standing there" report is the authentic art.
(THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices;
more evidence the hardcode was an unfinished dev shortcut.)

Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the
quadratic sink per frame (driven from the dead mech's UpdateDeathState),
hides both pieces at burial and reports it so the wreck-smoke re-arm stops
with the wreck.  DPLStaticChildRenderable::SetOffsetTranslation added
(Execute re-reads OrientationMatrix per frame -- same in-place idiom as
SetDrawObj).

Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm
@10s -> wreck buried @~17s -> smoke stops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:55:01 -05:00
arcattackandClaude Fable 5 c0fa6bf21a Death = the burning WRECK-HULK swap (effect 104) -- the authentic chain, reconstructed
The user was right: a dead mech turns into a pile of wreckage.  The authentic
1995 chain, recovered end-to-end:

  kill -> the victim's per-mech DEATH ModelList ('blhdead'/'lokdead'/'owndead'/
  'thrdead', .RES ids 22-25) -> its authored effects: 104 (the WRECK script) +
  1007 (dnboom big explosion) + 1001 (ddthsmk rubble smoke plume) + a damage-
  smoke burst (3/4/5/15).

Effect 104 = ExplosionScripts case 4 (part_008.c:2663, LIVE in the 1996 binary;
the "disabled" warning is case 6): loads the destroyed hulk + flamesml/flamebig
flame meshes with sweep flicker.  Every mech ships its hulk (BLHDBR/MADDBR/
LOKDBR/... + GENDBR generic); the 1996 script hardcoded thrdbr.bgf (dev
shortcut) -- we use the victim's own.

Reconstructed as BTL4VideoRenderer::SwapToWreck: hide every segment mesh, hang
"<prefix>dbr.bgf" on the tree root (pending-swap if death precedes tree build);
routed from the engine's ExplosionClassID dispatch (effect 104 ->
BTSwapMechToWreck(explosion->GetEntityHit())).  The kill now fires the
authentic 'blhdead' resource (manual 7+1 pfx calls removed -- the list carries
1007/1001 itself).  Verified live:
  [death] firing authentic death list 'blhdead' id=22
     ** effect_number = 104
  [BTrender] wreck swap: victim -> 'blhdbr.bgf'

Follow-ups noted: mesh flames + hulk settle (cosmetic), DeathSplash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:43:08 -05:00