From 310e2bf7e934aab1d3ba774788248a9452dc1f4f Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 14:18:44 +0200 Subject: [PATCH] feat(social): friends list resolution + fix ping request 500 - RequestUsersPingRequest: the client sends ping=2147483648 (int.MaxValue+1) for "no ping", which overflowed the int field and made every ping request return 500. Widened to long. - ClientsideFriendsImport: persist the imported Steam friend ids per user. - GetFriendList: resolve those ids to the players that exist on this server (batch lookup by Steam auth key) and return them as friends. Response shape is best-effort pending in-game confirmation. - DbUserService.FindManyAsync: batch-resolve auth keys to players. First step of the station lobby (see friends on the server). Squad formation / invite flow to follow once friends are visible. Co-Authored-By: Claude Opus 4.8 --- .../Functions/ClientsideFriendsImport.cs | 25 ++++++- .../CloudScript/Functions/GetFriendList.cs | 70 +++++++++++++++++-- .../Functions/RequestUsersPingRequest.cs | 2 +- .../Services/Database/DbUserService.cs | 16 ++++- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/ClientsideFriendsImport.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/ClientsideFriendsImport.cs index ff45d57..68e83b6 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/ClientsideFriendsImport.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/ClientsideFriendsImport.cs @@ -1,5 +1,8 @@ +using System.Text.Json; using System.Text.Json.Serialization; +using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript; +using Prospect.Server.Api.Services.UserData; public class ClientsideFriendsImportRequest { @@ -18,15 +21,33 @@ public class ClientsideFriendsImportResponse public class ClientsideFriendsImportFunction : ICloudScriptFunction { private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserDataService _userDataService; - public ClientsideFriendsImportFunction(ILogger logger) + public ClientsideFriendsImportFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService) { _logger = logger; + _httpContextAccessor = httpContextAccessor; + _userDataService = userDataService; } public async Task ExecuteAsync(ClientsideFriendsImportRequest request) { - _logger.LogInformation("Processing friends import"); + var context = _httpContextAccessor.HttpContext; + if (context == null) + { + throw new CloudScriptException("CloudScript was not called within a http request"); + } + var userId = context.User.FindAuthUserId(); + + // Persist the platform friend ids the client imports (Steam ids) so GetFriendList + // can later resolve which of them actually play on this server. + var ids = request.UserIDs ?? Array.Empty(); + await _userDataService.UpdateAsync(userId, userId, new Dictionary + { + ["ImportedSteamFriends"] = JsonSerializer.Serialize(ids), + }); + _logger.LogInformation("Imported {Count} {Platform} friend id(s) for user {User}", ids.Length, request.Platform, userId); return new ClientsideFriendsImportResponse{}; } diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetFriendList.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetFriendList.cs index 34d6d78..0bc51b0 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetFriendList.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetFriendList.cs @@ -1,15 +1,71 @@ -using Prospect.Server.Api.Services.CloudScript.Models; +using System.Text.Json; +using Prospect.Server.Api.Services.Auth.Extensions; +using Prospect.Server.Api.Services.CloudScript.Models; +using Prospect.Server.Api.Services.Database; +using Prospect.Server.Api.Services.Database.Models; +using Prospect.Server.Api.Services.UserData; namespace Prospect.Server.Api.Services.CloudScript.Functions; [CloudScriptFunction("GetFriendList")] public class GetFriendList : ICloudScriptFunction { - public Task ExecuteAsync(FYBaseSocialRequest request) + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserDataService _userDataService; + private readonly DbUserService _userService; + + public GetFriendList(ILogger logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, DbUserService userService) { - return Task.FromResult(new - { - - }); + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _userDataService = userDataService; + _userService = userService; } -} \ No newline at end of file + + public async Task ExecuteAsync(FYBaseSocialRequest request) + { + var context = _httpContextAccessor.HttpContext; + if (context == null) + { + throw new CloudScriptException("CloudScript was not called within a http request"); + } + var userId = context.User.FindAuthUserId(); + + // Resolve the imported Steam friends (persisted by ClientsideFriendsImport) to the + // players that actually exist on this server. It's a private server, so only friends + // who have logged in here will show up. + var steamIds = new List(); + var userData = await _userDataService.FindAsync(userId, userId, new List { "ImportedSteamFriends" }); + if (userData.TryGetValue("ImportedSteamFriends", out var rec) && !string.IsNullOrWhiteSpace(rec.Value)) + { + try { steamIds = JsonSerializer.Deserialize>(rec.Value) ?? new List(); } + catch { /* ignore malformed */ } + } + + var friends = new List(); + if (steamIds.Count > 0) + { + var players = await _userService.FindManyAsync(PlayFabUserAuthType.Steam, steamIds); + foreach (var player in players) + { + if (player.Id == userId) continue; // never list yourself + friends.Add(new + { + profile = new + { + playerId = player.Id, + displayName = player.DisplayName, + avatarUrl = "", + }, + onlineState = 0, // EYUserState — real presence tracking is a later phase + }); + } + } + + // NOTE: the exact response shape the client expects is not yet confirmed; this is a + // best-effort structure. The log lets us verify resolution while we validate in game. + _logger.LogInformation("GetFriendList for {User}: {Count} friend(s) resolved on server", userId, friends.Count); + return new { friends }; + } +} diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestUsersPingRequest.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestUsersPingRequest.cs index 9a6f9c1..c522a0a 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestUsersPingRequest.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestUsersPingRequest.cs @@ -6,7 +6,7 @@ public class RegionObject [JsonPropertyName("region")] public string Region { get; set; } [JsonPropertyName("ping")] - public int Ping { get; set; } + public long Ping { get; set; } // client sends 2147483648 (int.MaxValue+1) for "no ping" -> must be long or deserialization 500s [JsonPropertyName("instanceType")] public int InstanceType { get; set; } } diff --git a/src/Prospect.Server.Api/Services/Database/DbUserService.cs b/src/Prospect.Server.Api/Services/Database/DbUserService.cs index db759c5..9feee53 100644 --- a/src/Prospect.Server.Api/Services/Database/DbUserService.cs +++ b/src/Prospect.Server.Api/Services/Database/DbUserService.cs @@ -40,7 +40,21 @@ public class DbUserService : BaseDbService public async Task FindOrCreateAsync(PlayFabUserAuthType type, string key) { - return await FindAsync(type, key) ?? + return await FindAsync(type, key) ?? await CreateAsync(type, key); } + + // Resolve several auth keys (e.g. a batch of Steam IDs) to the players that exist on + // this server in a single query. Used by the friends list to keep only the imported + // Steam friends who have actually played here. + public async Task> FindManyAsync(PlayFabUserAuthType type, IReadOnlyCollection keys) + { + if (keys == null || keys.Count == 0) + { + return new List(); + } + var wanted = new HashSet(keys); + return await Collection.Find(user => user.Auth.Any(auth => + auth.Type == type && wanted.Contains(auth.Key))).ToListAsync(); + } } \ No newline at end of file