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.CloudScript.Functions; using Prospect.Server.Api.Services.CloudScript.Models; using Prospect.Server.Api.Services.UserData; // Closed Beta Function public class CompleteInventoryUpdateRequest { [JsonPropertyName("userId")] public string UserID { get; set; } [JsonPropertyName("reason")] public string Reason { get; set; } [JsonPropertyName("newSet")] public LoadoutData NewSet { get; set; } [JsonPropertyName("itemsToAdd")] public FYCustomItemInfo[] ItemsToAdd { get; set; } [JsonPropertyName("itemsToUpdateAmount")] public FYCustomItemInfo[] ItemsToUpdateAmount { get; set; } [JsonPropertyName("itemsToRemove")] public HashSet ItemsToRemove { get; set; } } public class CompleteInventoryUpdateResponse { [JsonPropertyName("userId")] public string UserId { get; set; } [JsonPropertyName("error")] public string Error { get; set; } [JsonPropertyName("newSet")] public LoadoutData NewSet { get; set; } [JsonPropertyName("itemsToAdd")] public FYCustomItemInfo[] ItemsToAdd { get; set; } [JsonPropertyName("itemsToUpdateAmount")] public FYCustomItemInfo[] ItemsToUpdateAmount { get; set; } [JsonPropertyName("itemsToRemove")] public HashSet ItemsToRemove { get; set; } } [CloudScriptFunction("CompleteInventoryUpdate")] public class CompleteInventoryUpdateFunction : ICloudScriptFunction { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly UserDataService _userDataService; private readonly TitleDataService _titleDataService; public CompleteInventoryUpdateFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService) { _logger = logger; _httpContextAccessor = httpContextAccessor; _userDataService = userDataService; _titleDataService = titleDataService; } public async Task ExecuteAsync(CompleteInventoryUpdateRequest 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{"Inventory"}); var inventory = JsonSerializer.Deserialize>(userData["Inventory"].Value); // Insurance payout: this request also reports the end-of-match inventory, so items // the player lost (death / left behind) arrive in ItemsToRemove. Any of those that // were insured on deploy (EnterMatchmakingStation) are recovered back to the stash — // the premium was already paid up-front — and the insurance is consumed (one raid). // Recovery is only performed if the item's policy declares a positive Payout. var recoveredInsured = new HashSet(); if (request.ItemsToRemove is { Count: > 0 }) { try { var td = _titleDataService.Find(new List { "InventoryInsurance" }); if (td.TryGetValue("InventoryInsurance", out var rawPolicies) && !string.IsNullOrEmpty(rawPolicies)) { var policies = JsonSerializer.Deserialize>(rawPolicies); foreach (var item in inventory) { if (!request.ItemsToRemove.Contains(item.ItemId)) continue; if (string.IsNullOrEmpty(item.Insurance) || item.InsuranceOwnerPlayfabId != userId) continue; if (policies == null || !policies.TryGetValue(item.Insurance, out var policy) || policy.Payout <= 0) continue; recoveredInsured.Add(item.ItemId); item.Insurance = ""; // consumed for this raid item.InsuranceOwnerPlayfabId = ""; } if (recoveredInsured.Count > 0) { _logger.LogInformation("Recovered {Count} insured item(s) for user {User}", recoveredInsured.Count, userId); } } } catch (Exception e) { // Never fail the inventory commit because of insurance handling. _logger.LogWarning(e, "Failed to process insurance payout"); } } // TODO: Optimize // TODO: Check deleted items to see if other stacks/mods were updated correctly // Process inventory update. The player may: // 1. Split item stack - creates a new item and updates amount of existing item. // 2. Stack existing items - deletes existing items and updates amount of existing item. // 3. Change vanity data - updates an existing item. // 4. Change weapon mods - updates an existing item. // IMPORTANT: The same request is currently used to report the updated inventory in the end of match. var newInventory = new List(inventory.Count); foreach (var item in inventory) { // Keep items that weren't removed, plus insured items recovered above. if (!request.ItemsToRemove.Contains(item.ItemId) || recoveredInsured.Contains(item.ItemId)) { newInventory.Add(item); } } foreach (var item in request.ItemsToUpdateAmount) { var inventoryItem = newInventory.Find(i => i.ItemId == item.ItemId); if (inventoryItem == null) { continue; } inventoryItem.Amount = item.Amount; inventoryItem.ModData = item.ModData; inventoryItem.PrimaryVanityId = item.PrimaryVanityId; inventoryItem.SecondaryVanityId = item.SecondaryVanityId; inventoryItem.Durability = item.Durability; } // TODO: Check updated items and validate new items foreach (var item in request.ItemsToAdd) { newInventory.Add(item); } await _userDataService.UpdateAsync(userId, userId, new Dictionary{ ["LOADOUT"] = JsonSerializer.Serialize(request.NewSet), ["Inventory"] = JsonSerializer.Serialize(newInventory), }); return new CompleteInventoryUpdateResponse { UserId = userId, //Error = "", //NewSet = request.NewSet, //ItemsToAdd = request.ItemsToAdd, //ItemsToRemove = request.ItemsToRemove, //ItemsToUpdateAmount = request.ItemsToUpdateAmount, }; } }