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 cleana9c3e96baseline -- 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:
co-authored by
Claude Fable 5
parent
c78662a0f1
commit
04a2049ce2
+37
-23
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user