GameServer: second login on the same ship leaves the first client as a zombie session (both fly "alone") #981

Closed
opened 2026-08-06 19:29:43 +00:00 by spikerj · 1 comment
Owner

Symptom

Two clients (e.g. the Godot space game and the iOS game) logging into the same account and same ship both enter the world and can fly around independently — but neither sees the other. Expected: only 1 ship per login in the world at a time, with the existing 30-second disconnect grace period.

Root cause

BaseZone.AddPlayerAsync / SpaceZone.AddPlayerAsync implement a same-character "takeover": the second login removes the first connection's player entry, pending-event queue, and ship entity, and reuses its EntityId. But the takeover never closes or notifies the old WebSocket:

  • The old client stays connected and authenticated; the server drops all its zone commands (no _players entry) and sends it no world updates (its event queue is gone). It keeps flying via client-side prediction in a frozen world.
  • The new client owns the single real entity. Since there is only one entity and each client hides its own EntityId as the local player, the two clients never see each other.
  • ZoneManager._connectionToZone keeps a stale mapping for the old connection, and its play-time session is never finalized until the socket eventually dies on its own.

The 30s grace period (PlayerEntity.DisconnectGracePeriodSeconds) only applies to sockets that actually close (soft-disconnect path), so the takeover bypassed it entirely.

Fix

On takeover, force-close the displaced connection's WebSocket (close reason: "Logged in from another location"). The socket close then routes through the normal disconnect path, which finalizes the play-time session and cleans the connection-to-zone mapping. Applies to all zone types (space, camp, dungeon, voxel), since the takeover lives in BaseZone.

Clients need no changes — the displaced client receives a clean close with the reason in the close frame.

## Symptom Two clients (e.g. the Godot space game and the iOS game) logging into the **same account and same ship** both enter the world and can fly around independently — but neither sees the other. Expected: only 1 ship per login in the world at a time, with the existing 30-second disconnect grace period. ## Root cause `BaseZone.AddPlayerAsync` / `SpaceZone.AddPlayerAsync` implement a same-character "takeover": the second login removes the first connection's player entry, pending-event queue, and ship entity, and reuses its EntityId. But the takeover **never closes or notifies the old WebSocket**: - The old client stays connected and authenticated; the server drops all its zone commands (no `_players` entry) and sends it no world updates (its event queue is gone). It keeps flying via client-side prediction in a frozen world. - The new client owns the single real entity. Since there is only one entity and each client hides its own EntityId as the local player, the two clients never see each other. - `ZoneManager._connectionToZone` keeps a stale mapping for the old connection, and its play-time session is never finalized until the socket eventually dies on its own. The 30s grace period (`PlayerEntity.DisconnectGracePeriodSeconds`) only applies to sockets that actually close (soft-disconnect path), so the takeover bypassed it entirely. ## Fix On takeover, force-close the displaced connection's WebSocket (close reason: "Logged in from another location"). The socket close then routes through the normal disconnect path, which finalizes the play-time session and cleans the connection-to-zone mapping. Applies to all zone types (space, camp, dungeon, voxel), since the takeover lives in `BaseZone`. Clients need no changes — the displaced client receives a clean close with the reason in the close frame.
Author
Owner

Resolved in spikersoft-backend PR #524 (merged to master). BaseZone now force-closes the displaced connection's WebSocket on same-character login takeover ("Logged in from another location"), routing it through the normal disconnect path (play-time finalize + connection-to-zone cleanup); SetConnectionManager hoisted to BaseZone and wired for all zones. Full GameServer suite 1,825 green + 2 new takeover tests. Known pre-existing limitation noted in the PR: the duplicate scan is per-zone (ships are fully covered — single space zone). Closing.

Resolved in spikersoft-backend PR #524 (merged to `master`). BaseZone now force-closes the displaced connection's WebSocket on same-character login takeover ("Logged in from another location"), routing it through the normal disconnect path (play-time finalize + connection-to-zone cleanup); `SetConnectionManager` hoisted to BaseZone and wired for all zones. Full GameServer suite 1,825 green + 2 new takeover tests. Known pre-existing limitation noted in the PR: the duplicate scan is per-zone (ships are fully covered — single space zone). Closing.
Sign in to join this conversation.