So, I’m trying to make a card game where you open packs and you get cards, pretty standard thing.
When you open a pack, the contents of the pack get randomized and then the items of the pack are added to the playerInventory item.
This was working until a couple of days ago, when pack opening started failing. I am adding cards one by one to the player inventory because no Batch add exists to add multple things to the player inventory at the same time.
I’ve investigated the issue, and what is happening is that the first call to InventoryAPI adds the card with no issue, but the second call gets rejected with 422 unknown error. Here’s the stack trace:
Unity.Services.CloudCode.Shared.ApiException: unknown at
Unity.Services.CloudCode.Shared.HttpApiClient.ToApiResponse[T](HttpResponseMessage response) at
Unity.Services.CloudCode.Shared.HttpApiClient.SendAsync[T](String path, HttpMethod method, ApiRequestOptions options, IApiConfiguration configuration, CancellationToken cancellationToken) at
Unity.Services.Economy.Api.EconomyInventoryApi.AddInventoryItemAsync(IExecutionContext executionContext, String accessToken, String projectId, String playerId, AddInventoryRequest addInventoryRequest, String configAssignmentHash, String unityInstallationId, String analyticsUserId, CancellationToken cancellationToken) at
InventoryService.FindCardOfSpecifiedRarity(IExecutionContext context, Int32 rarity, List`1 possibleCardsInPack) in
D:\Repos\Unity\TripleWarring\InventoryModule\Project\InventoryService.cs:line 271 at
InventoryService.RandomizeCardsInPackAndAddToCollection(IExecutionContext context, List`1 possibleCardsInPack) in
:\Repos\Unity\TripleWarring\InventoryModule\Project\InventoryService.cs:line 224 at
InventoryService.OpenPack(IExecutionContext context, String playersInventoryPackItemId) in
D:\Repos\Unity\TripleWarring\InventoryModule\Project\InventoryService.cs:line 107 at
InventoryManagerModule.InventoryManager.OpenPack(IExecutionContext context, String playersInventoryPackItemId) in
D:\Repos\Unity\TripleWarring\InventoryModule\Project\InventoryManager.cs:line 31 at
ScriptRunner.Runner.AwaitTaskIfNecessary(Type returnType, Object returnValue) in
/src/ScriptRunner/Runner.cs:line 235 at ScriptRunner.Runner.<>c__DisplayClass10_0.<<RunMethod>b__0>d.MoveNext() in /src/ScriptRunner/Runner.cs:line 138 --- End of stack trace from previous location --- at
ScriptRunner.Runner.RunMethod(MethodInfo method, JObject paramsJson, IServiceProvider services, IServiceCollection serviceCollection) in /src/ScriptRunner/Runner.cs:line 135"
Here’s my code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using InventoryManagerModule;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudSave.Api;
using Unity.Services.Economy.Api;
using Unity.Services.Economy.Model;
public class InventoryService
{
private readonly ILogger<InventoryManager> _logger;
private readonly ICloudSaveDataApi _cloudSaveApi;
private readonly IEconomyInventoryApi _inventoryApi;
private readonly IEconomyConfigurationApi _configurationApi;
private IGameApiClient gameApiClient;
private const int PageSize = 50;
public InventoryService(ILogger<InventoryManager> logger, IGameApiClient injectedGameApiClient)
{
_logger = logger;
gameApiClient = injectedGameApiClient;
_cloudSaveApi = gameApiClient.CloudSaveData;
_inventoryApi = gameApiClient.EconomyInventory;
_configurationApi = gameApiClient.EconomyConfiguration;
}
public async Task<List<CardMetadata>> OpenPack(IExecutionContext context, string playersInventoryPackItemId)
{
try
{
_logger.LogInformation("OpenPack started with playersInventoryPackItemId: {playersInventoryPackItemId}", playersInventoryPackItemId);
var packData = await PopPack(context, playersInventoryPackItemId);
var packsLevels = Enumerable.Range(packData.MinimumPackLevel, packData.MaximumPackLevel - packData.MinimumPackLevel + 1).ToList();
var possibleCardsInPack = await FilterEligibleCards(context, packData.DeckIdsInPack, packsLevels);
var cardsInPack = await RandomizeCardsInPackAndAddToCollection(context, possibleCardsInPack);
_logger.LogInformation("OpenPack completed successfully with cards: {cards}", JsonConvert.SerializeObject(cardsInPack));
return cardsInPack;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in OpenPack");
throw;
}
}
private async Task<PackData> PopPack(IExecutionContext context, string playersInventoryPackItemId)
{
try
{
_logger.LogInformation("PopPack started with playersInventoryPackItemId: {playersInventoryPackItemId}", playersInventoryPackItemId);
var playerInventory = await _inventoryApi.GetPlayerInventoryAsync(context, context.AccessToken, context.ProjectId, context.PlayerId, playersInventoryItemIds: new List<string> { playersInventoryPackItemId });
var config = await _configurationApi.GetPlayerConfigurationAsync(context, context.AccessToken, context.ProjectId, context.PlayerId);
var inventoryConfig = config.Data.Results.Where(configItem => configItem.ActualInstance.GetType() == typeof(InventoryItemResource)).ToList();
var packData = playerInventory.Data.Results.Select(inventoryItem =>
{
var inventoryItemConfig = inventoryConfig.FirstOrDefault(configItem => configItem.GetInventoryItemResource().Id == inventoryItem.InventoryItemId);
return JsonConvert.DeserializeObject<PackData>(inventoryItemConfig?.GetInventoryItemResource().CustomData.ToString());
}).FirstOrDefault();
await _inventoryApi.DeleteInventoryItemAsync(context, context.AccessToken, context.ProjectId, context.PlayerId, playersInventoryPackItemId);
_logger.LogInformation("PopPack completed successfully with packData: {packData}", JsonConvert.SerializeObject(packData));
return packData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in PopPack");
throw;
}
}
private async Task<List<CardMetadata>> FilterEligibleCards(IExecutionContext context, List<int> deckIds, List<int> levels)
{
try
{
_logger.LogInformation("FilterEligibleCards started with deckIds: {deckIds} and levels: {levels}", JsonConvert.SerializeObject(deckIds), JsonConvert.SerializeObject(levels));
var config = await _configurationApi.GetPlayerConfigurationAsync(context, context.AccessToken, context.ProjectId, context.PlayerId);
var inventoryConfig = config.Data.Results.Where(configItem => configItem.ActualInstance.GetType() == typeof(InventoryItemResource)).ToList();
var possibleCardsInPack = inventoryConfig.Where(configItem =>
{
var customData = JsonConvert.DeserializeObject<CardMetadata>(configItem.GetInventoryItemResource().CustomData.ToString());
return (deckIds == null || deckIds.Count == 0 || deckIds.Contains(customData.DeckId)) && levels.Contains(customData.Level);
}).Select(configItem => JsonConvert.DeserializeObject<CardMetadata>(configItem.GetInventoryItemResource().CustomData.ToString())).ToList();
_logger.LogInformation("FilterEligibleCards completed successfully with possibleCardsInPack: {possibleCardsInPack}", JsonConvert.SerializeObject(possibleCardsInPack));
return possibleCardsInPack;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in FilterEligibleCards");
throw;
}
}
private async Task<List<CardMetadata>> RandomizeCardsInPackAndAddToCollection(IExecutionContext context, List<CardMetadata> possibleCardsInPack)
{
try
{
_logger.LogInformation("RandomizeCardsInPackAndAddToCollection started");
var addedCardsResponse = new List<CardMetadata>();
var random = new Random();
while (addedCardsResponse.Count < 5)
{
var cardRarityRoll = random.NextDouble();
CardMetadata addedCard;
if (cardRarityRoll > 0.1)
{
addedCard = await FindCardOfSpecifiedRarity(context, 0, possibleCardsInPack);
}
else if (cardRarityRoll > 0.01 && cardRarityRoll <= 0.1)
{
addedCard = await FindCardOfSpecifiedRarity(context, 1, possibleCardsInPack);
}
else
{
addedCard = await FindCardOfSpecifiedRarity(context, 2, possibleCardsInPack);
}
addedCardsResponse.Add(addedCard);
}
_logger.LogInformation("RandomizeCardsInPackAndAddToCollection completed successfully with addedCardsResponse: {addedCardsResponse}", JsonConvert.SerializeObject(addedCardsResponse));
return addedCardsResponse;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in RandomizeCardsInPackAndAddToCollection");
throw;
}
}
private async Task<CardMetadata> FindCardOfSpecifiedRarity(IExecutionContext context, int rarity, List<CardMetadata> possibleCardsInPack)
{
try
{
_logger.LogInformation("FindCardOfSpecifiedRarity started with rarity: {rarity}", rarity);
var possibleCardsInPackFiltered = possibleCardsInPack.Where(card => card.Rarity == rarity).ToList();
if (!possibleCardsInPackFiltered.Any())
{
possibleCardsInPackFiltered = possibleCardsInPack;
}
var random = new Random();
CardMetadata card = null;
while (card == null)
{
var cardIndex = random.Next(possibleCardsInPackFiltered.Count);
card = possibleCardsInPackFiltered[cardIndex];
if (card.Rarity == rarity)
{
AddInventoryRequest addInventoryRequest = new AddInventoryRequest(card.Id.ToString());
_logger.LogDebug("FindCardOfSpecifiedRarity about to add card: {}", addInventoryRequest.ToJson());
await _inventoryApi.AddInventoryItemAsync(context, context.AccessToken, context.ProjectId, context.PlayerId, addInventoryRequest);
}
}
_logger.LogInformation("FindCardOfSpecifiedRarity completed successfully with card: {card}", JsonConvert.SerializeObject(card));
return card;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in FindCardOfSpecifiedRarity");
throw;
}
}
}
My question is: How can I make this work? am I not using the API the right way? is it not allowed to receibe a burst of requests to the same endpoint? I’m seriously very worried about this because the next feature I’m going to do is mass opening packs. And if the API can’t take two to five calls ina row, how is it gonna cope with 125 to 500 calls?