From 4d58130acd545c14b2d5285a43cb32e5e76cabe9 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:54:08 +0300 Subject: [PATCH] Refactor PlayerPresenceController and NotifyHub: enhance online presence tracking and improve connection handling logic --- .../Match/PlayerPresenceController.cs | 26 +++++++++++-------- DorkNet.Server/Hubs/NotifyHub.cs | 15 ++++++++--- .../Services/OnlinePresenceService.cs | 20 +++++++++++--- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs b/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs index d22f120..ad04074 100644 --- a/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs +++ b/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs @@ -35,7 +35,9 @@ namespace DorkNet.Server.Controllers.Match; /// null. Players who haven't /goto'd anywhere still report offline. /// [ApiController] -public class PlayerPresenceController(PlayerPresenceService presence) : ControllerBase +public class PlayerPresenceController( + PlayerPresenceService presence, + OnlinePresenceService onlinePresence) : ControllerBase { [HttpGet("/player")] public ActionResult> GetPresences() @@ -81,17 +83,19 @@ public class PlayerPresenceController(PlayerPresenceService presence) : Controll // privacy concern actually applies. var callerId = this.CurrentPlayerId(); var callerRoom = callerId is long cid ? presence.GetRoom(cid) : null; + var onlineIds = onlinePresence.OnlinePlayerIds().ToHashSet(); var result = ids.Select(id => { var room = presence.GetRoom(id); - // IsOnline is derived from current room presence: a player - // who has heartbeated recently has a non-null RoomInstance - // in PlayerPresenceService; one who hasn't shows up as - // null. Without this, IsOnline defaulted to `true` and the - // watch's friends list showed every queried account as - // Online regardless of actual session state — verified live - // (six accounts shown Online despite none being connected). - var isOnline = room is not null; + // Other players need both a room presence and an active + // notify connection. Presence keys intentionally live for + // minutes to survive long room loads, so using them alone + // leaves friends stuck online after a graceful disconnect. + // For self, keep using room presence: boot-time bulk probes + // can race the SignalR connection, and returning self as + // offline there can desync the heartbeat cache. + var isSelf = callerId == id; + var isOnline = room is not null && (isSelf || onlineIds.Contains(id)); return new PlayerPresenceDto { PlayerId = (int)id, @@ -109,9 +113,9 @@ public class PlayerPresenceController(PlayerPresenceService presence) : Controll // joins; returning a redacted same-room instance there // leaves the 2020 client with an incomplete roomInstance // and can null-ref while reconciling player presence. - RoomInstance = ShouldExposeFullRoom(callerId, callerRoom, id, room) + RoomInstance = isOnline && ShouldExposeFullRoom(callerId, callerRoom, id, room) ? room - : RoomInstanceDto.Redact(room), + : RoomInstanceDto.Redact(isOnline ? room : null), IsOnline = isOnline, }; }).ToList(); diff --git a/DorkNet.Server/Hubs/NotifyHub.cs b/DorkNet.Server/Hubs/NotifyHub.cs index 2c43265..273ee3a 100644 --- a/DorkNet.Server/Hubs/NotifyHub.cs +++ b/DorkNet.Server/Hubs/NotifyHub.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using DorkNet.Server.Data; using DorkNet.Server.Services; using System.Security.Claims; using System.Text.Json; @@ -40,6 +41,8 @@ namespace DorkNet.Server.Hubs; public class NotifyHub( OnlinePresenceService presence, NotificationService notifications, + DorkNetDbContext db, + ServerSettingsService serverSettings, ILogger logger) : Hub { /// SignalR group name for a given player id. Use this @@ -111,11 +114,17 @@ public class NotifyHub( var pid = TryGetPlayerId(); if (pid is long id) { - await presence.RemoveConnectionAsync(id, Context.ConnectionId); + var wentOffline = await presence.RemoveConnectionAsync(id, Context.ConnectionId); await Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupForPlayer(id)); + if (wentOffline) + { + var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, id); + if (friendIds.Count > 0) + await notifications.PlayerWentOffline(friendIds, id); + } logger.LogInformation( - "[hub] disconnected player={PlayerId} conn={ConnectionId} error={Error}", - id, Context.ConnectionId, exception?.Message); + "[hub] disconnected player={PlayerId} conn={ConnectionId} wentOffline={WentOffline} error={Error}", + id, Context.ConnectionId, wentOffline, exception?.Message); } else { diff --git a/DorkNet.Server/Services/OnlinePresenceService.cs b/DorkNet.Server/Services/OnlinePresenceService.cs index 64aef08..b38c92f 100644 --- a/DorkNet.Server/Services/OnlinePresenceService.cs +++ b/DorkNet.Server/Services/OnlinePresenceService.cs @@ -68,7 +68,10 @@ public class OnlinePresenceService lock (set) set.Add(connectionId); } - public async Task RemoveConnectionAsync(long playerId, string connectionId) + /// Remove one SignalR connection. Returns true when that was + /// the player's final active connection and they should now be treated + /// as offline by friends-list presence. + public async Task RemoveConnectionAsync(long playerId, string connectionId) { if (_redis is { } mux) { @@ -84,16 +87,25 @@ public class OnlinePresenceService // from the online set so admin queries see "offline". await db.SetRemoveAsync(OnlineSetKey, playerId); await db.KeyDeleteAsync(connsKey); + return true; } } - return; + return false; } if (_localConns.TryGetValue(playerId, out var set)) { - lock (set) set.Remove(connectionId); - if (set.Count == 0) _localConns.TryRemove(playerId, out _); + lock (set) + { + set.Remove(connectionId); + if (set.Count != 0) return false; + } + + _localConns.TryRemove(playerId, out _); + return true; } + + return false; } /// Snapshot of SignalR connection ids that currently belong