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

真的是一波三折,卡壳卡了好几天,结果踏破铁鞋无觅处,得来全不分功夫。

目录

本系列已完结,以下是各章节说明,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. 动态图、鉴赏模式

程序如何显示文字 - GAME.SCR

前面我们知道了,资源中有几个字体文件,这些文件中保存了文字的写法,所以我们基本可以肯定,程序会根据某些信息,来确认到底是要画出哪个字。

另一个我们基本可以肯定的事情是文字的编码,Big5 基本上也是没跑。

而最大的问题,是程序显示的文字从哪里来。

我们可以合理推测这个文本应该是出自 game.scr,看名字这应该是脚本文件,游戏大到一定程度应该是会有脚本,这样可以把一部分游戏逻辑的工作从程序那边分离出来,程序可以更专注功能的实现。

我最初的想法是硬解,game.scr 中的数据,如果能在字体文件中找到,那么就是文字, 找不到的,那可能是流程控制字符。那么怎么定义「找到」,字体文件的头部不是有所有文字的编码么, 每次读取两个字节,如果是一个字,则保存,然后移动两位继续读,如果不是字,那么把保存的字输出出来,移动一位继续读。

这个方法是可以输出不少找到的文字,但是经过粗略的筛查就可以知道,输出的文字都是连起来无意义的单字,所以这个方法不可行。

不过我们起码知道了一点,game.scr 并不是明文的数据,可能是加密后的脚本。

程序如何显示文字 - FON

这条路走不通,我们还可以有两个选择:

  1. 看程序如何使用字体文件去画字
  2. 继续看 GAME.SCR 是如何读取的。

我选了第一条路,结果事实证明是我最近卡壳的重要原因,确实我也找到了文字数据:

offset data comment
028F:50D0 043F ??
028F:50D2 028F:50EA
028F:50D6 028F:51EA
028F:51EA 1A 长度
028F:51EB C4 5F A4 47 B7 DD A1 42 C4 5F A4 47 B7 DD A1 41 A7 D6 B0 5F A8 D3 B0 DA A1 49
028F:50EA 1A 长度
028F:50EB C4 5F A4 47 B7 DD A1 42 C4 5F A4 47 B7 DD A1 41 A7 D6 B0 5F A8 D3 B0 DA A1 49

这个就是开头的第一句话:

可是我就是看着这段数据从这里复制到那里,从那里再复制到另一个地方,有时候还会拷回原来的地方,这就叫人崩溃,到最后也没有摸出来源头。

而另一个让人崩溃的点是,ghidra 开始不给力了:

我们可以看到,相同的位置,明明是指令,却被 ghidra 识别成了资源,更让人难受的是,这种情况还不止一个,经常会想看一个 cpu log 中的函数,在 ghidra 中却找不到位置,理解函数和处理流程的效率大打折扣。我 ghidra 也刚用没多久,不晓得这种情况是否可以手动调整一下。对于这个情况,我觉得其实也不一定是 ghidra 的锅,程序里有脚本,那大概率有些脚本也是以资源的形式保存在程序中的,以我目前的程度,还不能完全掌控这种局面。

不过,看了这么久,也不是一点收获也没有,我们来看程序是如何把数据画成字的:

我之前卖了个关子,程序如何把 big5 编码的字画出来,因为两字节可以容纳 65536 个字,不过程序里只有 16366 个,所以实际占不了那么多空间,这种情况下如何确认是哪个字并画出来,其实这就是程序的算法,看着这三个每次除以二的数字,有点基础的朋友可能会会心一笑,二分法,写程序的老哥基础不错。那个时代用二分法确实可能比较简单,我印象中 c 引入 unordered_map,也是很后面的事情,那个时候只有 map,这个地方用 map 也合适,因为我们可以控制字符编码的顺序一定是递增的,不过如果放到现在,我想更好的做法应该是用 hashmap 类似的容器,查找更快一点。这个逻辑细节我就不展开说了,因为我们重点不是这个,我们要找文字是哪里来的。

我就这么在内存数据、cpu log 以及 ghidra 中毫无实质进展地耗了好几天,直到一天晚上,我觉得算了,还是从读取文件下手。

喔,这几天我还学到了一个 dosbox 的小技巧,Save / Load State:

由于我的主要实验场景是开新游戏,所以在播完动画保存一下状态,之后每次再加载状态,就可以直接回到游戏主界面,不用重开游戏甚至 dosbox。

另外还有一个发现,程序在文字界面用来打印字符串的函数应该是 FUN_1048_0ab1

发现这个函数其实也很偶然,我其实没看懂这个函数的流程,不过看他传入的参数,是字符串的地址,所以我想应该是它没错。

你看,我是真的在不停研究这个东西,但是也是真的卡壳了。

程序如何显示文字 - GAME.SCR - 二

前面也说了,由于我现在不太相信 ghidra 的反编结果,所以我主要仰仗的还是 cpu log, cpu log 和反编两者各有利弊,而且互相补充,cpu log 可以快速帮我们抓住逻辑主线, 而 ghidra 能帮我们掌握程序的结构。我们最终还原的程序流程, 有时候也需要从 cpu log 提取出的信息来验证。现在在我看来,ghidra 已经是靠不太住了,那就看看 cpu log 好了。

思路其实也很简单,bpint 21 3d 确定 GAME.SCR 是何时打开的,然后去看 bpint 21 3f 找到何时读取这个文件,然后打 log。

看 log 看了好几天,整个人都要麻木了,不过这个思路印出来的 log 倒是很容易发现端倪:

从原始的位置读出来,然后再写回原始的位置,这似乎是一个简单的类似 ROT13 的加密方式,那这个程序太好写了,立即写个程序还原游戏脚本:

SHOWEVENT 117,255,32
msg '神秘的玉門關,已經完全門戶大開,似乎正引誘著你長驅直入呢!'

脚本很长,而且内容我应该是不能贴的,看来这个游戏基本上对我没有什么秘密可言了。

总结

我大致对了一下游戏内容,这个游戏脚本化的水平还是相当高的,我有印象的游戏内容, 都来自脚本,所以程序老哥的水平我觉得还是很高的,说出来你可能不信, 其实之前我觉得老哥水平是堪忧的,因为有些资源的处理有些毛草,这个我们以后再说好了。

后来我转念一想,既然脚本怎么玩我已经知道了,那其实我可以自己做游戏内容了。