ピロ彦の何か置き場

ドラクエ4 TAS の サブフレームリセットと任意コード実行の補足

※注意:TASの操作は大変危険なものですので決して真似しないでください。


 今回のTASは、TAS制作統合エミュレータであるBizhawkの開発版に実装されたSubNESHawkによってファミコンでもサブフレームリセットサブフレームインプットが可能になったのを知って急遽制作しました。
 ざっくりとチャートを説明すると、0人PTを作って並び替えをした時にバグってSRAMをプログラムとして呼び出すのでそれを利用して任意コード実行しよう、ということです。

 この画像はBizhawkでコアにSubNESHawkにしている時のTAStudioのタイムラインです。

 赤いラインは通常はラグフレームとして扱われますが、SubNESHawkではVBlankが発生した時に挿入され、プログラムがコントローラの入力処理を検知するたびに緑色のサブフレームインプットが発生します。
 ドラクエ4ではカーソル移動処理時に1フレームに2回×2コントローラの入力受付があり、サブ1で決定・キャンセルの判定をしサブ3でカーソル移動の判定をしています。
 そのおかげで通常のTASと違い、カーソルが毎フレーム連続移動出来るので名前入力時などのカーソル移動がとても速くなってます。
 左のカラムにReset Cycleと書かれているのはリセット入力時に待機するPPU Cycleを数値で入力するもので、『けこけこ』[13 14 13 14]冒険の書3に書き込まれる瞬間のトレースログを見ると実際にPPU-Cy:30400を越えたところでリセット処理が発生していることが分かります。


 名前を入力して「おわり」を選択した瞬間に冒険の書に名前が書き込まれますが、メッセージ速度を決定するまではチェックサムが0000のままなので、その前にリセットをすると冒険の書が消えるメッセージを簡単に見ることが出来ることができます。

 動画3秒の地点でのエラー処理中にサブフレームリセットをすることで冒険の書3の名前の手前までを4Bで埋め尽くしています。
 これをしない場合は0人PTでの並び替え時に$68F1が実行された時に 00:BRK 命令が発生して目的地にたどり着けないので無害な命令である 4B:ALR #immで回避しています。
 この命令は未定義命令ですが A = A AND immediate, A = shift right A という処理が実行されるので通過し終わった頃にはCPUのAレジスタ00になってます。

 『けこけこ』[13 14 13 14]という名前は 13:ASO ($14),Y というプラグラムを2回実行するためにつけてます。

  00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00
