|
前言
《过山车大亨3》是我喜欢玩的老游戏之一,最近发布了一个完全版(炒冷饭),就想起了之前完成的一个游戏启动器,放在现在来看代码和功能都挺垃圾的。
(不要脸的放上仓库地址。这个版本是很久以前刚学C#的时候写的,代码质量很差,最近简单的添加了对新版本的支持后,就放弃维护了。)
我一直在想能不能研究一下这个游戏的数据结构,完成一个功能更加强大的游戏启动器,同时锻炼一下自己的能力,于是就有了这整个系列。
预备知识
《过山车大亨3》的存档文件格式是 .dat ,在游戏里的表现主要有几个方面:
- 存档名直接保存为存档文件名。
- 每次存档会在当前的游戏视角截取一张缩略图记录进存档文件。
- 如果是覆盖已有存档的情况,游戏会将被覆盖的存档后添加 .bak 后缀(如果已经存在该文件就覆盖),然后再创建新存档。
另外,游戏还有几个特点:
- 所有的数据文件都使用小端模式保存。
- 所有的UI都是固定尺寸,不随分辨率整体缩放。
获取存档缩略图的原因
新版本的启动器中有一个构想功能就是让用户不用启动游戏就可以管理存档。虽然根据上一节所说,存档名称就是存档的文件名,但是作为一个强大的启动器,怎么能只显示一个名称呢!所以就要开始寻找存档缩略图的存储方式了。
利用控制变量法生成分析用的存档
首先我想到用游戏的存档机制来尽量创造只跟缩略图有关的差异,我新建了一个自由模式存档,关闭乐园,暂停了时间。


这次分析使用的三个存档
- Test.dat.old 是第一个创建的存档。
- Test.dat.new 只是在第一个存档的基础上改变了视角(改变了缩略图)。
- Test.dat.new.same 与第二个存档具有完全相同的视角,只是隔了一段时间重新保存了一次。
使用二进制工具进行文件对比
存档有了,肯定就得开始找不同,这个时候就得祭出Hex Workshop了。


比较结果(只显示了被替换的块)
看起来被替换的地方比较多,但是我们要找的是缩略图,肯定要赌一把直接保存RGB值的情况,所以长度不会太短,符合筛选条件的也就两条:

其实这个时候0008D8CA这一条的嫌疑已经超级高了,主要有以下原因:
- 游戏中的缩略图很小,即使是直接存储RGB值也差不多在5位数这个量级上。
- Source和Target中找到的起始位置和长度差别不大(这里碰巧相同),说明他们极有可能表达的相同意义的数据。
所以我们直接转过去:

比较的结果,注意看顶上黄色区域的前面,还有两个可疑的字节
看旁边排列规律的字符,明显发现是三个一组,这个时候基本上就可以确定缩略图就是它了,而且居然就是直接保存的RGB值。
缩略图的保存方式
我们先根据比较的结果计算一下像素量: \frac{12873}{3}=4291
把4291进行分解,发现只能分解为 7\times613 ,这很明显不是一个正常的缩略图,它应该是一个长和宽比较接近的矩形。
仔细看这一段文件数据,发现被替换的块的前面还有很可疑的两字节,可能也是包含在图片信息中的。接着切到这一块末尾,果然还有一个字节在那。

那么加上这三个字节,这一段长度就是12876了,继续除以3分解,发现可以被分解为2\times2\times29\times37 ,把两个2分别乘到后面去就是 74\times58 ,越看越像一张缩略图了。
验证结论
我们知道游戏中的UI是没有缩放的,所以可以直接测量存档缩略图的大小。强烈推荐QQ自带的截图工具,超级好用!

QQ截图有放大镜,这是跟图片边角对齐了的
可以发现跟我们计算出来的结果一模一样,所以存档存放缩略图的地方确实是这里。同时我也使用了第三个存档和第二个存档比较,过程差不多,就不放图了,最后发现这一段是完全相同的。
别急,还有问题没有解决
知道了图片大小还不够,图片像素是怎么排列的还要研究。这个就比较简单了,我们知道图片大小是 74\times58 ,所以只需要找第1个(确定像素起始位置)、第2个(确定像素单行/列走向)和第75个像素(确定像素跨行/列走向)即可。
第1个像素:文件内格式为0C 74 5E,值为#5E740C(94,116,12)
第2个像素:文件内格式为04 6E 50,值为#506E04(80,110,4)
第75个像素:文件内格式为10 7A 62,值为#627A10(98,122,16)

第1个像素点

第2个像素点

第75个像素点
这样就可以知道,缩略图的像素点是从左下角开始,先按行再按列绘制。
然后就是怎么在不同的文件中寻找这个缩略图了,通过对多个文件分析容易看出来,缩略图的RGB数据前都有一段相同的头(50 32 00 00 C4 10 00 00),而且在单个文件中仅存在这一个头。
程序验证
class Program
{
static void Main(string[] args)
{
int[] header = new int[] { 0x50, 0x32, 0x00, 0x00, 0xC4, 0x10, 0x00, 0x00 }; //信息头
FileStream file;
int index = 0;
bool flag = false;
try
{
file = File.Open("save.dat", FileMode.Open);
}
catch (FileNotFoundException ex)
{
Console.WriteLine(ex.Message);
return;
}
while (file.Position < file.Length)
{
int temp = file.ReadByte();
if (temp != header[index])
index = 0;
if (temp == header[index])
index++;
if (index == header.Length)
{
flag = true;
break;
}
}
if (flag)
{
//只有RGB信息,所以使用Format24bppRgb
Bitmap bitmap = new Bitmap(74, 58, PixelFormat.Format24bppRgb);
BitmapData bitmapData = bitmap.LockBits(new Rectangle(new Point(0, 0), new Size(bitmap.Width, bitmap.Height)), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] temp = new byte[3];
int height = bitmap.Height - 1, width;
unsafe
{
byte * scan = (byte * ) bitmapData.Scan0;
for (; height >= 0; height--)
{
for (width = 0; width < bitmap.Width; width++)
{
int offset = height * bitmapData.Stride + width * 3;
file.Read(temp, 0, 3);
scan[offset] = temp[0]; //B
scan[offset + 1] = temp[1]; //G
scan[offset + 2] = temp[2]; //R
}
}
}
bitmap.UnlockBits(bitmapData);
bitmap.Save(&#34;save.bmp&#34;);
}
else
Console.WriteLine(&#34;Not Found Image&#34;);
file.Close();
file.Dispose();
}
}

随便找了个存档测试,成功输出了要找的缩略图
结论
《过山车大亨3》的存档文件中,缩略图数据的结构特征有:
- 以 50 32 00 00 C4 10 00 00 开头。
- 除去开头,数据部分有 74\times58\times3=12876 字节,文件中呈BGR排列。
- 像素点从左下角开始,先按行再按列保存。
总结
没想到这么快就找到了存档的缩略图,整个过程还是有许多乐趣和收获,也是见识到了这种工作的难度,但是这是这个系列最简单的一个开始,希望之后的分析能够顺利,也希望能得到大佬的支持。 |
|