欢迎光临散文网 会员登陆 & 注册

口袋妖怪绿宝石——数据提取与代码分析(9-脚本)

2022-08-23 18:15 作者:围巾胖头鱼  | 我要投稿

说在前面:

    关于反作弊机制的介绍告一段落,本期专栏来介绍口袋妖怪绿宝石ROM里一种新的数据:脚本。

    如果抛开源代码不看,单独只关注脚本的话,会发现脚本是一个自成体系的编程语言,它有自己的语法、库函数、变量等等。

    关于绿宝石脚本方面的知识,网上能够找到诸如“XSE脚本”之类的资料。本期专栏希望从源代码的角度来理解绿宝石ROM中的脚本,并将一些有意思的金手指结合到例子中来。

再谈接口

    上期专栏提到了接口这个概念,它可以让使用者不必关心实现功能的具体细节来完成某项任务。例如上期专栏提到的GetMonData函数,这个函数提取的是精灵信息。

    本来,精灵信息种类繁多,又由于原版绿宝石的反作弊机制,将许多信息用复杂的方式“包装起来”,这些原因都导致每次使用精灵信息的时候,直接通过结构体访问成员变量的方式来访问精灵信息是非常麻烦的。而GetMonData函数的出现,让这一过程变得简单了起来,只需要提供必要的参数(想提取哪只精灵的信息、提取精灵的什么信息等),作为接口的GetMonData就可以直接返回想要的信息,而不必关注其中的细节(例如数据打乱、数据加密等)。

    绿宝石ROM中的脚本,就可以看作是连续调用绿宝石ROM接口的一段代码脚本是基于接口的,关于这一点,在本期专栏还会反复说明。

脚本是什么样子的?

    如果要简单地回答这个问题,那么答案就会过于简单:既然ROM文件里面只有0/1序列,那么脚本当然也是0/1序列。

    读者肯定是不会满足于这么简单的答案的,但这个答案确实说出了脚本的本质。网上各种对绿宝石游戏进行改版的教程,在提到修改脚本时,最后的落脚点还是在修改ROM文件的0/1序列上(只不过用十六进制编辑器来看的话,修改的是字节序列,字节序列和0/1序列本身就是一回事)。

    我们需要把字节序列翻译成脚本语言才能让人类看懂。如果觉得这句话似曾相识的话,不妨回到口袋妖怪绿宝石——数据提取与代码分析(5-THUMB汇编指令基础)里去看看机器语言、汇编语言的定义。其实可以把脚本语言就当做是一种汇编语言,它也需要“反汇编”才能将ROM中的字节序列翻译成人类看得懂的代码;同样,脚本也需要“汇编”才能翻译成ROM能看得懂的字节序列。

    举个例子,在原版ROM中,有一个脚本的名字叫做PlayersHouse_1F_EventScript_MomHealsParty,可以在符号表中找到它在ROM中的地址,也可以在源代码项目中找到它的定义:

    这个例子是说,上半边那些用英文写成的脚本语言,翻译过来之后就是下半边ROM中的字节序列;或者反过来,下半边ROM中的字节序列,翻译过来之后就是上半边的脚本。

    这个脚本是做什么的呢?它的功能是:出现一条“主角妈妈让主角休息一下”的对话,然后治愈主角队伍中的所有精灵。

    本来,这个脚本只会在主角回到家里和妈妈对话的时候才会执行,如果有办法可以让金手指执行这个脚本,就可以随时随地回复精灵,这就是网上流传的“随身回复”金手指的原理,在之前的一期专栏究极绿宝石5.3——科普向,什么是金手指(十)详细说到了这种金手指应该怎样去找。

    为了理解上面那个脚本是什么含义,以及脚本语言是怎么翻译到字节序列的,下面专栏将介绍脚本的语法。

绿宝石脚本的语法

    在源代码项目的data/scripts文件夹内(script就是脚本),有大量的以inc为后缀名的文件,里面定义了原版绿宝石的所有脚本:

