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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:47:03 -05:00
arcattackandClaude Opus 4.8 93456be051 MP: FIX keyboard-driving peer skip -- non-pose records no longer kick the dead-reckon clock (task #50)
The discriminating user report: peer motion was smooth under the autonomous
harness but skipped when driven by KEYBOARD -- even pure walking / pure spinning.
The harness pins throttle/turn constant; the keyboard SWEEPS the throttle lever
every frame of a key-hold (mech4 sLever integrator), so mapper->speedDemand
changes every frame, and the speed-deadband gate (authentic exact !=) fires a
type-2 speed record EVERY FRAME for the whole accel/decel.  The base reader
Simulation::ReadUpdateRecord stamps lastUpdate=Now() on EVERY record (flagged
'HACK - should be based upon message->timeStamp' in the 1995 source) -- so each
type-2 SHRINKS the peer reckoner's projection span (nextUpdate-lastUpdate)
without refreshing updateOrigin: the position target jumps backward toward the
stale origin, the next pose record yanks it forward -> target oscillation every
frame during any input sweep.  Matches the session-long 'worst on accel/decel'.

FIX (mech.cpp Mech::ReadUpdateRecord): non-pose records (2,3,5,6,7,8) preserve
lastUpdate around the base call (keeping the real payload, simulationState).
Pose (0) and resync (4) keep their authentic clock behavior.  BT_T2_CLOCK
restores the old stamping for A/B.

REPRO HARNESS (mechmppr.cpp): BT_FORCE_SWEEP=<period> triangle-sweeps the forced
throttle 0.2..0.9 -- a type-2 record per frame, the keyboard-skip repro the
constant-throttle harness could never produce.

Verified A/B, autonomous circle+sweep (the keyboard regime):
  old clock:  worst frame spike 10.4x avg, path/net ratio 1.069 (backtracking)
  guarded:    worst 4.6x, ratio 1.0022 (no backtracking)

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:30:18 -05:00
arcattackandClaude Fable 5 a3b735d5da MP: replicant motion chop ROOT-CAUSED + FIXED -- stale peer-mirror spammed re-base
records; also the 2007 call-counter clock stub + two crash fixes

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

THREE layered defects found:

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:08:47 -05:00
arcattackandClaude Fable 5 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 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
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00
arcattackandClaude Fable 5 065c114590 Impact-FX FORENSICS wave: the i860 specialfx engine, per-round detonations, the ram economy closed
The "we're off the rails" reinvestigation -- a 5-thread evidence workflow
(logs / PFX data / RES model lists / decomp / port audit) + the i860 firmware
decode, then surgical fixes.  Every claim tiered; the wrong turns are on the
record in the KB alongside the corrections.

THE OLD-STYLE SPECIALFX ENGINE [T1, firmware-decoded from VREND.MNG]:
rebuilt scratchpad/i860dis.py (binutils opcode table); mapped the dispatch
(data+0xdd0c; sfx trigger 0xf040cda0, install 0xf040cdc0, step ~0xf0413698,
instance init 0xf04128d8); decoded the heat model EXACTLY: per 30Hz board
frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch - 0.25) + 0.25 (K=0.25 =
the ember floor @VA 0xF080; kill at h <= 1e-4 @0xF0A0); alpha fades cool_b
x RAW dt (PER-SECOND -- the x30 scaling made laser hits invisible); 7s cap;
y_off = the kill plane; "variance" is DEAD DATA (binary reads "variance",
INI authors "varience").  The 13 descriptors (PPCHit/LaserHit/MissileHit/
Chunks/Sparks/Fireball...) parse from BTDPL.INI and render via the BTPfx
layer -- heat bursts draw the FIERY sheet (brightness over fire), .PFX keeps
GRAYSCALE (authored colours: DNBOOM orange, DDAM gray).

HIT-PACKAGE CENSUS [T1, RES byte-verified]: ppchit=[8] lzrhit=[9] mghit=[7,11]
canhit=[11] mslhit=[10,12,1023] explode=[6]; all 8 mech death lists identical.
Effect routing corrected in BOTH consumers: <100 = specialfx, >=1000 = psfx.

