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.