Second call in a row to add items fails with 422 unknown

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?

Hello, could I get some more info:

Can you please DM me your ProjectID?

Can you confirm that the same code and behaviour was working a few days ago, and these 422 errors are recent?

Thanks!

1 Like

IT was working a few days ago. Then got 422. I moved all my logic from Unity Services to the client (effectively damaging my server-authoritative architecture). If we get this fixed I’ll be hella happy. Te project id is: 6ef7a80f-5d0f-4443-904b-9fb6a1cd5a75.

BTW nice tyrande mount there. Cheers!

Hello, so it turns out you’ve unfortunately been hitting the Inventory item count limit of 1000.

Apologies for the vague error message.

Each player can only have up to 1000 inventory items.
Might I suggest maybe grouping some of your Economy items into bundles to save space, or perhaps saving some types of items in cloud save instead?
Cheers

1 Like

Actually, that was my test account hitting the big limit because I tetted too much card packs opening. I see so that was the issue in the end. I guess I’ll mobve everything to the server. This may be an issue int efuture though. Is there a way to get more bandwith for the inventory? 1000 seems a little restrictive. Considering each card collection is 110 cards and I plan on making multipple card expansions and you can have duplicates, that makes up for a lot of cards. IW as thining of ways to burn down the extra cards. But other solutinos should be implemented too.

Unfortunately there are no ways as of now to increase the Economy service inventory item limit. We will keep it in mind for the future.

Thanks, have a good day!