#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:
co-authored by
Claude Opus 5
parent
29b4d68ba6
commit
61f21107b4
@@ -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;
|
||||
|
||||
@@ -3311,9 +3311,23 @@ void
|
||||
sEjAt = (e && *e) ? atoi(e) : -1;
|
||||
}
|
||||
++sEjFrame;
|
||||
// Receipt: five separate rigs failed to fire a punch-out
|
||||
// and it was never established whether this hook is even
|
||||
// REACHED. Announce once a second while armed.
|
||||
if (sEjAt > 0)
|
||||
{
|
||||
static int sEjLog = 0;
|
||||
if ((++sEjLog % 60) == 0)
|
||||
DEBUG_STREAM << "[ejecttest] armed at " << sEjAt
|
||||
<< ", frame " << sEjFrame << "\n" << std::flush;
|
||||
}
|
||||
if (sEjAt > 0 && sEjFrame >= sEjAt
|
||||
&& ((sEjFrame - sEjAt) % 300) == 0)
|
||||
{
|
||||
ejectPress = 1;
|
||||
DEBUG_STREAM << "[ejecttest] FIRING punch-out at frame "
|
||||
<< sEjFrame << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (ejectPress)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# =========================================================================
|
||||
# A REAL PANIC EJECT -- at last. (#108 ghost + the chart's "-1000 ejecting")
|
||||
#
|
||||
# WHY SIX RIGS FAILED. It was never the trigger. BT_EJECT_AT reaches the
|
||||
# dispatch every time (proved with an [ejecttest] receipt: "FIRING punch-out
|
||||
# at frame 600/900/1200"), and the HANDLER refuses it:
|
||||
#
|
||||
# [eject] 1:139 REFUSED (mech not crippled enough)
|
||||
#
|
||||
# Mech::EjectPilotMessageHandler gates on EvaluateEjectPermission() (@0x414) --
|
||||
# a healthy mech cannot punch out. Every bench so far ejected a pristine mech.
|
||||
# In the field players eject BECAUSE they are wrecked, which is why it works
|
||||
# for them and never for me. (The button seam was never broken either; that
|
||||
# earlier conclusion was wrong too.)
|
||||
#
|
||||
# So: CRIPPLE FIRST, then eject. BT_SELF_DAMAGE grinds A down; BT_EJECT_AT
|
||||
# retries every 300 frames, so the first retry after permission is granted
|
||||
# takes it.
|
||||
#
|
||||
# WHAT THIS SETTLES:
|
||||
# chart "-1000 ejecting" -- the eject total, currently arithmetic only
|
||||
# #108 eject-ghost -- does the peer un-wreck after an EJECT death?
|
||||
# (normal deaths replicate fine: 9 deaths -> 8
|
||||
# un-wrecks, benched)
|
||||
# =========================================================================
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
bt_assert_player_env
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
rm -f er_a.log er_b.log er_relay.log matchlog_*.txt
|
||||
bt_expert_egg MP.EGG ER.EGG
|
||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" ER.EGG
|
||||
|
||||
( export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
|
||||
bt_launch er_b.log ER.EGG 0x0C -net 1601 )
|
||||
sleep 2
|
||||
# A: grind itself down, then punch out once permission is granted
|
||||
# THE GATE, decoded from Mech::EvaluateEjectPermission (@0049fa1c):
|
||||
# permitted = liveWeapons < ejectMinWeapons || liveGenerators == 0
|
||||
# || coolantFrac < 0.05 || (leg-gimped && !simLive)
|
||||
# Armour damage satisfies NONE of them -- which is why grinding A down gave
|
||||
# 53 attempts and 785 refusals. Killing the GENERATORS is the direct lever,
|
||||
# and BT_KILL_SUBSYS's comma-list form was built for precisely this bench.
|
||||
# It fires at frame 900, so arm the eject after that.
|
||||
( export BT_KILL_SUBSYS=GeneratorA,GeneratorB,GeneratorC,GeneratorD BT_EJECT_AT=1200
|
||||
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_DMG_LOG=1
|
||||
bt_launch er_a.log ER.EGG 0x03 -net 1501 )
|
||||
sleep 5
|
||||
python ../tools/btconsole.py ER.EGG 127.0.0.1:1501 127.0.0.1:1601 > er_relay.log 2>&1 &
|
||||
RELAY=$!
|
||||
sleep 300
|
||||
kill $RELAY 2>/dev/null
|
||||
sleep 3
|
||||
bt_kill_ours
|
||||
sleep 2
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 3
|
||||
|
||||
AID=$(grep -aoE "MY mech entityID=[0-9]+:[0-9]+" er_a.log | head -1 | cut -d= -f2)
|
||||
echo "=================== REAL EJECT ==================="
|
||||
echo "A entity: ${AID:-UNKNOWN}"
|
||||
echo "--- 1. did a punch-out finally LAND? ---"
|
||||
echo -n "FIRING attempts : "; grep -ac "FIRING punch-out" er_a.log
|
||||
echo -n "REFUSED : "; grep -ac "REFUSED" er_a.log
|
||||
echo -n "PUNCH-OUT : "; grep -ac "PUNCH-OUT" er_a.log
|
||||
grep -aE "PUNCH-OUT|DeathWithoutHonor" er_a.log | head -3
|
||||
echo
|
||||
echo "--- 2. CHART '-1000 ejecting': the score trajectory around it ---"
|
||||
grep -ah "player=2:1 type=" matchlog_*.txt | tail -6 | cut -c1-115
|
||||
grep -a "\[deathcost\]" er_a.log | head -2 | cut -c1-140
|
||||
echo
|
||||
echo "--- 3. #108: does the peer UN-WRECK after an eject death? ---"
|
||||
echo -n "B wreck-enters for A : "; grep -a "entered wreck state" er_b.log | grep -ac "${AID:-@@@}"
|
||||
echo -n "B un-wrecks for A : "; grep -a "un-wrecked + warp" er_b.log | grep -ac "${AID:-@@@}"
|
||||
grep -aE "entered wreck state|un-wrecked \+ warp" er_b.log | tail -6 | cut -c1-120
|
||||
echo -n "B ghost lines : "; grep -ac "\[ghost\]" er_b.log
|
||||
Reference in New Issue
Block a user