Skip to content

Remove player entities and connected state stranded after an abrupt disconnect - #1385

Open
GeekOfWires wants to merge 3 commits into
psforever:masterfrom
GeekOfWires:fix/stuck-player-entities
Open

Remove player entities and connected state stranded after an abrupt disconnect#1385
GeekOfWires wants to merge 3 commits into
psforever:masterfrom
GeekOfWires:fix/stuck-player-entities

Conversation

@GeekOfWires

Copy link
Copy Markdown
Contributor

Summary

Players sometimes remain present in the game world, or keep counting as online, after they are no longer connected — most often following a client crash. It shows up as a corpse left on the ground, a player standing in a sanctuary respawn tube, occasionally a body on a continent, or as a "connected" player with no visible entity at all. These persist until the server restarts.

The cause is in how a PersistenceMonitor tears a player down in PerformLogout (AccountPersistenceService.scala). This PR fixes two independent gaps there and adds visibility to a third.

1. Logout looked only in the last-recorded zone

PerformLogout located the player via inZone.Players / inZone.AllPlayers, where inZone is the zone last reported through an Update heartbeat. On an abrupt disconnect this can be stale — a crash during/after a zone transfer, or right after a respawn before the next Update landed — so the live avatar/corpse actually resides in a different zone. The name lookups then find nothing, execution falls through to the "user stalled / caught between zone transfer" no-op, and the entity (with its GUID registration and zone population membership) is stranded until restart.

Now, if the player isn't present in inZone, all zones are searched and inZone is re-pointed to wherever they actually are, so the existing four-case logout logic (in-vehicle / standing / released / absent) runs against the correct zone.

2. A body-less connected player was never cleared from the online registries

A player who released, whose corpse then decayed, and who was sitting at the deployment map when the client dropped, has no player body in any zone — so even the all-zones search above finds nothing and reaches the same no-op. But LivePlayerList.Remove and the squad/galaxy/population departures live only inside AvatarLogout, so the account stays listed as online (in /who, the PSFPortal, squad, galaxy) indefinitely, with no visible entity — a "phantom connected".

The no-op branch now resolves the avatar from LivePlayerList by name and, if it is still present, runs AvatarLogout to clear that residual connected state.

3. GUID unregister failures were silent

The GUIDTask.unregisterPlayer / unregisterObject(locker) tasks were fire-and-forget; a failure silently leaked the GUID and left the ObjectDelete incomplete until restart. Their futures are now observed and failures logged at error level so the condition is visible.

Each corrective branch also logs a warn when it triggers (stale recorded zone, or a phantom-connected cleanup), making these conditions observable in the wild.

Scope

Deliberately limited to PerformLogout and its helpers. Related hardening left for follow-ups:

  • sending AccountPersistenceService.Logout from the socket-death path so cleanup fires promptly on disconnect rather than after the ~60s inactivity timer;
  • context.watch / Terminated in AccountPersistenceService so a monitor that dies abnormally still prunes the accounts map;
  • unregistering avatar.locker in its own zone when a transfer split the body and locker across zones.

Testing

  • sbt Compile/compile succeeds.
  • Runs on a local Docker server (Flyway migrations apply, login + world session establish).
  • Behavioral effect not yet confirmed against a live client. The intended outcome — stranded entities and phantom-connected accounts clearing within ~60s of an abrupt disconnect instead of persisting until restart — has been reasoned from the code path but not yet reproduced end-to-end. A targeted crash test in each state (standing on a continent, released at the deploy map, mid-transfer) is worth doing before merge.

GeekOfWires and others added 3 commits July 20, 2026 19:32
A PersistenceMonitor removes a player's world entity via PerformLogout,
which keyed the lookup solely off `inZone` -- the zone last reported
through an Update heartbeat. On an abrupt disconnect (a crash during or
after a zone transfer, or right after a respawn before an Update landed)
`inZone` is stale: the live avatar or corpse actually resides in a
different zone. The name lookups then find nothing, execution falls
through to the "user stalled / caught between zone transfer" no-op, and
the entity -- along with its GUID registration and zone population
membership -- is stranded in the world until the server restarts.

This is the mechanism behind stuck player entities after crashes: a
corpse left on the ground, a player standing in a sanctuary respawn tube,
or occasionally a body on a continent.

Before cleaning up, if the player is not present in `inZone`, search all
zones for wherever they actually are and re-point `inZone` there, so the
existing four-case logout logic (in-vehicle / standing / released /
absent) runs against the correct zone. When the player is found nowhere
(genuinely gone), behavior is unchanged and it falls through to the
existing no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ister failures

Two follow-on gaps in PerformLogout beyond the stale-zone lookup:

A body-less connected player -- released, corpse decayed, sitting at the
deployment map when the client dropped -- has no player body in any zone,
so even the all-zones search finds nothing and execution reaches the
"user stalled" no-op. But LivePlayerList.Remove and the squad/galaxy/
population departures live only inside AvatarLogout, so the account stays
listed as online (in /who, the portal, squad, galaxy) until a restart,
with no visible entity. The no-op branch now resolves the avatar from
LivePlayerList by name and, if still present, runs AvatarLogout to clear
that residual connected state.

The GUID unregister tasks (unregisterPlayer, unregisterObject for the
locker) were fire-and-forget; a failure silently leaked the GUID and left
the ObjectDelete incomplete until restart. Their futures are now observed
and failures logged at error level so the condition is visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reword the comments added by this branch so they describe the behaviour
of the code as it now stands, rather than contrasting it with the code it
replaced. A reader arriving at these files has no view of the previous
implementation, so that framing carries no meaning for them; the
before-and-after reasoning belongs in the commit messages, where it
already is.

No behavioural change: comments only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@GeekOfWires

Copy link
Copy Markdown
Contributor Author

Addressed the review note about comments describing the change rather than the current behaviour. Pushed as Improve comment etiquette for our previous work — comments only, no behavioural change, build verified before committing.

Small footprint here: 1 file / 9 lines, both blocks in AccountPersistenceService.

  • The inZone block explained that keying the lookup solely off a stale inZone "finds nothing, falls through to the no-op case below, and strands the entity" — a description of the path the code no longer takes. It now leads with what the code does: locate the entity across all zones and re-point inZone at wherever it actually is before proceeding, so the logout acts on it rather than leaving it holding a GUID and population membership until restart.
  • The body-less logout block was phrased around what would happen without it ("otherwise they would linger as connected"). It now states the condition it handles — no live body in any zone while the account is still registered as connected, e.g. released, corpse decayed, sitting at the deployment map when the client dropped — and what it clears.

The failure modes being guarded are still documented, since those remain useful to a reader; only the contrast with the prior implementation was dropped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant