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>
This commit is contained in:
arcattack
2026-07-09 12:09:50 -05:00
co-authored by Claude Fable 5
parent c78662a0f1
commit 04a2049ce2
7 changed files with 55 additions and 75 deletions
+37 -23
View File
@@ -62,31 +62,45 @@ the solo `gEnemyMech` to the picked `hotTarget` (missile impact + projectile val
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`:
**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()`. 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).
(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