--- 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 ` = networked mode (the pod listens on for a CONSOLE, boots ConsoleOnly; game mesh = +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=`** (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] ## MATCH FORENSICS (`BT_MATCHLOG`, permanent -- 2026-07-22) [T2] Per-peer forensic combat log for MP playtests, built for the first 8-player session. Because damage authority is VICTIM-side, no single peer sees the whole match: every instance writes `matchlog__