Azure Functions で、Economy V2 の インベントリ関連の API を呼び出す

PlayFab の Economy V2 は、クライアント側インベントリ関連の API を呼び出し、ログインしているプレイヤー以外のプレイヤーのインベントリにアイテムを追加または参照しようとすると、`NotAuthorized` エラーが発生します。

他のプレイヤーとアイテムを取引したり、プレゼントを送ったり、といった用途に利用したい場合は、CloudScript を利用してサーバーサイドで処理を行う必要があります。本記事では、Azure Functions を用いて、指定したプレイヤーのインベントリにアイテムを追加する方法について解説します。

:::note info
TransferInventoryItems は、クライアントサイドでプレイヤー自身のインベントリ内のコレクション間・スタック間でのみアイテムを転送でき、他のプレイヤーのインベントリには適用できません。
::::


## 作成する機能

Azure Functions を利用して CloudScript を実装し、指定したプレイヤーのインベントリにアイテムを追加する方法を説明します。


この機能では、対象プレイヤーのマスタープレイヤーアカウント ID (PlayFab ID) を事前に把握していることを前提とします。Azure Functions 呼び出しの際のパラメーターとして、対象プレイヤーのマスタープレイヤーアカウント ID と追加するアイテムの フレンドリ ID を指定します。


Azure Functions で PlayFab CloudScript を作成する方法は、以下のドキュメントを参照してください。

https://learn.microsoft.com/ja-jp/gaming/playfab/features/automation/cloudscript-af/quickstart


:::note info
今回のサンプルの実装の Azure Functions、分離ワーカープロセス .NET 8 で作成しています。
::::

## 処理の流れ

### インベントリに追加する相手プレイヤーのタイトルプレイヤーアカウント ID を取得

インベントリに追加する際、プレイヤーのタイトルプレイヤーアカウント ID が必要になるため、PlayFab Server API の `GetUserAccountInfoAsync` メソッドを利用して、クライアントから渡されたマスタープレイヤーアカウント ID (PlayFab ID) をもとに、プレイヤーのタイトルプレイヤーアカウント ID を取得します。

```cs
// アイテムを追加するプレイヤーのタイトルプレイヤーアカウント ID　を取得する
var requestGetUserAccountInfo = new GetUserAccountInfoRequest()
{
    PlayFabId = playerId,
};

var getUserAccountInfoResult = await serverApi.GetUserAccountInfoAsync(requestGetUserAccountInfo);

if (getUserAccountInfoResult.Error != null)
{
    throw new Exception($"GetUserAccountInfoAsync failed. Error={getUserAccountInfoResult.Error.GenerateErrorReport()}");
}

// アイテムを追加するプレイヤーのタイトルプレイヤーアカウント ID
var titlePlayerAccountId = getUserAccountInfoResult.Result.UserInfo.TitleInfo.TitlePlayerAccount.Id;
```

### インベントリにアイテムを追加する

AddInventoryItemsAsync メソッドを利用し、指定したプレイヤーのインベントリにアイテムを追加します。

その際 `AddInventoryItemsRequest.Entity` に、アイテムを追加するプレイヤーのタイトルプレイヤーアカウント ID を指定します。


```cs
  // アイテムを追加するプレイヤーのタイトルプレイヤーアカウント ID
  var titlePlayerAccountId = getUserAccountInfoResult.Result.UserInfo.TitleInfo.TitlePlayerAccount.Id;

  var playerEntity = new PlayFab.EconomyModels.EntityKey()
  {
      Id = titlePlayerAccountId,
      Type = "title_player_account",
  };

  // Economy V2 API
  var economyApi = new PlayFabEconomyInstanceAPI(apiSettings, authContext);

  // 指定されたプレイヤーのインベントリにアイテムを追加する
  var addInventoryItemsAsyncRequest = new AddInventoryItemsRequest()
  {
      Item = new InventoryItemReference
      {
          AlternateId = new AlternateId()
          {
              Type = "FriendlyId",
              Value = itemId, // 追加するアイテムの FriendlyId を指定
          },
      },
      Amount = 1,
      Entity = playerEntity, // 追加するプレイヤーのエンティティを指定
      IdempotencyId = idempotencyId,
  };

  var addInventoryItemsAsyncResponse = await economyApi.AddInventoryItemsAsync(addInventoryItemsAsyncRequest);

  if (addInventoryItemsAsyncResponse.Error != null)
  {
      throw new Exception($"AddInventoryItemsAsync failed. Error={addInventoryItemsAsyncResponse.Error.GenerateErrorReport()}");
  }
```


## 制限

`AddInventoryItemsAsync`　は、Entity にプレイヤーを指定しての呼び出しには、90 秒間に 60回の制限(スロットリング)があります。

制限以上呼び出すと、TooManyRequests エラーが返されます。

