#108 peer-side WRECK receipt: make ghosts countable

The un-wreck receipt had no partner, so counting ghosts in a field log meant
pairing it against

    [BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback

which is a MISSING-ASSET WARNING, not a death -- it only prints for chassis
whose wreck model is absent.  Night 13's census found ONE ghost while testers
reported many, and there was no way to separate a real count from a chassis
accident.

Emit one ungated line for every REPLICANT entering the wreck state, symmetric
with the existing un-wreck line, so a log's ghost count is exactly
(wreck-enters minus un-wrecks) per entity:

    [wreck]   replicant H:E entered wreck state (mode X->9) at (x,z)
    [respawn] replicant H:E un-wrecked + warp   (mode 9->1) at (x,z)

Verified 2-node (200s, force-damage victim): 5 enters, 5 exits, exactly
paired -- while the old marker printed ZERO times in the same run.  That gap
is the point: five real deaths, invisible to what the census was reading.

Also lands the night-13 census tooling (ghostcensus.py) and the eject benches
that did NOT reproduce, with their failure modes in the headers so the next
attempt does not repeat them: five rigs failed to trigger a punch-out at all
(BT_BTNTEST never reached the mapper for 0x3D or 0x14; BT_EJECT_AT did not
fire either).  Panic-eject replication remains UNTESTED by bench.

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 07:48:04 -05:00
co-authored by Claude Opus 5
parent 43d30ca7e4
commit 4642129e76
5 changed files with 358 additions and 0 deletions
+25
View File
@@ -1358,6 +1358,31 @@ void
const bool nowDead = (mode == 2 || mode == 9);
const bool prevDead = (oldMode == 2 || oldMode == 9);
prevMode = mode;
// #108 GHOST CENSUS, the ENTER edge (ungated, 2026-08-07). The
// un-wreck receipt below has had no partner, so counting ghosts in a
// field log meant pairing it against
// "[BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback" --
// which is a MISSING-ASSET WARNING, not a death: it only prints for
// chassis whose wreck model is absent. Night 13's census therefore
// found ONE ghost while testers reported many, and there was no way to
// tell a real count from a chassis accident. This line is emitted for
// EVERY replicant that enters the wreck state, so a log's ghost count
// is exactly (wreck-enters minus un-wrecks) per entity.
if (!prevDead && nowDead
&& owner->GetInstance() == Entity::ReplicantInstance)
{
DEBUG_STREAM << "[wreck] replicant " << owner->GetEntityID()
<< " entered wreck state (mode "
<< oldMode << "->" << mode << ") at ("
<< owner->localOrigin.linearPosition.x << ","
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
if (BTMatchLogActive())
BTMatchLog("WRECK", "mech=%d:%d mode=%d->%d x=%.1f z=%.1f",
BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(),
oldMode, mode,
(float)owner->localOrigin.linearPosition.x,
(float)owner->localOrigin.linearPosition.z);
}
if (prevDead && !nowDead
&& owner->GetInstance() == Entity::ReplicantInstance)
{
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# =========================================================================
# PANIC EJECT -> does the respawn replicate? (#108, night 13)
#
# Fourth attempt at the trigger. The first three drove the panic button
# through BT_BTNTEST (EmitButton -> RIO queue). It never reached the
# mapper: no [eject], no PUNCH-OUT, not even a lamp change -- and the same
# seam failed to toggle the searchlight (0x14), so it was the SEAM, not the
# button address. BT_EJECT_AT is the purpose-built hook (mech4.cpp:3310):
# it synthesizes the identical press inside the input path -- "one message,
# one dispatch", the same Mech::EjectPilotMessageID the key sends -- and
# repeats every 300 frames, so one run yields many punch-outs.
#
# PASS : for every punch-out on A, B logs
# "[respawn] replicant <A> un-wrecked + warp (mode 9->1)"
# FAIL : punch-outs on A with no matching un-wreck <- the ghost
#
# Control already banked (ejectghost.sh): 9 force-kill deaths -> 8 peer
# un-wrecks, 0 ghost lines. NORMAL death replication is good on this build,
# so an eject-only shortfall here is the defect, not a broken rig.
# =========================================================================
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
rm -f ea_a.log ea_b.log ea_relay.log
bt_expert_egg MP.EGG EA.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" EA.EGG
( export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_FOG_LOG=1
bt_launch ea_b.log EA.EGG 0x0C -net 1601 )
sleep 2
( export BT_AUTODRIVE=0.6 BT_EJECT_AT=1800
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
bt_launch ea_a.log EA.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py EA.EGG 127.0.0.1:1501 127.0.0.1:1601 > ea_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]+" ea_a.log | head -1 | cut -d= -f2)
echo "=================== PANIC-EJECT REPLICATION ==================="
echo "A entity: ${AID:-UNKNOWN}"
echo
echo "--- 1. did punch-outs actually happen? (if 0 the run is VOID) ---"
echo -n "DeathWithoutHonor notices : "; grep -ac "DeathWithoutHonor" ea_a.log
echo -n "PUNCH-OUT lines : "; grep -ac "PUNCH-OUT" ea_a.log
grep -a "PUNCH-OUT" ea_a.log | head -6
echo
echo "--- 2. A's own death/respawn cycles ---"
echo -n "death cycle STARTs : "; grep -ac "death cycle START" ea_a.log
echo -n "Mech::Reset : "; grep -ac "Mech::Reset ${AID:-@@@}" ea_a.log
echo
echo "--- 3. B: the peer's un-wrecks for A (THE NUMBER THAT MATTERS) ---"
echo -n "un-wrecks seen : "; grep -a "respawn\] replicant" ea_b.log | grep -ac "${AID:-@@@}"
grep -a "respawn\] replicant" ea_b.log | grep -a "${AID:-@@@}" | head -8
echo
echo "--- 4. B: ghost detector + searchlight cones ---"
echo -n "ghost lines : "; grep -ac "\[ghost\]" ea_b.log
grep -a "\[ghost\]" ea_b.log | head -4
echo -n "cone SHOWN/HIDDEN : "; grep -ac "\[spot\] cone" ea_b.log
echo
echo "--- 5. VERDICT INPUTS ---"
echo "punch-outs=$(grep -ac 'PUNCH-OUT' ea_a.log) A-respawns=$(grep -ac 'death cycle START' ea_a.log) B-unwrecks=$(grep -a 'respawn\] replicant' ea_b.log | grep -ac "${AID:-@@@}")"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# =========================================================================
# EJECT-GHOST bench (#108 / night 13) -- does a PANIC EJECT replicate its
# respawn to peers?
#
# FIELD REPORT: many ghost mechs tonight, all from panic-button self
# destructs, none respawning properly -- and the operator has seen
# eject-respawn work every time before tonight, so this is a REGRESSION
# (night 12 = build 774, tonight = 817).
#
# FIELD EVIDENCE (staged logs): the ejecting mech's OWNER ran the whole
# death path and respawned and kept driving in the same round, while BOTH
# peers created his wreck and never processed the un-wreck. The same mech's
# NORMAL deaths replicated their respawns 4-6 times in that same session.
#
# THE TEST: A punches out via the real click seam (button 0x3D, the panic
# button -- the same addr the field log shows). B watches.
# PASS : B logs "[respawn] replicant <A> un-wrecked + warp (mode 9->1)"
# FAIL : B logs the wreck and never the un-wreck <- the ghost
# A CONTROL kill comes first (B force-damages A), so the SAME run shows a
# normal death replicating correctly -- otherwise a silent B proves nothing.
#
# Press polls: >=900 per the harness contract (round-start jitter eats
# earlier presses). Panic twice, so one missed press does not read as a
# pass.
# =========================================================================
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
rm -f eg_a.log eg_b.log eg_relay.log
bt_assert_player_env
bt_expert_egg MP.EGG EG.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" EG.EGG
# ---- node B: OBSERVER + the control killer -------------------------------
( export BT_MP_FORCE_DMG=1
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
bt_launch eg_b.log EG.EGG 0x0C -net 1601 )
sleep 2
# ---- node A: walks, gets killed (control), then PUNCHES OUT twice --------
( export BT_AUTODRIVE=0.6
export BT_BTNTEST=0x3d,900,960 # panic eject #1
export BT_BTNTEST2=0x3d,2400,2460 # panic eject #2
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
bt_launch eg_a.log EG.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py EG.EGG 127.0.0.1:1501 127.0.0.1:1601 > eg_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]+" eg_a.log | head -1 | cut -d= -f2)
echo "=================== EJECT-GHOST RESULT ==================="
echo "A's entity id: ${AID:-UNKNOWN}"
echo
echo "--- did the scripted panic actually press? ---"
grep -a "btntest" eg_a.log
echo
echo "--- A: punch-outs + its own death/respawn cycle ---"
grep -aE "\[eject\]|PUNCH-OUT|death cycle START|dz\] GRANTED|Mech::Reset" eg_a.log | head -20
echo
echo "--- B: the peer's view of A, in order (wreck / un-wreck) ---"
grep -aE "wreck:|respawn\] replicant" eg_b.log | grep -a "${AID:-@@@}" | head -20
echo
echo "--- B: totals for A ---"
echo -n "wrecks seen : "; grep -a "wreck:" eg_b.log | grep -ac "${AID:-@@@}"
echo -n "respawns seen : "; grep -a "respawn\] replicant" eg_b.log | grep -ac "${AID:-@@@}"
echo
echo "--- B: ghost detector ---"
grep -a "\[ghost\]" eg_b.log | head -5
echo -n "ghost lines: "; grep -ac "\[ghost\]" eg_b.log
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# =========================================================================
# EJECT + SEARCHLIGHT (#108, night 13) -- the operator's observation was
# that the ghost mechs all had their SEARCHLIGHTS ON. A plain panic eject
# respawns fine (ejectghost.sh), so the searchlight is the variable.
#
# WHY IT IS PLAUSIBLE, mechanically: the searchlight is the only thing that
# attaches EXTRA GEOMETRY into a peer's mech render tree -- btl4vid.cpp
# builds a spot.bgf cone as a DPLStaticChildRenderable parented to the
# lamp's mount-segment renderable. The death path then does a wreck swap
# ('victim -> thrdbr.bgf + debris') and respawn does 'rebuilt intact model
# (N segs restored, hulk dropped)'. A child renderable held across those
# two rebuilds is a lifetime hazard, and it would only bite mechs whose
# lamp was ON -- which is exactly the reported population.
#
# SEQUENCE on A: searchlight ON (0x14) -> confirm it lit -> panic eject
# (0x3D) -> respawn. B watches for the un-wreck.
# PASS : B logs "[respawn] replicant <A> un-wrecked + warp"
# FAIL : wreck with no un-wreck <- the ghost, and the searchlight is it
#
# NB 0x14 is the searchlight per the shipped bindings comment (left column
# 0x10-0x15 = map+/map-/IR/CROUCH/searchlight/display). The hardware name
# table calls it "Secondary5" -- the FUNCTION comes from the streamed .CTL
# rows, so the script VERIFIES the lamp actually lit before trusting the
# result; a silent B with a lamp that never came on proves nothing.
# =========================================================================
#
# MODE=light -- searchlight ON, then punch out
# MODE=nolight -- punch out with the lamp OFF (the A/B control)
#
# NB no BT_MP_FORCE_DMG here. ejectghost.sh had it, and that is why its
# panic presses did nothing: the "control killer" had A dead or mid-respawn
# for most of the run, and a punch-out press on a dead mech is a no-op.
# That run still produced its control result -- 8 force-kill deaths, 8 peer
# un-wrecks, so NORMAL death replication is proven good on this build.
set -x
MODE="${1:-light}"
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
rm -f el_${MODE}_a.log el_${MODE}_b.log el_${MODE}_relay.log
bt_expert_egg MP.EGG EL.EGG
# NIGHT map: the searchlight is a night system and the fog swap only has
# meaning there; also the field population was night-map rounds.
sed -i "s/^map=.*/map=cavern/; s/^time=.*/time=night/; s/^vehicle=.*/vehicle=madcat/" EL.EGG
( export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SPLASH_LOG=1
bt_launch el_${MODE}_b.log EL.EGG 0x0C -net 1601 )
sleep 2
( export BT_AUTODRIVE=0.6
if [ "$MODE" = "light" ]; then
export BT_BTNTEST=0x14,600,660 # SEARCHLIGHT on
export BT_BTNTEST2=0x3d,1500,1560 # then punch out
else
export BT_BTNTEST=0x3d,1500,1560 # punch out, lamp OFF
fi
export BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1
bt_launch el_${MODE}_a.log EL.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py EL.EGG 127.0.0.1:1501 127.0.0.1:1601 > el_${MODE}_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]+" el_${MODE}_a.log | head -1 | cut -d= -f2)
echo "=================== EJECT+SEARCHLIGHT RESULT ==================="
echo "A's entity id: ${AID:-UNKNOWN}"
echo
echo "--- 1. did the scripted presses land? ---"
grep -a "btntest" el_${MODE}_a.log
echo
echo "--- 2. DID THE SEARCHLIGHT ACTUALLY COME ON? (if not, the run is void) ---"
grep -aiE "searchlight|\[spot\]|lightState" el_${MODE}_a.log | head -10
echo
echo "--- 3. A: punch-out + own respawn ---"
grep -aE "\[eject\]|PUNCH-OUT|dz\] GRANTED" el_${MODE}_a.log | head -10
echo
echo "--- 4. B: the peer's view of A (wreck / un-wreck, in order) ---"
grep -aE "wreck:|respawn\] replicant" el_${MODE}_b.log | grep -a "${AID:-@@@}" | head -20
echo -n "wrecks: "; grep -a "wreck:" el_${MODE}_b.log | grep -ac "${AID:-@@@}"
echo -n "respawns: "; grep -a "respawn\] replicant" el_${MODE}_b.log | grep -ac "${AID:-@@@}"
echo
echo "--- 5. B: searchlight cones + ghost detector ---"
grep -aE "\[spot\]" el_${MODE}_b.log | head -6
echo -n "cone skipped: "; grep -ac "cone skipped" el_${MODE}_b.log
echo -n "ghost lines : "; grep -ac "\[ghost\]" el_${MODE}_b.log
grep -a "\[ghost\]" el_${MODE}_b.log | head -3
+85
View File
@@ -0,0 +1,85 @@
"""Night 13: census every 'wreck with no following un-wreck' across all logs.
A peer's view of a mech death is a pair:
[BTrender] wreck: ... (entity=H:E) <- the wreck appears
[respawn] replicant H:E un-wrecked + warp <- it comes back
An UNPAIRED wreck -- one with no un-wreck before that log's session ends --
is the ghost signature. Report per SESSION so 'was it just one game?' is
answerable, and dump the lines immediately before the first one.
"""
import re, sys, os, io
LOGDIR = r"C:\git\bt411\scratchpad\night13"
SESS = re.compile(r"===== BT411 SESSION.*?local=(\S+ \S+)")
WRECK = re.compile(r"\[BTrender\] wreck:.*?entity=(\d+:\d+)")
UNWRECK = re.compile(r"\[respawn\] replicant (\d+:\d+) un-wrecked")
EJECT = re.compile(r"\[eject\]|PUNCH-OUT")
def scan(path):
sessions = [] # (startline, stamp)
events = [] # (line, kind, ent)
with io.open(path, "r", encoding="latin-1", errors="replace") as f:
for n, line in enumerate(f, 1):
m = SESS.search(line)
if m:
sessions.append((n, m.group(1)))
continue
m = WRECK.search(line)
if m:
events.append((n, "wreck", m.group(1))); continue
m = UNWRECK.search(line)
if m:
events.append((n, "unwreck", m.group(1))); continue
if EJECT.search(line):
events.append((n, "eject", "-"))
return sessions, events
def session_of(sessions, line):
idx, stamp = 0, "?"
for i, (sl, st) in enumerate(sessions):
if sl <= line:
idx, stamp = i + 1, st
else:
break
return idx, stamp
for fn in sorted(os.listdir(LOGDIR)):
if not fn.endswith(".log") or fn.startswith("FAILURE"):
continue
path = os.path.join(LOGDIR, fn)
sessions, events = scan(path)
# pair wrecks to the next un-wreck of the same entity IN THE SAME SESSION
pending = {} # ent -> (line, sessidx)
unpaired = []
for (n, kind, ent) in events:
si = session_of(sessions, n)[0]
if kind == "wreck":
if ent in pending and pending[ent][1] == si:
unpaired.append(pending[ent]) # wreck superseded by another wreck
pending[ent] = (n, si)
elif kind == "unwreck":
if ent in pending and pending[ent][1] == si:
del pending[ent]
for ent, (n, si) in pending.items():
unpaired.append((n, si, ent))
norm = []
for u in unpaired:
norm.append(u if len(u) == 3 else (u[0], u[1], "?"))
norm.sort()
print("=" * 72)
print("%s sessions=%d wrecks=%d unwrecks=%d ejects=%d"
% (fn, len(sessions),
sum(1 for e in events if e[1] == "wreck"),
sum(1 for e in events if e[1] == "unwreck"),
sum(1 for e in events if e[1] == "eject")))
if not norm:
print(" no unpaired wrecks")
continue
bysess = {}
for (n, si, ent) in norm:
bysess.setdefault(si, []).append((n, ent))
for si in sorted(bysess):
stamp = sessions[si - 1][1] if 0 < si <= len(sessions) else "?"
print(" SESSION %d (%s): %d unpaired -> %s"
% (si, stamp, len(bysess[si]),
", ".join("%s@%d" % (e, n) for n, e in bysess[si][:6])))