mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Refactor PlayerPresenceController and NotifyHub: enhance online presence tracking and improve connection handling logic
This commit is contained in:
@@ -35,7 +35,9 @@ namespace DorkNet.Server.Controllers.Match;
|
|||||||
/// null. Players who haven't /goto'd anywhere still report offline.
|
/// null. Players who haven't /goto'd anywhere still report offline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class PlayerPresenceController(PlayerPresenceService presence) : ControllerBase
|
public class PlayerPresenceController(
|
||||||
|
PlayerPresenceService presence,
|
||||||
|
OnlinePresenceService onlinePresence) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("/player")]
|
[HttpGet("/player")]
|
||||||
public ActionResult<List<PlayerPresenceDto>> GetPresences()
|
public ActionResult<List<PlayerPresenceDto>> GetPresences()
|
||||||
@@ -81,17 +83,19 @@ public class PlayerPresenceController(PlayerPresenceService presence) : Controll
|
|||||||
// privacy concern actually applies.
|
// privacy concern actually applies.
|
||||||
var callerId = this.CurrentPlayerId();
|
var callerId = this.CurrentPlayerId();
|
||||||
var callerRoom = callerId is long cid ? presence.GetRoom(cid) : null;
|
var callerRoom = callerId is long cid ? presence.GetRoom(cid) : null;
|
||||||
|
var onlineIds = onlinePresence.OnlinePlayerIds().ToHashSet();
|
||||||
var result = ids.Select(id =>
|
var result = ids.Select(id =>
|
||||||
{
|
{
|
||||||
var room = presence.GetRoom(id);
|
var room = presence.GetRoom(id);
|
||||||
// IsOnline is derived from current room presence: a player
|
// Other players need both a room presence and an active
|
||||||
// who has heartbeated recently has a non-null RoomInstance
|
// notify connection. Presence keys intentionally live for
|
||||||
// in PlayerPresenceService; one who hasn't shows up as
|
// minutes to survive long room loads, so using them alone
|
||||||
// null. Without this, IsOnline defaulted to `true` and the
|
// leaves friends stuck online after a graceful disconnect.
|
||||||
// watch's friends list showed every queried account as
|
// For self, keep using room presence: boot-time bulk probes
|
||||||
// Online regardless of actual session state — verified live
|
// can race the SignalR connection, and returning self as
|
||||||
// (six accounts shown Online despite none being connected).
|
// offline there can desync the heartbeat cache.
|
||||||
var isOnline = room is not null;
|
var isSelf = callerId == id;
|
||||||
|
var isOnline = room is not null && (isSelf || onlineIds.Contains(id));
|
||||||
return new PlayerPresenceDto
|
return new PlayerPresenceDto
|
||||||
{
|
{
|
||||||
PlayerId = (int)id,
|
PlayerId = (int)id,
|
||||||
@@ -109,9 +113,9 @@ public class PlayerPresenceController(PlayerPresenceService presence) : Controll
|
|||||||
// joins; returning a redacted same-room instance there
|
// joins; returning a redacted same-room instance there
|
||||||
// leaves the 2020 client with an incomplete roomInstance
|
// leaves the 2020 client with an incomplete roomInstance
|
||||||
// and can null-ref while reconciling player presence.
|
// and can null-ref while reconciling player presence.
|
||||||
RoomInstance = ShouldExposeFullRoom(callerId, callerRoom, id, room)
|
RoomInstance = isOnline && ShouldExposeFullRoom(callerId, callerRoom, id, room)
|
||||||
? room
|
? room
|
||||||
: RoomInstanceDto.Redact(room),
|
: RoomInstanceDto.Redact(isOnline ? room : null),
|
||||||
IsOnline = isOnline,
|
IsOnline = isOnline,
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using DorkNet.Server.Data;
|
||||||
using DorkNet.Server.Services;
|
using DorkNet.Server.Services;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -40,6 +41,8 @@ namespace DorkNet.Server.Hubs;
|
|||||||
public class NotifyHub(
|
public class NotifyHub(
|
||||||
OnlinePresenceService presence,
|
OnlinePresenceService presence,
|
||||||
NotificationService notifications,
|
NotificationService notifications,
|
||||||
|
DorkNetDbContext db,
|
||||||
|
ServerSettingsService serverSettings,
|
||||||
ILogger<NotifyHub> logger) : Hub
|
ILogger<NotifyHub> logger) : Hub
|
||||||
{
|
{
|
||||||
/// <summary>SignalR group name for a given player id. Use this
|
/// <summary>SignalR group name for a given player id. Use this
|
||||||
@@ -111,11 +114,17 @@ public class NotifyHub(
|
|||||||
var pid = TryGetPlayerId();
|
var pid = TryGetPlayerId();
|
||||||
if (pid is long id)
|
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));
|
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(
|
logger.LogInformation(
|
||||||
"[hub] disconnected player={PlayerId} conn={ConnectionId} error={Error}",
|
"[hub] disconnected player={PlayerId} conn={ConnectionId} wentOffline={WentOffline} error={Error}",
|
||||||
id, Context.ConnectionId, exception?.Message);
|
id, Context.ConnectionId, wentOffline, exception?.Message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ public class OnlinePresenceService
|
|||||||
lock (set) set.Add(connectionId);
|
lock (set) set.Add(connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveConnectionAsync(long playerId, string connectionId)
|
/// <summary>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.</summary>
|
||||||
|
public async Task<bool> RemoveConnectionAsync(long playerId, string connectionId)
|
||||||
{
|
{
|
||||||
if (_redis is { } mux)
|
if (_redis is { } mux)
|
||||||
{
|
{
|
||||||
@@ -84,16 +87,25 @@ public class OnlinePresenceService
|
|||||||
// from the online set so admin queries see "offline".
|
// from the online set so admin queries see "offline".
|
||||||
await db.SetRemoveAsync(OnlineSetKey, playerId);
|
await db.SetRemoveAsync(OnlineSetKey, playerId);
|
||||||
await db.KeyDeleteAsync(connsKey);
|
await db.KeyDeleteAsync(connsKey);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_localConns.TryGetValue(playerId, out var set))
|
if (_localConns.TryGetValue(playerId, out var set))
|
||||||
{
|
{
|
||||||
lock (set) set.Remove(connectionId);
|
lock (set)
|
||||||
if (set.Count == 0) _localConns.TryRemove(playerId, out _);
|
{
|
||||||
|
set.Remove(connectionId);
|
||||||
|
if (set.Count != 0) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_localConns.TryRemove(playerId, out _);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Snapshot of SignalR connection ids that currently belong
|
/// <summary>Snapshot of SignalR connection ids that currently belong
|
||||||
|
|||||||
Reference in New Issue
Block a user