From 04a2049ce276861e0f186e5ec024d84f3e3f0bb6 Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 12:09:50 -0500 Subject: [PATCH] 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 --- context/multiplayer.md | 60 ++++++++++++++++++++++-------------- engine/MUNGA/ENTITY.cpp | 7 ----- engine/MUNGA/EVENT.cpp | 5 --- engine/MUNGA/NTTMGR.cpp | 17 ---------- engine/MUNGA/RECEIVER.cpp | 6 ---- engine/MUNGA_L4/L4NET.CPP | 8 ----- game/reconstructed/mech4.cpp | 27 ++++++++++------ 7 files changed, 55 insertions(+), 75 deletions(-) diff --git a/context/multiplayer.md b/context/multiplayer.md index bcb22c7..ae19b24 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -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] 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 diff --git a/engine/MUNGA/ENTITY.cpp b/engine/MUNGA/ENTITY.cpp index 1fdb495..c214eb2 100644 --- a/engine/MUNGA/ENTITY.cpp +++ b/engine/MUNGA/ENTITY.cpp @@ -241,13 +241,6 @@ void // If this is a replicant instance, reroute the message to the master //------------------------------------------------------------------- // - if (getenv("BT_MP_NET") && message->messageID != 6) - DEBUG_STREAM << "[mp-disp] Entity::Dispatch msgID=" << (int)message->messageID - << " inst=" << (int)GetInstance() - << (GetInstance() == ReplicantInstance ? " REPLICANT -> reroute to master" - : (IsValid() ? " master/valid -> local Receive" : " INVALID -> Post")) - << "\n" << std::flush; - if (GetInstance() == ReplicantInstance) { application->SendMessage( diff --git a/engine/MUNGA/EVENT.cpp b/engine/MUNGA/EVENT.cpp index 56d338a..1ce56e0 100644 --- a/engine/MUNGA/EVENT.cpp +++ b/engine/MUNGA/EVENT.cpp @@ -258,11 +258,6 @@ void Event::Process() { Check(this); - // MP DIAGNOSTIC (task #47): a network-delivered TakeDamage (msgID 21) is - // carried by an event -- confirm it actually gets Processed (vs deferred). - if (messageToSend && messageToSend->messageID == 21 && getenv("BT_MP_NET")) - DEBUG_STREAM << "[mp-evt] Event::Process TakeDamage target=" - << (void*)targetReceiver.GetCurrent() << "\n" << std::flush; targetReceiver.GetCurrent()->Receive(this); } diff --git a/engine/MUNGA/NTTMGR.cpp b/engine/MUNGA/NTTMGR.cpp index 13725af..8847f06 100644 --- a/engine/MUNGA/NTTMGR.cpp +++ b/engine/MUNGA/NTTMGR.cpp @@ -130,23 +130,6 @@ void Entity *entity = application->GetHostManager()->GetEntityPointer(message->entityID); - - // MP DIAGNOSTIC (task #47, BT_MP_NET): log every incoming entity-manager - // message (a dispatched cross-pod message arrives here) + whether the - // target entity was found. msgID 6 (update records) filtered as noise. - if (getenv("BT_MP_NET") && message->messageID != 6) - DEBUG_STREAM << "[mp-recv] msgID=" << (int)message->messageID - << " entityID=" << message->entityID - << " len=" << (int)message->messageLength - << (entity ? " ENTITY FOUND" : " NO ENTITY (remake?)") - << (entity ? " ptr=" : "") << (entity ? (void*)entity : (void*)0) - << (entity ? " classID=" : "") ; - if (getenv("BT_MP_NET") && message->messageID != 6 && entity) - DEBUG_STREAM << (int)entity->GetClassID() - << ((int)entity->GetClassID() == 0xBB9 ? " (MECH) -> Post" : " (NOT MECH!) -> Post"); - if (getenv("BT_MP_NET") && message->messageID != 6) - DEBUG_STREAM << "\n" << std::flush; - if (entity) { // diff --git a/engine/MUNGA/RECEIVER.cpp b/engine/MUNGA/RECEIVER.cpp index 90a57bf..6966179 100644 --- a/engine/MUNGA/RECEIVER.cpp +++ b/engine/MUNGA/RECEIVER.cpp @@ -98,12 +98,6 @@ void // Receiver::SharedData *sharedData = GetSharedData(); Handler handler = sharedData->activeMessageHandlers->Find(what->messageID); - // MP DIAGNOSTIC (task #47): for a TakeDamage (msgID 21), report whether a - // handler is found on this receiver (the cross-pod delivery endpoint). - if (what->messageID == 21 && getenv("BT_MP_NET")) - DEBUG_STREAM << "[mp-recv2] Receive TakeDamage this=" << (void*)this - << " handler=" << (handler != Receiver::NullHandler ? "FOUND" : "NULL") - << "\n" << std::flush; if (handler != Receiver::NullHandler) { (this->*handler)(what); diff --git a/engine/MUNGA_L4/L4NET.CPP b/engine/MUNGA_L4/L4NET.CPP index 46250f7..71b816e 100644 --- a/engine/MUNGA_L4/L4NET.CPP +++ b/engine/MUNGA_L4/L4NET.CPP @@ -1140,14 +1140,6 @@ void L4NetworkManager::Send( //Net_Common_Ptr->Buffer_Length = (short)SEND_BUFFER_SIZE(packet_size); //send_packet_request->Socket_Ptr = l4host->GetNetworkSocket(); //NetNub::SendCommand(); - // MP DIAGNOSTIC (task #47, BT_MP_NET): log every non-update send so a - // dispatched cross-pod message (e.g. TakeDamage) is visible on the wire. - // (Update records are msgID 6, ~144 bytes -- filtered out to cut noise.) - if (getenv("BT_MP_NET") && !(message->messageID == 6 && message->messageLength <= 160)) - DEBUG_STREAM << "[mp-send] host=" << (long)host_ID << " client=" << (int)client - << " msgID=" << (int)message->messageID - << " len=" << (int)message->messageLength << "\n" << std::flush; - send(l4host->GetNetworkSocket(), (char *)my_temp_packet, packet_size, 0); free(my_temp_packet); // Check for errors and abort if there were any diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 7f163d6..002cea1 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2390,6 +2390,10 @@ void static float s_candLog = 0.0f; s_candLog += dt; const int candLog = (getenv("BT_MP_LOG") && s_candLog >= 1.0f); if (candLog) s_candLog = 0.0f; + if (candLog) + DEBUG_STREAM << "[mp-self] MY mech entityID=" << GetEntityID() + << " inst=" << (int)GetInstance() << " (" << nc << " candidates)\n" + << std::flush; if (candLog) for (int ci = 0; ci < nc; ++ci) { @@ -2405,10 +2409,16 @@ void << " zones=" << m->damageZoneCount << "\n" << std::flush; } - // CROSS-POD DAMAGE PROOF HOOK (BT_MP_FORCE_DMG, task #46): once a - // second, dispatch a real TakeDamage at the first live REPLICANT - // candidate -- isolating the Dispatch-reroute-to-owning-master path - // from the MP interactive aim/drive plumbing. + // CROSS-POD DAMAGE PROOF HOOK (BT_MP_FORCE_DMG, task #46/#47): once a + // second, dispatch a real unaimed TakeDamage at the first live + // REPLICANT candidate through the SAME engine path a beam hit uses + // (Entity::Dispatch -> replicant reroute -> SendMessage(ownerID)). + // This isolates the cross-pod delivery from the interactive aim/drive + // plumbing so a 2-node test can watch the wire without having to line + // up a boresight. It does NOT stamp/patch the id -- Entity::Dispatch + // reads this->entityID at offset 0x17C, which the [mp-layout] probe + // proved is IDENTICAL in the reconstructed and engine TUs (both 380, + // sizeof(Entity)=444), so no cross-TU translation is needed or done. if (getenv("BT_MP_FORCE_DMG")) { static float s_fd = 0.0f; s_fd += dt; @@ -2430,11 +2440,10 @@ void Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), GetEntityID(), -1, dmg); - DEBUG_STREAM << "[mp-force] PRE m->GetEntityID()=" << m->GetEntityID() - << " td.entityID=" << td.entityID << "\n" << std::flush; - ((Entity *)m)->Dispatch(&td); - DEBUG_STREAM << "[mp-force] POST td.entityID=" << td.entityID - << " (== what Dispatch put on the wire)\n" << std::flush; + DEBUG_STREAM << "[mp-force] " << (void*)m + << " Dispatch unaimed TakeDamage id=" << m->GetEntityID() + << " ownerID=" << (int)m->GetOwnerID() << "\n" << std::flush; + m->Dispatch(&td); break; } }