---
id: multiplayer
title: "Multiplayer — replication, netcode, the console"
status: provisional
source_sections: "PROGRESS_LOG.md §7 (Phase 7), §8 (P6); docs/HARD_PROBLEMS.md (P6)"
related_topics: [wintesla-port, combat-damage, locomotion, decomp-reference]
key_terms: [master, replicant, EGG, dead-reckon]
open_questions:
  - "pod-LAN config (real IPs); peer warp DropZoneLocation-anchoring [T3] (world-anchored peer sphere already visible, 160b78e); console-death replication freeze (pending recurrence to diagnose)"
---

# Multiplayer

Pod-to-pod networked play. The hard infrastructure exists (WinTesla replaced DOS `NETNUB` with a
WinSock2 TCP stack), and MP is now **substantially reconstructed + 2-node verified** — replication,
cross-pod combat, beam visuals, replicant gait, respawn (tasks #46-#52). Remaining: pod-LAN config,
fidelity nuances, and the Mech-level update records. Full detail: `docs/PROGRESS_LOG.md §7, §8`;
the problem writeup: `docs/HARD_PROBLEMS.md` (P6).

## The stack (already reconstructed)
- WinTesla replaced `NETNUB` with a **~3.5k-line WinSock2 TCP** reimplementation (`MUNGA_L4/L4NET.CPP`,
  `SOCK_STREAM`/ReliableMode, zero SOCK_DGRAM); the master/replicant distributed-sim core
  (InterestManager/Entity/HostManager) is complete. MP was integrated on that stack and verified
  end-to-end 2-node. The rumored `L4NET.CPP:1853` send-size
  bug is DEAD CODE (`#if MESSAGE_BUFFERING`==0). [T2]

## Master / replicant model
- **[[master]]** = a locally-simulated entity (your mech + dummies); drives itself + EMITS update records.
- **[[replicant]]** = a peer's mech, a local proxy; DeadReckons toward the master's replicated position.
  MUST `SetValidFlag()` at creation or every message defers forever ([[reconstruction-gotchas]] §9).

## How a session forms
`-net <port>` = networked mode (the pod listens on <port> for a CONSOLE, boots ConsoleOnly; game mesh
= <port>+1). Solo (no `-net`) never touches WinSock. The mission egg's `[pilots]` section IS the
roster (`pilot=ip[:port]` + a per-address page); each pod self-identifies by local-IP+game-port,
connects to earlier pilots, listens for later ones — a deterministic full mesh; the mission loads when
all connect. The **CONSOLE** (operator station — ABSENT from every archive) delivers the egg as chunked
`ReceiveEggFileMessage` packets + the LAUNCH `RunMissionMessage`; **`tools/btconsole.py` is our console
emulator** (⚠ `NotationFile::ReadText` expects NUL-SEPARATED lines). [T2]

## Verified milestones (one box, two instances)
- ⚠ **`-net` node render glitch — LIKELY ALREADY FIXED / WATCHING (task #53):** a 2-node MP session
  once showed ONE window come up visually wrong — shadow clipping, camera stuck inside/blocked,
  targeting/firing dead — while the other was fine. Two earlier framings were WRONG and are corrected:
  (i) the "two-D3D9-apps-on-one-GPU / device contention" theory was retracted — a solo instance, AND
  two solo instances, are all clean, so it is not device contention; (ii) "glitched FROM LOAD, before
  any death" was ALSO wrong — **the trigger is a RESPAWN** (user correction, 2026-07-10). So it is a
  **post-respawn render-rebuild fault on the MP path**, not a load-time one. Does NOT affect the pod
  (one instance per machine).
  - **What the respawn rebuild does (reproduced 2026-07-10):** on respawn the player's mech is rebuilt
    via `MakeEntityRenderables`, which runs a TWO-STEP view sequence — inside skeleton + `COCKPIT
    eyepoint`, then outside skeleton + `external chase` (`[view] …` logs, btl4vid.cpp:2158) — and the
    external chase camera is NOT re-installed (only ONE `[BTrender] external debug chase camera
    installed`, btl4vid.cpp:647, for the whole session despite multiple respawns). Plausible failure:
    if that two-step completes only to the COCKPIT step (raced/interrupted on the `-net` ladder), the
    node is left stuck INSIDE — body hidden by the inside skeleton, shadow/reticle flagging with it =
    the reported bundle. Camera + reticle + view + `tshd` shadow are all gated on `buildDebugChaseCamera
    = (view_type == insideEntity)` (btl4vid.cpp:297).
  - **Status: could NOT reproduce (2026-07-10) → likely already fixed.** 9 automated 2-node loads PLUS a
    forced death/respawn all came up CLEAN (the respawn completed to external chase; clean backbuffer via
    the new `BT_SHOT` dump). Since debug uninit is a deterministic `0xCDCDCDCD` (would glitch every run),
    the intermittency implies a **race**, and the task #52 respawn fixes (the `LoadMission` re-entry
    killer bug, viewpoint relink, death-latch) plausibly ALREADY closed it. **Left as WATCH** — re-open
    and capture with `BT_SHOT` if it recurs. Tools kept: **`BT_SHOT=<path>`** (per-instance backbuffer
    PNG, off by default, L4VIDEO.cpp) + `scratchpad/hunt53.sh` (N-run divergence hunter).
- **P6 smoke test:** two instances share a world — console egg → TCP mesh → synchronized start →
  bidirectional entity replication (each renders its own mech + the peer's REPLICANT), 0 crashes.
- **Movement replication:** A's mech WALKS on B's screen (the replicant tracks the master to ~1.5u via
  dead-reckoning). The authentic mission-start ladder runs (CreatingMission→…→RunningMission via the
  console LAUNCH). SIX bugs fixed to get here (dead-reckoner install, replicant-motion DeadReckon,
  master emission threshold, emission gated on RunningMission, the console-must-LAUNCH fact, replicant
  validity). [T2]
- **Wire-format bug class found+fixed:** MakeMessages replicate RAW over TCP, so string payload must be
  INLINE (`char[N]` at the binary offsets), not a `const char*` pointer (garbage cross-pod). Check
  EVERY MakeMessage for pointer payloads. [T2]

## Debug tooling (`BT_NET_TRACE`, permanent)
`[net-tx]/[net-rx]` (L4NET), `[net-upd]` (update lookup), `[upd-repl]`, `[ent-exec]` (state ladder).
Per-instance: `BT_LOG=<file>` + `BT_AFFINITY=<mask>` (CPU pin). Update stream ≈ 60 Hz × 144-byte
records per moving master. [T2]

## Cross-pod combat step 1+2 (task #46, 2026-07-09) [T2 findings]
**Step 1 — target ANY peer mech: DONE + verified.** A live-mech registry (`BTRegisterMech`/
`BTDeregisterMech` from the Mech ctor/dtor, btplayer.cpp) collects every Mech — player, dummy,
AND peer replicants.  The mech4 boresight world-pick now walks `BTGetTargetCandidates` (all mechs
≠ shooter) and picks the CLOSEST the ray strikes, retargeting the whole fire/damage block from
the solo `gEnemyMech` to the picked `hotTarget` (missile impact + projectile validation
generalised via `BTIsRegisteredMech`).  Verified one-box: instance A correctly enumerates B's
mech as a live `ReplicantInstance` (inst=4), 20 zones, `ownerID=3`, alive.  Solo un-regressed
(pick→damage→kill chain clean).
**Step 2 — cross-pod damage: SOLVED + verified end-to-end (task #47, 2026-07-09).** A one-line fix
(`Mech::Make`) yields a full cross-pod KILL: instance A (host 2) shoots B's mech (host 3) dead over
the network, B runs the death transition (wreck swap + explosion) on its own screen. [T2]

**THE BUG (root cause): B's own MASTER mech was INVALID, so every dispatched network message to it was
DEFERRED forever.** `Entity::Receive(Event*)` (ENTITY.cpp:165) does `if(!IsValid()) event->Defer();
return;`. The reconstructed `Mech::Make` (mech.cpp) force-set `ValidFlag` ONLY for replicants (the P6
bring-up hack for the partial MakeReady/CheckLoad handshake); a locally-created **master** never got
validated. So A→B TakeDamage arrived, resolved to B's real master mech, Posted, drained to
`Entity::Receive` — which saw `valid=0` and Deferred it. Handler never ran → 0 damage.

**THE FIX:** `Mech::Make` now sets `ValidFlag` for the master too (`if (mech != 0) mech->SetValidFlag();`
— the reconstructed ctor builds the whole mech synchronously, so it's safe). NOTHING ELSE changed: the
targeting, the virtual `victim->Dispatch`, the reroute, the wire, the receive, and the id resolution
were all already correct. Solo un-regressed (the solo enemy was already force-valid via btplayer:839;
the change only ADDS `ValidFlag`).

**Verified full path** (2-node one-box, `BT_MP_FORCE_DMG` on A firing at B's replicant once/sec):
1. A: `victim->Dispatch(&td)` (VIRTUAL — the real beam path) → `Entity::Dispatch` reads `inst=Replicant`,
   stamps `entityID=3:22` (B's master id), `if(ReplicantInstance) SendMessage(ownerID=3, EntityManagerClientID)`.
2. Wire: `NetworkManager::Send` (L4NET.CPP:1062) → `send()` to host 3 (online).
3. B: `ReceiveNetworkPacket` (NTTMGR.cpp:107) → `GetEntityPointer(3:22)` = `entityIndexSocket.Find`
   (HOSTMGR.h:270) → resolves to B's **master mech (classID 3001=0xBB9)** → `Post(EntityManagerEventPriority)`.
4. B: event drains → `Entity::Receive` (now `valid=1`) → `Mech::TakeDamageMessageHandler` (mech.cpp:584)
   → invalidZone (−1) cylinder lookup → zone damage. ~36 hits × 12 → B's mech DESTROYED + death transition.

**Two prior mis-diagnoses corrected — both were the WRONG messageID.** `Entity::TakeDamageMessageID` is
**18**, NOT 21 (confirmed at runtime: `td.msgID=18`, so `Simulation::NextMessageID`=3, not 6). Every
earlier probe filtered `msgID==21`, so it NEVER matched a real
TakeDamage — the stray `msgID=21` hits it caught were an unrelated message, and correlating their `3:19` /
`classID 48` against the real `3:22` TakeDamage produced two phantom root causes: (a) the "cross-TU Entity
layout divergence" (disproven separately — an `offsetof` probe showed `Entity` is IDENTICAL in both TUs:
`sizeof=444`, `entityID@380 (0x17C)`, `ownerID@388`, `simulationFlags@32`; distinct from the REAL P5
raw-offset stomps at `this+0x2d4..0x2f0` in `docs/HARD_PROBLEMS.md`), and (b) the "localID drops by hostID
on the wire" claim. Both are FALSE. The id is correct end-to-end (`3:22` = B's master); nothing on the wire
is corrupted. Lesson: when a message probe seems to see a wrong id, first confirm the numeric messageID.

Retained gated instrument: `[mp-force]` (BT_MP_FORCE_DMG + BT_MP_LOG) — the 2-node cross-pod reproducer,
off in solo. `[mp-hdlr]` (BT_MP_NET, mech.cpp:591) logs each applied cross-pod hit. 2-node launch recipe +
`scratchpad/mp_test.sh`: A `-egg MP.EGG -net 1501`, B `-net 1601`, then `python tools/btconsole.py MP.EGG
127.0.0.1:1501 127.0.0.1:1601` (MP.EGG `[pilots]` = game ports 1502/1602).

## Interactive -net aim + drive (task #48, 2026-07-09) [T2]
Re-measured on a 2-node one-box run. **The prior "aim ray dead / drive dead in -net" claim was STALE — both
work:** A's aim ray is live (`noRay=0`) and A drives (`BT_AUTODRIVE`/`BT_GOTO` move it).

**FIXED — torso-pitch aim ray (gyro-stabilized boresight).** The aim/pick ray was the active RENDER EYE's
view direction, which (a) inherits the mech BODY pitch — on a slope the body tilts ~8° to conform to terrain
and the boresight pointed that far DOWN, hitting terrain ~50-100 m short of the enemy (`groundPicks`, never
`mechPicks`); and (b) in CHASE view (the DEFAULT) is the elevated behind-the-shoulder camera, so the ray
started over the mech's shoulder. Fix (L4VIDRND `DPLEyeRenderable::Execute`, the `BTSetAimCamera` publish):
LEVEL the boresight (drop the view-dir pitch, rebuild an upright basis with world +Y) AND anchor its ORIGIN
at the mech's torso (`myEntity->localToWorld` translation + ~5) instead of the render eye. The guns now fire
along the mech's horizontal heading from the torso, independent of view mode and terrain tilt. The reticle X
still carries the torso twist (`BTTwistToReticleX`); reticle Y any elevation. VERIFIED end-to-end: solo,
enemy 120 m 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 757 m in a 2-node run); the replicant `localToWorld` is correct (DeadReckon + `localToWorld=
localOrigin`, mech4.cpp:1191). [T2]

**FIXED — drive-to-range (the automated cross-pod kill now runs end-to-end).** The `BT_GOTO` beeline had
three separate faults: (1) it was gated behind `if(gBTDrive.forced)` (mech4.cpp), and `forced` was set ONLY
by `BT_AUTODRIVE` — so `BT_GOTO` alone never drove (chicken-and-egg: the block that would drive was gated on
the drive being on). Fix: `BT_GOTO` now enables forced mode itself + supplies its own forward throttle (a
"beeline" must drive, not just steer), cutting throttle inside a stop radius (weapon range for `enemy`, so it
holds and shoots instead of ramming). (2) The steering used the `gDriveHeading` SCALAR MIRROR (seeded to 0),
but MP pilots spawn facing a non-zero heading, so the beeline steered the WRONG way. Fix: compute the 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 read 0 in -net — 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. **CAUSE FOUND LATER (task #51): turnDemand zeroed because `controlMode` was pointer
garbage — the `pilotArray[1]` shrunk-span overrun (see [[reconstruction-gotchas]] §5), MP-only because solo's
overrun wrote 0==BasicMode. Fixed at the root (10-slot array); the direct goto-turn stays (harmless, and it
skips the 1-frame mapper latency). KEYBOARD turning in MP was restored by the root fix.**
VERIFIED 2-node: A `BT_GOTO="enemy"` alone (no autodrive) drives `dist 1673→231 m`, holds at 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 + goto both work). `scratchpad/mp_kill_test.sh`;
`BT_GOTO_LOG` traces the beeline.

## Replicant gait animation (task #50, 2026-07-09) — DONE [T2]
A peer's replicant used to slide as a frozen statue (DeadReckon moved it; nothing animated it). Now the
replicant block (mech4.cpp, after `DeadReckon`) derives 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 = the forward dot
against the mech's -Z facing — writes it to `controlsMapper->speedDemand` (the LEG channel's live source)
and advances the LEG state machine for its JOINT writes only. Travel stays DeadReckon (the returned
distance is discarded) so position always follows the master; the clip cadence matches the replicated
speed. ⚠ REPLICANT GAIT-SCALAR PRIMING (same class as the master's per-frame primes, which live in the
PLAYER-only drive block): `globalTimeScale`/`idleStrideScale` read 0 on a replicant (clip frozen at
advance-time dt*0, legState engaged but legCycle stuck 0), and `reverseSpeedMax2`@0x7a0 (the run-cycle
rise-clamp; LoadLocomotionClips does NOT set it) read debug-heap garbage → legCycleSpeed clamped to
-4.3e8 entering run state 13. Both primed in the replicant block; `legCycleSpeed` itself sane-banded once.
VERIFIED 2-node: the observer's replicant runs the full lifecycle — stand→walk(5,7)→run(10,12) with
legCycle 7.5→34 tracking the master's speed, wind-down (8) and back to Standing(0) when the master stops.
Solo un-regressed. `BT_REPL_LOG` traces pos/vel/legState/legCycle; recipe `scratchpad/mp_gait_test.sh`.
NOTE: the update-record velocity is currently the master's ACTUAL travel velocity (worldLinearVelocity
from the leg channel); the binary publishes the BODY channel's smoothed projection (projectedVelocity,
disasm +0x2a0). Swapping the source is a fidelity refinement DEFERRED — it risks a verified working feed
for a smoothing nuance. [T3 on that nuance]

## Mech-level update records (task #1, 2026-07-11) — DONE [T1 transcription / T2 live]

The 9-type Mech record channel is LIVE end-to-end: **writer @004a0c2c** transcribed
instruction-for-instruction from `reference/decomp/mech_writeupdate_004a0c2c.disasm.txt`, **reader
@004a1232** rewritten from part_012.c:9576-9692 with every Wword absorber promoted to the NAMED
engine/port member.  Wire sizes verified live (0x14/0x20/0x2c/0x78).  Key mechanics:

- **Record type == the `updateModel` BIT INDEX** (`Simulation::WriteSimulationUpdate` walks the
  mask, SIMULATE.cpp:302-328; binary twin FUN_0041bd98).  Request = `Mech::ForceUpdate(1<<N)`
  (@0x4a4c54: `updateModel |= mask`, `&= 0xfe03` filter when disabled).  THREE prior mislabels of
  the same binary fns are retired: `SetInstanceFlags` and `RequestActionFlags` (both @0x4a4c54 —
  +0x18 is `Simulation::updateModel`, NOT entity flags) and `IsNetworkCopy` (@0x49fb54 =
  `IsDisabled`, simulationState 2||9).
- **`movementMode` == `Simulation::simulationState.currentState`** (binary mech+0x40 =
  StateIndicator@0x2c +0x14; oldState @+0x3c).  The port's parallel `int movementMode` member is
  GONE — `MovementMode()/SetMovementMode()` accessors ride the engine StateIndicator, so the state
  (death 9 / limbo 2 / airborne 3,4) replicates in EVERY record header (+0xC) and the replicant
  tracks the master automatically.
- **Every record tail = the mapper's `speedDemand`** (subsystemArray[0]+0x128) mirrored into
  `bodyTargetSpeed@0x6b4` on both sides — the authentic replicant-gait feed (it was misread as a
  "sequence number"; the type-2 record exists solely to ship it on change).  Task #50's
  velocity-derived gait stays as the leg-channel driver meanwhile.
- **Types**: 0 pose (Mover record + tail; orientation SAVED/RESTORED around the base call — it
  rides ONLY type 4), 1 damage zones (engine), 2 speedDemand, 3 leg-state+stability (write-through
  body dispatch both sides), 4 orientation+angular-velocity resync (euler-encoded, quat
  normalized), 5 knockdown (SetBodyAnimation 0x20 both sides), 6 death, 7 impact (fields only),
  8 airborne/reverse selector.  5/6/7 share the 0x2c heat/throttle/fall-basis payload.
- **Senders wired byte-exact** (the perf-loop + event map recovered from 27 binary call sites):
  gait transitions `Force(8)` (mech2 — already transcribed under the RequestActionFlags mislabel,
  masks matched the binary exactly), knockdown `Force(1|0x20)` (mech4), death `Force(1|0x40)`
  BEFORE `SetMovementMode(9)` (the disabled filter would mask it after), `Reset` `Force(0x1f)` +
  the binary zero-set (state 0, cycle/target speeds, poseSyncLatch), perf-loop deadbands: position
  → type 0, orientation deviation → type 4, speedDemand change → type 2, heat-level change → type
  7 (snapshot @0x780).  [T3 deadband thresholds — the model record's `UpdatePositionDiffrence` /
  `UpdateTurnVelocityDiffrence` / `UpdateTurnDegreeDiffrence` fields (+0x26..0x28) are the
  authentic constants, not yet streamed to the runtime members +0x768/+0x76c/+0x770.]
- **VERIFIED 2-node**: types 2/3/4 flow while driving; kill → B emits ONE type-6 (simState=9 in
  the header) → A's replicant flips to MovementMode 9 and its own `UpdateDeathState` runs the
  wreck-sink loop on the OBSERVER's screen (smoke re-arm logged on A; NO double death transition,
  NO replicant VehicleDead) — the death SINK/burial gap (old item 3) is CLOSED.  Respawn:
  `Force(0x1f)` burst snaps the replicant to the drop zone + un-wrecks it.  Walking replicant
  un-regressed (run states 10→12, legCycle tracking, with type-3 records flowing).  Solo clean.
