Files
BT411/context/multiplayer.md
T
arcattackandClaude Fable 5 04a2049ce2 MP task #47: DISPROVE the Entity-layout root cause; correct the record
The prior commit (c78662a) filed cross-pod damage failure under a
"cross-TU Entity/EntityID layout divergence" (and the P5 base-region
audit). A compile-time offsetof probe injected into BOTH translation
units disproves that: game/reconstructed AND engine/MUNGA compute an
IDENTICAL Entity layout --

  sizeof(Entity)=444  offsetof(entityID)=380 (0x17C)
  offsetof(ownerID)=388  offsetof(simulationFlags)=32
  offsetof(Mech,entityID)=380  (Entity subobject at 0)

So Entity::Dispatch reads this->entityID at the SAME offset mech4's
GetEntityID() does -- there is no per-TU read difference, and the
logged 3:22 (mech4 candidate) vs 3:19 (engine Dispatch) cannot be one
object read two ways. Those lines were different objects/messages,
mis-correlated. The distinct, REAL P5 "base-region layout divergence"
(HARD_PROBLEMS.md) is about raw-offset stomps at this+0x2d4..0x2f0 --
far above entityID -- and is unrelated to #47.

Changes:
- Revert the committed BT_MP_NET engine diagnostics (ENTITY/EVENT/
  NTTMGR/RECEIVER/L4NET) to the clean a9c3e96 baseline -- those are
  the very instruments that produced the mis-correlated data.
- mech4 BT_MP_FORCE_DMG hook: dispatch via the real m->Dispatch(&td)
  path (no false id stamping); comment records the offsetof finding.
- context/multiplayer.md: layout-divergence RULED OUT; 3:22/3:19 marked
  unconfirmed; leading hypotheses reframed as H2 (wire host-relative
  (de)serialization) vs H3 (replicant id != master's registered key),
  to be distinguished by a 2-node run with per-message correlation.

Solo un-regressed (mech walks + targets, 0 faults). Cross-pod delivery
remains open, but the investigation is redirected off the wrong (large,
structural) layout-audit path onto the EntityID wire/id-assignment path.

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

10 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: the delivery step FAILS; the DISPATCH + POST + PROCESS chain works. Traced end-to-end (BT_MP_NET/BT_MP_FORCE_DMG). VERIFIED parts:

  1. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's msg — if(ReplicantInstance) application->SendMessage(ownerID, EntityManagerClientID, msg)NetworkManager::Send (L4NET.CPP:1062) → send(). A dispatches a TakeDamage (msgID 21) at B's replicant (ownerID=3).
  2. On B: EntityManager::ReceiveNetworkPacket (NTTMGR.cpp:107) → GetEntityPointer(entityID) = entityIndexSocket.Find(id) (HOSTMGR.h:270, a hash keyed by EntityID operator==) → Post(EntityManagerEventPriority, entity, msg) → event drains → Event::Processentity->Receive(msg)activeMessageHandlers->Find(21) → handler. The message ARRIVES, is Posted, Processed, and a handler is Found+called — but on the WRONG entity, so B's mech takes 0 dmg.

ROOT CAUSE — CORRECTED 2026-07-09 (task #47). The earlier "cross-TU Entity layout divergence" explanation is DISPROVEN. A one-shot offsetof probe compiled into BOTH translation units ([mp-layout], since removed) proved the Entity layout is IDENTICAL in the reconstructed (game/reconstructed) and engine (engine/MUNGA) TUs: sizeof(Entity)=444, offsetof(entityID)=380 (0x17C), offsetof(ownerID)=388, offsetof(simulationFlags)=32 — and offsetof(Mech,entityID)=380 too (Entity subobject at 0). So Entity::Dispatch reads this->entityID at the SAME offset mech4's GetEntityID() does; there is NO per-TU read difference. (This is distinct from the REAL P5 "base-region layout divergence" in docs/HARD_PROBLEMS.md, which is about raw-offset STOMPS at this+0x2d4..0x2f0 — offsets far above entityID — and does not touch the entityID member. The prior session wrongly filed #47 under that P5 issue.)

Consequence: the previously-logged 3:22 (mech4 candidate) vs 3:19 (engine Dispatch) readings CANNOT be the same object read two ways (identical offset ⇒ identical value) — they were different objects/messages logged on different lines and mis-correlated. So the "localID drops by hostID on the wire" claim is unconfirmed and must be re-measured. Two live hypotheses remain, to be distinguished by a 2-node run with PER-MESSAGE-correlated logging (a sequence id + the entity pointer on both ends):

  • H2 wire (de)serialization: the dispatched-message packet path encodes/decodes EntityID host-relative and one direction is mismatched (the auto-replicated UPDATE path resolves fine — diff the two packet encoders). EntityID has operator±/operator- (ENTITYID.cpp) that a host-relative encoder would use.
  • H3 id-assignment mismatch: A's replicant carries an EntityID that never equalled B's master's registered key, so entityIndexSocket.Find hits a different (classID-48) entity; UPDATEs still land because replication routes them via the subscription, not a raw Find. Check the replicant's id at creation (the Make/Subscribe from B) against B's master's registered id. Next (needs the 2-node rig): run BT_MP_FORCE_DMG (now dispatches via the real m->Dispatch(&td) path, logging [mp-force] <ptr> id=… ownerID=…) on A while capturing, on B, the Find key + the master's own id + classID, correlated per message. That resolves H2 vs H3 directly. [mp-force] is the retained gated instrument (off in solo — verified).

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