ユーザーブロマガは2021年10月7日(予定)をもちましてサービスを終了します

  • なぜ"電源ON/OFFバグ"は発生するのか? ~ファミコン版ドラゴンクエスト3~

    2020-12-18 17:464

    【電源ON/OFFバグ】を簡潔に言い表すと

    「りせっとをおさずに
     でんげんをきると
     ぼうけんのしょが きえてしまう
     ばあいがあります!!」
    というメッセージの表示後に電源をON/OFFすると
    メモリがグチャグチャになるバグである。



    電源ON/OFFバグ後にコントローラのいずれかのボタンを押す、
    またはボタンに反応がない場合はリセットボタンを押してから操作をすると
    画面がバグった状態で『全滅復帰』に遷移するので、
    バグった画面のまま王様に話しかけてセーブした後に
    リセットをすることで画面が正常化される。

    バージョンによる違い

    FC版DQ3にはいくつかのバージョンがあり、ツルツル無印のガワが初期版で
    写真では判別しにくいが右上に薄っすらと刻印があり、無印とA刻印は問題ないが
    後期版(ザラザラB刻印)ではこのバグが発生しないことが知られている。

    この後期版は初期版と比べてセーブデータが消えにくいとの噂があるが、
    DQ3初期版にはMMC1Aという制御回路が搭載され、
    DQ3後期版にはDQ4にも搭載されているMMC1B2が使用されているため
    前者に比べて比較的安定していると推測される。



    また、海外版"Dragon Warrior III"にはMMC1B3が搭載されているので
    電源ON/OFFバグは発生しないのではないかと予想され、
    森田将棋にはMMC1が搭載されている。

    バッテリーバックアップの仕様

    wikipediaの"バッテリーバックアップ"の頁によると
    • ファミリーコンピュータはバッテリーバックアップを考慮していないプラットフォームである[注 1]。すなわちCPUとカートリッジのメモリ空間がCPUのバスを通じて直接接続されており、さらに一定容量以上のカートリッジは、カートリッジ内にメモリ空間を拡張するための制御回路(MMC, Multi Memory Controller)を搭載しバンク切り替えを行っている。このため単純に電源を切ると、電源を切断して回路の電圧・電流が低下した瞬間に、CPUやMMCが誤動作する可能性がある。カートリッジバス上のMMCの誤動作やメモリ空間にSRAMが見えている最中にこのような状況が発生すると、SRAMのデータ化けを起こす確率が非常に高くなる。バッテリーバックアップ機能を搭載したファミリーコンピュータ用カートリッジの多くは、電源を切る際に、リセットボタンを押したまま電源スイッチをOFFにする必要があるが、これはリセットボタンを押している間はCPUの動作が完全に停止する為である[注 2]
    • 前述のエラーや書き込まれるデータの問題によってセーブデータが化けていた場合、そのエラーの大小にかかわらず該当データはゲームによって破棄される実装になっているものが多く、取り扱いや接続端子の状態によっては記録したデータを喪失するケースも発生する。
    とのことで冒険の書にエラーが発生しやすい設計になっているが、
    少しでもデータに不具合があるとCRCによる誤り検出に引っかかり
    【呪いのモチーフ】とメッセージが流れた後、
    ボタン操作をすると冒険の書を削除する処理が発生する。

    この時、MMCの誤動作によってバッテリーバックアップの
    読み込みミスがあった場合も同様に冒険の書の削除が行われてしまうが、
    ごく稀に冒険の書5番などが消えてしまうことがある。

    電源バグ検証中に冒険の書5が消えたぜ。これだからドラクエ3はやめられないぜ。

    FC版 ドラクエ3の電源バグ 雑談 / ニコ生TS https://t.co/U1vR0ef0hK pic.twitter.com/vtn74sAVaI

    — Hitsheegame (@Hitsheegame) October 29, 2020
    このような読み込みミスが発生した場合は、
    冒険の書が削除される前にリセットをすることで
    冒険の書が復活したように見えることがあるが、
    本当に誤り検出に引っかかっている場合は
    何度リセットをしても冒険の書が復活することはない。

    復活することはないのだが、
    データの先頭である主人公のレベルが0に削除された瞬間にリセットを押して
    仲間の誰かがレベル0ではないという状態を作れれば
    誤り検出をスルー出来る冒険の書になるという仕様があるため、
    いわゆるサブフレームリセットTASへの利用が期待されている。


    またDQ4は削除してから曲とメッセージを流す無慈悲な仕様であるため
    いくらリセットをしても意味はない。

    MMCについて

    調査したところ、
    DQ3後期版ではバンク切り替え時にカートリッジ上のSRAMへのアクセスを
    ON/OFF可能なMMC1B2を搭載しているので
    「ぼうけんのしょが きえてしまう ばあいがあります!!」
    の最後の文字が表示され無限ループ処理に入る直前に
    SRAMへのアクセスを無効にするコードが書かれているが、

    DQ3初期版のMMC1AはカートリッジのSRAMへのアクセスが常に有効であるために
    SRAMへのアクセスを無効にするコードの名残りのようなものが存在しているが
    仕様上オフにはならず無限ループ時には不安定になっていると予想され、
    SNROM+MMC1の構成では$A000~$BFFFの4ビットを1にすることで
    カートリッジのSRAMへのアクセスがオフにされるらしく、
    その画面でリセットを押すと王様の前から全滅復帰する。

    DQ3 後期版
    0D:BBC7:AD D5 06 LDA $06D5 = #$0D // bank 0Dへの切り替え準備
    0D:BBCA:09 10   ORA #$10     // bank 1xでSRAMへのアクセス無効
    0D:BBCC:20 BD FF JSR $FFBD     // バンク切り替えのサブルーチン
    0D:BBCF:4C CF BB JMP $BBCF     // 無限ループ

    DQ3 初期版
    0D:BBC7:A9 10   LDA #$10     //4bit=1
    0D:BBC9:8D C8 06 STA $06C8 = #$00 // NMI内で$BFFF=#$10になる
    0D:BBCC:4C CC BB JMP $BBCC    // 無限ループ
    -------------------------NMI-----------------------------
    ~~~~~~~~~~~~~~~~~~~~~~~~~~
    0F:C64E:8D C8 06 STA $06C8 = #$10
    0F:C651:8D FF BF STA $BFFF = #$BF
    0F:C654:4A LSR
    0F:C655:8D FF BF STA $BFFF = #$BF
    0F:C658:4A LSR
    0F:C659:8D FF BF STA $BFFF = #$BF
    0F:C65C:4A LSR
    0F:C65D:8D FF BF STA $BFFF = #$BF
    0F:C660:4A LSR
    0F:C661:8D FF BF STA $BFFF = #$BF
    0F:C664:60 RTS
    ~~~~~~~~~~~~~~~~~~~~~~~~~~

    全滅復帰する理由について

    調査すると一つのフラグが影響してることがわかる。

    バッテリーバックアップ上の$6A58というフラグは通常
    冒険の書を作成してマップが読み込まれる際に0x03にセットされたり
    冒険の書を選択して再開した場合には0xFFにセットされたりしている。
    このフラグはリセットもしくは電源投入時にこの$6A58が0に初期化され、
    そのしばらく後に$6A58=0であれば条件分岐によってタイトル画面へ遷移するが
    何らかの理由で$6A58が0以外であれば全滅復帰へ分岐する。
    通常の全滅時に何らかの理由でこのフラグがオフになっていれば
    タイトル画面に戻ることになると予想される。

    後期版では$6A58への書き込みがMMCの初期化前に行われているが、
    初期版ではMMCの初期化前に$6A58に書き込みを行おうとするために
    MMCが不安定になった状態では$6A58への書き込みが失敗し、
    $6A58を読み込む際に0以外の値が取得されて全滅復帰に分岐していると考えられる。

    メモリがグチャグチャになる理由

    本体側のメモリは電源を切ることで時間経過とともに揮発していくが、
    本来必要な初期化処理は冒険の書選択時に行われているため
    タイトル画面がスキップされたことで揮発したメモリがそのまま使用されることになる。

    文字がグチャグチャになる理由としては、
    大半の画像は暗転時に読み込み直されるのに対して
    文字の画像だけはタイトル画面で一度しか読み込まれないため、
    電源ON/OFFバグ時にはタイトル画面がスキップされて
    文字が読み込み直されないことが原因であると考えられる。

    出来ることと出来ないこと

    本体メモリをバグらせられるのは主にキャラステータスである名前や習得呪文や
    キャラステータスに含まれる所持アイテムとルーラ登録地などで、
    フラグ類はすべてカートリッジ側で管理しているため
    復活場所やゾーマ打倒フラグなどをイジることは出来ません。
    また後期版は使えないため、2コンAボタン押しノーエンカ技も使えません。

    カセットの抜き差しや交換などをしていると
    イキナリ全滅復帰する事例も報告されているので
    もしかしたら抜き差しによってMMCを不安定化させて
    王様までの道のりを全滅復帰でショートカットすることも可能かもしれません。

    ついでにRTAについて

    電源ON/OFFバグによってメモリのビットが立ちやすい本体であれば
    高レベル高ステータスやにじのしずくが期待しやすくRTA向きであり、
    逆にビットが下がりやすい本体であればPTの存在フラグなども消え
    低レベル低ステータスになりやすくRTAには不向きであると言える。

    DQ3なんでもありRTAガチ勢は本体ガチャを行っているが、
    初期型ファミコンやNEWファミコン(AV仕様)ではバグでの変化が乏しく
    一部のファミコン互換機なども出荷時期によっては
    期待できるロットと期待できないロットが混在し混迷を極めているらしい。


    また、時間と温度によってバグり方が変化することが知られているが
    メモリの揮発速度が温度で変化することが原因の一つとして考えられ、
    それ以外にも本体内部の各種コンデンサなども熱の影響を受けそうである。

    電源タップのスイッチを用いた電源ON/OFFバグもあり、この場合は
    アダプタの性能や本体電源部の三端子レギュレータの温度も考慮する必要がある。

    うちには3種類のACアダプタがあるが、
    うちの後期型ファミコンと純正アダプタの組み合わせでは
    にじのしずくが出やすいのは2.5~8秒前後電源を落とした時で、
    コロンバスサークルのミニACアダプタでは1.2~3秒程度でにじのしずくが出やすかった。

    最後は電源タップのスイッチを入れてから映像が来るまでに1秒かかるアダプタで
    定格電流が1000mAと他のアダプタの850mAと違うことも影響しているのか
    メモリの変化も乏しく、RTAには不向きであった。

    また、本体温度が上がると酒場に登録された賢者が高火力になりやすいが、
    賢者が遊び人に変化しやすくなるというジレンマがあるため
    RTAでは温度管理が非常に大事になってくるため、
    ファミコンの下に古いPCケースのサイドパネルを放熱板として敷いている。

    当初は本体側の電源ON/OFFによって棺桶バグを誘発して
    アイテム変化を狙う安定型チャートだったが、
    電源タップのスイッチを使うことでにじのしずくが狙えて
    理力の杖持ちルカニ僧侶とバイキルト商人と高攻撃力の賢者が出る
    何気に記録が狙える神個体だったことが判明し、リアルラック勢への転向を決意。


    最後に

    2020年12月27日~31日に行われる『RTA in Japan 2020』では、初日の15:50あたりに
    『ドラゴンクエスト3 なんでもありRTA 4人レース』が行われます。
    日曜日のいい感じの時間なので、お時間がある人は是非応援してください!
    お時間のない人もアーカイブでのご視聴をどうぞ!

    また走者も増えているのでこれを見て自分もRTAしてみたいと思った方は是非!!

    本体個体値、時間調整、温度調整、
    ACアダプタ、電源タップ、
    組み合わせは無限大!
    君だけのRTAチャートを作ろう!!!








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

    2019-02-13 19:003

    ※注意: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回実行するためにつけてます。

    000102030405060708090A0B0C0D0E0F
    00
    10
    20
    30
    40
    50
    60
    70Ex
     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の更新部分のちょっとした解説

    2015-06-06 02:282

    英語による解説文は 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待ちの箇所へジャンプしてエンディングを待つ

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