10
20
30      
40
50
60
70     Ex

 ASOという未定義命令は memory = shift left memory, A = A OR memory という処理が実行されるので、単に左シフトとして利用します。($14),Yの部分は$14$15の2バイト+Yの間接アドレス先のメモリを書き換えるというものです。
 未定義命令はエミュレータによっては無視されたりエラーを起こしたりしますが、現在のTAS環境においては未定義命令も実機と同等の動作をするようなので遠慮なく使います。
 $14はコントローラ1、$15はコントローラ2の入力が反映するメモリなので、好きなアドレスのメモリ値を2回左シフトすることになります。純粋な左シフトは間接アドレスが使えないのでこちらを採用しています。『け』は『こ』に近いので便利です。
 ちなみに一番最初に『けこけこ』の処理をする理由は冒険の書が多いほうが処理が多くなるからで、冒険の書が埋まったときのほうがウィンドウの処理自体は速いですが総合的には遅くなります。

 続いて冒険の書1に『にへ゜ 』[20 27 6A 00]という名前をつけてスタートしています。この名前は 20:JSR $6A27という命令で、実行されると冒険の書3の所持金のアドレスにサブルーチンとしてジャンプすると意味になります。
 純粋なジャンプ命令である4Cや相対ジャンプでループするための負数(0x80以上)は名前から得られませんが、エニックスが『゜』を6Aにしてくれたので冒険の書3の中でループを作る事が可能になっています。
 制作直後の冒険の書にはPTの加入情報は0なのですが、第一章が始まる時にライアンの加入処理が発生するので、0人PTを作るためにまず教会でセーブした状態を作ります。

 動画53秒地点で冒険の書1を2にコピーした直後に冒険の書2を消して、再び冒険の書1を2にコピーをしていますが、この作業でデータ2の所持金が[32 00 00]=50Gだったものが[4B 4B 4B]=4,934,475Gになります。



 $65DF・$65E0冒険の書2のCRCっぽいチェックサムとなっていますが、16bitのCRCを調整するのは簡単ではありません……

 と思いきや、最終的な計算結果の比較処理にあるべき1行が抜けてる為に8bit分しか判定に貢献していません(;´Д`)

 こんな感じのFCEUX用のLuaスクリプトを書いて画面に表示して、HEXエディタで4Bを書き込んでいって偶然にも$65E0CRCの計算結果が一致するものを手動で探してました。BizhawkよりもFCUEXの方がHEXエディタが使いやすかったのでチャート構築や解析はこっちでしてました。
 このように4Bで埋める範囲を調整することでCRC調整出来ますが、$66A8から$66AFまでのライアンの道具欄も4Bで埋めてしまうと天空の兜(4B)だらけになって売り買いが出来なくなります(´・ω・`)
 $673C00にしてるのは、そこも4Bで埋めた場合は道具欄が埋まるまでにCRCが一致しなかったからです。冒険の書のメッセージ速度はデータの一番後ろにあるので、+1するごとに+0x10*nをXORしたCRCになるので5bit分だけ一致していれば調整可能でしたが、今回はなんとかその手間を掛けずにCRCを一致させることが出来ました。

 さて、名前のアドレスと近い位置にある所持金を増やすことに成功したので動画1分過ぎから冒険の書2で金額調整に入ります。
 ここで欲しい値は[D3 14]で、これはD3:DCM ($14),Y という命令になり$14-$15のコントローラ情報を利用して特定のアドレスの値を1つ減らすことが可能になります。
 DCMという命令も未定義命令で memory = memory - 1, compare A and memory という処理がされるので、比較部分以外はほぼDEC命令と同じですがDECは間接アドレスを扱うものがないのでこちらを使っています。
 しかし売り買いだけでその金額にしようとすると13944Gも買い物をしないといけないので、買っては捨てて買っては捨ててで時間がエラい掛かります。 そこで先程用意した冒険の書3の『けこけこ』[13 14 13 14]を用いた任意コード実行で金額を変更してしまおうという魂胆です。


 先に所持金を[D3 45 4B]に調整します、そのために必要な購入額は1400Gと分かりやすいので調整も楽にできます。 $67380x45を2回左シフトすると、0x45*4=0x114となり、8bit分の0x14が残ります。
 このコードを実行するために0人PTを作ります。上図の$674Aにある[86 00 00 00]がライアン・無人無人無人というパーティー構成を意味しているのでこのライアン00にしてしまえば良いのですが、4Bをつっこんでも存在しないキャラ扱いになって0人PTになるので冒険の書1を削除して$645Aライアン[86]まで4Bで埋めて、冒険の書2を1へコピーしてCRCを再度一致させます。


 このとき$645Aまでではなく、PTの2人目以降も4Bで埋めた場合は神父に呼ばれる名前が改行込みのちょっと長い空白になり、人数が多いほどロードも長くなるので今回のTASでは良い感じに調整できてますが駄目な場合は$63A5のライアンの生存フラグまで4Bで埋まってもCRCが一致しない可能性がありました。 なお名前も上書きされて『ソソソソ』[4B 4B 4B 4B]になってますが問題ありません。

 冒険の書1に0人PTを作れたので、早速1回目の任意コードを実行していきます。 0人だとバグが発生するのはRPGには良くあることで、人数分の処理を行う際に0からスタートすると判定前に-1して255になってしまうのが主な原因だと思われます。
 動画時間1分39秒で並び替えを実行した後の入力判定で1Pは上下スタート、2Pは下左セレクトBAを押すことで$14が0x6738となり、ソソソソ†が32人分ほど表示されたタイミングでバグが発生して冒険の書3の$68F1が呼び出され、4B4Bゾーンを通り抜けて$6A2D『けこけこ』[13 14 13 14]が実行されて$6738が晴れて4514になるわけです。


 ちなみ余談ですが上図でいうところの$6775$6777はそれぞれ船座標XYと気球座標XYで、教会でロードをすると街に対応した位置に配置されてて街を出る時にフラグチェックをして0000に戻されたりします。 船・気球フラグは冒険の書2でいうところの$686Eでこの画像よりも大分下の方にあり、船がある時は+01・気球がある時は+02という感じになってます。
 つまり4Bだと船も気球も手に入った状態になるということなので、一章が始まって教会する前にリセットした冒険の書を空っぽのデータにコピーし終わる前にリセットを押すと1/256の確率でCRCが一致し、上書き位置が悪くなければロード後に一章が全滅BGMと变化状態で始まり、教会で再開かキメラの翼を使うと気球と船が外に配置されるようになります。
 失敗例としてはリセットが早すぎて名前の2バイト前にある章番号が4Bになったせいで、4B章が始まってしまうケースです。 このデータでは主人公不在のバグった5章スタートで、並び替えバグをしても$62F1が実行されないので多分役に立ちません。
 ただし、成功例でもデータの終端にあるメッセージ速度が4Bになってしまい、48~4Fの範囲外に戻すことが出来なくなって戦闘が凄く遅くなります(´・ω・`)
 名前によって成功率が変わると思いますが、コピー決定の2~3フレーム後くらいに適当にリセットを押していれば実機でも現実的な試行回数で一章から気球に乗れるようになると思います。
※追記 戦闘中にセレクトを押して速度を3に変更するとメッセージが速くなるそうです!!

 話を戻すと、動画時間1分40秒で冒険の書2の所持金が[D3 45 4B]から[D3 14 4B]になったことで冒険の書2のCRCが一致しなくなり、データ消去処理が発生するので前述と同様に消去中のサブフレームリセット冒険の書1から2へ上書き中のサブフレームリセット冒険の書2を復活させます。

 この先は冒険の書1しか起動することはないので、冒険の書2は所持金と名前さえ無事ならどの範囲を使って調整しても大丈夫です。 じゃあ何のために冒険の書2を復活させたかというと、冒険の書3にコピーするためです。

 冒険の書3はコピーしたあとに00を埋めるために所持金手前まで4Bで埋めます。 消えたデータ扱いでいいのでCRCは気にしません。

 これによって欲しいプログラムの第一形態が完成しました。以下プログラムの中身↓

  $68F1: 4B 4B     ALR #$4B    Aレジスタと#$4Bの論理積を取った後に右シフト
  $68F3: 4B 4B     ALR #$4B  
  ---------------------------
  $6A27: D3 14     DCM ($14),Y  $14の16bitにYレジスタを足した間接アドレス先を1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00    Aレジスタは0になる
  $6A2D: 20 27 6A  JSR $6A27   $6A27へサブルーチンとしてジャンプする

 $6A27から$6A2Dの間で無限ループして好きなアドレスを好きな値に書き換えることが可能になりましたが、無限ループ中に$14を変更することは出来ないので狙った値になるタイミングでリセットをかける必要があります。動画1:52では$6A260xA8に、1:59で$6A220xECに、2:07で$6A230xC8に、最後に2:13で$6A210x20にしています。
 A8:TAYはAレジスタをYレジスタにコピーする1バイトの命令で、3バイト命令を通った後にプログラム位置がずれるのを直すのとYレジスタを0にするために設置しています。
 [20 EC C8]JSR $C8EC で、コントローラ情報を取得して$14と$15を更新するサブルーチンになってます。
 これで晴れてループ中にコントローラ情報を更新できるようになったかと思いきや、まだループ外なので最後にちょっとした工夫をします。

 現在のループは$6A27へジャンプするものなので、$6A2Eの27を減らして$6A21へジャンプするようにするようにすれば良いのですが、1つずつ減らす間にループ位置が一つずつずれるので正しくループが出来なくなる可能性がありますが、そこはちゃんと考えての配置にしてます。 以下は一連の流れ。

  $6A21: 20 EC C8  JSR $C8EC   コントローラ情報を取得し$14-$15を更新するサブルーチン
  $6A24: 4B 4B     ALR #$4B     $C8ECの後、0x10になっているAレジスタが0x00になる
  $6A26: A8      TAY        YレジスタにAレジスタがコピーされる
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 26 6A  JSR $6A26    $6A26へジャンプ
  -----
  $6A26: A8      TAY        YレジスタにAレジスタがコピーされる
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 25 6A  JSR $6A25    $6A25へジャンプ
  -----
  $6A25: 4B A8    ALR #$A8
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 24 6A  JSR $6A24    $6A24へジャンプ
  -----
  $6A24: 4B 4B    ALR #$4B
  $6A26: A8      TAY        YレジスタにAレジスタがコピーされる
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 23 6A  JSR $6A23    $6A23へジャンプ
  -----
  $6A23: C8      INY         Yレジスタが+1される
  $6A24: 4B 4B    ALR #$4B    Aレジスタが0になる
  $6A26: A8      TAY        YレジスタにAレジスタがコピーされる
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 22 6A  JSR $6A22    $6A22へジャンプ
  -----
  $6A22: EC C8 4B  CPX $4BC8   $4BC8とXレジスタを比較
  $6A25: 4B A8    ALR #$A8
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 21 6A  JSR $6A23    $6A21へジャンプ
  -----
  $6A21: 20 EC C8  JSR $C8EC   コントローラ情報を取得し$14-$15を更新するサブルーチン
  $6A24: 4B 4B     ALR #$4B     $C8ECの後、0x10になっているAレジスタが0x00になる
  $6A26: A8      TAY        YレジスタにAレジスタがコピーされる
  $6A27: D3 14     DCM ($14),Y   ($14)+Y=$6A2Eを1減らす
  $6A29: 4B 4B     ALR #$4B  
  $6A2B: 4B 00     ALR #$00  
  $6A2D: 20 21 6A  JSR $6A21    $6A21へジャンプ
  -----

 このようにループ変更途中にC8:INYが実行されてしまうのでA8:TAYでYレジスタを0に戻してやっています。 また、JSR $C8EC のサブルーチンから戻って来た後はAレジスタが0x10になるので ALR #$4B によってAレジスタが0になるようにしてます。
 Yレジスタがずれたままだと$6A2Eではなく$6A2Fを減らすことになってループ先が$6923になってしまいます。 それ自体は4Bゾーンを通った長めのループになるだけなので問題なさそうですが、一回のループが長いとVBlankが発生した時にループが途切れる可能性が増えます。

 これでようやく任意コード実行の最終形態とも呼べるであろうトータル・コントロールが可能になったので、メモリをモリモリ減らしていきましょう。
 動画では10フレームしか表示されなかった部分を細かいサブフレームインプットまで表示するとこんな感じになります。
 $14の数値に対応したアドレスが1ずつ減らされていくのが見てわかると思います。$6921から書いてるコードはエンディングへ飛ぶためのもので、書き終わった後に$6A2Fの値を1つ減らしてジャンプ先を$6A21から$6921に変更して最後の操作を終了しています。
 その直前にはフラグ操作も行っていて、0人PT時の動けないフラグ$058F[10][0F]に変更し、エンディング途中でメッセージが発生すると入力が必要になるので、コーリンゲンで止まらないように$627E$62830xFFにしてます。しかし、実は$62A1[20]のエンディング待機フラグあればその2つは必要ないので1フレーム未満の差ですが本当はそっちの方が早いですが、通常のエンディングを見るよりも特殊なエンディングの方が価値が高いと思ったのでこちらを採用。

エンディングに飛ぶプログラムについては以下の通り

  $6921: 49 1A     EOR #$1A    Aレジスタ排他的論理和で0x1Aにする
  $6923: 20 91 FF   JSR $FF91    $FF91のサブルーチンでバンクを0x1Aに切り替える
  $6926: 20 FC 9B   JSR $9BFC   エンディング処理の途中である$9BFCにジャンプ

 Aレジスタを変更する命令は49:EOR以外にも沢山ありますが、4Bから近いので採用。
 バンク切り替えというものはROMが読み込んでる範囲を変更するものです。 CDで言えばディスクチェンジを一瞬でしてるようなものだと思うと合ってるかもしれませんし間違ってるかもしれません。 グラフィックが読み込まれる範囲を動的に変更したりも出来るので、バンクが間違った状態だとグラフィックも崩れたりします。
 DQ4の場合は$8000~$BFFFと$C000~$FFFFの部分にプログラムが書かれてますが、前者は細かく切り替えられながらプログラムを実行しています。 その中でエンディング処理が書かれているのは 1A:9BFC でそのままだとバンクが違うのでJSR $9BFCを実行してもエンディングへは飛べないのでバンク切り替えを行ってからジャンプしています。


 最後に、スタックが溢れてないか気になってる人がいると思うのでGIFを貼っておきます。












 
 エンディング中はイベント処理を実行し続けてサブルーチンから戻ってこないので、
スタックは溢れまくりましたがエンディングが見れたので問題ありません。

FDSゼルダ1任意コード実行TASの更新部分のちょっとした解説


英語による解説文は http://tasvideos.org/4709S.html をご参照ください。
前回のTAS(sm26296564)からRAT926氏によって移動部分で1:12で10Fと2:36で40Fほど更新されています。
※参考URL http://tasvideos.org/forum/viewtopic.php?p=408522#408522

ゼルダにするためにファイル名にZELDAの文字が必要で、
  1. ZELDAEヤ6
  2. Wヤ6AG2シ2
  3. ユノ6ソク+
と3つのファイルを使って任意コードを書いています。
ZELDAの後ろに文字が入っててもゼルダになるので19文字分使えますが、
2マス空白にしているのでここでは17文字が任意コードに使われています。
ゼルダのファイル名は$0638・$0640・$0648の3つ分が連続して格納されています。

墓地にてギーニを沢山出して枠を埋めた後に笛を装備しながらABを押すと
本来スプライトのためではない部分が変更され、
バグAIとして$0602からプログラムが実行されます。
その近辺はBGMの制御に使われているのでタイミング調整で
ファイル名アドレス$0638部分までプログラムカウンタが進むようにしています。
  • $0638: 23 0E 15 0D 0A 0E 4B 06
  • $0640: 20 4B 06 0A 10 02 33 02
  • $0648: 4C 40 06 36 2F 64 24 24
ファイルに使われた値は上の様になってます。
はじめの5文字はZELDAに使われているので$063Dからがコードになります。
  • $063D: 0E 4B 06 ASL $064B

この ASL(Arithmetic Shift Left)」「算術左シフト」を表し、
$064B[36]の値を2倍[6C]へ書き換えてます。
ファイル名に使用可能な文字が[00-64]の範囲なので、
それを超える値を使うためにこのような手法をとっています。

ここからはリアルタイムキー入力メモリに書き込んで
$06C3から新しいコードを書くためのループ文となっています。
このコードの原型はsockfolder氏によって書かれました。
※参考URL http://tasvideos.org/forum/viewtopic.php?p=408475#408475

  • $0640: 20 4B 06 JSR $064B
  • $0643: 0A ASL
  • $0644: 10 02 BPL $0648
  • $0646: 33 02 RLA ($02),Y
  • $0648: 4C 40 06 JMP $0640
  • $064B: 6C 2F 64 JMP ($642F)

下流

  • $0640: 20 4B 06 JSR $064B
JSR=ジャンプ・サブルーチン命令によって$064Bへ飛びます。
  • $064B: 6C 2F 64 JMP ($642F)
JMP=ジャンプ命令によってキー入力更新ルーチンの$EA1Fへ飛びます。
EA1Fが文字で表現できないので、($642F)に書かれているEA1Fを拾って間接ジャンプ。
このルーチンが終わると先程のJSRの次の$0643に戻ってきます。
この時、CPUのAレジスタYレジスタには前フレーム差分と現フレームのキーが入ってます。
  • $0643: 0A ASL
ASL命令によってAレジスタ2倍にされます。
キーはそれぞれ A[80],B[40],Sel[20],sT[10],↑[08],↓[04],←[02],→[01]
が合わさったものが入ります。
  • $0644: 10 02 BPL $0648
ASL2倍にされたAレジスタ[FF]を超えた場合、
つまりAが押されていた場合はキャリー・フラグが発生します。
ASL2倍にされたAレジスタ[80]以上だった場合、
つまりBが押されていた場合にはここでネガティブ・フラグが立ち、
Bが押されていなかった場合にはネガティブ・フラグクリアになっています。
BPL命令によってネガティブ・フラグクリアならば$0648相対ジャンプします。
つまりBが押されたフレームは$0646が実行され、そうでない場合は次の命令へ飛びます。
  • $0646: 33 02 RLA ($02),Y
RLAファミコンCPU6502の未定義命令なのでVirtuaNESなどでは実行されません。
($02)にはこの時$0602という値が入っているので、$0602+Yレジスタメモリ
キャリーつき左ローテートされ、Aレジスタメモリとの論理積になります。
左ローテート左シフトの後にキャリーフラグがあった場合に+1されます。
つまりAB右が新規に押された場合はキャリーフラグとネガティブフラグが立ち、
$0602+Y[C1]= $06C3メモリ2倍+1になり、
前フレームにAが押され、次フレームにAB左が押された場合はネガティブフラグのみで、
$0602+Y[C2]= $06C4 のメモリは単に2倍になります。
  • $0648: 4C 40 06 JMP $0640
JMP命令によって$0640に戻り1フレームの間に何度もループします。
Bが新規に押されたフレームのみ$0646が一度だけ実行されるので、
実質的に新規コードを書き込む速度は2フレームに1bitとなります。
2フレームに1バイトではなくとてもゆっくりです(;´Д`)

