メモ帳

備忘録

A列車で行こう3D コンストラクション出力データの仕様について


2016年12月18日 コメント欄に追記あり

仕様について

解析途中なので間違っているところがあるかも知れません。コメント等で指摘があれば訂正します。

出力されたデータは数枚の画像ファイルから構成されています。シナリオデータはLZSSで圧縮された後、ヘッダとフッタが付け加えられた上で、これらの画像のExif情報内のメーカーノートと呼ばれる部分と、画像の中のグレースケール部分の2箇所に交互に分割されて保存されるようですーーすなわち、出力された画像から取り出したこれらのデータを以下のような順番に繋げることで、画像ファイルから(圧縮された上でヘッダとフッダを付け加えられた)シナリオデータを取り出すことができます。

  1. 1枚目のメーカーノート部分に格納されたデータ
  2. 1枚目のグレースケール部分に格納されたデータ
  3. 2枚目のメーカーノート部分に格納されたデータ
  4. 2枚目のグレースケール部分に格納されたデータ
  5. 3枚目のメーカーノート部分に格納されたデータ
  6. (以下略)

以下にそれぞれのデータを取り出す方法を書き残しておきます。

1. メーカーノート部分からデータを取り出す: ExifにおけるMakerNoteのTag IDは0x927Cとなっています。メーカーノートに格納されたデータのうち、最初の0x35バイトと最後の0x41バイトは何のデータなのかよく分かりませんが、この部分は分割されたシナリオデータを格納する以外の、何か別のことに使われているようです。現状では必要のないデータなので、カットします。
C#でメーカーノート部分からデータを取り出すためのサンプルコードを以下に掲載しておきます。

static byte[] GetDataFromMakerNote(BitmapFrame bitmapFrame)
{
	const int headerSize = 0x35;
	const int footerSize = 0x41;
	const int tagID = 0x927C;		// MakerNote Tag ID

	var metadata = bitmapFrame.Metadata as BitmapMetadata;
	var query = string.Format("/app1/ifd/exif/{{uint={0}}}", tagID);
	var obj = metadata.GetQuery(query) as BitmapMetadataBlob;
	var value = obj.GetBlobValue();

	int length = value.Length - headerSize - footerSize;
	var data = new byte[length];
	for (int i = 0, j = headerSize; i < length; )
		data[i++] = value[j++];

	return data;
}

2. グレースケール部分からデータを取り出す: ひとつひとつの画像ファイルはサイズが704x512で、そのうちの上部704x64はA列車で行こう3Dのロゴやシナリオのタイトルが書かれいるヘッダー部分です。下部704x448はシナリオデータの一部を以下のような手順でグレースケール画像データに変換されたものとなっています。

  1. 元となるバイト列から、大きさ352x224、1ピクセルあたり2ビットのグレースケール画像を生成する
  2. その画像を2倍に拡大し、大きさ704x448の画像を生成する。

こうして生成された大きさ704x448の画像にヘッダを付け加えることで大きさ704x512の画像を出力しているようです。この手順を逆から辿ることで、画像データからシナリオデータの一部を取り出すことが出来ます。
C#でグレースケール部分からデータを取り出すためのサンプルコードを以下に掲載しておきます。FormatConvertedBitmapで直接Gray2に変換すると上手くいかないため、一旦Gray8に変換したあと、自力でGray2へ変換します。