scripts文件夹内的某个文件

    其中每个inc文件都包含了一系列脚本,这些脚本的语法非常简单:

  • 第一行:脚本名称,后面跟上2个英文冒号

  • 剩下的行(脚本主体):每行的格式是“指令符 参数1(, 参数2, ……)”

    脚本主体就是由一行一行的接口调用组成的,例如上图中出现的setvar, return, msgbox等都是绿宝石ROM提供的接口,同时也是脚本语言中的指令符。指令符可以没有参数(例如return, end等),也可以有一个或多个参数,指令符和参数之间用空格隔开,参数之间用英文逗号隔开

    之所以说这些指令符都是接口,是因为对于某些指令符,在它们前面加上ScrCmd_前缀,就是绿宝石源代码项目里的一个函数;而剩下的指令符都是由这些有对应函数的指令符拼接得到的。以setvar为例,它对应了ScrCmd_setvar函数:

setvar指令符对应的接口

    也就是说,当脚本执行到setvar所在的一行,就相当于ROM程序调用了ScrCmd_setvar函数。在这个函数所在的scrcmd.c文件中,定义了所有指令符对应的接口。

    不过,有的指令符没有直接对应的接口,例如msgbox,无法找到ScrCmd_msgbox这个函数,这是因为msgbox指令符是由其他指令符拼接得到的。在源代码项目的asm/macros中的event.inc文件,定义了每个指令符应该如何翻译到字节序列,例如msgbox:

msgbox指令符的定义

    event.inc定义了所有指令符的含义,这里可以看到msgbox指令符是由loadword和callstd两个指令符拼接得到的,msgbox的两个参数text和type分别用于loadword和callstd。而那些直接对应到ScrCmd函数(不是由其他指令符拼接成的)指令符,它们的定义就说明了怎样将脚本语言翻译为字节序列:

setvar的定义

    还是以setvar为例,它需要两个参数:destination和value,setvar这个指令符自己占一个字节(.byte),并且这个字节就是0x16;destination和value各自需要2个字节(.2byte)。也就是说,一个完整的setvar指令需要5个字节,在翻译成字节序列时,就按照脚本中给出的顺序(指令符 参数1,参数2,……)翻译即可。

    这样一来,回到刚才那个和妈妈对话的例子:

    我们就可以“人工”将下面的字节序列对应到上面的脚本:

  • msgbox是两个指令符loadword和callstd拼接成的,所以翻译成2行。0F对应loadword,后面的00是loadword的第一个参数,081F7D08是第二个参数;09对应callstd,04是callstd的第一个参数。

  • goto对应05,后面的脚本PlayersHouse_1F_EventScript_HealParty对应08092A9E

  • end对应02

    可以看到,下面的那一行字节序列被拆成了若干行,每行字节序列和一行脚本对应,并且字节序列的顺序严格按照脚本的顺序排列。为了表述清晰,作者在上面的字节序列中加上括号和竖线,这样和上面每行脚本的对应关系更好理解。

    将字节序列翻译成脚本的道理是一样的,就是按照顺序依次将每个字节对应到指令符或者指令的参数。

    这样一来,如何修改脚本的问题就完全变成了理解每个指令符接口的问题。

XSE工具

    将脚本转换为字节序列,或者反过来,是一个非常机械性的工作,将它交给电脑而不是人来处理比较合适。XSE就是一种这样的工具。

    XSE是eXtreme Script Editor(超级脚本编辑器)的简称,它是一个好用的绿宝石ROM脚本编辑器,以至于许多网上关于绿宝石脚本的教程都把脚本称作“XSE脚本”,其实XSE只是一个工具的名称。官方网址位于:

    https://www.hackromtools.info/xse/

    在XSE程序中,“帮助”菜单栏的作用很大,首先它给出了每个字节对应到的指令:

XSE对脚本指令符的介绍

    这个列表里面有对于每个指令符的详细介绍,功能和源代码项目中的event.inc文件类似。

    有关XSE的功能本专栏不再多做介绍,它的使用方法在“帮助”菜单里已经写得很清楚了。