入力の終わりに$064Bを2倍に書き換えてループを解きます。
$06C3から書かれた新規コード5秒弱24バイト
29 02 20 4C 84 09 09 85 10 6E 00 20 A5 3C D0 FC 09 13 20 4C 84 20 7B 63
となっています。
エンディングはLevel9ダンジョンじゃないと読み込まれないのではないかと思います。
このコードの詳細はまだMasterjun氏が解説してないので略しますが、
  1. ゲームモードを読み込み状態に書き換え
  2. マップIDをLevel9のダンジョンに書き換え
  3. ディスクを読み込み
  4. 笛で止まった時間のタイマーが0になるまでループすることで読み込み終了を検知し
  5. ゲームモードをイベント状態に書き換えてイベントを暴走させ
  6. メインループのNMI待ちの箇所へジャンプしてエンディングを待つ

多分こんな感じだと思います。


【TAS】 スーパーマリオワールド "game end glitch" 41秒98 について。

またしてもMasterjun氏によってクレジット呼び出しバグによるクリアTASが制作されたので、解説を少しだけ書こうと思います。足りないところは http://tasvideos.org/4315S.html を読むべし。
※kentora12氏は全く無関係なのでコメントする際には注意しましょう。

・任意コード実行まで

炎で亀を燃やして発生したコインをヨッシーの舌で掴んでいる最中にマリオがそのコインを取ると、舌の上に無を取得し、そこでブルが画面内に現れると舌の上にワープしてそのまま食べてしまいます。
マリオの状態や食べるブルの種類によってストックアイテムに様々な変化をもたらします。
今回利用されたのは、ゴール玉をストックするのに使われたブルとは別の種類のブルです。

