MP task #47 SOLVED: cross-pod damage + kill (validate the master mech)

Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3)
dead over the network; B runs the death transition on its own screen. Verified
on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits
of 12 and is DESTROYED, with wreck swap + explosion.

ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*)
does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that
arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler
never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants
(the P6 bring-up hack for the partial MakeReady handshake); a locally-created
master never got validated.

FIX (one line): Mech::Make now sets ValidFlag for the master too -- the
reconstructed ctor builds the mech synchronously, so it's safe. Nothing else
changed: targeting, the virtual victim->Dispatch, the replicant reroute, the
wire, receive, and id resolution were ALL already correct.

Two prior root causes were BOTH mis-diagnoses from the WRONG messageID:
Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered
msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21
hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU
Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof
444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the
id is correct end-to-end (3:22 = B's master).

- game/reconstructed/mech.cpp: validate master + replicant at Make.
- game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated
  2-node cross-pod reproducer (real virtual Dispatch path).
- context/multiplayer.md, reconstruction-gotchas.md: corrected root cause +
  the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean.

Solo un-regressed (the solo enemy was already force-valid; the change only ADDS
ValidFlag). btl4.exe builds; full mission loop clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 12:52:26 -05:00
co-authored by Claude Fable 5
parent 04a2049ce2
commit 54b6663b4d
4 changed files with 75 additions and 66 deletions
+11 -13
View File
@@ -2409,16 +2409,13 @@ void
<< " zones=" << m->damageZoneCount << "\n" << std::flush;
}
// 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.
// CROSS-POD DAMAGE TEST HOOK (BT_MP_FORCE_DMG, task #47): once a second,
// dispatch an unaimed TakeDamage at the first live REPLICANT through the
// SAME virtual Entity::Dispatch path a beam hit uses -- Entity::Dispatch
// reroutes a replicant's message to the owning master over the wire, the
// master's now-valid mech resolves + applies it (cylinder lookup). Lets a
// 2-node test exercise cross-pod delivery without lining up a boresight.
// Off by default; the interactive beam path needs no such hook.
if (getenv("BT_MP_FORCE_DMG"))
{
static float s_fd = 0.0f; s_fd += dt;
@@ -2440,9 +2437,10 @@ void
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(), -1, dmg);
DEBUG_STREAM << "[mp-force] " << (void*)m
<< " Dispatch unaimed TakeDamage id=" << m->GetEntityID()
<< " ownerID=" << (int)m->GetOwnerID() << "\n" << std::flush;
if (getenv("BT_MP_LOG"))
DEBUG_STREAM << "[mp-force] " << (void*)m
<< " cross-pod TakeDamage id=" << m->GetEntityID()
<< " -> ownerID=" << (int)m->GetOwnerID() << "\n" << std::flush;
m->Dispatch(&td);
break;
}