A midnight boundary splits one evening across two files at exactly the moment
the session is most worth reading whole: the 01:30 crash lands in a different
file from the 23:00 run that set it up. And a 02:00 session is "last night" to
everyone who was in it, so filing it under the new calendar date reads wrong
even when nothing goes bad.
So shift the clock back 6h before taking the date for the FILENAME. The log day
runs 06:00 -> 06:00 and one night stays in one file.
Only the filename moves. The session header and the lastrun breadcrumb each
call GetLocalTime separately (three distinct SYSTEMTIMEs in this function), so
every timestamp a human reads is still true local time -- checked the scoping
rather than assuming it, since sharing one variable would have silently
backdated the header by six hours.
FileTimeToSystemTime carries the month and year boundaries, verified against a
scratch harness rather than reasoned about:
2026-07-28 20:00 -> solo_20260728.log one playtest night,
2026-07-28 23:59 -> solo_20260728.log start to finish,
2026-07-29 00:01 -> solo_20260728.log in a single file
2026-07-29 05:59 -> solo_20260728.log
2026-07-29 06:00 -> solo_20260729.log the boundary
2026-08-01 01:00 -> solo_20260731.log month
2027-01-01 03:00 -> solo_20261231.log year
2026-03-01 02:00 -> solo_20260228.log non-leap February
Nothing in the tooling parses these filenames -- the operator app's own log is
operator_relay.log and _collect_egg is mission settings, not logs -- so the
console auto-transfer is unaffected.
The player-facing text did need fixing though: the bats and README told players
to send the log "dated today", which is now wrong for anyone who plays past
midnight. They now say to take the NEWEST one and ignore the date, with a note
on why. That was already the safer instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Operator report: "X-closing the game leaves the terminal open and leaves
orphaned processes." Investigated on the rig against 4.11.600.
The terminal half is real and is THIS: :btwait polled
`tasklist /FI "IMAGENAME eq btl4.exe"`, which is machine-wide, so a bat that
launched nothing at all keeps spinning while an unrelated instance lives --
proved with btwait_probe.ps1. A second client, the operator's own pod, or an
orphan from a crash therefore hangs every join window, which reads as "the game
never exited" and invites people to start killing processes. All four
launchers carried the identical block.
Fix: snapshot the btl4 PIDs alive BEFORE the launch; wait only on PIDs absent
from that snapshot. The `if /I "%%P"=="btl4.exe"` guard is deliberately kept --
tokens=2 alone parses tasklist's "INFO: No tasks are running" line as a PID and
spins forever with nothing running, which would be worse than the bug.
Verified with the text lifted verbatim from the shipped play_solo.bat: nothing
running -> signs off (the regression guard); someone else's instance -> signs
off; our own generation -> keeps waiting; decoy gone -> signs off. The patched
bat still launches (pid + launch_report.txt). NOT verified: the full handoff
E2E, because the bat blocks on the FE menu waiting for a human.
The orphan half did NOT reproduce on 600: closing the MAIN window exits cleanly
in ~1s during solo model-load, in the relay join wait, and after a real console
launch, with the relay logging the seat freed. The orphans the playtesters saw
match 584 and earlier, where every close relaunched. Full write-up, including
the aux windows that hide instead of closing and the WM_QUIT that BTLoadPump
swallows, in phases/phase-12-orphan-processes.md.
Co-Authored-By: Claude Opus 5 <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>
Regression in my own fix from an hour ago, reported immediately by the
operator on relaunch:
find: '/I': No such file or directory
find: 'btl4.exe': No such file or directory
The game has exited. ...
The wait loop called bare `find`, which resolves to the MSYS/Git-Bash **Unix**
find on any box with git on PATH (confirmed here: `which find` ->
/usr/bin/find, `which timeout` -> /usr/bin/timeout). So the loop errored out
and fell straight through -- restoring exactly the misleading "The game has
exited" message it was meant to remove. `timeout` was the same exposure one
line later (GNU coreutils ships one, with different arguments), so the block
had TWO PATH-dependent failures.
Harmless to the game (the loop only reports; it never touched the session),
but it made the fix a no-op for anyone with git installed -- which is every
developer and a fair number of players.
FIX: no external tools at all.
* detection is now a pure-cmd `for /f "tokens=1"` over tasklist output,
comparing the first token to btl4.exe -- no find/findstr;
* tasklist and timeout are both invoked by ABSOLUTE %SystemRoot%\System32
path, so a shadowing PATH cannot reach them.
Failure modes stay safe: if tasklist is unavailable the variable never gets
set and the loop degrades to reporting immediately (the old behaviour, never a
hang), and a genuinely dead exe still falls straight through so the #41 launch
forensics are untouched.
VERIFIED both branches by running the shipped detection lines from THIS bash
shell -- i.e. the hostile shadowed PATH that broke v1, a harder case than a
playtester's clean environment:
game running -> RESULT: WOULD-WAIT [process detected]
absent-name control -> RESULT: WOULD-EXIT [no process]
No find/timeout errors in either.
(players/ is the shipped source per tools/mkdist.py:78; the gitignored root
copies re-synced byte-identical.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator report while trying the build: the window said the game had exited
"with an error", yet the game loaded and played fine.
There was no error. The launchers were lying. The front end does NOT stay
resident: btl4console.cpp CreateProcessW()es the mission generation, closes
both handles and ExitProcess(0)s itself. So the bat's child exiting means
"the menu handed off", not "the game closed". All four launchers then
immediately printed
[.. ] btl4.exe exited with code 0
The game has exited. If it closed unexpectedly, send the operator ...
while the game was still loading in the handed-off process. Verified live on
the operator's own session: first process pid 33352 = MENU profile, handed off
to pid 12564 = GLASS profile, which was the one actually running; exit code 0
throughout. Harmless in itself, but it reads as a crash to a playtester and
would have generated false bug reports on a test night.
FIX: after the front-end child exits, poll until no btl4.exe remains, THEN
report. Applied to play_solo / join / join_lan / play_steam.
The #41 launch forensics are preserved: an exe that dies before it can run
leaves no process, so the wait falls straight through and launch_report.txt
still records timing, exit code and whether the log was ever created.
Verified the wait condition both ways against the live session (read-only, no
second instance spawned): btl4.exe present -> WOULD-WAIT; absent -> WOULD-EXIT.
NOTE for future edits: players/ is the SOURCE OF TRUTH (mkdist packages from
there, tools/mkdist.py:78); the root-level copies are gitignored local
conveniences AND were LF-only, while players/ is correctly CRLF. Since this
change adds a `goto` label -- the construct most likely to misbehave in an
LF-only bat -- the root copies have been re-synced byte-identical from
players/ so local runs match the shipped artifact.
Co-Authored-By: Claude Opus 5 (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.
The steam bat missed the two client flags the join bats set -- steam-launched
missions booted 3rd-person (the same field bug fixed for join.bat earlier).
Env inherits through the menu's mission relaunch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three build dirs (build\ pod, build-glass\, build-steam\) were compile-
time splits of the same game; the tax was real: a stale steam exe shipped 2
days behind, the felt keymap flipped with the boot flavor, and cockpit
clicks were silently dead in the pod build (the trigger-config incident).
Now everything compiles into the single build\ exe:
- CMakeLists: BT_GLASS/BT_STEAM options removed; the glass TUs (PadRIO,
bindings, panel, glass windows, plasma) + FE/console + Steam transport
compile unconditionally; both macros always defined (seam markers).
- Steam: steam_api.lib linked with /DELAYLOAD:steam_api.dll + delayimp --
the DLL maps only when SteamAPI is first called, and every call site
gates on BT_STEAM_NET=1 (BTSteamNet_* short-circuit on steamActive), so
machines without the DLL run everything but Steam sessions.
- CABINET GUARD (btl4main): the miniconsole menu previously claimed any
zero-arg launch -- unified, that would hijack the pod cabinet's boot
shape. The menu now requires BT_PLATFORM=glass (or BT_FE_MENU=1);
platform parse moved above the fe_menu_mode computation. A bare boot
behaves exactly like the old pod build (DEV profile, wait for egg).
- Bats (play_solo/join/join_lan/play_steam) + the btoperator.py template:
single build\ path + BT_PLATFORM=glass. mkdist: one exe (+DLL scan
picks up OpenAL32 + steam_api). build-glass\/build-steam\/build-glass2\
(a stale Jul-20 cache) deleted.
Verified matrix on the unified exe:
bare zero-arg boot == old pod exe (same DEV profile + egg-load path;
the segfault on a missing egg is pre-existing, baselined vs the zip exe)
glass + BT_BTNTEST scripted click -> [cfgmap] ENTER/EXIT (trigger config)
runs with steam_api.dll ABSENT (delay-load proven)
glass zero-arg -> miniconsole menu
2-node loopback MP: 145 ticks each, cross-connected
BT_STEAM_NET=1 without Steam client -> "staying on Winsock", game runs
KB: context/glass-cockpit.md compile-gates section rewritten for the
unified model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zero-mission-arg launch of the build-steam exe -> lands in the glass FE
menu, where Steam sessions actually form (host or join an ISteamMatchmaking
lobby; there is no address to dial, so this is a LAUNCHER not a joiner --
unlike join.bat/join_lan.bat). Sets BT_PLATFORM=glass + BT_STEAM_NET=1,
logs to content\steam.log. Guarded like the sibling bats, with the two
failure modes spelled out (zip without build-steam\; Steam client not
running = menu works, no lobbies).
Requires distributing the build-steam variant (BT_GLASS+BT_STEAM) alongside
content\ -- a different zip shape; the pod-build zips are unaffected. For
dev testers to exercise the Steam path further (prior tests were buggy);
NOT for general players yet: the live 2-machine exit criterion
(docs/STEAM_TEST.md) is still open and the AppID is the 480 test id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>