sm21390603 で出現したジュゲムの雲ストックについて Masterjun氏 が調査されてた時、"任意コード実行" = ACE(Arbitrary Code Execution) の可能性にたどり着いてしまったようです。
ちび状態でダッシュするブルを食べると、ジュゲムの雲ストックした後にマリオの状態が異常になり、本来は呼び出されることのないルーチンによってプログラムは$014A13へジャンプします。
これらは、あんた氏が以前に予見していたことでもあります。
そのジャンプ先は Open Bus とか SNES Bus とか呼ばれ不安定な部分なのでフリーズ待ったなしですが、なんとか$4218のコントローラレジスタ部へたどり着くことが出来たとのこと。

・任意コード実行

コードは4つのコントローラで5フレームの間実行されています。
これらのコードは65816というCPUのアセンブリ言語で書かれています。
  1. E0 0A FB 64 10 CB 00 00
  2. A9 18 D8 64 10 CB 80 F8
  3. 87 3D 0B 64 10 CB 80 F8
  4. A9 08 AB 64 10 CB 80 F8
  5. 8D C6 13 20 72 80 80 F8
上記の操作によって、$0100(ゲームモード)を 0x18(Load Credits/Cutscene?) にセットし、$13C6(カットシーン)を 0x08(エンディング) にセットした後、ゲームを正常にスタートさせています。

