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**