Conn Man crashed twice in an Owens on 2026-07-27, kept playing, and by the time
we asked for his logs they no longer existed. Not a mystery: every launcher did
if exist content\X.old.log del content\X.old.log
if exist content\X.log ren content\X.log X.old.log
which keeps TWO generations. He relaunched more than twice, so his own bat
deleted both crash sessions. The player who crashes is exactly the player who
relaunches immediately, so the retention window was aimed at the wrong case.
THE LOG. BT_LOG set still means "use this path verbatim" -- that contract is
load-bearing (mp_a.log/mp_b.log for 2-node runs from one cwd, btoperator's
operator_N.log, every scratchpad/mp_*.sh that greps the file it named) and is
untouched. Only when BT_LOG is UNSET does the exe now name the file itself:
content\<stem>_YYYYMMDD.log, appended, one per day, no rotation window to fall
out of. The stem (solo/join/steam/joyconfig) comes from the gates the bat
already sets, so solo still cannot overwrite the MULTIPLAYER log.
Self-named files append UNCONDITIONALLY. The default open mode is ios::out --
TRUNCATE -- and the operator GUI's exported bats never set BT_LOG_APPEND, so
they were truncating already; leaving append implicit would have let the second
launch of the day erase the morning.
IDENTITY, because these arrive from many players at once and Discord renames
half of them to message.txt (most of the night-5 evidence had to be
re-identified by hand):
===== BT411 SESSION build=4.11.603 (d64d75f+) machine=... user=...
callsign=Conn Man mode=solo pid=13192 local=... log=... =====
Also the session separator inside a per-day file, and it puts build+hash ABOVE
any [crash] block so btl4+0xNNNN offsets stay matchable to a PDB across builds.
SIZE. Never delete, but stay sendable: past 8 MB the next launch rolls to
<stem>_YYYYMMDD.1.log. Checked only at open, so a live session is never split.
CONSOLIDATION. launch_report.txt is gone, folded into lastrun_<stem>.txt (one
launch record: the exe appends its block at first breath, the bat appends the
exit line). Per-stem because one shared lastrun.txt let a second launcher's
`del` destroy the first launch's block. Its ABSENCE after a run is now the
#41 "never reached WinMain" probe -- the old "if not exist X.log" test cannot
work against a per-day file, which survives earlier launches, and would have
reported every healthy run as blocked by antivirus. marshal.log folded into
the day log too; it keeps its own handle and gained a CRITICAL_SECTION because
it runs on a worker thread while the engine log stream is single-threaded.
play_steam.bat gained a launch bracket it never had -- which is why a Steam
player killed before WinMain previously left no evidence at all.
_putenv_s, not SetEnvironmentVariableA, to pin the resolved name: MSVC's CRT
keeps its own environment copy, so getenv() in-process does NOT see a
SetEnvironmentVariableA write (measured). btl4console/btl4lobby resolve the
day log via getenv, so with the Win32-only call they silently fell back to
marshal.log and the fold never happened.
logfile.open() is now checked -- a failed open used to rebind cout to a dead
buffer, silently dropping the header, [boot] and any crash stack while
lastrun still claimed log=<name>.
Verified: append across sessions with distinct pids, crash block landing under
its own header, BT_LOG verbatim unmangled, an inherited BT_LOG cleared by the
bats, roll-over at 8 MB, and both forensic verdicts (exe ran / exe blocked).
Console auto-retrieval is unaffected -- it uploads the matchlog, a different
file, and a crashed round never reaches the upload call anyway.
Known gaps, deliberately not in this commit: the setlocal hoist for gate
leakage when two bats share one console (dev-only), and btoperator's exported
bats still lack the lastrun bracket -- do not press Export on a shipped zip.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prompted by "does the steam path still generate our logs?". Answer: yes --
play_steam.bat sets BT_LOG=steam.log with append, and matchlogs arm because both
Steam launch paths pass -net (the default trigger). But the audit found two
evidence-destroying bugs on the way:
1. play_steam.bat DELETED the previous steam.log at every launch -- the same
pattern fixed in join.bat/join_lan.bat last night. A Steam player who
crashes and relaunches loses the stack. Now rotates to steam.old.log.
2. WORSE: play_solo.bat and joyconfig.bat deleted content\join.log -- the
MULTIPLAYER log, not theirs. A player who crashed in MP and then ran solo
to investigate (or ran the joystick wizard) DESTROYED THEIR OWN CRASH
EVIDENCE. That is a very plausible part of how Conn Man's owens-laser stack
vanished (#35) -- he had every reason to poke around after crashing. Each
bat now writes its OWN log (solo.log / joyconfig.log), rotates it, and never
touches join.log. Their launch-forensics + "send the operator" lines are
repointed to match.
VERIFIED in the zip layout (the bats need build\ + content\ beside them, so a
repo-relative test is meaningless -- my first two attempts hit the badpath
guard and proved nothing):
solo.log = [boot] btl4 4.11.599 ... (the NEW run)
solo.old.log = OLD-SOLO-CONTENT (rotated, not deleted)
join.log = SEEDED-MP-EVIDENCE (untouched by the solo run)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
David's failures leave a 0-byte (or absent) join.log -- the exe can't
testify about a death before WinMain. The bats now bracket it from
OUTSIDE: launch_report.txt records the launch timestamp, the exe's
EXIT CODE (fingerprints the failure class: 0xC0000135 DLL-not-found,
0xC0000005 AV, AV-kill signatures, our clean exits), whether join.log
was ever created (never-created = blocked before our first
instruction), and its size. Verified with a synthetic -1073741819
exit. The 'exited' message now asks players to send BOTH files.
With this + the 507 first-breath line + the crash self-report, every
failure mode now leaves evidence somewhere: bat report (pre-WinMain),
first-breath (WinMain reached), [crash] stack (anything after).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field 2026-07-23 (second night): a player's join.bat failed 3x in a row
with a 0-byte join.log -- indistinguishable from 'never ran'. Two
causes of evidence loss fixed:
1. The first flushed log line now happens the INSTANT the log opens
(version, pid, args, first-process vs relaunched-generation). An
empty log after this build means the exe was killed before WinMain
-- an external cause (antivirus/loader), not ours.
2. The menu->game relaunch chain truncated join.log each generation
(BT_LOG_APPEND was unset in the player bats): the crashed
generation's evidence was erased by the next attempt. The bats now
delete the log once per bat run and append across generations.
Also explains the confusing 'The game has exited' bat message on
SUCCESSFUL launches (the menu process exits after relaunching the game
detached) -- the log now records the whole chain.
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8):
enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling
with range normalization, reacquire-on-loss, 3s hot-plug re-enum.
XInput devices are excluded via the documented RawInput IG_ VID/PID
check (live-verified skipping a real XBOX360 pad -- no double-feed).
bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots
by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/
deadzone; hats as 4-direction buttons with diagonal chords). PadRIO
runs them through the existing channel/event machinery -- per-binding
edge arrays release automatically on detach (the #24 rule), and a
direct throttle axis maps full lever travel onto the 0..1 channel
while the gait detent still snaps a lever parked in the walk/run dead
band.
BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move
each control when prompted (stick/twist/throttle/fire), invert derived
from the move direction, hat look cluster auto-added, output written
between markers in bindings.txt with the rest of the file preserved.
Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched.
Verified: grammar accept/reject per line, XInput-exclusion live,
30s in-mission no-device polling clean. Awaiting a tester with real
stick hardware for axis verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>