Files
BT412/context/multiplayer.md
T
arcattackandClaude Fable 5 bac45851ad MP task #48 (partial): -net aim+drive already work; localize the real gap
Re-measured interactive -net combat on a 2-node run. The KB's "aim ray dead /
drive dead in -net" was STALE: A's aim ray is live (noRay=0, the active eye
publishes the aim camera) and A drives (BT_AUTODRIVE/BT_GOTO move it). The real
blocker to a human cross-pod kill via the beam path is NOT the netcode:

- Boresight inherits the mech BODY pitch. The aim ray = the active eye's view
  dir; on sloped terrain the body pitches to conform and the boresight points
  ~8 deg down ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y=-0.145 every frame),
  so at range the ray hits terrain ~50-100m short of the enemy (mechPicks=0).
- Drive/goto stalls ~700m short before closing to pick/weapon range.
- PickRayHit vs a replicant is thus unconfirmed, but the replicant localToWorld
  IS correct (lstart magnitude == true A->B distance; the DeadReckon +
  localToWorld=localOrigin block already tracks the replicated position).

Change: add a gated BT_GOTO="enemy" test mode (beeline toward the nearest live
replicant) -- MP spawn coords vary per run so a fixed "x z" can't reliably face
the peer. KB (multiplayer.md) updated with the corrected findings + the real
remaining work (torso-pitch/level-boresight aim ray + drive-to-range).

Solo un-regressed; btl4.exe builds; KB validator clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:13:41 -05:00

12 KiB
Raw Blame History

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)
wintesla-port
combat-damage
locomotion
decomp-reference
master
replicant
EGG
dead-reckon
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 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 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:548) → 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:554) 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) — partial; the blocker is NOT the netcode [T2]

Re-measured on a 2-node one-box run (A BT_AUTODRIVE/BT_GOTO, BT_AIM_LOG, BT_MP_LOG). The prior "aim ray dead / drive dead in -net" claim was STALE — both work now: A's aim ray is live (noRay=0, the active eye publishes the aim camera), and A DRIVES (BT_AUTODRIVE/BT_GOTO move it). What actually blocks a HUMAN cross-pod kill via the real beam path:

  • Boresight inherits the mech BODY pitch. The aim ray = the active EYE's view direction (with reticle at 0,0). On sloped terrain the mech body pitches to conform (ground model), the cockpit eye/boresight pitches with it, and the pick ray points ~8° DOWN ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y≈-0.145, every frame). At range that ray hits terrain (groundPicks) ~50-100 m short of the enemy → mechPicks=0. A close enemy (< ~50 m) would still resolve. Real BT aims the TORSO pitch independently of leg/body tilt; the reconstruction ties the boresight to the render eye. Fixing = derive the aim ray from a level torso boresight (yaw only) OR add torso pitch, not the pitched body eye.
  • Drive/goto stalls. A repeatedly stops (gait adv=0) ~700-750 m from the enemy before closing to weapon/pick range — a locomotion/goto reliability issue, not MP.
  • PickRayHit vs a replicant is UNCONFIRMED (blocked by the two above — A never faces the enemy at close range). localToWorld on the replicant IS correct ([pick] lstart magnitude ≈ true A→B distance; the DeadReckon + localToWorld=localOrigin block at mech4.cpp:1191 tracks the replicated position). Test aid added: BT_GOTO="enemy" beelines toward the nearest live replicant (spawn coords vary per run, so a fixed "x z" can't reliably face the peer). Recipe: scratchpad/mp_kill_test.sh.

Remaining (P6 phase 4 / Phase 7)

Human-playable cross-pod aim (torso-pitch / level-boresight aim ray + drive-to-range reliability, above) so a HUMAN — not just the FORCE_DMG hook — can aim + fire cross-pod; 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