Hatena::ブログ(Diary)

naruko の開発メモ

2013年09月09日

シルヴァ・サーガのセーブデータ

移動速度が速いゲームとしてこれを思い出した。これをエミュレータで起動したところ、セーブデータ欄が名前が空欄でレベル1となっていて、これを再開すると船に乗っていて変。

f:id:na6ko:20130909091746p:imagef:id:na6ko:20130909091745p:image

対処方法は異常なセーブデータを消すと、ゲームが最初から開始できる。

f:id:na6ko:20130909091747p:image

ROM の redump をしていただいたところ、CRC は database と一致して、不具合はやはり再現したので原因はエミュレータの実装不備とした。

エミュレータでは CPU address 0x6000-0x7fff は基本的に RAM としていて*1、セーブデータに使うような保存された RAM イメージがある場合は .sav ファイルの中身を展開し、RAM イメージがない場合は data を 0 で埋める。

この手のセーブデータの処理は RAM 上の checksum を算出し、不正な場合はソフトから初期化する。シルヴァ・サーガの場合はセーブデータの領域のデータが全部 0 だと checksum 算出が正常として判断し、本当は不正なセーブデータを読み込んでゲームを再開してしまう。気になる人はプログラムコードを確認して欲しい。

ソフトウェア上で RAM の扱いをする場合は下記の基本的な約束事がある。

  • 変数の初期値は不定値なので、使う前に初期化する。
  • RAM の電源投入直後の初期値は不定値なので、BIOS などの初期プログラムで 0 で埋める。

全部 0 で通過してしまうシルヴァ・サーガのプログラム実装も悪いのだが、エミュレータではターゲットシステム上の RAM はホストからはソフトウェアとしての変数領域だが、ターゲットからはハードウェアの SRAM となる。約束事はソフトウェアとして使う場合なので守らなくてよい。

エミュレータでのソフトのライブラリが気を利かせて、RAM が 0 で初期化していてくれることは実際にありえるし、いつもの癖で memset で初期化するエミュレータのコードもたくさんある。しかし、ターゲット上の RAM を厳密に再現する場合には意図的に不定値を書き込んでおく必要がある、ということになる。

RAM の電源投入直後の初期値は RAM の種類が SRAM か DRAM でも変わるし、初期値なんて不定なんだから再現のしようがないかもしれない。1つ聞いた噂によると SRAM の初期値は当然不定値だが、各 data bit の 0 と 1 の割合は 1:1 になるらしい。

ゲームの中身

移動速度が速く、メッセージも最速ででてキビキビとした動作はプログラマが気を利かせてくれたいいユーザーインタフェースである。

このゲームの致命的な欠点はシナリオが微妙、戦闘システムが雑となっていてユーザーの心に残らないのが、ドラゴンクエストシリーズと異なる点である。

考えさせられる。

プログラム

アセンブラではここら辺の処理。

d8c9: 
	jsr	$d918; initial save data pointer $29.$28
	sty	$20 ;16bit checksum $21.$20 = 0x0000
	sty	$21
	lda	#$00
	sta	$18 ;xor checksum
	jsr	$d74a
	ldx	#$00 ;carry count
d8d9: 
	clc
	lda	$20
	adc	($28),y
	sta	$20
	bcc	$d8e4
	inc	$21
d8e4: 
	lda	$18
	eor	($28),y
	sta	$18
	iny
	bne	$d8f2
	inc	$29
	inx
	bne	$d8d9
d8f2: 
	cpy	#$fd
	bne	$d8d9
	cpx	#$03
	bcc	$d8d9
d8fa:
	lda	($28),y
	cmp	$18
	bne	$d911
	iny
	lda	($28),y
	cmp	$20
	bne	$d911
	iny
	lda	($28),y
	cmp	$21
	bne	$d911
	clc
	bcc	$d912
d911: 
	sec
d912: 
	jsr	$d754
	ldy	#$fd
	rts

C で人力逆コンパイルするとこんな感じ。

/*
save data start address 0x7400,0x7800,0x7c00, length is 0x400
offset length
0x000 0x3fd game data
0x3fd 1     xor each byte data
0x3fe 2     16bit add sum data
*/
carry d8c9()
{
	uint16_t sum = 0; //$21.$20
	uint8_t xor = 0;
	uint8_t *savedata = func_d918(); //$29.28
	uint8_t x; //0x100 byte unit count
	uint8_t y = 0; //1 byte  count / pointer index
	while(1){
		do{
			sum += savedata[y];
			xor ^= savedata[y];
			y++;
			if(x >= 3 && y == 0xfd){
				goto d8fa;
			}
		}while(y != 0);
		savedata += 0x100;
	}
d8fa:
	if(savedata[y++] != xor){
		return 0;
	}
	if(savedata[y++] != (checksum & 0xff)){
		return 0;
	}
	if(savedata[y] != (checksum >> 8)){
		return 0;
	}
	return 1;
}

*1:実物では RAM がない場合もエミュレータ上では RAM がある

トラックバック - http://d.hatena.ne.jp/na6ko/20130909/p1