#108 THE EJECT GHOST: the death-edge latch tested the wrong field

One substitution, three field symptoms.  Mech::TakeDamageMessageHandler arms
the whole death tail -- kill report, VehicleDead, death blast -- from a
was-alive-at-entry latch:

    const int deathBlastArmed = !IsMechDestroyed();   // graphicAlarm >= 9

The binary tests movementMode 9|10 there (@0x4a0303).  The port swapped in the
graphic alarm and justified it: "the death transition sets mode 9 synchronously
with the structural flag on every path through here, so the edges coincide".
True of every DAMAGE path.  False of the one that matters:
Mech::EjectPilotMessageHandler raises graphicAlarm to 10 (the EJECT state)
BEFORE dispatching its self-damage, while movementMode is still 1.  So on an
eject the handler entered already reading "destroyed", the latch never armed,
and the death tail was skipped entirely -- including VehicleDead, which IS the
respawn trigger.

Everything the field reported on night 13 follows from that:
  * "they all self destructed with panic button and didn't respawn properly"
    -- no VehicleDead, so no drop-zone hunt, so no respawn;
  * the EJECT GHOST -- the peer wrecks the mech and never un-wrecks it, because
    the un-wreck rides the master's respawn.  Normal deaths replicated fine all
    along (9 deaths -> 8 un-wrecks, benched), which is why only ejects ghosted;
  * the manual chart's "-1000 ejecting" never materialised -- the negated kill
    award and the death cost both live in the tail that never ran.

Fix: use the binary's own predicate.  MovementMode is untouched by the eject's
alarm write, so the latch arms on an eject exactly as on a combat death.

WHY SEVEN RIGS MISSED IT: the punch-out was being REFUSED, not undelivered.
EvaluateEjectPermission (@0049fa1c) grants only on
  liveWeapons < ejectMinWeapons || liveGenerators == 0 || coolantFrac < 0.05
  || (leg-gimped && !simLive)
-- armour damage satisfies none of them, and every bench ejected a healthy
mech.  An [ejecttest] receipt (2 lines) proved the dispatch fired every time
and the handler declined; the "[eject] REFUSED (mech not crippled enough)" line
was sitting in the very first bench log, ungrepped.  BT_KILL_SUBSYS's
comma-list form ("GeneratorA,GeneratorB,...") was already built for this bench.

Verified 2-node (scratchpad/night13/ejectreal.sh), before -> after:
  PUNCH-OUT landed        0 (785 refusals)  ->  1, charge=500
  peer wreck-enters       1                 ->  1
  peer UN-WRECKS          0  (the ghost)    ->  1
  eject score (type=2)    absent            ->  award=-1000.00, score 1000 -> 0
  death cost              never ran         ->  APPLYING, penalty=500

That -1000 is the manual chart's eject row to the digit: killBonus 500 plus the
500 self-damage tally, negated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
This commit is contained in:
Joe DiPrima
2026-08-07 15:38:50 -05:00
co-authored by Claude Opus 5
parent 29b4d68ba6
commit 61f21107b4
3 changed files with 118 additions and 1 deletions
+25 -1
View File
@@ -1070,7 +1070,31 @@ void
// Capturing it after the divert (where #89 first placed it) meant a
// COLLISION death could never arm the tail.
//
const int deathBlastArmed = !IsMechDestroyed(); // [ebp-0x10], inverted
// WAS-ALIVE-AT-ENTRY, on the BINARY's predicate (movementMode), not the
// graphic alarm. FIXED 2026-08-07 -- this substitution was the eject-ghost.
//
// The binary tests movementMode 9|10 here; the port used IsMechDestroyed()
// (graphicAlarm >= 9) and justified it with "the death transition sets mode
// 9 synchronously with the structural flag on every path through here, so
// the edges coincide". That is true of every DAMAGE path and false of the
// one that matters: Mech::EjectPilotMessageHandler raises graphicAlarm to
// 10 (the EJECT state) BEFORE dispatching its self-damage, while
// movementMode is still 1. So on an eject the handler entered already
// reading "destroyed", the latch never armed, and the whole death tail was
// skipped -- no VehicleDead, which IS the respawn trigger.
//
// Consequences, all three reported from the field on the same night:
// * the ejecting player never respawns ("panic button, didn't respawn");
// * the peer wrecks the mech and never un-wrecks it, because the un-wreck
// rides the master's respawn -> the permanent EJECT GHOST (#108);
// * the eject scores only its -500 self-damage: no negated kill award and
// no -500 death cost, because both live in the tail that never ran --
// which is why the manual chart's "-1000 ejecting" never materialised.
//
// MovementMode 9|10 is untouched by the eject's alarm write, so the latch
// now arms on the eject exactly as it does on a combat death.
const int deathBlastArmed =
!(MovementMode() == 9 || MovementMode() == 10); // [ebp-0x10], inverted (@0x4a0303)
//
// The zone the reports + VehicleDead carry: msg+0x24 as of loop entry.
// The binary never rewrites msg+0x24 after the initial cylinder resolve;