Commit Graph
15 Commits
Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 1777d5a62e one log per day, and every log says who it sent it -- rotation was eating the crash stacks
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>
2026-07-28 10:33:07 -05:00
arcattackandClaude Opus 5 7afbda900e the terminal that never closes: every launcher waited for ANY btl4.exe on the machine
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>
2026-07-27 16:06:39 -05:00
arcattackandClaude Opus 5 35bdb505df join bats keep the previous session's log as join.old.log
Conn Man's owens-laser crash stack (#35) was in his join.log -- and re-running
join.bat deleted it before it could be sent.  The bat now rotates instead:
join.log -> join.old.log at launch.  One generation of history, the exact
insurance that would have closed #35 tonight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 00:42:15 -05:00
arcattackandClaude Opus 5 bf40c53e1e Launchers: make the handoff wait tool-independent -- my first version broke on any PATH carrying Git-Bash/MSYS
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>
2026-07-25 18:37:56 -05:00
arcattackandClaude Opus 5 145bf368c8 Launchers (players/, the shipped source): wait for the handed-off game process instead of announcing "The game has exited" mid-load
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>
2026-07-25 18:31:34 -05:00
arcattackandClaude Opus 4.8 41e899a9d7 Issue #41: launch forensics in the player bats -- exit code + never-started detection
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>
2026-07-24 10:02:51 -05:00
arcattack 26439c2e1a Boot forensics: first-breath log line + generation-safe bat logging
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.
2026-07-23 21:33:27 -05:00
arcattackandClaude Opus 4.8 fcedc046cf Walk-up callsign/mech request: join menu -> relay -> the session egg
The 1995 front-desk conversation, over the internet.  join.bat/
join_lan.bat now open a JOIN-GAME menu (the FE in BT_FE_JOIN trim:
CALLSIGN edit + the 18-mech list + JOIN); the choice relaunches into the
normal join with BT_CALLSIGN/BT_MECH env, rides the relay SEAT_REQUEST
as {callsign NUL mech NUL} (empty payload = roster defaults, wire-
compatible), and the relay validates the mech tag, HOLDS egg delivery
until every pod's console pad is connected, rewrites the egg via
eggmodel (vehicle= + rasterized callsign bitmaps, graceful no-PySide6
fallback) and streams it to all pads -- so every player's egg copy
carries every player's callsign.

The hold is gated on CONSOLE-PAD count, not game-side registration: a
pod HELLOs only after parsing the egg's roster, so a registration gate
deadlocks (hit live in the first run; pods animate in WAITING FOR
MISSION ASSIGNMENT during the hold).

Verified 2-node e2e (env-injected requests): thor/vulture requested over
roster defaults bhk1/ava1 -> relay held 1/2, released at 2/2, rewrote
both seats (relay log), the delivered egg carries vehicle=thor/vulture +
name=VIPER/MONGOOSE, both pods launched and built [cyl] tables for both
requested mechs.  Join-menu layout screenshot-verified; the menu's
click-through relaunch awaits live human verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:58:51 -05:00
arcattackandClaude Opus 4.8 10ed66bd7c UNIFY: one exe -- retire the pod/glass/steam compile split
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>
2026-07-21 10:28:51 -05:00
arcattackandClaude Opus 4.8 3b17b60c19 Join bats: same glass-preference treatment as play_solo
join.bat + join_lan.bat + the btoperator.py generator template now prefer
build-glass\Release\btl4.exe when present and set BT_PLATFORM=glass with it
-- so MP sessions on a dev checkout get PadRIO (mouse-clickable cockpit
buttons: trigger config, MFD banks, radar rails) and the bindings.txt keymap,
identical to solo play.  Player zips (which ship only build\) fall back to
the pod build exactly as before.

Verified: 2-node loopback MP smoke on the GLASS build (the documented
procedure: A -egg MP.EGG -net 1501 + B -net 1601 + tools/btconsole.py) --
both nodes cross-connected (GameMachineHost 1502/1602), ~150 sim ticks
each, no errors.  btoperator.py syntax-checked; regenerated bats carry the
same guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:04:02 -05:00
arcattackandClaude Fable 5 2fb2f8a36d join bats: default to cockpit view + gauges (BT_START_INSIDE + BT_DEV_GAUGES)
Field report: a remote player had smooth gameplay but in 3RD-PERSON with
NO gauges.  Cause: view mode + gauges are CLIENT launch flags, not
something the relay/console can enforce -- and join.bat set neither, so
remote players booted in the game's desktop default (external camera, no
MFDs).  The operator's own local launch sets both (the app's 'Start in
cockpit' + 'Dev gauges' checkboxes, on by default), which is why the
operator saw the correct view but remote players didn't.

