汉化完了《数码宝贝物语 遗失的进化》,群里有人问接下来该做什么,提到了这个游戏。于是就来研究一下。
文本提取
拆包的流程和大多数NDS游戏一样,这里就不重复说了。拆包之后照例在/data/
文件夹下查找可能是文本的文件,但是搜了一圈也没有找到。怎么办呢?直接运行试试。
打开DeSmuME模拟器,在出现文本时暂停,使用“Tools”→“View Memory”→“Dump All”可以把内存转储出来,得到一个15.0 MiB的文件。根据DeSmuME的源代码,内存数据在转储文件和NDS主机内存中的对应关系大致如下表所示:
转储文件地址起始 |
大小 |
含义 |
0x000000 |
8192 KiB |
ARM9 主内存 |
0x900000 |
16 KiB |
ARM9 DTCM |
0xA00000 |
32 KiB |
ARM9 ITCM |
0xB00000 |
656 KiB |
LCD |
0xC00000 |
2 KiB |
调色板 |
0xD00000 |
64 KiB |
ARM7 WRAM |
0xE00000 |
64 KiB |
ARM7 Wifi RAM ? |
0xF00000 |
32 KiB |
ARM9/ARM7共享 WRAM |
(注:对于0xC00000
处的数据,源代码中的注释说是OAM,但实际上转储的数据是调色板,这里给出的是实际转储出的数据)
然后用HxD打开搜索文本序列。考虑到这游戏是2005年发售的,编码方式很可能用的是Shift-JIS,所以将要找的文本序列转成Shift-JIS编码然后搜索。
游戏中最初的一处文本
作为示例,根据图片里的显示搜索“データのコピー”,转成Shift-JIS编码之后是83 66 81 5B 83 5E 82 CC 83 52 83 73 81 5B
。搜索这个字节序列,果然在内存中找到了,位于0xEDA6A
位置:
- Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
- 000EDA60 01 07 81 40 81 40 82 C5 81 40 83 66 81 5B 83 5E ...@.@‚Å.@ƒf.[ƒ^
- 000EDA70 82 CC 83 52 83 73 81 5B 0A 1A 06 05 00 01 08 81 ‚̃Rƒs.[........
- 000EDA80 40 81 40 82 C5 81 40 83 66 81 5B 83 5E 82 F0 82 @.@‚Å.@ƒf.[ƒ^‚ð‚
往上翻一翻,可以看到一个熟悉的字节序列:MESGbmg1
,这是NDS游戏很常用的一个文本格式,有现成的工具可以用。因为我喜欢用Python写脚本,所以可以直接调用ndspy库来解析。那么,文本究竟藏在了什么地方呢?
如果对NDS的内存结构比较熟悉的话,其实可以发现这个内存地址是比较靠前的。而这个游戏的arm9.bin
有1.36 MiB,换成十六进制是0x15DBA4
,所以这些文本实际上是被硬编码在了arm9.bin
里。NDS早期的很多公司估计还保留了GBA时代的开发习惯,喜欢把数据都放在可执行文件里,之前研究过的《Another Code 两种记忆》也是如此。
既然已经知道了文本的位置,提取也变得简单了。直接在arm9.bin
中搜索MESGbmg1
这个文件头,由于文件头的后面4个字节就是大端序编码的文件长度,所以很轻松就能全部提取出来。
字库分析
本以为这游戏文本用了常见格式,字库也会用常见格式。但是我不管是在arm9.bin
里找还是在/data/
文件夹下找都没找到字库,搜索fnt
、font
之类的词也没有找到。由于这个游戏的汇编代码里有很多通过寄存器跳转的指令,静态调试有些头疼。怎么办呢?动态调试吧。
上一步找到了文本所在的内存,省了大事了。继续打开DeSmuME模拟器,使用“Tools”→“View Memory”,在0x20EDA6A
这个内存地址下一个读取断点。通过“Tools”→“Disassmbler”可以查看汇编代码和寄存器的值。经过一番排查,最后锁定在了0x206E3DC
这里,汇编代码是这样的:
- loc_206E3DC
- LDRB R5, [R1]
- MOV R2, R2, LSL#16
- LDRB R3, [R3, #1]
- MOV R1, R2, ASR#16
- LDR LR, [LR, #0x20]
- ORR R2, R3, R5, LSL#8
最初R1
和R3
寄存器中的地址都是0x20EDA6A
,在这番处理之后,R2
寄存器中的值是0x8366
,恰好是デ
这个字的Shift-JIS编码。为了验证这里是否就是用于文字显示的代码,手动修改这个寄存器的值为0x8396
(ヶ
),然后点击“Update Registers”保存,取消断点继续运行:
修改寄存器后的运行结果
显示出来的文字确实修改了,说明这里就是用于文字显示的代码。继续往下追踪,直到发现0x206EDD8
这处函数:
- sub_206EDD8
- AND R2, R1, #0xFF
- MOV R1, R1, ASR#8
- SUB R3, R2, #0x40
- AND R2, R1, #0x3F
- MOV R1, #0xC0
- MLA R1, R2, R1, R3
- MOV R1, R1, LSL#16
- LDR R2, [R0, #8]
- MOV R0, R1, LSR#16
- LDR R0, [R2, R0, LSL#2]
- BX LR
来分析一下这段代码做了什么:
寄存器R0
和R1
分别是这个函数的两个输入参数,R0 = 0x229CCC8
,R1 = 0x8396
。然后将R1
的低8位和高8位分别运算,低8位(R3
)减去0x40
,高8位(R2
)和0x3F
做与运算(相当于取了低6位)。接着将R3
乘以0xC0
,然后加上R2
,保存在R1
中。也就是说,这里实际上是将字形的偏移量计算出来了。随后将[R0 + 8]
这个地址处的指针读取到R2
寄存器(0x20EE3A8
),然后读取[R2 + R1 * 4]
这个位置的数据作为函数返回值(0xEDF8
)。伪代码:
- uint32_t func(void *struct_ptr, uint32_t encoded_index) {
- uint8_t low = encoded_index & 0xFF;
- uint8_t high = (encoded_index >> 8) & 0x3F;
- int32_t adjusted_low = low - 0x40;
- uint32_t index = adjusted_low + high * 0xC0;
- uint32_t *array = *( (uint32_t**)( (char*)struct_ptr + 8 ) );
- return array[ (uint16_t)index ];
- }
为什么要这么处理呢?查看Shift-JIS的编码方式可知,双字节编码时高8位的范围应该是0x81-0x9F
和0xE0-0xEF
,低8位的范围应该是0x40-0x7E
和0x80-0xFC
,所以上面的算法相当于是比较节省空间的一种存储方式。但是到这里还没分析完,根据上面的分析,每个字符分配的空间只有4个字节,但是游戏中字符的大小是8x16像素(128像素),至少也要16个字节(128位)才能存储。继续追踪这个函数的返回值,发现了调用它的地方:
- sub_206ED0C
- PUSH {R4, LR}
- LDR R2, [R0]
- MOV R4, R0
- LDR R2, [R2]
- BLX R2
- LDR R1, [R4, #0xC]
- ADD R0, R1, R0
- POP {R4,LR}
- BX LR
这里BLX R2
是调用了sub_206EDD8
这个函数,返回值保存在R0
寄存器中。接着读取[R4 + 0xC]
这个地址处的指针(0x20EE3A4
),然后将这个指针加上R0
的值,最后返回(0x20FD19C
)。也就是说,实际上上面的顺序保存时保存的数据是另一个偏移量,真正的数据在后面。总结如下:
- 字符编号的计算方式:
index = (low - 0x40) + (high & 0x3F) * 0xC0
- 字库数据的开始地址:
0x20EE3A4
- 字符偏移量列表的开始地址:
0x20EE3A8
(第n
个字符的偏移量是0x20EE3A8 + n * 4
)
- 字符数据的开始地址:
0x20EE3A4 + 字符偏移量
Shift-JIS双字节编码的第一个可见字符是顿号(、
,U+3001),Shift-JIS编码是0x8141
。计算可知其编号为0xC1
,偏移量的地址为0x20EE6AC
,此处的数据为0xC026
,因此字符数据的地址为0x20FA3CA
。为了验证是否正确,在HxD中查看arm9.bin
,果然找到了字符数据:
- Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
- 000FA3C0 00 00 00 00 00 00 00 00 00 00 08 02 00 00 00 00 ................
- 000FA3D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 000FA3E0 00 00 00 00 0C 00 30 00 D0 00 40 03 08 02 00 00 ......0.Ð.@.....
- 000FA3F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 000FA400 00 00 00 00 00 00 F4 01 0C 03 0C 03 F4 01 08 02 ......ô.....ô...
- 000FA410 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 000FA420 00 00 00 00 00 00 00 00 3C 00 3C 00 30 00 1C 00 ........<.<.0...
可以看到数据的最前面有2个重复出现的字符序列0x08 0x02
(可能代表字符的宽高?),而后面的32个字节便是字符数据。32个字节(256位)存储了8x16像素(128像素)的数据,因此每个像素的位深度是2bpp。采用CrystalTile2载入arm9.bin
,设置为Tile视图、偏移地址FA3CA
、宽度8
、高度16
、跳过字节2
、Tile颜色格式4色 2bpp
、绘图模式Tile
、左右对调启用
、水平翻转启用
,可以看到字符的显示效果:
字符数据
接下来呢?
知道了字库的保存方式,接下来就可以动手修改了。但是有个问题:由于字库是硬编码在arm9.bin
中的,对数据大小比较敏感,原本的字库只保存了448个字符。如果要替换为汉字,这么小的字库肯定是不够用的。那么该怎么办呢?且听下回分解。
《超级碧姬公主》汉化笔记(二):字库扩容 »
《超级碧姬公主》汉化笔记(三):图片导出 »
《超级碧姬公主》汉化笔记(四):数据压缩 »