ドラゴンスレイヤー英雄伝説 (SFC) 初回アクダム撃破バグの調査 (暫定)
概要:
- 初回アクダムを呪文で倒すとバグが発生してフリーズし、セーブデータが消える。
- バグの原因はスタックの不整合である。これは RAM 上のコードの実行を伴う。
- 今のところスピードランへの応用は難しそうである (よほど都合の良いパターンがない限り)。
SFC版『ドラゴンスレイヤー英雄伝説』は、初回アクダム戦がいわゆる負けイベントとなっており、打撃による致死ダメージは必ず回避される。しかし、攻撃呪文で止めを刺すことは可能であり、これを行うとフリーズが発生してセーブデータが消えることが知られている (他機種版にも類似のバグが存在する模様)。
このバグの原因は、ダメージ系呪文の処理においてスタックの不整合が生じることである。以下に詳細を記す。
ダメージ系呪文の処理はルーチン $02C3D7 で行われる。また、ダメージが致死量の場合、下請けルーチン $02DAAA が呼ばれ、初回アクダムを致死ダメージから保護する (HPが 1 ならば呪文が効かなかったことにし、さもなくばダメージを (HP)-1 とする) 処理が行われる。この付近のコードを以下に示す:
; ダメージが致死量の場合に実行される
L_02C48B:
.a16
.i8
sta $1B27
; XXX: 初回アクダム戦の場合、X=0 (8 ビット) をスタックに積む。
; しかし、これが正しく pop されない!
phx
; 初回アクダム保護ルーチンを呼ぶ。
; アクダムの元のHPが 1 でないなら L_02C4A7 へ。
jsr $02DAAA
bcs L_02C4A7
; アクダムの元のHPが 1 の場合、呪文が効かなかった扱いになっている。
; この場合 $1B2D が 1 になるので直ちに終了処理 (L_C2C69D) へ飛ぶ。
sep #$30
.a8
lda $1B2D
beq L_02C49E
jmp L_02C69D
; 初回アクダム以外を倒した場合、その敵を気絶状態にする。
L_02C49E:
lda #$80
plx ; 正常系では先ほど push した X が正しく pop される。
ora $108D,x
sta $1B32
L_02C4A7:
; ... (中略) ...
L_02C69D:
plp ; XXX: 初回アクダム戦の場合、先ほど積んだ 0 を P として pop してしまう!
rts
上記の通り、初回アクダム戦で呪文による致死ダメージが発生した場合は 0 (8 ビット) がスタックに積まれるが、この値は正しく pop されず、ルーチン終了時に P として pop される (アクダムの元のHPが 1 かどうかによらない)。また、これは本来ルーチン冒頭で push した P を pop する意図なので、スタックが 1 バイトずれてしまっている。
結論だけ言うと、これは「アキュムレータとインデックスレジスタがともに 16 ビットの状態で $028815 へ戻る」動作となる。ここからは CPU が暴走状態となるが、$02882B で phd を行った後に $02882E で rts するまでの動作は一定と思われる。本作は D レジスタの値が常に 0 なので、これは 0x0000 を積んでから rts する、即ち $020001 へ飛ぶことになる。
$020000- は LowRAM のミラーなので、これは WorkRAM $01- を実行するのと等価である。特に工夫せずマスクーンに入ってそのまま初回アクダムを倒した場合、これは最終的に $11 の brk を実行する。本作は BRK 割り込みアドレスが 0xFFFF となっている関係上、再び $11 の brk が実行され、無限ループが発生してフリーズする。
なお、このように brk を実行し続けるとスタックオーバーフローが発生し、スタックポインタは ROM 領域を指すようになる。こうなるとスタック経由での値の退避/復元、特に plb が正しく動かなくなるため、NMI ハンドラ内で DB レジスタの値がおかしくなる (0 になるはずが 0xFF になる)。すると、例えば sta $2100 のようなコードは $002100 でなく $FF2100 (SaveRAM のミラー) に書き込むことになり、結果としてセーブデータまで壊れてしまう。
このバグは RAM 領域を実行するので、スピードランへの応用を考えたいところだが、$00- 付近は雑用変数であり、$03-$0E あたりはアクダム撃破までに一定の値になると思われるので、制御はなかなか難しそうである。
可能性があるとすれば $00-$02 の調整である。これはマスクーンに入った際に一定の値になるが、そこからアクダム戦撃破までの間は書き換えられず、またセーブデータをロードした際も値が変化しないようである。よって、アクダム戦開始前にセーブしておき、別のセーブデータを用いて $00-$02 を調整してからロード→アクダム戦開始、とすれば $01- の実行コードを変化させられる。とはいえ、$00-$02 はほぼポインタ変数として使われており、書ける値には制限があるため、スピードラン的に都合の良いパターンがあるかどうかはやや疑問である。