Files
BT411/context/steam-networking.md
T

92 lines
6.2 KiB
Markdown

---
id: steam-networking
title: "Steam networking — the BT_STEAM wire (transport + lobby)"
status: living
source_sections: "docs/GLASS_COCKPIT.md §4; docs/STEAM_TEST.md; commits e79e8fa/48ede2f/f703bb1"
related_topics: [glass-cockpit, multiplayer]
key_terms: [PadRIO, miniconsole]
open_questions: ["live 2-account cross-machine session (docs/STEAM_TEST.md) — the step-4 exit criterion"]
---
# Steam networking — the BT_STEAM wire
Internet MP for the glass cockpit (dev tooling; gate `BT_STEAM`, requires `BT_GLASS`).
**Status: CODE COMPLETE 2026-07-18; single-machine verified; live 2-machine test pending.**
## The three layers
1. **The wire seam** (`L4NET.CPP`, always compiled): file-local `BTNetSend/Recv/Accept/Close`
wrappers around the 9 raw Winsock sites — pure code motion when OFF; under `BT_STEAM` each
offers the op to the transport first. Est. delegates: `CheckSocket` (peer address),
`OpenConnection` TCP_OPEN head, `GetMyAddress` (self token).
2. **The transport** (`engine/MUNGA_L4/L4STEAMNET.*`, gated): `ISteamNetworkingSockets`
pseudo-SOCKETs (`0x5EA0xxxx`); engine TCP byte stream over reliable-NoNagle messages with
per-connection reassembly rings (recv: empty→`WSAEWOULDBLOCK`, closed→0); listen channels
console=0 / game=1 (`CreateListenSocketP2P`); install on env `BT_STEAM_NET=1`; degrades to
Winsock at every failure. Pump = `SteamAPI_RunCallbacks` AND sockets `RunCallbacks` (the
general dispatch carries the async results — sockets-only pumping stalls).
3. **The lobby** (`game/glass/btl4lobby.*`, gated): ISteamMatchmaking room (`btl4=1` list
filter); pilots publish name/mech/color; the host's GO mints the roster and signals.
## THE TOKEN MODEL (the FakeIP lesson) [T2]
FakeIP does NOT survive the per-mission process relaunch (live-disproven prior-art assumption:
menu and mission processes got different FakeIPs; the pod timed out dialing its own stale
address). Roster addresses are therefore **opaque ipv4-shaped tokens**`169.254.77.N`, port
1501=console / 1502=game — minted by the HOST at GO and mapped to **Steam identities**;
connections ride `ConnectP2P(identity, channel)`. Handoff to mission processes via env:
`BT_FE_MYFAKE` (own token → `GetMyAddress` self-match) + `BT_FE_STEAMMAP`
(`ip=steamid64;...`). The engine's roster matching, Waterloo swap logic, and self-identification
all run UNCHANGED on tokens.
## Testing
Single-machine (verified): lobby-of-1 GO → token map → egg roster token → mission runs over the
stock console ladder. Live 2-machine procedure + checklist: `docs/STEAM_TEST.md`. AppID 480
(Spacewar) via auto-written gitignored `content/steam_appid.txt`.
## FIRST REAL MULTI-PLAYER VERIFICATION — playtest night 4 (2026-07-27, 4.11.584) [T2 live]
Players ran **three back-to-back Steam lobbies, host-and-join, 3+ players**, with no operator
console involved — the first genuine field exercise of this path. **What worked:** hosting and
joining (Draco, SAURON, somebodee, RajelAran all succeeded; "steam works for self-hosting? neat"),
mission launch, **damage application** (previously suspected dead over Steam — "panels were
changing color", "I was damaged"), **2 kills in one round**, clean round end, and the next mission
launching after it. SAURON: "3 missions ran smooth ... felt smooth and normal". Load after LAUNCH
felt "a bit long" (unquantified).
**Found, and filed:** #68 (a failed JOIN/HOST exits the process silently — the FE returns on any
`BTLobby_JoinAndWait` failure, so "Steam not running" and a real crash look identical to a player).
Everything else the night surfaced was NOT Steam-specific and is tracked in the general issues:
#67 (replicant torso twist / muzzle placement), #70 (twist after respawn), #55 (heat readout not
reset on death), #69 (Sunder V1 → Denkou naming).
**Lesson for triage:** a bug found in a Steam lobby is a Steam bug only if it lives in the lobby /
token / transport layer. Everything above the seam is the same arcade mesh — the three replication
symptoms this night produced would all reproduce on the relay.
## THE EXIT CONTRACT (#163 -> #170, settled 2026-08-12) [T2 field-proven]
Process-exit topology for every glass/steam process: **flush -> quiesce -> TerminateProcess.**
`BTSteamNet_ShutdownAll` (L4STEAMNET.cpp) is a QUIESCE: gate `steamActive` FIRST (stops
marshal/game-thread seam re-entry), `CloseConnection(..., linger=true)` on every connection
("round over", not a peer-side timeout), close listeners, one `sockets->RunCallbacks()` drain,
`Sleep(60)` grace. **It deliberately never calls `SteamAPI_Shutdown()`, and no exit path may
return through the CRT** (menu-quit included -- it dies via `BTOrderlyDie`, btl4console.cpp).
The two-failure history, because each fix's reason constrains the other:
- **883 (no teardown at all):** bare `ExitProcess` ran DLL_PROCESS_DETACH with live SNS threads
-> intermittent loader-lock deadlock -> "frozen view of how the game ended" + the menu child
hung in `SteamAPI_Init` against the wedged half-dead sibling (#163, night15).
- **913 (full `SteamAPI_Shutdown` before TerminateProcess):** at round end every peer closes
simultaneously, so status events are ALWAYS in flight; Shutdown tears client state down while
steamclient's own service thread dispatches them -> that thread calls literal NULL
(0xc0000005 access=8 target=0x0, BaseThreadInitThunk root, ZERO btl4 frames) in the window
before TerminateProcess (#170, night16: 7 hits / 3 machines / one module-relative stack).
- **The stable point:** TerminateProcess kills all threads atomically with every pointer still
valid (no detach, no dispatch window); the Steam client GCs the session on IPC pipe break --
the standard crashed-game path (883 relaunched all night with no Shutdown ever called). The
child-hang needed a WEDGED sibling, which TerminateProcess makes structurally impossible.
Forensics rider: the crash filter (btl4main.cpp `BTCrashFilter`) now prints foreign frames as
`<module.dll>+0xOFFSET` (VirtualQuery AllocationBase + GetModuleFileName), so a future
foreign-thread crash names its DLL instead of an unresolvable ASLR address.
## Key Relationships
- Rides: [[glass-cockpit]] miniconsole (marshal has a Steam-wire branch) · Extends:
[[multiplayer]] (the arcade mesh, unchanged above the seam)