口袋妖怪绿宝石——数据提取与代码分析(2-基于名称列表的详情信息提取)
说在前面:
在上期专栏,我们利用字符集,可以对ROM中的文本列表进行提取,这些文本列表通常是一类游戏对象的名称,例如精灵名称、道具名称、招式名称等,因此也可以称作名称列表。以名称列表为基础,利用Excel表格的自动填充功能,上面提到的3个名称列表又可以对应到精灵代码、道具代码和招式(技能)代码。
精灵除了种族名称之外,还有许多其他的信息,例如种族值、特性、获得努力值、属性、蛋组等等;道具除了名称之外,还有出售价格、使用效果、背包分类等信息。这些都可以称为详情信息,本期专栏的目标是提取名称列表对应的详情信息,填充成为一个更大的表格。

详情信息在哪儿找
上期专栏提到,如果想要找到名称列表,可以通过搜索一个已知名称的编码字节序列来定位。以精灵名称列表为例,怎样找到精灵的详情信息呢?
我们需要从精灵名称列表对应的符号“gSpeciesNames”入手,利用VS Code提供的查找功能,找到精灵详情信息对应的符号是什么。
在VS Code中打开原版绿宝石的源代码工程,选择“编辑——在文件中查找”,或者使用Ctrl+Shift+F快捷键,在搜索栏里填入gSpeciesNames,VS Code可以找到这个字符串出现在了哪些文件中:

现在可以用更专业一点的说法了,其实gSpeciesNames是一个常量的名字,这是符号表里面的一类符号,其他的符号还有函数名、变量名等等。之所以说是常量,是因为查找结果的第一条恰好就是gSpeciesNames的声明,它使用了const进行修饰,所以是常量,这是C语言的编程语法。
鼠标单击任意一个查找结果,VS Code就可以自动打开这个查找结果对应的文件,比如打开第一条查找结果:

这里可以看到打开的文件是data.h,里面的第127行是常量gSpeciesNames的声明。声明只说明了这个常量的类型,是一个二维数组,要知道它的内容还需要找到它的定义。VS Code作为代码编辑器,找到任何一个名字的定义都是非常方便的,只需要在gSpeciesNames上点击右键,就可以看到“转到定义”的选项:

单击“转到定义”后,就可以看到VS Code打开了一个新的文件species_names.h:

这个文件恰好就是我们在上一期专栏提取出来的精灵名称列表。
按照C语言的语法,在定义gSpeciesNames这个数组时,还定义了一系列的枚举类型常量,也就是以SPECIES_开头的那些符号。由于C语言的数组下标从0开始,所以上面的定义相当于:
可以理解为,这些SPECIES_开头的常量就是精灵代码:它表示一个数字,这个数字对应到一个精灵的种族名称。在源代码项目中,会用这些常量的名称来代替纯粹的数字(这是为了程序的可读性),比如说之后程序中的某处需要用到“妙蛙种子”的精灵代码,就会用SPECIES_BULBASAUR这个常量代替,而不是直接写一个数字1。
有了精灵代码,定义精灵详情的时候一定会用到这些常量,于是“文件查找”功能再次上线,这次查找SPECIES_BULBASAUR:

这里出现了28个查找结果,其实每一个都可以点进去看看,它所在的文件名大致描述了这个常量用在这里的原因。把几个可能有用的查找结果列举如下:
species_info.h文件,精灵详情信息
evolution.h文件,精灵进化信息
tmhm_learnsets.h文件,精灵可以学习的技能机器
以species_info.h文件为例,查找SPECIES_BULBASAUR结果是这样的:

可以看到,妙蛙种子这只精灵的详情详细地列举在文件中,并且往下依次是第二只、第三只……精灵的详情信息,这正是我们要找的精灵详情信息!
这里对我们价值最大的信息是:精灵详情信息对应的常量名称是gSpeciesInfo,它的类型是struct SpeciesInfo,这是C语言中的结构体。这两个信息对于我们在ROM中提取数据是至关重要的:常量名称可以通过符号表来找到地址;类型可以让我们知道一只精灵的详情信息需要多少个字节来描述。一个一个来:
在符号表(pokeemerald.sym.txt)文件中查找gSpeciesInfo,可以看到它的地址:

gSpeciesInfo地址从083203cc开始,长度是0x2d10个字节。
在species_info.h文件中,右键点击struct SpeciesInfo跳转到它的定义:

这个结构体内,每个成员变量的左侧都有注释,解释了这些变量的相对地址。例如变量friendship位于相对地址0x12处,这个信息对于我们填写精灵详情信息的表格非常重要。
这里还有一点解释一下:以evYield_开头的变量,它们的含义是击败这只精灵后会获得多少努力值(神奇宝贝百科上把努力值也称作基础点数),可以看到前4个这样的变量相对地址都是0x0A,这是因为这些变量并没有占据一整个字节的空间,而是只占了2个二进制位,4个变量一共占据8位,从而利用整个字节的空间。变量名称后面的“:2”就是指一个变量占2个二进制位的意思。
同样地,0x19处的一个字节也分为两个变量,低7位(第0位到第6位)是bodyColor变量,最高位(第7位)是noFlip变量。
观察0x0B位置的读者可能有疑惑:一个字节有8个二进制位,0x0A和0x19处的变量都已经把所有二进制位充分利用了,但是0x0B处只用了4位(evYield_SpAttack和evYield_SpDefense),剩下的4位为什么没有对应的变量呢?
如果没有的话,那就说明剩下的4位没有实际意义,这4位不包含任何信息。
这个结构体的定义告诉我们:一只精灵的详情需要0x1A个字节(最后一个变量在0x19处,且占用1个字节,0x19+1=0x1A),也就是26个字节。

