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:
- Note the current online count (e.g. 1, just you).
- Use /offlevel a few times (relog in between).
- 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
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:
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