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
86 lines
3.3 KiB
Python
86 lines
3.3 KiB
Python
"""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])))
|