- Telemetry: `[mrec-tx]/[mrec-rx]` per non-pose record (BT_REPL_LOG).  Note the type-1 rx line
  prints a bogus simState (the damage record's own layout at +0xC) — cosmetic.
- New by-name members: `poseSyncLatch@0x77c` (dead-reckon re-base latch, consumed by
  FUN_004ab1c8), `fallDirection@0x4a8` + `fallScalar@0x4b4` [T4 names], `heatLevelSnapshot@0x780`.
  A TU-local stub type forced the `BTMapperSpeedDemandRaw(void*)` bridge (mech.cpp carries a local
  `struct MechControlsMapper` recon stub — typed cross-TU signatures mangle differently).

## Subsystem update-record DIRECTION + the Torso pair (task #57, 2026-07-13) [T0/T1]

**Engine semantics [T0 SIMULATE.cpp:270-297]: `WriteUpdateRecord(record, model)` PRODUCES the
outgoing record from the object (master serialize; base sets `recordLength=sizeof`, stream
advances by `recordLength` — variable-length records are the mechanism); `ReadUpdateRecord(record)`
APPLIES the record into the object (replicant).** The names read backwards — do not flip them.
Base fns in every subsystem vtable: slot 6 = 41bd34 (Read/apply), slot 7 = 41c500 (Write/produce,
3 args).

- **Torso pair:** slot 6 `@004b6a78` = `ReadUpdateRecord` (apply: lastUpdateTime clock stamp +
  twistAtUpdate/twistVelocity/twistRate ← record +0x10/14/18). Slot 7 `@004b6a1c` =
  `WriteUpdateRecord` — **Ghidra missed the function start**; raw-disasm recovery: chains base,
  sets `recordLength=0x1C`, writes currentTwist/twistVelocity/twistRate → +0x10/14/18, snapshots
  `twistAtUpdate = currentTwist`.
- **The bug this fixed:** the old recon had the APPLY body on the WRITE virtual (and no Write at
  all) — the master consumed its own uninitialized stream buffer (0xCDCDCDCD → twistAtUpdate =
  −4.316e8) and never serialized twist; the replicant MadCat pinned at −140° (the "torso ghost
  twist"). With the pair correct the replicant tracks `targetTwist = twistAtUpdate +
  twistRate·elapsed` cleanly (cur=0 when master still). [T2 live]
- **Residual (authentic):** the initial full-state snapshot record does NOT carry the torso
  extras; the replicant's blind +0x10 read can pick up 0xCD fill ONCE at spawn. The binary does
  the same read; the twist clamp contains it (on the fixed-torso BLH it pins to ±0.01° —
  invisible). First real twist record overwrites it.

## Remaining (P6 phase 4 / Phase 7)
Pod-LAN config: **real-IP path VALIDATED (2026-07-15)** — an egg with the machine's real LAN IP in
`[pilots]` (not `127.0.0.1`) connected + replicated + moved end-to-end (`Connected to
GameMachineHost at 10.0.0.46:1602`, `All connections completed!`, peer telemetry flowing). So
`gethostbyname` local-address matching (L4NET.CPP:2612, which also appends 127.0.0.1) + the
`WSAStringToAddressA` `IP:PORT` parse (ResolveAddress) both work with real addresses — NOT
localhost-locked. Recipe: egg `[pilots]` = each machine's LAN IP + its GAME port (`= -net port + 1`,
e.g. `10.0.0.46:1502`); each machine runs `btl4 -egg lan.egg -net <consolePort>`; console relay
(`btconsole.py lan.egg A_IP:1501 B_IP:1601`) from any machine; open the TCP console+game ports in
the Windows firewall. STILL UNTESTED: an ACTUAL two-physical-machine run (validated single-box via
the real LAN IP), so firewall is the main first-attempt unknown. Affinity pinning is NOT needed
across separate machines (the packet-jitter artifact is single-box only). Also: update-record
velocity sourced from the body-channel projection (above). See [[open-questions]]. [T2 path / T3 two-box]

## MP-front scout (task #45, 2026-07-09) — the P6 chain SURVIVES the combat/HUD rework [T2]
Re-ran the one-box smoke test on the current build (world-pick targeting, weapon groups, death
transition, HUD all landed since P6): console egg → mesh → RunningMission on BOTH instances, 174+
×144-byte update records EACH side (msgID=6), 2 mech trees per instance, 0 crashes over ~6 min.
**Gap map to a first playable LAN fight** (in wiring order):
1. ✅ **Enemy selection — DONE (task #46).** The world-pick used to test only `gEnemyMech` (the solo
   dummy, btplayer.cpp:791); now generalized to the live-mech registry candidate walk
   (`BTGetTargetCandidates`, all mechs != shooter) — see "Cross-pod combat step 1+2" above.
2. ✅ **Cross-pod damage — DONE (task #47).** TakeDamage dispatched at the REPLICANT reroutes to the
   owning master (verified end-to-end: full cross-pod kill; the master takes real zone damage + runs
   its own death transition; task #42 placed the death dispatch victim-side — MP-correct by
   construction). See "Cross-pod combat step 1+2" above.
3. **Victim visuals on the shooter's screen** — PARTIALLY resolved (user-observed 2026-07-09): the
   wreck SWAP does appear on the killer's screen, but the death SINK/burial does not (the collapse+
   burial runs only in the owning pod's death transition). Fix path: transcribe the recovered
   Mech-level WriteUpdateRecord death/knockdown record types (5/6 — disasm preserved, task #51).
3b. **Respawn cycle** (task #52) — the AUTHENTIC cycle is now reconstructed [T1 @004c05c4 +
   engine PLAYER.cpp]: mech death transition (mech4.cpp) dispatches `VehicleDead` (ctor default
   `deathCount = -1` = the death notification) to `GetPlayerLink()` → `BTPlayer::
   VehicleDeadMessageHandler` (@004c05c4 structure): MissionEnding(state 4) skips all; -1 →
   dedup on deathPending, ++deathCount, debit `scenarioRole` life, sever `playerVehicle`
   (the WRECK entity stays), re-post the same message at **now + 5.0 s** (5.0f @004c0830)
   stamped with deathCount; out of lives → the binary posts msgID 0x18 at +10 s (mission
   review; deferred). deathCount ≥ 0 / -2 with a vehicle in hand = acquired (clears
   deathPending); without one → engine base @0042db80 (PLAYER.cpp:214: closest non-"win"
   DropZone → AssignDropZone → 2s re-post probe) → DropZoneReply → CreatePlayerVehicle (a NEW
   mech; `deathCount = 0`) + viewpoint relink (Verify(viewpointEntity==NULL) is compiled out at
   DEBUG_LEVEL 0 — release overwrites, same as the 1995 build). @0049fe0c = roster-wide
   slot-11 DeathShutdown broadcast (the death transition already runs that pass).
   CORRECTIONS to the earlier scouting: BTL4.RES **type 12 = InternalAudioStream** (the engine's
   own resource-type table) — `*int.scp` are internal-AUDIO scripts, NOT per-mech intro scripts;
   and `adrop` (id 1994) is a **type-14 MakeMessageStream** = the map's four DropZone make
   records (0xE0 each: classID 3, origin, facing, name "two"/"three"/…), i.e. the respawn
   infrastructure. **⚠ CORRECTION (2026-07-09): the "blue whirlwind" IS in the game — it is the
   TRANSLOCATION SPHERE, and our port STUBS it.** (My earlier "not a shipped asset" verdict was
   WRONG; it was wrong because I searched only RES resource NAMES + the keyword "whirlwind" — but the
   effect (a) loads `tsphere.bgf` by FILENAME via `d3d_OBJECT::LoadObject`, NOT through the RES table,
   and (b) is called "translocate," not "whirlwind.") The mechanism, fully recovered:
   - **Asset:** `content/VIDEO/GEO/TSPHERE.BGF` (2085-byte low-poly sphere; also in the pod install
     REL410/BT + RP + VWETEST/CYCLE). The exe string table has `tsphere.bgf` / `tsphere`.
   - **Engine class:** `POVTranslocateRenderable` (L4VIDRND.cpp:1749 / .h:638) — a `VideoRenderable`
     (Watcher) that loads `tsphere.bgf` and runs a state machine keyed on a `StateIndicator` "effect
     trigger" (the player's `SimulationState` dial) vs a control state: `IdleState` → (trigger hits
     control state) `InitialCollapseState` (sphere scales `COLLAPSE_START_SCALE 100`→1 over
     `COLLAPSE_TIME 1.3s`, then `SetIsDead(true)`) → `WaitForReincarnateState` (jitters via cos/sin
     `TRANSLATE_LIMIT 2.0`) → (trigger leaves control state = respawn) `ExpandRevealState` (scales
     1→`EXPAND_END_SCALE 150` over `EXPAND_TIME 1.0s`, `SetIsDead(false)`) → IdleState. Rotates
     throughout (`ROTATE_RATE`). I.e. the sphere COLLAPSES onto the mech at death and EXPANDS to
     reveal the reborn mech at respawn — exactly the "blue warp that comes to get you & respawns you."
   - **Wiring (already present in our port!):** `btl4vid.cpp` `MakeEntityRenderables` for a
     `BTPlayerClassID` REPLICANT creates `new BTTranslocationRenderable(entity, Watcher, GetMainView(),
     sim_state=GetAttributePointer(SimulationState), drop_zone=GetAttributePointer("DropZoneLocation"),
     1)` (FUN_00458d2c, alloc 0x40, control state 1); our own POV instead gets `BTPOVStartEndRenderable`
     (the mission fade in/out). So peers watch each other's translocation sphere; your own death is a
     POV fade.
   - **✅ DONE + LOG-VERIFIED (2026-07-09, task #52):** `BTTranslocationRenderable` was a no-op stub
     in `btstubs.cpp`; now reconstructed in `btl4vid.cpp` (state machine + `d3d_OBJECT::LoadObject
     ("tsphere.bgf")`), drawn direct from the render loop by `BTDrawTranslocationSpheres` (hooked in
     L4VIDEO.cpp beside `BTDrawBeams`, PASS_ALPHABLEND) — the same direct-draw accommodation the beams/
     reticle use since our VideoComponent tree is partial. Trigger: the LOCAL player's own
     `SimulationState` dial, pulsed `DropZoneAcquired(1)`→`VehicleTranslocated(2)` at respawn (btplayer.cpp
     create branch + a 1.4 s flip timer in `PlayerSimulation`), with the respawn origin written into the
     player's `DropZoneLocation` attribute (the sphere's draw position). Wiring (btl4vid.cpp
     `MakeEntityRenderables`, BTPlayerClassID) originally built the sphere only for REPLICANT players
     (third-party view) + a POV fade for self; extended to build it for the LOCAL player too, since peer
     player-attribute replication (SimulationState/DropZoneLocation) isn't wired (on a replicant both read
     0xCD… uninitialised — confirmed via `BT_TLOC_LOG`). Verified 2-node: on B's respawn the sphere
     COLLAPSES (scale ~100→5) then EXPANDS (→150) at the valid drop-zone origin, deduped to ≤2 active,
     respawn cycle un-regressed, no render errors. Env gate `BT_TLOC_LOG`.
   - **✅ VISUAL DONE + LIVE-VERIFIED (task #52, follow-up):** the on-screen look now matches the
     original cabinet photo (`capture.png`) — a smooth spinning lavender vortex with a bright core. The
     full geometry/material/render authority is now **[[translocation-warp]]**; headlines: tsphere is a
     12-facet BICONE viewed ON-AXIS + spun in place (decomp `FUN_00453dc4` Z-spin, which the WinTesla
     port had stubbed), coloured by the `bintA` cloud through a WIDE lavender ramp, drawn as SKY. The
     defect that hid it for the whole effort was NOT the warp code — it was a general
     **texture-scroll precision-collapse bug** in `L4D3D::SetTextureScrolling` (offset = `scrollDelta ×
     absolute_time` → unbounded → UV precision loss → radial "spokes"), fixed with `fmodf`; that also
     cleans the scrolling beam grit. See [[reconstruction-gotchas]] §13. PEER WARP IS WIRED + VISIBLE
     (`160b78e`): an observer sees a peer's un-wreck warp via the world-anchored `BTStartWarpEffect`
     (mechdmg.cpp:1074, `ReplicantInstance`-gated; `simulationState` rides every record header).
     REMAINING [T3, non-gating]: the peer sphere is anchored to the peer's WORLD position, not the
     peer's authentic `DropZoneLocation` (that attribute isn't replicated) — a fidelity refinement.
     Also confirm the COLLAPSE (death-side) warp fires on every MP death path (force-damage test logged
     EXPAND reliably, COLLAPSE less so).
   NOTE the respawn-cycle SIM path is separate + done: `DropZoneReply` (FUN_004bffd0) → `Mech::Reset`
   (@0049fb74) + the level-2 "translocated" cockpit alarm; `CreatePlayerVehicle` (FUN_004bfcac,
   disassembled — make+link only, matches RP analog). The sphere is a pure RENDER-side watcher on the
   SimulationState dial, independent of that sim path.
   Mech::Reset (@0049fb74) does the full subsystem-reset sweep (mech4.cpp:1616 loops every subsystem →
   `DeathReset(mode)`, restoring heat/power/ammo/charge) + heals all damage zones + ForceUpdate(0x1f).
   (Per-subsystem `DeathReset` bodies are authentically trivial/empty for some classes — the shipped
   build folds those overrides — so "fuller" reset behavior is per-subsystem, not a missing sweep.)
   **Three respawn bugs found + fixed on the way (all [T2], 2-node force-damage verified):**
   (i) **death-latch un-latch** — `IsMechDestroyed` tested `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,
   double `VehicleDead(-1)`). Fixed to latch on `movementMode 2||9` (see combat-damage.md
   "Death-latch correction"). (ii) **score into severed vehicle** — `BTPlayer::ScoreMessageHandler`
   / `ScoreInflictedMessageHandler` deref `playerVehicle` unguarded; during the 5s dead window it is
   0 → guard added (apply the award, skip the vehicle response). (iii) **renderer LoadMission
   re-entry** — the KILLER bug: `MakeAndLinkViewpointEntity` (APP.cpp:1265) calls
   `videoRenderer->LoadMission` on EVERY viewpoint-make, so the respawn re-runs
   `DPLReadEnvironment`→`DPLReadINIPage`; its light block `Fail`s ("All lights must be defined on a
   single INI page") because `sceneLightCount` was never reset between reads (ctor-only init). Fixed
   in L4VIDEO.cpp `DPLReadEnvironment`: tear down the prior mission's `sceneLight[]` +
   `sceneLightCount=0` before re-reading (the per-page invariant is intra-read only). This is a
   PORT-layer re-entrancy gap in the 2007 renderer, not BT logic. Caught via cdb `bp ucrtbased!abort`
   — a `Fail`/`Check` is an abort() MessageBox, not an AV, so `sxe av` misses it.
4. ✅ **Cross-pod beam visuals — DONE (task #51, 2026-07-09).** The keepalive theory needed one
   correction: `FUN_0041c350` = (a) queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the
   app+0x34 manager (our per-weapon render walk plays that role) AND (b) set the subsystem DIRTY bit —
   which maps to the 2007 engine's `updateModel`/`ForceUpdate()`. The REPLICATION rides **subsystem
   update records inside the mech's update message**: the roster walk passes the entity's stream to
   every subsystem's `PerformAndWatch`; `Emitter::WriteUpdateRecord` (@004ba65c, corrected: record
   LENGTH not recordID; rec+0x30 = the TARGET's **EntityID** not a colour) serializes
   firingActive/beamEndpoint/beamFlag/targetID; the engine's `Entity::UpdateMessageHandler` routes
   records by `subsystemID` to `Emitter::ReadUpdateRecord` (@004ba568 — was MIStranscribed as
   TakeDamage; the whole weapon-family slot map was swapped: slot 6=Read, 7=Write, 9=TakeDamage — see
   mechweap.hpp). `ServiceDischarge`/`ContinueDischarge` call `ForceUpdate()` per keepalive tick +
   once at beam end. The render walk was extracted to `Mech::DrawWeaponBeams` (per-mech port cache)
   and runs for REPLICANTS. ⚠ two silent-failure traps fixed: the overrides originally used the
   class's own shadowing `UpdateRecord` typedef as the param type → never overrode the engine virtual
   (base ran, nothing serialized); and `recordLength` must be each level's sizeof or the stream
   framing misparses. VERIFIED 2-node: A fires 57 volleys → 225 records → B applies 225 → draws 414
   replicant beams (PPC blue / laser red from A's replicant's gun ports). Solo un-regressed.
   ✅ **The MECH-level records LANDED (task #1, 2026-07-11)** — see the section below; this
   REMAINING note is closed.
5. ✅ **2-window driving — RESOLVED (tasks #48/#51).** Keyboard MP turning was restored at the root
   (the `pilotArray[1]` shrunk-span overrun fix, task #51); `BT_KEY_NOFOCUS` exists for automated
   harnesses. DEATHS scoring lights via the existing BTPostKillScore MP branch. Respawn is
   reconstructed (task #52 — see item 3b).

## Key Relationships
- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]]
  (entity lifecycle). Detail: `docs/HARD_PROBLEMS.md` (P6).


## Authentic coupled peer motion — DONE (task #50, 2026-07-14) [T1]
Decomp-verified (workflows w0odszxro/wh1h5gnmc, 3 make-or-break claims adversarially CONFIRMED; committed c52a1ad + default-promoted). The 1995 peer per-frame path FUN_004ab430 -> FUN_004ab1c8 (IntegrateMotion) drives motion SINGLE-SOURCE:
- LINEAR position = GAIT. IntegrateMotion advances the BODY gait channel FUN_004a5678 (FUN_004a5028 is DEAD CODE, zero call sites), turns its cycleDistance into velocity `{0,0,-cd/dt}`, rotates by heading, integrates into projectedOrigin.linear@0x260 (part_012.c:14994/14997/15008); localOrigin.linear@0x100 is copied VERBATIM from it (FUN_00408440, part_012.c:15131 -- NOT a lerp). updateVelocity.linear is never used for position.
- ANGULAR/heading = replicated-velocity slerp (the ONLY thing Mover::DeadReckon-style velocity extrapolation drives): projectedOrigin.angular@0x26c built from updateVelocity.angular@0x2d4 (FUN_004ab188), localOrigin.angular slerped toward it.
- 0x260 aliasing: "motionDelta@0x260" and "projectedOrigin@0x260" are LITERALLY THE SAME field (base Mover::projectedOrigin, Origin 0x1c); "worldPose@0x26c" is its quaternion half. Our reconstruction's shadow names hid this and drove the false velocity-vs-gait "contradiction".
- Re-anchor: motionEventVector@0x598 = updateOrigin - localOrigin captured on poseSyncLatch, DECAYED into localOrigin over ~_DAT_004ab9cc (~0.2s) -- no snap. Zeroed in idle states.
- MASTER + PEER run the SAME predictor. The master's SEND-mirror (FUN_004a9b5c @0x4aab9c) advances projectedOrigin by IntegrateMotion(mj=0) fed the LAST-SENT bodyTargetSpeed, then deadbands |localOrigin-projectedOrigin|^2 vs UpdatePositionDiffrence@0x768. A gait-driven peer REQUIRES this gait mirror (a constant-velocity mirror under-sends -> tug-of-war, measured ratio 1.86).
- T4 CLOSED: both leg(0x65c) and body(0x6bc) animators are AnimationInstances of the SAME JointedMover/JointSubsystem (JMOVER.cpp:1382) -- either poses the WHOLE skeleton. Our port keeps the peer on the LEG channel (body unbound on the peer); faithful functional equivalent.

RECONSTRUCTION STATE: coupled path is DEFAULT ON (mech4.cpp: s_drPos gait-coupled, s_gaitMirror/Send). Reverts: BT_DR_POS=1 (peer velocity), BT_NO_MASTER_GAITMIRROR (master const-vel mirror). Measured: single-source coupling backward-steps 0.1% (vs the two-source split's churn), position ratio 1.043. RESIDUAL (follow-up): MEASURED SMALL / effectively resolved (2026-07-15). With the peer now on the BODY channel (`96a896a`, matching the master's body-channel mirror) + affinity pinning, an accel/decel soak (node B `BT_AUTODRIVE`+`BT_DRIVE_SWEEP0` through STOP, node A `BT_SNAPLOG`) measures peer drift maxing at **~0.64u** (median 0.54u, 18 events >0.5u over 48s), gently absorbed at k~0.24/frame -- SUB-UNIT, not the earlier ~2.9u (which predated the body-channel swap AND was measured without CPU-affinity, i.e. with packet-jitter-sparse records). At the noise floor; no fix warranted. The old "run the master mirror on a leg-channel prediction" plan is moot now the peer is on the body channel. Still open (cosmetic): exact `_DAT_004ab9cc` decay constant; whether IntegrateMotion's 2-stage angular integrate is intended.


## Peer motion: the "random shakiness" is single-box packet jitter, NOT the game (task #50, 2026-07-15) [T2]
After the coupled body-channel peer landed (96a896a + turn-step f094d78 + cadence-mirror 23f1532), residual peer shakiness on accel/decel was RANDOM -- sometimes perfect, sometimes shaky. Root cause is the TEST RIG, proven with BT_RXJIT (record inter-arrival ms on the peer): two Debug btl4 nodes on ONE box contend for CPU, so Windows BATCHES their TCP delivery -- update records arrive in bursts (min ~0ms back-to-back, max 56-226ms gaps, burstiness max/avg 3-7x) instead of an even ~17ms. A peer dead-reckons across the long gaps then snaps when a record lands -> random shake. Pinning each node to a DISJOINT core set (ProcessorAffinity 0x00F / 0x3C0) restored even ~17ms delivery (burstiness ~1.0) and the shakiness vanished (user-confirmed). REAL pods are dedicated machines with no contention -- they never see this. DO NOT add an interpolation/jitter buffer or other un-authentic netcode to mask a single-box artifact. Use tools/mp_launch.sh (bakes in the affinity) for all 2-node tests. Diagnostic env: BT_RXJIT (arrival jitter), BT_SLIDE ([slide]/[mslide] slide-in-stand), BT_GAITEV, BT_MIRDIV, BT_NO_MIRROR_CAD / BT_PEER_LEGCH (revert the coupled-peer fixes).