まずは共通して使用されているコードについて解説

 64 10 = STZ: $10
これはアドレス$10のメモリの値に0をセットするという意味になります。
このアドレスは1フレーム間に行われなければいけない処理が完了した時に0がセットされ、
0以外の場合は無限ループする処理が発生するのでそれを回避するために書き換えています。

 CB = WAI
これは1/60毎に発生するNMIという割り込み処理を待つという意味のコードです。
これによって複数フレームのコードを実行できるようにしています。

 80 F8 = BRA: -8
これは無条件分岐命令で、相対アドレスの F8(-8)先へジャンプするという意味です。
これによってコントローラ部の命令を何度も実行できるようにしています。

メモリの書き換え命令

 A9 18 = LDA: #$18
これはCPUのAレジスタに0x18という数値を読み込むという意味です。

 87 3D = STA: [$3D]=$000100
これはアドレス$3Dから3バイトのメモリ値をアドレスとして読み込んで、その先の$000100にAレジスタの値(0x18)を書き込むという意味です。
アドレスを直接指定しようとすると3バイトも使ってしまうので、たまたま 00 01 00 と並んでいた$3Dを利用してコードを2バイトに圧縮してるようです。

 A9 08 = LDA: #$08
同様にAレジスタに0x08を読み込み、

 8D C6 13 = STA: $13C6
$13C6にAレジスタの0x08を書き込んでいます。

その他の命令
 E0 0A = CPX: #$0A <- Xから0x0Aを減算して結果をN・Z・Cフラグへ返す
 0A = ASL <- Aレジスタを左にシフト($4218からじゃなくて$4219かもしれない?)
 FB = XCE <- CフラグとEフラグの値を入れ替え
 D8 = CLD <- Dフラグをクリア(D=0)
 0B = PHD <- DをSに格納
 AB = PLB <- DBをSから取出
 20 72 80 = JMP: $8072 <- バンク内のサブルーチン呼び出し
