FC『ダブルムーン伝説』預かり所バグの実用化 その4

前回で預かり所バグを実用化する具体的手順を示しましたが、実はこの手順中には実機での再現性が疑わしい箇所がありました。「4人目のキャラIDを 0x5D にしてセーブ→ロード」というのがそれで、実機でもフリーズせずにロードできるかどうかが疑問だったので取り急ぎ検証してみました。

結論から言うと:

  • ロードは可能
  • おそらくそのままTASと同様の手順でクリア可能
  • ただしムービーをそのまま再生しても多分desyncする

ということになると思います。検証は以下の手順で行いました:

  • 実機(ニューファミコン)で宝箱フラグ8Byte $642F-$6436 が全て非0、所持金最下位Byte $6482 が 0x5D の状態から開始
  • 預かり所バグを8回連続で行う
  • セーブ→ロードして挙動を見る

最初から宝箱フラグ8Byteを立てているのは実機ではエンカウント調整が不可能なためです。エンカウントが発生してしまうと戦闘終了後に $6481 にゴミが残り、3人目のキャラIDまで変わってしまうので、それを避けるための措置です。
実際にやってみると、問題なくロードされ、戦闘や教会での治療もできるのですが、画面表示がFCEUXとはかなり異なります:


FCEUXでは戦闘画面で普通にPTステータスが表示されるのですが、実機ではなぜか魔法名が表示されてしまっています。また、教会で治療を行う際、FCEUXでは4人目は空欄になるのですが、実機では「ボボ99」と表示されています。

まあ、要はロードさえできれば後はキャラIDを 0x70 に書き換えてセーブするだけなので、基本的な手順はそのまま適用可能なはずだと思います(エンカウント調整ができない関係で2回目のキャラID書き換え以降は実際に試してませんが)。ただ画面表示の挙動がこれほど異なっているということは、ムービーをそのまま再生したとしてもdesyncする公算大だと思います。

2012/02/17 追記: 預けアイテム個数が48を超えているとさらにアイテムを預けられることが判明したので、次回記事にて補足します。

最後に、キャラID 0x5D が抱えている微妙な問題について一応記しておきます(特に役に立つ内容ではないのでこれ以降は読み飛ばしてもOK)。

そもそも異常なキャラIDが含まれたデータがロードに失敗するのは、キャラIDに対応するキャラ名取得処理が無限ループに陥るのが原因です。該当コード PRG #24 $AA3D を見てみると:

; 終端マーカ 0xFF が見つかるまでキャラ名データをコピーする
; $E8:コピー元ポインタ(キャラIDによって決まる)
; $EA:コピー先ポインタ(PT内でのキャラ番号によって決まる)
AA3D : ldy #$00
AA3F : lda ($E8),y
AA41 : sta ($EA),y
AA43 : iny
AA44 : cmp #$FF
AA46 : bne $AA3F

となっており、コピー元ポインタから256Byte以内に終端マーカ 0xFF があることを仮定しています。しかしキャラIDが異常値だとこの仮定は必ずしも成り立たないため、無限ループに陥ってフリーズするケースも出てきます。そこで、キャラIDとコピー元ポインタ $E8 の対応表を作ることでフリーズするかどうかがある程度予想できるわけですが、キャラID 0x5D はどうかというと

0x5D    $20E1

となっています。NESのメモリマップをご存知の方はわかると思いますが、これは明らかに異常で、「0xFF が見つかるまでPPUレジスタを順番に読み続ける」という意味になります(NesDevWikiにある通り、$2008-$3FFF は $2000-$2007 のミラーであることに注意)。実機での再現性が疑わしいというのはまさにこの点で、このコードが無限ループになるのか否かがよくわからなかったため、実際に試してみたわけです(結果的にはとりあえずロードは成功したので無限ループにはならないようですが)。

FCEUXの場合、この処理では $2000-$2006 の読み取りが 0xFF を返すことはなく、またPPUアドレスインクリメントは +1 ($2000 のbit2が0)になっているので、要するに $2007 を読み取り続けてPPUアドレス空間内の 0xFF を検索するということになります。検索開始位置はPPUアドレス $2800 からになるようです。ところがさらに奇妙なことに、検索は1Byte飛ばしで行われることがあります(例えばPPUアドレス $2800 と $2801 はともに読み取られるが、$3FF0 は読み取られるのに $3FF1 は読み取られていない)。これはindirect indexedアドレッシングモードの実装によるもののようです。x6502.cpp の GetIYRD() マクロを見ると:

#define GetIYRD(target)  \
{  \
 unsigned int rt;  \
 uint8 tmp;  \
 tmp=RdMem(_PC);  \
 _PC++;  \
 rt=RdRAM(tmp);  \
 tmp++;  \
 rt|=RdRAM(tmp)<<8;  \
 target=rt;  \
 target+=_Y;  \
 if((target^rt)&0x100)  \
 {  \
  target&=0xFFFF;  \
  RdMem(target^0x100);  \
  ADDCYC(1);  \
 }  \
}

となっており、「Y レジスタを加算した結果がページ境界をまたいだ場合は、その結果に 0x100 をXORしたアドレスに対しても読み取りが発生する」という仕様で、これが発生すると $2007 の2重読み取りになって1Byteスキップされるということです(そもそもこの仕様が正しいのかどうか私にはわかりませんが)。
ともかく、FCEUXの場合はこのように検索が行われ、PPUアドレス終端に達しても 0xFF は見つからず、PPUアドレス $0000 (パターンテーブル)に戻り、$0020 に達したところで 0xFF が見つかってループが終了するようです(これはかなり時間がかかる処理なので、通常のロード処理よりもラグが大きくなります)。

しかし、このFCEUXの処理は実機の動作と一致するのかどうかかなり怪しいものがあります。ちょっと考えただけでも次のような疑問が浮かびます:

  • $2000-$2006 の読み取りが 0xFF を返すことはないのか(特に書き込み専用レジスタの場合、どんな値を返すのか)
  • PPUアドレスが $3FFF に達した状態でさらに $2007 を読み取ると $0000 に戻る、というのは正しいのか
  • indirect indexedアクセスによる $2007 の2重読み取りは実機でも発生するのか

実機でもFCEUXでも無限ループにならないのだから大差ないと言ってしまえばそれまでですが、実際に戦闘画面などで表示の違いを確認したこともあり、挙動が全く一致しているとは考えにくいところです。

まあ、個人的にはエミュレータ=互換機だと思っているので、TASの大まかな戦略/手順がそのまま実機でも通用するのであれば、そこまで細かい挙動の違いを問題にする必要もないとは思いますが、今回のように微妙なケースもあるので一応注意に越したことはないのかなーなどと思ったり。