GetStoreItems was a stub returning an empty object, so every shop (Korolev / ICA / Osiris / QuickShop vendors) showed up empty. Build the store on the fly from the Blueprints title data: for the requested StoreId, list every item that declares an ItemShopsCraftingData entry for that store, converting its currency recipe ingredients into VirtualCurrencyPrices (SoftCurrency->SC, Aurum->AU, InsuranceCurrency->IN). Item-ingredient (crafting) entries are left to the crafting UI. Adds the StoreId/CatalogVersion fields to the request and the FGetStoreItemsResult / FStoreItem PlayFab response models. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -155,11 +155,72 @@ public class ClientController : Controller
|
|||||||
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
|
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
|
||||||
public IActionResult GetStoreItems(FGetStoreItems request)
|
public IActionResult GetStoreItems(FGetStoreItems request)
|
||||||
{
|
{
|
||||||
return Ok(new ClientResponse<object>
|
// 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<FStoreItem>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var titleData = _titleDataService.Find(new List<string> { "Blueprints" });
|
||||||
|
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(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<string, uint>();
|
||||||
|
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<string, uint>(),
|
||||||
|
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<FGetStoreItemsResult>
|
||||||
{
|
{
|
||||||
Code = 200,
|
Code = 200,
|
||||||
Status = "OK",
|
Status = "OK",
|
||||||
Data = new {}
|
Data = new FGetStoreItemsResult
|
||||||
|
{
|
||||||
|
StoreId = string.IsNullOrEmpty(storeId) ? null : storeId,
|
||||||
|
CatalogVersion = "StaticItems",
|
||||||
|
Store = store,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] Store specific custom data. The data only exists as part of this store; it is not transferred to item instances.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("CustomData")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public object? CustomData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] Intended display position for this item. Note that 0 is the first position and -1 is the last position.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("DisplayPosition")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public uint? DisplayPosition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier of the item as it exists in the catalog - note that this must exactly match the ItemId from the catalog.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("ItemId")]
|
||||||
|
public string ItemId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] Override prices for this item for specific currencies.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("RealCurrencyPrices")]
|
||||||
|
public Dictionary<string, uint> RealCurrencyPrices { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] Override prices for this item in virtual currencies and "RM" (the base Real Money purchase price, in USD pennies).
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("VirtualCurrencyPrices")]
|
||||||
|
public Dictionary<string, uint> VirtualCurrencyPrices { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Prospect.Server.Api.Models.Client.Data;
|
using Prospect.Server.Api.Models.Client.Data;
|
||||||
|
|
||||||
namespace Prospect.Server.Api.Models.Client;
|
namespace Prospect.Server.Api.Models.Client;
|
||||||
|
|
||||||
public class FGetStoreItems
|
public class FGetStoreItems
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] Catalog version to store items from. Use default catalog version if null.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("CatalogVersion")]
|
||||||
|
public string? CatalogVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for the store which is being requested.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("StoreId")]
|
||||||
|
public string? StoreId { get; set; }
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] The base catalog that this store is a part of.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("CatalogVersion")]
|
||||||
|
public string? CatalogVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] Array of items which can be purchased from this store.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("Store")]
|
||||||
|
public List<FStoreItem> Store { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [optional] The ID of this store.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("StoreId")]
|
||||||
|
public string? StoreId { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user