#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:
co-authored by
Claude Opus 5
parent
43d30ca7e4
commit
4642129e76
@@ -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:-@@@}")"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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])))
|
||||
Reference in New Issue
Block a user