fix(matchmaking): deploy signal targets only the deploying player
Build & Deploy / build (push) Successful in 32s
Build & Deploy / build (push) Successful in 32s
EnterMatchmakingMatch broadcast OnSquadMatchmakingSuccess to Clients.All, so one player deploying pulled every connected player into the match. Map each player to their SignalR connection (GetSignalRConnection tags the URL with uid; CycleHub records it via a SignalRConnectionRegistry) and send the deploy signal only to that connection (broadcast fallback if unmapped). Also enable m_automaticImportPlatformFriends (Steam friends). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,31 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Prospect.Server.Api.Hubs;
|
||||
|
||||
public class CycleHub : Hub
|
||||
{
|
||||
private readonly SignalRConnectionRegistry _registry;
|
||||
|
||||
public CycleHub(SignalRConnectionRegistry registry)
|
||||
{
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
Console.WriteLine("Connected {0}", Context.ConnectionId);
|
||||
// The client connects with the SignalR URL returned by GetSignalRConnection, which
|
||||
// carries a `uid` query param — use it to map this connection to the player so
|
||||
// server-initiated messages can target a single player instead of everyone.
|
||||
var uid = Context.GetHttpContext()?.Request.Query["uid"].ToString();
|
||||
if (!string.IsNullOrEmpty(uid))
|
||||
_registry.Add(uid, Context.ConnectionId);
|
||||
Console.WriteLine("Connected {0} (uid={1})", Context.ConnectionId, uid);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
_registry.Remove(Context.ConnectionId);
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Prospect.Server.Api.Hubs;
|
||||
|
||||
// Maps players to their live SignalR connection so server-initiated messages (e.g. the
|
||||
// matchmaking-success "travel to match" signal) can target a single player instead of being
|
||||
// broadcast to everyone. The client's SignalR URL carries a `uid` query param (added by
|
||||
// GetSignalRConnection); CycleHub records it on connect.
|
||||
public class SignalRConnectionRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, string> _userToConn = new();
|
||||
private readonly ConcurrentDictionary<string, string> _connToUser = new();
|
||||
|
||||
public void Add(string userId, string connectionId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId)) return;
|
||||
_userToConn[userId] = connectionId;
|
||||
_connToUser[connectionId] = userId;
|
||||
}
|
||||
|
||||
public void Remove(string connectionId)
|
||||
{
|
||||
if (_connToUser.TryRemove(connectionId, out var userId))
|
||||
_userToConn.TryRemove(userId, out _);
|
||||
}
|
||||
|
||||
public string? GetConnection(string userId)
|
||||
=> _userToConn.TryGetValue(userId, out var c) ? c : null;
|
||||
}
|
||||
@@ -24,13 +24,15 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAz
|
||||
private readonly IHubContext<CycleHub> _hubContext;
|
||||
private readonly UserDataService _userDataService;
|
||||
private readonly TitleDataService _titleDataService;
|
||||
private readonly SignalRConnectionRegistry _registry;
|
||||
|
||||
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
|
||||
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, SignalRConnectionRegistry registry)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_hubContext = hubContext;
|
||||
_userDataService = userDataService;
|
||||
_titleDataService = titleDataService;
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
|
||||
@@ -94,11 +96,20 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAz
|
||||
}
|
||||
);
|
||||
|
||||
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
|
||||
// Send the "matchmaking succeeded / travel to match" signal only to the deploying
|
||||
// player (via their mapped SignalR connection) — broadcasting to Clients.All made
|
||||
// every connected player travel too. Fall back to broadcast only if the connection
|
||||
// isn't mapped yet.
|
||||
var deployMessage = new OnSquadMatchmakingSuccessMessage {
|
||||
Success = true,
|
||||
SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
|
||||
SessionID = request.MapName, // TODO: pass real squad session info
|
||||
SquadID = request.SquadId,
|
||||
});
|
||||
};
|
||||
var connectionId = _registry.GetConnection(userId);
|
||||
if (connectionId != null)
|
||||
await _hubContext.Clients.Client(connectionId).SendAsync("OnSquadMatchmakingSuccess", deployMessage);
|
||||
else
|
||||
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", deployMessage);
|
||||
|
||||
return new FYEnterMatchmakingResult
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Prospect.Server.Api.Config;
|
||||
using Prospect.Server.Api.Services.Auth.Extensions;
|
||||
using Prospect.Server.Api.Services.CloudScript.Models;
|
||||
|
||||
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
||||
@@ -43,7 +44,13 @@ public class GetSignalRConnection : ICloudScriptFunction<FYGetSignalRConnection,
|
||||
catch { /* keep the configured URL on any parsing issue */ }
|
||||
}
|
||||
|
||||
_logger.LogInformation("GetSignalRConnection: remoteIp={Remote} lan={Lan} -> {Url}", remote, lan, url);
|
||||
// Tag the connection with the player id so the hub can map connection -> user and
|
||||
// target server messages (deploy signal) instead of broadcasting to everyone.
|
||||
var userId = _httpContextAccessor.HttpContext?.User.FindAuthUserId();
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
url += (url.Contains('?') ? "&" : "?") + "uid=" + Uri.EscapeDataString(userId);
|
||||
|
||||
_logger.LogInformation("GetSignalRConnection: remoteIp={Remote} lan={Lan} uid={Uid} -> {Url}", remote, lan, userId, url);
|
||||
|
||||
return Task.FromResult(new FYGetSignalRConnectionResult
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -36,6 +36,7 @@ public class Startup
|
||||
services.AddSingleton<DbUserDataService>();
|
||||
|
||||
services.AddSingleton<Prospect.Server.Api.Services.Squad.SquadService>();
|
||||
services.AddSingleton<Prospect.Server.Api.Hubs.SignalRConnectionRegistry>();
|
||||
|
||||
services.AddHostedService<QosService>();
|
||||
services.AddSingleton<QosServer>();
|
||||
|
||||
Reference in New Issue
Block a user