Files
BT411/docs/OPERATOR_GUIDE.md
T
arcattackandClaude Opus 5 ab5828a866 relay/console: fix the three remaining hardening items from the review
1. UDP ENDPOINT HIJACK.  `from_host` in a UDP envelope is the sender's OWN claim,
   and the relay used it directly to refresh that host's downstream endpoint --
   so ANY datagram claiming host N silently stole host N's traffic (a zombie pod
   from a previous round, a stale NAT mapping, or anyone who guessed a host id).
   The victim simply stopped receiving on a channel that still looked healthy.
   The claim is now bound to the identity we actually authenticated: the IP of
   that host's live TCP game connection.  The PORT is deliberately not checked --
   it moves on a NAT rebind, which is the whole reason the endpoint map refreshes
   per datagram -- and a genuine IP change cannot happen without the TCP
   connection breaking and re-registering, so a legitimate pod is never rejected.
   Rejections are counted (udp_spoofed) and logged once per offending (host, IP).
   Known limit: two pods on one machine share an IP, so this cannot separate
   them; same-machine trust is assumed.

2. THE EGG ACK COULD BE MISSED ENTIRELY -- a silent, unrecoverable wedge.  The
   pod's ACK is the launch gate's ONLY signal, and it was detected by parsing a
   single recv() at fixed offset 0 behind a `len(data) >= 24` test.  On a real
   network (loopback hid both cases) the 28-byte ACK arriving SPLIT -- 16 bytes
   then 12 -- was dropped by that test and never looked at again, and two
   COALESCED messages meant only the first was read.  A missed ACK means that
   seat never counts toward the gate, so the round can never be released.
   _console_read now buffers per connection and walks every complete frame
   (16 + messageLength); an implausible length drops the connection WITH A REASON
   rather than mis-reading that pod all night, since a stream protocol cannot be
   resynced by guessing.  Bounds: CONSOLE_MSG_MAX 64K, CONSOLE_INBUF_MAX 1 MiB.

3. REMOTE-OPERATOR MODE WAS HALF A CONSOLE.  LAUNCH and END MISSION could never
   enable (both conditions required `console_proc is not None`, which the remote
   path never sets), the pilot lights were permanently blank (SessionMonitor was
   built with an empty tag list, so `seated` was always 0), and Launch-local was
   refused by the same guard even though the code below it already built
   BT_RELAY from the remote host.  All three enable conditions now accept EITHER
   channel, and the monitor ADOPTS THE ROSTER from the relay's
   "roster: N pilot(s) -> hostIDs [...]: [...]" line -- which the relay replays to
   every newly AUTHed operator -- so a remote operator gets real lights and a
   real seat count.

VERIFIED
  * scratchpad/test_relay_net.py (new, 17 checks): spoofed datagram cannot move
    an endpoint and is counted; a NAT rebind on the same IP still is honoured; an
    unregistered host id still dropped; the ACK is found whole, split in two,
    split three ways mid-header, and when hiding behind another message;
    a garbage length drops the conn; the roster line is adopted, maps a
    subsequent SEATED onto a real tag, lifts `seated` off 0, and re-feeding it
    preserves known state.
  * Live 2-pod rig: real pods ACKed through the new reassembly path (2/2 ready,
    zero desync drops), the mission launched and ran, UDP flowed throughout
    (93 rx / 85 tx, udp-known [2,3]) with ZERO false spoof rejections, then a
    clean StopMission + round RESET.
  * scratchpad/test_relay_rearm.py still 19/19; checkctx CLEAN.

Docs updated to match (context/operator-console.md gains reassembly and
anti-hijack sections; the guide no longer claims remote mode is crippled).
Remaining open items are now recorded in the topic's frontmatter: mesh mode's
operator buttons are inert by construction (no stdin reader in that path -- left
alone, mesh self-launches), _log_launch_readiness can cry STALL on a healthy
launch-with-whoever, and a straggler's late FIN is still misread as a pod dying
mid-load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:35:09 -05:00

9.4 KiB

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:

waitingseated (claimed a seat, callsign and mech shown) → egg sentregisteredready (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.

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.

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. 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
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.


8. 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.log, is never uploaded — ask for it explicitly when something went wrong on their end.


9. 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.


10. 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.