2145 lines
76 KiB
C#
2145 lines
76 KiB
C#
using Mocha2023.Auth;
|
|
using Mocha2023.Classes;
|
|
using Mocha2023.Classes.DBs;
|
|
using Mocha2023.Classes.DBs.DBClasses;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Text.Json;
|
|
using SystemJsonSerializer = System.Text.Json.JsonSerializer;
|
|
using static Mocha2023.Classes.DBs.DBClasses.PlayerDBClasses;
|
|
|
|
namespace Mocha2023.Controllers
|
|
{
|
|
|
|
public sealed class NotificationHub : Hub
|
|
{
|
|
public override async Task OnConnectedAsync()
|
|
{
|
|
HttpRequest? request = Context.GetHttpContext()?.Request;
|
|
long? playerId = request == null
|
|
? null
|
|
: AuthStuff.GetPlayerId(request);
|
|
|
|
if (!playerId.HasValue)
|
|
{
|
|
Console.WriteLine(
|
|
$"[SignalR] Rejected unauthenticated connection {Context.ConnectionId}");
|
|
Context.Abort();
|
|
return;
|
|
}
|
|
|
|
bool becameOnline = NotiController.RegisterConnection(
|
|
Context.ConnectionId,
|
|
playerId.Value);
|
|
|
|
NotiController.RegisterHubContext(Context.ConnectionId, Context);
|
|
|
|
NotiController.EnsurePersistentSubscriptions(
|
|
Context.ConnectionId,
|
|
playerId.Value);
|
|
|
|
Console.WriteLine(
|
|
$"[SignalR] Player {playerId.Value} connected as {Context.ConnectionId}");
|
|
|
|
await base.OnConnectedAsync();
|
|
|
|
await Clients.Caller.SendAsync("OnConnect");
|
|
Console.WriteLine(
|
|
$"[SignalR] Player {playerId.Value} OnConnect sent");
|
|
|
|
await NotiController.DeliverPendingNotificationsAsync(playerId.Value);
|
|
await NotiController.NotifyAnnouncementsUpdatedAsync(playerId.Value);
|
|
await NotiController.PrimeConnectionAsync(
|
|
Context.ConnectionId,
|
|
playerId.Value);
|
|
}
|
|
|
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
|
{
|
|
bool found = NotiController.UnregisterConnection(
|
|
Context.ConnectionId,
|
|
out long playerId,
|
|
out bool becameOffline);
|
|
|
|
NotiController.UnregisterHubContext(Context.ConnectionId);
|
|
|
|
await base.OnDisconnectedAsync(exception);
|
|
|
|
if (!found)
|
|
return;
|
|
|
|
Console.WriteLine(
|
|
$"[SignalR] Player {playerId} disconnected from {Context.ConnectionId}" +
|
|
(exception == null ? string.Empty : $": {exception.Message}"));
|
|
|
|
}
|
|
|
|
public async Task<string> SubscribeToPlayers(
|
|
PlayerSubscriptionListDTO? subscriptionList)
|
|
{
|
|
List<int> requestedPlayerIds =
|
|
subscriptionList?.PlayerIds ?? new List<int>();
|
|
|
|
List<long> newlySubscribedPlayerIds = NotiController.ReplacePlayerSubscriptions(
|
|
Context.ConnectionId,
|
|
requestedPlayerIds);
|
|
|
|
HttpRequest? request = Context.GetHttpContext()?.Request;
|
|
long? ownerPlayerId = request == null
|
|
? null
|
|
: AuthStuff.GetPlayerId(request);
|
|
|
|
if (ownerPlayerId.HasValue && newlySubscribedPlayerIds.Count > 0)
|
|
{
|
|
await NotiController.PrimePlayerSubscriptionsAsync(
|
|
Context.ConnectionId,
|
|
ownerPlayerId.Value,
|
|
newlySubscribedPlayerIds.Select(id => (int)id));
|
|
}
|
|
|
|
return "200 OK";
|
|
}
|
|
|
|
public Task<string> SubscribeToPlayer(
|
|
PlayerSubscriptionListDTO? subscriptionList) =>
|
|
SubscribeToPlayers(subscriptionList);
|
|
|
|
public Task<string> UnsubscribeFromPlayers(
|
|
PlayerSubscriptionListDTO? subscriptionList)
|
|
{
|
|
NotiController.RemovePlayerSubscriptions(
|
|
Context.ConnectionId,
|
|
subscriptionList?.PlayerIds ?? new List<int>());
|
|
|
|
return Task.FromResult("200 OK");
|
|
}
|
|
|
|
public Task<string> UnsubscribeFromPlayer(
|
|
PlayerSubscriptionListDTO? subscriptionList) =>
|
|
UnsubscribeFromPlayers(subscriptionList);
|
|
|
|
public Task<string> SubscribeToChannels(
|
|
ChannelSubscriptionListDTO? subscriptionList)
|
|
{
|
|
NotiController.ReplaceChannelSubscriptions(
|
|
Context.ConnectionId,
|
|
subscriptionList?.Channels ?? new List<string>());
|
|
|
|
return Task.FromResult("200 OK");
|
|
}
|
|
|
|
public Task<string> UnsubscribeFromChannels(
|
|
ChannelSubscriptionListDTO? subscriptionList)
|
|
{
|
|
NotiController.RemoveChannelSubscriptions(
|
|
Context.ConnectionId,
|
|
subscriptionList?.Channels ?? new List<string>());
|
|
|
|
return Task.FromResult("200 OK");
|
|
}
|
|
|
|
public Task<string> SubscribeTo(
|
|
ChannelSubscriptionListDTO? subscriptionList) =>
|
|
SubscribeToChannels(subscriptionList);
|
|
|
|
public Task<string> UnsubscribeFrom(
|
|
ChannelSubscriptionListDTO? subscriptionList) =>
|
|
UnsubscribeFromChannels(subscriptionList);
|
|
}
|
|
|
|
public sealed class PlayerSubscriptionListDTO
|
|
{
|
|
public List<int> PlayerIds { get; set; } = new();
|
|
}
|
|
|
|
public sealed class ChannelSubscriptionListDTO
|
|
{
|
|
public List<string> Channels { get; set; } = new();
|
|
}
|
|
|
|
public static class NotiController
|
|
{
|
|
public sealed class GiftLiveDelivery
|
|
{
|
|
public long ReceiverPlayerId { get; }
|
|
public object? Gift { get; }
|
|
|
|
public GiftLiveDelivery(
|
|
long receiverPlayerId,
|
|
object? gift)
|
|
{
|
|
ReceiverPlayerId = receiverPlayerId;
|
|
Gift = gift;
|
|
}
|
|
}
|
|
|
|
public sealed class GiftLiveDeliveryResult
|
|
{
|
|
public int TargetPlayers { get; set; }
|
|
public int LivePlayers { get; set; }
|
|
public int LiveSockets { get; set; }
|
|
public int OfflinePlayers { get; set; }
|
|
}
|
|
|
|
private sealed class PreparedGiftLiveDelivery
|
|
{
|
|
public long SenderPlayerId { get; set; }
|
|
public long ReceiverPlayerId { get; set; }
|
|
public object Payload { get; set; } = new object();
|
|
public long GiftPackageId { get; set; }
|
|
public HashSet<string> PrimedConnectionIds { get; } =
|
|
new(StringComparer.Ordinal);
|
|
}
|
|
|
|
private const int MaxPlayerSubscriptionsPerConnection = 250;
|
|
|
|
private const string ClientNotificationHubTarget = "Notification";
|
|
private const string PresenceUpdateNotificationId = "PresenceUpdate";
|
|
private const string RoomUpdateNotificationId = "RoomUpdate";
|
|
private const string AccountUpdateNotificationId = "AccountUpdate";
|
|
private const string SelfAccountUpdateNotificationId = "SelfAccountUpdate";
|
|
|
|
private const string RelationshipChangedTarget = "1";
|
|
private const string MessageReceivedTarget = "2";
|
|
private const string SubscriptionUpdatePresenceTarget = "12";
|
|
private const string ServerMaintenanceTarget = "25";
|
|
private const string GiftPackageReceivedTarget = "30";
|
|
private const string GiftPackageReceivedImmediateTarget = "31";
|
|
private const string ConsumableMappingRemovedTarget = "71";
|
|
private const string GiftConsumedTarget = "GiftConsumed";
|
|
private const string RelationshipsInvalidTarget = "50";
|
|
private const string ChatMessageReceivedTarget = "ChatMessageReceived";
|
|
private const string CommunityBoardUpdateTarget = "CommunityBoardUpdate";
|
|
private const string CommunityBoardAnnouncementUpdateTarget = "96";
|
|
private const string FreeGiftButtonItemsAddedTarget = "110";
|
|
|
|
private static readonly IReadOnlyDictionary<string, string> PushTargetNames =
|
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
|
{
|
|
[RelationshipChangedTarget] = "RelationshipChanged",
|
|
[MessageReceivedTarget] = "MessageReceived",
|
|
[SubscriptionUpdatePresenceTarget] = "SubscriptionUpdatePresence",
|
|
[ServerMaintenanceTarget] = "ServerMaintenance",
|
|
[GiftPackageReceivedTarget] = "GiftPackageReceived",
|
|
[GiftPackageReceivedImmediateTarget] = "GiftPackageReceivedImmediate",
|
|
[ConsumableMappingRemovedTarget] = "ConsumableMappingRemoved",
|
|
[GiftConsumedTarget] = "GiftConsumed",
|
|
[RelationshipsInvalidTarget] = "RelationshipsInvalid",
|
|
[ChatMessageReceivedTarget] = "ChatMessageReceived",
|
|
[CommunityBoardUpdateTarget] = "CommunityBoardUpdate",
|
|
[CommunityBoardAnnouncementUpdateTarget] = "CommunityBoardAnnouncementUpdate",
|
|
[FreeGiftButtonItemsAddedTarget] = "FreeGiftButtonItemsAdded"
|
|
};
|
|
|
|
private static IHubContext<NotificationHub>? _hubContext;
|
|
private static FileSystemWatcher? _announcementWatcher;
|
|
private static int _announcementRefreshPending;
|
|
|
|
private static readonly ConcurrentDictionary<string, long>
|
|
ConnectionPlayers = new(StringComparer.Ordinal);
|
|
|
|
private static readonly ConcurrentDictionary<long, ConcurrentDictionary<string, byte>>
|
|
PlayerConnections = new();
|
|
|
|
private static readonly ConcurrentDictionary<string, HubCallerContext>
|
|
ActiveHubContexts = new(StringComparer.Ordinal);
|
|
|
|
internal static void RegisterHubContext(string connectionId, HubCallerContext context) =>
|
|
ActiveHubContexts[connectionId] = context;
|
|
|
|
internal static void UnregisterHubContext(string connectionId) =>
|
|
ActiveHubContexts.TryRemove(connectionId, out _);
|
|
|
|
internal static int ForceDisconnectPlayer(long playerId)
|
|
{
|
|
if (!PlayerConnections.TryGetValue(playerId, out var sockets))
|
|
return 0;
|
|
|
|
int aborted = 0;
|
|
foreach (string connectionId in sockets.Keys.ToList())
|
|
{
|
|
if (ActiveHubContexts.TryGetValue(connectionId, out HubCallerContext? context))
|
|
{
|
|
try
|
|
{
|
|
context.Abort();
|
|
aborted++;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Console.WriteLine(
|
|
$"[SignalR] Failed aborting {connectionId} for player {playerId}: {exception.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
return aborted;
|
|
}
|
|
|
|
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<long, byte>>
|
|
ConnectionPlayerSubscriptions = new(StringComparer.Ordinal);
|
|
|
|
private static readonly ConcurrentDictionary<long, ConcurrentDictionary<string, byte>>
|
|
PlayerSubscribers = new();
|
|
|
|
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>>
|
|
ConnectionChannelSubscriptions = new(StringComparer.Ordinal);
|
|
|
|
public static void Initialize(IHubContext<NotificationHub> hubContext)
|
|
{
|
|
_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
|
|
InitializeAnnouncementWatcher();
|
|
Console.WriteLine("[SignalR] Notification hub broadcaster initialized");
|
|
}
|
|
|
|
internal static int ConnectedSocketCount => ConnectionPlayers.Count;
|
|
|
|
internal static int GetPlayerSocketCount(long playerId) =>
|
|
PlayerConnections.TryGetValue(playerId, out var connections)
|
|
? connections.Count
|
|
: 0;
|
|
|
|
private static void InitializeAnnouncementWatcher()
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Program.dataDir);
|
|
|
|
_announcementWatcher?.Dispose();
|
|
_announcementWatcher = new FileSystemWatcher(
|
|
Program.dataDir,
|
|
"announcements.json")
|
|
{
|
|
IncludeSubdirectories = false,
|
|
NotifyFilter = NotifyFilters.FileName |
|
|
NotifyFilters.LastWrite |
|
|
NotifyFilters.CreationTime |
|
|
NotifyFilters.Size,
|
|
EnableRaisingEvents = true
|
|
};
|
|
|
|
_announcementWatcher.Changed += (_, _) =>
|
|
QueueAnnouncementBroadcast();
|
|
_announcementWatcher.Created += (_, _) =>
|
|
QueueAnnouncementBroadcast();
|
|
_announcementWatcher.Deleted += (_, _) =>
|
|
QueueAnnouncementBroadcast();
|
|
_announcementWatcher.Renamed += (_, _) =>
|
|
QueueAnnouncementBroadcast();
|
|
|
|
Console.WriteLine(
|
|
$"[Announcements] Watching {Path.Combine(Program.dataDir, "announcements.json")}");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Console.WriteLine(
|
|
$"[Announcements] File watcher disabled: {exception.Message}");
|
|
}
|
|
}
|
|
|
|
private static void QueueAnnouncementBroadcast()
|
|
{
|
|
if (Interlocked.Exchange(ref _announcementRefreshPending, 1) != 0)
|
|
return;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
|
|
await Task.Delay(300);
|
|
await BroadcastAnnouncementsUpdatedAsync();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Console.WriteLine(
|
|
$"[Announcements] Live refresh failed: {exception.Message}");
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _announcementRefreshPending, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
internal static bool RegisterConnection(string connectionId, long playerId)
|
|
{
|
|
|
|
if (ConnectionPlayers.TryGetValue(connectionId, out long oldPlayerId))
|
|
{
|
|
UnregisterConnection(
|
|
connectionId,
|
|
out _,
|
|
out _);
|
|
|
|
Console.WriteLine(
|
|
$"[SignalR] Replaced reused connection {connectionId} " +
|
|
$"from player {oldPlayerId} with player {playerId}");
|
|
}
|
|
|
|
ConnectionPlayers[connectionId] = playerId;
|
|
|
|
ConcurrentDictionary<string, byte> sockets = PlayerConnections
|
|
.GetOrAdd(playerId, _ => new ConcurrentDictionary<string, byte>(
|
|
StringComparer.Ordinal));
|
|
|
|
bool wasOffline = sockets.IsEmpty;
|
|
sockets[connectionId] = 0;
|
|
|
|
ConnectionPlayerSubscriptions.TryAdd(
|
|
connectionId,
|
|
new ConcurrentDictionary<long, byte>());
|
|
|
|
ConnectionChannelSubscriptions.TryAdd(
|
|
connectionId,
|
|
new ConcurrentDictionary<string, byte>(StringComparer.Ordinal));
|
|
|
|
return wasOffline;
|
|
}
|
|
|
|
internal static bool UnregisterConnection(
|
|
string connectionId,
|
|
out long playerId,
|
|
out bool becameOffline)
|
|
{
|
|
becameOffline = false;
|
|
|
|
if (!ConnectionPlayers.TryRemove(connectionId, out playerId))
|
|
return false;
|
|
|
|
if (PlayerConnections.TryGetValue(playerId, out var sockets))
|
|
{
|
|
sockets.TryRemove(connectionId, out _);
|
|
if (sockets.IsEmpty)
|
|
{
|
|
PlayerConnections.TryRemove(playerId, out _);
|
|
becameOffline = true;
|
|
}
|
|
}
|
|
|
|
if (ConnectionPlayerSubscriptions.TryRemove(
|
|
connectionId,
|
|
out var subscribedPlayers))
|
|
{
|
|
foreach (long subscribedPlayerId in subscribedPlayers.Keys)
|
|
RemoveSubscriberIndex(connectionId, subscribedPlayerId);
|
|
}
|
|
|
|
ConnectionChannelSubscriptions.TryRemove(connectionId, out _);
|
|
return true;
|
|
}
|
|
|
|
// Returns only the subject IDs that were newly added by this call -
|
|
// the client re-sends its full subscription list on every call
|
|
// (observed multiple times per second per connection), so priming
|
|
// the whole requested list unconditionally on every call turned
|
|
// every one of those into a full profile+presence re-broadcast for
|
|
// everyone already subscribed, flooding the hub with redundant
|
|
// traffic instead of just the players that are actually new.
|
|
internal static List<long> ReplacePlayerSubscriptions(
|
|
string connectionId,
|
|
IEnumerable<int> requestedPlayerIds)
|
|
{
|
|
if (!ConnectionPlayers.TryGetValue(
|
|
connectionId,
|
|
out long ownerPlayerId))
|
|
{
|
|
return new List<long>();
|
|
}
|
|
|
|
ConcurrentDictionary<long, byte> current =
|
|
ConnectionPlayerSubscriptions.GetOrAdd(
|
|
connectionId,
|
|
_ => new ConcurrentDictionary<long, byte>());
|
|
|
|
long[] requested = requestedPlayerIds
|
|
.Where(id => id > 0)
|
|
.Select(id => (long)id)
|
|
.Concat(RelationshipDB.GetPersistentLiveSubjectIds(ownerPlayerId))
|
|
.Where(id => id != ownerPlayerId)
|
|
.Distinct()
|
|
.Take(MaxPlayerSubscriptionsPerConnection)
|
|
.ToArray();
|
|
|
|
var newlyAdded = new List<long>();
|
|
foreach (long addedPlayerId in requested)
|
|
{
|
|
if (!current.TryAdd(addedPlayerId, 0))
|
|
continue;
|
|
|
|
newlyAdded.Add(addedPlayerId);
|
|
PlayerSubscribers
|
|
.GetOrAdd(
|
|
addedPlayerId,
|
|
_ => new ConcurrentDictionary<string, byte>(
|
|
StringComparer.Ordinal))
|
|
[connectionId] = 0;
|
|
}
|
|
|
|
if (newlyAdded.Count > 0)
|
|
{
|
|
Console.WriteLine(
|
|
$"[SignalR] Player {ownerPlayerId} subscriptions=" +
|
|
string.Join(',', current.Keys.OrderBy(id => id)));
|
|
}
|
|
|
|
return newlyAdded;
|
|
}
|
|
|
|
internal static void RemovePlayerSubscriptions(
|
|
string connectionId,
|
|
IEnumerable<int> playerIds)
|
|
{
|
|
if (!ConnectionPlayerSubscriptions.TryGetValue(
|
|
connectionId,
|
|
out var current))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (long playerId in playerIds
|
|
.Where(id => id > 0)
|
|
.Select(id => (long)id)
|
|
.Distinct())
|
|
{
|
|
current.TryRemove(playerId, out _);
|
|
RemoveSubscriberIndex(connectionId, playerId);
|
|
}
|
|
}
|
|
|
|
internal static void ReplaceChannelSubscriptions(
|
|
string connectionId,
|
|
IEnumerable<string> channels)
|
|
{
|
|
ConcurrentDictionary<string, byte> current =
|
|
ConnectionChannelSubscriptions.GetOrAdd(
|
|
connectionId,
|
|
_ => new ConcurrentDictionary<string, byte>(
|
|
StringComparer.Ordinal));
|
|
|
|
HashSet<string> desired = channels
|
|
.Where(channel => !string.IsNullOrWhiteSpace(channel))
|
|
.Select(channel => channel.Trim())
|
|
.Distinct(StringComparer.Ordinal)
|
|
.Take(250)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
foreach (string removed in current.Keys.Except(
|
|
desired,
|
|
StringComparer.Ordinal))
|
|
{
|
|
current.TryRemove(removed, out _);
|
|
}
|
|
|
|
foreach (string added in desired.Except(
|
|
current.Keys,
|
|
StringComparer.Ordinal))
|
|
{
|
|
current[added] = 0;
|
|
}
|
|
}
|
|
|
|
internal static void RemoveChannelSubscriptions(
|
|
string connectionId,
|
|
IEnumerable<string> channels)
|
|
{
|
|
if (!ConnectionChannelSubscriptions.TryGetValue(
|
|
connectionId,
|
|
out var current))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (string channel in channels
|
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
|
.Select(value => value.Trim())
|
|
.Distinct(StringComparer.Ordinal))
|
|
{
|
|
current.TryRemove(channel, out _);
|
|
}
|
|
}
|
|
|
|
private static void RemoveSubscriberIndex(
|
|
string connectionId,
|
|
long subscribedPlayerId)
|
|
{
|
|
if (!PlayerSubscribers.TryGetValue(
|
|
subscribedPlayerId,
|
|
out var subscribers))
|
|
{
|
|
return;
|
|
}
|
|
|
|
subscribers.TryRemove(connectionId, out _);
|
|
if (subscribers.IsEmpty)
|
|
PlayerSubscribers.TryRemove(subscribedPlayerId, out _);
|
|
}
|
|
|
|
internal static void EnsurePersistentSubscriptions(
|
|
string connectionId,
|
|
long ownerPlayerId)
|
|
{
|
|
if (ownerPlayerId <= 0 ||
|
|
!ConnectionPlayers.ContainsKey(connectionId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ConcurrentDictionary<long, byte> current =
|
|
ConnectionPlayerSubscriptions.GetOrAdd(
|
|
connectionId,
|
|
_ => new ConcurrentDictionary<long, byte>());
|
|
|
|
foreach (long subjectPlayerId in
|
|
RelationshipDB.GetPersistentLiveSubjectIds(ownerPlayerId)
|
|
.Take(MaxPlayerSubscriptionsPerConnection))
|
|
{
|
|
if (subjectPlayerId <= 0 ||
|
|
subjectPlayerId == ownerPlayerId ||
|
|
!current.TryAdd(subjectPlayerId, 0))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PlayerSubscribers
|
|
.GetOrAdd(
|
|
subjectPlayerId,
|
|
_ => new ConcurrentDictionary<string, byte>(
|
|
StringComparer.Ordinal))
|
|
[connectionId] = 0;
|
|
}
|
|
}
|
|
|
|
private static void RefreshPersistentSubscriptionsForPlayer(
|
|
long playerId)
|
|
{
|
|
if (!PlayerConnections.TryGetValue(playerId, out var connections))
|
|
return;
|
|
|
|
foreach (string connectionId in connections.Keys)
|
|
EnsurePersistentSubscriptions(connectionId, playerId);
|
|
}
|
|
|
|
internal static async Task PrimeConnectionAsync(
|
|
string connectionId,
|
|
long ownerPlayerId)
|
|
{
|
|
if (!ConnectionPlayers.ContainsKey(connectionId))
|
|
return;
|
|
|
|
EnsurePersistentSubscriptions(connectionId, ownerPlayerId);
|
|
|
|
long[] subjectIds = ConnectionPlayerSubscriptions.TryGetValue(
|
|
connectionId,
|
|
out var subscriptions)
|
|
? subscriptions.Keys
|
|
.Where(id => id > 0)
|
|
.Distinct()
|
|
.Take(MaxPlayerSubscriptionsPerConnection)
|
|
.ToArray()
|
|
: Array.Empty<long>();
|
|
|
|
long[] profileIds = subjectIds
|
|
.Append(ownerPlayerId)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
foreach (PlayerDTOBase account in PlayerDB.GetAccountsBulk(
|
|
profileIds.ToList(),
|
|
callerId: ownerPlayerId))
|
|
{
|
|
string eventId = account.accountId == ownerPlayerId
|
|
? SelfAccountUpdateNotificationId
|
|
: AccountUpdateNotificationId;
|
|
|
|
await SendProfilePushToConnectionIdsAsync(
|
|
new[] { connectionId },
|
|
account,
|
|
eventId);
|
|
}
|
|
|
|
foreach (ClientRelationshipDTO relationship in
|
|
RelationshipDB.GetClientRelationships(ownerPlayerId))
|
|
{
|
|
await SendPushEventToConnectionIdsAsync(
|
|
new[] { connectionId },
|
|
RelationshipChangedTarget,
|
|
relationship);
|
|
}
|
|
|
|
foreach (long subjectPlayerId in subjectIds)
|
|
{
|
|
Heartbeat heartbeat = PlayerDB.GetPlayerHeartbeat(
|
|
subjectPlayerId,
|
|
ownerPlayerId);
|
|
|
|
await SendPresencePushToConnectionIdsAsync(
|
|
new[] { connectionId },
|
|
heartbeat);
|
|
}
|
|
|
|
Console.WriteLine(
|
|
$"[LIVE PRIME] player={ownerPlayerId} " +
|
|
$"profiles={profileIds.Length} " +
|
|
$"relationships={RelationshipDB.GetClientRelationships(ownerPlayerId).Count} " +
|
|
$"presence={subjectIds.Length}");
|
|
}
|
|
|
|
internal static async Task PrimePlayerSubscriptionsAsync(
|
|
string connectionId,
|
|
long ownerPlayerId,
|
|
IEnumerable<int> requestedPlayerIds)
|
|
{
|
|
if (!ConnectionPlayers.ContainsKey(connectionId))
|
|
return;
|
|
|
|
long[] subjectIds = requestedPlayerIds
|
|
.Where(id => id > 0)
|
|
.Select(id => (long)id)
|
|
.Where(id => id != ownerPlayerId)
|
|
.Distinct()
|
|
.Take(MaxPlayerSubscriptionsPerConnection)
|
|
.ToArray();
|
|
|
|
if (subjectIds.Length == 0)
|
|
return;
|
|
|
|
foreach (PlayerDTOBase account in PlayerDB.GetAccountsBulk(
|
|
subjectIds.ToList(),
|
|
callerId: ownerPlayerId))
|
|
{
|
|
await SendProfilePushToConnectionIdsAsync(
|
|
new[] { connectionId },
|
|
account);
|
|
}
|
|
|
|
foreach (long subjectPlayerId in subjectIds)
|
|
{
|
|
Heartbeat heartbeat = PlayerDB.GetPlayerHeartbeat(
|
|
subjectPlayerId,
|
|
ownerPlayerId);
|
|
|
|
await SendPresencePushToConnectionIdsAsync(
|
|
new[] { connectionId },
|
|
heartbeat);
|
|
}
|
|
|
|
Console.WriteLine(
|
|
$"[LIVE SUBSCRIBE PRIME] player={ownerPlayerId} " +
|
|
$"subjects={subjectIds.Length} socket={connectionId}");
|
|
}
|
|
|
|
private static void AddPlayerConnections(
|
|
ISet<string> destination,
|
|
long playerId)
|
|
{
|
|
if (playerId <= 0 ||
|
|
!PlayerConnections.TryGetValue(playerId, out var connections))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (string connectionId in connections.Keys)
|
|
destination.Add(connectionId);
|
|
}
|
|
|
|
private static string[] GetProfileRecipientConnectionIds(
|
|
long subjectPlayerId)
|
|
{
|
|
var recipients = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
AddPlayerConnections(recipients, subjectPlayerId);
|
|
|
|
if (PlayerSubscribers.TryGetValue(
|
|
subjectPlayerId,
|
|
out var explicitSubscribers))
|
|
{
|
|
recipients.UnionWith(explicitSubscribers.Keys);
|
|
}
|
|
|
|
foreach (long watcherPlayerId in
|
|
RelationshipDB.GetPersistentLiveWatcherIds(subjectPlayerId))
|
|
{
|
|
AddPlayerConnections(recipients, watcherPlayerId);
|
|
}
|
|
|
|
foreach (long roomPeerPlayerId in
|
|
PlayerDB.GetActiveSameInstancePlayerIds(subjectPlayerId))
|
|
{
|
|
AddPlayerConnections(recipients, roomPeerPlayerId);
|
|
}
|
|
|
|
return recipients.ToArray();
|
|
}
|
|
|
|
private static string[] GetPresenceRecipientConnectionIds(
|
|
long subjectPlayerId)
|
|
{
|
|
var recipients = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
if (PlayerSubscribers.TryGetValue(
|
|
subjectPlayerId,
|
|
out var explicitSubscribers))
|
|
{
|
|
recipients.UnionWith(explicitSubscribers.Keys);
|
|
}
|
|
|
|
foreach (long watcherPlayerId in
|
|
RelationshipDB.GetPersistentLiveWatcherIds(subjectPlayerId))
|
|
{
|
|
AddPlayerConnections(recipients, watcherPlayerId);
|
|
}
|
|
|
|
if (PlayerConnections.TryGetValue(subjectPlayerId, out var own))
|
|
recipients.ExceptWith(own.Keys);
|
|
|
|
return recipients.ToArray();
|
|
}
|
|
|
|
public static bool IsPlayerConnected(long playerId) =>
|
|
PlayerConnections.TryGetValue(playerId, out var connections) &&
|
|
!connections.IsEmpty;
|
|
|
|
public static object GetDebugStatus(long playerId)
|
|
{
|
|
string[] connectionIds = PlayerConnections.TryGetValue(
|
|
playerId,
|
|
out var own)
|
|
? own.Keys.ToArray()
|
|
: Array.Empty<string>();
|
|
|
|
return new
|
|
{
|
|
playerId,
|
|
connected = connectionIds.Length > 0,
|
|
connectionCount = connectionIds.Length,
|
|
connections = connectionIds.Select(connectionId => new
|
|
{
|
|
connectionId,
|
|
playerIds = ConnectionPlayerSubscriptions.TryGetValue(
|
|
connectionId,
|
|
out var subscriptions)
|
|
? subscriptions.Keys.OrderBy(id => id).ToArray()
|
|
: Array.Empty<long>()
|
|
}).ToArray()
|
|
};
|
|
}
|
|
|
|
public static Task SendNotificationToPlayer(long playerId, object message)
|
|
{
|
|
JsonElement root = SystemJsonSerializer.SerializeToElement(message);
|
|
if (root.ValueKind != JsonValueKind.Object ||
|
|
!TryGetPropertyIgnoreCase(root, "target", out JsonElement targetElement))
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
string? target = targetElement.GetString();
|
|
if (string.IsNullOrWhiteSpace(target))
|
|
return Task.CompletedTask;
|
|
|
|
object?[] arguments = Array.Empty<object?>();
|
|
if (TryGetPropertyIgnoreCase(root, "arguments", out JsonElement argumentsElement) &&
|
|
argumentsElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
arguments = argumentsElement
|
|
.EnumerateArray()
|
|
.Select(argument => (object?)argument.Clone())
|
|
.ToArray();
|
|
}
|
|
|
|
return SendHubEventToPlayerAsync(playerId, target, arguments);
|
|
}
|
|
|
|
public static Task SendHubEventToPlayerAsync(
|
|
long playerId,
|
|
string target,
|
|
params object?[] arguments)
|
|
{
|
|
if (!PlayerConnections.TryGetValue(playerId, out var connections))
|
|
return Task.CompletedTask;
|
|
|
|
return SendTargetToConnectionsAsync(
|
|
connections.Keys,
|
|
target,
|
|
arguments);
|
|
}
|
|
|
|
public static Task SendHubEventToSubscribersAsync(
|
|
long subjectPlayerId,
|
|
string target,
|
|
params object?[] arguments)
|
|
{
|
|
if (!PlayerSubscribers.TryGetValue(
|
|
subjectPlayerId,
|
|
out var subscribers))
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
return SendTargetToConnectionsAsync(
|
|
subscribers.Keys,
|
|
target,
|
|
arguments);
|
|
}
|
|
|
|
public static async Task DeliverPendingNotificationsAsync(long playerId)
|
|
{
|
|
List<NotificationDB.ClientNotification> pending =
|
|
NotificationDB.GetMessages(playerId)
|
|
.OrderBy(message => message.CreatedAt)
|
|
.Take(100)
|
|
.ToList();
|
|
|
|
if (pending.Count == 0)
|
|
return;
|
|
|
|
Console.WriteLine(
|
|
$"[NOTIFICATION REPLAY] player={playerId} count={pending.Count}");
|
|
|
|
foreach (NotificationDB.ClientNotification message in pending)
|
|
{
|
|
await SendPushEventToPlayerAsync(
|
|
playerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
}
|
|
}
|
|
|
|
public static async Task NotifyAnnouncementsUpdatedAsync(long playerId)
|
|
{
|
|
await Task.WhenAll(
|
|
SendPushSignalToPlayerAsync(
|
|
playerId,
|
|
CommunityBoardAnnouncementUpdateTarget),
|
|
SendPushSignalToPlayerAsync(
|
|
playerId,
|
|
CommunityBoardUpdateTarget));
|
|
}
|
|
|
|
public static async Task BroadcastAnnouncementsUpdatedAsync()
|
|
{
|
|
long[] connectedPlayers = PlayerConnections.Keys
|
|
.Where(playerId => playerId > 0)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
if (connectedPlayers.Length == 0)
|
|
return;
|
|
|
|
Console.WriteLine(
|
|
$"[Announcements] Broadcasting live refresh to " +
|
|
$"{connectedPlayers.Length} connected players");
|
|
|
|
await Task.WhenAll(connectedPlayers.Select(
|
|
NotifyAnnouncementsUpdatedAsync));
|
|
}
|
|
|
|
public static async Task BroadcastServerMaintenanceAsync(int startsInMinutes)
|
|
{
|
|
if (startsInMinutes < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(startsInMinutes));
|
|
|
|
long[] connectedPlayers = PlayerConnections.Keys
|
|
.Where(playerId => playerId > 0)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
await Task.WhenAll(connectedPlayers.Select(playerId =>
|
|
SendPushSignalToPlayerAsync(
|
|
playerId,
|
|
ServerMaintenanceTarget)));
|
|
|
|
Console.WriteLine(
|
|
$"[Maintenance] Sent {startsInMinutes}-minute countdown to " +
|
|
$"{connectedPlayers.Length} connected player(s)");
|
|
}
|
|
|
|
public static async Task NotifyFriendRequestAsync(
|
|
long senderPlayerId,
|
|
long receiverPlayerId)
|
|
{
|
|
NotificationDB.ClientNotification message =
|
|
NotificationDB.CreateFriendInvite(
|
|
senderPlayerId,
|
|
receiverPlayerId);
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
|
|
await NotifyRelationshipChangedAsync(
|
|
senderPlayerId,
|
|
receiverPlayerId);
|
|
}
|
|
|
|
public static async Task NotifyFriendAcceptedAsync(
|
|
long acceptingPlayerId,
|
|
long requesterPlayerId)
|
|
{
|
|
NotificationDB.ClientNotification message =
|
|
NotificationDB.CreateFriendAccepted(
|
|
acceptingPlayerId,
|
|
requesterPlayerId);
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
requesterPlayerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
|
|
await NotifyRelationshipChangedAsync(
|
|
acceptingPlayerId,
|
|
requesterPlayerId);
|
|
}
|
|
|
|
public static async Task NotifyFriendIntroductionAsync(
|
|
long senderPlayerId,
|
|
long receiverPlayerId,
|
|
long aboutPlayerId)
|
|
{
|
|
NotificationDB.ClientNotification message =
|
|
NotificationDB.CreateFriendIntroduction(
|
|
senderPlayerId,
|
|
receiverPlayerId,
|
|
aboutPlayerId);
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
}
|
|
|
|
public static Task NotifyFriendRemovedAsync(
|
|
long firstPlayerId,
|
|
long secondPlayerId) =>
|
|
NotifyRelationshipChangedAsync(firstPlayerId, secondPlayerId);
|
|
|
|
public static async Task NotifyChatMessageReceivedAsync(
|
|
long receiverPlayerId,
|
|
object chatMessageDto)
|
|
{
|
|
int socketCount = PlayerConnections.TryGetValue(
|
|
receiverPlayerId,
|
|
out var sockets)
|
|
? sockets.Count
|
|
: 0;
|
|
|
|
Console.WriteLine(
|
|
$"[CHAT PUSH] receiver={receiverPlayerId} " +
|
|
$"eventId={ChatMessageReceivedTarget} sockets={socketCount}");
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
ChatMessageReceivedTarget,
|
|
chatMessageDto);
|
|
}
|
|
|
|
public static Task NotifyRelationshipChangedAsync(
|
|
long firstPlayerId,
|
|
long secondPlayerId) =>
|
|
NotifyRelationshipFlagsChangedAsync(
|
|
firstPlayerId,
|
|
secondPlayerId,
|
|
"relationship-change");
|
|
|
|
public static async Task NotifyRelationshipFlagsChangedAsync(
|
|
long firstPlayerId,
|
|
long secondPlayerId,
|
|
string reason)
|
|
{
|
|
|
|
RefreshPersistentSubscriptionsForPlayer(firstPlayerId);
|
|
RefreshPersistentSubscriptionsForPlayer(secondPlayerId);
|
|
|
|
ClientRelationshipDTO firstView =
|
|
RelationshipDB.GetClientRelationship(
|
|
firstPlayerId,
|
|
secondPlayerId) ??
|
|
CreateNoClientRelationship(secondPlayerId);
|
|
|
|
ClientRelationshipDTO secondView =
|
|
RelationshipDB.GetClientRelationship(
|
|
secondPlayerId,
|
|
firstPlayerId) ??
|
|
CreateNoClientRelationship(firstPlayerId);
|
|
|
|
Console.WriteLine(
|
|
$"[RELATIONSHIP LIVE] reason={reason} " +
|
|
$"first={firstPlayerId}->{secondPlayerId} " +
|
|
$"type={firstView.RelationshipType} " +
|
|
$"ignored={firstView.Ignored} muted={firstView.Muted} " +
|
|
$"sockets={GetPlayerConnectionCount(firstPlayerId)}");
|
|
Console.WriteLine(
|
|
$"[RELATIONSHIP LIVE] reason={reason} " +
|
|
$"second={secondPlayerId}->{firstPlayerId} " +
|
|
$"type={secondView.RelationshipType} " +
|
|
$"ignored={secondView.Ignored} muted={secondView.Muted} " +
|
|
$"sockets={GetPlayerConnectionCount(secondPlayerId)}");
|
|
|
|
await Task.WhenAll(
|
|
SendPushSignalToPlayerAsync(
|
|
firstPlayerId,
|
|
RelationshipsInvalidTarget),
|
|
SendPushSignalToPlayerAsync(
|
|
secondPlayerId,
|
|
RelationshipsInvalidTarget));
|
|
|
|
await Task.Delay(50);
|
|
|
|
await Task.WhenAll(
|
|
SendPushEventToPlayerAsync(
|
|
firstPlayerId,
|
|
RelationshipChangedTarget,
|
|
firstView),
|
|
SendPushEventToPlayerAsync(
|
|
secondPlayerId,
|
|
RelationshipChangedTarget,
|
|
secondView));
|
|
|
|
Console.WriteLine(
|
|
$"[RELATIONSHIP LIVE] complete reason={reason} " +
|
|
$"players={firstPlayerId},{secondPlayerId}");
|
|
}
|
|
|
|
private static int GetPlayerConnectionCount(long playerId) =>
|
|
PlayerConnections.TryGetValue(playerId, out var connections)
|
|
? connections.Count
|
|
: 0;
|
|
|
|
public static async Task NotifyPlayerProfileUpdatedAsync(long playerId)
|
|
{
|
|
PlayerDTOBase? account = PlayerDB.GetAccountsBulk(
|
|
new List<long> { playerId },
|
|
callerId: null)
|
|
.FirstOrDefault();
|
|
|
|
if (account == null)
|
|
return;
|
|
|
|
string[] ownConnections = PlayerConnections.TryGetValue(
|
|
playerId,
|
|
out var ownSockets)
|
|
? ownSockets.Keys
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray()
|
|
: Array.Empty<string>();
|
|
|
|
var ownConnectionSet = ownConnections.ToHashSet(StringComparer.Ordinal);
|
|
string[] viewerConnections = GetProfileRecipientConnectionIds(playerId)
|
|
.Where(connectionId => !ownConnectionSet.Contains(connectionId))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
Task selfPush = ownConnections.Length == 0
|
|
? Task.CompletedTask
|
|
: SendProfilePushToConnectionIdsAsync(
|
|
ownConnections,
|
|
account,
|
|
SelfAccountUpdateNotificationId);
|
|
|
|
Task viewerPush = viewerConnections.Length == 0
|
|
? Task.CompletedTask
|
|
: SendProfilePushToConnectionIdsAsync(
|
|
viewerConnections,
|
|
account,
|
|
AccountUpdateNotificationId);
|
|
|
|
await Task.WhenAll(selfPush, viewerPush);
|
|
|
|
Console.WriteLine(
|
|
$"[PROFILE PUSH] player={playerId} " +
|
|
$"selfSockets={ownConnections.Length} viewerSockets={viewerConnections.Length} " +
|
|
$"displayName={account.displayName} emoji={account.displayEmoji} " +
|
|
$"profileImage={account.profileImage}");
|
|
}
|
|
|
|
public static async Task NotifyPlayerPresenceUpdatedAsync(long playerId)
|
|
{
|
|
string[] recipientConnections =
|
|
GetPresenceRecipientConnectionIds(playerId);
|
|
|
|
if (recipientConnections.Length == 0)
|
|
return;
|
|
|
|
var connectionsByViewer = recipientConnections
|
|
.Where(ConnectionPlayers.ContainsKey)
|
|
.GroupBy(connectionId => ConnectionPlayers[connectionId])
|
|
.ToArray();
|
|
|
|
foreach (IGrouping<long, string> viewerConnections in
|
|
connectionsByViewer)
|
|
{
|
|
Heartbeat heartbeat = PlayerDB.GetPlayerHeartbeat(
|
|
playerId,
|
|
viewerConnections.Key);
|
|
|
|
await SendPresencePushToConnectionIdsAsync(
|
|
viewerConnections,
|
|
heartbeat);
|
|
}
|
|
|
|
Heartbeat publicHeartbeat = PlayerDB.GetPlayerHeartbeat(playerId);
|
|
Console.WriteLine(
|
|
$"[PRESENCE PUSH] player={playerId} " +
|
|
$"online={publicHeartbeat.isOnline} " +
|
|
$"room={publicHeartbeat.roomInstance?.roomId.ToString() ?? "none"} " +
|
|
$"instance={publicHeartbeat.roomInstance?.roomInstanceId.ToString() ?? "none"} " +
|
|
$"viewers={connectionsByViewer.Length} " +
|
|
$"sockets={recipientConnections.Length}");
|
|
}
|
|
|
|
public static async Task NotifyProgressionAsync(
|
|
long playerId,
|
|
int oldLevel,
|
|
int newLevel,
|
|
int xp)
|
|
{
|
|
int clientPlayerId = checked((int)playerId);
|
|
|
|
object CreateClientProgression(int level) => new
|
|
{
|
|
PlayerId = clientPlayerId,
|
|
Level = level,
|
|
XP = xp
|
|
};
|
|
|
|
if (newLevel > oldLevel)
|
|
{
|
|
await SendPushEventToPlayerAsync(
|
|
playerId,
|
|
"PlayerProgressionLevelUpdate",
|
|
CreateClientProgression(oldLevel));
|
|
|
|
await Task.Delay(40);
|
|
}
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
playerId,
|
|
"PlayerProgressionLevelUpdate",
|
|
CreateClientProgression(newLevel));
|
|
|
|
if (newLevel > oldLevel)
|
|
{
|
|
Console.WriteLine(
|
|
$"[LEVEL EFFECT] player={playerId} " +
|
|
$"staged={oldLevel}->{newLevel}");
|
|
}
|
|
|
|
await SendHubEventToPlayerAsync(
|
|
playerId,
|
|
"PlayerProgressionUpdated",
|
|
clientPlayerId,
|
|
newLevel,
|
|
xp);
|
|
|
|
await NotifyPlayerProfileUpdatedAsync(playerId);
|
|
|
|
if (newLevel > oldLevel)
|
|
{
|
|
await SendHubEventToPlayerAsync(
|
|
playerId,
|
|
"LevelUpNotified",
|
|
newLevel);
|
|
}
|
|
}
|
|
|
|
public static async Task NotifyCheerAsync(
|
|
long receiverPlayerId,
|
|
NotificationDB.ClientNotification notification,
|
|
int category)
|
|
{
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
MessageReceivedTarget,
|
|
notification);
|
|
|
|
await SendHubEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
"PlayerCheerReceived",
|
|
notification.FromPlayerId,
|
|
category);
|
|
}
|
|
|
|
public static async Task NotifyConsumableRemovedAsync(
|
|
long playerId,
|
|
long mappingId,
|
|
string consumableItemDesc,
|
|
string createdAt,
|
|
int previousCount,
|
|
int remainingCount)
|
|
{
|
|
var payload = new
|
|
{
|
|
Id = mappingId,
|
|
ConsumableItemDesc = consumableItemDesc ?? string.Empty,
|
|
CreatedAt = createdAt,
|
|
Count = Math.Max(0, remainingCount),
|
|
InitialCount = Math.Max(0, previousCount),
|
|
IsActive = false,
|
|
ActiveDurationMinutes = 0,
|
|
IsTransferable = false
|
|
};
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
playerId,
|
|
ConsumableMappingRemovedTarget,
|
|
payload);
|
|
|
|
Console.WriteLine(
|
|
$"[CONSUMABLE PUSH] player={playerId} id={mappingId} " +
|
|
$"count={remainingCount} initial={previousCount} " +
|
|
$"eventId={ConsumableMappingRemovedTarget}");
|
|
}
|
|
|
|
public static async Task<NotificationDB.ClientNotification>
|
|
NotifyRoomInviteAsync(
|
|
long inviterPlayerId,
|
|
long receiverPlayerId,
|
|
long roomId,
|
|
long roomInstanceId,
|
|
string? photonRoomId)
|
|
{
|
|
|
|
Heartbeat inviterHeartbeat =
|
|
PlayerDB.GetPlayerHeartbeat(inviterPlayerId);
|
|
RoomInstance? inviterInstance = inviterHeartbeat.roomInstance;
|
|
bool isExactInviterInstance =
|
|
inviterInstance != null &&
|
|
inviterInstance.roomInstanceId == roomInstanceId &&
|
|
inviterInstance.roomId == roomId;
|
|
|
|
string roomName =
|
|
isExactInviterInstance &&
|
|
!string.IsNullOrWhiteSpace(inviterInstance!.Name)
|
|
? inviterInstance.Name
|
|
: RoomDB.GetRoom(roomId)?.Name ?? "Room";
|
|
|
|
bool isPrivateInstance =
|
|
isExactInviterInstance &&
|
|
Sessions.IsRestrictedInstance(inviterInstance);
|
|
string resolvedPhotonRoomId = isExactInviterInstance
|
|
? inviterInstance!.photonRoomId
|
|
: photonRoomId ?? string.Empty;
|
|
|
|
NotificationDB.ClientNotification message =
|
|
NotificationDB.CreateRoomInvite(
|
|
inviterPlayerId,
|
|
receiverPlayerId,
|
|
roomId,
|
|
roomInstanceId,
|
|
roomName,
|
|
inviteMode: isPrivateInstance ? 1 : 0,
|
|
photonRoomId: resolvedPhotonRoomId);
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
|
|
Console.WriteLine(
|
|
$"[ROOM INVITE PUSH] from={inviterPlayerId} " +
|
|
$"to={receiverPlayerId} messageId={message.Id} " +
|
|
$"messageType={message.Type} data={message.Message} " +
|
|
$"roomInstanceId={roomInstanceId} room={roomId} " +
|
|
$"private={isPrivateInstance.ToString().ToLowerInvariant()} " +
|
|
$"name={roomName} eventId={MessageReceivedTarget} " +
|
|
$"idJsonType=string " +
|
|
$"wireContract=game-invite-v1 " +
|
|
$"sockets={GetPlayerConnectionCount(receiverPlayerId)}");
|
|
|
|
return message;
|
|
}
|
|
|
|
public static async Task NotifyMessageAsync(
|
|
long receiverPlayerId,
|
|
NotificationDB.ClientNotification message)
|
|
{
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
}
|
|
|
|
public static async Task NotifyPartyInviteAsync(
|
|
long inviterPlayerId,
|
|
long receiverPlayerId,
|
|
long partyId)
|
|
{
|
|
NotificationDB.ClientNotification message =
|
|
NotificationDB.CreatePartyInvite(
|
|
inviterPlayerId,
|
|
receiverPlayerId,
|
|
partyId);
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
MessageReceivedTarget,
|
|
message);
|
|
|
|
await SendHubEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
"PartyInviteReceived",
|
|
message);
|
|
}
|
|
|
|
public static async Task NotifyPartyUpdatedAsync(
|
|
IEnumerable<long> memberPlayerIds,
|
|
object party)
|
|
{
|
|
await Task.WhenAll(memberPlayerIds
|
|
.Where(id => id > 0)
|
|
.Distinct()
|
|
.Select(playerId => SendHubEventToPlayerAsync(
|
|
playerId,
|
|
"PartyUpdated",
|
|
party)));
|
|
}
|
|
|
|
public static async Task NotifyGiftAsync(
|
|
long senderPlayerId,
|
|
long receiverPlayerId,
|
|
object? gift = null,
|
|
bool immediate = true)
|
|
{
|
|
await NotifyGiftsAsync(
|
|
senderPlayerId,
|
|
new[]
|
|
{
|
|
new GiftLiveDelivery(receiverPlayerId, gift)
|
|
},
|
|
immediate);
|
|
}
|
|
|
|
public static async Task<GiftLiveDeliveryResult> NotifyGiftsAsync(
|
|
long senderPlayerId,
|
|
IEnumerable<GiftLiveDelivery> deliveries,
|
|
bool immediate = true)
|
|
{
|
|
GiftLiveDelivery[] requested = deliveries?
|
|
.Where(delivery =>
|
|
delivery != null &&
|
|
delivery.ReceiverPlayerId > 0)
|
|
.ToArray() ?? Array.Empty<GiftLiveDelivery>();
|
|
|
|
var result = new GiftLiveDeliveryResult
|
|
{
|
|
TargetPlayers = requested
|
|
.Select(delivery => delivery.ReceiverPlayerId)
|
|
.Distinct()
|
|
.Count()
|
|
};
|
|
|
|
if (requested.Length == 0)
|
|
return result;
|
|
|
|
PreparedGiftLiveDelivery[] prepared = requested
|
|
.Select(delivery =>
|
|
{
|
|
object payload = delivery.Gift is GiftPackage package
|
|
? CreateClientGiftPackagePayload(
|
|
package,
|
|
senderPlayerId,
|
|
delivery.ReceiverPlayerId)
|
|
: delivery.Gift ?? CreateFallbackGiftPackagePayload(
|
|
senderPlayerId,
|
|
delivery.ReceiverPlayerId);
|
|
|
|
int effectiveSenderPlayerId =
|
|
delivery.Gift is GiftPackage sentGift
|
|
? NormalizeGiftSenderId(
|
|
sentGift.FromPlayerId,
|
|
senderPlayerId,
|
|
delivery.ReceiverPlayerId)
|
|
: NormalizeGiftSenderId(
|
|
0,
|
|
senderPlayerId,
|
|
delivery.ReceiverPlayerId);
|
|
|
|
return new PreparedGiftLiveDelivery
|
|
{
|
|
SenderPlayerId = effectiveSenderPlayerId,
|
|
ReceiverPlayerId = delivery.ReceiverPlayerId,
|
|
Payload = payload,
|
|
GiftPackageId = delivery.Gift is GiftPackage sentPackage
|
|
? sentPackage.GiftPackageId
|
|
: 0
|
|
};
|
|
})
|
|
.ToArray();
|
|
|
|
bool primedAnyProfile = false;
|
|
foreach (IGrouping<long, PreparedGiftLiveDelivery> senderGroup in
|
|
prepared.GroupBy(delivery => delivery.SenderPlayerId))
|
|
{
|
|
var connectionOwners = new Dictionary<string, PreparedGiftLiveDelivery>(
|
|
StringComparer.Ordinal);
|
|
foreach (PreparedGiftLiveDelivery delivery in senderGroup)
|
|
{
|
|
foreach (string connectionId in
|
|
GetPlayerConnectionIds(delivery.ReceiverPlayerId))
|
|
{
|
|
connectionOwners[connectionId] = delivery;
|
|
}
|
|
}
|
|
|
|
if (connectionOwners.Count == 0)
|
|
continue;
|
|
|
|
PlayerDTOBase? senderAccount = ResolveGiftSenderAccount(
|
|
senderGroup.Key);
|
|
if (senderAccount == null)
|
|
{
|
|
Console.WriteLine(
|
|
$"[GIFT SENDER CONTEXT] sender={senderGroup.Key} " +
|
|
$"profileMissing=true sockets={connectionOwners.Count}");
|
|
continue;
|
|
}
|
|
|
|
int primedSockets =
|
|
await TrySendProfilePushToConnectionIdsAsync(
|
|
connectionOwners.Keys,
|
|
senderAccount,
|
|
AccountUpdateNotificationId);
|
|
if (primedSockets > 0)
|
|
{
|
|
foreach (KeyValuePair<string, PreparedGiftLiveDelivery> entry in
|
|
connectionOwners)
|
|
{
|
|
entry.Value.PrimedConnectionIds.Add(entry.Key);
|
|
}
|
|
primedAnyProfile = true;
|
|
}
|
|
|
|
Console.WriteLine(
|
|
$"[GIFT SENDER CONTEXT] sender={senderGroup.Key} " +
|
|
$"receivers={connectionOwners.Values.Select(value => value.ReceiverPlayerId).Distinct().Count()} " +
|
|
$"profileEvent={AccountUpdateNotificationId} " +
|
|
$"sockets={primedSockets} batch=true");
|
|
}
|
|
|
|
if (primedAnyProfile)
|
|
await Task.Delay(75);
|
|
|
|
string eventId = immediate
|
|
? GiftPackageReceivedImmediateTarget
|
|
: GiftPackageReceivedTarget;
|
|
|
|
var livePlayerIds = new HashSet<long>();
|
|
foreach (PreparedGiftLiveDelivery delivery in prepared)
|
|
{
|
|
string[] connectionIds =
|
|
GetPlayerConnectionIds(delivery.ReceiverPlayerId);
|
|
if (connectionIds.Length == 0)
|
|
{
|
|
Console.WriteLine(
|
|
$"[GIFT PUSH] from={senderPlayerId} " +
|
|
$"to={delivery.ReceiverPlayerId} " +
|
|
$"effectiveFrom={delivery.SenderPlayerId} " +
|
|
$"package={delivery.GiftPackageId} eventId={eventId} " +
|
|
"idJsonType=string senderProfilePrimed=false sockets=0 " +
|
|
"live=false pendingFallback=true");
|
|
continue;
|
|
}
|
|
|
|
string[] unprimedConnectionIds = connectionIds
|
|
.Where(connectionId =>
|
|
!delivery.PrimedConnectionIds.Contains(connectionId))
|
|
.ToArray();
|
|
if (unprimedConnectionIds.Length > 0)
|
|
{
|
|
PlayerDTOBase? senderAccount = ResolveGiftSenderAccount(
|
|
delivery.SenderPlayerId);
|
|
if (senderAccount != null)
|
|
{
|
|
int primedSockets =
|
|
await TrySendProfilePushToConnectionIdsAsync(
|
|
unprimedConnectionIds,
|
|
senderAccount,
|
|
AccountUpdateNotificationId);
|
|
if (primedSockets > 0)
|
|
{
|
|
foreach (string connectionId in
|
|
unprimedConnectionIds)
|
|
{
|
|
delivery.PrimedConnectionIds.Add(connectionId);
|
|
}
|
|
await Task.Delay(50);
|
|
}
|
|
}
|
|
}
|
|
|
|
string jsonArgument = CreateClientNotificationJson(
|
|
eventId,
|
|
delivery.Payload);
|
|
int deliveredSockets =
|
|
await TrySendPushJsonToConnectionIdsAsync(
|
|
connectionIds,
|
|
eventId,
|
|
jsonArgument);
|
|
|
|
bool deliveredLive = deliveredSockets > 0;
|
|
if (deliveredLive)
|
|
{
|
|
livePlayerIds.Add(delivery.ReceiverPlayerId);
|
|
result.LiveSockets += deliveredSockets;
|
|
}
|
|
|
|
bool senderProfilePrimed = connectionIds.Any(connectionId =>
|
|
delivery.PrimedConnectionIds.Contains(connectionId));
|
|
Console.WriteLine(
|
|
$"[GIFT PUSH] from={senderPlayerId} " +
|
|
$"to={delivery.ReceiverPlayerId} " +
|
|
$"effectiveFrom={delivery.SenderPlayerId} " +
|
|
$"package={delivery.GiftPackageId} eventId={eventId} " +
|
|
$"idJsonType=string senderProfilePrimed={senderProfilePrimed} " +
|
|
$"sockets={deliveredSockets} live={deliveredLive.ToString().ToLowerInvariant()} " +
|
|
$"pendingFallback={!deliveredLive}");
|
|
}
|
|
|
|
result.LivePlayers = livePlayerIds.Count;
|
|
result.OfflinePlayers = Math.Max(
|
|
0,
|
|
result.TargetPlayers - result.LivePlayers);
|
|
Console.WriteLine(
|
|
$"[GIFT LIVE BATCH] sender={senderPlayerId} " +
|
|
$"targets={result.TargetPlayers} livePlayers={result.LivePlayers} " +
|
|
$"liveSockets={result.LiveSockets} offline={result.OfflinePlayers} " +
|
|
$"eventId={eventId}");
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string[] GetPlayerConnectionIds(long playerId)
|
|
{
|
|
if (!PlayerConnections.TryGetValue(
|
|
playerId,
|
|
out var receiverConnections))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
return receiverConnections.Keys
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Where(id =>
|
|
ConnectionPlayers.TryGetValue(id, out long ownerId) &&
|
|
ownerId == playerId)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
}
|
|
|
|
private static PlayerDTOBase? ResolveGiftSenderAccount(
|
|
long senderPlayerId)
|
|
{
|
|
|
|
return PlayerDB.GetAccountsBulk(
|
|
new List<long> { senderPlayerId },
|
|
callerId: null)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
public static async Task NotifyGiftConsumedAsync(
|
|
long receiverPlayerId,
|
|
long giftId,
|
|
IEnumerable<GiftPackage>? remainingGifts = null)
|
|
{
|
|
object[] remainingPayloads = remainingGifts?
|
|
.Select(giftPackage => CreateClientGiftPackagePayload(
|
|
giftPackage,
|
|
giftPackage.FromPlayerId,
|
|
receiverPlayerId))
|
|
.ToArray() ?? Array.Empty<object>();
|
|
|
|
var payload = new
|
|
{
|
|
GiftId = giftId,
|
|
RemainingGifts = remainingPayloads
|
|
};
|
|
|
|
await SendPushEventToPlayerAsync(
|
|
receiverPlayerId,
|
|
GiftConsumedTarget,
|
|
payload);
|
|
|
|
Console.WriteLine(
|
|
$"[GIFT CONSUMED PUSH] player={receiverPlayerId} " +
|
|
$"gift={giftId} remaining={remainingPayloads.Length} " +
|
|
$"eventId={GiftConsumedTarget}");
|
|
}
|
|
|
|
private static object CreateClientGiftPackagePayload(
|
|
GiftPackage gift,
|
|
long senderPlayerId,
|
|
long receiverPlayerId)
|
|
{
|
|
int fromPlayerId = NormalizeGiftSenderId(
|
|
gift.FromPlayerId,
|
|
senderPlayerId,
|
|
receiverPlayerId);
|
|
int giftContext = NormalizeGiftContext(gift.GiftContext);
|
|
|
|
return new
|
|
{
|
|
AvatarItemDesc = EmptyIfWhiteSpace(gift.AvatarItemDesc),
|
|
AvatarItemType = 0,
|
|
BalanceType = gift.BalanceType ??
|
|
(int)BalanceType.NonPurchasedNotUsableInP2P,
|
|
ConsumableItemDesc = EmptyIfWhiteSpace(gift.ConsumableItemDesc),
|
|
Currency = gift.Currency,
|
|
CurrencyType = gift.CurrencyType,
|
|
EquipmentModificationGuid =
|
|
EmptyIfWhiteSpace(gift.EquipmentModificationGuid),
|
|
EquipmentPrefabName =
|
|
EmptyIfWhiteSpace(gift.EquipmentPrefabName),
|
|
FromGiftDropId = 0,
|
|
FromPlayerId = fromPlayerId,
|
|
GiftContext = giftContext,
|
|
GiftRarity = gift.Rarity,
|
|
Id = gift.GiftPackageId,
|
|
Level = 0,
|
|
Message = gift.Message ?? string.Empty,
|
|
Platform = gift.Platform,
|
|
PlatformsToSpawnOn = gift.PlatformMask,
|
|
Xp = gift.XP
|
|
};
|
|
}
|
|
|
|
private static object CreateFallbackGiftPackagePayload(
|
|
long senderPlayerId,
|
|
long receiverPlayerId)
|
|
{
|
|
int fromPlayerId = NormalizeGiftSenderId(
|
|
0,
|
|
senderPlayerId,
|
|
receiverPlayerId);
|
|
|
|
return new
|
|
{
|
|
AvatarItemDesc = string.Empty,
|
|
AvatarItemType = 0,
|
|
BalanceType =
|
|
(int)BalanceType.NonPurchasedNotUsableInP2P,
|
|
ConsumableItemDesc = string.Empty,
|
|
Currency = 0,
|
|
CurrencyType = 2,
|
|
EquipmentModificationGuid = string.Empty,
|
|
EquipmentPrefabName = string.Empty,
|
|
FromGiftDropId = 0,
|
|
FromPlayerId = fromPlayerId,
|
|
GiftContext = 2,
|
|
GiftRarity = 0,
|
|
Id = 0L,
|
|
Level = 0,
|
|
Message = string.Empty,
|
|
Platform = -1,
|
|
PlatformsToSpawnOn = -1,
|
|
Xp = 0
|
|
};
|
|
}
|
|
|
|
private static int NormalizeGiftSenderId(
|
|
int storedSenderPlayerId,
|
|
long requestedSenderPlayerId,
|
|
long receiverPlayerId)
|
|
{
|
|
long senderPlayerId = storedSenderPlayerId > 0
|
|
? storedSenderPlayerId
|
|
: requestedSenderPlayerId;
|
|
|
|
if (senderPlayerId <= 0)
|
|
return 0;
|
|
if (senderPlayerId > int.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Gift sender id {senderPlayerId} cannot be represented by the April 2023 client.");
|
|
}
|
|
|
|
if (senderPlayerId == receiverPlayerId)
|
|
return receiverPlayerId == 1 ? 2 : 1;
|
|
|
|
return checked((int)senderPlayerId);
|
|
}
|
|
|
|
private static int NormalizeGiftContext(int giftContext) =>
|
|
giftContext is (int)PlayerDBClasses.GiftContext.Default
|
|
or (int)PlayerDBClasses.GiftContext.Store_RecCenter
|
|
? (int)PlayerDBClasses.GiftContext.Game_Drop
|
|
: giftContext;
|
|
|
|
private static string EmptyIfWhiteSpace(string? value) =>
|
|
string.IsNullOrWhiteSpace(value) ? string.Empty : value;
|
|
|
|
private static ClientRelationshipDTO CreateNoClientRelationship(
|
|
long otherPlayerId)
|
|
{
|
|
if (otherPlayerId <= 0 || otherPlayerId > int.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Player id {otherPlayerId} cannot be represented by the April 2023 client.");
|
|
}
|
|
|
|
return new ClientRelationshipDTO
|
|
{
|
|
PlayerID = checked((int)otherPlayerId),
|
|
RelationshipType = (int)RelationshipType.None,
|
|
Favorited = 0,
|
|
Muted = 0,
|
|
Ignored = 0
|
|
};
|
|
}
|
|
|
|
private static Task SendPushSignalToPlayerAsync(
|
|
long playerId,
|
|
string eventId) =>
|
|
SendPushJsonToPlayerAsync(
|
|
playerId,
|
|
eventId,
|
|
CreateClientNotificationJson(eventId, null));
|
|
|
|
private static Task SendPushEventToPlayerAsync(
|
|
long playerId,
|
|
string eventId,
|
|
object payload) =>
|
|
SendPushJsonToPlayerAsync(
|
|
playerId,
|
|
eventId,
|
|
CreateClientNotificationJson(eventId, payload));
|
|
|
|
private static Task SendPushEventToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
string eventId,
|
|
object payload) =>
|
|
SendPushJsonToConnectionIdsAsync(
|
|
connectionIds,
|
|
eventId,
|
|
CreateClientNotificationJson(eventId, payload));
|
|
|
|
private static Task SendProfilePushToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
PlayerDTOBase account) =>
|
|
SendProfilePushToConnectionIdsAsync(
|
|
connectionIds,
|
|
account,
|
|
AccountUpdateNotificationId);
|
|
|
|
private static async Task SendProfilePushToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
PlayerDTOBase account,
|
|
string eventId)
|
|
{
|
|
await TrySendProfilePushToConnectionIdsAsync(
|
|
connectionIds,
|
|
account,
|
|
eventId);
|
|
}
|
|
|
|
private static async Task<int> TrySendProfilePushToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
PlayerDTOBase account,
|
|
string eventId)
|
|
{
|
|
string[] ids = connectionIds
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
if (ids.Length == 0)
|
|
return 0;
|
|
|
|
object profile = CreateClientProfilePayload(account);
|
|
string jsonArgument = CreateClientNotificationJson(
|
|
eventId,
|
|
profile);
|
|
|
|
int deliveredSockets = await TrySendTargetToConnectionsAsync(
|
|
ids,
|
|
ClientNotificationHubTarget,
|
|
new object?[] { jsonArgument });
|
|
|
|
Console.WriteLine(
|
|
$"[PROFILE TRANSPORT] player={account.accountId} " +
|
|
$"hubTarget={ClientNotificationHubTarget} " +
|
|
$"eventId={eventId} " +
|
|
$"sockets={deliveredSockets} bytes={jsonArgument.Length}");
|
|
return deliveredSockets;
|
|
}
|
|
|
|
private static object CreateClientProfilePayload(PlayerDTOBase account)
|
|
{
|
|
FullPlayer? fullPlayer = PlayerDB.Players.FindById(account.accountId);
|
|
string? bannerImage = fullPlayer?.Player?.BannerImage;
|
|
if (string.IsNullOrWhiteSpace(bannerImage))
|
|
bannerImage = null;
|
|
|
|
return new
|
|
{
|
|
accountId = checked((int)account.accountId),
|
|
username = account.username ?? string.Empty,
|
|
displayName = account.displayName ?? string.Empty,
|
|
profileImage = account.profileImage ?? string.Empty,
|
|
bannerImage,
|
|
displayEmoji = account.displayEmoji ?? string.Empty,
|
|
isJunior = account.isJunior ?? false,
|
|
platforms = account.platforms,
|
|
personalPronouns = account.personalPronouns,
|
|
identityFlags = account.identityFlags,
|
|
createdAt = account.createdAt
|
|
};
|
|
}
|
|
|
|
private static async Task SendPresencePushToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
Heartbeat heartbeat)
|
|
{
|
|
string[] ids = connectionIds
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
if (ids.Length == 0)
|
|
return;
|
|
|
|
string jsonArgument = CreateClientNotificationJson(
|
|
PresenceUpdateNotificationId,
|
|
heartbeat);
|
|
|
|
await SendTargetToConnectionsAsync(
|
|
ids,
|
|
ClientNotificationHubTarget,
|
|
new object?[] { jsonArgument });
|
|
|
|
Console.WriteLine(
|
|
$"[PRESENCE TRANSPORT] player={heartbeat.playerId} " +
|
|
$"hubTarget={ClientNotificationHubTarget} " +
|
|
$"eventId={PresenceUpdateNotificationId} " +
|
|
$"sockets={ids.Length} bytes={jsonArgument.Length}");
|
|
}
|
|
|
|
private static Task SendPushJsonToPlayerAsync(
|
|
long playerId,
|
|
string eventId,
|
|
string jsonArgument)
|
|
{
|
|
if (!PlayerConnections.TryGetValue(playerId, out var connections))
|
|
{
|
|
Console.WriteLine(
|
|
$"[Push] queued/persisted event={eventId}, " +
|
|
$"player={playerId}, online=false");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
return SendPushJsonToConnectionIdsAsync(
|
|
connections.Keys,
|
|
eventId,
|
|
jsonArgument);
|
|
}
|
|
|
|
private static async Task SendPushJsonToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
string eventId,
|
|
string jsonArgument)
|
|
{
|
|
await TrySendPushJsonToConnectionIdsAsync(
|
|
connectionIds,
|
|
eventId,
|
|
jsonArgument);
|
|
}
|
|
|
|
private static async Task<int> TrySendPushJsonToConnectionIdsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
string eventId,
|
|
string jsonArgument)
|
|
{
|
|
string[] ids = connectionIds
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
if (ids.Length == 0)
|
|
return 0;
|
|
|
|
int deliveredSockets = await TrySendTargetToConnectionsAsync(
|
|
ids,
|
|
ClientNotificationHubTarget,
|
|
new object?[] { jsonArgument });
|
|
|
|
string readableEvent = PushTargetNames.TryGetValue(
|
|
eventId,
|
|
out string? eventName)
|
|
? $"{eventId} ({eventName})"
|
|
: eventId;
|
|
Console.WriteLine(
|
|
$"[Push] hubTarget={ClientNotificationHubTarget}, " +
|
|
$"event={readableEvent}, sockets={deliveredSockets}, " +
|
|
$"idJsonType=string, bytes={jsonArgument.Length}");
|
|
return deliveredSockets;
|
|
}
|
|
|
|
private static string NormalizeClientNotificationId(string eventId)
|
|
{
|
|
|
|
if (string.Equals(
|
|
eventId,
|
|
"11",
|
|
StringComparison.Ordinal) ||
|
|
string.Equals(
|
|
eventId,
|
|
"SubscriptionUpdateProfile",
|
|
StringComparison.Ordinal))
|
|
{
|
|
return AccountUpdateNotificationId;
|
|
}
|
|
|
|
if (string.Equals(
|
|
eventId,
|
|
SubscriptionUpdatePresenceTarget,
|
|
StringComparison.Ordinal) ||
|
|
string.Equals(
|
|
eventId,
|
|
"SubscriptionUpdatePresence",
|
|
StringComparison.Ordinal))
|
|
{
|
|
return PresenceUpdateNotificationId;
|
|
}
|
|
|
|
foreach (KeyValuePair<string, string> entry in PushTargetNames)
|
|
{
|
|
if (string.Equals(
|
|
entry.Value,
|
|
eventId,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return entry.Key;
|
|
}
|
|
}
|
|
|
|
return eventId;
|
|
}
|
|
|
|
private static string CreateClientNotificationJson(
|
|
string eventId,
|
|
object? message)
|
|
{
|
|
|
|
string normalizedEventId = NormalizeClientNotificationId(eventId);
|
|
|
|
return message == null
|
|
? SystemJsonSerializer.Serialize(new
|
|
{
|
|
Id = normalizedEventId
|
|
})
|
|
: SystemJsonSerializer.Serialize(new
|
|
{
|
|
Id = normalizedEventId,
|
|
Msg = message
|
|
});
|
|
}
|
|
|
|
public static Task NotifyRoomUpdatedAsync(
|
|
long recipientPlayerId,
|
|
object roomUpdate) =>
|
|
SendPushEventToPlayerAsync(
|
|
recipientPlayerId,
|
|
RoomUpdateNotificationId,
|
|
roomUpdate);
|
|
|
|
private static async Task SendTargetToConnectionsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
string target,
|
|
object?[] arguments)
|
|
{
|
|
await TrySendTargetToConnectionsAsync(
|
|
connectionIds,
|
|
target,
|
|
arguments);
|
|
}
|
|
|
|
private static async Task<int> TrySendTargetToConnectionsAsync(
|
|
IEnumerable<string> connectionIds,
|
|
string target,
|
|
object?[] arguments)
|
|
{
|
|
IHubContext<NotificationHub>? hubContext = _hubContext;
|
|
if (hubContext == null)
|
|
{
|
|
Console.WriteLine(
|
|
$"[SignalR] Tried to send {target} before broadcaster initialization");
|
|
return 0;
|
|
}
|
|
|
|
string[] ids = connectionIds
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Where(ConnectionPlayers.ContainsKey)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
if (ids.Length == 0)
|
|
return 0;
|
|
|
|
try
|
|
{
|
|
await hubContext.Clients
|
|
.Clients(ids)
|
|
.SendCoreAsync(
|
|
target,
|
|
arguments,
|
|
CancellationToken.None);
|
|
return ids.Length;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(
|
|
$"[SignalR] Send target={target} failed: {ex.Message}");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private static bool TryGetPropertyIgnoreCase(
|
|
JsonElement element,
|
|
string propertyName,
|
|
out JsonElement value)
|
|
{
|
|
foreach (JsonProperty property in element.EnumerateObject())
|
|
{
|
|
if (string.Equals(
|
|
property.Name,
|
|
propertyName,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
value = property.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[ApiController]
|
|
[Mocha2023.Classes.ApiProtection]
|
|
[Route("/noti/debug/v1")]
|
|
public sealed class NotiDebugController : ControllerBase
|
|
{
|
|
[HttpGet("status")]
|
|
public IActionResult Status()
|
|
{
|
|
long? playerId = AuthStuff.GetPlayerId(Request);
|
|
if (!playerId.HasValue)
|
|
return Unauthorized();
|
|
|
|
return Ok(NotiController.GetDebugStatus(playerId.Value));
|
|
}
|
|
}
|
|
}
|