GTA:VC罪吧汉化组存档不兼容原因解析及修复
写在开头
这里给出本文思路的 C#实现
BakaFT/sv2b: Remove illegal characters from GTA:VC savefiles (github.com)
存档的“单向兼容”现象
用过汉化的朋友们可能会注意到,汉化版的存档后缀名发生了变化,即变为了.sv
并且,原版存档可以改个后缀直接在汉化版使用,但是反过来却不行,游戏甚至会直接卡死
这是为什么?下面进行分析
逆向分析
代码逻辑参考 GTAModding/re3
由于 DMCA takedown,目前仓库无限期停止公开访问
游戏的存档/读档逻辑
下面是游戏存档时的操作:
1 | // from GTAmodding/revc |
可以看到,savename
最终是.b
结尾的
同理,读档的时候也是这样,并且只扫描使用该后缀的文件
汉化补丁的相关修改
这是gta_cn.asi
的某个函数,实现了对b
到sv
的替换:
1 | void *__cdecl sub_10001FD0(int a1) |
所以,原版与汉化会互相忽略掉对方的存档
那为什么原版的可以给汉化用,反过来就崩溃?因为问题在于存档文件内容,而非后缀名
存档的二进制内容
存档名
根据 Saves (GTA VC) - GTAMods Wiki 可以了解到
文件中,[0x0004,0x0033]
这长为48
字节的区域为存档名
你可以在src\save\GenericGameStorage.cpp
中看到这部分的文件结构与操作逻辑
可能是出于日本市场的原因,R*在游戏中提供了 Unicode 的支持,GXT/Japanese - GTAMods Wiki 中给出的字符表似乎就是SHIFT_JIS
编码表
可以看到存档的名字其实是一个wchar
类型的数组,这与存档中每2
个字节表示一个字符相符合
1 | // 最后一次完成的任务名 |
接下来是存档名的写入过程,在这之前,你可能需要了解一下什么是 GXT
GXT 格式是一种加密的文本,通过类似于字典(Key - Value)的方式,实现了游戏文本的映射,其初衷是方便游戏的本地化工作
可由类似如下的形式表达
1
2 [ITBEG]
In the beginning也就是说
TheText.Get("ITBEG") == "In the beginning"
想深入了解可以查阅 GXT - GTAMods Wiki
1 | // 如果完成过任务,则获取最后一个完成的任务的名字,否则获取[ITBEG]对应的字符,即`In the beginning` |
编码
而引起游戏崩溃的关键就在于获取任务名这一步
游戏的字库是通过一张保存了所有字符的大贴图实现的,每个字符都有自己的唯一编号,存档文件中即使用这个编号表示
比如英文原版中,The Party
在存档中是这样保存的,其实就是 ASCII 编码
1 | 5400 6800 6500 2000 5000 6100 7200 7400 7900 |
对于英文版来说,使用 ASCII 码是十分方便的,而对于拉丁语系的其他语言,也仅仅是多加一些符号的问题
但是对于日语和汉语,字符实在太多了,这个时候编号就会变得多起来,但是这个编码表最开始的部分必须和英文版的一样,不然可能会出现应该显示字母A
但显示汉字阿
的情况
现实的计算机操作系统中的编码也是如此,编码不兼容 ASCII 就会出现各种想不到的问题
到这里,就可以解释最初的“单向兼容”现象了
问题解析
在使用TheText.Get(*const wchar)
获取任务名时,是基于当前的游戏的语言设定的。如果使用了汉化补丁,那么获取到的字符编码很大概率对于英文原版来说都是不认识的(除非有纯英文,数字的任务名),这个时候游戏又没有做对应的处理(毕竟汉化是第三方作品),那么游戏就炸掉了
反过来,汉化编码是兼容英文的,所以无论如何都是可以正常显示存档名,然后正确读取的
如何修复存档
知道原理就很轻松的可以想到,将存档名中的非 ASCII 编码去掉就行
但需要注意的是,游戏存档中有一个校验和,储存在存档的最后 4 个字节,使用小端储存方式
算法如下:
设置一个unsigned int
型变量,以单个字节为基本单元,将[0,201824)
区间的所有字节对应值相加,即为校验和,并从201824
开始写入
此处给出 C#实现:
1 | public static void FixCheckSum(string path) |
GTA:VC罪吧汉化组存档不兼容原因解析及修复
https://bakaft.github.io/2021/09/02/GTA-VC罪吧汉化组存档不兼容原因解析及修复/