ROOT CAUSE: target designation wrote a Mech* into deathPending, disabling respawn (#57/#48)
MEASURED our compiled BTPlayer layout (new one-shot [layout] ctor diagnostic): sizeof=652 (0x28c) killCount@0x270 pad_0x280@0x274 objectiveMech@0x278 deathPending@0x284 The three target-designation sites wrote RAW at pilotArray[0]+0x284 intending the BINARY's objectiveMech. On our object 0x284 is deathPending -- the death-cycle latch. So EVERY target designation (the Comm bank 0x30-0x37, or the typed t/y/u/i/o keys) stored a Mech pointer into deathPending, and a non-zero deathPending makes VehicleDeadMessageHandler take its dedup early-return, which skips the warp, ++deathCount, the PLAYER_DEAD record AND the drop-zone hunt. => designating a target silently disabled your respawn for the rest of the process. That is #57's missing first cause: it explains David's very first death being swallowed with no prior death, and the 3-of-3 swallowed deaths in the final round. It also explains the operator's persistent observation that the trouble began when the comms panel went live -- before the pilot list populated, there was nothing to designate. And the designation never worked either: the value never reached objectiveMech (input-audit finding #1). FIX: all three sites now use named-member bridges in the complete-BTPlayer TU -- BTPilotSetObjectiveMech / BTPilotObjectiveMech / BTPilotVehicle / BTPilotPosition / BTVehicleDestroyed. Also retires the raw +0x1fc (playerVehicle) reads and the raw +0x100 position reads in ChooseNearestPilot, and routes its destroyed-check through the real Mech predicate instead of the Is_Destroyed stub. LAYOUT LOCKS: static_asserts on objectiveMech@0x278, deathPending@0x284, killCount@0x270 and sizeof==0x28c, in the friend fn BTPlayerLayoutSelfCheck, so a member shift fails the BUILD instead of silently clobbering the latch again. Rigs: scratchpad/commstest.py (drives designation + captures panels), valvetest.py, lampgallery.py (428-bitmap gallery for #48 visual search). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
This commit is contained in:
co-authored by
Claude Opus 5
parent
cb50a1d491
commit
2dd4ee3910
@@ -203,6 +203,39 @@ static_assert(offsetof(BTPlayer::MakeMessage, roleName) == 0x90,
|
||||
static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
|
||||
"BTPlayer::MakeMessage wire size must be 0xD0");
|
||||
|
||||
//
|
||||
// LAYOUT LOCK (Gitea #48/#57). Our compiled BTPlayer is NOT the binary's -- the
|
||||
// Entity base differs -- so the 1995 offsets must never be used as raw byte
|
||||
// addresses on this object. These are the MEASURED offsets (printed by the ctor
|
||||
// 2026-07-25); they exist to make the difference explicit and to fail the BUILD
|
||||
// if a member shifts under code that assumes it.
|
||||
//
|
||||
// The bug they memorialise: the target-designation path wrote raw at byte 0x284
|
||||
// intending `objectiveMech` (the binary's offset). Here 0x284 is
|
||||
// **`deathPending`** -- so designating a target silently set the death-cycle
|
||||
// latch and disabled the pilot's respawn for the rest of the process, while the
|
||||
// designation itself never reached `objectiveMech` at all.
|
||||
//
|
||||
// If one of these fires, do NOT "fix" it by changing the number: find whoever is
|
||||
// using a raw offset and give them a named-member bridge instead (this file's
|
||||
// BTPilot* functions are the pattern).
|
||||
//
|
||||
void BTPlayerLayoutSelfCheck() // never called -- a compile-time lock only
|
||||
{
|
||||
static_assert(offsetof(BTPlayer, objectiveMech) == 0x278,
|
||||
"BTPlayer::objectiveMech moved -- the target-designation bridge "
|
||||
"(BTPilotSetObjectiveMech) and any raw 0x284-era offset must be re-checked");
|
||||
static_assert(offsetof(BTPlayer, deathPending) == 0x284,
|
||||
"BTPlayer::deathPending moved -- it is what the old raw 0x284 designation "
|
||||
"write actually clobbered (Gitea #57); re-verify nothing raw-writes it");
|
||||
static_assert(offsetof(BTPlayer, killCount) == 0x270,
|
||||
"BTPlayer::killCount moved -- the scoreboard bridges (BTPilotKills) and the "
|
||||
"binary's 0x27c-era offsets must be re-checked");
|
||||
static_assert(sizeof(BTPlayer) == 0x28c,
|
||||
"BTPlayer size changed -- re-measure the offsets above before trusting any "
|
||||
"raw-offset code that touches this class");
|
||||
}
|
||||
|
||||
static const char *SelfDestructName = "self destruct"; // &DAT_00524b38
|
||||
static const Scalar RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830)
|
||||
static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation)
|
||||
@@ -1354,6 +1387,32 @@ BTPlayer::BTPlayer(
|
||||
currentScore = 0; // this[0x9e]
|
||||
deathPending = 0; // this[0xa4]
|
||||
|
||||
//
|
||||
// LAYOUT GROUND TRUTH (Gitea #48/#57 investigation, one-shot). The target
|
||||
// designation path writes RAW at `pilotArray[0] + 0x284` (mechmppr.cpp:1344,
|
||||
// :1364, :1420) intending the binary's `objectiveMech`. Our compiled layout
|
||||
// is NOT the binary's (our Entity base is ~0x1BC vs the binary's 0x300), so
|
||||
// that write lands on whatever member actually occupies byte 0x284 HERE.
|
||||
// Print the real offsets once so we know what it clobbers -- if 0x284 falls
|
||||
// on `deathPending` this single line explains BOTH the #48 artifacts AND
|
||||
// #57's mysterious first-respawn failures.
|
||||
//
|
||||
{
|
||||
static int s_once = 0;
|
||||
if (!s_once)
|
||||
{
|
||||
s_once = 1;
|
||||
DEBUG_STREAM << "[layout] sizeof(BTPlayer)=" << (int)sizeof(BTPlayer)
|
||||
<< " (0x" << std::hex << (int)sizeof(BTPlayer) << std::dec << ")"
|
||||
<< " killCount@0x" << std::hex << (int)offsetof(BTPlayer, killCount)
|
||||
<< " pad_0x280@0x" << (int)offsetof(BTPlayer, pad_0x280)
|
||||
<< " objectiveMech@0x" << (int)offsetof(BTPlayer, objectiveMech)
|
||||
<< " deathPending@0x" << (int)offsetof(BTPlayer, deathPending)
|
||||
<< std::dec << " <-- the raw write targets byte 0x284\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Remember our team name from the creation message.
|
||||
//
|
||||
@@ -1558,6 +1617,86 @@ int BTPilotDeaths(void *pilot)
|
||||
// roster row was a blank box + blanked-zero numerals: invisible until the
|
||||
// pilot scored. Camera players carry index -1 (PLAYER.cpp:744) -> NULL ->
|
||||
// DrawMechIcon's authentic cache-miss box branch.
|
||||
//
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// TARGET DESIGNATION bridges (Gitea #48/#57 -- THE `+0x284` BUG).
|
||||
//
|
||||
// `MechControlsMapper`'s three designation sites wrote the chosen target RAW at
|
||||
// `pilotArray[0] + 0x284`, intending the binary's `BTPlayer::objectiveMech`, and
|
||||
// read the target's vehicle RAW at `chosen + 0x1fc` (`Player::playerVehicle`) and
|
||||
// its position at `+0x100`. Those are the 1995 offsets. MEASURED ground truth
|
||||
// for OUR compiled object (printed by the ctor, 2026-07-25):
|
||||
//
|
||||
// sizeof(BTPlayer) = 652 (0x28c)
|
||||
// killCount @ 0x270 pad_0x280 @ 0x274
|
||||
// objectiveMech @ 0x278 deathPending @ 0x284 <-- what 0x284 really is
|
||||
//
|
||||
// So every target designation wrote a Mech pointer into **`deathPending`**, the
|
||||
// death-cycle latch. A non-zero `deathPending` makes
|
||||
// `VehicleDeadMessageHandler` take its dedup early-return (btplayer.cpp:~404),
|
||||
// which skips the warp, `++deathCount`, the `PLAYER_DEAD` record AND the
|
||||
// drop-zone hunt -- i.e. **designating a target silently disabled your respawn
|
||||
// for the rest of the process.** That is the missing first cause in #57 (and
|
||||
// why David's very first death was already swallowed with no prior death to
|
||||
// explain it). The designation itself also never worked, because the value
|
||||
// never reached `objectiveMech` (input-audit finding #1).
|
||||
//
|
||||
// Fixed the only correct way: named members, resolved in this complete-type TU.
|
||||
//
|
||||
void BTPilotSetObjectiveMech(void *pilot, void *target_vehicle)
|
||||
{
|
||||
if (pilot == 0)
|
||||
return;
|
||||
((BTPlayer *)pilot)->objectiveMech = (Mech *)target_vehicle;
|
||||
}
|
||||
|
||||
void *BTPilotObjectiveMech(void *pilot)
|
||||
{
|
||||
return (pilot != 0) ? (void *)((BTPlayer *)pilot)->objectiveMech : 0;
|
||||
}
|
||||
|
||||
//
|
||||
// The designated target's VEHICLE (the binary's raw `chosen + 0x1fc`) --
|
||||
// `Player::playerVehicle`, via the engine accessor.
|
||||
//
|
||||
void *BTPilotVehicle(void *pilot)
|
||||
{
|
||||
return (pilot != 0) ? (void *)((BTPlayer *)pilot)->GetPlayerVehicle() : 0;
|
||||
}
|
||||
|
||||
//
|
||||
// The pilot's world position (the binary's raw `+ 0x100`). Reads it off the
|
||||
// pilot's VEHICLE, which is where a Player's position actually lives in our
|
||||
// layout -- a Player is not a positioned entity. Returns 0 if the pilot has no
|
||||
// vehicle (dead / not yet acquired), so callers can skip it in a distance test.
|
||||
//
|
||||
int BTPilotPosition(void *pilot, float *out_xyz)
|
||||
{
|
||||
if (pilot == 0 || out_xyz == 0)
|
||||
return 0;
|
||||
Entity *veh = (Entity *)((BTPlayer *)pilot)->GetPlayerVehicle();
|
||||
if (veh == 0)
|
||||
return 0;
|
||||
const Point3D &p = ((Mover *)veh)->localOrigin.linearPosition;
|
||||
out_xyz[0] = p.x; out_xyz[1] = p.y; out_xyz[2] = p.z;
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Is this pilot's vehicle destroyed? (the binary's `Is_Destroyed(entity)` on the
|
||||
// raw `+0x1fc` handle -- our `Is_Destroyed` is still a stub, btstubs.cpp:76, so
|
||||
// resolve it through the real Mech predicate in this complete-type TU).
|
||||
//
|
||||
int BTVehicleDestroyed(void *vehicle)
|
||||
{
|
||||
if (vehicle == 0)
|
||||
return 1; // no vehicle == not targetable
|
||||
Entity *e = (Entity *)vehicle;
|
||||
if (e->GetClassID() != Mech::MechClassID)
|
||||
return 0;
|
||||
return ((Mech *)e)->IsMechDestroyed() ? 1 : 0;
|
||||
}
|
||||
|
||||
BitMap *BTPilotNameBitmap(void *pilot)
|
||||
{
|
||||
if (pilot == 0 || application == 0)
|
||||
|
||||
Reference in New Issue
Block a user