DQ3戦闘部分解説18

今回から16進数を羅列していた部分をアセンブリ命令?に変えることにします。本当はJavaScriptとCSSで切り替えるようにできればいいんですが、はてなはJSは禁止らしいので、とりあえずペンディング。このブログ見ながら16進数をぽちぽち打ってた人には悪いのですが、まあ今までの表記が邪道過ぎたので。なお、表記はdis65816で逆汗した結果っぽい感じのものを使っています。暇があれば過去のエントリも同様の表記に変更していきます。

  • SR: $0287B9 一撃必殺武器ヒット判定(ヒットc=on)
0287B9 PHY Push Y
0287BA PEA $2011 Push $2011
0287BD PEA $0020 Push $0020
0287C0 PEA $7E00 Push $7E00
0287C3 JSL $09029E SR: $09029E イベント戦闘か調べる
0287C7 CLC c=off
0287C8 BNE #$67 if(z==off) goto $028831
0287CA LDA $23F2 A=$23F2
0287CD CLC c=off
0287CE BNE #$61 if(z==off) goto $028831
0287D0 LDX $23EE X=$23EE
0287D3 JSL $02CC47 SR: $02CC47 引数:1#$007A 引数:2#$0040 戦闘行動が一撃必殺が発生する行動か調べる
0287DB CLC c=off
0287DC BEQ #$53 if(z==on) goto $028831
0287DE LDA $23E4 A=$23E4
0287E1 STA $2428 $2428=A
0287E4 LDA #$0000 A=#$0000 アイテム種別:武器をセット
0287E7 JSL $02B8AA SR: $02B8AA 装備アイテムID取得
0287EB LDA $242C A=$242C
0287EE CMP #$0009 A==#$0009? 毒針か
0287F1 BEQ #$06 if(z==on) goto $0287F9
0287F3 CMP #$0031 A==or>=#$0031? アサシンダガーか
0287F6 CLC c=off
0287F7 BNE #$38 if(z==off) goto $028831
0287F9 JSL $0012D1 SR: $0012D1 乱数発生 $00-FF
0287FD AND #$000F A&=#$000F 15/255で一撃必殺発生
028800 CLC c=off
028801 BNE #$2E if(z==off) goto $028831
028803 LDA $23E8 A=$23E8
028806 STA $2428 $2428=A
028809 LDA #$FFFF A=#$FFFF ダメージ65535をセット
02880C STA $00 DP($00)=A
02880E JSL $02BE8A SR: $02BE8A 引数:1#$06 HP減産処理
028813 JSL $02B977 SR: $02B977 パーティウィンドウ再描画?
028817 LDA $23EE A=$23EE
02881A PHA Push A
02881B LDA #$0018 A=#$0018
02881E STA $23EE $23EE=A
028821 JSL $02CFCE SR: $02CFCE 消滅処理?
028825 PLA Pull A
028826 STA $23EE $23EE=A
028829 JSR $8833SR: $028833 毒針ヒット時の処理
02882C JSL $02B054 SR: $02B054 行動対象者を戦闘から離脱させる処理メイン
028830 SEC c=on
028831 PLY Pull Y
028832 RTS return

イベント戦闘(ボス戦)の場合は一撃必殺が発生しません。ゾーマが毒針一撃で倒されることもない、ということですね。また、毒針もアサシンダガーもどちらも一撃必殺の確率は同じです(約1/16)。一撃必殺時には65535をHPから引ことで確実に死ぬ、ということのようです。FC版のDQ2でもザラキはHP255マイナスとかそんな実装だったような。

さて、ようやく一番のメインディッシュの「戦闘行動実行」にたどり着きました。

  • SR: $02885E 戦闘行動実行
02885E PHY Push Y
02885F SEP #$20 m=on(A/M:8b)
028861 LDA #$C2 A=#$C2
028863 PHA Push A 1回目
028864 REP #$20 m=off(A/M:16b)
028866 LDA #$888E A=#$888E
028869 DEC A– 戦闘行動を実行した後のプログラムカウンタを用意
02886A PHA Push A 2回目
02886B LDX $23EE X=$23EE
02886E SEP #$20 m=on(A/M:8b)
028870 JSL $0903EE SR: $0903EE 引数:1#$00 引数:2#$001D 引数:3#$C20060 引数:4#$0008 戦闘行動アドレス上位1バイトを取得(m=offなので1バイトしかセットされない)
02887C PHA Push A 3回目
02887D REP #$20 m=off(A/M:16b)
02887F JSL $0903EE SR: $0903EE 引数:1#$00 引数:2#$001D 引数:3#$C20060 引数:4#$0006 戦闘行動アドレス下位2バイトを取得
02888B DEC A– 3回目 戦闘行動開始用のプログラムカウンタを用意
02888C PHA Push A 4回目
02888D RTL return <-JSRでコールしたのにRTLでリターン
02888E BCC #$21 if(c==off) goto $0288B1
028890 LDA #$0001 A=#$0001
028893 PEA $23AD Push $23AD
028896 PEA $0001 Push $0001
028899 PEA $7E00 Push $7E00
02889C JSL $0902E9 SR: $0902E9 RAM上情報変更
0288A0 PEA $23AE Push $23AE
0288A3 PEA $0002 Push $0002
0288A6 PEA $7E00 Push $7E00
0288A9 JSL $0902E9 SR: $0902E9 RAM上情報変更
0288AD JSL $02CF00 SR: $02CF00 描画系?表示エフェクトプログラム2関連
0288B1 PLY Pull Y
0288B2 RTS return <-JSRでもともとコールされているのでRTSで帳尻あわせ?

