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>
123 lines
8.7 KiB
Markdown
123 lines
8.7 KiB
Markdown
---
|
||
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).
|