Host cannot accept Steam player after reconnect (e.g. Fix & Restart) - #958
Host cannot accept Steam player after reconnect (e.g. Fix & Restart)#958romangr wants to merge 6 commits into
Conversation
…e-prompt The P2P session request handler only removed a Steam ID from session.pendingSteam on accept. A rejected (or disconnected-before-accept) player left a stale entry, and the guard then skipped the handler on every reconnect, leaving the host without a prompt and the player stuck "waiting for host to accept". Clear the entry on reject and scope the dedup guard to the prompt enqueue so pendingSteam membership tracks exactly an unanswered prompt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011htpmJM4qmJhthgQ9djDWT
SteamBaseConn.OnClose previously left the Steam P2P session open (TODO), so after a client left, the host kept a half-open session with their Steam ID. On reconnect Steam reused that session instead of posting a fresh P2PSessionRequest_t, so the host never got a new accept prompt and the client sat on "waiting for host to accept". Free the session via CloseP2PSessionWithUser in OnClose, and also in SteamServerConn's P2P timeout/error path, which tears the connection down through SetDisconnected without going through OnClose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011htpmJM4qmJhthgQ9djDWT
When a joiner reconnects before the host has torn down their previous connection (a fast rejoin, before the Steam P2P timeout fires), their new join packet arrives on the still-open session and previously hit the "shouldn't happen" branch in SteamP2PNetManager.Tick, where it was dropped. The client never resends its join packet, so the joiner stayed stuck on "waiting for host to accept". Treat a join packet from an already-known remote as a reconnect: drop the stale player via SetDisconnected and fall through to the normal accept path. SetDisconnected (not Close) is used deliberately so OnClose -> CloseSteamSession doesn't tear down the shared P2P session the new join just arrived on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011htpmJM4qmJhthgQ9djDWT
Closing the session in OnClose right after queueing the goodbye dropped it: SendP2PPacket only queues, and CloseP2PSessionWithUser discards queued unsent packets. Clients never received server-initiated disconnect reasons over Steam (wrong password, kick, username taken) and fell back to a generic timeout. Keep the immediate close for the no-goodbye paths, but when a goodbye was sent, defer CloseP2PSessionWithUser by 3 seconds via Task.Delay + server.Enqueue so it runs on the server thread after Steam has flushed the packet. Skip the deferred close if a connection with the same remoteId exists in playerManager.Players by then: the old player was already removed, so any match is a fast rejoin riding the same underlying session, and closing would tear it down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@notfood please take a look |
|
I discussed it with Mibac, a more robust approach is to close any stale connection upon the arrival of a new client sharing the same identification clues. This logic can be applied uniformly, including to non‑Steam clients. |
|
Thanks — happy to go that way. Before I write it, one trade-off I'd rather have your call on, since the "identification clues" differ in how much we can trust them. Steam gives us a
Worth weighing against how much it buys for direct clients: a reconnecting direct client gets its own Either way I'd extract the policy into one Separately, one thing I found that keeps the Steam-side code needed regardless: Which way do you and Mibac want the username case? |
|
Direct connections should allow for extra validation checks, such as verifying the originating IP address, information we already have and can potentially test against. Steam's reuse of the connection is acceptable, as the identifier remains unique throughout the process. |
The Steam reconnect fix lived entirely in SteamP2PNetManager.Tick, which left no way to apply the same rule to other transports. Review on rwmt#958 asked for it to be uniform. Add ConnectionBase.RemoteIdentity, identifying the remote endpoint, and CloseReplacedBy, which closes a connection being superseded. Steam overrides it to close nothing when the replacement shares its P2P session, so the knowledge that a replacement can ride the stale connection's own session stays in SteamBaseConn rather than being asked about from outside. PlayerManager.ReplaceStale then just tells the stale connection to close and forgets the player. Tick now delegates to ReplaceStale. Creating the connection before the stale player is dropped also removes the joinPacket/player == null guard, which no longer had to encode "a reconnect looks like a new connection". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A player reconnecting before the server reaped their old peer was turned away with UsernameAlreadyOnline until it timed out. Replacing on a username match alone would let anyone holding your name evict you from a passwordless server, so match on two factors instead: the username says who, and the remote address corroborates the origin. LiteNetConnection reports peer.Address as its RemoteIdentity. A match replaces the stale connection; anything else falls through to the existing rejection, so no case gets worse than before. The address is corroborating evidence only, never an identity on its own: players behind one NAT share it, and a player whose address changed won't match. Steam needs no second factor because a CSteamID is unique per user. Tests cover both directions, standing in for the stale connection with a RecordingConnection at a chosen address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Went with the IP check you suggested. A direct join now replaces the connection holding the username only when it also comes from the same address. Both paths go through a single PlayerManager.ReplaceStale. Tests cover a direct join from the same address replacing the stale connection, and one from a different address being turned away. |
|
|
||
| public abstract void OnError(EP2PSessionError error); | ||
|
|
||
| // A goodbye is only ever non-null server-side, and CloseP2PSessionWithUser discards queued unsent |
There was a problem hiding this comment.
CloseP2PSessionWithUser discards queued unsent
Since this seems to be a Steam thing, should let them know about this, otherwise nothing gets fixed and workarounds keep getting needed.
Fix Steam P2P fast rejoin
Fixing #843
Fast rejoin dropped
When a joiner reconnects before the host has torn down their previous connection (before the Steam P2P timeout fires), the new join packet arrives on the still-open session and hit the "shouldn't happen" branch in SteamP2PNetManager.Tick, where it was dropped. The client never resends its join packet, so the joiner stayed stuck on "waiting for host to accept".
Now a join packet from an already-known remote is treated as a reconnect: the stale player is dropped via SetDisconnected and we fall through to the normal accept path. SetDisconnected is used instead of Close so OnClose → CloseSteamSession doesn't tear down the shared P2P session the new join just arrived on.
Disconnect reasons delivered
Closing the session in OnClose immediately after queueing the goodbye dropped it: SendP2PPacket only queues, and CloseP2PSessionWithUser discards queued unsent packets. Clients never received server-initiated disconnect reasons (wrong password, kick, username taken) and fell back to a generic timeout.
The immediate close is kept for the no-goodbye paths. When a goodbye was sent, CloseP2PSessionWithUser is deferred by 3 seconds via Task.Delay + server.Enqueue so it runs on the server thread after Steam has flushed the packet. The deferred close is skipped if a connection with the same remoteId exists in playerManager.Players by then — the old player was already removed, so any match is a fast rejoin riding the same underlying session, and closing would tear it down.
Testing