DQ3戦闘部分解説18

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

  • SR: $0287B9 一撃必殺武器ヒット判定(ヒットc=on)
0287B9PHYPush Y
0287BAPEA $2011Push $2011
0287BDPEA $0020Push $0020
0287C0PEA $7E00Push $7E00
0287C3JSL $09029ESR: $09029Eイベント戦闘か調べる
0287C7CLCc=off
0287C8BNE #$67if(z==off) goto $028831
0287CALDA $23F2A=$23F2
0287CDCLCc=off
0287CEBNE #$61if(z==off) goto $028831
0287D0LDX $23EEX=$23EE
0287D3JSL $02CC47SR: $02CC47 引数:1#$007A 引数:2#$0040戦闘行動が一撃必殺が発生する行動か調べる
0287DBCLCc=off
0287DCBEQ #$53if(z==on) goto $028831
0287DELDA $23E4A=$23E4
0287E1STA $2428$2428=A
0287E4LDA #$0000A=#$0000アイテム種別:武器をセット
0287E7JSL $02B8AASR: $02B8AA装備アイテムID取得
0287EBLDA $242CA=$242C
0287EECMP #$0009A==#$0009?毒針か
0287F1BEQ #$06if(z==on) goto $0287F9
0287F3CMP #$0031A==or>=#$0031?アサシンダガーか
0287F6CLCc=off
0287F7BNE #$38if(z==off) goto $028831
0287F9JSL $0012D1SR: $0012D1乱数発生 $00-FF
0287FDAND #$000FA&=#$000F15/255で一撃必殺発生
028800CLCc=off
028801BNE #$2Eif(z==off) goto $028831
028803LDA $23E8A=$23E8
028806STA $2428$2428=A
028809LDA #$FFFFA=#$FFFFダメージ65535をセット
02880CSTA $00DP($00)=A
02880EJSL $02BE8ASR: $02BE8A 引数:1#$06HP減産処理
028813JSL $02B977SR: $02B977パーティウィンドウ再描画?
028817LDA $23EEA=$23EE
02881APHAPush A
02881BLDA #$0018A=#$0018
02881ESTA $23EE$23EE=A
028821JSL $02CFCESR: $02CFCE消滅処理?
028825PLAPull A
028826STA $23EE$23EE=A
028829JSR $8833SR: $028833毒針ヒット時の処理
02882CJSL $02B054SR: $02B054行動対象者を戦闘から離脱させる処理メイン
028830SECc=on
028831PLYPull Y
028832RTSreturn

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

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

  • SR: $02885E 戦闘行動実行
02885EPHYPush Y
02885FSEP #$20m=on(A/M:8b)
028861LDA #$C2A=#$C2
028863PHAPush A1回目
028864REP #$20m=off(A/M:16b)
028866LDA #$888EA=#$888E
028869DECA–戦闘行動を実行した後のプログラムカウンタを用意
02886APHAPush A2回目
02886BLDX $23EEX=$23EE
02886ESEP #$20m=on(A/M:8b)
028870JSL $0903EESR: $0903EE 引数:1#$00 引数:2#$001D 引数:3#$C20060 引数:4#$0008戦闘行動アドレス上位1バイトを取得(m=offなので1バイトしかセットされない)
02887CPHAPush A3回目
02887DREP #$20m=off(A/M:16b)
02887FJSL $0903EESR: $0903EE 引数:1#$00 引数:2#$001D 引数:3#$C20060 引数:4#$0006戦闘行動アドレス下位2バイトを取得
02888BDECA–3回目 戦闘行動開始用のプログラムカウンタを用意
02888CPHAPush A4回目
02888DRTLreturn<-JSRでコールしたのにRTLでリターン
02888EBCC #$21if(c==off) goto $0288B1
028890LDA #$0001A=#$0001
028893PEA $23ADPush $23AD
028896PEA $0001Push $0001
028899PEA $7E00Push $7E00
02889CJSL $0902E9SR: $0902E9RAM上情報変更
0288A0PEA $23AEPush $23AE
0288A3PEA $0002Push $0002
0288A6PEA $7E00Push $7E00
0288A9JSL $0902E9SR: $0902E9RAM上情報変更
0288ADJSL $02CF00SR: $02CF00描画系?表示エフェクトプログラム2関連
0288B1PLYPull Y
0288B2RTSreturn<-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) <-よくない例?
0DE6E1LDA $3544A=$3544
0DE6E4AND #$0010A&=#$0010
0DE6E7BEQ #$02if(z==on) goto $0DE6EB
0DE6E9SECc=on
0DE6EARTLreturn<-2箇所でリターンしている
0DE6EBCLCc=off
0DE6ECRTLreturn<-2箇所でリターンしている

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

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

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

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

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

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

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

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

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

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

コメント

タイトルとURLをコピーしました