byte[] GetDataFromGray(BitmapSource bitmapSource)
{
	// crop
	int cropX = 0;
	int cropY = 64;
	int cropWidth = bitmapSource.PixelWidth;			// 352px
	int cropHeight = bitmapSource.PixelHeight - cropY;	// 224px
	var sourceRectangle = new Int32Rect(cropX, cropY, cropWidth, cropHeight);
	var croppedBitmap = new CroppedBitmap(bitmapSource, sourceRectangle);

	// resize
	double scale = 1d / 2d;
	var transform = new ScaleTransform(scale, scale);
	var scaledBitmap = new TransformedBitmap(croppedBitmap, transform);

	// restore dpi
	int width = scaledBitmap.PixelWidth;
	int height = scaledBitmap.PixelHeight;
	double dpiX = scaledBitmap.DpiX;
	double dpiY = scaledBitmap.DpiY;

	// convert to 8-bit grayscale bitmap
	var format8 = System.Windows.Media.PixelFormats.Gray8;
	int stride8 = width * format8.BitsPerPixel / 8;
	var gray8Bitmap = new FormatConvertedBitmap(scaledBitmap, format8, null, 0);

	// convert to 8-bit grayscale byte array
	byte[] gray8Data = new byte[stride8 * height];
	gray8Bitmap.CopyPixels(gray8Data, stride8, 0);

	// convert to 2-bit grayscale byte array
	var format2 = System.Windows.Media.PixelFormats.Gray2;
	int stride2 = width * format2.BitsPerPixel / 8;
	byte[] gray2Data = new byte[stride2 * height];

	byte byteData = 0;
	int mask = 0x3;
	for (int i = 0, j = 0; i < gray8Data.Length; i++)
	{
		byte gray8 = gray8Data[i];					// 0x00 ... 0xFF
		byte gray2 = (byte)(((gray8) + 42) / 85);	// 0x00 ... 0x03
		byteData <<= 2;
		byteData += gray2;

		if ((i & mask) == mask)
			gray2Data[j++] = byteData;
	}

	return gray2Data;
}

3. データを繋げる: あとは普通にデータを繋げます。

var dirname = "a-train/";
var files = Directory.GetFiles(dirname, "*.JPG");

var memory = new MemoryStream();
foreach (var inputPath in files)
{
	var uri = new Uri(inputPath, UriKind.Relative);
	var frame = BitmapFrame.Create(uri);

	var dataMakerNote = GetDataFromMakerNote(frame);
	memory.Write(dataMakerNote, 0, dataMakerNote.Length);

	var dataScan = GetDataFromGray(frame);
	memory.Write(dataScan, 0, dataScan.Length);
}

{
	var extension = ".dat";
	var filename = "scenario";
	var outputPath = dirname + filename + extension;
	var data = memory.ToArray();
	using (var fs = new FileStream(outputPath, FileMode.Create))
		fs.Write(data, 0, data.Length);
}

4. 解凍する: ここがまだよく分かっていない部分です。上の手順で取り出したデータにはヘッダとフッダらしき部分があるのですが、それらのサイズがよく分かっていません。

バイナリエディタで開くと分かると思うのですが、まずシナリオのタイトルや説明文が圧縮されずにUTF-8形式で格納されたヘッダ部分から始まり、その後、LZSSで圧縮されたシナリオ説明文らしきデータが続きます。分かっていることとしては、少なくとも位置0x4FC以降は確実に圧縮されています。大抵の場合、0x4FCに0x02というデータが格納されています。この0x02 = 0b00000010はLZSSを解凍するための情報で、各ビットにそれ以降のデータがただの1バイトデータなのか、それともリングバッファを参照するための数値のペア(長さ, 距離)からなる2バイトのデータなのかを表すフラグが格納されています。したがって、とりあえず0x4FC以降のデータをLZSSで解凍することで2.5MB程度のシナリオデータを得ることが出来ます。

こうして解凍したデータをUTF-8形式に対応したバイナリエディタDANDP Binary Editorなど)で開くことで、テキスト部分であれば一応読めます。解凍したデータのサイズは0x288E80程度で、末尾の0x00が連続した部分を除けば長さは0x27C200程度あります。0x279D40前後に再びシナリオの説明文が読める箇所が現れるのですが、このあたりより後ろのデータは上手く解凍されていないようで、全く読めないデータが続きます。この部分はおそらく、解凍する前に取り除く必要があったフッタ部分も含めて解凍してしまったためだと思います。

現状分かっていることは大体書きました。もし新しく何か分かったら続きを書きます。