4.3. テキスト解析

4.3.1. 戦闘モードテキスト解析
4.3.1.1. 解析手法
4.3.1.2. データ
4.3.2. 移動モードテキスト解析
4.3.2.1. 素人の解析手法
4.3.2.2. 達人の解析手法
4.3.2.3. データ

本節ではテキスト出力について見ていく。 移動モードと戦闘モードとでテキスト処理系統を分離してあるのは前作同様だが、 前者が圧縮符号の復号処理を有するのに対し、後者は生の文字コードを取り扱うように簡略化された。 解読が容易な戦闘モードから述べる。

4.3.1. 戦闘モードテキスト解析

既に先人 [URL1] の手によって戦闘モードにおける全テキストデータの格納アドレスが判明している。 ROM イメージさえあれば、ドラクエビューワで戦闘用メッセージの完全なリストを閲覧することができる。 本節では「どのようにすれば戦闘メッセージのデータ格納アドレスをサーチできるか」を述べる。

4.3.1.1. 解析手法

以下のことを(都合が良いのだが)仮定し、Diff サーチを提供するエミュレータを使用して探索作業を行う。

  • メッセージ ID 自体のサイズは 2 バイト

「○○○○が あらわれた!」 「○○○○の こうげき!」 「○○○○に ○○の ダメージ!」 「○○○○を たおした!」 等、表示メッセージが変化するたびに執拗に Diff サーチをすることにより、 それらしい値変化をしているアドレスを細かく見ていく。 すると次のようなアドレスの集合に収束していくことが観察できるはずだ:

  • $7E3056

  • $7E5966

  • $7E5998

  • $7E59A8

  • $7E59B8

  • $7E59BA

  • $7E59BC

それからは戦闘突入直後に、これらのアドレスひとつひとつに Break Point を置き、 それらがメッセージの表示と関係が本当にあるかどうかを調べる。ここからは運任せである。

$7E3056 は頻繁に Read されるので、メッセージの更新とは無関係とみなし、これを候補から除外する。 次に $7E5966 だが、これも同様な感じがするのでやはり除外する。 その次の $7E5998 でそれらしい挙動を見せるようになる。 GSD のアウトプットウィンドウの表示はこうなる:

$C0/2A36 8D 98 59    STA $5998  [$7E:5998]   A:000B X:0042 Y:0000 D:1E1F DB:7E S:083E [...]
$C0/27B8 AD 98 59    LDA $5998  [$7E:5998]   A:0001 X:0004 Y:0000 D:1E1F DB:7E S:0837 [...]
$C0/27C1 AD 98 59    LDA $5998  [$7E:5998]   A:0003 X:0004 Y:0000 D:1E1F DB:7E S:0837 [...]

ここで、あらかじめ用意しておいた逆アセンブリコードリストと照合する。 なお、バンクごとに逆アセンブリコードを用意しておくのは解析人の鉄則だ。 すると $C0/27B8$C0/27C1 の命令を一つのサブルーチンが含んでいる。 これが戦闘用メッセージのデータを取得するものであると推論したい。 そのサブルーチンの逆アセンブリコードは次のものだ:

C0/27B0:    08          PHP
C0/27B1:    C230        REP #$30
C0/27B3:    F47E7E      PEA $7E7E
C0/27B6:    AB          PLB
C0/27B7:    AB          PLB
C0/27B8:    AD9859      LDA $5998
C0/27BB:    290700      AND #$0007
C0/27BE:    8D1E5A      STA $5A1E
C0/27C1:    AD9859      LDA $5998
C0/27C4:    4A          LSR A
C0/27C5:    4A          LSR A
C0/27C6:    4A          LSR A
C0/27C7:    48          PHA
C0/27C8:    0A          ASL A
C0/27C9:    6301        ADC $01,S
C0/27CB:    AA          TAX
C0/27CC:    68          PLA
                                            ; x == 戦闘メッセージ ID
C0/27CD:    BFD15AC1    LDA $C15AD1,X       ; 戦闘用テキスト 開始アドレス群
C0/27D1:    18          CLC
C0/27D2:    69BDDE      ADC #$DEBD
C0/27D5:    85A0        STA $A0             $A0 = アドレス + DEBDh;
C0/27D7:    BFD35AC1    LDA $C15AD3,X       ; 戦闘用テキスト 開始アドレス群
C0/27DB:    29FF00      AND #$00FF
C0/27DE:    69F600      ADC #$00F6
C0/27E1:    85A2        STA $A2             $A2 = (バンク & 0x00FF) + 00F6h;
C0/27E3:    64A4        STZ $A4             $A4 = 0000h;
                                            for(;;){
C0/27E5:    AD1E5A      LDA $5A1E
C0/27E8:    F013        BEQ $27FD               if($5A1E)  return;
                                                do{
C0/27EA:    22FF27C0    JSR $C027FF                 ; $A0 にストアされているアドレス値を増加させる
                                                    ; a = 文字コードをセット
C0/27EE:    C9AC00      CMP #$00AC
C0/27F1:    F005        BEQ $27F8                   if(a == 00ACh) break;
C0/27F3:    C9AE00      CMP #$00AE
C0/27F6:    D0F2        BNE $27EA               }while(a != 00AEh);
C0/27F8:    CE1E5A      DEC $5A1E               --$5A1E;
C0/27FB:    80E8        BRA $27E5           }
C0/27FD:    28          PLP
C0/27FE:    6B          RTL

このルーチンの LDA 命令をチェックしておくと、 $C15AD1 がいかにも戦闘用メッセージデータのアドレスの先頭であることが特定できる。 ソニタウン [URL2] の結果と一致して、まずは一安心である。 あとはこのアドレスから 1 バイトずつ SFC 版ドラクエ 6 の小フォント文字コードとみなして、 力にまかせてデコードしていけばよい。 こちらは dq_analyzer [URL1] のデコーダにある配列を流用すればよい。 最初から dq6decoder -s する場合、戦闘メッセージは出力ファイルの 655599 行目の 39 半角文字目に現れる。

4.3.1.2. データ

ここにテキストベースの戦闘メッセージリストを公開する。 dq_analyzer [URL1] のデコーダが出力するものと基本的に同等のものである。 ただし「印字不可能文字」は文字コードの直接出力に修正した。

4.3.2. 移動モードテキスト解析

この文書では人々・動物・魔物の台詞やナレーション等の 「大きいフォントを用いて表示するテキスト」全般の解析手段の検討および実践について述べる。

4.3.2.1. 素人の解析手法

SFC 版ドラクエ 6 で初登場した移動中の呪文コマンド「おもいだす」「わすれる」を組み合わせて、RAM Diff サーチを行う。 これにより、セリフ・メッセージの内容と ID の対応表を手作業で作ることが可能だと考える(実際可能であった)。 以下のような都合のよい仮定の下、サーチに励む:

  • 「おもいだす」用のデータ格納領域が存在する

  • そこには「おもいだす」の記憶リスト「1つめ」「2つめ」に対応するデータが直列している

  • ゲームをプレイした感じから、メッセージは ID で表現されている

  • メッセージ ID は 2 バイト 長である

長い時間とかなりの忍耐を要するが、以下のことが判明する:

  • ID と セリフ・メッセージの対応

  • セリフ・メッセージのおおまかな総数

4.3.2.2. 達人の解析手法

GSD さえあれば、特定のセリフまたはメッセージと ID の対応がわかれば十分であり、憶えておく対応は一つでいい。 本節では記者の実体験、というより達人の追体験を比較的ゆっくりとしたペースで述べる。

例えば、ダーマの広間にいる熟練度を教える婆さんのセリフのしょっぱなの ID は #$199F であることが判明しているとする。 この婆さんに話しかけるや否や、#$199F になっているメモリアドレスをサーチする。 そうすることで、プログラムが RAM 中のどのアドレスにメッセージ ID を格納するのかを特定したいのだ。 実際にサーチすると、3 つ程度 ($7E3EEC, $7E5998, $7FB0AC) しか該当しない。 この際バンクが $7F のものはうさんくさいので、候補から除外する。 ここで一旦エミュレータを GSD に切り替え、残り二つのアドレスを Read タイプの Break Point で監視する。 すると、$7E5998 のほうで有力な処理が見つかる:

$C0/2B69 AD 98 59    LDA $5998  [$7E:5998]   A:0000 X:0051 Y:FFFF D:0000 DB:7E S:083B P:envmxdIZc [...]
$C0/2B72 AD 98 59    LDA $5998  [$7E:5998]   A:0007 X:0051 Y:FFFF D:0000 DB:7E S:083B P:envmxdIzc [...]
$C6/E0E1 AD 98 59    LDA $5998  [$7E:5998]   A:0001 X:0003 Y:0018 D:0000 DB:7E S:083A P:envmxdIZC [...]

$C02B69, $C02B72 がお互いに近いところにあるので、 これらの命令は一つのサブルーチンの中にあると考えるのが自然だ。 以下、このサブルーチン全体のアセンブリコードを詳しく見ていく。

4.3.2.2.1. メッセージ ID からメッセージデータ位置を特定するルーチン

このサブルーチンは大きく分けて二つのことを行っている。 その境目となるのが $C02BB4 だ。 アセンブリコードをつぶさに整理していくと、プログラムが $C02BB4 に到達する時点で満たされる条件は、次のようになっている:

  • $7E5998 にメッセージ ID が格納 (2 バイト) されている

  • $7E5A1E にメッセージ ID の下位3ビットが格納されている

  • $A0-$A3 には、アドレスらしきデータが格納 (3 バイト + #$00); $F7175B 以上の値だ

  • $A4 には、$C02BCC - $C02BD3 にある 1 バイト の値のどれかが格納されている

  • $A5 には #$00 が格納されている

C0/2B69:    AD9859      LDA $5998           ; $7E5998 にメッセージ ID が格納されている
C0/2B6C:    290700      AND #$0007
C0/2B6F:    8D1E5A      STA $5A1E           $7E5A1E = メッセージ ID & 0007h
C0/2B72:    AD9859      LDA $5998
C0/2B75:    4A          LSR A
C0/2B76:    4A          LSR A
C0/2B77:    4A          LSR A
C0/2B78:    48          PHA 
C0/2B79:    0A          ASL A
C0/2B7A:    6301        ADC $01,S
C0/2B7C:    AA          TAX                 ; x1 = メッセージ ID の上位13bit
; x1 は C15BB5 からのオフセットを意味する
; 8 個の連続した ID が一つのアドレスを見ているのがわかる
C0/2B7D:    68          PLA 
C0/2B7E:    BFB55BC1    LDA $C15BB5,X
C0/2B82:    85A0        STA $A0             $A0 = $(C15BB5 + x1);
; $A0-$A1: something data
C0/2B84:    290700      AND #$0007
C0/2B87:    DA          PHX
; x2: something data from C15BB5,X の下位 3bit
C0/2B88:    AA          TAX                 x2 = $(C15BB5 + x1) & 0007h
C0/2B89:    BFCC2BC0    LDA $C02BCC,X
C0/2B8D:    29FF00      AND #$00FF          ; マスクビット取得
C0/2B90:    85A4        STA $A4             $A4 = $(C02BCC + x2) & 00FFh
; $A4: mask from C02BCC,X
; $A5: 00h
C0/2B92:    FA          PLX
C0/2B93:    BFB75BC1    LDA $C15BB7,X
C0/2B97:    29FF00      AND #$00FF
C0/2B9A:    4A          LSR A               ; LSR と ROR が入り乱れているわけだが
C0/2B9B:    66A0        ROR $A0             ; carry bit を介して $C15BB7,X の
C0/2B9D:    4A          LSR A               ; 下位 3 ビットが $A1 の上位 3 ビットになる
C0/2B9E:    66A0        ROR $A0             ;
C0/2BA0:    4A          LSR A               ;
C0/2BA1:    66A0        ROR $A0
C0/2BA3:    85A2        STA $A2             $A0 = cast<3byte>($(C15BB5 + x1)) >> 3;
C0/2BA5:    A5A0        LDA $A0
C0/2BA7:    18          CLC 
C0/2BA8:    695B17      ADC #$175B
C0/2BAB:    85A0        STA $A0
C0/2BAD:    A5A2        LDA $A2
C0/2BAF:    69F700      ADC #$00F7
C0/2BB2:    85A2        STA $A2             $A0 += 00F7175Bh;
; $A0 に何らかのアドレス値がセットされた

ここで $C15BB5 から格納されているデータを ROM イメージファイルからダンプしてみる。 この時点ではメッセージの個数がわからないため、データ領域の終端位置もわからない。 しかし、いざダンプリストを見ると、データが単調増加しているではないか。 これに注目すれば自ずと終端位置がわかる。すなわち、 データ一個につき、メッセージ 8 個が対応しているので、 最後のメッセージ ID の候補もあたりがつけられる。

C1/5BB5:    070000  ; ID 0000h:0008h に対応するデータ
C1/5BB8:    A60200  ; ID 0008h:0010h
C1/5BBB:    D90600  ; ID 0010h:0018h
; 3 byte の数値が昇順に配列されている……
C1/65E1:    986F23  ; ID 1B20h:1B28h
C1/65E4:    817823  ; ID 1B28h:1B30h
C1/65E7:    000060  ; ここからは明らかに異質なデータなので除外
; ...

このサブルーチンの後半で、別のサブルーチンを複数回呼び出している。 これにより、$7E5A1E の意味が「何らかの処理の反復回数」と判明する。 このループ+サブルーチン $C02BD4 がメッセージデータの取得部の核心だ。

C0/2BB4:    AD1E5A      LDA $5A1E           while($7E5A1E){ ; == メッセージ ID & 0007h;
C0/2BB7:    F012        BEQ $2BCB               do{
C0/2BB9:    20D42B      JSR $2BD4                   ; サブルーチン呼び出し
C0/2BBC:    C9AC00      CMP #$00AC                  if(a == 00ACh) ; delimeter の役を果たす
C0/2BBF:    F005        BEQ $2BC6                       break;
C0/2BC1:    C9AE00      CMP #$00AE
C0/2BC4:    D0F3        BNE $2BB9               }while(a != 0x00AE);
C0/2BC6:    CE1E5A      DEC $5A1E               --$7E5A1E;
C0/2BC9:    80E9        BRA $2BB4           }
C0/2BCB:    60          RTS                 return;
4.3.2.2.2. 文字コード取得ルーチン

サブルーチン $C02BD4 も二つのパートにわかれているが、 今興味があるのは後半部分 $C02BFA から $C02C26 までだ。

前作における対応サブルーチンについての考察 3.6.2.2 ハフマン符号を復号する を参照して欲しいが、ここで文字コードを逐次ハフマン木から取得している。 本サブルーチンの目的とは、$A0 が示すアドレスに格納されている値から文字コード (2 バイト) を復号し、 アキュームレータに格納する。さらに、$A0 の示すアドレスも必要に応じてインクリメントすることだ。

$A0 に格納されているアドレスに格納されているデータは、 「文字コードの配列を圧縮したもの」だとイメージすればよい。 圧縮してある故、各データの格納位置がビット単位で指定されていなければならず、 その「ベース」アドレスが $A0-$A3 に、 オフセット・ビットを示す値が $A4 にストアされているのだと、ここで理解できる。 前項で触れたが、$A0 の初期値はメッセージ ID の下位 3 ビットから決まる、 配列 $C02BBC のどれかとなる。

C0/2BCC:    01   ; == 1 << 0
C0/2BCD:    02   ; == 1 << 1
C0/2BCE:    04   ; == 1 << 2
C0/2BCF:    08   ; == 1 << 3
C0/2BD0:    10   ; == 1 << 4
C0/2BD1:    20   ; == 1 << 5
C0/2BD2:    40   ; == 1 << 6
C0/2BD3:    80   ; == 1 << 7
4.3.2.2.3. 照合

前項で見たアキュームレータの値が、本当に文字コードを表すものかを確認すべく、 アセンブリコードを見るのではなく、デバッガの力を借りて検証する。 上記サブルーチンの呼び出し元に遡って、Exec ブレークポイントを一個セットする。 ここでセリフと文字とコードの対応がわかりやすいキャラに話しかけ、 文字一個一個をウィンドウにアウトプットするたびに、 ブレークポイントに到達すれば OK だ。

記者は、イヌに話しかけた。 *「わんっ わんっ わんっ! という、お誂え向けのセリフを持つイヌがモンストルにいるのだ。 以下、そのトレースである:

$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0511     不明
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0577     不明
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:00AB     不明
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0523     「わ」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:04F7     「ん」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0620     「っ」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0200     スペース
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0523     「わ」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:04F7     「ん」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0620     「っ」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0200     スペース
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0523     「わ」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:04F7     「ん」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0620     「っ」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:0551     「!」
$C0/2C92 8D B2 59    STA $59B2  [$7E:59B2]   A:00AC     00ACh は終端を意味する?

「わ」「ん」「っ」を見ると、 A レジスタの値と表示される文字が一対一対応している。 これにより、アキュームレータの値はコードと断定してよいことがわかる。 念を入れて、他の人物に話しかけてコードと表示される文字の対応をもう少し調べてもよい。

ここで、ドラクエビューア [URL2] が出力する SFC_DQ6gra.bmp のフォントテーブルと、「わ」「ん」等の位置=インデックスを照合してみる。

表 4.15 わんっ!

文字 A BMP
#$0523 #$0322
#$04F7 #$02F6
#$0620 #$041F
#$0551 #$0350

どの文字も差が定数 #$0201 になる。 復号ルーチンはこのフォントテーブルへのインデックス値を直接取得していると考えてよい。 定数分の差は、何か別のデータがフォントの直前にあり、 そこから生じたのものだと、とりあえず予想しておく。

4.3.2.3. データ

今まで見てきたサブルーチンおよびフォントテーブルを Python スクリプトで再現し、 没メッセージを含むすべての移動モード時のテキストを ROM イメージファイルから抽出することに成功した。 ソースコードは GitHub の記者のアカウント内のどこかのリポジトリーで公開中のはずだ。 また、抽出したテキストデータは UTF-8 テキストファイルとして 付録 B データ に含めてある。

テキスト抽出スクリプトを書くに当たっての最大の難関は、符号データから UTF-8 文字に変換する仕組みの実装だ。 ここは偉大な先人の成果をありがたく再利用させていただくことで果たせる。 具体的に言うと dq_analyzer [URL1] に同梱されている dq6decode.c の「メッセージ文字列用」配列を自作するソースファイル内に、 自作プログラムの言語仕様に合わせてコピー&ペーストする。

そうすると残りは上で述べたサブルーチン、 つまりメッセージ ID からデータ格納位置を取得するコードとハフマン復号をするコードを 65816 コードから C/C++ 言語なり Python なりに書き直すだけで済む。