Server API は、10 秒間に 1,000 回の制限があります。Economy V2 のプレイヤー指定の制限は、それに比べて厳しいですが、Entity ごとの制限のため、サーバーでの処理でもプレイヤーがばらける用途なら、むしろ使い安いように感じます。
この制限に関しては、用途に応じた事前検証を推奨します。

https://learn.microsoft.com/ja-jp/gaming/playfab/features/economy-v2/limits

https://learn.microsoft.com/ja-jp/gaming/playfab/features/throttling/what-is-throttling

## Function 全体のコード

Function 全体のコードは以下の通りです。

```cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.Functions.Worker;
using PlayFab;
using PlayFab.EconomyModels;
using PlayFab.ServerModels;
using PlayFab.Samples;

namespace Samples.Function
{
    public class AddPlayerInventoryItem
    {
        private readonly ILogger<AddPlayerInventoryItem> _logger;

        public AddPlayerInventoryItem(ILogger<AddPlayerInventoryItem> logger)
        {
            _logger = logger;
        }

        [Function("AddPlayerInventoryItem")]
        public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
        {
            _logger.LogInformation("C# HTTP trigger function processed a request.");

            string body = await new StreamReader(req.Body).ReadToEndAsync();
            _logger.LogInformation($"body={body}");
            var functionContext = JsonConvert.DeserializeObject<FunctionExecutionContext<dynamic>>(body);
            var args = functionContext.FunctionArgument;

            // CloudScript の呼び出し元のプレイヤーの MasterPlayerAccountId
            string masterPlayerAccountId = functionContext.CallerEntityProfile.Lineage.MasterPlayerAccountId;
            _logger.LogInformation($"masterPlayerAccountId={masterPlayerAccountId}");


            // クライアントから渡された PlayerId と ItemId を取得
            dynamic request = null;
            if (args != null && args["Request"] != null)
                request = args["Request"];

            string playerId = request.PlayerId;
            if (string.IsNullOrWhiteSpace(playerId))
            {
                throw new ArgumentException("PlayerId is empty", "PlayerId");
            }

            string itemId = request.ItemId;
            if (string.IsNullOrWhiteSpace(itemId))
            {
                throw new ArgumentException("ItemId is empty", "ItemId");
            }

            string idempotencyId = request.IdempotencyId;

            _logger.LogInformation($"playerId={playerId}, itemId={itemId}, idempotencyId={idempotencyId}");


            // PlayFab Server API のインスタンスを取得
            var apiSettings = new PlayFabApiSettings
            {
                TitleId = functionContext.TitleAuthenticationContext.Id,
                DeveloperSecretKey = Environment.GetEnvironmentVariable("PLAYFAB_DEV_SECRET_KEY", EnvironmentVariableTarget.Process),
            };
            var serverApi = new PlayFabServerInstanceAPI(apiSettings);

            PlayFabAuthenticationContext authContext = new PlayFabAuthenticationContext()
            {
                EntityToken = functionContext.TitleAuthenticationContext.EntityToken
            };

            // アイテムを追加するプレイヤーのタイトルプレイヤーアカウント ID　を取得する
            var requestGetUserAccountInfo = new GetUserAccountInfoRequest()
            {
                PlayFabId = playerId,
            };

            var getUserAccountInfoResult = await serverApi.GetUserAccountInfoAsync(requestGetUserAccountInfo);

            if (getUserAccountInfoResult.Error != null)
            {
                throw new Exception($"GetUserAccountInfoAsync failed. Error={getUserAccountInfoResult.Error.GenerateErrorReport()}");
            }

            // アイテムを追加するプレイヤーのタイトルプレイヤーアカウント ID
            var titlePlayerAccountId = getUserAccountInfoResult.Result.UserInfo.TitleInfo.TitlePlayerAccount.Id;

            var playerEntity = new PlayFab.EconomyModels.EntityKey()
            {
                Id = titlePlayerAccountId,
                Type = "title_player_account",
            };

            // Economy V2 API
            var economyApi = new PlayFabEconomyInstanceAPI(apiSettings, authContext);

            // 指定されたプレイヤーのインベントリにアイテムを追加する
            var addInventoryItemsAsyncRequest = new AddInventoryItemsRequest()
            {
                Item = new InventoryItemReference
                {
                    AlternateId = new AlternateId()
                    {
                        Type = "FriendlyId",
                        Value = itemId, // 追加するアイテムの FriendlyId を指定
                    },
                },
                Amount = 1,
                Entity = playerEntity, // 追加するプレイヤーのエンティティを指定
                IdempotencyId = idempotencyId,
            };

            var addInventoryItemsAsyncResponse = await economyApi.AddInventoryItemsAsync(addInventoryItemsAsyncRequest);

            if (addInventoryItemsAsyncResponse.Error != null)
            {
                throw new Exception($"AddInventoryItemsAsync failed. Error={addInventoryItemsAsyncResponse.Error.GenerateErrorReport()}");
            }

            return new OkObjectResult("Success");
        }
    }
}
```