From 49df7393dcc38baad9a5e6dc305e95c34d355a01 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:40:11 +0300 Subject: [PATCH] fix: push presence when a player comes back online MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OnDisconnectedAsync told friends a player went offline, but nothing ever pushed the opposite transition — PlayerCameOnline existed in NotificationService with zero callers. The pair was asymmetric, so a SignalR reconnect (network blip, replica restart) greyed the player out on every friend's list for the rest of the session even though they were still playing. Only re-entering a room, which pushes presence via /goto, brought them back. AddConnectionAsync already computed the first-connection transition to maintain the online set but returned void; it now reports it, mirroring RemoveConnectionAsync. The push is deliberately conditional on a room already being known. The presence payload derives isOnline from roomInstance being non-null, so pushing with no room reads as OFFLINE — worse than staying quiet. On a fresh boot there is no room yet and the /goto that follows sends the real presence anyway, so this only fires on reconnects, where the cached room is exactly what friends need to see again. --- DorkNet.Server/Hubs/NotifyHub.cs | 27 ++++++++++++++++--- .../Services/OnlinePresenceService.cs | 20 +++++++++++--- docs/recroom-2023-client-api-complete.md | 1 + 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/DorkNet.Server/Hubs/NotifyHub.cs b/DorkNet.Server/Hubs/NotifyHub.cs index 273ee3a..1258bce 100644 --- a/DorkNet.Server/Hubs/NotifyHub.cs +++ b/DorkNet.Server/Hubs/NotifyHub.cs @@ -40,6 +40,7 @@ namespace DorkNet.Server.Hubs; [Authorize] public class NotifyHub( OnlinePresenceService presence, + PlayerPresenceService playerRooms, NotificationService notifications, DorkNetDbContext db, ServerSettingsService serverSettings, @@ -95,10 +96,30 @@ public class NotifyHub( } await Groups.AddToGroupAsync(Context.ConnectionId, GroupForPlayer(id)); - await presence.AddConnectionAsync(id, Context.ConnectionId); + var cameOnline = await presence.AddConnectionAsync(id, Context.ConnectionId); logger.LogInformation( - "[hub] connected player={PlayerId} conn={ConnectionId} group={Group}", - id, Context.ConnectionId, GroupForPlayer(id)); + "[hub] connected player={PlayerId} conn={ConnectionId} group={Group} cameOnline={CameOnline}", + id, Context.ConnectionId, GroupForPlayer(id), cameOnline); + + // Tell friends they're back. OnDisconnectedAsync pushes + // PlayerWentOffline, but nothing used to push the opposite + // transition, so the pair was asymmetric: a SignalR reconnect + // (network blip, replica restart) greyed the player out on every + // friend's list for the rest of the session even though they were + // still playing. + // + // Only push when a room is already known. The presence payload + // derives isOnline from roomInstance being non-null, so a push with + // no room reads as OFFLINE — worse than staying quiet. On a fresh + // boot there is no room yet and the /goto that follows sends the + // real presence anyway; this branch is for reconnects, where the + // cached room is exactly what friends need to see again. + if (cameOnline && playerRooms.GetRoom(id) is { } room) + { + var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, id); + if (friendIds.Count > 0) + await notifications.PlayerCameOnline(friendIds, id, room); + } } else { diff --git a/DorkNet.Server/Services/OnlinePresenceService.cs b/DorkNet.Server/Services/OnlinePresenceService.cs index b38c92f..a783fee 100644 --- a/DorkNet.Server/Services/OnlinePresenceService.cs +++ b/DorkNet.Server/Services/OnlinePresenceService.cs @@ -45,7 +45,11 @@ public class OnlinePresenceService public OnlinePresenceService(IConnectionMultiplexer? redis = null) => _redis = redis; - public async Task AddConnectionAsync(long playerId, string connectionId) + /// Register one SignalR connection. Returns true when that was the + /// player's FIRST active connection, i.e. they just came online and friends + /// should be told. Mirrors , which + /// reports the opposite transition. + public async Task AddConnectionAsync(long playerId, string connectionId) { if (_redis is { } mux) { @@ -59,13 +63,21 @@ public class OnlinePresenceService if (added) { var size = await db.SetLengthAsync(connsKey); - if (size == 1) await db.SetAddAsync(OnlineSetKey, playerId); + if (size == 1) + { + await db.SetAddAsync(OnlineSetKey, playerId); + return true; + } } - return; + return false; } var set = _localConns.GetOrAdd(playerId, _ => new HashSet()); - lock (set) set.Add(connectionId); + lock (set) + { + set.Add(connectionId); + return set.Count == 1; + } } /// Remove one SignalR connection. Returns true when that was diff --git a/docs/recroom-2023-client-api-complete.md b/docs/recroom-2023-client-api-complete.md index aa1c0f5..693c9bf 100644 --- a/docs/recroom-2023-client-api-complete.md +++ b/docs/recroom-2023-client-api-complete.md @@ -235,6 +235,7 @@ verified against the binary before the change, and the full test suite passes. | `SignalR push `Id` (notify)` | enum NAME sent for most ids; client only registered numbers — pushes vanished | numeric by default, with the six string-keyed ids listed explicitly | | `SignalR SubscriptionUpdateRoom` | sent as `SubscriptionUpdateRoom`; client listens on `RoomUpdate` | sent as `RoomUpdate` | | `Room ban notification` | sent id 24, for which the client has no handler | sent as ModerationKick (22) with IsBan | +| `Presence on SignalR reconnect` | disconnect pushed offline; nothing ever pushed back online, so a reconnect greyed the player out for the rest of the session | connect re-pushes presence when a room is already known | Everything else in the per-subsystem Defects sections below is still outstanding.