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>
264 lines
14 KiB
Markdown
264 lines
14 KiB
Markdown
# BattleTech 4.11 — Operator Console Guide
|
|
|
|
How to run a games night: set up a mission, get pods seated, launch rounds, and recover when
|
|
something sticks. Written for the sysop at the console, not for players.
|
|
|
|
*Engineering detail (ports, protocol, state machine) lives in `context/operator-console.md`.
|
|
Player-facing instructions ship in the zip as `README.txt`.*
|
|
|
|
---
|
|
|
|
## 1. Starting the console
|
|
|
|
```
|
|
cd C:\git\bt411
|
|
python tools\btoperator.py
|
|
```
|
|
|
|
A window titled **BT411 Operator Console** opens. That is the console.
|
|
|
|
> **One thing worth knowing:** there are two programs. `btoperator.py` is the window you use.
|
|
> `btconsole.py` is the headless relay it starts for you in the background — all the actual
|
|
> networking. You never launch that one directly; its output is what scrolls in the log pane at the
|
|
> bottom of the window. When this guide says "the relay said…", that is where to look.
|
|
|
|
---
|
|
|
|
## 2. Set up the mission, then start the session
|
|
|
|
Work top to bottom:
|
|
|
|
1. **Pick the mission settings** — map, mission length, weather, experience level, and the seat
|
|
(pod) count. Every dropdown is populated live from `BTL4.RES`, so anything offered is valid.
|
|
2. **Set the seat count to roughly who you expect.** It does not have to be exact — you can launch
|
|
with fewer (see §4) — but a wildly oversized roster makes the staging messages confusing.
|
|
3. **Choose the mode**: **Relay (internet)** is normal — players dial out to you, so *they* need no
|
|
port forwarding. **Mesh (LAN)** is the original cabinet direction for a local network.
|
|
4. **Press Start Session.** The relay comes up and begins accepting pods.
|
|
|
|
**For internet play, you must be reachable.** Forward/allow these TCP ports to this machine
|
|
(default console port 1500):
|
|
|
|
| Port | Why |
|
|
|---|---|
|
|
| 1500/tcp | the console channel (hands out the mission egg, sends the launch) |
|
|
| 1501/tcp | game traffic between pods |
|
|
| 1501/udp | the fast update channel (position/motion) |
|
|
|
|
Port 1507/tcp is the operator control channel — only needed if someone drives this relay remotely.
|
|
Players give their client `BT_RELAY=<your address>:1500`; `players/join.bat` handles that.
|
|
|
|
---
|
|
|
|
## 3. Watch the pods arrive
|
|
|
|
Each pilot row lights up as they progress. In order:
|
|
|
|
**waiting** → **seated** (claimed a seat, callsign and mech shown) → **egg sent** →
|
|
**registered** → **ready** (mission loaded) → **LAUNCHED**.
|
|
|
|
You will see this in the log and it is **normal, not an error**:
|
|
|
|
```
|
|
console conn 1.2.3.4:5678: egg HELD (3/8 pods present)
|
|
```
|
|
|
|
The mission file is deliberately held until every seat is present, so that every pod's copy
|
|
contains everyone's callsign and mech choice. If you are not waiting for the rest, just launch —
|
|
see next.
|
|
|
|
---
|
|
|
|
## 4. Launch a round
|
|
|
|
Press **🚀 LAUNCH MISSION**.
|
|
|
|
- If everyone has loaded, the round starts within a few seconds.
|
|
- **If some seats are empty, launching is still correct** — the relay shrinks the session to the
|
|
players who are actually here and starts. You do not need to edit the roster down first.
|
|
- The relay holds the start until every pod reports **ready**, so nobody loads into a mission
|
|
already in progress. If one pod is slow you will see who, every 10 seconds; after 3 minutes it
|
|
starts anyway rather than letting one wedged client hold up the night.
|
|
|
|
The mission then runs on its own clock (the mission length from the egg). **⏹ END MISSION** stops
|
|
it early.
|
|
|
|
---
|
|
|
|
## 5. Running back-to-back rounds
|
|
|
|
When a round ends, players' clients relaunch themselves and reconnect. Once they are back, press
|
|
**LAUNCH** again. You can change mission settings between rounds and press **Apply** first.
|
|
|
|
### If LAUNCH looks dead — press ↻ Re-arm
|
|
|
|
This is the fix for the most annoying failure this console had. If a round has ended and the LAUNCH
|
|
button is greyed out, or pressing it does nothing:
|
|
|
|
> **Press ↻ Re-arm, then LAUNCH.**
|
|
|
|
Re-arm clears the finished round's state and re-opens the launcher **while keeping everyone who is
|
|
already connected**. You do **not** need Stop Session / Start Session, which disconnects everybody.
|
|
|
|
Re-arm is **for between rounds only**, and the console enforces that: while a mission is running it
|
|
is greyed out (and the relay refuses it with *"press End Mission first"*), and while a launch is in
|
|
the middle of firing it is refused with *"launch sequence in flight"*. So you cannot break a live
|
|
round with it. Expect each already-waiting player's client to bounce once (a few seconds) right
|
|
after a Re-arm — that is the seat re-sync, not a crash.
|
|
|
|
**Why this happens:** the console used to only consider itself ready for a new round if *every*
|
|
player from the last round came back, or if *all* of them left. In between — the normal case when
|
|
one person closes their window or their client crashes — it got stuck, and said nothing. That is
|
|
fixed (an explicit LAUNCH now re-arms by itself), and Re-arm is the belt-and-braces button. If you
|
|
ever see the old behaviour, it is a bug worth reporting.
|
|
|
|
---
|
|
|
|
## 6. Known limitations — buttons that are not what they appear
|
|
|
|
Read this once; each of these is a control that looks live but does nothing in that mode.
|
|
|
|
**Mesh (LAN legacy) mode: the mission buttons do not work.** LAUNCH, END MISSION and Re-arm all log
|
|
">> operator … sent" and are then **silently discarded** — the mesh console has no command channel.
|
|
`Apply mission settings` is also inert (mesh reads the mission file once at startup). Mesh launches
|
|
its rounds by itself once the pods have the mission. **If you want to control launches, use Relay
|
|
mode.**
|
|
|
|
**Remote relay mode** (driving another machine's relay) is now a full console — LAUNCH, END MISSION,
|
|
Re-arm, Apply and Launch-local all work, and the pilot lights populate from the relay's own roster
|
|
when you connect. Two things still differ: **Stop Session only disconnects you** (the remote relay
|
|
keeps running, which is usually what you want), and **Restart Session** likewise just reconnects the
|
|
control link rather than restarting that relay.
|
|
|
|
### Running the relay PARKED (console from anywhere)
|
|
|
|
The recommended setup for a travelling operator: the relay lives permanently on the home machine,
|
|
so **no player ever edits `join.bat`**, and you drive it from a laptop — cell internet is fine,
|
|
since the console only makes an outbound connection.
|
|
|
|
**On the home machine, before you leave** — double-click **`tools\park_relay.cmd`** (or run
|
|
`python tools\btrelay_park.py --egg OPERATOR.EGG --port 1500`).
|
|
|
|
That window is the supervisor: it keeps the relay alive, **relaunching it if it ever dies** and
|
|
rotating the old log to `content\parked_relay.log.1` so you can still see what happened. Closing
|
|
the window (or Ctrl-C) stops the relay for good. To have it come back after a reboot, put a
|
|
*shortcut* to `park_relay.cmd` in your Startup folder (Win+R → `shell:startup`) — no admin needed.
|
|
|
|
Leave it running.
|
|
|
|
> **If you log in from a second account** (a dedicated remote-admin login over SSH/RDP), always
|
|
> park the relay with `park_relay.cmd` rather than typing `python ...` yourself. `python` only
|
|
> works for the main account here — it resolves through pyenv shims driven by `PYENV*` variables
|
|
> that are scoped to that user, and the one machine-wide Python entry points at a folder that no
|
|
> longer exists. The `.cmd` finds a real interpreter explicitly and prints which one it used;
|
|
> a bare `python` command from the other account just says "not recognized".
|
|
|
|
Note the port it prints for operator control (**1507** = console port + 7) and copy
|
|
`content\operator_secret.txt` — that string is what any console needs to connect, and it grants
|
|
full mission control, so hand it out deliberately.
|
|
|
|
**From the road:** run `python tools/btoperator.py`, put the home address in Host, Console port
|
|
1500, paste the secret, and connect. You get launch, end, re-arm, mission changes (map / time /
|
|
weather / scenario / temperature / length, applied to the egg on the home machine, effective next
|
|
round) and live pilot lights.
|
|
|
|
**Restart Session now really restarts the parked relay.** In remote mode the button asks the relay
|
|
to bounce itself; the supervisor brings it straight back with the egg re-read, and your console
|
|
reconnects a few seconds later. It is refused while a mission is running (it would drop everyone
|
|
mid-round) — press End Mission first. Use it to clear a wedge, or after someone has edited the egg
|
|
on the home machine.
|
|
|
|
**What still needs someone at the home machine:** starting the supervisor in the first place, and
|
|
editing the roster itself — seat count, callsigns, mechs, colours. Only the six mission settings
|
|
travel over the control link, so a *roster* change means editing the egg there and then pressing
|
|
Restart Session from wherever you are.
|
|
|
|
**Two traps worth knowing:**
|
|
- **The secret depends on the relay's working directory.** It is read as a bare `operator_secret.txt`
|
|
relative to wherever the relay was started, and if it is missing the relay *silently generates a
|
|
new one*. Always start the parked relay from the same folder (`content\`), or a console using
|
|
yesterday's secret will just be dropped on the auth timeout with no clear error.
|
|
- **Don't forward the control port to the open internet.** It is plaintext with a shared secret.
|
|
Keep the player ports (1500/1501) forwarded exactly as they already are and carry the operator
|
|
link over a private tunnel (Tailscale or similar) instead.
|
|
|
|
**Other sharp edges:**
|
|
- **Start Session overwrites your egg file** on disk, without asking. Keep a copy of any mission you
|
|
care about under a different name.
|
|
- **`Apply` only pushes the six mission settings** (map, time, weather, scenario, temperature,
|
|
length). Callsign / mech / colour / badge / patch / experience edits need **Save**. Changing the
|
|
**seat count** mid-session is refused outright — that needs a session restart.
|
|
- **The Colour dropdown offers colours the game cannot paint.** Only Black, Brown, Crimson, Green,
|
|
Grey, Tan and White work; anything else silently comes out grey. The relay warns in the log.
|
|
- **If the relay dies on its own**, `Launch local instances` stays clickable and will start clients
|
|
against nothing. Press Start Session first.
|
|
- **`Public host`** is used only when exporting player scripts; it does not affect what the relay
|
|
binds or advertises.
|
|
|
|
## 7. When a player leaves
|
|
|
|
Their pilot light goes grey (**waiting**) and the seat's callsign reverts to whatever you had
|
|
configured for that row — so a free seat no longer looks occupied by a departed player.
|
|
|
|
**Their seat is held for them for 90 seconds.** That is deliberate: someone who crashes or gets
|
|
dropped comes straight back into the same seat with the same mech. During that window a *brand-new*
|
|
player is given the next free seat instead, or told the roster is full if there wasn't one. After 90
|
|
seconds the vacated seat is fair game for anybody.
|
|
|
|
So if a new arrival can't get in right after somebody left, wait a minute and have them retry — or
|
|
raise the seat count if you're simply out of seats.
|
|
|
|
## 8. Troubleshooting
|
|
|
|
| What you see | What it means | What to do |
|
|
|---|---|---|
|
|
| `egg HELD (n/N pods present)` | normal staging — waiting for the rest of the seats | wait, or press LAUNCH to start with who is here |
|
|
| LAUNCH greyed out after a round | the launcher is still holding the last round | press **↻ Re-arm** |
|
|
| `LAUNCH pressed but NO players are seated yet` | nobody has claimed a seat | check the players are pointed at the right address/port |
|
|
| `LAUNCH pressed but NOT all seats have ACKed` | some pods are still taking the mission file | wait a few seconds; if one is never coming, press **Re-arm** (between rounds only — it is refused mid-launch/mid-mission) |
|
|
| `launch HELD -- still loading: PLAYER n` | that pod has not finished loading | wait; it force-starts after 3 minutes |
|
|
| `*** WARNING: ... EMPTY seat(s) ... the mission will STALL` | **can be a false alarm** after the roster is trimmed | if the round starts and plays fine, ignore it |
|
|
| `seat RECLAIMED by identity` | a player who dropped got their seat back | nothing — this is working as intended |
|
|
| `game[...] dropped: closed` | a pod disconnected | normal at round end; mid-round it means they crashed or lost connection |
|
|
| A player gets **ROSTER FULL** | no free seat in the current round | Re-arm between rounds so the roster re-opens, then let them join |
|
|
| Console window stops responding | the relay stalled on a dead connection | Stop Session, Start Session; please keep the log (below) |
|
|
| A player says they cannot connect at all | ports, or wrong address | confirm 1500/1501 TCP + 1501 UDP reach this machine |
|
|
|
|
### The one thing to do before restarting a session
|
|
|
|
**Copy `content\operator_relay.log` somewhere first.** Start Session **overwrites** it — so
|
|
restarting to clear a problem also destroys the evidence of that problem. If something misbehaved,
|
|
save that file and hand it over; it is timestamped line-by-line and it is what makes these bugs
|
|
diagnosable.
|
|
|
|
---
|
|
|
|
## 9. Match reports
|
|
|
|
At the end of each clean round, every player's client **uploads its own match report to you
|
|
automatically** (relay mode only). They land in `matchlogs\` named by the sender's address. You do
|
|
not need to ask players to send anything.
|
|
|
|
Exceptions worth knowing: a client that **crashes mid-round** loses its report, and players on
|
|
**solo** or **Steam** have no relay to upload through. In those cases ask them for
|
|
`content\matchlog_*.txt` directly. Their crash log — `content\join_YYYYMMDD.log`, one file per day
|
|
holding every session (plus the tiny `content\lastrun_<mode>.txt`) — is **never** uploaded — ask
|
|
for it explicitly when something went wrong on their end.
|
|
|
|
---
|
|
|
|
## 10. Local test instances
|
|
|
|
**Launch local instances** starts game clients on this machine against your own session — useful
|
|
for testing alone or filling a seat. **Stop games** closes them. In relay mode the session must
|
|
already be started, or a local instance has nothing to dial.
|
|
|
|
---
|
|
|
|
## 11. Which build everyone needs
|
|
|
|
**All pods must run the same build as each other.** Build **4.11.554** changed what pods tell each
|
|
other about scores, so a mixed session shows disagreeing KILLS/DEATHS columns (nothing crashes —
|
|
the remote numbers just do not move). When you hand out a new zip, make sure everyone takes it
|
|
before the session.
|