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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.AddPlayerAsyncimplement 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:_playersentry) and sends it no world updates (its event queue is gone). It keeps flying via client-side prediction in a frozen world.ZoneManager._connectionToZonekeeps 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.
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);SetConnectionManagerhoisted 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.