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>
192 lines
9.4 KiB
Markdown
192 lines
9.4 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.
|
|
|
|
**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.
|