《悠刻のファムファタル》简体中文翻译 · 技术复盘与反思
日期:2026-08-08 定位:这是一份“事后之明”的复盘,记录我走过的路、卡在哪、为什么卡、以及现在回头看该怎么走。
一、任务与目标
任务:为 Escu:de 2024 年的 galgame《悠刻のファムファタル》制作高质量简体中文 AI 翻译,要求:简体中文、无乱码、无闪退、角色名统一、对话自然流畅。
表面目标:一份能玩的中文翻译补丁。 深层目标:超越现有 GPT-4o 翻译(用户已有一份,质量差)和花咲夜 Claude-3.5-sonnet 补丁(繁体、有残留、有乱码)——即“更优秀的翻译”。
当时我理解的“优秀” = 简体 + 完整 + 无乱码 + 自然。
二、走过的路(技术历程)
第一阶段:侦察与参考
- 拿到参考补丁:花咲夜机翻组的 Claude-3.5-sonnet 翻译补丁(moyu.moe,解压密码 julixian)。这是关键的情报来源——它证明了
script.bin可以被解包→翻译→重新打包,游戏能读取。 - 找到正确工具:EscudeTools(github.com/Chenx221/EscudeTools),专门处理 Escu:de 双层脚本格式。这比 GARbro 强——GARbro 只能解外层,EscudeTools 能全流程。
第二阶段:解包与数据
- 双层解包:
script.bin(ESC-ARC2) → 381 个.bin/.001文件 → SQLite 数据库。 - 数据结构:每个剧本表 +
__text(UI 文本)+__mess(台词)表。共 32,380 条台词。 - 编码关键:
.001文件用 XOR 0x55 加密,文本按 Shift-JIS 编码。
第三阶段:翻译(这里埋下了祸根)
我犯的第一个方向性错误:把“翻译”外包给了花咲夜补丁。
- 导出 31,165 条日文台词
- 导入了花咲夜补丁的翻译(30,111 条),只做了:繁转简(OpenCC)+ 修复乱码 + 修复日本汉字残留
- 自己真正翻译的只有序章几百条
这个选择在当时看起来“聪明”:省时、复用已验证可用的数据。但后果是:
- 翻译质量被花咲夜锁死(它的局限就是我的局限)
- 引入了花咲夜的字符集问题(SJIS 只能表达 JIS X 0208 汉字)
- 让我在后续反复“与汉化组的成品过不去”
第四阶段:封装
用 EscudeTools 把 GBK 简体的翻译写回 SQLite → 重新打包 → 生成 script.bin。这步技术上跑通了。
第五阶段:游戏加载(真正的泥潭开始)
这里开始了漫长的“让游戏显示简体”的攻坚战:
- 花咲夜 exe + Mai@KF.dll → 能跑,但 Mai@KF.dll 有 DRM(protect.dll/pkutil.dll),且用户目录被杀软隔离
- GPT-4o exe → 能跑(破解过 DRM),但翻译内嵌在 exe,无法替换
- 原版 exe → 有 SoftDenchi 启动检查(乱码弹窗跳网页)
- 自己写 version.dll 代理(hook GetACP/MultiByteToWideChar)→ 反复编译、反复失败
- 发现关键事实:引擎自解密(exe 启动时生成 femme_fatale.log 再运行)、引擎用自定义 SJIS 解码不走系统 API
- 终极发现:引擎原生支持多语言(LOC_CN),只需切换 language
- 生成 zh_cn 字库(PIL + 系统字体渲染 3097 个汉字,114 页)
- 卡死在 language 切换:扫描内存改值会崩溃,patch 点逆向难定位
三、关键卡点与深层原因
卡点 1:引擎自解密机制
现象:原版 exe 会生成 femme_fatale.log(实际是 PE 文件)再从它加载代码运行。
影响:
- 静态逆向被分成两层,
Japanese_Japan.932等关键字符串在 log 里而非磁盘 exe - 磁盘 exe 的导入表 ≠ 实际运行代码的导入表
- 很多基于磁盘 exe 的分析(找 setlocale 调用点、找 GetACP 调用)全部落空
教训:遇到“exe 会自解密”的游戏,第一时间该意识到逆向对象是解密后的代码,而不是磁盘文件。我花了大量时间在磁盘 exe 上分析 GetACP/MultiByteToWideChar 调用点,全是无用功。
卡点 2:引擎自定义 SJIS 解码,不走系统 API
现象:引擎 text.c 的 put_char 里写死了 sjis_to_jis(c0, c1),用 system->language 分支选择解码方式。它根本不调 MultiByteToWideChar / GetACP(诊断证实:0 调用点)。
影响:我花了两天写的 version.dll hook(hook GetACP→936、hook MultiByteToWideChar 932→936、hook _mbsinc)全部无效——因为引擎根本不经过这些 API。
教训:hook 之前必须先确认“目标到底调用什么”。我一开始就应该做“只读诊断 DLL 记录引擎实际调用”,而不是先写一堆 hook 再逐个试错。诊断的成本远低于盲试。
卡点 3:SJIS 字符集的硬限制
现象:引擎默认 LOC_JP(SJIS 解码),而 SJIS 只覆盖 JIS X 0208 的汉字。简体常用字“你/吗/吧/啊/说”等 118 个字符根本无法用 SJIS 编码。
影响:这是数学层面的死结,不是技术不够:
- 花咲夜用繁体 + 日式汉字变体规避(牺牲自然度,还残留 3.4% 日文)
- 我一度想“把我们的简体转成 SJIS 繁体”——但那样显示的还是繁体,且是花咲夜的替代方案
真正的钥匙:引擎其实原生支持 LOC_CN(简体,GBK 解码 + zh_cn 字库),只是 LANGUAGE LOC_JP 被编译期写死。只要切到 LOC_CN,整个简体路径就通了。
卡点 4:language 切换——最后的也是最硬的骨头
找到钥匙却开不了锁。system->language 的赋值:
- 编译期
#define LANGUAGE LOC_JP main.c里system->language = LANGUAGE- 存档
game_data.sys第 4 字节 = language(当前 = 1)
尝试过的方案及失败原因:
| 方案 | 结果 | 为什么失败 |
|---|---|---|
| 扫描内存改 language=3 | 闪退 | 扫描范围太广,改错了内存 |
| 只读扫描定位 | 找不到 | SYSTEMDATA 不在可写内存,或时机/范围不对 |
patch exe 的 mov [reg+4],1 |
未实施 | 逆向成本高,无法精确定位是哪一处 |
| hook ReadFile 改存档流 | 无效 | 引擎用自定义文件读取,不走 CreateFile/ReadFile |
| hook GetACP/MBW | 无效 | 引擎自定义 SJIS 解码,不走系统 API |
这本质上是“缺一个可靠的进程内 hook 时机 + 精确的内存定位”。我需要的是:
- 在
system结构 malloc 之后、第一次读取之前,拿到system指针 - 或 hook 引擎的
sysfile(读档函数)——但它是引擎内部 trap,不在导入表
回头看,最该做的是:hook malloc,当分配大小 == sizeof(SYSTEMDATA) 时,记录地址,延迟改它的 [+4] 字段。这个思路我想到时已经太晚(清理阶段了)。
四、我的思考和决策过程(诚实版)
为什么我“跟汉化组的成品过不去”
用户问得尖锐:“你为什么非得跟汉化组的这个成品过不去?你自己翻译一份不行吗?”
真实原因(反思):
- 数据崇拜:我一开始就拿到了花咲夜的翻译数据,它“已验证可用”,让我产生了“复用 > 自研”的路径依赖。
- 时间压力:3.1 万条台词手动翻译在会话内不现实,我本能地找捷径。
- 技术聚焦错位:我把精力都放在“怎么让引擎显示”,而把“翻译本身”外包了——本末倒置。翻译才是这个任务的核心价值,显示只是最后一公里。
讽刺的是:如果我一开始就自己翻译(用 AI 模型逐条翻译日文→简体),产出的 script.bin 内容会更好,而且不会纠缠花咲夜的 Mai@KF.dll 方案。自己翻译 + GBK 简体 + 攻 language 切换,才是正路。
为什么反复在 version.dll 上横跳
因为我一直在“没有确认引擎怎么解码”的情况下盲试 hook。这是典型的“用战术勤奋掩盖战略懒惰”——每失败一次就换一个 hook 点,而不是停下来做诊断。
为什么清理时这么彻底
用户要“恢复到只有一份游戏解压”。这既是对技术失败的不满,也是想重新开始。我理解——技术路线走死了,不如清空重来。
五、反思与错误清单(按严重程度)
错误 1:把翻译外包给汉化组(方向性错误)
- 导致:质量受制于人、字符集受限、路径依赖
- 正确做法:自己用 AI 翻译全部台词
错误 2:hook 之前不做诊断
- 导致:两天时间花在无效 hook 上
- 正确做法:先写只读诊断 DLL,确认引擎实际调用什么
错误 3:低估引擎自解密的影响
- 导致:大量磁盘 exe 分析无用
- 正确做法:第一时间意识到逆向对象是解密后的代码
错误 4:验证不充分就下结论
- 多次误判 script.bin 版本(把翻译版当原版)、误判编码
- 正确做法:每次拿到新文件先做字节级验证 + 编码检测
错误 5:工具链扩散
- 下载了 MinGW、Locale Emulator 等,造成清理负担
- 正确做法:能复用系统已有工具的(如 PowerShell、现有 Python 库)就不装新工具
错误 6:会话 fork 过多
- 5 个 fork 导致上下文碎片化,每次都要重新加载任务
- 正确做法:一个会话专注一条主线,用 memory 持久化关键发现
六、留下的有用成果
即使最终没产出简体补丁,这个项目留下了真正有价值的东西:
- 完整的技术路径图:解包→SQLite→翻译→打包全流程跑通
- 引擎机制画像(已存 memory):
- 双层脚本格式 + XOR 0x55 加密
- 引擎自解密机制(femme_fatale.log)
- 自定义 SJIS 解码 +
system->language多语言分支 - 位图字体系统(gfx\font<lang>_,16×6 格/页)
- 引擎原生支持简体中文(LOC_CN)——这是最值钱的发现
- zh_cn 字库生成方案:用 PIL + 系统字体渲染 GBK 字符到位图,格式已验证兼容
- 关键数据点:
SYSTEMDATA.version=1, language 在 offset 4、存档game_data.sys结构
七、给未来(如果重做这个项目)
正确路径(我现在的答案)
- 解包(EscudeTools,已验证)→ 导出纯日文台词到 SQLite
- 自己翻译:用 AI 逐条翻译日文→简体中文(可分批,保持术语表)
- 打包:GBK 简体 script.bin(已验证可打包)
- 显示简体——两条路:
- 首选:hook
malloc,定位SYSTEMDATA,把system->language从 1 改成 3(LOC_CN),配合已生成的 zh_cn 字库 + 已打包的 GBK script.bin - 备选:patch exe 把
LANGUAGE编译常量从 LOC_JP(1) 改成 LOC_CN(3)(需要精确定位 patch 点,可用调试器)
- 首选:hook
关键心态
- 翻译是核心,显示是最后一公里,别本末倒置
- 先诊断,再动手;一次只验证一个假设
- 遇到字符集死结,先找“引擎是否原生支持”而不是硬绕
八、结语
这个项目我犯了很多错,但它们共同指向一个更根本的教训:当任务的核心是“做出更好的翻译”时,我不该把翻译这个核心外包出去,然后花全部精力去抠边缘的技术问题。
引擎的机制我摸得很清楚,简体显示的技术路径也基本打通(只差 language 切换这一步的可靠实现)。如果重来,我有把握按上面的正确路径走通。
这份复盘不是为了辩解,而是为了下次能真正做出那个“更优秀的简体翻译”。
—— 2026-08-08
相关文章
- 戦巫センナギ-存档修改与脚本逆向全复盘 —— 同引擎(Escu:de)的存档修改与脚本逆向实践
- 当 Galgame 变成「旮旯给木」:我们还能守住那份感动吗? —— 对 Galgame 翻译质量与文化传递的思考
- Python-Windows下Errno22排查实录 —— 编码问题排查经验(与 SJIS/GBK 字符集问题相通)
- WSL2 实战手册:空间账单、symlink 陷阱与 cc-switch 四连坑 —— 工具链管理的教训
