using System.Text.Json; using Prospect.Server.Api.Models.Data; using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript.Models; using Prospect.Server.Api.Services.CloudScript.Models.Data; using Prospect.Server.Api.Services.UserData; namespace Prospect.Server.Api.Services.CloudScript.Functions; [CloudScriptFunction("RequestPlayerContracts")] public class RequestPlayerContracts : ICloudScriptFunction { // Board slots are ordered Easy / Medium / Hard (same convention as ClaimActiveContract). private static readonly string[] Difficulties = ["Easy", "Medium", "Hard"]; private const int RefreshHours = 12; private readonly IHttpContextAccessor _httpContextAccessor; private readonly UserDataService _userDataService; private readonly TitleDataService _titleDataService; private readonly Random _rnd = new(); public RequestPlayerContracts(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService) { _httpContextAccessor = httpContextAccessor; _userDataService = userDataService; _titleDataService = titleDataService; } public async Task ExecuteAsync(FYGetPlayerContractsRequest 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 userData = await _userDataService.FindAsync( userId, userId, new List { "ContractsActive", "JobBoardsData", "FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris", } ); var contractsActive = JsonSerializer.Deserialize(userData["ContractsActive"].Value)!; var jobBoardsData = JsonSerializer.Deserialize(userData["JobBoardsData"].Value)!; var titleData = _titleDataService.Find(new List { "Contracts", "Jobs", "LevelData" }); var contracts = JsonSerializer.Deserialize>(titleData["Contracts"])!; var jobs = JsonSerializer.Deserialize>>(titleData["Jobs"])!; var levelsData = JsonSerializer.Deserialize>(titleData["LevelData"])!; var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var lastRefresh = jobBoardsData.LastBoardRefreshTimeUtc?.Seconds ?? 0; // Initial fill only: populate boards that were seeded empty / never refreshed. // The periodic rotation is owned by RequestActiveObjectivesAndBoardsData, so we // don't refresh on a timer here to avoid two competing refresh policies. var boardsEmpty = jobBoardsData.Boards == null || jobBoardsData.Boards.Any(b => b.Contracts == null || b.Contracts.Any(c => string.IsNullOrEmpty(c.ContractId))); if (jobBoardsData.Boards != null && (lastRefresh == 0 || boardsEmpty)) { foreach (var board in jobBoardsData.Boards) { if (board.Contracts == null || !jobs.TryGetValue(board.FactionId, out var factionJobs)) { continue; } var factionKey = "FactionProgression" + board.FactionId; var factionProgression = userData.TryGetValue(factionKey, out var fp) ? JsonSerializer.Deserialize(fp.Value) : 0; var level = levelsData.TryGetValue(board.FactionId, out var levels) ? levels.Length - Array.FindIndex(levels, xp => factionProgression >= xp) : 0; for (var idx = 0; idx < board.Contracts.Count && idx < Difficulties.Length; idx++) { if (!factionJobs.TryGetValue(Difficulties[idx], out var difficultyJobs)) { continue; } var available = Array.FindAll(difficultyJobs, jobId => contracts.TryGetValue(jobId, out var c) && level >= c.UnlockData.Level); if (available.Length > 0) { board.Contracts[idx].ContractId = available[_rnd.Next(available.Length)]; } } } jobBoardsData.LastBoardRefreshTimeUtc = new FYTimestamp { Seconds = now }; await _userDataService.UpdateAsync( userId, userId, new Dictionary { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) } ); } return new FYGetPlayerContractsResult { UserId = userId, Error = null, ActiveContracts = contractsActive.Contracts, FactionsContracts = new FYFactionsContractsData { Boards = jobBoardsData.Boards, LastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc ?? new FYTimestamp { Seconds = now }, }, RefreshHours24UtcFromBackend = RefreshHours, }; } }