概要:

  • 初回アクダムを呪文で倒すとバグが発生してフリーズし、セーブデータが消える。
  • バグの原因はスタックの不整合である。これは 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 が暴走状態となるが、$02882Bphd を行った後に $02882Erts するまでの動作は一定と思われる。本作は D レジスタの値が常に 0 なので、これは 0x0000 を積んでから rts する、即ち $020001 へ飛ぶことになる。

$020000- は LowRAM のミラーなので、これは WorkRAM $01- を実行するのと等価である。特に工夫せずマスクーンに入ってそのまま初回アクダムを倒した場合、これは最終的に $11brk を実行する。本作は BRK 割り込みアドレスが 0xFFFF となっている関係上、再び $11brk が実行され、無限ループが発生してフリーズする。

なお、このように brk を実行し続けるとスタックオーバーフローが発生し、スタックポインタは ROM 領域を指すようになる。こうなるとスタック経由での値の退避/復元、特に plb が正しく動かなくなるため、NMI ハンドラ内で DB レジスタの値がおかしくなる (0 になるはずが 0xFF になる)。すると、例えば sta $2100 のようなコードは $002100 でなく $FF2100 (SaveRAM のミラー) に書き込むことになり、結果としてセーブデータまで壊れてしまう。

このバグは RAM 領域を実行するので、スピードランへの応用を考えたいところだが、$00- 付近は雑用変数であり、$03-$0E あたりはアクダム撃破までに一定の値になると思われるので、制御はなかなか難しそうである。

可能性があるとすれば $00-$02 の調整である。これはマスクーンに入った際に一定の値になるが、そこからアクダム戦撃破までの間は書き換えられず、またセーブデータをロードした際も値が変化しないようである。よって、アクダム戦開始前にセーブしておき、別のセーブデータを用いて $00-$02 を調整してからロード→アクダム戦開始、とすれば $01- の実行コードを変化させられる。とはいえ、$00-$02 はほぼポインタ変数として使われており、書ける値には制限があるため、スピードラン的に都合の良いパターンがあるかどうかはやや疑問である。