【ストV】ラグ改善パッチを調べてみた【改造?】

「PC版のスト5のラグ改善する改造プログラムがあるらしい?」 

をプログラムとかよく分からない人に解説するやつ。

 

これ。クリックすると字が表示されます。怖がらないで。

github.com

 

結論。普通。悪さはしてなさそう。

使用は自己責任で。

 

目次

 

さて、読んでいきましょう。パッチ製作者の気持ちを想定して書いています。

 

 

まず、はじめに

という意味で「Readme(私を読んで!)」というファイルを作るのが慣例です。日本人もやります。なのでまずはそれを読んでみましょう。というか最初に表示されます。

f:id:manie:20200111230335p:plain

ファイル一覧にあるREADME.mdの内容と同じです。

  • Instructions
  • Why is this needed?
  • Does the other player need to have this fix as well?
  • Fix yout shit Capclown

はい英語きた。Google翻訳さん助けて!

f:id:manie:20200111231144p:plain

  • 説明書
  • なぜこれが必要ですか?
  • 他のプレイヤーもこれを修正する必要がありますか?
  • たわごとキャップクラウンを修正します。

Capclownがうまく翻訳できなかったみたいです。clownは道化師・ピエロという意味ですから、カプコンと道化師から作った造語みたいです。まぁオチなので調査は各自で。

 

要約すると……

  • zipをストVのフォルダで解凍してね
  • 巻き戻りを減らすよ
  • 対戦相手はパッチ当てなくていいよ
  • 解析2日、制作30分、カプコン放置4年

 

巻き戻りを減らす?とは?

では第2段落の「巻戻りを減らすよ」について詳しく見ていきます。とりあえずGoogle翻訳で読んでみましょう。

 

Why is this needed?

SFV has a bug where one player's game can lag behind the other's online. This can cause artificial lag and one sided rollback for the other player.

なぜ必要なのですか?

ストVにはバグがあります。片方のプレイヤー画面がラグって、オンラインの相手より遅れるのです。この現象は人為的なラグであり、片方のプレイヤーだけ相手より巻き戻るのです。

 

When the players' "clocks" are synced, if there is e.g. a 4 frame packet round trip time between them, each player should be 2 frames ahead of the time of the last received input from their opponent, and experience 2 frame rollbacks.

互いの「クロック」が同期しているなら、例えば4フレームのパケット往復時間があるとすると、相手から入力を受け取ってから2フレーム先にいて、2フレームの巻き戻りが発生します。

 

If one player lags behind, the other player will receive inputs from farther "in the past" (up to 15 frames!) than they should, causing unnecessarily big rollbacks and artificial lag, while the player that's behind may even be receiving inputs that appear to be "in the future" to their game and never experience rollbacks at all.

あるプレイヤーが遅れているとすると、対戦相手のプレイヤーは「過去」よりもさらに遠く(最大15フレーム!)から入力を受け取って不必要に大きな巻き戻りと人為的ラグを引き起こします。一方、遅れている方のプレイヤーは、「未来」から来た入力を受け取ることもあります。ロールバックは一切発生しません。

 

This fix ensures your "clock" never gets more than half of your packet round trip time ahead of your opponent's so that you never experience more rollback than them.

この修正は、「クロック」が相手のパケットの往復時間の半分を超えないようにします。だから、相手よりも多くのロールバックが発生することはありません。

 

Readmeの内容は以上です。ネットワーク遅延は仕方ないにしても「不必要に大きな巻戻りを人為的に発生させ、ラグが大きすぎるバグ」があるという主張です。

ではひとつずつ見ていきましょう。

 

クロックとは時計のこと

英語の時計にはclockとwatchがありますが、clockは置き時計watchは腕時計です。腕時計は持ち歩いて人それぞれの時刻を保持しますが、置き時計はみんなで眺めるものです。「いま何時何分だよね?」と確認するためのものです。

互いに時計を持ち、互いに時計を進め、なにかひとつやるごとに相手の時計とズレていないか確認します。ズレていたら自分(または相手)の時刻を修正したり追いつくための特別な操作をします。これを「クロックを同期する」といいます。

時計の最小単位は秒ですが、格闘ゲームでは伝統的に1/60秒を1フレームと呼んで使います。フレームはフレームバッファ(画面表示用メモリ)の略で、画面表示1回に由来します。

