Skip to content

Command /offlevel leaks the real player from the player list: online count stays inflated ("phantom online") after offline leveling starts #804

@nolt

Description

@nolt

Component: src/GameLogic/Offline/OfflinePlayerManager.cs, src/GameLogic/Player.cs (offline-leveling feature, bdc15b2 / PR #710)

Summary:
Starting an offline-leveling session via /offlevel calls Player.SuppressDisconnectedEvent(), which sets the entire PlayerDisconnected event to null. This removes not only the intended GameServer.OnPlayerDisconnectedAsync subscriber (to avoid a double save / double login-server logoff) but also GameContext.RemovePlayerAsync. RemovePlayerAsync is the only place that removes the player from _playerList, decrements PlayerCounter, and clears PlayersByCharacterName. Because it never runs, the real player object is never removed from the player list, so the online/connection count stays permanently inflated until a server restart. Each /offlevel leaks one entry.

Steps to reproduce:

  1. Note the current online count (e.g. 1, just you).
  2. Use /offlevel a few times (relog in between).
  3. Observe the website/server status shows several players online (e.g. 4) while only one — or zero — are really connected. The count never drops back.

Expected:
After /offlevel, the real player is fully removed from the world: gone from the map and removed from _playerList / PlayerCounter / PlayersByCharacterName, so the online count reflects reality. The ghost (OfflinePlayer) is the only remaining object.

Actual:
The real player is removed from the map (via InternalDisconnectAsync → currentMap.RemoveAsync) but stays in _playerList, so it counts as online forever. This is why the disconnected character is invisible on the map yet still shows up in the online count ("phantom online").

Root cause:
SuppressDisconnectedEvent() in src/GameLogic/Player.cs (line ~1506):
public void SuppressDisconnectedEvent()
{
this.PlayerDisconnected = null; // nulls ALL subscribers, including RemovePlayerAsync
}
Called from OfflinePlayerManager.TransitionToOfflineAsync (line ~105) before DisconnectAsync(). Both GameContext.RemovePlayerAsync (subscribed in AddPlayerAsync, GameContext.cs:318) and GameServer.OnPlayerDisconnectedAsync (GameServer.cs:463) are subscribed to PlayerDisconnected; nulling the event drops both. In DisconnectAsync the guard if (this.PlayerDisconnected is { } ...) is then false, so removal never happens.

Note: This bug is independent of the client auto-reconnect — it reproduces purely server-side and predates that change.

Suggested fix:
Don't null the whole event. Either unsubscribe only the specific handler that causes the double save/logoff (GameServer.OnPlayerDisconnectedAsync), or explicitly remove the real player from the game (GameContext.RemovePlayerAsync) during TransitionToOfflineAsync after disconnecting, so the player list, PlayerCounter, and PlayersByCharacterName are

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions