前置き
ゲームに登場する世界データは全プレイヤー間で一つ
配置場所を表すインデックスでアクセスし、データは 32 bit のキューブ情報の集合体です。
クライアントから直接この世界データにアクセスするにはインデックス指定は必須だと思いますが、配置するときにいずれかのキューブであることを表す情報を渡すのはいかがなものでしょうか?
そう、無限増殖とか、希少なキューブタイプを不正にリクエストすれば簡単に増やすことができてしまいます。
これは許せないですね。
リクエストにキューブのタイプを含めない
最近作られてきているインベントリ操作まわり
simplestar-tech.hatenablog.com
アイテムの CustomData に slotXX とカウントという情報が入るようになったので
クライアントからは、どのスロットから sideA, sideB を取り出すのか、そのスロット番号を与えれば良いことに気づけた
ということで、サーバーでスロット番号からアイテムを特定するロジックを組んでみることにしました。
現在のリクエスト内容は?
次の実装の通り
var microWorldCubeDataByteOffset = this.GetMicroWorldCubeDataByteOffset(microWorldKey, chunkKeyXYZ, cubeIndex);
PlayFabCloudScript.RequestCubeAction(microWorldName, 1, microWorldCubeDataByteOffset, (byte)category, (byte)rotationType, (byte)sideTypeA, (byte)sideTypeB, requestResult =>
{
Debug.Log($"requestResult.status = {requestResult.status} message = {requestResult.message}");
});
rotationType までは送る必要があります(まぁ、回転情報を知っているのはクライアントだけなので仕方なし)
sideTypeA,B を送るところをアクティブな Slot 番号にするのはどうか
現在のアクティブスロット番号
取れない。
ということで取れるように例のアクティブスロット更新イベントを購読させます。
CubeLocator にて次の実装を書いて、常に現在のアクティブスロットインデックスを同期して持たせます。
this.itemInventory.onChangeActiveSlot += this.OnChangeActiveSlot;
}
<summary>
</summary>
<param name="activeViewItem"></param>
private void OnChangeActiveSlot(ViewItem activeViewItem)
{
this.activeSlotIndex = activeViewItem.slotIndex;
}
あとはこれをリクエストにつなげるだけ
<summary>
</summary>
public static void RequestCubeAction(string microWorldIndex, int cubeAction, int byteOffset, byte category, byte rotation, byte sideA, byte sideB, int activeSlotIndex, UnityAction<RequestCubeDataResult> callback)
{
ExecuteCloudScript<RequestCubeDataResult>("requestCubeData", new
{
index = microWorldIndex,
action = cubeAction,
offset = byteOffset,
category,
rotation,
sideA,
sideB,
slot = activeSlotIndex
},
successResult =>
{
callback?.Invoke(successResult);
});
}
空のキューブを配置するときは slot に -1 を詰めてもらうことにしました。
CloudScript 側で slot から Cube が取れるか?
最後にしてしまいましたが、最も重要な処理を書くことにします。
サーバー処理が重たくなりますが、これも無限増殖というチートを防ぐため、受け入れましょう。
取れることが確認できたコードがこちらです。
let item = null;
let remainingUses = 0;
let slotRemainingUses = 0;
const playerInventory = server.GetUserInventory({
PlayFabId: currentPlayerId
});
const length = playerInventory.Inventory.length;
for (let i = 0; i < length; i++) {
item = playerInventory.Inventory[i];
if (null != item.CustomData) {
const pattern = /slot([0-9]{2})$/;
for (let key in item.CustomData) {
const match = key.match(pattern);
if (null != match && match[1] == args.slot) {
slotRemainingUses = item.CustomData[key];
break;
}
}
if (0 < slotRemainingUses) {
remainingUses = item.RemainingUses;
break;
}
}
}
入力を厳しくチェック
クライアントからのリクエストがサーバーのインベントリの情報とすべてマッチしていることを確認します。
本当はクライアントのロジックで不正なリクエストは絶対発生しないように作ります。
それでも改竄してアイテム増殖を行おうという人がこのロジックで弾かれて、バイナリキャッシュサーバーまで処理が届けられなくなります。
if (0 != args.action && -1 != args.slot) {
let item = null;
let slotRemainingUses = 0;
const playerInventory = server.GetUserInventory({
PlayFabId: currentPlayerId
});
const length = playerInventory.Inventory.length;
for (let i = 0; i < length; i++) {
item = playerInventory.Inventory[i];
if (null != item.CustomData) {
const pattern = /slot([0-9]{2})$/;
for (let key in item.CustomData) {
const match = key.match(pattern);
if (null != match && match[1] == args.slot) {
slotRemainingUses = item.CustomData[key];
break;
}
}
if (0 < slotRemainingUses) {
break;
}
}
}
if (0 <= args.slot && 0 == slotRemainingUses) {
return JSON.stringify({
status: 402,
message: "empty slot"
});
}
let uses = 0;
if (0 != args.sideA) {
if (args.sideA != item.ItemId) {
return JSON.stringify({
status: 403,
message:
"required itemId: " + args.sideA + " but ItemId: " + item.ItemId
});
}
uses++;
}
if (0 != args.sideB) {
if (args.sideB != item.ItemId) {
return JSON.stringify({
status: 404,
message:
"required itemId: " + args.sideB + " but ItemId: " + item.ItemId
});
}
uses++;
}
if (uses > slotRemainingUses) {
return JSON.stringify({
status: 405,
message:
"required remainging uses: " +
uses +
" but slotRemainingUses: " +
slotRemainingUses
});
}
}
一応、クライアント改竄による不正にアイテムを増やそうとする対応をわざと入れてみましたが、弾かれました。
うまくチェック機構が機能しているみたい
サーバー側でスロットのアイテムが減る
まずは配置に成功したら、uses の数だけ CustomData からカウントを減らします。
チェック機構で必ず残数があることを確かめているので、ここは単純に減らすだけ
加えてアイテム本体の数を減らす必要があります。
PlayFab の API を
api.playfab.com
調べると次の通り
if (1 == args.action && 201 == response.status) {
let grants = [];
if (0 != response.sideA) {
grants.push(response.sideA);
}
if (0 != response.sideB) {
grants.push(response.sideB);
}
if (0 < grants.length) {
server.GrantItemsToUser({
PlayFabId: currentPlayerId,
CatalogVersion: "cube",
ItemIds: grants
});
}
if (null != item && 0 < uses) {
const reduceCount = -uses;
server.ModifyItemUses({
PlayFabId: currentPlayerId,
ItemInstanceId: item.ItemInstanceId,
UsesToAdd: reduceCount
});
}
}
サーバー側でスロット内のアイテム数が減る
先ほどの例はアイテム自体のカウントの消費ですが、これから行うのは CustomData 内のカウントの消費です。
スロットのアイテムを消費するとなると、こうですかね(期待通り、スロット番号が減っていきました)
if (null != item && 0 < uses) {
const reduceCount = -uses;
server.ModifyItemUses({
PlayFabId: currentPlayerId,
ItemInstanceId: item.ItemInstanceId,
UsesToAdd: reduceCount
});
if (null != matchecKey) {
const nextSlotRemainUses = slotRemainingUses - uses;
if (0 < nextSlotRemainUses) {
const data = {
[matchecKey]: nextSlotRemainUses
};
server.UpdateUserInventoryItemCustomData({
PlayFabId: currentPlayerId,
ItemInstanceId: item.ItemInstanceId,
Data: data
});
} else {
server.UpdateUserInventoryItemCustomData({
PlayFabId: currentPlayerId,
ItemInstanceId: item.ItemInstanceId,
KeysToRemove: [matchecKey]
});
}
}
最後 2個一緒に減らしたときにインベントリからアイテム自体が消えたので、成功ですね。
PlayFab の管理メニューから確認できました。
現在のクライアントの残り作業
いま rock アイテム固定になっているので、スロットのアイテムで上書きします。
var sideTypeA = SideType.Air;
var sideTypeB = SideType.Air;
var activeSlotType = 0;
if (null != itemInventory.ActiveSlotItem)
{
var itemInstance = itemInventory.ActiveSlotItem.item;
if (null != itemInstance)
{
if (int.TryParse(itemInstance.ItemId, out int itemId))
{
activeSlotType = itemId;
}
}
}
switch (this.locateMode)
{
case LocateMode.Cube:
sideTypeA = (SideType)activeSlotType;
sideTypeB = (SideType)activeSlotType;
break;
case LocateMode.SideA:
sideTypeA = (SideType)activeSlotType;
break;
case LocateMode.SideB:
sideTypeB = (SideType)activeSlotType;
break;
default:
break;
}
これでいけるはず
クライアントのロジックはそのほかにも削除に成功したなら、表示しているスロット内容も変更しないといけない
こんなかんじかな
<summary>
</summary>
private void OnResponseCubeData(int lastSlotIndex, SideType sideTypeA, SideType sideTypeB, RequestCubeDataResult requestResult)
{
if (-1 != lastSlotIndex)
{
var lastItem = this.items[lastSlotIndex];
if (null != lastItem)
{
var lastCount = lastItem.count;
if (SideType.Air != sideTypeA)
{
lastCount--;
}
if (SideType.Air != sideTypeB)
{
lastCount--;
}
lastItem.count = lastCount;
this.UpdateSlotView(lastSlotIndex);
}
}
else
{
}
}
あと数字が残るバグもあったので直します。
ほか、スロット変更は mining モード時も有効になっていてほしい→対応
サーバー側で slot カウントも増やさなければならない
これが大変なケースがあって、例えばアイテムスロットに空きがあって、そこにアイテムが追加されてほしいじゃないですか
でもそういう計算をサーバーで行おうとすると、すべてのアイテムのすべての CustomData にある slotXX の XX の部分を総なめして
空きがある最小の値を見つけ出し、ここに置くと宣言する…という計算を走らせなければならない
それはやめようと思う
やめると困るのは、新しいキューブを手に入れたとき
サーバーで適切なスロットに格納されないから、ローカルで頑張らなければならない
そこで、サーバーで大変なケースになっていることはわかるので、その時だけクライアント側で計算してもらい
正しいスロット番号状態をリクエストしてもらうのはどうだろう
そうしよう、ということでサーバー側は簡易な以下のコードでスロット内の数を加算することにしました。
動作確認済みです。
let grants = [];
if (0 != response.sideA) {
grants.push(response.sideA);
}
if (0 != response.sideB) {
grants.push(response.sideB);
}
if (0 < grants.length) {
server.GrantItemsToUser({
PlayFabId: currentPlayerId,
CatalogVersion: "cube",
ItemIds: grants
});
for (let itemId of grants) {
let addSlotIndex = 1000;
let addSlotKey = null;
let addSlotCount = 99 * 2;
let addSlotItem = null;
const playerInventory = server.GetUserInventory({
PlayFabId: currentPlayerId
});
for (let slotItem of playerInventory.Inventory) {
if (itemId != slotItem.ItemId) {
continue;
}
if (null != slotItem.CustomData) {
const pattern = /slot([0-9]{2})$/;
for (let key in slotItem.CustomData) {
const match = key.match(pattern);
if (null != match && 99 * 2 > slotItem.CustomData[key]) {
if (addSlotIndex > match[1]) {
addSlotIndex = match[1];
addSlotKey = key;
addSlotCount = Number(slotItem.CustomData[key]);
addSlotItem = slotItem;
}
}
}
}
}
if (
null != addSlotKey &&
1000 > addSlotIndex &&
99 * 2 > addSlotCount &&
null != addSlotItem
) {
const newCount = addSlotCount + 1;
server.UpdateUserInventoryItemCustomData({
PlayFabId: currentPlayerId,
ItemInstanceId: addSlotItem.ItemInstanceId,
Data: {
[addSlotKey]: newCount
}
});
}
}
}
最後にクライアント側でのアイテムの増加
サーバー側で行われたアイテム増減の結果は返ってきませんので
クライアント側で、サーバーの状態を類推して合わせてあげる必要があります。
<summary>
</summary>
private void OnResponseCubeData(int lastSlotIndex, SideType sideTypeA, SideType sideTypeB, RequestCubeDataResult requestResult)
{
if (201 != requestResult.status)
{
return;
}
if (-1 != lastSlotIndex)
{
var lastItem = this.items[lastSlotIndex];
if (null != lastItem)
{
var lastCount = lastItem.count;
if (SideType.Air != sideTypeA)
{
lastCount--;
}
if (SideType.Air != sideTypeB)
{
lastCount--;
}
lastItem.count = lastCount;
if (0 >= lastItem.count)
{
this.items[lastSlotIndex] = null;
}
this.UpdateSlotView(lastSlotIndex);
}
}
else
{
List<int> grants = new List<int>();
if (0 != requestResult.sideA)
{
grants.Add(requestResult.sideA);
}
if (0 != requestResult.sideB)
{
grants.Add(requestResult.sideB);
}
foreach (var grantItemId in grants)
{
var reloadFlag = true;
for (int slotIndex = 0; slotIndex < MAX_SLOT_INDEX - 1; slotIndex++)
{
var item = this.items[slotIndex];
if (null == item)
{
continue;
}
if (null == item.item)
{
continue;
}
if (int.TryParse(item.item.ItemId, out int itemId))
{
if (itemId == grantItemId)
{
if (99 * 2 > item.count)
{
var newCount = item.count;
newCount++;
item.count = newCount;
this.UpdateSlotView(slotIndex);
reloadFlag = false;
break;
}
}
}
}
if (reloadFlag)
{
this.OnOpenInventory();
}
}
}
}
まとめ
上記の工程を経て、タイミングが良ければ…一見してアイテムを消費したり、掘削してキューブを取得したりができていそう
ここからデバッグしていきます。
ともかく、クライアントから送られてくるアイテム情報を利用せず、スロット番号だけを頼りにサーバーにあるインベントリ情報から世界データを更新
安全にアイテムの利用、キューブの破壊ができているようです。
現在のゲームプレイの様子