TwilightRain | 一个汉化包的牌组 bug

一个汉化包的牌组 bug

约 3050 字 · 阅读约 8 分钟

「第一次玩必须先启动一次原版游戏,进去会出现三选一卡牌,选一张就可以退出,之后才能用汉化版。否则卡牌无法正常载入,战斗永远只有三张拳头。」

朋友拿来一份游戏的汉化包,说首次游玩有问题。上面这段话摘自包里附的那份「初次运行游戏必看」文档。

第一次读到它,我觉得这是在甩锅。玩个汉化版还得先跑一遍原版?典型的「反正按我说的做就能用」嘛。可把这句话拆开看,它其实已经把答案说了一半:「必须先跑原版」说明问题出在写存档那条路径上,「之后就能用汉化版」说明汉化版读同一个存档是好的,再加上「三张拳头」这种具体到不能再具体的症状,基本已经指向新游戏初始化了。

文档还给了补救办法:已经卡住的话,删掉 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 个文本资源)。

然后一条条排除:

每一项都干净。

我自己坑了自己一次

资源包由 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.jsondeviceInfo.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 个字节。

评论