脚本引擎

    刚才只是介绍了:执行脚本其实就是执行脚本指令符对应的接口。这是一个原理性的说明,真正在代码中实现,需要依赖一个被称作脚本引擎的代码。

    脚本引擎的作用是为执行脚本提供必要的准备工作。比如说将读入的字节序列转换为可执行的脚本接口;脚本存在函数调用时,控制程序调用栈;控制执行哪条脚本等等。原版绿宝石的脚本引擎位于src/script.c文件中:

脚本引擎所在的文件script.c

    上图显示的InitScriptContext函数就是脚本引擎的初始化函数,这里把结构体ScriptContext(脚本上下文)作为脚本引擎本体,第33行的ctx->scriptPtr就是当前脚本引擎需要执行的脚本,初始化时定义为空指针。参数cmdTable是一个函数指针的数组,它的常见取值是gScriptCmdTable,里面包含了所有脚本指令符对应的接口函数:

gScriptCmdTable的定义

    这里我们最关心的就是ctx->scriptPtr(脚本指针),因为它控制着当前会执行什么脚本,只要把脚本地址赋值给这个变量,剩下执行脚本的事情就交给脚本引擎了,我们无须关心其中的细节。这里的脚本指针,其实就是之前专栏究极绿宝石5.3——科普向,什么是金手指(十)中提到的“对话程序”,当时找到的地址03000e48同时也是原版绿宝石脚本指针的地址,向这里写入脚本地址就可以执行对应的脚本。

    03000e48这个地址的来源是这样的。在符号表中找到sGlobalScriptContext这个符号(奇怪的是,它在源代码项目中并没有对应的变量,只能将它暂时看作sScriptContext1),它的地址位于030000e40。然后去看结构体ScriptContext的定义:

ScriptContext结构体的定义

        里面的scriptPtr位于第15行,需要计算一下它的相对地址(在这里,源代码项目并没有“好心地”给出每个成员变量的相对地址)。

  • stackDepth,相对地址0x00,u8是1个字节;

  • mode,相对地址0x01;

  • comparisonResult,相对地址0x02;

  • nativePtr,相对地址0x03(?),它的类型是函数指针,4个字节

    在上面,nativePtr的相对地址是0x03吗?作者打了一个问号。实际上,因为它是4字节变量,根据数据对齐的概念(在口袋妖怪绿宝石——数据提取与代码分析(2-基于名称列表的详情信息提取)提到),它的相对地址其实是在0x04,有一个字节的空间必须因为数据对齐浪费掉。

    再下一个变量就是脚本指针scriptPtr了,它的相对地址应该是0x04+4=0x08。所以,脚本指针的地址就是03000e40+0x08=03000e48。接下来拼接金手指的方法已经在上一系列专栏的最后一期究极绿宝石5.3——科普向,什么是金手指(十)讲过了。

利用Advance Map查看脚本地址

    脚本利用最多的地方,就是地图中与各个NPC的对话所触发的事件。而地图信息用Advance Map查看再方便不过,下面用Advance Map来打开原版绿宝石的ROM:

Advance Map打开原版绿宝石

    上图是古辰镇的地图,在古辰镇的中心有一个路牌,我们可以切换到“事件”选项卡(上图有“地图-运动许可-事件-野生精灵-地图头”5个选项卡),在右侧菜单里找到这个路牌对应的脚本是什么:

路牌的脚本信息

    右侧的“事件”菜单栏可以选择当前地图内的脚本类型,有人物事件、出入口、脚本、标志和飞行点。选择“标志”(右上角的“标志[03]”),可以看到编号00的标志就是古辰镇的路牌,在左侧的地图中用红色的框标注出来(地图中间的S)。它对应的脚本地址是001E8EEA,可以到源代码项目中去找081E8EEA,发现它对应的符号是OldaleTown_EventScript_CitySign,定义如下:

