From 7afbda900e20aec3461da15a57b225caaa26108a Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 27 Jul 2026 16:06:39 -0500 Subject: [PATCH] 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 --- context/build-and-run.md | 10 +++ phases/phase-12-orphan-processes.md | 110 ++++++++++++++++++++++++++++ players/join.bat | 21 +++++- players/join_lan.bat | 21 +++++- players/play_solo.bat | 21 +++++- players/play_steam.bat | 21 +++++- scratchpad/btwait_probe.ps1 | 33 +++++++++ scratchpad/orphan_win.ps1 | 106 +++++++++++++++++++++++++++ scratchpad/test_loop_logic.ps1 | 48 ++++++++++++ 9 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 phases/phase-12-orphan-processes.md create mode 100644 scratchpad/btwait_probe.ps1 create mode 100644 scratchpad/orphan_win.ps1 create mode 100644 scratchpad/test_loop_logic.ps1 diff --git a/context/build-and-run.md b/context/build-and-run.md index e02d1ab..2497dbe 100644 --- a/context/build-and-run.md +++ b/context/build-and-run.md @@ -49,6 +49,16 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg mission generation the menu launched (the child inherits the parent's cwd). Invisible for years because every launcher `cd`s to `content\` first; it became reachable once glass became the desktop default and a zero-arg launch started opening the menu. +- **The launcher's handoff wait must track ITS OWN generation [T2, 2026-07-27].** The front end + does not stay resident — it `CreateProcess`es the mission generation and exits — so every + launcher ends in a `:btwait` loop that waits for the handed-off process before printing its + sign-off. That loop used to poll `tasklist /FI "IMAGENAME eq btl4.exe"`, which is **machine-wide**: + a second client, the operator's own pod, or an orphan from a crash kept the window spinning + forever, which is the field report *"closing the game leaves the terminal open"*. Fixed by + snapshotting the PIDs alive before the launch and waiting only on PIDs absent from that snapshot. + ⚠ **Keep the `if /I "%%P"=="btl4.exe"` guard** — parsing tasklist with `tokens=2` alone reads its + *"INFO: No tasks are running"* line as a PID and spins forever with nothing running. Full + investigation, including which windows exit vs merely hide: `phases/phase-12-orphan-processes.md`. - **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412 keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's diff --git a/phases/phase-12-orphan-processes.md b/phases/phase-12-orphan-processes.md new file mode 100644 index 0000000..1d664ab --- /dev/null +++ b/phases/phase-12-orphan-processes.md @@ -0,0 +1,110 @@ +# Phase 12 — orphaned processes, the terminal that never closes, and the MP-hiccup claim + +*2026-07-27. Triggered by an operator report: "if you X close the game window it leaves the +terminal/console open sometimes, and leaves orphaned processes — I think this is a major +contributor to multiplayer game hiccups."* + +All findings below were produced against **4.11.600** on the dev rig, not recalled. Harnesses live +in `scratchpad/orphan_*.ps1`, `scratchpad/btwait_probe.ps1`, `scratchpad/test_loop_logic.ps1`. + +--- + +## What the process actually owns + +A running pod owns four top-level windows (enumerated live, `orphan_win.ps1`): + +| class | title | size | X does | +|---|---|---|---| +| `MainWndClass` | `BattleTech 4.11.600 — node 1601` | 1468x1038 | **destroy → real exit** | +| `BTPlasmaWnd` | `BattleTech - Plasma` | 528x167 | **hide only** (`L4PLASMAWIN.cpp:21`) | +| `MSCTFIME UI` / `IME` | — | 0x0 | OS input plumbing | + +Under `BT_GLASS_PANELS` the per-display panels (`L4GLASSWIN.cpp:693`) and the pad panel +(`L4PADPANEL.cpp:468`) behave like the plasma window: `ShowWindow(SW_HIDE)`, never quit. + +## Finding 1 — closing the MAIN window is CLEAN [T2] + +Verified in three states: solo mid-model-load (`[loadobj]` active), the relay join wait, and 16 s +after a real console `launch`. Every run: **process exited ~1 s, no relaunch, no orphan**, and the +relay logged `PLAYER 1 LEFT ... (0 seated)`. The close-means-quit gate (`ecb9b33`) works. + +**So the orphans the playtesters saw are consistent with 584 and earlier**, where every window +close hit `BTFE_RelaunchSelfAndExit` and resurrected the client. That is already fixed in 600 and +still `awaiting-verification`. + +## Finding 2 — closing an AUX window hides it irrecoverably [T2] + +X on the plasma window (or a glass panel) hides it with **no way to bring it back** — no menu item, +no hotkey — while the game runs on. Verified: `vis=True → vis=False`, process alive, both relay +sockets still `Established`. Not an orphan (the main window is still there), but it explains the +report *"eventually all I could find was my plasma display"*: the small plasma window is what +survives when the big one is gone, and a player who closes it has silently lost it for the session. + +## Finding 3 — the terminal that never closes [T2] — FIXED + +`join.bat`'s `:btwait` loop waited for **any `btl4.exe` on the machine**: + +```bat +for /f "tokens=1" %%P in ('tasklist /FI "IMAGENAME eq btl4.exe" /NH') do ( + if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1" +) +``` + +Proved with `btwait_probe.ps1`: a bat that launched **nothing** reports `would KEEP SPINNING` +purely because an unrelated instance is alive. So the window never prints its sign-off and looks +hung whenever *anything else* is running — a second client, the operator's own pod, or a genuine +orphan. **This is the reported symptom**, and it is a detector of an orphan, not a cause. + +All four launchers carried the identical block. Fixed by snapshotting the PIDs alive *before* the +launch and only waiting on PIDs absent from that snapshot. The `if /I "%%P"=="btl4.exe"` guard is +retained deliberately: `tokens=2` alone would parse tasklist's *"INFO: No tasks are running"* line +as a PID and spin forever with nothing running — a worse bug than the one being fixed. + +Verified with the text lifted verbatim out of the shipped `play_solo.bat`: + +| case | expected | got | +|---|---|---| +| nothing running (regression guard) | sign off | sign off | +| someone else's instance alive | sign off | sign off | +| our own generation alive | keep waiting | keep waiting | +| decoy gone, re-check | sign off | sign off | + +The patched bat still launches the game (pid + `launch_report.txt` confirmed). **Not verified:** the +full handoff E2E — the bat blocks on the FE menu, which waits for human input, so an automated run +never reaches `:btwait`. Needs one human run. + +Note the bat also ends in `pause` by design, so the window always stays until a keypress. That is +intended and is a second, benign reason a terminal is "still open". + +## Finding 4 — `BTLoadPump` eats WM_QUIT [T3, latent] + +`L4VIDEO.cpp:9039` drains the queue during mission load: + +```c +while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageA(&msg); } +``` + +`WM_QUIT` dispatched this way is silently discarded — it belongs to no window — so a quit posted +during load is destroyed and `APPMGR.cpp:102`'s pump never sees it. **This did not produce a hang +in testing** (the destroyed HWND breaks rendering, which tears the process down another way), so it +is not today's orphan mechanism. Flagged as a trap: any future code that relies on WM_QUIT +surviving a load will fail here. + +## The multiplayer-hiccup claim — verdict + +The mechanism is real **conditional on an orphan existing**: an orphan keeps both TCP connections +to the relay (`:1600` console and `:1601` game) `Established`, so the relay counts a pilot who will +never ready — and `SEAT_LEFT_GRACE_SECONDS` never starts, because nothing ever left. That matches +"waiting for mission assignment" forever. + +But **no orphan could be produced from an X-close in 4.11.600**. So on the current build the more +likely contributor is Finding 3: a second instance (the operator's own pod is enough) hangs every +join window, which reads as "the game didn't exit", which invites people to kill processes and +relaunch — and *that* is what manufactures real ghosts. + +## Follow-ups + +- Aux windows should either refuse to close or be restorable (Finding 2). +- Consider making `BTLoadPump` re-post WM_QUIT instead of swallowing it (Finding 4). +- The `:btwait` fix reaches players only when the zip is re-cut; 4.11.600 as shipped still has the + machine-wide wait. diff --git a/players/join.bat b/players/join.bat index b2c3262..6d3a2f9 100644 --- a/players/join.bat +++ b/players/join.bat @@ -27,6 +27,19 @@ rem LAUNCH FORENSICS (#41): bracket the exe from outside -- if it dies rem before it can write its own log, this report still records when it rem ran, how it exited, and whether it ever created its log at all. echo [%DATE% %TIME%] launching btl4.exe > launch_report.txt +rem ------------------------------------------------------------------ +rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold +rem this window open for ANY btl4.exe on the machine -- a second client, +rem the operator's own pod, or an orphan left by a crash. The window +rem then never printed the sign-off and looked hung: the reported "it +rem leaves the terminal open". Snapshot the PIDs alive BEFORE we launch; +rem those belong to somebody else and must not hold us here. +rem ------------------------------------------------------------------ +setlocal EnableDelayedExpansion +set "BT_PRE=," +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" set "BT_PRE=!BT_PRE!%%Q," +) ..\build\Release\btl4.exe set BT_EXITCODE=%ERRORLEVEL% echo [%DATE% %TIME%] btl4.exe exited with code %BT_EXITCODE% >> launch_report.txt @@ -54,8 +67,12 @@ rem behaviour, never a hang. rem ------------------------------------------------------------------ :btwait set "BT_STILL_RUNNING=" -for /f "tokens=1" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( - if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1" +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" ( + rem a PID that was NOT in the pre-launch snapshot is ours + set "BT_STRIP=!BT_PRE:,%%Q,=!" + if "!BT_STRIP!"=="!BT_PRE!" set "BT_STILL_RUNNING=1" + ) ) if defined BT_STILL_RUNNING ( %SystemRoot%\System32\timeout.exe /t 2 /nobreak >nul 2>nul diff --git a/players/join_lan.bat b/players/join_lan.bat index 602fca9..edd0073 100644 --- a/players/join_lan.bat +++ b/players/join_lan.bat @@ -30,6 +30,19 @@ rem LAUNCH FORENSICS (#41): bracket the exe from outside -- if it dies rem before it can write its own log, this report still records when it rem ran, how it exited, and whether it ever created its log at all. echo [%DATE% %TIME%] launching btl4.exe > launch_report.txt +rem ------------------------------------------------------------------ +rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold +rem this window open for ANY btl4.exe on the machine -- a second client, +rem the operator's own pod, or an orphan left by a crash. The window +rem then never printed the sign-off and looked hung: the reported "it +rem leaves the terminal open". Snapshot the PIDs alive BEFORE we launch; +rem those belong to somebody else and must not hold us here. +rem ------------------------------------------------------------------ +setlocal EnableDelayedExpansion +set "BT_PRE=," +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" set "BT_PRE=!BT_PRE!%%Q," +) ..\build\Release\btl4.exe set BT_EXITCODE=%ERRORLEVEL% echo [%DATE% %TIME%] btl4.exe exited with code %BT_EXITCODE% >> launch_report.txt @@ -57,8 +70,12 @@ rem behaviour, never a hang. rem ------------------------------------------------------------------ :btwait set "BT_STILL_RUNNING=" -for /f "tokens=1" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( - if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1" +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" ( + rem a PID that was NOT in the pre-launch snapshot is ours + set "BT_STRIP=!BT_PRE:,%%Q,=!" + if "!BT_STRIP!"=="!BT_PRE!" set "BT_STILL_RUNNING=1" + ) ) if defined BT_STILL_RUNNING ( %SystemRoot%\System32\timeout.exe /t 2 /nobreak >nul 2>nul diff --git a/players/play_solo.bat b/players/play_solo.bat index 7466cfa..b26d9f6 100644 --- a/players/play_solo.bat +++ b/players/play_solo.bat @@ -29,6 +29,19 @@ rem LAUNCH FORENSICS (#41): bracket the exe from outside -- if it dies rem before it can write its own log, this report still records when it rem ran, how it exited, and whether it ever created its log at all. echo [%DATE% %TIME%] launching btl4.exe > launch_report.txt +rem ------------------------------------------------------------------ +rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold +rem this window open for ANY btl4.exe on the machine -- a second client, +rem the operator's own pod, or an orphan left by a crash. The window +rem then never printed the sign-off and looked hung: the reported "it +rem leaves the terminal open". Snapshot the PIDs alive BEFORE we launch; +rem those belong to somebody else and must not hold us here. +rem ------------------------------------------------------------------ +setlocal EnableDelayedExpansion +set "BT_PRE=," +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" set "BT_PRE=!BT_PRE!%%Q," +) ..\build\Release\btl4.exe set BT_EXITCODE=%ERRORLEVEL% echo [%DATE% %TIME%] btl4.exe exited with code %BT_EXITCODE% >> launch_report.txt @@ -56,8 +69,12 @@ rem behaviour, never a hang. rem ------------------------------------------------------------------ :btwait set "BT_STILL_RUNNING=" -for /f "tokens=1" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( - if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1" +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" ( + rem a PID that was NOT in the pre-launch snapshot is ours + set "BT_STRIP=!BT_PRE:,%%Q,=!" + if "!BT_STRIP!"=="!BT_PRE!" set "BT_STILL_RUNNING=1" + ) ) if defined BT_STILL_RUNNING ( %SystemRoot%\System32\timeout.exe /t 2 /nobreak >nul 2>nul diff --git a/players/play_steam.bat b/players/play_steam.bat index 54f0bca..9e57579 100644 --- a/players/play_steam.bat +++ b/players/play_steam.bat @@ -24,6 +24,19 @@ rem full menu here (all modes + steam buttons); clear the solo trim in rem case this shell ran play_solo.bat first set BT_FE_SOLO= cd content +rem ------------------------------------------------------------------ +rem OUR GENERATION ONLY (2026-07-27). The wait loop below used to hold +rem this window open for ANY btl4.exe on the machine -- a second client, +rem the operator's own pod, or an orphan left by a crash. The window +rem then never printed the sign-off and looked hung: the reported "it +rem leaves the terminal open". Snapshot the PIDs alive BEFORE we launch; +rem those belong to somebody else and must not hold us here. +rem ------------------------------------------------------------------ +setlocal EnableDelayedExpansion +set "BT_PRE=," +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" set "BT_PRE=!BT_PRE!%%Q," +) ..\build\Release\btl4.exe rem ------------------------------------------------------------------ @@ -47,8 +60,12 @@ rem behaviour, never a hang. rem ------------------------------------------------------------------ :btwait set "BT_STILL_RUNNING=" -for /f "tokens=1" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( - if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1" +for /f "tokens=1,2" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" ( + rem a PID that was NOT in the pre-launch snapshot is ours + set "BT_STRIP=!BT_PRE:,%%Q,=!" + if "!BT_STRIP!"=="!BT_PRE!" set "BT_STILL_RUNNING=1" + ) ) if defined BT_STILL_RUNNING ( %SystemRoot%\System32\timeout.exe /t 2 /nobreak >nul 2>nul diff --git a/scratchpad/btwait_probe.ps1 b/scratchpad/btwait_probe.ps1 new file mode 100644 index 0000000..be8aff3 --- /dev/null +++ b/scratchpad/btwait_probe.ps1 @@ -0,0 +1,33 @@ +# Does join.bat's :btwait loop wait for ANY btl4.exe, or only its own? +# Run the EXACT fragment from join.bat while an unrelated btl4 is alive. +$content = 'C:\git\bt411\content' +$scratch = 'C:\Users\epilectrik\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad' + +# an UNRELATED instance -- stands in for the operator's own pod, a second +# client, or a genuine orphan. Nothing to do with the bat we are testing. +$env:BT_LOG='btwait_other.log'; $env:BT_START_INSIDE='1' +foreach ($v in 'BT_RELAY','BT_FE_LOOP','BT_SELF') { Remove-Item -LiteralPath "Env:\$v" -ErrorAction SilentlyContinue } +$other = Start-Process 'C:\git\bt411\build\Release\btl4.exe' -ArgumentList '-egg','ARENA1.EGG' ` + -WorkingDirectory $content -PassThru +"unrelated btl4 running as pid $($other.Id)" +Start-Sleep -Seconds 6 + +# the exact loop body from join.bat lines 55-63, minus the goto +$frag = @' +@echo off +set "BT_STILL_RUNNING=" +for /f "tokens=1" %%P in ('%SystemRoot%\System32\tasklist.exe /FI "IMAGENAME eq btl4.exe" /NH 2^>nul') do ( + if /I "%%P"=="btl4.exe" set "BT_STILL_RUNNING=1" +) +if defined BT_STILL_RUNNING (echo BTWAIT: would KEEP SPINNING -- terminal stays open) else (echo BTWAIT: would exit and print "The game has exited") +'@ +$fragPath = Join-Path $scratch 'btwait_frag.bat' +Set-Content $fragPath $frag -Encoding Ascii + +'--- the bat that launched NOTHING now runs the wait loop ---' +& cmd.exe /c $fragPath + +Stop-Process -Id $other.Id -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 2 +'--- same loop with no btl4 anywhere ---' +& cmd.exe /c $fragPath diff --git a/scratchpad/orphan_win.ps1 b/scratchpad/orphan_win.ps1 new file mode 100644 index 0000000..e7f48b5 --- /dev/null +++ b/scratchpad/orphan_win.ps1 @@ -0,0 +1,106 @@ +# Identify every top-level window the pod owns (real titles + class names), +# then X-close ONLY the main game window -- the actual user action. +param([int]$CloseAfterLaunch = 5, [string]$Tag = 'mainwin', [switch]$SkipLaunch) + +Add-Type @' +using System; +using System.Text; +using System.Runtime.InteropServices; +public class W4 { + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern bool EnumWindows(EnumProc cb, IntPtr p); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, StringBuilder s, int n); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassNameW(IntPtr h, StringBuilder s, int n); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); + [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern bool PostMessageW(IntPtr h, uint m, IntPtr w, IntPtr l); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } + public delegate bool EnumProc(IntPtr h, IntPtr p); + public class Win { public IntPtr H; public string Title; public string Cls; public bool Vis; public int W, Ht; } + public static System.Collections.Generic.List List = new System.Collections.Generic.List(); + public static uint Target; + public static bool Cb(IntPtr h, IntPtr p) { + uint pid; GetWindowThreadProcessId(h, out pid); + if (pid == Target) { + StringBuilder t = new StringBuilder(256); GetWindowTextW(h, t, 256); + StringBuilder c = new StringBuilder(256); GetClassNameW(h, c, 256); + RECT r; GetWindowRect(h, out r); + List.Add(new Win { H=h, Title=t.ToString(), Cls=c.ToString(), Vis=IsWindowVisible(h), + W=r.R-r.L, Ht=r.B-r.T }); + } + return true; + } + public static void Scan(uint pid) { List.Clear(); Target = pid; EnumWindows(Cb, IntPtr.Zero); } +} +'@ + +$content = 'C:\git\bt411\content' +$mine = @() +function Pods { @(Get-CimInstance Win32_Process -Filter "Name='btl4.exe'" | Select-Object -ExpandProperty ProcessId) } +function ShowWins($pid_, $label) { + [W4]::Scan([uint32]$pid_) + " $label" + foreach ($w in [W4]::List) { + " hwnd={0,-10} vis={1,-5} {2,4}x{3,-4} class='{4}' title='{5}'" -f $w.H.ToInt64(), $w.Vis, $w.W, $w.Ht, $w.Cls, $w.Title + } +} + +$relayLog = Join-Path $content "orphan_relay_$Tag.log" +$relay = Start-Process 'python' -ArgumentList 'C:\git\bt411\tools\btconsole.py','--relay','1600','OPERATOR.EGG' ` + -WorkingDirectory $content -PassThru -RedirectStandardOutput $relayLog ` + -RedirectStandardError (Join-Path $content "orphan_relay_$Tag.err") -WindowStyle Hidden +$mine += $relay.Id +Start-Sleep -Seconds 4 + +$log = "orphan_$Tag.log" +Remove-Item (Join-Path $content $log) -Force -ErrorAction SilentlyContinue +$env:BT_LOG=$log; $env:BT_LOG_APPEND='1'; $env:BT_START_INSIDE='1'; $env:BT_DEV_GAUGES='1' +$env:BT_PLATFORM='glass'; $env:BT_RELAY='127.0.0.1:1600'; $env:BT_FE_LOOP='1'; $env:BT_CALLSIGN='ORPHANTEST' +$pod = Start-Process 'C:\git\bt411\build\Release\btl4.exe' ` + -ArgumentList '-egg','OPERATOR.EGG','-net','1601','-platform','glass' ` + -WorkingDirectory $content -PassThru +$mine += $pod.Id +"pod pid $($pod.Id)" +Start-Sleep -Seconds 20 +ShowWins $pod.Id "WINDOWS IN THE JOIN WAIT:" + +if (-not $SkipLaunch) { + $secret = (Get-Content (Join-Path $content 'operator_secret.txt') -Raw).Trim() + $cli = New-Object System.Net.Sockets.TcpClient('127.0.0.1', 1607) + $wr = New-Object System.IO.StreamWriter($cli.GetStream()); $wr.AutoFlush = $true + $wr.WriteLine("AUTH $secret"); Start-Sleep -Milliseconds 700; $wr.WriteLine("launch") + "LAUNCH sent; closing in ${CloseAfterLaunch}s" + Start-Sleep -Seconds $CloseAfterLaunch + ShowWins $pod.Id "WINDOWS MID-LOAD:" +} + +# The MAIN game window: the big visible one (the D3D target), not a panel. +[W4]::Scan([uint32]$pod.Id) +$main = [W4]::List | Where-Object { $_.Vis } | Sort-Object { $_.W * $_.Ht } -Descending | Select-Object -First 1 +"--- X-closing ONLY the main window: hwnd=$($main.H.ToInt64()) '$($main.Title)' $($main.W)x$($main.Ht) ---" +[void][W4]::PostMessageW($main.H, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero) + +$exited = $false; $secs = 0 +for ($i = 1; $i -le 45; $i++) { + Start-Sleep -Milliseconds 1000 + if (-not $exited -and (Get-Process -Id $pod.Id -ErrorAction SilentlyContinue) -eq $null) { $exited=$true; $secs=$i } +} +"" +"RESULT exited : $exited$(if($exited){" after ${secs}s"})" +if (-not $exited) { + ShowWins $pod.Id "*** ORPHAN SURVIVES -- its windows now: ***" + " cpu=$([math]::Round((Get-Process -Id $pod.Id).CPU,1))s" + Get-NetTCPConnection -OwningProcess $pod.Id -ErrorAction SilentlyContinue | + Where-Object { $_.State -eq 'Established' } | + ForEach-Object { " STILL CONNECTED: :$($_.LocalPort) -> $($_.RemoteAddress):$($_.RemotePort)" } + " --- relay's view (is the ghost still seated?) ---" + Get-Content $relayLog -Tail 4 | ForEach-Object { " $_" } +} +"--- pod log tail ---" +Get-Content (Join-Path $content $log) -Tail 5 | ForEach-Object { " $_" } + +"--- cleanup (only my pids: $($mine -join ',')) ---" +foreach ($m in $mine) { Stop-Process -Id $m -Force -ErrorAction SilentlyContinue } +Start-Sleep -Milliseconds 800 +"btl4 remaining: $((Pods) -join ',')" diff --git a/scratchpad/test_loop_logic.ps1 b/scratchpad/test_loop_logic.ps1 new file mode 100644 index 0000000..e19d59a --- /dev/null +++ b/scratchpad/test_loop_logic.ps1 @@ -0,0 +1,48 @@ +# Test the PATCHED wait logic, using the exact snapshot + loop text lifted +# out of the shipped play_solo.bat. Three cases, one of them the regression +# guard that matters most (nothing running must never spin). +$scratch = 'C:\Users\epilectrik\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad' +$bat = Get-Content 'C:\git\bt411\players\play_solo.bat' -Raw + +# lift the two patched blocks verbatim +$snap = [regex]::Match($bat, '(?ms)^setlocal EnableDelayedExpansion\r?\nset "BT_PRE=,".*?^\)\r?\n').Value +$loop = [regex]::Match($bat, '(?ms)^:btwait\r?\nset "BT_STILL_RUNNING=".*?^\)\r?\n').Value +if (-not $snap -or -not $loop) { throw "could not lift the blocks from play_solo.bat" } +"lifted snapshot block : $($snap.Split("`n").Count) lines" +"lifted wait block : $($loop.Split("`n").Count) lines" + +function Probe($label, $snapshotFirst) { + # $snapshotFirst = does the decoy exist when we take the snapshot? + $body = "@echo off`r`n" + if ($snapshotFirst) { $body += $snap } else { + # snapshot with nothing running, decoy starts after -> looks like OURS + $body += "setlocal EnableDelayedExpansion`r`nset `"BT_PRE=,`"`r`n" + } + $body += $loop + $body += 'if defined BT_STILL_RUNNING (echo VERDICT: SPIN) else (echo VERDICT: SIGN-OFF)' + "`r`n" + $p = Join-Path $scratch 'loop_probe.bat' + Set-Content $p $body -Encoding Ascii + $r = & cmd.exe /c $p + " {0,-42} {1}" -f $label, $r +} + +'=== C: nothing running (REGRESSION GUARD -- must sign off) ===' +Probe 'no btl4 anywhere' $true + +$env:BT_LOG='loopprobe.log'; $env:BT_START_INSIDE='1' +foreach ($v in 'BT_RELAY','BT_FE_LOOP') { Remove-Item -LiteralPath "Env:\$v" -ErrorAction SilentlyContinue } +$decoy = Start-Process 'C:\git\bt411\build\Release\btl4.exe' -ArgumentList '-egg','ARENA1.EGG' ` + -WorkingDirectory 'C:\git\bt411\content' -PassThru +"decoy btl4 pid $($decoy.Id)" +Start-Sleep -Seconds 6 + +'=== A: someone ELSE''s btl4 (in the snapshot) -- must sign off ===' +Probe 'unrelated instance alive' $true + +'=== B: OUR generation (not in the snapshot) -- must keep waiting ===' +Probe 'our own generation alive' $false + +Stop-Process -Id $decoy.Id -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 2 +'=== C2: decoy gone, re-check the guard ===' +Probe 'nothing running again' $true