一連の操作で正常にゲームのメインプログラムへジャンプ出来るように、各種フラグやDB(データバンクレジスタ)を調整してるのだと思います。

多分こんな感じだと思います。


スーパーマリオブラザーズ3のTASについて簡単に解説



更新点

1-1ではキノコを取る時、亀の直前で小ジャンプをして減速をしていたものを、大ジャンプからの亀踏みに変更したことで若干のフレームを更新したそうです。
それと、ゴールパネルのフラワーを回避する必要が無くなった点も更新されています。
砦でクッキー回避するところで若干更新してるかもしれない。

土管バグ

土管の右側に壁キックの要領で引っかかる瞬間に下を押していると、ズレた状態で土管に入ることが出来ます。
これがスクロール土管の場合は、土管の出口に触るまで潜り続けるといった挙動になります。
この時、本来はいけない空間に突入した時のブロックの配置が日本語版と英語版とで異なるので、このTASのようなクリア方法は英語版でしか出来ないそうです。

エンディング呼び出しコード

ピーチのいる部屋まで飛ぶために、"6502"というファミコンのCPUに「JSR:$8FE1」という命令を実行させる必要があります。
JSRはサブルーチンを呼び出すコードで、値は0x20です。
TASでは地下のバグったブロックに触った時にRAMアドレスの$0081から命令が実行されるので、その近辺の $0090(マリオのX座標)と$0091-95(敵のX座標)を利用してコードを実行しています。
1匹目の敵のX座標は$0095が使われるので、最初の亀を画面内に保持し続けます。
上に登るとパックンが3匹いますが、そのうち左のパックンのX座標が0x20になるタイミングを有するので、画面外に消して再度読み込まれた時に3匹目の敵として認識されるように高い位置のパックンを殺してます。
2番目に画面に現れる亀は5匹目の敵として扱われ、3番目に現れる亀が2匹目の敵として認識されるので下へ持っていきます。
最終的に、「$0093:0x20, $0094:0xE1, $0095:0x8F」という状態になる瞬間に土管バグで潜って画面から敵が消え、コードが完成します。
最後にしゃがみながらジャンプすることでバグったブロックを叩き、コードが実行されます。

多分こんな感じだと思います。


Wizardry V TAS におけるキャラクターメモリ情報


キャラクター データ アドレス

Address2人目移行は+0x80*ID
7E1000存在識別(非実在=0, 訓練場=0x05, 冒険中=0x85)
7E1001名前の文字数
7E1002-7E1009キャラクターの名前8文字
7E100Aアライメント・種族・職業
7E100B
7E100C知恵
7E100D信仰心
7E100E生命力
7E100F素早さ
7E1010運の強さ
7E1011-7E1016Gold
7E1017-7E101CE.P
7E101D-7E101E現在 H.P.
7E101F-7E1020最大 H.P.
7E1021-7E1022Level
7E1023ステータス状態
7E1024Age (年)
7E1025Age (週)
7E1026A.C.
7E1027-7E102D魔法使いの M.P.
7E102E-7E1034僧侶の M.P.
7E1035-7E103B魔法使いの呪文
7E103C-7E1042僧侶の呪文
7E1043-7E104Aアイテムの状態
7E104B-7E1052アイテムの種類
7E1053アイテムの所持数
7E1054毒で受けるダメージ
7E1055現在のX座標
7E1056現在のY座標
7E1057現在の階層
7E1058クリア称号の@マーク
7E105ASwimのE.P
7E105Bミルワの残り時間
7E105C-7E105Dマポーフィックの残り時間
7E105Eラツマピックの残り時間
7E105Fリトフェイトの残り時間
7E1063-7E1068Marks(撃破数)
7E1069-7E106ARip(死亡数)
7E1079前回のX座標
7E107A前回のY座標
7E107B前回の階層
7E107E-7E107FCRC-CCITT-16(FFFF)

ステータス情報

Statusじょうたい
00OK
01恐慌
02睡眠
03麻痺
04石化
05死亡
06
07ロスト
08以上バグ表示(全部ロスト)

MOTHER 2 TAS の補足

※注意:TASの操作は大変危険なものですので決して真似しないでください。


今回のTASでは今までのTASで使われていた[snes9x-rr]ではなく、[Lsnes]という現時点で最も再現度が高く実機に近い挙動をするSFC用TASクライアントを使用しております。

TAS製作時における最も大きな違いは、サブフレームでの入力を可能にしていることです。

Lsnes以外では、1フレーム=1/60秒の精度での操作が記録され、リセットを受け付けるタイミングもフレームフレームの切れ目に限定されていまが、
Lsnesでは機械語1命令単位の任意のタイミングでのリセットが記録可能になっています。
もちろん、実際のSFCはいつでもリセットを受け付けているわけですから問題ありません。

セーブ中のリセット

セーブデータが上書きされてる途中でリセットすると、一体何が起こるでしょう?
多分、セーブデータが消えてしまうゲームが多いと思われます。
しかし、ほんのちょっと狂っただけですべてのデータが消えるわけではありません。
では、一体何故セーブデータが消えてしまうのかというと、プログラムが不正なデータを検知して消してしまう処理があるからです。
DQ3等は呪いのBGMが鳴っている間にデータを綺麗サッパリにしてるなんて話も聞きます。