ラグとは?

ラグは、結果です。原因ではありません。通信が遅延した、複雑な画像処理に時間がかかった、などの理由で予定時刻に画面表示が間に合わない場合があります。その異常事態に対して、「ゲームを一旦停止する」「ワープする」などの方法で対処します。この対処による「遅れる体験」をラグといいます。

ネットワークがラグい、というのは通信機器の擬人化です。深淵すぎます。

パケットとは?

パケットは小包という意味です。通信とは小さく分けたものを送り出し、相手から受け取った返事をもらっておこないます。実は送る・受け取るという分類はあまり意味がなく、受け取る方も返事を送るので常に送受信なのです。往復はがきやLineの既読と同じです。

かつてデータサイズと送信時間は同じで、受け取り確認の時間はほぼゼロと考えられていましたが、今はそうではありません。格闘ゲーム程度の通信量(1秒あたり100kB/sぐらい)であれば、相手にデータを送り終わるまでの時間と相手から受け取った返事が返ってくるまでの時間はほとんど同じです。

クロックが同期していて通信が往復で4フレームかかると仮定した場合は?

常に往復で4フレームの通信時間を仮定します。送信に2フレーム、受信確認に2フレームと考えるべきでしょう。これは平均値です。

通信は遅れていますが、クロックは同期しています。互いに「2フレーム前に受信した入力で作った画面が、表示するタイミングに2フレーム間に合っていない」とわかっています。だから「2フレーム巻き戻して互いのクロックを2フレーム調整する」ことで解決すべきでしょう。

クロックがズレていて通信が往復で4フレームかかると仮定した場合は?

この状況では「4フレーム時間がズレた」のですから未作成画面4枚分に追いつく必要があります。クロックを同期し、新たな互いの入力と画像処理をして「次のフレーム」の生成を開始しなければなりません。

