Audit of what actually landed in context/ versus what only ever got said in
chat. Five gaps and one stale claim repeated in four files.
New env gates were the biggest hole -- BT_TORSO_LOG, BT_MYOMERS_LOG,
BT_MYOMERS_REPAIR_TEST, BT_SELF_DAMAGE and BT_POWER_DETACH_TEST were all in the
code and none of them in decomp-reference §6, which is supposed to be the hub.
Added with the reasoning that makes them usable: why the myomers probe samples
every frame while unpowered (the NoVoltage window is about a second and a 1 Hz
probe steps straight over it), why the repair test also forces Manual (or
AutoConnect restores power a frame later), why SELF_DAMAGE latches off at the
first death, and why DETACH_TEST taking a NAME is what proves failover rather
than a same-generator re-attach.
The 6am log day was in the code and the player bats but not the KB. Now in
build-and-run with the stem table, the unconditional append, the 8 MB part
roll-over and the BT_LOG-truncates trap, plus the operator-facing note in
operator-console and OPERATOR_GUIDE that you ask for the NEWEST log, never
"today's".
Recorded the crash-symbolization gap, which is the one with teeth. BTCrashFilter
writes a stack into the day log and its own comment claims "we hold the PDB". We
do not: no Release PDB is produced at all, and /O2 implies /Oy so the EBP walk is
unreliable anyway. When the Owens crash finally lands we get an address we cannot
resolve. Written down with the fix (/Oy-, /Zi + /DEBUG, archive the PDB per
build) rather than left as something I mentioned once.
New gotcha 22: HeatModelOff() reads simulationState == 1, i.e. "am I destroyed",
and has nothing to do with the heat model. Behaviour at the call sites is right,
the name is not, and while chasing #70 it reads as "novice pilots cannot twist"
and sends you hunting an experience-level bug that does not exist. Same misnomer
in torso/gyro/sensor headers.
Also documented the dist flavor trap, having nearly shipped it myself: mkdist
reads build/CMakeCache.txt, so a tree configured BT_STEAM=OFF silently produces
a -nosteam zip with no play_steam.bat and no steam_api.dll. OFF is the CMake
default; ON is the documented dev state.
Swept the stale `btl4.log` filename out of reconstruction-gotchas,
reconstruction-method, build-and-run and CLAUDE.md itself -- the log has been
<stem>_YYYYMMDD.log since 1777d5a and every "read btl4.log" instruction was
pointing at a file that is no longer written.
checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
267 lines
14 KiB
Markdown
267 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. Ask for the **newest** `<stem>_*.log`
|
|
rather than "today's": the log day runs **06:00 → 06:00**, so a session after midnight is still
|
|
filed under the evening it started. Always ask for `lastrun_<mode>.txt` too, **even when they say
|
|
there is no log** — that combination is how a launch blocked before `WinMain` is identified.
|
|
|
|
---
|
|
|
|
## 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.
|