From d4ba91bd394e3ebf9d4bfdbfc607c9ef3e045310 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 30 Jul 2026 08:31:29 -0500 Subject: [PATCH] #81 THE ANSWER: deathPending is OUR invention -- the binary has no death latch Read BT's own respawn code and checked it against BTL4OPT.EXE itself. FAITHFUL: BT's Player::VehicleDeadMessageHandler (FUN_0042db80, part_003.c:12029) gates on message->deathCount == player->deathCount AND player+0x40 != 1 (simulationState != DropZoneAcquiredState) -- exactly WinTesla's two gates -- then runs the same closest-DropZone search skipping "win*" zones, dispatches AssignDropZone and re-posts to itself on a timer. So the retry loop and the cross-machine hunt are authentic 1995 behaviour, not a WinTesla artifact. NOT FAITHFUL: BT's BTPlayer::VehicleDeadMessageHandler (FUN_004c012c, part_013.c:10504) ENDS with *(param_1 + 0x290) = 0 -- it CLEARS the field our reconstruction calls deathPending, at the end of every death. Verified in the raw binary: +0x290 is written in exactly three places in the whole executable (0x0b75fb, 0x0bffe3, 0x0c0a05) and ALL THREE store a zeroed register (xor ecx,ecx / xor eax,eax / xor edx,edx immediately before). There is NO write of 1 or of any non-zero value to +0x290 anywhere in BTL4OPT.EXE. Two are ctor/reset sweeps; the middle one is the death handler, sitting right after the call to Post and add esp,0x14, matching the decompiled tail exactly. So 1995 has NO death-pending gate. We invented it (btplayer.cpp:505) and then needed six clear sites to patch the strandings it caused (:382 :469 :1431 :1442 :1461 :1532). #57 and #55 are artifacts of that invention. It is also what makes a ghost PERMANENT: in BT a failed respawn is harmless (the 2s re-post keeps hunting, the next death starts a clean cycle); in ours the first failure latches the pilot and every later death is SWALLOWED forever -- exactly the field signature of 8 cycles, 6 stranded, none recovering. Fix proposed in the doc (match the binary: clear instead of latch, drop the dedup gate) but NOT applied -- six sites depend on the latch and the dedup is load-bearing, so it wants a deliberate two-node bench, not a 1am edit. Co-Authored-By: Claude Fable 5 --- docs/GHOST_MECH_ANALYSIS.md | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/GHOST_MECH_ANALYSIS.md b/docs/GHOST_MECH_ANALYSIS.md index fab04bf..7bd825e 100644 --- a/docs/GHOST_MECH_ANALYSIS.md +++ b/docs/GHOST_MECH_ANALYSIS.md @@ -131,6 +131,53 @@ reply whose `deathCount` is *ahead* of ours (our counter is behind) instead of d never leave `deathPending` latched on a discard. ⚠ Neither should be guessed at blind — the `[ghost] DROP-ZONE REPLY DISCARDED` line's mismatch direction decides it in one line of log. +## HOW THE BINARY HANDLES IT — `deathPending` is OUR INVENTION [T1, decisive] +Read BT's own respawn code (it *is* in the export, even though `munga/dropzone.cpp` is not) and +checked it against the raw binary. + +**The hunt is faithful.** BT's `Player::VehicleDeadMessageHandler` = `FUN_0042db80` +(`part_003.c:12029`) gates on `message->deathCount == player->deathCount` **and** +`player+0x40 != 1` — i.e. `simulationState != DropZoneAcquiredState`, exactly WinTesla's two gates — +then runs the same closest-DropZone search skipping `"win*"` zones, dispatches `AssignDropZone`, and +**re-posts the message to itself** on a timer. So the retry loop and the cross-machine hunt are +authentic 1995 behaviour, not a WinTesla artifact. + +**But the latch is not.** BT's `BTPlayer::VehicleDeadMessageHandler` = `FUN_004c012c` +(`part_013.c:10504`) ends with: +``` +++deathCount; message->deathCount = deathCount; --lives; +updateModel |= 1; alarm(+0x2c, 1); save dropZoneLocation; +Post(HighPriority, self, message, Now()+delay); +*(param_1 + 0x290) = 0; // <-- the field our reconstruction calls deathPending +``` +**The binary CLEARS that field at the end of every death.** Verified against `BTL4OPT.EXE` itself: +`+0x290` is written in exactly **three** places in the whole executable +(`file_off 0x0b75fb`, `0x0bffe3`, `0x0c0a05`), and **all three store a zeroed register** — +`xor ecx,ecx` / `xor eax,eax` / `xor edx,edx` immediately before. There is **no write of 1, or of +any non-zero value, to `+0x290` anywhere in the binary.** Two of the three are constructor/reset +sweeps; the middle one is the death handler above (it sits right after the `call` to `Post` and +`add esp,0x14`, matching the decompiled tail exactly). + +**So in 1995 there is no death-pending gate at all.** Our `btplayer.cpp:505` does +`deathPending = 1`, and six separate sites then have to clear it (`:382 :469 :1431 :1442 :1461 +:1532`) — each one a patch for a different stranding scenario. Gitea **#57 and #55 are artifacts of +that invention**, not of the original design. + +**And that is what makes a ghost PERMANENT.** In BT a failed respawn is harmless: the 2-second +re-post keeps hunting, and the next death starts a clean cycle. In ours the first failure latches +the pilot — every later death hits the dedup and is SWALLOWED ("deaths so far 1" forever), so the +pilot can never recover for the rest of the mission. That is precisely the field signature: 8 death +cycles, 6 stranded, none of them ever recovering. + +**Proposed fix (faithful, and it removes the whole bug class):** match the binary — clear the field +at the end of the death handler instead of latching it, and drop the dedup gate that depends on it. +⚠ Six sites currently rely on that latch's existence, and the SWALLOWED dedup is load-bearing for +the "one death, one cycle" behaviour, so this wants a deliberate two-node bench rather than a +late-night edit. Caveat on the identification: `+0x290 ↔ deathPending` rests on our own +reconstruction's mapping (our compiled `BTPlayer` is only `0x28c` bytes, so the offsets cannot +match directly) — but the *behavioural* evidence (BT's death handler clears it; nothing in the +binary ever sets it) is read straight from the executable. + ## What the instrumentation already killed `slots=8` (measured, `[dz] POOL`). An 8-slot pool **cannot** be cooldown-starved by 5 players (at most 5 slots on cooldown at once), so the shared `dropzone=one` in `tools/eggmodel.py:42` is **not**