红楼梦十二金钗游戏资源分析(十八)

停更了一断时间了,一个原因是最近工作很忙,用来研究的时间没有以前多, 还有一个原因是我掉坑里了,研究一直没有进展,还好目前看到一丝曙光, 那就记录一下现在的困境,准备开启新的一章。

目录

本系列已完结,以下是各章节说明,17 之前是 dos 版相关,之后是 2001 版:

  1. 背景、简单分析
  2. 显存位置
  3. 事件图保存算法: LZW
  4. 调色板
  5. MGP2
  6. 结局图
  7. 事件图中的眼睛
  8. 音频文件
  9. 按位读取
  10. 循环之前
  11. 读取循环
  12. 重现 LZW
  13. PAT 的图形格式
  14. STAFF 调色板
  15. 字体文件
  16. 脚本解密
  17. 版本比较
  18. 第一张图
  19. 调色板1
  20. 第二张图
  21. 调色板2
  22. 调色板处理
  23. 静态事件图、结局图
  24. 动态图、鉴赏模式

目前研究的内容

其实我最近研究的内容都并不在我的计划内,只是因为想看看修订版的游戏脚本, 就顺带分析了修订版的游戏脚本,于是乎,觉得那顺带看看修订版的事件图是如何解析的吧, 我也不想再开一个系列,索性都放一起。

于是我就掉坑里了。

为什么会卡住

从 dos 切换到 windows,调试程序不再有 doabox 这么丝滑顺利,几经比较, 我选择了 ollydbg 1.10,其实 ollydbg 有 2.0,但是我在 win98 上跑一直会有问题, 比如 trace into 很快就会报错

尝试自己解决未果,于是退守回了 1.10。

由于 ollydbg 也是运行在 win98 上,所以本身就会有一些限制, 比如日志没办法以 gb 为单位记录,调试和游戏在一起,有时卡住没办法切回去。

熟悉我的解析系列的朋友都知道,我主要靠 ghidra 和大日志这两条腿来分析资源, 外带一些推测。现在日志了不起记个几十兆,于是我就像少了一条腿。

PAF 的读取

读取其实很容易去推测位置,在 ghidra 中搜索文件名就好。

于是我们很容易就找到了读取文件的位置:

iVar1 = read_paf_0041b6d0((void *)((int)param_1 + 0x188c),(undefined4 *)s_AREA.PAF_00554ea4,DAT_0056c8d0,0,0);
if (iVar1 == 0) {
  local_4 = 0xffffffff;
  assert_wrapper_00496fdc((int *)&local_10);
  ExceptionList = local_c;
  return 0;
}
iVar1 = read_paf_0041b6d0((void *)((int)param_1 + 0x1a14),(undefined4 *)s_REDSYS.PAF_00554e98,DAT_0056c8d0,0,0);
if (iVar1 == 0) {
  local_4 = 0xffffffff;
  assert_wrapper_00496fdc((int *)&local_10);
  ExceptionList = local_c;
  return 0;
}
...

与文件名相关的入口只有这么一个,那么看上去只要我们吃透 read_paf_0041b6d0 这一个函数就好了,不过事实远不止这么简单,不然我也不会掉坑里。

根据读取的日志,AREA.PAF 大约只读了 10 万个字节,而一张背景图的数据量大概是 23 万, 这个数据量明显是不足以承载一百多张图,所以我们推测还是在游戏中用到哪张读哪张。

有这个思路,我的想法是既然现在我可以控制脚本,那么我在脚本中运行 ShowEvent, 那只要找出 ShowEvent 执行的位置,那我们也就能找到程序是如何读取具体图片了,这个位置也不难找:

所以我们只要断点放在 405cb0,进断点后 trace into,然后就等日志就好了。

现实很残酷,日志并没有很长,而且没有读文件的记录。很显然,我掉坑里了。

填坑