真是26个字节?数据对齐引发的问题
刚才仅仅是说明:SpeciesInfo这个结构体定义的单个变量至少占用26个字节,但它实际占用的字节数不一定是26。为了说明这一点,有一个C语言程序的传统需要介绍。
先说一个结论:ROM中的变量或者常量往往会对齐到以4为倍数的地址。这是因为原本ROM运行在GBA游戏机上,这个游戏机模拟的是32位操作系统,32位就是4个字节,对于每次操作以4个字节为单位的数据是最方便快捷的。同样的道理,如果数据放在ROM中的地址是4的倍数,中央处理器(CPU)在取数据时也会更快捷。这个道理在后面的专栏进行汇编代码分析时还会再做说明!
gSpeciesInfo的起始地址083203cc就是4的倍数,但是刚才分析的结构体大小26不是4的倍数。比26大(需要保证能放得下结构体的所有内容)、同时又是4的倍数的数字,最小是28(“最小”是为了节约空间)。gSpeciesInfo里的第二组数据(编号为1的精灵对应的详情信息)需要对齐到083203cc+0x1C这个地址,这就是数据对齐。
还有一个方法验证SpeciesInfo结构体究竟是占用26个字节还是28个字节。上期专栏提取出的精灵列表,序号从0x0000到0x019B,共有0x19C行数据,而gSpeciesInfo长度0x2d10个字节,0x2d10和0x19C相除正好是0x1C(十进制的28)。

利用“多行编辑”高效编辑字节序列
现在可以从HxD中将ROM的字节序列复制出来了:

将这部分数据复制到VS Code中,接下来是给数据分段。字节序列的格式是2个数字或英文字母,后面跟着一个空格,然后重复同样的格式。我们需要把每28个字节放在一行内,正则表达式的查找功能非常适合这个操作。
在Ctrl+F弹出的查找对话框内输入这个正则表达式:
这个正则表达式含义是2个数字或英文字母、后面跟一个空格,这个模式重复28次。按组合键Alt+Enter进入多行编辑模式,就可以在每28个字节后面输入一个换行符,实现数据分段:

通过调整光标的位置,可以把光标定位到每一行的开头,对每行的编辑等价于对所有行的编辑。接下来需要按照SpeciesInfo结构体的定义,把字节进行分组归类,组与组之间用制表符隔开,这在多行编辑的情况下处理起来非常方便。
从baseHP到expYield,前10个字节都是单字节的变量(u8类型就是8位2进制,也就是1个字节),在编辑时直接删除空格换成制表符即可。
0x0A和0x0B都是和努力值相关的变量,需要把它们合并在一起。在合并时,由于多字节变量的取值是按照小端序排列的,需要把字节的顺序反过来再拼接到一起,这就需要在多行编辑的时候能够进行“选中特定的一段数据”这种操作,并且这个操作是在所有行同时进行的。

按住Shift键,然后通过方向键控制光标,就可以在多行编辑的情况下选中特定的一段数据,例如上图选中了0x0B对应的一列,我们需要把它剪切,并粘贴到到0x0A那一列的前面。操作后的效果如下:

可以用同样的方式继续处理后面的变量。虽然按照每行28个字节来划分,但只有前26个字节是有意义的,因此可以放心地删掉最后2个字节。
最后处理结果如下图,这是可以直接复制到Excel表格中的数据格式:

复制到Excel表格中,添加合适的列头,和之前的精灵名称列表结合到一起,一个精灵详情表就创建出来了:


利用“详情表”探索未知数据
在源代码项目的指导下,我们很清楚字节序列中每个字节代表的含义。但是当分析没有源代码的数据时,可能有些字节的含义事先我们并不知道。此时就需要利用Excel的“筛选”功能,推测未知字节可能代表的含义。
以上面的“精灵详情表”为例,比如说第J列的“属性1”我们并不知道是什么含义,但是在每只精灵的数据都已知的情况下,我们可以通过对具有相同“属性1”的精灵分类,来推测它可能是什么含义。
选中整个表格,使用Excel的“数据——筛选”功能,在第一行的每个单元格右边都会出现一个下拉箭头,点击“属性1”所在的列,可以看到有一个按照取值分类的下拉列表:

这里我们把其他所有的对勾都取消(可以简单地通过把“全选”对勾取消来实现),然后只勾选01,看看剩下的都是什么精灵:

这里面的精灵有:猴怪、火爆猴、腕力、豪力、飞腿郎、快拳郎、无畏小子、怪力、幕下力士、铁掌力士、玛莎那、恰雷姆。这些精灵有什么共同的特点呢?
基于对口袋妖怪绿宝石这个游戏的了解,熟悉的玩家应该能立刻想到这些精灵都是格斗系的精灵。“属性1”这个标签就算是未知的,通过刚才的分析也能大致猜到它的含义。
这种通过提取数据、数据筛选的分析方式在探索改版绿宝石ROM时能起到很大的作用!

一些常见的名称列表和详情列表常量名称
这里列举一些口袋妖怪绿宝石ROM中常见的名称列表和详情列表的常量名称。结合符号表和这两期专栏介绍的数据提取操作,可以制作出许多张Excel详情表格:
注:这里的“特性”举个例子比较好理解,例如“妙蛙种子”的普通特性是“茂盛”。

这两期专栏主要介绍利用文本信息来提取ROM数据的方法,提取的数据都遵循列表格式,可以方便地复制到Excel表格中进行处理。但是ROM中还有其他长度不定的数据,也不遵循列表的格式,例如地图数据、对话文本、脚本等等。这些数据的提取方法将会留到接下来的专栏进行分析。
继续感谢众位读者的支持!