正常な側(4フレーム待たされた側)が表示を4フレーム巻き戻し遅れた側は相手の入力がないまま表示だけ正確に行うとします。(実際のストVの仕様です

遅れている側は「4フレームに1回しか相手の入力データがこない!」わけですが、ゲームのプレイとしては問題ありません。表示は正常です。妙に相手の操作が飛び飛びですが、ちょっと下手な人かな?ぐらいの感覚でしょう。

通信が待たされている側は「4フレームに1回だけ正常」で、「残りの3フレームは常に巻き戻る」という地獄が待っています。4~5フレームの先行入力がありますからコマンド自体は失敗しませんが、見てから対応するゲーム性は失われます。

パッチ製作者はここがおかしいと感じたのでしょう。待たせた側は何のペナルティもなくプレイでき、待たされた側はゲームとはとても呼べないような環境を強いられます。

ストリートファイターVには「巻き戻る」という機能があるのですから、お互いに半分ずつ巻き戻せばよいのです。

まず「待たせた側の入力」を実際に入力された時間より2フレーム未来へ移動します。つまり押したボタンを「2フレーム遅れて反映」させるのです。同様に「正常な側」も入力を2フレーム未来に移動します。ただし、4フレーム待たされているわけですから反映されるのはボタンを押した2フレーム後の画面です。結局は「2フレーム過去に反映」させることになります。

お互いに「押したボタンが2フレーム遅れる」体験をします。パッチによる修正はこの巻き戻りに関するものです。

 

プログラムを見てみよう

見ると言ってもファイルひとつだけです。main.cppが本体です。何でそれが本体なのかというと、本体につける名前だからです。ではmain.cppをクリックして、たった217行のソースコードを表示してみましょう。短いですね。

まず識別子を見てみます。識別子とはひとつひとつが独立した別のものだと識るための単語のことです。コンピュータの世界で使う、モノにつける名前のことです。識別子は後続する中括弧~閉じ中括弧が内容になります。まずは内容を無視して列挙してみましょう。

  1. namespace Proud
  2. class UInputUnit
  3. class UnknownNetBullshit
  4. class UnknownNetList
  5. UnknownNetlist *Netlist
  6. uintptr_t UpdateTimestampsOrig
  7. extern "C" void UpdateTImestampsOrigWrapper
  8. bool GetPing
  9. extern "C" void UpdateTimestampsHook
  10. bool GetModuleBounds
  11. uintptr_t Sigscan
  12. uintptr_t GetRel32
  13. void JmpHook
  14. BOOL WINAPI DllMain

これ以外に出てくる識別子はその場の使い捨てか、システムが始めから用意しているものです。気にしなくてかまいません。

 

さて、まず最初に見るべきなのはDllMainです。202行目から最後までです。なぜならプログラムを始めたい場所にDllMainと書くからです。そして、内容の最後の行から読みます。なぜならWindowsのプログラミングは前準備が大変かつ長すぎるからです。よって214行目の

JmpHook ( UpdateTimestampsOrig, UpdateTimestampsHook );

が最重要の行です。

JmpHookは「いま動いているプログラムから別に用意したプログラムへ処理を移す」ための命令をいま動いているプログラムに」上書きする処理が書いてあります。上書きするサイズは小さなものですが、現在動いているプログラムが少し失われることになります。

そこで「書き潰した部分を先頭に付け足してもともとのプログラムに処理を戻す」ためのUpdateTimestampsOrigWrapperが必要になり、その中に本来の処理であるUpdateTimestampOrigを埋め込み、実際に動かしたい改造部分であるUpdateTimestampsHookを動作させます。

図にするとこんな感じでしょうか。既存のプログラムを改造する場合の一般的な方法だと思います。一般的にはソースコードなしに改造しないんですけど。

f:id:manie:20200112084113p:plain

ここまでストVに関係ない話でした。さて、核心に迫ります。UpdateTimestampsHookは何をやっているのでしょうか。108行目から153行目までです。

111行目。まずもともとの処理を行います。まぁ普通。

123行目。GetPingで時刻のズレを確認します。といってもなぜこの方法でPing値が得られるのかは不明です。ここのここのここからこうすると得られる値がPing値です、と書いてあるだけです。でもまぁ、そうなんでしょう。

129行目。Pingの単位はms(ミリ秒)のようです。Ping値をフレーム数に変換しています。1フレームは1/60秒ですから、1000ミリ秒を60で割った約16.6ミリ秒です。この数字でPing値を割ればフレーム数となります。割ることと逆数を掛けることは同じですから、60倍して1000で割っているのはそういう意味です。ちょっと分かりにくいですね。

Ping値からフレーム数のズレに変換しました。ここからはいろんなタイムスタンプが出てきます。すべてのタイムスタンプはフレーム単位でカウントされています。35行目から59行目に答えが書いてあります。

138行目。フレーム数のズレに1フレーム加えてMaxFramesAheadに格納します。

140行目。相手のタイムスタンプ+ズレ+1より自分のタイムスタンプが進んでいる場合は時刻を1フレーム過去に戻します。

147行目。相手のタイムスタンプ+ズレ+1と現在時刻を比べて小さい方(より昔の時間)を目標時刻とします。

148行目。目標時刻より現在時刻のほうが過去なら、その差をとって「追いつかせるフレーム数」として設定します。

138行目から152行目までが進めたり遅らせたりしている部分です。

 

結局何をするパッチなの?

まぁなんと言いますか、「相手より進んでいたら遅らせて、遅れていたら追いつかせる」という内容でした。

 

おわり。

 

追伸。

もともとの処理がどういうものだったか、を調べるにはどうすればよいでしょうか。答えはStreetFighterV.exeの中に書いてあります。書いてあると言ってもこういう状態です。

f:id:manie:20200112103402p:plain

ここからCPUの命令の何に対応するかを調べながら意味を理解するのは面倒ですが、難しくはありません。ただ数字を探すだけだからです。

intelの資料 https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol2A_i.pdf

スクリーンショット

f:id:manie:20200112111609p:plainf:id:manie:20200112111831p:plain

この表を読むと、なるほど!195行目にあったB848はmov命令で32ビットレジスタに32ビット即値を入れる命令って意味だったのか!だから直後に即値を置くのか!と1/60秒で分かるわけです。

ただ大量にある命令から「探したい意味のある部分」を見つけるのは特殊な能力が必要です。推理小説の中から犯人が判明するシーンを見つけるような能力です。

例えば、対戦前と対戦後のメモリ全体を比較して「差異が経過フレーム数」となる部分があるのでは?と推測して「時計の場所」を探したり、通常の対戦で変化する箇所すべての中から「ラグ発生時だけ0でなくなる場所」があるのでは?と推測する、といった能力です。

 

私はできません。