前言
在数个月前我听到了这游戏的OP,感觉挺好听的。
然后找了一下这个游戏,发现并没有汉化版,甚至连开坑的消息都没有~~,个人开坑的不算,谁知道整到什么时候~~。
于是乎,我花了2天时间来进行程序的分析及修改。(初版完成)
后来又花了4天时间,对乱码文本进行修复。(解析完善)
最后,加上写这篇文章的时间,便是一周了。
操作环境
系统:macOS+Windows
软件:
AnimED,
010editor,
LordPE,
Python3.6,
ollydbg,
易语言/C/C++/...(能编译windows动态链接库就行)
软件请自行下载/安装,纯windows亦可操作。
资源汉化
资源汉化过程:
-
使用AnimED对游戏目录下arc文件解包。
-
使用AnimED对解包后内容进行解析。(崩溃了) -
使用Python3对解包后内容进行解析。
-
对资源进行汉化处理。
-
使用AnimED对解包后内容重新打包。(也崩溃了) -
使用Python3对解包后内容重新打包。
BGI2_Archive解包
这是汉化文本的第一步,过程如图。
解压出来的都是解密后的文件。
对此我曾尝试用Python来编写解包工具,arc文件解析出来了,能正常解包。
但是里面的文件都被DSC算法(可能是buriko的独家算法)加密了,由于我能力不足,对此没辙了。
虽然AnimED是开源的(delphi写的,pascal语言),我去看了下dsc算法的方法,试着用python抄了下来,可是解析失败,细节太多了,于是就放弃了。
BGI2_Script解析
这就是汉化文本的关键了。游戏的所有文本都存放在这个脚本里面。
看到AnimED里有个Script Tool,我天真的以为这玩意能解析脚本,结果一点,然后就没有然后了,TM的单纯是个按钮。
BGI2_Script结构
使用010editor进行分析。
BGI2_Script指令
还是使用010editor进行分析。
经过一番痛苦的猜测,测试,已知指令如下:
- key为command id
- value['param']为参数数量
{
# body
0: {'param': 1}, # 00 00 00 00
1: {'param': 1}, # 01 00 00 00
2: {'param': 0}, # 02 00 00 00
3: {'param': 1}, # 03 00 00 00, text
4: {'param': 0}, # 04 00 00 00
9: {'param': 0}, # 09 00 00 00
16: {'param': 0}, # 10 00 00 00
17: {'param': 0}, # 11 00 00 00
24: {'param': 0}, # 18 00 00 00
25: {'param': 1}, # 19 00 00 00
27: {'param': 1}, # 1b 00 00 00, end
28: {'param': 0}, # 1c 00 00 00
32: {'param': 0}, # 20 00 00 00
33: {'param': 0}, # 21 00 00 00
48: {'param': 0}, # 30 00 00 00
56: {'param': 0}, # 38 00 00 00
63: {'param': 2}, # 3f 00 00 00
127: {'param': 2}, # 7f 00 00 00
320: {'param': 0}, # 40 01 00 00
# foot
249: {'param': 0}, # f9 00 00 00
244: {'param': 0}, # f4 00 00 00
}
BGI2_Script数据
BGI2_Script主要包含指令数据以及文本数据(包含BSB)。
指令数据
首先按结构图的结构从开始读到Package List,读完List,取得当前位置为code_pos。
读取Command Code - head,参数一为未知,参数二为code_foot_position。
然后按照前面的指令字典,从当前位置一直读到code_foot_position,code_body部分就解析完成了。
code_body部分解析完成时,当前stream的位置必须与code_foot_position相等,否则这个文件也许不是剧本的脚本。
然后开始读code_foot部分,方法和读code_body一样。
顺便读取BSB(~~其实这个没用,~~重新封包加上)。
文本数据
然后根据之前读取到的command,筛选出command id为3的command(使用文本的命令)。
再在筛选出的内容里筛选出翻译的内容。
这buriko是真的操蛋,文件路径都在里面,否则就不用筛选了。
-
角色谈话文本的下一位Command id一般为320(40 01 00 00)。
-
角色名称文本的Command在角色谈话文本之前。
-
角色线路选项文本的下一位Command id一般为9(09 00 00 00),下两位为2(02 00 00 00)。
-
错误文本在code_foot里,下一位Command id一般为249(f9 00 00 00),下两位为244(f4 00 00 00)。
符合以上条件的文本都需要翻译(translate)。
我筛选的方法如下:
for i in range(len(code_list)):
code = code_list[i]
if code['cmd_id'] == 3:
p0 = code['param'][0]
next_cmd_id = code_list[i + 1]['cmd_id']
next_2_cmd_id = code_list[i + 2]['cmd_id']
character_text = next_cmd_id == 320
character_name = next_cmd_id == 3 and next_2_cmd_id == 320
character_select = next_cmd_id == 9 and next_2_cmd_id == 2
error_message = next_cmd_id == 249 and next_2_cmd_id == 244
trans = character_text or character_name or character_select or error_message
BGI2_Archive封包
还是还是使用010editor进行分析。
Archive的结构是十分的简单,如下图所示。
由于过于简单,不做解释。
程序修改
绕过检测
由于原程序是japan only的,所以实际上我们并不能运行。
方法一
使用ollydbg对原程序进行修改。
按下ctrl+n,在弹出的api列表中寻找GetSystemDefaultLangID,然后按Enter查找调用位置。
在简体中文环境下,GetSystemDefaultLangID 返回值是 0x804,而在日语环境下,返回值是 0x411。
按空格对代码进行修改,直接返回0x411。
修改前:
修改后:
右键模块窗口,对修改进行保存:
方法二
使用apihook对程序进行注入。
用易语言编写一个动态链接库,在启动时把KERNEL32的GetSystemDefaultLangID给hook掉,修改返回值为1041(0x411)。
当然,也可以用C/C++来写这个动态链接库~~,只是我不会而已~~。
然后使用LordPE进行静态注入,即把该dll添加到程序的入口(随便做个方法就好,比如我这是test),这样程序运行时便会自动被dll把api hook掉。
修改文本显示
虽然前面的资源汉化了,但是程序本身是日文环境的,读取汉化资源就会导致乱码,所以就要做以下的修改。
修改编码
由于我并不熟悉OD,所以只能以hook的方式来修改。
和之前的hook同理,这次hook的方法是以下两个:
- GDI32的CreateFontA,把fdwCharSet参数修改为134(0x86),同时可以在此处hook字体的参数,做个配置项,就可以随时更换字体了。
- KERNEL32的MultiByteToWideChar,把CharSet参数修改为936(0x3A8)。
以上方法任一没有hook,都会导致乱码。
修改字符边界
编码修改了,但是程序还是乱码,这是由于字符边界的关系,把某些字符过滤掉了。
- gbk字符边界: 0x80-0xFE
- shift_jis字符边界:0x80-0xA0
这里需要使用OD对程序进行修改。
按下ctrl+f进行搜索,内容为cmp al,0xA0。
然后把所有见到的cmp al,0xA0都修改成cmp al,0xFE。
这样子程序就能正常加载gbk编码的资源了。
退出崩溃
由于进行了hook,程序退出的时候,hook找不到原来的地址发生了崩溃。
由于能力不足,没法从源头解决。所以我选择把这个问题堵掉。
还是hook,这次把USER32的DestroyWindow hook掉,当destroy的句柄为游戏的窗口时,强制把自身的进程杀掉,于是关闭就不会崩溃了~~,嗯,完美~~。