"SILVER MIST x5" ROOT CAUSE: SHKWAVE.PFX (mslhit's 1023) authors maxIssue=5
relPeriod=0.2 rate=1 -- the ONE file where rate contradicts the window; the
emitter trusted rate -> 5 shells at exactly 1Hz.  Emission rate now always
maxIssue/releasePeriod.

PER-ROUND DETONATIONS [T1 @004bef78]: every missile round spawns its own
ExplosionModelFile at ITS impact (hull + terrain) -- a volley ripples 12
fireballs like the demos; rack-tube launch spread [T3] (GUIDED rounds only --
the slot-0 deflection sent every AFC100 shell 3.4deg left/2.5deg down: the
phantom "4th gold beam"); replicant salvos detonate too (launcher index rides
the visual push).  Missile damage bundles through the shooter's messmgr with
the launcher's subsystemID (mslhit fires at the consolidated point; the
binary's dedup CONFIRMED [T1 @0049b784] -- a workflow agent's per-record
claim REFUTED by direct decomp read).

DAMAGE-BAND SEMANTICS [T1]: MechDeathHandler fires the CURRENT band effect on
any damage rise (the binary's changed-flag coalescing) -- a mauled mech under
fire smokes/burns per hit; bands 3/4 are authored fire plumes.

THE RAM ECONOMY -- CLOSED (3 layers, measured live):
 1. armor = POINTS (every zone: damageScale = 1/armorPoints, armor 50-140;
    the [zone-armor] BT_DMG_LOG dump);
 2. StaticBounce prices rams with authored moverMass ~1.3e6 -> ~59,000 pts
    @10m/s; the binary dispatches it RAW but pod MP dropped it on the local
    replicant (MECH.CPP:986 warns) -- ram damage was NETWORK-INERT; our
    task-#47 replicant forwarding surfaced it as a one-shot.  Port
    normalizes x1e-3 to the point economy [T3];
 3. contact EDGE (ramLastVictim/ramContactLinger): the binary's bounce made
    separation implicit; our gait-derived velocity re-priced full rams at
    60Hz (the respawn explode-loop).  One bump = one hit; pressed = BLOCK.

PLUS: sfx size x0.5 + 1.25m visibility floor + hot-phase occlusion [T3];
particle pool 2048->8192 (missile traffic starved laser bursts -- the
"intermittent effects"); replicant beam aging (a lost beam-END record pinned
a stale beam on forever); UV-variant noise stamps (mask-safe mirror/swap);
no scroll on particle stamps (material-path only); BT_FX_TEST/fxshot.py
self-verification harness ([beam-draw]/[zone-armor]/[collide-tx] telemetry).

KB: rendering.md (specialfx engine + census + closeout), combat-damage.md
(ram economy + band semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 08:14:12 -05:00
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:15 -05:00
arcattackandClaude Fable 5 afefaeece5 Mech: THE 0xBD3 UNTANGLE -- real SubsystemMessageManager in [0x10d], the mapper's one true home is roster slot 0 (task #7)
Binary census [T1]: mech+0x434 has exactly 2 writers (ctor init + factory
case 0xBD3) and ONE reader (@0x4b984b in MechWeapon::SendDamageMessage
@004b9728) -- pure message-manager semantics, nothing mapper-like.  The
factory case now builds the real SubsystemMessageManager (ctor @0049bca4,
0x130, ConsolidateAndSendDamage Performance); the misbuilt MechControlsMapper
squatter is evicted.  Every mapper consumer re-pointed to MappingMapper()
(roster slot 0 = the binary's **(mech+0x128); SetMappingSubsystem @0049fe40
touches ONLY slot 0 -- its [0x10d] mirror removed).  Non-viewpoint mechs get
a slot-0 demand LATCH (base mapper, [T3] accommodation -- the binary leaves
slot 0 NULL off-viewpoint but never drives/animates those paths; ours does).
Verified: solo drive+gait clean; MP replicant gait through the latch (full
run lifecycle); 3x kill/respawn cycle with type-6 records on the observer.
Remaining (task #7 tail): the messmgr's consolidated TakeDamageStream send +
weapon-path routing (@004b9728 real body).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:16:27 -05:00
arcattackandClaude Fable 5 63206d7f19 Model record: the authoritative 200-byte overlay -- struct skew fixed, floor-25 era retired (task #4)
Two size errors (animationPrefix char[12] vs 4 bytes; a FICTITIOUS 64-byte
skeletonName block -- the record has no skl name) skewed every member after
word 0x10 by +2/+15 words and pushed the fieldXX block past the record end.
Full writer map recovered (FUN_004a2da8 + Mover FUN_004238bc + Reticle
FUN_00435ac8 chains) + runtime reader map (ctor pass @0x4a24d1-0x4a2968,
capstone-verified).  Struct rewritten as the exact overlay (static_assert-
locked); all Pass-2 consumers renamed to real fields; retired: ctor +
replicant + master-gait floor-25 hacks, the throttle guard, the fictitious
classID stamp.  Verified: accel=30 superStop=10 throttleAdj=1 (was ~1/OOB/0),
deadbands byte-identical via named fields, full walk->run lifecycle clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:36:12 -05:00
arcattackandClaude Fable 5 a652ddcdbc MP: authentic update-record deadbands streamed from the model record (task #3)
Binary ctor @0x4a26a7-0x4a26ef: mech+0x768/76c/770 <- raw record words
0x26/0x27/0x28 (UpdatePositionDiffrence / UpdateTurnVelocityDiffrence /
UpdateTurnDegreeDiffrence x pi/180 [const @0x4a2d44]).  Read at RAW record
offsets -- the Mech__ModelResource struct is layout-skewed (field98 gave
-1.5 where the record holds 0.3; audit filed as task #4, also explains the
old forwardCycleRate "floor 25" hack).  Sender triggers now use the
authored constants: pos-deadband type 0, quat-Y delta + yaw-rate delta +
stopped-turn edge type 4 (binary @0x4aad35/@0x4aac2b/@0x4aac6c).
Verified 2-node: madcat streams 0.3/0.045/3deg, type-4 spam gone, walking
replicant clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:55:25 -05:00
arcattackandClaude Fable 5 c9f0c2a7f1 MP: the Mech-level update records @0x4a0c2c/@0x4a1232 -- all 9 types byte-exact (task #1)
- Writer transcribed from the recovered disasm; reader rewritten from
  part_012.c with every Wword absorber promoted to named engine/port members
  (updateOrigin/updateVelocity/projectedOrigin/projectedVelocity/nextUpdate/
  lastUpdate + bodyTargetSpeed/latches/alarms).  Wire sizes verified live:
  0x14/0x20/0x2c/0x78.
- movementMode UNIFIED with Simulation::simulationState (binary mech+0x40 =
  StateIndicator@0x2c currentState) -- death/limbo/airborne now replicate in
  every record header.  Three mislabels of the same binary fns retired:
  SetInstanceFlags + RequestActionFlags -> Mech::ForceUpdate (updateModel |=
  mask, 0xfe03 disabled filter); IsNetworkCopy -> IsDisabled.
- Senders wired byte-exact: gait transitions Force(8), knockdown Force(1|0x20),
  death Force(1|0x40), Reset Force(0x1f) + binary zero-set, perf-loop
  deadbands (speed type 2, orientation type 4, heat type 7).
- 2-node verified: types 2/3/4 flow while driving; kill -> type-6 record
  (simState=9) -> the OBSERVER's replicant runs the wreck sink loop with no
  double death transition; respawn 0x1f burst snaps + un-wrecks the peer;
  walking replicant un-regressed (run 12, cycle tracking); solo clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:38:49 -05:00
arcattackandClaude Fable 5 946323b696 Gyro: damage fan-out FUN_004b2980 -- the cockpit hit-BOUNCE is live (task #56)
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
  or a RANDOM horizontal fallback when ~zero (per-component sign roll then
  value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
  -> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
  .{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
  each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
  vibrationDirection@0x390) and verticalImpulse(yaw, dir)

Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.

Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
  in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
  impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
  x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor

DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.

Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.

Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 07:36:25 -05:00
arcattackandClaude Fable 5 cb85517ede Gyro: byte-exact re-enable -- ctor map, integrators, jointeye writers (task #56)
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):

- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
  mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
  0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
  everything after was mis-offset, and clamps/accumulators were 0xCD fill --
  every field now initialised per the binary; 33 offsets static_assert-locked,
  sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
  OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
  without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
  force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
  eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
  multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
  (GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
  pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
  bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
  0xCDCDCDCD gated the writers off silently

Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).

Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:47:22 -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 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 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 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 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 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 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 b44c0f98f6 PFX continuous-rate emission (data-verified) + persistent wreck smoke
The one-burst emitter batching was wrong: in EVERY shipped .PFX,
maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2, DDAM2
35/16~=2, DDTHSMK 30/3=10) -- release_period is the EMISSION WINDOW and rate
is particles/second emitted continuously across it.  The death smoke plume is
authored as a 3/sec trickle for 10 seconds (+6s particle life), not one
same-frame puff.  Emitters now integrate rate*dt until maximum_issue.

Wreck smoke (port addition, [T3]): a destroyed mech re-arms the death/rubble
smoke plume (psfx 1, DDTHSMK) every 10s -- its own authored window -- so the
dead hulk visibly keeps smoking instead of standing there looking alive (the
freeze-death is invisible on an already-stationary target).  New member
Mech::wreckSmokeTimer; UpdateDeathState takes dt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:13:10 -05:00
arcattackandClaude Fable 5 a3d67cc639 Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed
faithfully:

- STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111)
  -- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit
  silently no-op'd.  Promoted to the named member Mech::damageLookupTable
  (binary this[0x111], was mislabeled ammoExpended).  New gotcha class recorded
  (reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged.

- Fire path migrated off the stale vital-zone aim onto the completed STEP-6
  unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the
  exterior zone).  No more invisible 1-shot kills; death via the authentic
  cascade (~14 center-mass hits).  Wreck stays TARGETED on kill (beams stop on
  it); scoring latches off.

- SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43
  unbound plugs/mech logged as an open question -- the binding itself is a gap).

- RemakeEntity (render damage swap): the 1996 render state machine's missing
  Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each
  segment's damage-zone graphic state (tree dtor doesn't cascade -> never
  rebuild).  Destroyed arms/guns visibly wreck (the only variants the RES
  registers).

- BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer,
  unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all
  stubs).  Parses the authentic VIDEO/*.PFX definitions via the [pfx_day]
  psfxN mapping; premultiplied blending renders BOTH families from the same
  data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK
  ramps negative: impossible additively); depth-sorted billboards with a
  radial-masked grit sprite; impact-frame orientation (.PFX offsets are
  authored mech-local, -Z = out of the struck armor toward the shooter) for
  weapon hits AND damage bands (via lastInflictingID, now maintained -- was
  declared but never written).  Both effect-number encodings route (raw dpl
  <100 + WinTesla 1000+slot carried by the band resources).  Death fires the
  authentic dnboom (7) + ddthsmk smoke plume (1).

- Effects anchor at the impact point / damaged zone's segment, not the mech
  origin (no more fire at the feet).

- Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain
  verification; BT_PFX_ADD=1 flips the particle blend for A/B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:43:32 -05:00
arcattackandClaude Opus 4.8 7c455303bd MechDeathHandler: per-zone destroyed-skins + explosions (faithful, exported)
Fixes the "kills are invisible" report: a mech died only at the state level, with
no visible destruction on its body.  Root cause: the per-zone damage-state
descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was
never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8)
was a no-op stub, and MechDeathHandler itself was a no-op stub.  The surrounding
plumbing (the per-zone load loop over the type-0x1e resource, the death-handler
slot) was already correct.  Whole pipeline is EXPORTED -- no stand-ins.

Reconstructed:
- Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed
  LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the
  real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource]
  [i32 GraphicState][f32 TimeDelay], ascending by DamageLevel.  Verified live:
  6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash.
- The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4
  by-graphic-state) as Mech__DamageZone methods.
- The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance /
  FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a
  zone's damageLevel rises across a descriptor threshold, fires that entry's
  explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic
  descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the
  destroyed skin) to the zone.  Runs for EVERY mech, so the enemy visibly falls
  apart as it dies.

Integration: the binary ticks MechDeathHandler off the mech's Performance list
(mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch
drives Tick() directly (same approach as UpdateDeathState).  Effect spawn uses the
established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch
(class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported.

Runtime: builds clean, boots clean, 42 descriptor tables load with sensible
byte-aligned data, no crash.  Live effect firing is combat-triggered (verified by
the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 11:18:30 -05:00
arcattackandClaude Opus 4.8 d07ac7dd49 STEP 6 COMPLETE: cylinder hit-location LIVE — unaimed hits resolve to zones
The Mech per-impact hit-location resolver (the cylinder damage table) is now
functional, wired, and runtime-verified [T2].  Unaimed (zone==-1) hits — the
collision-damage path — now resolve an impact point to a damage zone via the
authentic height x angle grid + weighted dice roll, instead of dropping.

dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream
shims; backed it with real std::vector storage and fixed 5 latent runtime bugs:
  - ReadEntries now consumes the leading cell name-string ([i32 len][len+1])
  - PieSlice ctor reads rotateWithTorso into the correct member
  - SelectSlice direct-indexes (was int lookup on a float-keyed table)
  - ResolveHit returns the zone (chains SelectSlice -> SelectZone)
  - real MemoryStream::ReadBytes (was a variadic no-op)

mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real
load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream ->
new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it.

Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays
Entity's by ID): on invalidDamageZone, resolve via the table then base-route;
aimed reticle hits pass through unchanged.

Three named accessors (no databinding-trap raw reads): WorldToLocal
(localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY
== collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a
BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8;
torso.hpp cannot be included into mech.cpp — subsystem-stub collision).

Stream format + geometry + roll + handler were all byte-verified against the
shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the
disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a).

Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer
count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD.  Env gate
BT_CYL_LOG=1.  Unblocks collision-damage application.

KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:32:17 -05:00
arcattackandClaude Opus 4.8 b91057aadc gauge-complete P4c: Condenser dedup + RecomputeCondenserValves -> ValveSetting gauge reads authentic 1/N
Two coupled fixes so the condenser valve gauge (ValveSetting -> coolantFlowScale
@0x15C) reads its authentic value instead of garbage/zero.

STEP 7 (gating dedup): heat.cpp carried STUB Condenser ctor/dtor/TestClass/
TestInstance/CreateStreamedSubsystem that ODR-duplicated the REAL bodies in
heatfamily_reslice.cpp and WON under /FORCE (heat.obj links first) -> the real
ctor (which sets valveState=1) was shadowed, leaving valveState=0xCDCDCDCD.
#if 0'd the heat.cpp stubs so the reslice ctor (@4ae568, byte-verified: valveState=1,
coolantFlowScale=0, massScale=refrigerationFactor, condenserNumber from name) is the
sole definition. DefaultData/GetClassDerivations/ResetToInitialState (not duplicated)
stay in heat.cpp.

STEP 9 (the real writer): FinishConstruction() at the Mech ctor tail was a no-op
template stub in place of FUN_0049f788 = RecomputeCondenserValves -> coolantFlowScale
was never written (stayed 0). Reconstructed it (byte-verified vs part_012.c:9264):
distribute coolant flow across the mech's condensers, coolantFlowScale_i =
valveState_i / sum(valveState), with the condenserAlarm@0x1DC change pulse
(2-if-flow<=old-else-1, then 0). Walks the populated subsystem roster filtering
Condensers via IsDerivedFrom (behaviorally identical to the binary's @mech+0x7cc
condenser chain, GUID 0x50e4fc). Wired as BTRecomputeCondenserValves(this) at the
Mech ctor post-init pass (binary @9457). _DAT_0049f850 fallback confirmed 0.0f (PE read).

Verified: [valve] the Blackhawk's 6 condensers each read flow=0.166667 (=1/6, total=6)
for both player + spawned enemy, no every-mech crash; combat TARGET DESTROYED,
un-regressed. Diagnostic BT_VALVE_LOG added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:22:01 -05:00
arcattackandClaude Opus 4.8 2bcca26cea WAVE 7 Phase A: wire the projectile/missile weapon SUBSYSTEMS byte-exact (un-mislabeled)
An 8-agent decomp-mapping workflow mapped every weapon-family ctor + fire/spawn path
(scratchpad/wave7_maps.txt).  KEY IDENTITY CORRECTION via VDATA.h enum (base 3000=0xBB8):
the factory ctor-address comments were right but the built CLASS names were stubs/base --
    0xBCD == ProjectileWeapon  (was building the base MechWeapon stub)
    0xBCE == GaussRifle         (NOT MissileLauncher -- was a MissileLauncher stub)
    0xBD0 == MissileLauncher    (NOT BallisticWeapon -- was a BallisticWeapon stub)
Un-mislabeled + wired via Create<Class>Subsystem bridges (mirroring CreateEmitterSubsystem),
each static_assert-locked byte-exact on the now-locked MechWeapon(0x3F0)/Emitter(0x478) bases:

  * ProjectileWeapon (0xBCD, @4bc3fc : MechWeapon, sizeof 0x448)
      FIX: AmmoBinConnection was an empty 1-byte struct -> retyped to the binary's 0xC
           SharedData::Connection (was making the object 8 bytes short, sliding
           MissileLauncher's missileCount off 0x448).
  * MissileLauncher (0xBD0, @4bcff0 : ProjectileWeapon, sizeof 0x44C)
      FIX: deleted muzzleVelocity (ALIAS of inherited ProjectileWeapon launchVelocity@0x410)
           and salvoCount (SHADOW of inherited MechWeapon damageData.burstCount@0x3d4),
           keeping the single own field missileCount@0x448.
  * GaussRifle (0xBCE, @4bdcb4 : Emitter, sizeof 0x484 = Emitter + Vector3D muzzleVelocity)
      DEDICATED bridge (not CreateEmitter): its FireWeapon is a no-op (mov [eax+0x414],0) --
      a non-functional weapon in this 1995 build.  Also fixed GAUSS.CPP's discharge write
      rechargeLevel -> currentLevel@0x414 (the binary writes this+0x414).

Phase A safe-stubs FireWeapon (consume round + recoil, NO spawn): the reconstructed
Projectile (@4be1bc, 0x340) + Missile (@4bf5b4, 0x368) ENTITY classes are NOT spawn-ready
(Entity-base phantom members overflow their New() alloc -> heap corruption; New() takes
(int)this instead of the 0xD4 descriptor; Entity base size unconfirmed 2007-vs-1995, the
Mech 0x638-vs-0x854 problem).  The workflow's adversarial verify flagged the live spawn as a
heap-overflow hazard and recommended exactly this phasing.  Phase B = the entity byte-exactness
+ descriptor build + New(MakeMessage*) so a fired shot becomes a flying entity that damages.

Factory now 18 of 20 cases wired to real ticking classes (remaining: SubsystemMessageManager
0xBD3 [WAVE 8], Gyroscope 0xBC4 [deferred]).  Verified: Mad Cat (LRM/ballistic) + BLH construct
the real weapon classes + tick authentic charge/ammo/heat/recoil, DESTROYED-in-8, 0 crashes,
heapcheck-clean through construction.  Energy weapons already damage via the mech4 beam path,
so combat is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 13:35:43 -05:00
arcattackandClaude Opus 4.8 0bceb9a638 gauges/map: populate the radar entity grid + draw mech contact blips
Root-caused why the radar showed 0 contacts: the gauge renderer's moving/static
entity grids (queried by GetMoving/StaticEntitiesWithinBounds) were NEVER
populated -- the 1995 game fed them from ExecuteImplementation's InterestingEntity
iterator, which the WinTesla port dropped.

Fix (engine): add GaugeRenderer::RebuildEntityGrid() -- Clear + repopulate
movingEntities from the world's DynamicMaster + DynamicReplicant entities (the
vehicles/mechs; AllEntity would flood the grid with ~300 props/effects/terrain).
Called from the map's Execute phase 0 (under the gauge's guarded Execute, so a
fault disables only the map).  Verified: contacts are now found, combat un-
regressed (TARGET DESTROYED), heap-clean under BT_HEAPCHECK.

Fix (map): DrawMoving now draws a cross blip for mech-class contacts (0xBB9) when
the authentic pip raster set is absent (the BT pip table is stubbed in this engine
build).  KEY PROJECTION FIX: worldToView is a camera (view->world) matrix and
Point3D::Multiply applies only rotation+scale (NOT translation), so projecting the
absolute entity position left the viewpoint un-subtracted and the blip landed
off-screen (verified: blip @(580,-1154)).  Project the RELATIVE position (delta =
entity - viewpoint, already computed for the range check) instead -> the blip
projects viewpoint-relative + heading-rotated correctly (@(164,221)).

radarRange default 500->1000 (1km zoom) so contacts within ~500m show on the
radar (SetTargetRange, the real player zoom, is still stubbed).  BT_MAP_LOG traces
the blip coords.

STATE: the grid population + viewpoint-relative blip projection are correct and
combat-safe.  A reliable on-screen contact demo still needs the display-scale vs
spawn-distance reconciliation (the BT_SPAWN_ENEMY dummy sits at a varying, large
distance -- 377-828m -- beyond the display radius across runs) + the authentic pip
symbol/name/video-object infrastructure (still stubbed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:07:19 -05:00
arcattackandClaude Opus 4.8 48ed3a8b73 gauges: register + wire the map (radar) -- the tactical display renders
MapDisplay (the radar/threat-map gauge, btl4rdr.cpp) was fully reconstructed
(ctor + Execute + DrawViewWedge/DrawStatic/DrawMoving/DrawNames) but never
registered and its data attributes never published.  Wire it up end-to-end:

DATA (the attributes the map binds):
* Mech table extended to the full 0x15..0x38 (was the 0x21 prefix): add
  RadarRange@0x2f -> radarRange, RadarLinearPosition@0x30 -> radarLinearPosition
  (Point3D* -> &localOrigin.linearPosition), RadarAngularPosition@0x31 ->
  radarAngularPosition (Quaternion* -> &localOrigin.angularPosition), DuckState
  @0x37 -> duckState; the rest bind the shared attrPad (kept dense so Find can't
  strcmp a gap).  New members set in the Mech ctor; radarRange defaults to 500
  (SetTargetRange is still stubbed) so nearby contacts are on-scale.  The map's
  position/angle attributes resolve to Point3D**/Quaternion** (AttributePointer
  is int Simulation::*, so the ATTRIBUTE_ENTRY reinterpret binds pointer members
  fine -- verified live: posPtr/anglePtr resolve).
* Sensor (named "Avionics") publishes RadarPercent/SelfTest/BadVoltage: define
  Sensor::AttributePointers[] + build the previously-empty Sensor::AttributeIndex
  chained to PoweredSubsystem::GetAttributeIndex() (= HeatSink's dense index).

WIDGET (the registration glue -- NOT in the assert-anchored decomp, so
reconstructed from the config + ctor):
* MapDisplay::methodDescription (config "map") + MapDisplay::Make.  The
  offset_position keyword "center"/"bottom" is typed as a STRING and converted
  in Make, avoiding a ModeManager named-constant registration.  Registered in
  BTL4MethodDescription[] (btl4grnd.cpp now includes btl4rdr.hpp).

Three crash fixes exposed once the map ran (each a pre-existing stub the map is
the first consumer of):
* Sensor radarPercent went hugely negative (RadarBaseline - HeatMasterEnergy(),
  where the inherited heatEnergy is un-normalized in the not-byte-exact heat-leaf
  branch) -> the radar reported non-operational.  Guard: treat an out-of-[0,1]
  heat term as no penalty (marked bring-up; real overheat penalty needs the heat
  normalization).
* ResolveOperatorEntity was a stub returning NULL -> EntityPosition(NULL) crashed
  CalculateBounds.  Fixed to the renderer linked-entity + application viewpoint
  fallback (same as HeadingPointer).
* MapName::ExtractFromEntity didn't guard a NULL entity (the binary's Verify(False)
  iterator-mismatch path compiles out at DEBUG_LEVEL 0) -> EntityPosition(NULL).
  Guarded.

Verified live (BT_DEV_GAUGES): the radar now draws the view wedge (the mech's
180-deg FOV cone) and completes the full wedge/static/moving/names cycle with
operating=1, cap=1, scale=500, position/angle resolved, 0 crashes.  Combat un-
regressed (TARGET DESTROYED after 8 hits), heap-clean under BT_HEAPCHECK.
Permanent gated diagnostic: BT_MAP_LOG traces the phase/draw pipeline.  Contacts
show 0 (the spatial-query entity classification is a follow-up); DuckState now
resolves for the duck button too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:16:29 -05:00
arcattackandClaude Opus 4.8 9e31408ba2 gauges: publish Mech attribute table (dense prefix 0x15..0x21 -> LinearSpeed live)
mech.cpp shipped an EMPTY, unchained Mech::AttributeIndex (a default-constructed
AttributeIndexSet passed to DefaultData), so Simulation::GetAttributePointer
severed the whole parent chain -> EVERY bare Mech gauge binding (LinearSpeed /
MaxRunSpeed / DuckState / RadarRange / ...) resolved to NullAttribute.

Add the Mech AttributeID enum (full binary set 0x15..0x38, VA 0x50be84) + a
Mech::AttributePointers[] table + Mech::GetAttributeIndex() chained to
JointedMover::GetAttributeIndex(), and change DefaultData to pass it.

DENSE-TABLE HAZARD (systemic): AttributeIndexSet::Build (SIMULATE.cpp:565) sizes
the built index to max(id) and Find (SIMULATE.cpp:663) strcmps EVERY slot, and
Build does NOT zero gap slots -> an unpublished id between the parent's
NextAttributeID and the max published id holds a garbage entryName -> AV on the
next name lookup.  So the table is a DENSE PREFIX 0x15..0x21 (JointedMover's
NextAttributeID through LinearSpeed): the ids the BLH cockpit doesn't bind
(collision/eyepoint/reticle/footstep/anim-state) point at one shared read-only
attrPad member; CurrentSpeed/MaxRunSpeed bind to the existing gait members
legCycleSpeed/reverseStrideLength; LinearSpeed binds a new linearSpeed member
populated each frame in PerformAndWatch (|adv|/dt = forward ground speed).  Ids
0x22..0x38 stay declared in the enum for later extension (RadarRange/DuckState
when the map/duck widgets land) but are NOT published yet.  New members appended
to Mech's own region (no locked base/gait offset shifts); inited in the ctor.

Verified live (BT_DEV_GAUGES + drive): the radar-surface SPEED readout
(numericSpeed(LinearSpeed), a base engine primitive already drawing NULL) now
shows 225 (~63 u/s in the widget's display units) instead of 0, tracking the
mech's motion.  Heat surface CurrentTemperature=77 un-regressed.  Combat un-
regressed (TARGET DESTROYED after 8 hits), 0 attribute-not-found, and Mech
construction + attribute-index build is heap-clean under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:26:58 -05:00
arcattackandClaude Opus 4.8 8b36440d05 subsystems: un-stub Wave 4 readouts + Wave 6 Myomers (10->15 factory cases)
WAVE 4 (standalone readouts) -- Sensor(0xBC3)/Searchlight(0xBD8)/
ThermalSight(0xBDE)/AmmoBin(0xBCB) un-stubbed via Create<Class>Subsystem
bridges + Torso-style de-shim (drop cross-family shadow fields, redirect
accessors to the real inherited base, static_assert layout locks).
- FIX: Searchlight/ThermalSight ctors gated their Performance on the shadow
  segmentFlags(=0) so it NEVER installed; switched to owner->simulationFlags.
- AmmoBin: retype ammoAlarm HeatAlarm->WatcherGaugeAlarm(0x54) + drop the
  statusState@0x40 shadow -> exact 0x22C layout.
- Guard HeatWatcher::WatchSimulation against the unresolved watchedLink
  (null-deref exposed once these sims run; faithful fix = resolve the link).
- Heat-leaf branch (Sensor) is not byte-exact -> overflow-lock only.

WAVE 6 (Myomers 0xBC6, mover-coupled) -- structural un-stub, gated
BT_MYOMERS (default on; =0 -> Actuator stub). Wired INERT: MyomersSimulation
early-returns (advanced-damage gate stubbed) + no-op mover feed, so the live
JointedMover is untouched and the gait cannot regress. De-shim drops the
owner*/segmentFlags shims to fit the exact-0x358 alloc. Authentic mover/heat
coupling deferred (needs messmgr 0xBD3 + reconciling the mover feed with the
gait cutover).

Verified: BLH tick 20->27, Mad Cat 24, combat DESTROYED un-regressed,
locomotion un-regressed, 0 crashes, 0 heap detections under BT_HEAPCHECK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 23:01:31 -05:00
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00