古辰镇路牌对应的脚本

    这个脚本只进行一个操作:利用msgbox弹出一个对话框,显示OldaleTown_Text_CitySign代表的文本。

    这样一来,通过观察在游戏中和NPC对话导致的剧情走向,和源代码项目中(或者通过XSE翻译)的脚本定义,可以很直观地理解每个脚本指令符的实际意义,知道这个脚本究竟做了什么事。

    除了和NPC的对话,地图上的各种道具(隐藏的或者不隐藏的)、对战训练师、利用秘传机进行的砍树、碎岩等都是脚本包含的范畴,所以才说,脚本信息也是ROM数据提取的一大重点,它包含的信息量远远超过了文本信息和对应的详情信息,不过还是要以文本信息和详情信息为基础,提取出来的脚本才能更容易看懂。

    对于改版的ROM,如果把所有的脚本信息都提取出来,那么它的剧情流程、隐藏道具等等就会一目了然。然而目前作者并没有找到把这些信息都提取到一个文件内的好用工具,只好自己写了一个parser,配合已经提取出来的文本信息,可以对游戏有一种“上帝视角”般的认识。下图为对某个改版ROM进行的脚本提取示例:

脚本信息片段

    在这种文件里查找隐藏道具(或者是某个你想要但不知道在哪的道具)非常方便。有了全部的脚本信息,贴吧里面问“什么什么道具在哪”的问题就再也不是问题了。可惜的是写一个parser对编程水平有一定的要求,并不是本系列专栏的重点,也许将来会有一天作者会将这种parser的思路放在另一个系列的专栏里。

举个例子:古辰镇与路牌对话遇到特定精灵的金手指

    了解绿宝石游戏脚本运行的原理之后,就可以做一些比较“另类”的金手指,这里以网上一个“古辰镇与路牌对话遇到特定精灵”的金手指为例,介绍它的思路。

    上面使用Advance Map的时候提到,原版绿宝石古辰镇的路牌对应的脚本地址是001E8EEA,如果我们能够“移花接木”,把这个脚本地址替换为其他的脚本地址,就可以在游戏中和路牌对话的时候执行别的功能。

    在HxD中搜索字节序列EA 8E 1E 08,就可以找到是哪里用到了古辰镇路牌脚本:

搜索古辰镇路牌脚本地址

    可以看到是在0852791C这个位置(搜索结果是唯一的)用到了古辰镇的路牌脚本,只要更换这个地址处的4字节变量就可以了。

    接下来就是找一个能和精灵对战的脚本,这里我们选的是120号道路台阶上那只挡路的变隐龙,来到Advance Map看一下它对应的脚本地址:

120号道路台阶上挡路的变隐龙

    这只挡路的变隐龙脚本地址在082722DB,来到符号表查到脚本名称是Route120_EventScript_Kecleon1,转到它的定义:

Route120_EventScript_Kecleon1脚本的定义

    这个脚本会检查背包是否携带得文观测镜(第51行),如果携带了,就跳转到57行的EventScript_AskUseDevonScope,在第58行弹出一个“是否要使用得文观测镜”的对话框,如果选择了“是”,就会在第59行跳转到第63行的EventScript_BattleKecleon,在第76行dowildbattle触发和变隐龙的战斗。

    我们不需要检测得文观测镜,因此直接让古辰镇的路牌指向EventScript_BattleKecleon就可以了(它在符号表中的地址是08272365),也就是说,如果和古辰镇路牌对话的结果是和一个变隐龙对战的话,那么这条金手指就是:

    效果是这样的:

和古辰镇的路牌对话

    对话框结束后进入战斗:

进入和变隐龙的战斗

    如果想要遇到的精灵不是变隐龙,而是其他精灵,就需要先了解精灵代码,这在之前的专栏中提到过如何提取(口袋妖怪绿宝石——数据提取与代码分析(1-字符集与文本信息的提取)),然后看到EventScript_BattleKecleon的第74行:

setwildbattle指令符

    setwildbattle指令符的作用是选择和一只什么样的精灵对战。查看一下setwildbattle的定义:

setwildbattle的定义

    可以看到,setwildbattle共有三个参数,第一个参数是精灵种族,第二个参数是精灵等级,第三个参数是精灵携带的道具。上面的第74行没有第三个参数,是因为这里选择了item的默认值ITEM_NONE(无道具)。

    从定义中可以看到,setwildbattle指令符占用1个字节0xb6;然后的两个字节是精灵种族,原版绿宝石变隐龙的精灵代码是013D;接下来精灵等级占用一个字节,30级是0x1E;最后的道具占2个字节,无道具就是0000。因此,在ROM的08272365地址(EventScript_BattleKecleon脚本地址)向下搜索字节序列:

    它对应的就是

    这一条脚本:

