diff --git a/src/Prospect.Server.Api/Controllers/ClientController.cs b/src/Prospect.Server.Api/Controllers/ClientController.cs index e20e172..a5aee37 100644 --- a/src/Prospect.Server.Api/Controllers/ClientController.cs +++ b/src/Prospect.Server.Api/Controllers/ClientController.cs @@ -155,11 +155,72 @@ public class ClientController : Controller [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)] public IActionResult GetStoreItems(FGetStoreItems request) { - return Ok(new ClientResponse + // The vendors (Korolev / ICA / Osiris / QuickShop) and other stores are defined + // inside the Blueprints title data: each item lists a per-store entry under + // ItemShopsCraftingData keyed by the store id, with the currency price in its + // recipe ingredients. This endpoint was previously a stub returning {}, which is + // why every shop showed up empty. We build the store on the fly from that data. + var storeId = request.StoreId ?? ""; + var store = new List(); + try + { + var titleData = _titleDataService.Find(new List { "Blueprints" }); + var blueprints = JsonSerializer.Deserialize>(titleData["Blueprints"]); + if (blueprints != null && !string.IsNullOrEmpty(storeId)) + { + uint position = 0; + foreach (var (itemId, blueprint) in blueprints) + { + if (blueprint.ItemShopsCraftingData == null) continue; + if (!blueprint.ItemShopsCraftingData.TryGetValue(storeId, out var shopData)) continue; + if (shopData?.ItemRecipeIngredients == null) continue; + + // Only currency ingredients form a purchasable price. Entries whose + // recipe is item-based (crafting) are handled by the crafting UI from + // the blueprint data the client already has, not by the buy store. + var prices = new Dictionary(); + foreach (var ingredient in shopData.ItemRecipeIngredients) + { + var code = ingredient.Currency switch + { + "SoftCurrency" => "SC", + "Aurum" => "AU", + "InsuranceCurrency" => "IN", + _ => null, + }; + if (code == null || ingredient.Amount < 0) continue; + prices[code] = prices.TryGetValue(code, out var current) + ? current + (uint)ingredient.Amount + : (uint)ingredient.Amount; + } + if (prices.Count == 0) continue; + + store.Add(new FStoreItem + { + ItemId = itemId, + VirtualCurrencyPrices = prices, + RealCurrencyPrices = new Dictionary(), + DisplayPosition = position++, + }); + } + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Failed to build store items for store {StoreId}", storeId); + } + + _logger.LogInformation("GetStoreItems '{StoreId}': {Count} item(s)", storeId, store.Count); + return Ok(new ClientResponse { Code = 200, Status = "OK", - Data = new {} + Data = new FGetStoreItemsResult + { + StoreId = string.IsNullOrEmpty(storeId) ? null : storeId, + CatalogVersion = "StaticItems", + Store = store, + } }); } diff --git a/src/Prospect.Server.Api/Models/Client/Data/FStoreItem.cs b/src/Prospect.Server.Api/Models/Client/Data/FStoreItem.cs new file mode 100644 index 0000000..f00c233 --- /dev/null +++ b/src/Prospect.Server.Api/Models/Client/Data/FStoreItem.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Prospect.Server.Api.Models.Client.Data; + +// PlayFab StoreItem: a single catalog item offered for sale in a store, with its prices. +public class FStoreItem +{ + /// + /// [optional] Store specific custom data. The data only exists as part of this store; it is not transferred to item instances. + /// + [JsonPropertyName("CustomData")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public object? CustomData { get; set; } + + /// + /// [optional] Intended display position for this item. Note that 0 is the first position and -1 is the last position. + /// + [JsonPropertyName("DisplayPosition")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public uint? DisplayPosition { get; set; } + + /// + /// Unique identifier of the item as it exists in the catalog - note that this must exactly match the ItemId from the catalog. + /// + [JsonPropertyName("ItemId")] + public string ItemId { get; set; } = ""; + + /// + /// [optional] Override prices for this item for specific currencies. + /// + [JsonPropertyName("RealCurrencyPrices")] + public Dictionary RealCurrencyPrices { get; set; } = new(); + + /// + /// [optional] Override prices for this item in virtual currencies and "RM" (the base Real Money purchase price, in USD pennies). + /// + [JsonPropertyName("VirtualCurrencyPrices")] + public Dictionary VirtualCurrencyPrices { get; set; } = new(); +} diff --git a/src/Prospect.Server.Api/Models/Client/FGetStoreItems.cs b/src/Prospect.Server.Api/Models/Client/FGetStoreItems.cs index 659aa1e..f22ed2a 100644 --- a/src/Prospect.Server.Api/Models/Client/FGetStoreItems.cs +++ b/src/Prospect.Server.Api/Models/Client/FGetStoreItems.cs @@ -1,8 +1,19 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Prospect.Server.Api.Models.Client.Data; namespace Prospect.Server.Api.Models.Client; public class FGetStoreItems { -} \ No newline at end of file + /// + /// [optional] Catalog version to store items from. Use default catalog version if null. + /// + [JsonPropertyName("CatalogVersion")] + public string? CatalogVersion { get; set; } + + /// + /// Unique identifier for the store which is being requested. + /// + [JsonPropertyName("StoreId")] + public string? StoreId { get; set; } +} diff --git a/src/Prospect.Server.Api/Models/Client/FGetStoreItemsResult.cs b/src/Prospect.Server.Api/Models/Client/FGetStoreItemsResult.cs new file mode 100644 index 0000000..036b7ac --- /dev/null +++ b/src/Prospect.Server.Api/Models/Client/FGetStoreItemsResult.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; +using Prospect.Server.Api.Models.Client.Data; + +namespace Prospect.Server.Api.Models.Client; + +public class FGetStoreItemsResult +{ + /// + /// [optional] The base catalog that this store is a part of. + /// + [JsonPropertyName("CatalogVersion")] + public string? CatalogVersion { get; set; } + + /// + /// [optional] Array of items which can be purchased from this store. + /// + [JsonPropertyName("Store")] + public List Store { get; set; } = new(); + + /// + /// [optional] The ID of this store. + /// + [JsonPropertyName("StoreId")] + public string? StoreId { get; set; } +}