イベントじゃない更新なんていつぶりだろうか。
Twitterにも書いたが、先週hanakopatchというパッチを公開した。これはダブルスポイラーのScene 12-6 正体不明「厠の花子さん」で発生するリプレイずれの修正パッチだ。
バグの原理についてはリンク先の方に書いたが、解析の過程についても残しておきたいのでこの機会に書いておく。
解析開始。自機、敵、弾の座標のログ取りを最初の目標に設定。
うさみみハリケーンで自機座標のアドレス特定、ただしヒープ上なので安定した取得方法を見つける必要がある。 特定したアドレスにwrite break pointを設定して、自機座標に関連するコードを探す。 風神録以降は同じエンジンが使われているということで、地霊殿AIの資料を見つつ、IDA Proで解析。
グローバル変数経由で自機座標を取り出す方法を確立。
当たり判定処理を取っ掛かりに弾ないし敵座標の取得方法確立を目指す。
弾や敵は自機と違って複数個あるので、どのようなコレクションで管理されているのかを知る必要がある。 自機座標にread break pointを設定して、ヒット回数と弾・敵の数がおおよそ比例する処理を探す。 円対円判定と思しき処理を見つけたので、呼び出し元を遡って、コレクションの構造を調べる。 コレクションの大雑把な構造は分かったが、敵か弾か分からない。
ログ取り処理のinjectionのために毎フレーム処理されるコードを探しておく。 キー取得関連のコードを読んでみたが、よく考えると実際のプレイとリプレイ再生両方で実行されるコードを探す必要があった。 キー取得はリプレイでは実行されないので方針変更。
また、この日突然死の再現をしているが、リプレイでは再現しなかったためそのまま放置。
ログ取り用のinjection用のコードを探す続き。
自機座標更新系のコードなら確実と判断。 この辺のコードを調べて、呼び出し元を調べたりした。
メモがないが、この日最後にDLL injectionでログを取る実装をしている。 あと花AI塚の頃に作っていたログ可視化ツールを引っ張り出した。
いつ発覚したか覚えてないが、0623で見つけたコレクションは敵のコレクションだった。
ログ取りのためにはゲームのSceneの区切りも知りたいということで調査。 Scene切り替わりのタイミングで敵のコレクションが初期化されると判断して、敵コレクション関連のデータにwrite break point設定。 なんだかんだで区切りを見つけられたので、ログ取り実装に追加。
ログを取りながらゲームプレイとリプレイを繰り返す。
すべてが違うずれ方をするケースを見つけた。青弾のずれが目立つ。
青弾の弾源に敵が発生するのかと思っていたがそうではないことが発覚。青弾座標のログも取る必要がある。
0623で見つけていた円対円当たり判定の候補から弾との判定っぽいものを探し、呼び出し元を遡る。
弾のコレクションの解析ができたので、弾座標のログ取り実装。最初の目標は達成。
改めてゲームプレイ・リプレイのログを取ったところ青弾からリプレイずれが始まっていることが分かった。
リプレイずれは青弾から始まっているということで、青弾の初期位置がどうやって決まるかを調べる。
弾は配列で管理されていて、一つ一つに今使われている弾かどうかのフラグが付いている。 このフラグにwrite break pointを設定して青弾の初期化コードを探す。
なんとなく初期位置の生成に関連しそうなコードを見つけたが、このコードの呼び出し元が絞れてない。
0701に見つけたコードの呼び出し元を絞り込むため、このコードにbreak pointを置いてScene 12-6開始時の呼び出し元を探る。 呼び出し元を遡りながら関連するデータ構造を調べてみたが、構造が入り組んでいて頭がこんがらがってきた。
0702でのデータ構造の解釈をミスっているのではということで再調査。結果いくつか解釈ミスを修正。
このデータ構造の初期化をたどれば青弾の初期位置生成も分かるのでは、ということで初期化関連を調べていたがイマイチ分からない。
引き続き0704で調べてたデータ構造の調査をするも成果なし。 なんでこの辺調べてたのかよく分からなくなってくる。 もう一度0701で見つけたコードに立ち返って、初期位置の生成を追ってみようと決心して就寝。
0707の決心に従い、loc_41595A周辺のコードを読む。
初期位置生成には2つのfloat値フィールドが絡むが、初期位置生成より前のコードにbreak pointを設定してこれらのフィールドを調べてみる。 片方は0、もう片方は0xBAADF00Dが入っていた(思えばこの時点でこのマジックナンバーに気づくべきだった・・・)。
デバッガとか回しているうちに、もう片方の値は0x0468EC8で計算された値であることが分かった。 ここでの計算はいくつかのデータを参照して行っているが、ここで領域外参照をしているのでは、と仮説を置いてみる。
0x0468EC8での計算がリプレイずれに影響するかを確定するため、このコードにbreak pointを設定し、ここでの計算のずれとリプレイでの座標のずれがタイミング的に一致するのかを検証。 一致が確認できた。
0x0468EC8の計算で参照しているデータの初期化を辿ろうと試みるが苦戦(0x0468EC8の呼び出し以前に読み書きが行われることを知りたいが、0x0468EC8の呼び出し時まで正確なアドレスが分からないのでwrite break pointでの検証ができない)。 結局0702で調べてたデータ構造を調べる必要があるのではと再考して、デバッガで調べてみたが分からずじまい。
0x0468EC8の計算で参照しているデータの初期化コードを探したいのだが行き詰まる。
アセンブリに対する文字列検索のアプローチを試す。 データを参照するときのアドレス計算が複雑なので、似たようなアドレス計算をしているコードを探す。 いくつかは空振りだったが、「レジスタ名+100Ch」のパターンで気になるコードに引っかかる。
このデータにread break pointを設定したところ、初期位置生成関連のコードも引っかかった。どうやら当たりっぽい。 このデータの初期化コード(0x040F670)は初期値生成に関連する他のデータの初期化も兼ねているのでは?
このころ、ふと0xBAADF00Dの値がマジックナンバーではないかと気づく。
リプレイずれの原因は初期位置生成に関連するデータの初期化漏れでは、という仮説が立つ(なおこのマジックナンバーはデバッガ経由でDSを起動した時に挿入されるもので、デバッガ経由でなければ単にヒープにあるゴミデータが突っ込まれることが後に判明する)。
0x040F670のコードは青弾生成以外でも呼ばれている様子なので、何回目の呼び出しが青弾生成関連なのかを調べ、そのときの呼び出し元を調べる。
また、0x0468EC8の計算で参照するデータは複雑なものの、実際の実行ではほぼ定数になるのではないかと思い調査。 いくつかのデータが定数であることが分かり、
00468EC8 |. D94430 08 FLD DWORD PTR DS:[EAX+ESI+8]
のeaxは青弾x座標初期化では常に0(yの場合は4)で、esiは0x040F670でのeaxと同じであると判明。
これらをまとめた結果、青弾初期位置生成に関連するオブジェクトは0x0410240で生成されることが分かった。 そしてこのコードでは0x0468EC8の計算で参照するデータ([esi+8]および[esi+Ch])が初期化されてないことが分かった。
この辺の初期化漏れでリプレイずれは説明できるが、突然死は説明できるのか? このあたりのコードにdll injectionしてログを落とすようにして、ゲームプレイをひたすらやる。 が、そもそも突然死が起きない・・・。
0716の解析から[esi+8]および[esi+Ch]の初期化漏れがリプレイずれの原因なのは分かったが、突然死の原因かは定かではない。 考えられるケースとしては、これらのデータがたまたま自機のX座標及びY座標だった場合がある。このケースをデバッガで再現して観察する。
観察した結果、確かに突然死は起きるが自機が死んだ場所で青弾の発生が見える。 ネット上の突然死バグの動画を見た感じ青弾が発生しないケースが多く、これが突然死の原因かは怪しい。
とはいえリプレイずれは直せるはずなので、その確認をする。 ログ取りに使っていたdll injectionのコードで、初期化漏れの部分を上書きしてみてリプレイずれが起きないことを確認。
SSGでパッチを書いて公開(←hanakopatch)。