diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs index b86fb2c..f36a28a 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs @@ -1,6 +1,9 @@ using System.Text.Json.Serialization; using Prospect.Server.Api.Services.CloudScript; using Prospect.Server.Api.Services.Auth.Extensions; +using Prospect.Server.Api.Services.Database; +using Prospect.Server.Api.Services.Database.Models; +using Prospect.Server.Api.Services.Squad; using Microsoft.AspNetCore.SignalR; using Prospect.Server.Api.Hubs; @@ -28,12 +31,16 @@ public class SquadMemberReadyForMatchFunction : ICloudScriptFunction _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHubContext _hubContext; + private readonly SquadService _squadService; + private readonly DbUserService _userService; - public SquadMemberReadyForMatchFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, IHubContext hubContext) + public SquadMemberReadyForMatchFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, IHubContext hubContext, SquadService squadService, DbUserService userService) { _httpContextAccessor = httpContextAccessor; _logger = logger; _hubContext = hubContext; + _squadService = squadService; + _userService = userService; } public async Task ExecuteAsync(SquadMemberReadyForMatchRequest request) @@ -44,27 +51,25 @@ public class SquadMemberReadyForMatchFunction : ICloudScriptFunction { private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SquadService _squadService; - public SquadMemberSelectedMapFunction(ILogger logger) + public SquadMemberSelectedMapFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, SquadService squadService) { _logger = logger; + _httpContextAccessor = httpContextAccessor; + _squadService = squadService; } - public async Task ExecuteAsync(SquadMemberSelectedMapRequest request) + public Task ExecuteAsync(SquadMemberSelectedMapRequest request) { - return new SquadMemberSelectedMapResponse - {}; + var context = _httpContextAccessor.HttpContext; + if (context != null && !string.IsNullOrEmpty(request.SquadID)) + { + var userId = context.User.FindAuthUserId(); + _squadService.JoinOrCreate(request.SquadID, userId, ""); + _squadService.SetMap(request.SquadID, userId, request.SelectedMapName ?? ""); + _logger.LogInformation("SquadMemberSelectedMap: {User} -> {Map} (squad {Squad})", userId, request.SelectedMapName, request.SquadID); + } + return Task.FromResult(new SquadMemberSelectedMapResponse { }); } } diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs index c73d420..9a4fc94 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs @@ -1,5 +1,8 @@ using System.Text.Json.Serialization; +using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript; +using Prospect.Server.Api.Services.Database; +using Prospect.Server.Api.Services.Squad; public class TryGetCompleteSquadInfoRequest { @@ -51,26 +54,30 @@ public class TryGetCompleteSquadInfoFunction : ICloudScriptFunction _logger; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SquadService _squadService; - public TryGetCompleteSquadInfoFunction(ILogger logger, IHttpContextAccessor httpContextAccessor) + public TryGetCompleteSquadInfoFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, SquadService squadService) { _httpContextAccessor = httpContextAccessor; _logger = logger; + _squadService = squadService; } - public async Task ExecuteAsync(TryGetCompleteSquadInfoRequest request) + public Task ExecuteAsync(TryGetCompleteSquadInfoRequest request) { var context = _httpContextAccessor.HttpContext; if (context == null) { throw new CloudScriptException("CloudScript was not called within a http request"); } - // var userId = context.User.FindAuthUserId(); - - return new FYPlayFabSquad + var userId = context.User.FindAuthUserId(); + var squad = _squadService.GetByUser(userId); + if (squad == null) { - // SquadID = "100", - // Members = [], - }; + // Not in a squad yet — return an empty squad (same as the previous stub). + return Task.FromResult(new FYPlayFabSquad()); + } + _logger.LogInformation("TryGetCompleteSquadInfo: user {User} in squad {Squad} ({Count} member(s))", userId, squad.SquadId, squad.Members.Count); + return Task.FromResult(_squadService.ToClientSquad(squad)); } } diff --git a/src/Prospect.Server.Api/Services/Database/DbUserService.cs b/src/Prospect.Server.Api/Services/Database/DbUserService.cs index 9feee53..206e58f 100644 --- a/src/Prospect.Server.Api/Services/Database/DbUserService.cs +++ b/src/Prospect.Server.Api/Services/Database/DbUserService.cs @@ -13,11 +13,17 @@ public class DbUserService : BaseDbService public async Task FindAsync(PlayFabUserAuthType type, string key) { - return await Collection.Find(user => user.Auth.Any(auth => - auth.Type == type && + return await Collection.Find(user => user.Auth.Any(auth => + auth.Type == type && auth.Key == key)).FirstOrDefaultAsync(); } + // Resolve a player by its PlayFab id (used to display squad member names). + public async Task FindByIdAsync(string id) + { + return await Collection.Find(user => user.Id == id).FirstOrDefaultAsync(); + } + private async Task CreateAsync(PlayFabUserAuthType type, string key) { var user = new PlayFabUser diff --git a/src/Prospect.Server.Api/Services/Squad/SquadService.cs b/src/Prospect.Server.Api/Services/Squad/SquadService.cs new file mode 100644 index 0000000..d43ca80 --- /dev/null +++ b/src/Prospect.Server.Api/Services/Squad/SquadService.cs @@ -0,0 +1,99 @@ +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(), + }; + } +} diff --git a/src/Prospect.Server.Api/Startup.cs b/src/Prospect.Server.Api/Startup.cs index 7d7e3fc..a294d5e 100644 --- a/src/Prospect.Server.Api/Startup.cs +++ b/src/Prospect.Server.Api/Startup.cs @@ -35,6 +35,8 @@ public class Startup services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); services.AddSingleton();