Build & Deploy / build (push) Successful in 31s
Adds SquadService (singleton, process-local) tracking squads by the squadId the client shares across party members, plus a user->squad index so TryGetCompleteSquadInfo (no squadId in request) can resolve the caller's squad. Members are lazily registered on any squad call. - TryGetCompleteSquadInfo: returns the caller's real squad. - SquadMemberReadyForMatch: joins the squad, records readiness/map, returns the squad + whether everyone is ready. - SquadMemberSelectedMap: records the member's selected map. - DbUserService.FindByIdAsync: resolve display name for squad members. Groundwork for the station lobby; assumes the client drives squads via these calls with a shared squadId (to confirm with a 2-client session). Real-time SignalR push (unknown event names) deferred; the squad is carried in responses + TryGetCompleteSquadInfo for now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.8 KiB
C#
100 lines
3.8 KiB
C#
using System.Collections.Concurrent;
|
|
|
|
namespace Prospect.Server.Api.Services.Squad;
|
|
|
|
// In-memory squad/lobby state. Squads are keyed by the squadId the client provides on the
|
|
// SquadMember* calls (the client forms the party and shares a squadId; each member's client
|
|
// then calls the backend with it). A member is (lazily) registered into that squad on any
|
|
// squad call, and the current squad for a user is tracked so TryGetCompleteSquadInfo — which
|
|
// carries no squadId — can still return it. State is process-local (fine for a small server).
|
|
public class SquadMemberInfo
|
|
{
|
|
public string UserId { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public int OnlineState { get; set; }
|
|
public bool IsReady { get; set; }
|
|
public string SelectedMap { get; set; } = "";
|
|
public bool IsLeader { get; set; }
|
|
}
|
|
|
|
public class SquadInfo
|
|
{
|
|
public string SquadId { get; set; } = "";
|
|
public ConcurrentDictionary<string, SquadMemberInfo> Members { get; } = new();
|
|
}
|
|
|
|
public class SquadService
|
|
{
|
|
private readonly ConcurrentDictionary<string, SquadInfo> _squads = new();
|
|
private readonly ConcurrentDictionary<string, string> _userToSquad = new();
|
|
|
|
public SquadInfo JoinOrCreate(string squadId, string userId, string displayName)
|
|
{
|
|
var squad = _squads.GetOrAdd(squadId, id => new SquadInfo { SquadId = id });
|
|
var isFirst = squad.Members.IsEmpty;
|
|
var member = squad.Members.GetOrAdd(userId, uid => new SquadMemberInfo { UserId = uid, IsLeader = isFirst });
|
|
if (!string.IsNullOrEmpty(displayName)) member.DisplayName = displayName;
|
|
_userToSquad[userId] = squadId;
|
|
return squad;
|
|
}
|
|
|
|
public SquadInfo? GetByUser(string userId)
|
|
=> _userToSquad.TryGetValue(userId, out var sid) && _squads.TryGetValue(sid, out var sq) ? sq : null;
|
|
|
|
public SquadInfo? Get(string squadId)
|
|
=> _squads.TryGetValue(squadId, out var sq) ? sq : null;
|
|
|
|
public void SetReady(string squadId, string userId, bool ready, string? selectedMap)
|
|
{
|
|
if (_squads.TryGetValue(squadId, out var sq) && sq.Members.TryGetValue(userId, out var m))
|
|
{
|
|
m.IsReady = ready;
|
|
if (!string.IsNullOrEmpty(selectedMap)) m.SelectedMap = selectedMap;
|
|
}
|
|
}
|
|
|
|
public void SetMap(string squadId, string userId, string map)
|
|
{
|
|
if (_squads.TryGetValue(squadId, out var sq) && sq.Members.TryGetValue(userId, out var m))
|
|
m.SelectedMap = map;
|
|
}
|
|
|
|
public bool IsSquadReady(string squadId)
|
|
=> _squads.TryGetValue(squadId, out var sq) && !sq.Members.IsEmpty && sq.Members.Values.All(m => m.IsReady);
|
|
|
|
public void Leave(string userId)
|
|
{
|
|
if (_userToSquad.TryRemove(userId, out var sid) && _squads.TryGetValue(sid, out var sq))
|
|
{
|
|
sq.Members.TryRemove(userId, out _);
|
|
if (sq.Members.IsEmpty) _squads.TryRemove(sid, out _);
|
|
}
|
|
}
|
|
|
|
// Build the client-facing squad object (models live in the global namespace).
|
|
public FYPlayFabSquad ToClientSquad(SquadInfo squad)
|
|
{
|
|
return new FYPlayFabSquad
|
|
{
|
|
SquadID = squad.SquadId,
|
|
Members = squad.Members.Values.Select(m => new FYPlayFabSquadMember
|
|
{
|
|
Profile = new FYPlayFabPlayerProfile
|
|
{
|
|
PlayerId = m.UserId,
|
|
DisplayName = m.DisplayName,
|
|
AvatarUrl = "",
|
|
},
|
|
onlineState = m.OnlineState,
|
|
matchmakingSettings = new FYUserMatchmakingSettings
|
|
{
|
|
isReadyForMatch = m.IsReady,
|
|
selectedMapName = m.SelectedMap,
|
|
isSecretLeader = m.IsLeader,
|
|
},
|
|
mapRowNamesUnlocked = Array.Empty<string>(),
|
|
}).ToArray(),
|
|
};
|
|
}
|
|
}
|