mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Refactor presence tracking: replace OnlinePresenceService with PlayerPresenceService for improved activity management and streamline message handling logic
This commit is contained in:
@@ -22,7 +22,6 @@ public class MessagesController(
|
||||
DorkNetDbContext db,
|
||||
NotificationService notifications,
|
||||
PrivateInstanceService privateInstances,
|
||||
OnlinePresenceService onlinePresence,
|
||||
PlayerPresenceService presence,
|
||||
ServerSettingsService serverSettings,
|
||||
ILogger<MessagesController> logger) : ControllerBase
|
||||
@@ -42,6 +41,7 @@ public class MessagesController(
|
||||
take = Math.Clamp(take, 1, 200);
|
||||
var rows = await db.Messages
|
||||
.Where(m => m.RecipientPlayerId == Me || m.SenderPlayerId == Me)
|
||||
.Where(m => m.Type >= 0)
|
||||
// Defensive filter for the empty-body ghost rows that
|
||||
// accumulated before SendFormMessage rejected them. The
|
||||
// watch's MessagesScreenItem renders TextMessage (type 30)
|
||||
@@ -65,7 +65,7 @@ public class MessagesController(
|
||||
public async Task<ActionResult<List<MessageDto>>> Unread()
|
||||
{
|
||||
var rows = await db.Messages
|
||||
.Where(m => m.RecipientPlayerId == Me && m.ReadAt == null)
|
||||
.Where(m => m.RecipientPlayerId == Me && m.ReadAt == null && m.Type >= 0)
|
||||
// Same ghost-row filter as GetMessages above — keep the
|
||||
// unread badge count honest by hiding empty TextMessages.
|
||||
.Where(m => m.Type != 30 || (m.Body != null && m.Body != ""))
|
||||
@@ -82,7 +82,7 @@ public class MessagesController(
|
||||
public List<long>? MessageIds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/messages/v3/delete</c> — hard-delete one or
|
||||
/// <summary>POST <c>api/messages/v3/delete</c> — delete one or
|
||||
/// more messages addressed to the caller. The 2020 watch's inbox
|
||||
/// "delete" action posts JSON <c>{"MessageIds":[8]}</c> (plural
|
||||
/// array, verified from the live trace where the body was
|
||||
@@ -90,7 +90,9 @@ public class MessagesController(
|
||||
/// some older / SPA call sites still post <c>id</c> or
|
||||
/// <c>messageId</c> as a form/query. Accept all three shapes and
|
||||
/// scope every delete to <c>RecipientPlayerId == Me</c> so the
|
||||
/// sender can't unsend.</summary>
|
||||
/// sender can't unsend. Game invites are tombstoned instead of
|
||||
/// hard-deleted because the watch accepts an invite by firing this
|
||||
/// delete request and <c>/goto/invite/{id}</c> in parallel.</summary>
|
||||
[HttpPost("/api/messages/v3/delete")]
|
||||
[HttpPost("/api/messages/v1/delete")]
|
||||
[Consumes("application/json")]
|
||||
@@ -128,10 +130,27 @@ public class MessagesController(
|
||||
{
|
||||
ids = ids.Distinct().ToList();
|
||||
if (ids.Count == 0) return BadRequest(new { error = "missing_id" });
|
||||
var deleted = await db.Messages
|
||||
|
||||
var inviteRows = await db.Messages
|
||||
.Where(m => ids.Contains(m.Id)
|
||||
&& m.RecipientPlayerId == Me
|
||||
&& (m.Type == 6 || m.Type == 7))
|
||||
.ToListAsync();
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var invite in inviteRows)
|
||||
{
|
||||
invite.Type = -invite.Type;
|
||||
invite.ReadAt ??= now;
|
||||
}
|
||||
|
||||
var hardDeleted = await db.Messages
|
||||
.Where(m => ids.Contains(m.Id) && m.RecipientPlayerId == Me)
|
||||
.Where(m => m.Type != 6 && m.Type != 7 && m.Type != -6 && m.Type != -7)
|
||||
.ExecuteDeleteAsync();
|
||||
return Ok(new { success = true, error = "", deleted });
|
||||
if (inviteRows.Count > 0)
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new { success = true, error = "", deleted = hardDeleted + inviteRows.Count });
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/messages/v3/markRead</c> — bulk
|
||||
@@ -537,11 +556,10 @@ public class MessagesController(
|
||||
{
|
||||
// Honors the global-friends toggle (everyone is a friend when on).
|
||||
var friendIds = await RelationshipQueries.EffectiveFriendIdsAsync(db, serverSettings, Me);
|
||||
var online = onlinePresence.OnlinePlayerIds().ToHashSet();
|
||||
return Ok(friendIds.Select(id => new
|
||||
{
|
||||
AccountId = id,
|
||||
Online = online.Contains(id),
|
||||
Online = presence.IsRecentlyActive(id),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ public class AdminController(
|
||||
q = q.Where(p => p.Username.Contains(query) || p.DisplayName.Contains(query));
|
||||
|
||||
var page = await q.Skip(skip).Take(take).ToListAsync();
|
||||
var onlineSet = onlinePresence.OnlinePlayerIds().ToHashSet();
|
||||
return Ok(page.Select(p => new
|
||||
{
|
||||
p.Id,
|
||||
@@ -94,7 +93,7 @@ public class AdminController(
|
||||
p.Level,
|
||||
p.XP,
|
||||
p.ProfileImageName,
|
||||
Online = onlineSet.Contains(p.Id),
|
||||
Online = playerPresence.IsRecentlyActive(p.Id),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -133,7 +132,7 @@ public class AdminController(
|
||||
p.Level,
|
||||
p.XP,
|
||||
p.ProfileImageName,
|
||||
Online = onlinePresence.OnlinePlayerIds().Contains(p.Id),
|
||||
Online = playerPresence.IsRecentlyActive(p.Id),
|
||||
Balances = balances,
|
||||
Avatar = avatar,
|
||||
});
|
||||
@@ -1169,7 +1168,7 @@ public class AdminController(
|
||||
.Select(r => new { r.Id, r.CreatorPlayerId, r.IsDormRoom })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var onlinePids = onlinePresence.OnlinePlayerIds().ToList();
|
||||
var onlinePids = playerPresence.RecentlyActivePlayerIds().ToList();
|
||||
var byInstance = new Dictionary<long, (long roomInstanceId, long roomId, long subRoomId, string roomName, string photonRoomId, string photonRegionId, string location, int maxCapacity, bool isPrivate, List<long> pids)>();
|
||||
foreach (var pid in onlinePids)
|
||||
{
|
||||
@@ -1235,7 +1234,7 @@ public class AdminController(
|
||||
// route the pulled player into. If nobody's online in the
|
||||
// instance anymore the instance has effectively died; reject.
|
||||
DorkNet.Server.Controllers.Match.RoomInstanceDto? sample = null;
|
||||
foreach (var pid in onlinePresence.OnlinePlayerIds())
|
||||
foreach (var pid in playerPresence.RecentlyActivePlayerIds())
|
||||
{
|
||||
var r = playerPresence.GetRoom(pid);
|
||||
if (r is not null && r.RoomId == id && r.RoomInstanceId == instanceId)
|
||||
@@ -1271,7 +1270,7 @@ public class AdminController(
|
||||
{
|
||||
var reason = body?.Reason ?? "Instance closed by admin";
|
||||
var kicked = new List<long>();
|
||||
foreach (var pid in onlinePresence.OnlinePlayerIds().ToList())
|
||||
foreach (var pid in playerPresence.RecentlyActivePlayerIds().ToList())
|
||||
{
|
||||
var r = playerPresence.GetRoom(pid);
|
||||
if (r is null || r.RoomId != id || r.RoomInstanceId != instanceId) continue;
|
||||
@@ -1787,7 +1786,7 @@ public class AdminController(
|
||||
var activeIpBans = await db.IpBans.CountAsync(b =>
|
||||
b.Until == null || b.Until > now);
|
||||
|
||||
var online = onlinePresence.OnlinePlayerIds().ToArray();
|
||||
var online = playerPresence.RecentlyActivePlayerIds().ToArray();
|
||||
var onlineSet = online.ToHashSet();
|
||||
|
||||
var currentRoomsByPlayer = online
|
||||
@@ -2569,7 +2568,7 @@ public class AdminController(
|
||||
[HttpGet("instances")]
|
||||
public async Task<ActionResult> ListInstances()
|
||||
{
|
||||
var onlineIds = onlinePresence.OnlinePlayerIds().ToHashSet();
|
||||
var onlineIds = playerPresence.RecentlyActivePlayerIds().ToHashSet();
|
||||
if (onlineIds.Count == 0) return Ok(Array.Empty<object>());
|
||||
|
||||
// Bulk-pull names so we can render usernames per participant.
|
||||
|
||||
@@ -402,7 +402,7 @@ public class GoToController(
|
||||
var invite = await db.Messages.FirstOrDefaultAsync(m =>
|
||||
m.Id == inviteId
|
||||
&& m.RecipientPlayerId == callerId.Value
|
||||
&& (m.Type == 6 || m.Type == 7));
|
||||
&& (m.Type == 6 || m.Type == 7 || m.Type == -6 || m.Type == -7));
|
||||
|
||||
long? roomInstanceId = null;
|
||||
if (invite is not null && TryParseRoomInstanceId(invite.Body, out var parsed))
|
||||
@@ -413,12 +413,12 @@ public class GoToController(
|
||||
// parallel — verified in the live server log:
|
||||
// 09:53:47.161Z DELETE messageIds=[60]
|
||||
// 09:53:47.245Z /goto/invite/60 → [goto-invite] invite 60 not found
|
||||
// DELETE usually wins the race, so the message row is gone
|
||||
// by the time we run FirstOrDefaultAsync. The
|
||||
// (instance, player) → LatestInviteMessageId mapping that
|
||||
// SendInvite writes onto the PrivateInstanceInviteeEntity
|
||||
// survives the message deletion and lets us recover
|
||||
// roomInstanceId here.
|
||||
// Current deletes tombstone invite rows so this should only
|
||||
// be needed for invite rows that were hard-deleted by older
|
||||
// builds. The (instance, player) → LatestInviteMessageId
|
||||
// mapping that SendInvite writes onto the
|
||||
// PrivateInstanceInviteeEntity survives those old deletions
|
||||
// and lets us recover roomInstanceId here.
|
||||
if (roomInstanceId is null)
|
||||
{
|
||||
var inviteeRoomInstance = await db.PrivateInstanceInvitees
|
||||
@@ -741,6 +741,7 @@ public class GoToController(
|
||||
}
|
||||
|
||||
presence.SetRoom(playerId, targetRoom);
|
||||
presence.MarkActive(playerId);
|
||||
|
||||
// Real visit tracking: upsert the per-(room, player) row so
|
||||
// we get distinct VisitorCount (unique players ever) AND the
|
||||
|
||||
@@ -56,6 +56,7 @@ public class MatchController(
|
||||
if (completed.HasValue)
|
||||
{
|
||||
presence.SetRoom(pid, completed.Value.TargetRoom);
|
||||
presence.MarkActive(pid);
|
||||
logger.LogInformation(
|
||||
"[joinresult] committed presence player={Player} room={RoomId}/{RoomName} instance={Instance} deferred={Deferred}",
|
||||
pid,
|
||||
@@ -64,6 +65,11 @@ public class MatchController(
|
||||
completed.Value.TargetRoom.RoomInstanceId,
|
||||
completed.Value.DeferPresenceCommit);
|
||||
}
|
||||
else
|
||||
{
|
||||
presence.MarkActive(pid);
|
||||
presence.TouchRoom(pid);
|
||||
}
|
||||
logger.LogInformation(
|
||||
"[joinresult] player={Player} instance={Instance} OK ({Label})",
|
||||
pid, instanceId, label);
|
||||
|
||||
@@ -128,6 +128,11 @@ public class MatchPlayerController(
|
||||
// an HTTP 401 mid-heartbeat.
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
var room = await GetPresenceRoomForResponseAsync(playerId);
|
||||
if (playerId != 0)
|
||||
{
|
||||
presence.MarkActive(playerId);
|
||||
if (room is not null) presence.TouchRoom(playerId);
|
||||
}
|
||||
return Ok(new PresenceDto
|
||||
{
|
||||
PlayerId = playerId,
|
||||
@@ -273,6 +278,7 @@ public class MatchPlayerController(
|
||||
"[player-heartbeat] synthesised dorm fallback player={PlayerId} room={RoomId} dataBlob={DataBlob} (presence cache miss)",
|
||||
playerId, fallback.RoomId, fallback.DataBlob);
|
||||
presence.SetRoom(playerId, fallback);
|
||||
presence.MarkActive(playerId);
|
||||
return fallback;
|
||||
}
|
||||
log.LogInformation("[player-heartbeat] player={PlayerId} room=null", playerId);
|
||||
@@ -291,6 +297,8 @@ public class MatchPlayerController(
|
||||
presence.SetRoom(playerId, room);
|
||||
}
|
||||
}
|
||||
presence.MarkActive(playerId);
|
||||
presence.TouchRoom(playerId);
|
||||
|
||||
log.LogInformation(
|
||||
"[player-heartbeat] player={PlayerId} room={RoomId} instance={InstanceId} subRoom={SubRoomId} location={Location} photon={PhotonRoomId} dataBlob={DataBlob}",
|
||||
|
||||
@@ -36,8 +36,7 @@ namespace DorkNet.Server.Controllers.Match;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class PlayerPresenceController(
|
||||
PlayerPresenceService presence,
|
||||
OnlinePresenceService onlinePresence) : ControllerBase
|
||||
PlayerPresenceService presence) : ControllerBase
|
||||
{
|
||||
[HttpGet("/player")]
|
||||
public ActionResult<List<PlayerPresenceDto>> GetPresences()
|
||||
@@ -83,19 +82,19 @@ public class PlayerPresenceController(
|
||||
// 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);
|
||||
// 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.
|
||||
// live signal. Use the recent-heartbeat marker as the
|
||||
// truth source: SignalR can drop while the player is still
|
||||
// heartbeating, or linger after the game is gone. Presence
|
||||
// keys intentionally live for minutes to survive long room
|
||||
// loads, so using them alone leaves friends stuck online
|
||||
// after a graceful disconnect.
|
||||
var isSelf = callerId == id;
|
||||
var isOnline = room is not null && (isSelf || onlineIds.Contains(id));
|
||||
var active = isSelf || presence.IsRecentlyActive(id);
|
||||
var isOnline = room is not null && active;
|
||||
return new PlayerPresenceDto
|
||||
{
|
||||
PlayerId = (int)id,
|
||||
|
||||
Reference in New Issue
Block a user