口袋妖怪绿宝石——数据提取与代码分析(3-地图信息提取)
说在前面:
口袋妖怪绿宝石的ROM中保存了游戏世界中的所有地图,这些地图信息要比前几期专栏介绍的文本信息更加直观。但作者并没有选择这么直观的信息作为一开始就介绍的对象,是因为要想把提取地图信息的功能拓展到绿宝石的改版上,前几期专栏介绍的知识和方法是必不可少的。
在网络上,提取ROM的地图信息有很好用的工具,在这期专栏主要介绍Advance Map。

Advance Map地图信息提取工具
Advance Map的官方网址:
http://ampage.no-ip.info/index.php?seite=advancemap
它的最新版本是1.95,最后一次更新于2011年。在这个版本更新后的10年(2021年),工具的作者在官网上表示TA已经不会再更新这款软件了。虽然是一款比较老的工具,但是还算好用。
打开Advance Map,如果看到的是英文界面,可以在左上角菜单栏的“Settings-Language”里面把语言改成“简体中文”:

从“文件——读取ROM”中打开口袋妖怪绿宝石的ROM文件。如果打开的是原版绿宝石的ROM,读取过程会十分顺利,左侧会出现3个选项,最常用的是第一个“通过头文件”,把它展开就可以看到一些标着数字的选项,如果再次打开其中一个,比如“0”,里面会列举出一系列地图的名字,双击第一个名字,地图的信息就会显示在界面中间:

对于第一次使用这个工具的读者,如果你们感觉这个工具很有趣的话,相信好奇心会驱使着你们探索这个工具的各种功能,这不是本期专栏的重点。网络上也有一个Advance Map的简单教程:
https://www.hackromtools.info/advance-map/
本期专栏是要利用Advance Map这个工具来探索ROM中地图信息的组织结构。

绿宝石ROM中地图信息的组织结构
从Advance Map左侧的列表中可以看到,每个地图被放在一个以数字命名的文件夹内,同时每个地图的名字后面都会有一个括号括起来的、由逗号隔开的两个数字。例如上图“橙华市”的地图名称“PETALBURG CITY (0,0)”,其中第一个0指的是它位于“0”这个文件夹内,第二个“0”表示它在该文件夹内是第一张地图。
事实上,ROM中组织地图信息的结构也是这样的:每张地图用两个数字来表示它的编号:第一个数字被称作地图库编号,第二个数字被称作地图编号。这两个编号就是每张地图区别于其他地图的标识。
一张地图需要存储的信息很多,而且每张地图大小不一定相同,这和前几期专栏处理的“每行结构一样”的数据是很不一样的。有一种办法可以让这些大小不一的数据拥有一个统一的“长度”,就是使用指针。
C语言中的指针是一个重要的知识点,本期专栏只介绍ROM中的指针。ROM中的指针是一段长度4个字节的数据,这4个字节按照小端序的方式拼接起来,表示的是ROM中的一个地址,也就是这个指针指向的地址。
有了指针这个概念,无论一张地图需要存储多少数据,只需要把这块数据的起始地址保存成一个指针,就可以用4个字节来表示这张地图,所有的地图就都具有相同的长度了。这就好比一条大街上有饭店、超市、医院等等,它们各自有长度不同的名称,但是用“某某街某号”就可以用统一的方式称呼它们一样。
Advance Map左侧的列表中,每个以数字命名的文件夹就是地图库,可以把它们称作“0号地图库”“1号地图库”……每个地图库中存储的地图数量也不相同,如果每个地图库都需要一个固定长度的数据来表示,指针也成为了最适合的选择。
指针在绿宝石ROM中的使用十分普遍,希望读者能够尽快熟悉指针的使用,下面马上就会给出一个例子。
在绿宝石源代码的符号表中,gMapGroups这个符号表示所有的地图库:

我们在HxD中找到这个地址,看看gMapGroups的内容是什么:

用HxD打开ROM文件,使用“搜索——跳转”或者Ctrl+G快捷键,在输入框内输入gMapGroups的地址,然后点击确定跳转到该地址:

光标停留到的位置就是gMapGroups的地址,观察一下这里的数据,有个很明显的规律就是以每4个字节为一组,组内的后两个字节都是48 08。这里,“以每4个字节为一组”就是指针的典型特征,把第一组数据60 5D 48 08按照小端序拼接的结果是08485D60,回到符号表中查看一下,发现这个地址是有名字的:

08485D60这个地址就是0号地图库的指针,它的名字“gMapGroup_TownsAndRoutes”表明这个地图库中存储的是各个城镇(Towns)和道路(Routes)的地图,读者可以从Advance Map中去验证这一点。
以此类推,第二组4字节数据44 5E 48 08,对应到地址08485E44,也就是“gMapGroup_IndoorLittleroot”,存储的是未白镇(Littleroot)各个房间室内(Indoor)的地图。
这样一来地图库的结构就非常清晰了。gMapGroup(地址088486578)存储了所有地图库的指针,并且有如下规律:地址08487578+X * 0x4 处存储的4字节变量就是X号地图库的指针。
接下来可以看看地图库gMapGroup_TownsAndRoutes(地址08485D60)的指针指向了什么内容:

规律很明显,0号地图库存储的也是一系列的指针,第一个指针是084824B8,同样可以在符号表中找到名字:

PetalburgCity就是橙华市,这和我们在Advance Map中看到的0号地图库内的0号地图是橙华市地图是吻合的。这样,地图库的结构也很清晰了。地图库存储了该地图库内所有地图的指针,并且有如下规律:X号地图库地址+Y * 0x4 处存储的4字节变量就是X号地图库Y号地图的指针。
因此,我们可以将gMapGroups称作“地图库指针列表”,每个地图库也可以称作“X号地图库的地图指针列表”,这是一个完全由指针组织的数据结构,它就像一个个指路牌,指引游戏程序找到需要使用的数据。