セーブデータが消えにくいゲームでは不正なセーブデータを消す処理がそもそもないものもありますが、MOTHER2の場合は1つのセーブデータに対してもうひとつ同じミラーデータ存在し、片方のデータが壊れていたとしても復元処理が働きます。
このため、MOTHER2でセーブデータ消失メッセージを見たことがある人は少ないはず。
※追記:ただし、データ頭の[HAL Laboratory, Inc.]に欠損があった場合は消失メッセージ無しに一発で消えます。

チェックサム

エラーチェックの方法として、チェックサムを記録してデータ範囲との整合性を計算する手法があります。
DQ復活の呪文などで、最後の1文字がチェックサムになっていたりします。
チェックサムアルゴリズムには、データ範囲の単純な総和をとったものや、排他的論理和(XOR)をとるものなどあります。

ではMOTHER2がどういうチェックサムを使っているかというと……
その両方を使っててかなり不正しにくいシステムになってました_(:3 」∠)_

1つ目のチェックサムは、 0000020から00004FF の値を1バイトずつ加算したものの合計2バイトで、2つ目は2バイト(16bit)ずつの値をXORにかけたものです。
XOR(排他的論理和)とはビット演算の1つで、2進数での同じ位置のビット同士が1と1・0と0の場合は0を返し、1と0・0と1の場合は1を返します。
例:_ 11111111 00000000 (FF00)
XOR 10001000 10001000 (8888)
__= 01110111 10001000 (7788)

ゲームをはじめる

設定を選択・変更するだけでもセーブデータは更新されますが、実はゲームデータが存在してるかどうかを判別しているのは「PKキアイ」のKの字が存在してるか否かだけです。
チートで[7E9ADA=00]にした状態でセーブをすると、当該データが「ゲームをはじめる」状態になって、強くてニューゲームのようにデータがそのままスタートされます。
逆に、ゲームデータが無い状態で[7E9ADA=4B]などにして、文字表示速度などを変更してキャンセルすると、いろんな値が0のままゲームを再開できるようになります。

では、チートをせずにTASの操作だけでそれを実現するにはどうしたらいいでしょうか。

チェックサムを突破するために

なにはともあれ、セーブデータをどうにかするにはチェックサムをごまかさないと始まりません。
設定画面で変更される値は、表示速度[01~03]サウンド[01~02]ウィンドウタイプ[01~05]と、「ゲームをはじめる」を選択してからの経過フレーム数(2バイト)だけです。
ある程度自由にできるのが経過フレーム数だけなので、ひたすら待つ羽目になりました。

一方で、PKキアイその他のデータはネスがベッドから降りた瞬間に初めてセーブされます。
MOTHER2で名前に使用できる文字の中で値が小さいのは 0x20="空白"、0x25="-"、0x26="~"、0x30="0"、0x31="1"...となっていて、0x20未満は命令文字となってます。
計算の結果、「PK-」という必殺技名をつけるのが最適であると判断しました。
空白文字を入れたほうが速そうに見えますが、残念ながら計算が合いませんでした。
数値に対応した文字の読み方に関してはMOTHER2 TBL」でググれば出てきます。

この時、セーブデータは上からではなく下の方から更新されていきます。
「PK-」のKがセーブデータに書き込まれ、Pが書き込まれる前にリセットします。
すると、電源入れた直後の最初にエラーチェックが行われ、当然不正なデータなのでセーブデータの消去後ミラーデータからの復元処理が行われます。

消去処理されてる途中で更に「K-」だけ残るようにリセットをすると、
チェックサム[7000 254B]と一致するデータになります。
0x4B00 XOR 0x0025 = 0x4B25
0x25 + 0x4B = 0x0070
チェックサムが一致したので復元処理はされなくなります。

ゼロの領域

あらゆる値が0のデータとは一体どういうものなのでしょうか?
レベルもHPも0なだけではなく、なんとPT人数0人な上に座標[0,0]から再開されます。

このデータは再開した直後に生存人数0人と判定されゲームオーバーになりますが、1フレームだけチェックまたはメニューを開くことが出来ます。Aボタンを押しっぱなしでもOKです

座標[0,0]はグミ族の村の左上隅位置し、テントバグネス家崖上のようにテキストバグを引き起こすことが可能な場所です。
ここでうまくテキストバグを引き起こすと、「ム     」「うロえロ   」などの文字列が表示されますが、文字表示ウェイトになっているために一瞬で通り過ぎます。
また、ウィンドウタイプなので、ウィンドウのカラーパレットがバグってます。

ここで、1PYS↓←02PBY↑XLR03を同時に押すことで、08 56 79 C8 00 という命令が実行され、テキストポインタC87956へジャンプする処理がなされます。
0とか3とかいうボタンってなんじゃい?と思うかもしれませんが、SFCのコントローラの識別信号として実際に受付けているもので、スーパースコープか何かを挿すと色々と押しっぱなし判定になります。
これはSnes9x等では入力信号として認識されませんが、Lsnesではちゃんと処理されます。
そんな入力ずるくない?と思われるかもしれませんが、過去にはスーパーマリオワールドGlitched TAS で普通に使われていたので問題無いと判断しました。

