「第一次玩必须先启动一次原版游戏,进去会出现三选一卡牌,选一张就可以退出,之后才能用汉化版。否则卡牌无法正常载入,战斗永远只有三张拳头。」
朋友拿来一份游戏的汉化包,说首次游玩有问题。上面这段话摘自包里附的那份「初次运行游戏必看」文档。
第一次读到它,我觉得这是在甩锅。玩个汉化版还得先跑一遍原版?典型的「反正按我说的做就能用」嘛。可把这句话拆开看,它其实已经把答案说了一半:「必须先跑原版」说明问题出在写存档那条路径上,「之后就能用汉化版」说明汉化版读同一个存档是好的,再加上「三张拳头」这种具体到不能再具体的症状,基本已经指向新游戏初始化了。
文档还给了补救办法:已经卡住的话,删掉 AppData\LocalLow\AleCubicSoft 下的存档重来。能靠删存档恢复,说明坏掉的状态确实落在存档里。
方向没错。走过去花的时间比预想长得多。
#一、先把两个目录摆在一起
第一步便宜得离谱,产出却最大:把汉化版和原版的目录摆在一起比。
| 文件 | 汉化版 | 原版 |
|---|---|---|
GameAssembly.dll |
32,678,912 | 同 |
UnityPlayer.dll |
29,952,536 | 同 |
baselib.dll |
418,840 | 同 |
boot.config |
180 | 同 |
data.unity3d |
258,513,497 | 257,817,456 |
global-metadata.dat |
7,264,092 | 5,470,096 |
三个 DLL 和构建配置逐字节相同。代码一个字都没改,嫌疑范围瞬间收缩到两个数据文件上。global-metadata.dat 是 IL2CPP 的元数据,装的是类型名、方法名和所有字符串字面量;翻译器把那些字面量改掉,它自然膨胀 1.79 MB。
顺手还确认了一件事:汉化版根目录根本没有 DeadEndCity.exe。这份整合包是坏的,连跑都跑不起来。从原版复制一份过来,游戏开了,bug 原样复现,至少说明它跟缺不缺 exe 无关。
#二、一路绿灯的静态取证
装上 UnityPy,又手写了个 IL2CPP 元数据 v29 的解析器(字面量表和标识符表的偏移分别在头部 offset 8 和 0x18,字面量条目表是 (长度, 偏移) 的 8 字节对)。材料齐了以后,能查的我都查了。
字面量一共 10017 条,1987 条被翻译过。资源包里 8793 个对象没有增删,其中 185 个内容变了(183 个 MonoBehaviour、1 张贴图、1 个文本资源)。
然后一条条排除:
- 占位符被翻译搞坏?数字和命名占位符错配 0 处,
string.Format不会抛异常 - 数据键被误译?纯 ASCII 的字面量一处都没改
- 重打包破坏了引用?28 个内部文件名、8793 个 pathID 全部一致
- 卡牌数据被写坏?逐字节比对卡牌对象,数值字段(枚举、整数、数组长度)原封不动,只有字符串变短
- 贴图名对不上?卡牌键和 Sprite 名字之间没有任何对应关系
每一项都干净。
我自己坑了自己一次资源包由 28 个 SerializedFile 组成,不同文件里的 pathID 会重复。我拿 pathID 当字典键做统计,8793 个对象被折叠成 7075 个,得出一个「114 个对象被改动」的假数字,还基于它推理了很久。
发现它纯属运气:我注意到 MonoScript 的数量莫名其妙掉到 0,才回去查。看起来合理的错误结论,比找不到答案危险。
#三、跑起来看真实运行
在静态分析里绕了很久,我才去做早该做的那件事:把游戏跑起来,两边对照。
清空存档,跑一遍原版,进新游戏,选一张卡,进战斗。手牌 5 张:リロード、手癖、攻撃、攻撃、防御,牌堆计数 5。总共 10 张牌。
同样清空存档,跑一遍汉化版,同样选一张卡,进战斗。手牌 3 张,全是拳头,牌堆计数 0,总共也是 3 张。三选一里选的那张稀有卡压根没进牌组。
一屏截图就把整个问题框定了,而且直接对应文档里那句「战斗永远只有三张拳头」。这句话我早该当成线索,而不是当成甩锅。
#四、那 5 条字面量
原版起手的 5 张牌,对应元数据里恰好 5 条独立字面量。这个对应关系我之前扫到过,当时没意识到它就是答案。
| 字面量 | 对应卡牌 | 汉化版 |
|---|---|---|
Attack |
攻撃 | 未改 |
Defense |
防御 | 未改 |
リロード |
リロード | 改成「重新加载」 |
手癖 |
手癖 | 改成「顺手牵羊」 |
隠れる |
隠れる | 改成「隐藏」 |
游戏用这些字符串字面量作为卡牌内部键。资源包的卡牌表里,键仍然是日文,汉化组在这件事上做对了,说明他们清楚这个字段是标识符。出问题的是元数据里那三条:翻译工具把它们当普通文本译掉了,多半看不出这些字符串在别处还承担着引用。
查表落空,新游戏初始化中断,牌组退化成三张兜底牌。
这也解释了为什么「先跑原版」能绕过去:原版会写出一个合法的存档,汉化版之后只是读取它,不再走构建牌组那条路。文档里那句听着像甩锅的话,其实是在描述一个真实的规避路径。
#五、27 字节的补丁
修法很小:把那三条字面量还原成日文原文。
具体做法是往字面量数据块末尾追加三个字符串,改这三条的索引(长度 + 新偏移),再把头部的 stringLiteralDataSize 加上 27。其余 10014 条字面量一个字节都没动。补丁后的文件和汉化版原文件只差这三处,而且这三处现在和原版逐字节相同。
重新跑一遍验证:清空存档,不跑原版,直接启动汉化版,选卡,进战斗。手牌 5 张(领域展开、防御、重新装填、躲藏、顺手牵羊),牌堆计数 5,总共 10 张。三选一选的「领域展开」在手牌里。界面文本翻译一点没受影响。
#六、我判错的一次
修完之后我注意到汉化版没生成 config.json 和 deviceInfo.json,而原版那次运行生成了。我把它当成残留缺陷写进了报告。
这是错的。朋友让我继续追,我才去建了个像样的对照:清空存档,让原版和汉化版走完全相同的流程,标题、进入新游戏、三选一、地图、对话、战斗、正常退出,每一步都记录文件状态。
结果是两边行为完全一致:都只生成 save.dat,大小变化也一样(选完卡 400 字节,战斗后 416 字节),运行日志里一个异常都没有。
又补了两条证据。朋友这台机器上,我介入之前的存档目录里本来就只有 save.dat;注册表 HKCU\Software\AleCubicSoft\DeadEndCity 下只有 Unity 自己的屏幕和画质项,没有任何游戏设置。这游戏本来就不写那两个文件。
我犯的错是拿单次观测当结论。跑一遍对照就能证伪,五分钟的事,我却先写进了交付报告。发现不了根因是能力问题,这个不是。
#七、尾声
最后把修复做成了自包含的补丁包:一个打补丁脚本、一份打好的元数据、那个缺失的 DeadEndCity.exe、说明文档和校验和,压缩后 2.4 MB。
补丁包在这里,需要的自取:DeadEndCity_fix.zip(2.4 MB)。用法、参数和已知限制都写在包内的 README.md 里。
| 项目 | 值 |
|---|---|
| zip 校验和 | ad9ee7c4f87d9302ad2283be51a90bf50cf236cc2dca4c6121c1ebb5906fa315 |
| 包内校验 | SHA256SUMS.txt,覆盖打补丁脚本、元数据、exe 与说明文档 |
脚本做了几件事来保证它不会帮倒忙:
- 幂等,已修复的文件会被识别并跳过
- 改之前自动备份
- 先校验,目标字面量的内容和预期对不上就直接中止,不会把补丁打到别的汉化版本上
做了七项测试加一次端到端,对一个模拟出来的「未修复且缺 exe」的全新汉化版跑了一遍,它正确补上了 exe、打完补丁、复核通过。
绕过这个问题也有别的办法:塞一个预置存档、在代码里加兜底、或者继续在说明里写「请先跑原版」。那三条都只是把症状盖住,改字面量是把查表本身修好。
整个修复最后动到 27 个字节。
评论