Files
the-cycle/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs
T
neckfireandClaude Opus 4.8 55a9571958
Build & Deploy / build (push) Successful in 32s
fix(matchmaking): deploy signal targets only the deploying player
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>
2026-07-14 17:56:22 +02:00

125 lines
5.6 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SignalR;
using Prospect.Server.Api.Hubs;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class OnSquadMatchmakingSuccessMessage {
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
}
[CloudScriptFunction("EnterMatchmakingMatch")]
public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
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, SignalRConnectionRegistry registry)
{
_httpContextAccessor = httpContextAccessor;
_hubContext = hubContext;
_userDataService = userDataService;
_titleDataService = titleDataService;
_registry = registry;
}
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "Inventory"}
);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
// Compute delivery quest progress before deploy.
// This ensures that the quest progress will be shown on the planet.
// The station doesn't seem to care about delivery quests progress and calculates progress
// based on actual items in stash. And so does the "ClaimActiveContract" function.
foreach (var contractActive in contractsActive.Contracts) {
if (!contracts.TryGetValue(contractActive.ContractID, out var contract)) {
continue;
}
for (var i = 0; i < contract.Objectives.Length; i++) {
var objective = contract.Objectives[i];
// Kill objectives: the client-hosted raid has no dedicated game server
// to report per-kill progress to the backend, so kills would never be
// saved. Auto-credit them on deploy so the objective can be completed
// (mirrors the player-kill auto-credit done in ActivateContract).
if (objective.Type == EYContractObjectiveType.Kills) {
contractActive.Progress[i] = objective.MaxProgress;
continue;
}
if (objective.Type != EYContractObjectiveType.OwnNumOfItem) {
continue;
}
int remaining = objective.MaxProgress;
foreach (var item in inventory) {
if (item.BaseItemId != objective.ItemToOwn) {
continue;
}
remaining -= item.Amount;
if (remaining <= 0) {
remaining = 0;
break;
}
}
contractActive.Progress[i] = objective.MaxProgress - remaining;
}
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["ContractsActive"] = JsonSerializer.Serialize(contractsActive),
}
);
// 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: 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
{
Success = true,
ErrorMessage = "",
SingleplayerStation = false,
NumAttempts = 1,
Blocker = 0,
IsMatchTravel = true,
SessionId = "", // TODO: Not sure how this affects match travel
};
}
}