Files
BT412/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

137 lines
10 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: 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::Process` →
`entity->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
- Base: [[wintesla-port]] (L4NET). Depends on: [[locomotion]] (update writer), [[combat-damage]]
(entity lifecycle). Detail: `docs/HARD_PROBLEMS.md` (P6).