The pod is a first-person cockpit with MFD gauges and no '3rd person' at
all, so cockpit is the right default for players.  join.bat/join_lan.bat
now set BT_START_INSIDE=1 + BT_DEV_GAUGES=1.  (Players can still press V
to toggle the external camera.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:50:21 -05:00
arcattackandClaude Fable 5 0b83d71578 Harden player bats: location guard for 'cannot find path specified'
Field report: a remote player ran join.bat from the wrong place (inside
the zip, or the .bat separated from content\/build\) and got 'The system
cannot find the path specified' + dumps -- the relative 'cd %~dp0content'
failed cryptically.

Now each bat: cd to its own dir, verify build\Release\btl4.exe +
content\<egg> exist, else jump to :badpath and print a clear message
('this file must sit in the extracted folder next to content and build;
right-click the zip - Extract All and run it from there').  Verified: run
from a dir with no content/build -> the guidance prints, no dump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:16:38 -05:00
arcattackandClaude Fable 5 954175e44e Diagnostics: pre-launch readiness warning + named friend logs (join.log)
Two additions after the live-session troubleshooting:

1. LAUNCH-SHORT is now VISIBLE (btconsole).  Pressing LAUNCH with an
   empty roster seat previously did NOTHING (eggs_done_at only sets when
   ALL seats ACK, so the manual-launch arming silently no-op'd -- the
   'I pressed launch and nothing happened' confusion).  Now the relay
   logs 'LAUNCH pressed but NOT all seats are filled' + a per-seat
   readiness line + a WARNING naming the empty seat(s) and the fix
   (reduce the roster).  When all seats DO fill it auto-arms (launch
   request persists).  Verified: 2/3 filled -> clear warning naming
   seat3.

2. Friend logs are now retrievable (btoperator exporter).  join.bat /
   join_lan.bat set BT_LOG=join.log and their exit message tells the
   player to send content\join.log to the operator if the game closed
   unexpectedly -- closes the friend-side-crash blind spot (their log
   was the generic btl4.log, un-named, easy to miss).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:05:34 -05:00
arcattackandClaude Fable 5 dfd1894fb9 Unreachable-server UX: message boxes + bat guidance instead of a 'crash'
Field report (first internet-join attempt): both join bats 'hard
crash' on the remote machine.  Root cause was procedural -- the host
session was not running -- but the failure PRESENTATION was the bug:
Release Fail() is a bare abort(), so a dead relay = silent process
death + instantly-closing console window.

- L4NET: the two player-facing dead-ends (LAN discovery no-answer,
  seat request unreachable/full) now show a MessageBox saying what
  happened and what to do before exiting (verified live against a
  dead relay: 'BattleTech -- can't join the game' appears).
- join bats: echo + pause after exit so the window stays readable;
  join_lan.bat header now says it is LAN-only (the remote user ran
  both -- join_lan can never work over the internet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:23:29 -05:00
arcattackandClaude Fable 5 f04e8019c2 D1: relay-assigned seats -- players never need a player number
A pod launched with no BT_SELF asks the relay for a seat before joining:
new control frames SEAT_REQUEST (-6) -> relay reserves the lowest roster
seat not claimed or reserved (60s reservation so simultaneous joiners
can't race onto one seat) -> SEAT_ASSIGN (-7, int32 hostID + NUL tag)
becomes relaySelf; everything downstream (egg self-match, HELLO) runs
exactly as if BT_SELF had been set. Roster full -> SEAT_FULL (-8) ->
clean Fail(). A real HELLO pops the reservation; a pod drop frees the
seat. Explicit BT_SELF still claims a specific seat (the operator's
local launches use it).

Client: L4NetworkManager::RelayRequestSeat (throwaway TCP dial to the
relay game port, 10s reply window). Relay: SEAT_REQUEST branch in
_handle_game_frame. Operator exporter collapsed from per-seat
join_as_playerN.bat to ONE universal join.bat (+ join_lan.bat with
BT_RELAY=auto) -- every player gets the same file, first come first
served, the arcade walk-up-to-a-pod model. players/ regenerated.

Verified: 8/8 seat stub tests (distinct seats in roster order, FULL on
exhaustion, claimed+reserved stays FULL, HELLO claim accepted, duplicate
HELLO refused); 2-node localhost e2e with NO BT_SELF on either pod ->
both seated, full ladder, RunMission pair, UDP flowing post-launch;
regression smoke: explicit-BT_SELF relay session clean with zero seat
requests, classic mesh (no BT_RELAY) un-regressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:11:03 -05:00