setwildbattle这条脚本所在的地址

    如果想让这个“路牌遇宠”金手指遇到别的精灵,只需要修改0827238D处的两个字节;如果需要修改等级,只需要修改0827238F处的一个字节;如果需要修改这只精灵携带的道具,只需要修改08272390处的两个字节。下面举个例子,是遇到携带大师球的5级烈空坐。

    烈空坐精灵代码0196,5级就是0x5,大师球道具代码0001,结合上面的“路牌遇宠”,需要三行代码实现“与古辰镇路牌对话,遇到5级烈空坐并携带大师球”:

    效果就是这样的:

遇到烈空坐

改版ROM的脚本

    理解绿宝石中的脚本,关键在于理解它的语法。理解了语法之后,就可以不必关心每个脚本指令符具体是怎么实现的,只需要了解它的功能并能够使用就好了(接口的特点)。因此,利用脚本制作的金手指往往会实现很复杂的功能,它充分利用了游戏提供的接口,比仅仅修改数据的金手指显得更“高级”。

    可以看到,每个脚本指令符对应到哪个字节完全是绿宝石ROM自己指定的规则,这和汇编语言不同(汇编语言的规则是由操作系统的架构决定的,对于绿宝石游戏来说就是GBA游戏机的操作系统架构ARM7TDMI)。分析绿宝石改版ROM时,任何GBA模拟器的反汇编功能都能正确地翻译汇编代码,但是脚本就不一定了。如果有的改版修改了脚本指令符和字节之间的对应关系,XSE工具就没有办法提取正确的脚本信息。

    此时,还有一个办法来找到脚本指令符和字节之间的对应关系,那就是找到改版ROM里gScriptCmdTable变量所在的地址,它作为函数指针的数组,下标和函数之间的对应关系就是脚本指令符和字节之间的对应关系。例如在原版中,gScriptCmdTable位于081db67c处,从这里开始的ROM数据呈现出明显的规律:

gScriptCmdTable在ROM中的位置

    每4个字节一组,拼成的4字节变量就是脚本指令符对应的接口函数(以ScrCmd_为前缀的函数)。比如0号下标对应的是081db67c处的CD 92 09 08,如果通过观察080992CD处函数的汇编代码,能够确定它和函数ScrCmd_nop的内容一模一样,那就说明nop指令对应的字节就是0x00。

说在后面:

    这个系列的专栏暂时告一段落。上个系列的专栏,作者还将专栏内容的难度分为“简单、中等、困难和疯狂”,这个系列的专栏就没有必要分难度了,说实话,能指望有多少玩绿宝石游戏的人学过编程呢?

    就找到绿宝石游戏的金手指而言,前一个系列的10期专栏加上本系列的10期专栏,已经够用了。再深入下去,就是改版爱好者探索的范围:改图片、改音乐、改剧情、改战斗机制……这恐怕就不是一个人能够胜任的工作了。

    如果还有下一期的话,会命名为

    口袋妖怪绿宝石——数据提取与代码分析(A-……)

    9的后面是A,相信众位读者不会陌生。介绍什么呢?作者的打算是拿一个真正的改版绿宝石游戏“开刀”,把里面的信息“解剖”出来。不过鉴于许多制作组因二改、倒卖之类的事情停止了更新,作者的专栏位于这种敏感地带,应该学会适可而止,等到哪天改版的环境更好一些了,说不定专栏会有它的后续吧……

    最后,还是要感谢众位读者的支持。也许作者的专栏内容驳杂、组织混乱,也许作者的专栏晦涩难懂、语焉不详,但是,哪怕读者能从其中找到一点点的灵感,作者就相信这些专栏还是有意义的!

口袋妖怪绿宝石——数据提取与代码分析(9-脚本)的评论 (共 条)

分享到微博请遵守国家法律