Commit Graph
4 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Fable 5 d4ba91bd39 #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 <noreply@anthropic.com>
2026-07-30 08:31:29 -05:00
Joe DiPrimaandClaude Fable 5 bf0555ccf1 #81: every MP respawn is a cross-machine round trip -- one degraded peer ghosts everyone
Two-node bench with the fixed host:local labels shows each player's drop-zone
request is answered by the OTHER machine's DropZone: A asks, B grants; B asks, A
grants.  FindGroup("DropZones") iterates replicants of remotely-mastered zones
too and takes the geometrically closest, so a respawn is
  my request -> an arbitrary peer's DropZone -> that peer's reply -> back to me.
With 5 players that is 5 round trips through arbitrary peers, and ONE degraded
peer can strand everybody else's respawn.  That finally explains the otherwise
unexplained field datum that one machine stopped processing the death-transition
stream 57% into a match and never recovered (0 explosions/wreck swaps/burials
while four other machines logged 4/4/4) -- a node in that state cannot answer
anyone's respawn.  It also explains why solo is 100% reliable (in-process) and
why a healthy 2-node bench passes.

Also fixes the instrumentation before it costs a night: every [dz] line printed
"entity 1" because a player's LOCAL entity id is 1 on every machine -- with five
players the log would have said a respawn stalled but not WHOSE.  All [dz] lines
now print host:local (including the usedBy= owner of each busy slot).

Doc: two candidate fixes recorded (prefer a locally-mastered DropZone / make the
reply path tolerant of a deathCount that is ahead of ours), neither to be guessed
at -- the [ghost] DISCARDED line's mismatch direction decides it in one line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 01:20:13 -05:00
Joe DiPrimaandClaude Fable 5 0dbdad17f3 #81 ghost: light the SILENT DISCARD -- the only path a respawn can vanish on
Walked the respawn handshake end to end and measured the assumptions.  Two
eliminations and one find:

ELIMINATED: the engine hunt re-posts to itself every 2s forever while the mech
is dead (PLAYER.cpp:325-327; its only state-based exit never fires, see below),
and the DropZone re-grants the SAME slot to a repeat request from the same
requester+deathCount even while busy (DROPZONE.cpp:182-194).  So neither a
transient "no slot" nor a single lost reply can strand a respawn -- the request
keeps being re-sent and re-granted.  A permanent strand needs a permanent cause.

THE FIND: BTPlayer::DropZoneReplyMessageHandler ends
  if (!playerVehicle) ... else if (deathCount == message->deathCount) ... else { return; }
and that last branch was a BARE RETURN WITH NO LOG.  The drop zone grants a
spot, replies, the numbers disagree, the reply is dropped, deathPending stays
latched, the mech is never Reset -> permanent ghost, zero evidence.  That is
why 6 of 8 field cycles stranded silently.  Now always-on, printing the
DIRECTION of the mismatch: msgDeath < ours = genuinely stale (dropping is
right); msgDeath > ours = our counter is behind and we just threw away a LIVE
respawn -> points straight at #45 (the death tally does not replicate).
Deliberately NOT auto-recovered: guessing would be a stand-in, and the wrong
guess resets a mech that is still alive.

LANDMINE DOCUMENTED IN CODE: Set_Alarm_Level is an empty stub (btstubs.cpp:87),
so the death path's Set_Alarm_Level(this+0x2c,1) and the reply's (+0x2c,2) are
no-ops.  Their values decode against Player's enum as DropZoneAcquiredState(1)
and VehicleTranslocatedState(2), which makes "these should obviously be
SetSimulationState() calls" both attractive and CATASTROPHIC: the engine hunt is
gated on GetSimulationState() != DropZoneAcquiredState, so setting 1 on death
would stop AssignDropZone ever being dispatched and ghost EVERY pilot.  Measured
our simulationState at 0x24 (not 0x2c) and the write leaves it 0; in the binary
+0x2c is the Simulation-base alarm (the field the mech side calls graphicAlarm,
@0x4ac126 "owner alarm+0x2C -> level 9"), which our layout models only on Mech.

Verified solo: a healthy death+respawn logs the grant and the RESET and emits
ZERO discard/gate-off lines (no false positives).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 01:08:01 -05:00
Joe DiPrimaandClaude Fable 5 372cbfa344 docs: GHOST_MECH_ANALYSIS -- the field analysis, durable across sessions
The ghost and the "wreckage moving and shooting" are the SAME failure at two
ages: a stranded respawn (drop-zone reply never arrives) leaves the mech never
Reset and never repainted, and the render-side wreck swap is ONE-WAY -- so the
pilot drives a burning hulk that sinks (pure ~18s timer, btl4vid.cpp:1277) and
then cannot be drawn at all.

Records: the 8-cycle ledger (only 2 of 8 reached RESET; stranded correlates with
ghosting 8/8 and 10/10); the host did not ghost because he never died; the
SWALLOWED warning is BENIGN (100% base rate) and two earlier readings of mine
that were wrong (spurious resets = aggregation artifact, per-match player IDs);
the WinTesla-vs-1995 provenance caveat on DROPZONE.cpp; what the new
instrumentation already killed (slots=8, so dropzone=one is not the bottleneck);
and the four-separate-bugs breakdown of the "desync cluster" including the
machine that stopped processing the death-transition stream mid-match.

Raw per-machine logs + the verbatim agent findings stay in scratchpad/night6/
(uncommitted -- machine names + Steam identities).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 00:26:48 -05:00