最近发现一款游戏 心流小筑(Chill Pulse),本质上是一个帮助专注的游戏,有播放音乐,番茄钟等等的功能,但是截至写这篇文章的时候,游戏还没有添加音乐的功能,只能播放内置的几首歌曲,对我来说较为不方便,所以尝试进行一下逆向。
相关技术搜索
先大致浏览了一下游戏目录,除了一堆音乐ogg文件以外,可以看到 data.win
文件,查找了一下发现是GameMaker游戏引擎的东西,因此可以确认这个游戏是GameMaker开发的。
先大致在互联网上查询了一下GameMaker mod制作相关的项目,发现有以下几个项目:
根据进一步研究发现,UndertaleModTool主要是为Undertale的mod开发做支持,但是他理论上支持所有GameMaker的游戏。
GS2ML是一个基于UndertaleModTool的GameMaker Studio 2 mod开发框架,它主要用到了UndertaleModTool中的反编译器和编译器。
GML语言
通过UndertaleModTool的GUI工具,可以轻松的解包 data.win
并提取其中的资源,这边主要关注游戏的GML脚本语言。
可以看到,游戏的音乐都是在这个函数中定义的, album_list
存放ogg文件名(GameMaker音频资源名), album_name_list
存放实际显示的名称。
添加自定义音乐
理论上来说,只需要通过 GS2ML 在游戏启动前加载我们自己的音频 ogg 资源,再添加到这个列表就行了。幸运的是,GS2ML有充分的Hook支持和AddSound函数,正好可以满足我们的需求。
样例代码如下:
data.HookFunction("all_music", "#orig#()\narray_push(album_list[0], mymusic)\narray_push(album_name_list[0], \"My Music\")");
为自定义音乐单独添加一个分类
上面的代码是将自定义音乐追加到第一个分类(即 Lofi 这个分类)的,为了完美主义,我想再添加一个音乐分类。
再经过查找可以发现,音乐分类的定义在一个 _music_genre
列表中,而且并不在一个独立的函数中,而是被存放于 gml_Object_obj_controller_Draw_64
这个对象的脚本中,这个脚本反编译后一共有3000行,较为棘手。
我尝试直接将反编译后的代码复制到hook的代码里面,但是可能是因为 UndertaleModTool 本身的编译器和反编译器具有缺陷,导致游戏不稳定,使用特定功能会直接导致游戏崩溃,只能另辟蹊径。
可以发现,GML语言编译的结果是字节码,所以我想到直接向原函数中插入字节码,来实现对 _music_genre
的修改。
以下是原 _music_genre
的定义的字节码表示:
理论上只需要多加几行 push.v
字节码,并将 call.i
后面的 argc
修改就好了。
这里发生了一点小插曲,因为我修改之后游戏会直接崩溃,后面发现是因为这个函数中的一些 goto
字节码,在插入代码之后的 JumpOffset
错位,所以需要额外修正。
以下是实现这个功能的部分代码:
var newGenres = new List<string>();
newGenres.Add("Example");
var draw = "gml_Object_obj_controller_Draw_64";
var code = data.Code.ByName(draw);
var codeLocals = data.CodeLocals.ByName(draw);
AsmCursor cursor = new AsmCursor(data, code, codeLocals);
cursor.GotoNext("pop.v.v local._music_genre"))
cursor.index -= 1;
cursor.Replace($"call.i @@NewGMLArray@@(argc={6+newGenres.Count})");
cursor.index -= 6;
foreach( var genre in newGenres)
{
cursor.Emit($"push.v self.{genre}");
}
var cur = cursor.GetCurrent().Address;
for ( int k = 0; k < code.Instructions.Count; k++)
{
var inst = code.Instructions.ElementAt(k);
if (UndertaleInstruction.GetInstructionType(inst.Kind) == UndertaleInstruction.InstructionType.GotoInstruction)
{
if (inst.Address < cur && inst.Address + inst.JumpOffset >= cur)
{
inst.JumpOffset += 2 * newGenres.Count();
}
if (inst.Address >= cur && inst.Address + inst.JumpOffset < cur)
{
inst.JumpOffset -= 2 * newGenres.Count();
}
}
}