前置き
自作ゲームの PlayFab におけるチート行為防止の話です。
キューブで構成される世界データをたった一つのサーバーに格納しています。
simplestar-tech.hatenablog.com
今のところユーザーからのリクエストはほぼすべて信用して処理しているのでかなり危険なつくりとなっています。
最近ゲームの仕様として、最も優れたゲーム内ツールを使ったとしても、一秒はブロックの破壊に要することに決めました。
となると、あるユーザーがキューブに対するリクエストを連続で行う場合に、一秒より短いリクエストをしてきた場合
これはクライアント改竄にほかなりません(またはコンピュータ内の時間の流れを速めているチート行為かもしれません)
別に最強すぎるピッケルとか作ってゲーム内で効率的に地面を掘ってもらってもよいのですが、データを集約しているサーバーにアクセスが集中してしまうのは
ほかのお客さんに対して不利益が生じてしまいます。アクセスが遅くなったり、貴重なアイテムの争奪戦で常に負かされてしまったりと
そこで、PlayFab の CloudScript でそういう高頻度アクセスの対処方法って何がベストなのかな~と調べたのが始まりでした。
UserInternalData に前回のリクエスト時刻を打つ
見つけたやり取りがこちら
PlayFab でチート対策するときの話を読んでみた。https://t.co/sz1L8Lc8f3
— Simplestar@Unityゲーム開発 (@lpcwstr) 2019年12月25日
どうやら UserInternalData にユーザーごとの情報を格納して、なんとか検知してみるといいとのことです。
自分は UserInternalData に LastRequestTime を記入して、次のリクエスト時にこれを参照して、ちゃんと 1秒以上経っていることを確認してからリクエストを処理し始めることにしました。
実験して気づけましたが、for文などでほぼ同時にリクエストすると、InternalData に書き込み終える前にリクエストを並列で処理し始めてしまいます。
そんなときのために DataVersion が確実にインクリメントされていることをチェックしています。(これ必須でした)
実際に動作確認できたコードがこちら
// cubedata request handlers.requestCubeData = function(args, context) { const now = Date.now(); // get last request time const lastTimeKey = "LastRequestTimeCubeData"; const getInternalDataResponse = server.GetUserInternalData({ PlayFabId: currentPlayerId, Keys: [lastTimeKey] }); // request must have 1000 ms interval const lastTimeData = getInternalDataResponse.Data[lastTimeKey]; if (null != lastTimeData) { const lastTime = Number(lastTimeData.Value); if (1000 > now - lastTime) { return JSON.stringify({ status: 400, message: "called in interval" }); } } // imidiately set last request time const updateInternalDataResponse = server.UpdateUserInternalData({ PlayFabId: currentPlayerId, Data: { [lastTimeKey]: now } }); // check DataVersion up 1 if ( updateInternalDataResponse.DataVersion != getInternalDataResponse.DataVersion + 1 ) { return JSON.stringify({ status: 401, message: "Data inconsistency" }); } // do something
クライアントを改竄してチート検出できているかチェック
まとめ
知見としては、連続して CloudScript を実行すると、関数は並列で走ることが裏付けされたことが大きい収穫
また DataVersion が関数内で正しくインクリメントされたケースを使えば、特定ユーザーの高速連続リクエストの不正なものだけを拒否できました。
良い例を示せたんじゃないかな?