PlayFab の Economy V2 は、クライアント側インベントリ関連の API を呼び出し、ログインしているプレイヤー以外のプレイヤーのインベントリにアイテムを追加または参照しようとすると、NotAuthorized エラーが発生します。
他のプレイヤーとアイテムを取引したり、プレゼントを送ったり、といった用途に利用したい場合は、CloudScript を利用してサーバーサイドで処理を行う必要があります。本記事では、Azure Functions を用いて、指定したプレイヤーのインベントリにアイテムを追加する方法について解説します。
TransferInventoryItems は、クライアントサイドでプレイヤー自身のインベントリ内のコレクション間・スタック間でのみアイテムを転送でき、他のプレイヤーのインベントリには適用できません。
作成する機能
Azure Functions を利用して CloudScript を実装し、指定したプレイヤーのインベントリにアイテムを追加する方法を説明します。
この機能では、対象プレイヤーのマスタープレイヤーアカウント ID (PlayFab ID) を事前に把握していることを前提とします。Azure Functions 呼び出しの際のパラメーターとして、対象プレイヤーのマスタープレイヤーアカウント ID と追加するアイテムの フレンドリ ID を指定します。
Azure Functions で PlayFab CloudScript を作成する方法は、以下のドキュメントを参照してください。
今回のサンプルの実装の Azure Functions、分離ワーカープロセス .NET 8 で作成しています。
処理の流れ
インベントリに追加する相手プレイヤーのタイトルプレイヤーアカウント ID を取得
インベントリに追加する際、プレイヤーのタイトルプレイヤーアカウント ID が必要になるため、PlayFab Server API の GetUserAccountInfoAsync メソッドを利用して、クライアントから渡されたマスタープレイヤーアカウント ID (PlayFab ID) をもとに、プレイヤーのタイトルプレイヤーアカウント ID を取得します。
// アイテムを追加するプレイヤーのタイトルプレイヤーアカウント 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 を指定します。
  // アイテムを追加するプレイヤーのタイトルプレイヤーアカウント 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 ごとの制限のため、サーバーでの処理でもプレイヤーがばらける用途なら、むしろ使い安いように感じます。
この制限に関しては、用途に応じた事前検証を推奨します。
Function 全体のコード
Function 全体のコードは以下の通りです。
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");
        }
    }
}