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>
8.7 KiB
id, title, status, source_sections, related_topics, key_terms, open_questions
| id | title | status | source_sections | related_topics | key_terms | open_questions | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| multiplayer | Multiplayer — replication, netcode, the console | provisional | PROGRESS_LOG.md §7 (Phase 7), §8 (P6); docs/HARD_PROBLEMS.md (P6) |
|
|
|
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
NETNUBwith 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 rumoredL4NET.CPP:1853send-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 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)
- 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 aconst 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:
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 carriesentityID=3:22(host:local) — the replicant's own ID, matching B's master (m->GetEntityID()==3:22).- 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. - ⚠ THE BUG: B receives the message with
entityID=3:19, NOT3:22— the localID dropped by exactly the hostID (3) somewhere between A'ssend()(3:22) and B's receive (3:19). SoGetEntityPointer(3:19)returns the WRONG entity (a classID 48, not the mech 0xBB9), whose baseEntity::TakeDamageMessageHandlerignores the unaimed (zone −1) hit → B's mech takes 0 damage. The AUTO-REPLICATED UPDATE records (msgID 18) arrive with the CORRECT3:22and 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 (allBT_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):
- 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. - 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).
- 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.
- 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. - 2-window driving: input gates on window focus (alternate windows;
BT_KEY_NOFOCUSfor 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).