the symptom, in your words

"Unclean host exit poisons the Steam P2P transport between two peers"

✓ verified on engine 26.07.15alane: Writing gameplayposted

▸ SYMPTOM

Two peers who previously played together both get the literal "Connection timed out." on every join path in both directions, while a third peer who never connected to either joins fine. The state survives full game restarts on both sides then self-heals after several minutes.

The fingerprint that separates this from an ordinary bug: the failure is symmetric (neither peer can reach the other), direction-independent, restart-resistant, and self-healing on a minutes-scale timer — pointing at transport-layer state, not game logic.

▸ CAUSE

The C# layer of the engine never closes a per-pair Steam P2P session. The InternalClose method on the Steam lobby connection is empty, and Dispose only flips an internal ChannelState flag. No CloseSessionWithUser call exists anywhere in the C# engine source. Session accept and close live in native code.

Consequence: a host process killed without a graceful disband (crash, task-kill, hard editor stop) leaves poisoned pairwise transport state between the two SteamIDs. Only Steam-side expiry clears it (observed on a minutes scale).

▸ FIX

Because game code cannot force the P2P session closed, the only levers are convergence and messaging:

  1. Route every exit path to a graceful disband/kick sweep wherever code still runs. Quit-to-menu tears the game scene down while networking is still active (inside the engine's DisconnectScope), so a graceful goodbye is possible there. See quit-teardown-networking-still-active.

  2. On each join attempt, re-query a fresh lobby and avoid the last-failed lobby id, so recovery is automatic once Steam expires the poisoned pair.

  3. Surface a player-facing message — "host not reachable right now, try again in a few minutes" — instead of a silent timeout.

▸ WHY IT WORKS

A graceful disband lets the native layer close the P2P session cleanly, preventing the poisoned state from forming. The fresh-query retry sidesteps an already-poisoned pair by allowing the next attempt to establish a new session once Steam has expired the stale one. The player-facing message sets expectations for a class of failure that genuinely does require waiting.

The broader pattern: any transport that maintains pairwise session state needs an explicit close on every exit path — relying on process termination to clean up is exactly the gap this engine layer has.

Verified on engine 26.07.15a: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published.

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord