TOP > プログラミング関係解説&調査 > セーブデータのセーブ・ロード

セーブデータのセーブ・ロード

当ページの内容は、ある程度のプログラミングの知識を必要とする。
まず、プログラミング関係解説&調査をひととおり読んでいただきたい。

本作のセーブデータのセーブ・ロード関係のサブルーチンを紹介する。
まず、「ライブ・ア・ライブ」だけでなく、スーパーファミコンのゲーム全般のバッテリーバックアップ式のセーブについて、簡単に説明する。

スーパーファミコンのゲームでは、カセットの中に別途、ボタン電池で電力を供給するメモリを入れてセーブデータを保存することができる。
いわゆるバッテリーバックアップであり、ボタン電池が切れると、スーパーファミコンに接続していない限り、セーブデータが保存できなくなる。
このセーブデータ保存用メモリをSRAM(Static Random Access Memory)という。

SRAMはスーパーファミコンを起動した時、規定のRAM(Random-Access Memory)領域に展開される。
スーパーファミコンのゲームソフトにおいて、メモリ領域の規格はHiROMとLoROMの2種類あり、データの割り当て方が異なる。
「ライブ・ア・ライブ」はHiROMという規格を採用している。
HiROMにおけるSRAMは、バンク$30$3Fのアドレス$6000$7FFFに割り当てられている。

「ライブ・ア・ライブ」では、セーブデータ領域にバンク$30のみ使用している。
セーブデータは4個あるが、データ領域は以下のように割り振られている。

アドレスセーブデータ
$30:6000$30:67F7セーブデータ1
$30:67F8$30:6FEFセーブデータ2
$30:6FF0$30:77E7セーブデータ3
$30:77E8$30:7FEFセーブデータ4

1つのセーブデータに$7F8バイトの領域が割り振られていることがわかる。
(これらとは別に、各シナリオをクリアすると対応した楽曲がサウンドルームで聞けるようになり、それらは4つのセーブデータで共有だが、ここでは触れない)

また、サブルーチンを見るとわかるのだが、1つのセーブデータにセーブされるのは、$00:0A00$00:11F7の領域にあたる。
$00:0A00はプレイ中のシナリオIDが記録されており、以降、キャラのいる座標、マップタイマーイベント、持ち物欄、シナリオ別キャラデータなどが続く。
どれもセーブしておく必要がある重要なデータばかりであるが、問題は、$00:11F7より後にも重要なデータが入っているということである。

アドレス内容
$00:1200$00:12FFシナリオ進行フラグ
$00:1300$00:1301心山拳伝承者のデータ
$00:1303初期値$00、西部編最後のマッドドッグとの戦闘で逃げると$01
$00:1304初期値$00、幕末編最後の選択肢で「面白い」を選ぶと$01

以上のデータはセーブデータに記録しておかなければならないが、$00:0A00$00:11F7の中にはない。
どのようにして、データを格納しているのか。

データの圧縮

$00:1200~からのデータだが、1バイトあたり、$00$01しか入らないようにプログラムが組まれている。
シナリオ進行フラグ一覧を見ていただければわかるが、例えば幕末編なら、パーティにとらわれの男がいたら$00:1200$01が入り、パーティに加入する前なら$00が入っている、というようにONとOFFの切り替えに使っている。

1バイトあたり8ビットのデータを格納できるのだから、01かを記録するだけなら、1バイト中にフラグを8個分格納可能である。
実際、状態異常など、石化・酔い・眠り・マヒ・毒・腕かため・足かため・首かための状況を1バイトにまとめて戦闘中に読み書きしているサブルーチンもある。
容量の節約にはなるが、その分、1バイトのデータからどの状態異常になっているかを取り出したり書き込むためのサブルーチンの処理量は増える。
ということで、容量と処理時間のバランスを見た上で、$00:1200~からのデータは1バイトあたり$00$01のみ記録するようにしたのだろう、と思われる(おそらく)。

とはいえセーブデータの容量は限りがあるので、$00:1200~については、1バイト(8ビット)に、8バイト分の$00または$01のデータを詰め込んで圧縮し保存する、という仕組みを使っている。
データ圧縮のサブルーチンが$CE/CD7A$CE/CE61になる。

$CE/CD7A LDX #$1200              ;Xに$1200をロード
$CE/CD7D LDY #$11D0              ;Yに$11D0をロード
$CE/CD80 LDA #$20                ;Aに$20をロード
$CE/CD82 STA $24    [$00:0024]   ;A($20)を[$00:0024]に書き込み
$CE/CD84 STZ $20    [$00:0020]   ;[$00:0020]に$00を書き込み
$CE/CD86 LDA #$08                ;Aに$08をロード
$CE/CD88 STA $22    [$00:0022]   ;A($08)を[$00:0022]に書き込み
;
;[$00:1200]~[$00:12FF]を8ビット区切りで[$00:11D0]~[$00:11EF]に書き込みループ
;[$00:1200]~を8ビット分[$00:0020]に書き込むループ
$CE/CD8A LDA $00,x               ;Aに[$00:1200~]をロード
$CE/CD8C LSR A                   ;Aを論理右シフト (/2)
$CE/CD8D ROR $20    [$00:0020]   ;[$00:0020]をキャリーフラグを含めた9bitで右ローテート
$CE/CD8F INX                     ;Xをインクリメント +1
$CE/CD90 DEC $22    [$00:0022]   ;[$00:0022]をデクリメント -1
$CE/CD92 BNE $F6    [$CD8A]      ;ゼロフラグが立っていないとき[$CD8A]分岐
;[$00:1200]~[$00:12FF]を8ビット分[$00:0020]に書き込むループここまで
;
$CE/CD94 LDA $20    [$00:0020]   ;Aに[$00:0020]をロード
$CE/CD96 STA $0000,y             ;Aを[$00:11EF~]に書き込み
$CE/CD99 INY                     ;Yをインクリメント +1
$CE/CD9A DEC $24    [$00:0024]   ;[$00:0024]をデクリメント -1
$CE/CD9C BNE $E6    [$CD84]      ;ゼロフラグが立っていないとき[$CD84]分岐
;[$00:1200]~[$00:12FF]を8ビット区切りで[$00:11D0]~[$00:11EF]に書き込みループここまで
;
$CE/CD9E LDX #$1300              ;Xに$1300をロード
$CE/CDA1 LDY #$10FE              ;Yに$10FEをロード
$CE/CDA4 LDA #$02                ;Aに$02をロード
$CE/CDA6 STA $24    [$00:0024]   ;A($02)を[$00:0024]に書き込み
$CE/CDA8 STZ $20    [$00:0020]   ;[$00:0020]に$00を書き込み
$CE/CDAA LDA #$08                ;Aに$08をロード
$CE/CDAC STA $22    [$00:0022]   ;A($08)を[$00:0022]に書き込み
;
;[$00:1300]~[$00:130F]を8ビット区切りで[$00:10FE]~[$00:10FF]に書き込みループ
;[$00:1300]~を8ビット分[$00:0020]に書き込むループ
$CE/CDAE LDA $00,x               ;Aに[$00:1300~]をロード
$CE/CDB0 LSR A                   ;Aを論理右シフト (/2)
$CE/CDB1 ROR $20    [$00:0020]   ;A[$00:0020]をキャリーフラグを含めた9bitで右ローテート
$CE/CDB3 INX                     ;Xをインクリメント +1
$CE/CDB4 DEC $22    [$00:0022]   ;[$00:0022]をデクリメント -1
$CE/CDB6 BNE $F6    [$CDAE]      ;ゼロフラグが立っていないとき[$CDAE]分岐
;[$00:1300]~を8ビット分[$00:0020]に書き込むループここまで

$CE/CDB8 LDA $20    [$00:0020]   ;Aに[$00:0020]をロード
$CE/CDBA STA $0000,y             ;Aを[$00:10FF~]に書き込み
$CE/CDBD INY                     ;Yをインクリメント +1
$CE/CDBE DEC $24    [$00:0024]   ;[$00:0024]をデクリメント -1
$CE/CDC0 BNE $E6    [$CDA8]      ;ゼロフラグが立っていないとき[$CDA8]分岐
;[$00:1300]~[$00:130F]を8ビット区切りで[$00:10FE]~[$00:10FF]に書き込みループここまで

まずここまでで、ループ処理により、[$00:1200][$00:1207]8バイトのデータを1ビットずつに収めた値を[$00:11D0]へコピー、……を繰り返し、

  • [$00:1200][$00:12FF]を8ビット区切りで[$00:11D0][$00:11EF]に書き込む
  • [$00:1300][$00:130F]を8ビット区切りで[$00:10FE][$00:10FF]に書き込む

というデータ圧縮を行う。
これで、$00:1200$00:130F272バイト)が、セーブデータに記録される$00:10FE$00:10FF$00:11D0$00:11EF(34バイト)の領域に収まったことになる。

$CE/CDC2~はシナリオによる分岐があり、どれが何なのか、詳細は未確認なので省略。
$CE/CDC2$CE/CE30あたりが分岐で、$CE/CE32$CE/CE61で転送処理を行う。
例えば最終編だと、

コピー元コピー先
$00:132E$00:1336$00:0FC0$00:0FC7
$00:133F$00:1347$00:0FC8$00:0FCF
$00:1350$00:1357$00:0FD0$00:0FD7

というようにコピーが行われる。
シナリオ別キャラデータが$00:0FBFまで入っているので、その直後の$00:0FC0~に$00:13??の重要なデータをコピーしているようだ。

セーブ処理

実際のセーブのサブルーチンは$C2/3D4D$C2/3D98になる。
その前に、4つあるセーブスロットの中でどれにセーブするかの判定が必要である。
セーブ先のSRAMの領域のどこからデータを書き込むかを決めなければならないからである。
ここでは処理の紹介は省くが、セーブデータ一覧画面で、$C2/069E~あたりの処理により、カーソルで示した位置のセーブデータの番号($00$03)が$00:0544に入る仕組みがある。セーブスロットの上から順に$00, $01, $02, $03である。

$C2/15C9 LDA $44    [$00:0544]   ;Aに[$00:0544]をロード
$C2/15CB STA $65    [$00:0565]   ;Aを[$00:0565]に書き込み
$C2/15CD AND #$0003              ;Aと$0003で論理積
$C2/15D0 STA $2B00  [$7E:2B00]   ;Aを[$7E:2B00]に書き込み
$C2/15D3 JSR $408B  [$C2:408B]   ;[$C2:408B]へジャンプ

上の処理で、[$00:0544]に入ったセーブデータの番号($00$03)が[$00:0565][$7E:2B00]に入る。
ここから少し飛んで$C2/3D4D以降の処理が以下から。

$C2/3D4D LDA $0AB4  [$7E:0AB4]   ;Aに[$7E:0AB5][$7E:0AB4](セーブ回数2バイト)をロード
$C2/3D50 AND #$7FFF              ;Aと$7FFFで論理積
$C2/3D53 INC A                   ;Aをインクリメント +1
$C2/3D54 CMP #$03E8              ;Aと$03E8(10進数1000)を減算比較(ステータスレジスタ変更のみ)
$C2/3D57 BMI $03    [$3D5C]      ;ネガティブフラグが立っているとき[$3D5C]分岐
;ネガティブフラグOFF(セーブ回数1000回)
$C2/3D59 LDA #$0000              ;Aに$0000をロード
;ネガティブフラグON
$C2/3D5C BIT $0AB4  [$7E:0AB4]   ;Aと[$7E:0AB5][$7E:0AB4]で論理積(ステータスフラグ変更のみ)
$C2/3D5F BPL $03    [$3D64]      ;ネガティブフラグが立っていないとき[$3D64]分岐
;ネガティブフラグOFF
$C2/3D61 ORA #$8000              ;Aと$8000で論理和
;ネガティブフラグOFF
$C2/3D64 STA $0AB4  [$7E:0AB4]   ;Aを[$7E:0AB5][$7E:0AB4]に書き込み

上はセーブ回数の処理である。
本作はセーブデータにセーブ回数を記録しており、セーブ回数は[$7E:0AB5][$7E:0AB4]の2バイトで記録している。
セーブ回数は999回($03E7)まで記録されるが、1000回セーブするとリセットされて0回に戻る仕組みなので、セーブ回数を+1した後、$03E8(10進数1000)と減算比較し、ネガティブフラグ判定で分岐させて、$03E8を越えていたら[$7E:0AB5][$7E:0AB4]$0000を書き込む、という仕組みになる。

$C2/3D67 LDA #$E41B              ;Aに$E41Bをロード
$C2/3D6A STA $307FF0[$30:7FF0]   ;A($E41B)を[$30:7FF1][$30:7FF0]に書き込み
$C2/3D6E LDA $65    [$00:0565]   ;Aに[$00:0566][$00:0565](セーブデータの番号$00~$03)をロード
$C2/3D70 AND #$0003              ;A(セーブデータ番号)と$0003で論理積
$C2/3D73 STA $307FF2[$30:7FF2]   ;A(セーブデータ番号)を[$30:7FF3][$30:7FF2]に書き込み
$C2/3D77 ASL A                   ;Aを算術左シフト *2 (セーブデータ番号*2)
$C2/3D78 TAX                     ;A(セーブデータ番号*2)の値をXレジスタに転送
$C2/3D79 LDA #$E41B              ;Aに$E41Bをロード
$C2/3D7C STA $307FE0,x           ;A($E41B)を[$30:7FE1+x][$30:7FE0+x]に書き込み
$C2/3D80 LDA $C278C2,x           ;Aに[$C2:78C3+x][$C2:78C2+x]をロード
$C2/3D84 STA $9F    [$00:059F]   ;Aを[$00:05A0][$00:059F]に書き込み
$C2/3D86 TAY                     ;Aの値をYレジスタに転送
$C2/3D87 LDA #$0030              ;Aに$0030をロード
$C2/3D8A STA $A1    [$00:05A1]   ;Aを[$00:05A1]に書き込み
$C2/3D8C PHX                     ;Xをスタックにプッシュ
$C2/3D8D LDX #$0A00              ;Xに$0A00をロード
$C2/3D90 LDA #$07F7              ;Aに$07F7をロード
$C2/3D93 PHB                     ;DBレジスタをスタックにプッシュ
$C2/3D94 MVN 00 30               ;データ転送MVN
;
;セーブデータ番号0なら
;A:転送したいデータサイズ-1 $07F7
;X:転送元の最初の16bitアドレス $00:0A00-$00:11F7
;Y:転送先の最初の16bitアドレス $30:6000-$30:67F8
;オペランド:転送元と転送先バンク 00,30
;
$C2/3D94 MVN 00 30               ;データ転送ここまで
$C2/3D97 PLB                     ;DBレジスタに値をプル
$C2/3D98 JSR $40BA  [$C2:40BA]   ;[$C2:40BA]へジャンプ

$C2/3D94で、データ転送MVNを使い、セーブデータのアドレスに書き込みを行う。
$C2/3D67$C2/3D93までがデータ転送MVNを行うための準備になる。
セーブデータ番号により変わってくるのは、$C2/3D6Eで呼び出すセーブデータ番号以降である。
処理をおっていくと、$C2/3D78で、セーブデータ番号*2の値がXレジスタに入る。
更に、$C2/3D80Aに呼び出す$C278C2,x(2バイトで呼び出すので、[$C2:78C3+x][$C2:78C2+x])が$C2/3D86Yレジスタに転送され、この時のYの値がそのまま、データ転送MVNにおける「Y:転送先の最初の16bitアドレス」になる。
では、[$C2:78C3+x][$C2:78C2+x]はどんな値なのか。
実際に値を確認してみると以下のようになっている。

セーブデータ
番号
X[$C2:78C3+x][$C2:78C2+x]値(=Y
0$0000[$C2:78C3][$C2:78C2]$6000
1$0002[$C2:78C5][$C2:78C4]$67F8
2$0004[$C2:78C7][$C2:78C6]$6FF0
3$0006[$C2:78C9][$C2:78C8]$77E8

$C2/3D94のデータ転送MVNの処理で異なるのは、上のYの値だけになる。
整理してみると、

セーブデータ
番号
転送元転送先
0$00:0A00$00:11F7$30:6000$30:67F7
1$00:0A00$00:11F7$30:67F8$30:6FEF
2$00:0A00$00:11F7$30:6FF0$30:77E7
3$00:0A00$00:11F7$30:77E8$30:7FEF

セーブデータ先スロットにより転送先を変えて、データをコピーしていることがわかる。
また、上の処理などの間にゲームを強制的に終了させたりすると、セーブデータが破損することになる。

ロード処理&データの解凍

セーブデータのロードは、基本的にセーブの逆の手順の実行と考えて良い。
ただしセーブ時と異なり、セーブ回数の処理は必要ない。

$C2/18CB LDA $44    [$00:0544]   ;Aに[$00:0544]をロード
$C2/18CD STA $65    [$00:0565]   ;Aを[$00:0565]に書き込み
$C2/18CF LDA #$0004              ;Aに$0004をロード
$C2/18D2 JMP $0677  [$C2:0677]   ;[$C2:0677]へジャンプ(レジスタをスタックに積まない)

上の処理で、[$00:0544]に入ったセーブデータの番号($00$03)が[$00:0565]に入る。
ここから少し飛んで$C2/3CB4以降の処理が以下から。

$C2/3CB4 PHP                     ;ステータスレジスタをスタックへプッシュ
$C2/3CB5 REP #$20                ;Aを16bit幅に変更、Mフラグをクリア
$C2/3CB7 LDA $65    [$00:0565]   ;Aに[$00:0566][$00:0565](セーブデータの番号)をロード
$C2/3CB9 AND #$0003              ;Aと$0003で論理積
$C2/3CBC ASL A                   ;Aを算術左シフト *2
$C2/3CBD TAX                     ;Aの値をXレジスタに転送
$C2/3CBE LDA $C278C2,x           ;Aに[$C2:78C2,x+1][$C2:78C2,x]をロード
$C2/3CC2 TAX                     ;Aの値をXレジスタに転送
$C2/3CC3 LDY #$0A00              ;Yに$0A00をロード
$C2/3CC6 LDA #$07F7              ;Aに$07F7をロード
$C2/3CC9 PHB                     ;DBレジスタをスタックにプッシュ
$C2/3CCA MVN 30 00               ;データ転送
;
;セーブデータ番号0なら
;A:転送したいデータサイズ-1 $07F7
;X:転送元の最初の16bitアドレス $30:6000-$30:67F8
;Y:転送先の最初の16bitアドレス $00:0A00-$00:11F7
;オペランド:転送元と転送先バンク 30,00
;
$C2/3CCD PLB                     ;DBレジスタに値をプル
$C2/3CCE JSR $3D0F  [$C2:3D0F]   ;[$C2:3D0F]へジャンプ

セーブデータの番号によって、転送元のアドレスのみ変更し、データ転送MVNでセーブデータをバンク$00に書き込んでいることがわかる。

セーブデータ
番号
転送元転送先
0$30:6000$30:67F7$00:0A00$00:11F7
1$30:67F8$30:6FEF$00:0A00$00:11F7
2$30:6FF0$30:77E7$00:0A00$00:11F7
3$30:77E8$30:7FEF$00:0A00$00:11F7

この時点では、[$00:1200][$00:130F]は圧縮されて[$00:10FE][$00:10FF][$00:11D0][$00:11EF]に入っている状態なので、元に戻す処理も行う。
まず、[$00:10FE][$00:10FF]に入った8ビット区切りデータを[$00:1300][$00:130F]に1バイトずつ戻す。
サブルーチンは$C0/0308$C0/032C

$C0/0308 LDX #$1300              x;Xに$1300をロード
$C0/030B LDY #$10FE              x;Yに$10FEをロード
$C0/030E LDA #$02                x;Aに$02をロード
$C0/0310 STA $24    [$00:0024]   x;A($02)を[$00:0024]に書き込み
;
;[$00:10FE],[$00:10FF]を8ビット区切りで[$00:1300]~に書き戻すループ(ループ回数$02)
$C0/0312 LDA $0000,y             ;Aに[$00:10FE~]をロード
$C0/0315 STA $20    [$00:0020]   ;Aを[$00:0020]に書き込み
$C0/0317 LDA #$08                ;Aに$08をロード
$C0/0319 STA $22    [$00:0022]   ;A($08)を[$00:0022]に書き込み
;
;[$00:0020]をローテートしながら[$00:1300]~を1バイトずつ書き込むループ(ループ回数$08)
$C0/031B LDA #$00                ;Aに$00をロード
$C0/031D LSR $20    [$00:0020]   ;A[$00:0020]を論理右シフト (/2)
$C0/031F ROL A                   ;Aをキャリーフラグを含めた9bitで左ローテート
$C0/0320 STA $00,x               ;Aを[$00:1300~]に書き込み
$C0/0322 INX                     ;Xをインクリメント +1
$C0/0323 DEC $22    [$00:0022]   ;[$00:0022]をデクリメント -1
$C0/0325 BNE $F4    [$031B]      ;ゼロフラグが立っていないとき[$031B]分岐
;[$00:0020]をローテートしながら[$00:1300]~を1バイトずつ書き込むループここまで
;
$C0/0327 INY                     ;Yをインクリメント +1
$C0/0328 DEC $24    [$00:0024]   ;[$00:0024]をデクリメント -1
$C0/032A BNE $E6    [$0312]      ;ゼロフラグが立っていないとき[$0312]分岐
;[$00:10FE],[$00:10FF]を8ビット区切りで[$00:1300]~に書き戻すループここまで
;
$C0/032C JSR $035F  [$C0:035F]   ;[$C0:035F]へジャンプ

続いて、[$00:11D0][$00:11EF]に入った8ビット区切りデータを[$00:1200][$00:12FF]に1バイトずつ戻す。
サブルーチンは$CE/CEEF$CE/CF13

$CE/CEEF LDX #$1200              ;Xに$1200をロード
$CE/CEF2 LDY #$11D0              ;Yに$11D0をロード
$CE/CEF5 LDA #$20                ;Aに$20をロード
$CE/CEF7 STA $24    [$00:0024]   ;A($20)を[$00:0024]に書き込み
;
;[$00:11D0]~[$00:11EF]を8ビット区切りで[$00:1300]~に書き戻すループ(ループ回数$20)
$CE/CEF9 LDA $0000,y             ;Aに[$00:11D0~]をロード
$CE/CEFC STA $20    [$00:0020]   ;Aを[$00:0020]に書き込み
$CE/CEFE LDA #$08                ;Aに$08をロード
$CE/CF00 STA $22    [$00:0022]   ;A($08)を[$00:0022]に書き込み
;
;[$00:0020]をローテートしながら[$00:1200]~を1バイトずつ書き込むループ(ループ回数$08)
$CE/CF02 LDA #$00                ;Aに$00をロード
$CE/CF04 LSR $20    [$00:0020]   ;A[$00:0020]を論理右シフト (/2)
$CE/CF06 ROL A                   ;Aをキャリーフラグを含めた9bitで左ローテート
$CE/CF07 STA $00,x               ;Aを[$00:1200~]に書き込み
$CE/CF09 INX                     ;Xをインクリメント +1
$CE/CF0A DEC $22    [$00:0022]   ;[$00:0022]をデクリメント -1
$CE/CF0C BNE $F4    [$CF02]      ;ゼロフラグが立っていないとき[$CF02]分岐
;[$00:0020]をローテートしながら[$00:1200]~を1バイトずつ書き込むループここまで
;
$CE/CF0E INY                     ;Yをインクリメント +1
$CE/CF0F DEC $24    [$00:0024]   ;[$00:0024]をデクリメント -1
$CE/CF11 BNE $E6    [$CEF9]      ;ゼロフラグが立っていないとき[$CEF9]分岐
;[$00:11D0]~[$00:11EF]を8ビット区切りで[$00:1300]~に書き戻すループここまで
;
$CE/CF13 RTL                     ;サブルーチン戻り

以上が大まかな処理である。
シナリオによりまだ処理が続くが、とりあえず紹介はここまで。



このページをシェアする

上へ