地图数据
然而指路牌毕竟不是目的地本身,指针指来指去最后还是要指向有实际意义的数据才行。下面以084824B8处的PetalburgCity为例,看看一张地图究竟包含了哪些数据。
在Advance Map中,打开中间界面的“地图头”选项卡:

如果读者看到的不是这个界面,可以在“设置”中勾选“专业地图头查看模式”,或者按Ctrl+H快捷键。这个“地图头”就是橙华市地图包含的数据,接下来依次解释一下:
地图库头:00486578,指的就是gMapGroups
地图库编号:0号地图库;地图库地址:0号地图库gMapGroup_TownsAndRoutes
地图编号:0号地图;地图头地址:0号地图库的0号地图PetalburgCity
这里出现了一个新的概念“地图头”,把Advance Map切换回英文界面时这个名称是Map Header,这是按照绿宝石源代码项目中的名字来命名的。事实上,源代码中恰好有一个叫做MapHeader的结构体,定义的就是地图:

可以看到结构体的定义里面,开头的4个变量都是指针,分别对应了Advance Map里面的“地图尾地址”“事件地址”“地图脚本地址”和“连接地址”。再后面的变量,在Advance Map的“专业地图头查看模式”下就看不到了,可以按Ctrl+H把它关闭了再看:

结构体中从相对地址0x10处的变量在上图中显示出来,音乐、洞穴、天气等等。
“专业地图头查看模式”下,可以看到“地图头”的后面列举了一串字节,这就是HxD中把字节照搬过来的结果,对比MapHeader结构体的定义、Advance Map两种查看模式下给出的解释,哪段数据是什么含义是一目了然的。
利用Advance Map,提取地图信息会变得十分方便。还有很多功能本期专栏没有介绍,有感兴趣的读者可以在讨论区讨论。

Advance Map的缺陷
作为一个超过十年没有更新的软件,Advance Map有各种缺陷是很正常的,其中作者认为最严重的两个缺陷如下:
不支持汉化版ROM,也没有开放能够支持汉化编码的功能。如果使用Advance Map打开一个汉化版的绿宝石ROM,所有的地图名称都会显示乱码,这和在“设置”中把软件语言改成“简体中文”完全是两回事。说明Advance Map本身就不支持对ROM中汉字的解码。显示乱码还是小问题,只要能打开地图就行,关键是有的乱码里面包含了没有配对的括号(比如只有左括号而没有右括号),Advance Map就会提示这个地图名称不合法,拒绝打开地图,但实际上能不能打开地图和地图的名字完全没关系。
打开ROM的过程中,出现的报错信息不够详细,有时很难根据报错信息找到打不开ROM的原因。对于一些改版的ROM,里面存储的地图信息的位置可能发生了变化,导致Advance Map找不到,这时它就会出现报错信息。有的报错信息会告诉你是因为哪个符号没找到导致的ROM打不开,有的报错信息则没有什么信息量,需要自己去配置文件里一个一个调整才能发现问题在哪。下面的一个小节会给出报错信息的例子。
既然Advance Map的作者不打算修bug了,那么这个系列的专栏会给出一些弥补的办法。

用Advance Map打开ROM
打开原版绿宝石时,作者用简单的一句“文件——读取ROM”就带过了,但实际上如果打算用Advance Map打开一个改版过的绿宝石ROM,会遇到数不清的问题。下图为作者试图打开某个改版ROM时,Advance Map的报错信息:


一般来说,只要这种报错的信息框弹出来,左侧就不会显示任何地图的列表,什么地图信息也没有。在第二张报错图里面,软件还“好心地”提示有什么问题可以联系软件作者的邮箱,当然这个办法肯定是不管用了。
解决报错问题的关键,在于修改Advance Map的配置文件,它涉及的内容很多,留到下期专栏具体来讲。
瞬移金手指和地图编号的关系
在绿宝石系列游戏中,有一类金手指被称作“瞬移金手指”,打开适当的瞬移金手指后,当游戏角色进行进门、出门等操作的时候,就会瞬移到金手指指定的地点,这类金手指利用的就是地图编号信息,下面用游戏来举个例子。
在符号表中,有一个符号名称叫做gSaveBlock1Ptr:

以“Ptr”结尾的变量往往表示一个指针(Pointer的简写Ptr),它指向的是gSaveBlock1这个变量。从VBA中打开原版绿宝石的游戏,看看这个地址处是什么:


03005D8C处确实是一个指针,指向02025A20,再看看这个地址处的数据:

这里的001A表示Y坐标,0006表示X坐标,后面的2B18数据,0x18是地图库编号,0x2B是地图编号,也就是24号地图库、43号地图,我们从Advance Map里验证一下:

这和游戏是吻合的,当前游戏角色的确位于冠军之路的第1层。在地图上移动鼠标时,Advance Map的左下角会显示当前的X坐标和Y坐标,也可以验证当前角色所在的位置是不是对应的上。
2B18的地址是02025A24,只要修改这个地址处的2字节变量就可以实现瞬移金手指。
但是,不要指望这个金手指能一直起作用!02025A24这个地址是会变化的,变化的原因需要在后续专栏分析代码的时候给出解释。

本期专栏还有一个遗留问题没有解决,就是如何修改Advance Map的配置文件以便打开改版的ROM,,这个问题留到下期专栏具体说明。
对Advance Map还不太满意的读者,如果有编程能力的话不妨写一个parser,本期专栏已经解释了地图数据是如何组织的,使用程序来读取并不困难。
继续感谢众位读者的支持!