通常ではやったらバグ確実のコードですが、非常にうまくできています。SNES(65816?)では、通常はSR内でスタックにPUSHしたら必ず出る前にPULLするのがお約束で、これを守らないとすぐにバグります。スタックにはSRコール時にコールしたアドレスの情報もPUSHされるらしく、RTS,RTLするとこれがPULLされて1インクリメントされるので次に実行すべき命令の位置がわかる、というようになっているようです。この情報のことをプログラムカウンタ(PC)と呼びます*1。したがって、SR内でPUSHとPULLの数の対応が取れていない場合、予期しない値がプログラムカウンタとして扱われるためにとんでもない場所に実行位置が飛んでしまってバグが起きる、ということのようです。また、JSR(#$20),JSL(#$22)によってPUSHされるプログラムカウンタのサイズ?が異なるせいか、JSRでコールしてRTL(#$6B)でリターンしたり、JSLでコールしてRTS(#$60)でリターンすると、リターン後のプログラムカウンタが正しく取得できなくなってこれまた即バグります。改造する場合は、SRを別の場所に作って飛ばして戻す、という処理をよくやるわけですが、JSRとRTS、JSLとRTLの対応はきちんととるようにしましょう。そういう意味では、分岐してそのままリターンというようなコード(下記参照)は書かないほうがいいかもしれません。

  • SR: $0DE6E1 光の鎧を入手しているか(該当c=on) <-よくない例?
0DE6E1 LDA $3544 A=$3544
0DE6E4 AND #$0010 A&=#$0010
0DE6E7 BEQ #$02 if(z==on) goto $0DE6EB
0DE6E9 SEC c=on
0DE6EA RTL return <-2箇所でリターンしている
0DE6EB CLC c=off
0DE6EC RTL return <-2箇所でリターンしている

もうひとつ、DQ3やDQ6ではSR: $0903EEのような直後に引数をとるようなSRが数多く使用されていますが、これもおそらくプログラムカウンタを利用して、呼び出し元のアドレスを取得し、その後ろに設定されている値を取得する、という手法がとられているものと思われます。

さて、SR: $02885Eに話を戻すと、通常ではやってはいけないPUSHしっぱなしを都合4回やっています。SRに入った直後ではプログラムカウンタはスタックの一番上に置かれています(多分)。リターン時にはスタックの一番上のデータをプログラムカウンタとして取得し、1インクリメントしてプログラムの実行を続ける、というのは説明したとおりですが、このルールに当てはめると、$02888Dでリターンした直後、スタックの一番上は以下のようになっているはずです。

0 戦闘行動実行アドレス(下位2バイト)-1
1 戦闘行動実行アドレス(#$00C2?)
2 #$888D
3 #$00C2
4 #$7DA7 SR: $02885Eを呼び出した位置

JSLでリターンしたので、スタックを2つPULLして3バイトのプログラムカウンタとして扱い、1インクリメントして実行を続けます。これが戦闘行動の実体部分の実行です。各戦闘行動の実体部分の最後もRTLでリターンします。このときのスタックの状態は

0 #$888D
1 #$00C2
2 #$7DA7 SR: $02885Eを呼び出した位置

となっているはずです。再度スタックが2つPULLされ、1インクリメントして$02888Eから実行を続け、戦闘行動に対応するエフェクトなどを実行します。$0288B2ではRTSでリターンします。このときのスタックの状態は

0 #$7DA7 SR: $02885Eを呼び出した位置

となっています。スタックを1つPULLして、これ以降はスタックは正常に戻ります。DQ6の戦闘行動実行部分の解析をやっていないのでわかりませんが、DQ6のエフェクト実行部分ではプログラム開始位置らしきものを指定する場合、あらかじめアドレスを-1しておくのがお作法でした。後発のDQ3では、同じエフェクト実行部分でプログラム開始位置らしきものを指定する場合は-1せず、実際の開始位置が指定されていました。根っこのSRで上記のように開始位置を-1してやることで、より直感的なアドレス指定が実現されている(=ひいては設定ミスによるバグがおきにくくなる)と思います。DQ3ではこうしたDQ6のエンジンをより洗練させて流用していると見受けられる部分が何箇所かあります。

結構な分量になったのでとりあえずここまでにして、次回は上記戦闘行動実行で呼び出される実体部分について、いくつか特徴的なものをピックアップして解説します。

*1:以前のSnes9x Debuggerではこの情報は見えなかったのですが、最新のSnes9x Debugger1.51ではレジスタの情報とともにこのプログラムカウンタの情報も見えるようになっており、気になる人は見てみるといいと思います