DQ3 プレイヤーキャラクター情報へのアクセス

以前バイナリスレで86氏に教えていただいたPC*1のステータス(HP、MP、力など)の取得/変更(SR: $043414 力取得など)は、大抵3,4個の引数を取りますが、なんとなく2番目が対象キャラクターを示し、3番目以降で変更(もしくは取得)する値の指定をしている程度の理解で実装をしていました。現在行っている作業の中でこれらのSRについて正しく理解する羽目になったのでエントリとして投下します。

そもそも各PCのステータス自体は$7E3925-から1PCにつき60バイトの領域を使用して連続して格納されています。$7E3925-は勇者の情報、$7E3961-はルイーダに最初から入る仲間の1人目の情報…のようになっており、この順番は基本固定で、キャラの順番を入れ替えても格納位置が変わることはありません。実際にパーティに参加しているキャラクターの情報等は以前のエントリ(https://retrogamehackers.net/dq3-pcinfo-management-001/)で説明したように$7E36E2-に各種人数、$7E36E8-に各キャラクターの開始アドレスが配列になって並んでいます。とどのつまりは「この開始アドレスをいかにして取得してくるか」が最終的な目的になります。PCのステータスにアクセスするSRは以下の2種類があります。

  1. Xレジスタに開始アドレスがセットされていることを前提としてAレジスタにデータを返す/Aレジスタのデータに変更する
  2. 1をラップしてより柔軟なデータのやり取りをできるようにする

1の実装は極めてシンプルです。

  • SR: $043442 力取得
043442 LDA $000C,X A=$000C+X
043445 AND #$00FF A&=#$00FF
043448 RTL return
  • SR: $043442 力変更
043450 CMP #$00FF A>=#$00FF?
043453 BCC #$03 if(c==off) goto $043458
043455 LDA #$00FF A=#$00FF
043458 SEP #$20 m=on(A/M:8b)
04345A STA $000C,X $000C+X=A
04345D REP #$20 m=off(A/M:16b)
04345F RTL return

Xに各PCの開始アドレスがセットされていることを期待してAに値をセット/Aの値で変更を行っています。他のステータスについても同様の実装がされています。次に2の実装を見てみます。

  • SR: $043414 力取得
043414 PHP Push P Flag
043415 PHB Push DB
043416 REP #$30 m=off(A/M:16b) x=off(X/Y:16b)
043418 PHA Push A
043419 PHX Push X
04341A PHY Push Y
04341B JSL $C42777 SR: $042777 呼び出し元の後の3バイトを引数として取得する
04341F PEA #$7E7E Push #$7E7E
043422 PLB Pull DB
043423 PLB Pull DB
043424 LDX #$40B5 X=#$40B5
043427 JSL $C4289E SR: $04289E 対象キャラのフィールド上情報スタートアドレスを取得
04342B BCC #$03 if(c==off) goto $043430
04342D LDA #$3925 A=#$3925
043430 TAX X=A
043431 JSL $C43442 SR: $043442 力取得
043435 LDX #$40B7 X=#$40B7
043438 JSL $C428FD SR: $0428FD リターン用の変数にセット
04343C PLY Pull Y
04343D PLX Pull X
04343E PLA Pull A
04343F PLB Pull DB
043440 PLP Pull P Flag
043441 RTL return

このSRは引数として1バイト☓3個の引数を取ります。これらの値の取得はSR:042777で行われ、引数の値は7E40B5-B7にセットされます。このSRでは同時にプログラムカウンタの操作も行っているのですが実装については省略します。次が一番重要なSRになります。

  • SR: $04289E 対象のスタートアドレスを特定(失敗c=on)
04289E LDA $0000,X A=$0000+X
0428A1 AND #$00FF A&=#$00FF
0428A4 STA $40BD $40BD=A 第1引数セット
0428A7 LDA $0001,X A=$0001+X
0428AA AND #$00FF A&=#$00FF 第2引数判定
0428AD CMP #$00FF A==#$00FF? #$FFはAレジスタを意味する
0428B0 BNE #$04 if(z==off) goto $0428B6
0428B2 LDA $08,S A=Stack($08) SR: $043414の初めにPUSHしたAレジスタの値を取得
0428B4 BRA #$15 goto $0428CB
0428B6 CMP #$00FE A==#$00FE? #$FEはXレジスタを意味する
0428B9 BNE #$04 if(z==off) goto $0428BF
0428BB LDA $06,S A=Stack($06) SR: $043414の初めにPUSHしたXレジスタの値を取得
0428BD BRA #$0C goto $0428CB
0428BF CMP #$00FD A==#$00FD? #$FDはYレジスタを意味する
0428C2 BNE #$04 if(z==off) goto $0428C8
0428C4 LDA $04,S A=Stack($04) SR: $043414の初めにPUSHしたYレジスタの値を取得
0428C6 BRA #$03 goto $0428CB
0428C8 TAX X=A
0428C9 LDA $00,X A=DP($00+X) A,X,YでなければDP($00+X)から値を取得
0428CB LDX $40BD X=$40BD 第1引数判定
0428CE CPX #$0006 X==#$0006? #$06は第2引数が開始アドレスであることを意味する
0428D1 BEQ #$0F if(z==on) goto $0428E2
0428D3 CPX #$0004 X==#$0004? #$04は第2引数がルイーダ待機中のPCのインデックスであることを意味する
0428D6 BEQ #$1F if(z==on) goto $0428F7
0428D8 CPX #$0005 X==#$0005? #$05は第2引数がバークに残したPCのインデックスであることを意味する
0428DB BEQ #$16 if(z==on) goto $0428F3
0428DD ASL A<<1 それ以外はアドレスリスト($7E36E8-)のインデックスを意味する
0428DE TAX X=A
0428DF LDA $36E8,X A=$36E8+X
0428E2 CMP #$3925 A>=#$3925? アドレスが1人目の開始アドレスより前は異常
0428E5 BCC #$07 if(c==off) goto $0428EE
0428E7 CMP #$3EC5 A>=#$3EC5? アドレスが23人目より後は異常
0428EA BCS #$02 if(c==on) goto $0428EE
0428EC CLC c=off
0428ED RTL return
0428EE SEC c=on
0428EF LDA #$0000 A=#$0000 開始アドレスに0をセットして戻る
0428F2 RTL return
0428F3 CLC c=off
0428F4 ADC $36E4 A+=($36E4+c) ルイーダ待機中人数を足す
0428F7 CLC c=off
0428F8 ADC $36E2 A+=($36E2+c) パーティ人数を足す
0428FB BRA #$E0 goto $0428DD

第1引数は第2引数の種別を意味しているということになります。第1引数が#$06の場合のみ第2引数は対象のPCの開始アドレスそのものを意味します。それ以外は第2引数は7E36E8-の配列のインデックスを意味します。これで対象の開始アドレスが特定できたので必要な値をAレジスタにセットした後、リターン用の変数にその値をセットして終わりになります。

  • SR: $0428FD リターン用の変数にセット
0428FD PHA Push A
0428FE LDA $0000,X A=$0000+X
042901 AND #$00FF A&=#$00FF 第3引数を取得
042904 TAX X=A
042905 PLA Pull A
042906 CPX #$00FF X==#$00FF? #$FFはAレジスタを意味する
042909 BNE #$03 if(z==off) goto $04290E
04290B STA $08,S Stack($08)=A SR: $043414の初めにPUSHしたAレジスタにセット
04290D RTL return
04290E CPX #$00FE X==#$00FE? #$FEはXレジスタを意味する
042911 BNE #$03 if(z==off) goto $042916
042913 STA $06,S Stack($06)=A SR: $043414の初めにPUSHしたXレジスタにセット
042915 RTL return
042916 CPX #$00FD X==#$00FD? #$FDはYレジスタを意味する
042919 BNE #$03 if(z==off) goto $04291E
04291B STA $04,S Stack($04)=A SR: $043414の初めにPUSHしたYレジスタにセット
04291D RTL return
04291E STA $00,X DP($00)+X=A それ以外はDP($00+X)にセット
042920 RTL return

今回は力の値を例として取り上げましたが、同じような実装はPCのステータスに限らず、パーティ人数、所持金額、アイテム名称IDなど、様々な情報をやりとりする場面で使われています。これらのSRをコールする前後では、A、X、YやDPに操作したい値をセットしているはずなので、それと合わせると処理の理解が進むと思います。

通常PCの情報にアクセスするのはインデックスベースであることがほとんどで、アドレスベースでアクセスする必要性は一見なさそうですが、戦闘中においてはアドレスベースでアクセスするのが確実になります。戦闘中はすべて戦闘中キャラクター情報(7E2030-)で話が進みます。ここには戦闘開始時に情報がセットされ、戦闘終了時までキャラクターの並び順が変わっても(パルプンテの効果で発生)変わることはありません。戦闘開始時にA, B, C, Dの順番で並んでいた場合、先頭から戦闘中インデックス0, 1, 2, 3が振られますが、順番が変わって、D, C, B, Aとなった場合(なった時点で7E36E8-の順番も変更)、Dの戦闘中インデックスは3のままです。このインデックスの値をそのまま使ってHPなどの情報を取得しようとすると、Aの値が返ってきておかしなことになります。手段としては「対象の戦闘中インデックスから移動中のインデックスに変換する」方式も考えられるのですが、「対象の戦闘中インデックスから移動中開始アドレスを取得する」のほうが直接的です。

  • SR: $02BE8A 戦闘中情報取得/変更
02BE8A PHP Push P Flag
02BE8B PHB Push DB
02BE8C REP #$30 m=off(A/M:16b) x=off(X/Y:16b)
02BE8E PHX Push X
02BE8F PHY Push Y
02BE90 SEP #$20 m=on(A/M:8b)
02BE92 LDA $09,S A=Stack($09)
02BE94 PHA Push A
02BE95 PLB Pull DB
02BE96 LDY #$0001 Y=#$0001
02BE99 LDA ($07,S),Y A=Stack($07+Y)
02BE9B REP #$20 m=off(A/M:16b)
02BE9D AND #$00FF A&=#$00FF 引数1バイト取得
02BEA0 TAY Y=A
02BEA1 LDA $07,S A=Stack($07)
02BEA3 INC A++
02BEA4 STA $07,S Stack($07)=A プログラムカウンタインクリメント
02BEA6 PEA #$7E7E Push #$7E7E
02BEA9 PLB Pull DB
02BEAA PLB Pull DB
02BEAB LDA $2428 A=$2428 対象の戦闘中インデックスを取得
02BEAE CMP #$0018 A>=#$0018?
02BEB1 BCS #$FE if(c==on) goto $02BEB1
02BEB3 ASL A<<1
02BEB4 TAX X=A
02BEB5 LDA $C2CBD3,X A=$02CBD3+X 戦闘中開始アドレスを取得する
02BEB9 TYX X=Y このSRの引数をXレジスタに移す
02BEBA TAY Y=A
02BEBB LDA $2050,Y A=$2050+Y
02BEBE AND #$0002 A&=#$0002
02BEC1 BEQ #$FE if(z==on) goto $02BEC1
02BEC3 LDA $2050,Y A=$2050+Y
02BEC6 AND #$0001 A&=#$0001
02BEC9 BNE #$0D if(z==off) goto $02BED8 敵味方フラグをチェック
02BECB JSR $BEE9 SR: $($02BEE9+X) 敵用SRアドレスリスト
02BECE BCS #$11 if(c==on) goto $02BEE1
02BED0 PLY Pull Y
02BED1 PLX Pull X
02BED2 PLB Pull DB
02BED3 PLP Pull P Flag
02BED4 PHA Push A
02BED5 PLA Pull A
02BED6 CLC c=off
02BED7 RTL return
02BED8 LDA $2049,Y A=$2049+Y 移動中開始アドレスを取得する(モシャス前)
02BEDB TAY Y=A 開始アドレスをYレジスタにセット
02BEDC JSR $3A30 SR: $($023A30+X) 味方用SRアドレスリスト
02BEDF BCC #$EF if(c==off) goto $02BED0
02BEE1 PLY Pull Y
02BEE2 PLX Pull X
02BEE3 PLB Pull DB
02BEE4 PLP Pull P Flag
02BEE5 PHA Push A
02BEE6 PLA Pull A
02BEE7 SEC c=on
02BEE8 RTL return

実際に$023A30でコールされているSRを1つ見てみます。

  • SR $02C030 戦闘中PC情報取得変更SR_SR_0000
02C030 JSL $C43115 SR: $043115 引数:1#$06 引数:2#$FD 引数:3#$FF 現在HP取得
02C037 RTS

アドレスベースで情報を得るので、第1引数は#$06、第2引数は$02BEDBでYレジスタにセットしているので#$FDになり、戻り値をAレジスタにセットさせるので第3引数は#$FFになります。この並びのSRをみると第1引数はどれも#$06になっています。当たり前といえば当たり前の話ですが、今の今までこの辺をぼんやりとしか理解しないでよく派手にバグらせずに済んだと思うと恥ずかしい限りですが、よくよく考えて見ればどれも既存のプログラムの延長線上だったので「ようわからんけど他と合わせておけばおk」程度の認識で何とかなっていたのでしょう。

*1:プレイヤーキャラクター