Files
BT412/context/multiplayer.md
T
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

123 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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:
- "Cross-pod combat (damage to a replicant's master); replicant gait; pod-LAN config"
---
# Multiplayer
Pod-to-pod networked play. The hard infrastructure exists (WinTesla replaced DOS `NETNUB` with a
WinSock2 TCP stack); the work is integrating + smoke-testing it for BT. 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 **3446-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. So MP = integrate + smoke-test that stack (gated
on P3 locomotion + P5 entity lifecycle + subsystem WAVEs). 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)
- **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: ROOT-CAUSED to an EntityID wire-corruption (task #47, full trace).**
The whole chain works EXCEPT one wire step; traced end-to-end with `BT_MP_NET`/`BT_MP_FORCE_DMG`:
1. `Entity::Dispatch` (ENTITY.cpp:244) reroutes a replicant's msg — `if(ReplicantInstance)
application->SendMessage(ownerID, EntityManagerClientID, msg)` → `NetworkManager::Send`
(L4NET.CPP:1062) → `send()`. VERIFIED: A dispatches TakeDamage (msgID 21) at B's replicant
(`ownerID=3`), and POST-Dispatch the message carries `entityID=3:22` (host:local) — the
replicant's own ID, matching B's master (`m->GetEntityID()==3:22`).
2. On B: `EntityManager::ReceiveNetworkPacket` (NTTMGR.cpp:107) → `GetEntityPointer(entityID)` →
`Post(EntityManagerEventPriority, entity, msg)` → event drains (ProcessEventTask =
`ProcessOneEvent(0)`, all priorities) → `Event::Process` → `entity->Receive(msg)` →
`activeMessageHandlers->Find(21)` → handler. VERIFIED: the message ARRIVES, is Posted,
Processed, and a handler is Found+called.
3. ⚠ **THE BUG:** B receives the message with `entityID=3:19`, NOT `3:22` — the localID dropped by
exactly the **hostID (3)** somewhere between A's `send()` (3:22) and B's receive (3:19). So
`GetEntityPointer(3:19)` returns the WRONG entity (a **classID 48**, not the mech 0xBB9), whose
base `Entity::TakeDamageMessageHandler` ignores the unaimed (zone 1) hit → B's mech takes 0
damage. The 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 path/direction, not the
receive lookup itself.
**Next:** find the host-relative EntityID (de)serialization that subtracts/adds the hostID on the
dispatched-message wire path (RoutePacket / the packet EntityID encoding) — the update path shows
the correct translation, so diff the two. Diagnostics retained (all `BT_MP_NET`-gated, off in
solo — verified): `[mp-force]` (pre/post-Dispatch entityID), `[mp-send]`/`[mp-recv]` (wire +
classID), `[mp-disp]` (reroute branch), `[mp-evt]`/`[mp-recv2]`/`[mp-hdlr]` (deliver chain),
`BT_MP_LOG` (candidate dump).
## Remaining (P6 phase 4 / Phase 7)
Cross-pod damage WIRE DELIVERY (the step-2 gap above — the next MP task); interactive 2-window
driving + the `-net`-mode aim projection / drive (A's aim ray was dead — `noRay`, and
`BT_AUTODRIVE`/`BT_GOTO` didn't move A in `-net` mode); replicant GAIT animation (derive from
replicated velocity); the pod-LAN config (real IPs, bare-IP pilot entries). See [[open-questions]]. [T3]
## 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**: the world-pick tests only `gEnemyMech` (the solo dummy, btplayer.cpp:791).
MP: the pick must test every OTHER mech (the peer replicants) — generalize to a candidate walk
(mech entities != viewpoint). Small.
2. **Cross-pod damage**: dispatch TakeDamage at the REPLICANT → verify the engine reroute carries
it to the owning master (the KB claims Dispatch reroutes; not yet exercised with our STEP-6
handler). The master takes real zone damage + runs its own death transition (task #42 placed the
death dispatch victim-side — MP-correct by construction).
3. **Victim visuals on the shooter's screen**: the wreck swap gates on zone/graphic state the
REPLICANT copy may never see (zone damage may not replicate — only pose updates confirmed).
Check DamageZone/subsystem state in the update stream; else replicate the death as an event.
4. **Cross-pod beam visuals**: replicant emitters don't run the local trigger sim, so the peer's
beams never draw today. ⭐ The authentic path exists: ServiceDischarge/ContinueDischarge SEND
**beam-keepalive messages** (`FUN_0041c350`, templates @0x511e6c/@0x511e78, emitter.cpp already
stubs them) — decode + route those to drive replicant beam draws.
5. **2-window driving**: input gates on window focus (alternate windows; `BT_KEY_NOFOCUS` for
harness). DEATHS scoring should light via the existing BTPostKillScore MP branch. Respawn =
the P5 teardown debt (deferred until needed).
## Key Relationships
- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]]
(entity lifecycle). Detail: `docs/HARD_PROBLEMS.md` (P6).