启动时没有读取完整的数据,调用脚本时 cpu log 又没有看到文件读取,这是一对矛盾, 而我又不知道哪里错了,于是我开始不断试错,这是个很沮丧的过程,我简单列一下我形成的一些结论:

  • PAF 文件启动时读取的内容可能是检索表、或是调色板,没有实际像素内容,这部分内容有一些可能有加密,但是应该没有解压
  • PAF 最外层是 MGFILE,对应以前的 MG
  • 440h 处有 PAT2000 标记,对应以前的 Pattern
  • PAF 文件包含之前多组数据,比如 Area.paf 既包含了 AREA,也包含了 SAREA
  • 播放影片时,游戏启动了另一个线程
  • 大部分时间,游戏只有一个线程在工作

多次确认启动时的读取内容不足以覆盖图像数据后,我觉得问题出在 ollydbg, 它可能没有抓到读取文件的点,或者说,trace 的记录并不完整,于是我去试了试 ollydbg 2, 但是 ollydbg 2 trace into 和下断点一直都会有问题,我甚至重新准备了 Windows ME,无奈还是切回了 110 的版本。

直到一个偶然的机会我发现了 ollydbg 的一个功能:

这个功能可以把程序中所有外界调用都列出来,更神奇的是,我们还可以给所有的 ReadFile 设断点:

于是断点多出来一大堆:

立即我就有了思路:进入 ShowEvent 后,立即给所有 ReadFile 下断点。

终于,看上去我从坑里出来了,我们可以根据中断的位置,从 ghidra 找到一个合适的函数入口开始 trace into 就可以了。

EVENT 1 读取

找到文件读取,我们就可以迈开步子往前走了:

加载事件时,只读了两次文件,第一次读取了 38c8ch(232588)个字节, 第二次读取了 1d69h(7273)个字节,第一次读取的内容超过了 640x360 的大小, 所以我们不难推测,这个就是底图,而另一个,我想就是以前的眼部动画, 因为这个数据量对于调色板来说也过大。基于这一点我们就可以进一步找到底图的处理:

这里的 EBX 保存的值,或者说 ESI 指向的值就是文件内容,程序会计算一个偏移量, 从 EDX 那边取一个值,然后写到 EDI 中去。ECX 看上去就是循环次数, 从 280h 开始,如果我们去搜 412A62 行,并且指定 ECX = 27F,日志会告诉我们, 有 360 条:

280h = 640,外层又循环了 360 次,看来我们找到了第一张图。

细心的读者可能会注意到另一个问题,每次写到 EDI 的值,其实都是 7FFFh, 因为 EAX 的值一直没有变化,这个其实很简单,我们很容易猜到 EDX 里面存的是调色板, 因为游戏里显示事件有一个先白屏,然后再显示图片的效果:

所以这个特效是通过修改调色板实现的。不过今天我们不去讨论调色板了,文章内容已经太长了, 有了这些内容,我们应该可以画出来第一张图了,不过这里面还有一个小坑,那就是图像数据并不连续,实际是这样读取的:

let content = from_bin("data/Redevent.paf");
let start = 0x8a2;
let width = 640;
let height = 360;
let mut image_buffer = image::ImageBuffer::new(width, height);
for y in 0..height {
    for x in 0..width {
        let offset = start + y * (width + 6) + x;
        let pix_val = content[offset as usize];
        let pixel = image_buffer.get_pixel_mut(x, y);
        *pixel = Rgb([pix_val, pix_val, pix_val]);
    }
}
image_buffer.save("event-1.png").unwrap();

注意 start + y * (width + 6) + x,每行都有 6 个字节的偏移量。这下我们可以看一下画出的结果了,还记得这个灰姑娘么:

总结

为期一个月的摸索终于有了个还算像样的结果,终于我们也有了点结论:Red2001 并没有压缩图像数据, 这也不难理解,2001 年已经不再是磁盘的天下了,大家都在想破脑筋如何塞满六百多兆的光盘, 明显修订版内容和原版高度一致,一定塞不满光盘,所以为啥还要再去压缩数据呢。