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 Members { get; } = new(); } public class SquadService { private readonly ConcurrentDictionary _squads = new(); private readonly ConcurrentDictionary _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(), }).ToArray(), }; } }