また、この入力のお陰でデバッグメニューを経由せずに TO BE CONTINUED.. にたどり着くことが出来たので、むしろ前回より正当なクリアといえるでしょう。

みんなは真似すんじゃないぞ!ちゃんとプレイしろよ!!

【TAS】 スーパーマリオワールド "glitched" in 01:39.74 について。

Masterjunさんによって制作された、クレジット呼び出しバグによるクリアTASの解説を少しだけ書こうと思います。足りないところは http://tasvideos.org/3957S.html を読むべし。
解説の大部分はMasterjun氏と、あんた氏によるテキストから引用してます。

・任意コード実行への道筋

以前の glitched TAS では、乗ると回転するリフト上で「無を吐く」ことでゲームが暴走することをトリガーにしていましたが、このTASではバグスプライトの#FAをトリガーとしています。
このスプライトは通常プレイでは決して登場せず、本来スプライトの挙動が記述されている領域とは異なるアドレス($0322)のコードを実行してしまいます。

・バグスプライト#FA生成

バグスプライトの#FAは、内部タイマーが動いてる『空飛ぶハテナブロック』をヨッシーの口内からしゃがんだ状態で吐き出すか、しゃがまずに吐き出してブロックを当てることで産み出されます。
このバグはスタンバグと呼ばれ、スイッチからプクプクが出たり中身の無い甲羅からハダカガメを産み出すような現象を指しています。
甲羅の場合は踏み潰した時にタイマーが起動し、それを一度口に含むことで消滅待ち状態が初期化され、中身発生待ち状態にすることができます。

・OAM
OAMとは、スプライトを画面上に描画するための状態を格納している領域です。
図は $0320 からのOAM領域を示していますが、実際には$0300から始まっています。

スプライトは、X座標・Y座標・タイル・状態の4バイトで構成され、#9~#0のスロットにはそれぞれ4×5バイトのサイズの枠が用意され、#B・#Aのスロットには特殊なスプライトしか入りません。

通常、画面に登場したスプライトは #9,#8,#7...と開いてるスロット順に格納されます。
$0322に出来るだけ近い位置のコードを途中で途切れないように実行するためには、多くのスプライトを調整しなければなりません。

・クレジットに必要なコード
クレジットに到達するためには、ゲームモード($0100)に0x18と$13C6に0x08を入れ、そして "return" する必要があります。
コードの詳細は説明しませんので、必要な方は各自 65816 Refernce 等を読むなりお願いします。
そのために必要なコードは " A9 18 8D 00 01 A9 08 8D C6 13 6B " と11バイトもあり、不自由なOAM領域だけで用意することはできません。

そこで、OAM領域にコントローラデータ領域($4218)にジャンプするコードを書くことで、ある程度自由なコードを実行することが可能になります。
そのために必要なコードは " 4C 18 42 " や " 4C 19 42 " のようになります。
コード " 4C 18 42 " を実行するには、X座標を0x4C、Y座標を0x18、タイルを0x42にセットする必要があります、タイル0x42を使うスプライトはPスイッチです。

・土管地下

物を持った状態で土管を潜るとスプライトのスロットが#0変化してしまいます。
ここではPスイッチを#2のままにしたいので、甲羅との2個持ちをすることで甲羅の方のスロットだけが#0になるようにスロット調整しています。

土管地下では#FAを生成するために、以下の手順を行なっています。
  1. ブルブルブロックを適切な位置に配置する
  2. 甲羅を踏み潰しながら食べ吐き出すことでハダカガメを出す
  3. ストックのキノコを出しつつダメージでチビになる
  4. キノコの咀嚼と同時にヨッシーの舌を出すことで舌を二度出す
  5. 1回目で甲羅を食べ、2回目でブルブルブロックを食べキャンする(ハダカガメに当たって)
  6. 右に移動してヨッシーの口内を羽の生えたハテナブロックへ変化させる
  7. 羽の生えたハテナブロック(見た目黄色甲羅)をしゃがまずに吐き出す
  8. 転がっている羽の生えたハテナブロックにブルブルブロックを当てる
以上の手順で#FAが生成されると同時にコードが実行されるので、コードが途中で途切れないように#B,#A,#0,#1,#2のスロットのスプライトの位置・状態を適切に調整しておく必要がある。
ただしY座標は毎フレーム0xF0にリセットされるために、バグスプライト#FAが格納されているスロットよりも小さいスロットのスプライトはY=0xF0として処理されてしまうので、#FAはスロット#2のPスイッチよりも小さいスロットの#1に格納されるように#9~#2までのスプライトを埋めておく必要がある。

・地上での事前調整

・コントローラデータ($4218-$421F)

Snes9xとBsnesとでここの処理が異なり、Bsnesの方が実機に近い処理が行われている。
以前のあんた氏の1-2クリアTASでは、実機と挙動が異なるSnes9xが使用されていたが、今回のTASでは正しい処理のLsnesが使用されている。

ひとつのコントローラは2バイトのデータを持ち、4つで合計8バイトのデータまでを自由に扱うことができる。
しかしながら、" A9 18 8D 00 01 A9 08 8D C6 13 6B " の11バイトには足りないので4分割入力する必要がある。
そのために最初の3バイトでコードを実行し、残り5バイトで WAI - Wait for Interrupt 命令でハードウェア割り込みがトリガされるまで待機させた後 8バイト前の $4218 ヘ飛ぶコードを書いている。