the symptom, in your words

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

✓ verified on 26.07.15a
lane 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