← 返回文章列表

《悠刻のファムファタル》简体中文翻译 · 技术复盘与反思

创作说明:本文基于作者与 AI 的调试对话整理生成,完整保留问题排查思路与技术细节。

日期:2026-08-08 定位:这是一份“事后之明”的复盘,记录我走过的路、卡在哪、为什么卡、以及现在回头看该怎么走。


一、任务与目标

任务:为 Escu:de 2024 年的 galgame《悠刻のファムファタル》制作高质量简体中文 AI 翻译,要求:简体中文、无乱码、无闪退、角色名统一、对话自然流畅。

表面目标:一份能玩的中文翻译补丁。 深层目标:超越现有 GPT-4o 翻译(用户已有一份,质量差)和花咲夜 Claude-3.5-sonnet 补丁(繁体、有残留、有乱码)——即“更优秀的翻译”。

当时我理解的“优秀” = 简体 + 完整 + 无乱码 + 自然。


二、走过的路(技术历程)

第一阶段:侦察与参考

  1. 拿到参考补丁:花咲夜机翻组的 Claude-3.5-sonnet 翻译补丁(moyu.moe,解压密码 julixian)。这是关键的情报来源——它证明了 script.bin 可以被解包→翻译→重新打包,游戏能读取。
  2. 找到正确工具:EscudeTools(github.com/Chenx221/EscudeTools),专门处理 Escu:de 双层脚本格式。这比 GARbro 强——GARbro 只能解外层,EscudeTools 能全流程。

第二阶段:解包与数据

  1. 双层解包script.bin (ESC-ARC2) → 381 个 .bin/.001 文件 → SQLite 数据库。
  2. 数据结构:每个剧本表 + __text(UI 文本)+ __mess(台词)表。共 32,380 条台词。
  3. 编码关键.001 文件用 XOR 0x55 加密,文本按 Shift-JIS 编码。

第三阶段:翻译(这里埋下了祸根)

我犯的第一个方向性错误:把“翻译”外包给了花咲夜补丁。

  • 导出 31,165 条日文台词
  • 导入了花咲夜补丁的翻译(30,111 条),只做了:繁转简(OpenCC)+ 修复乱码 + 修复日本汉字残留
  • 自己真正翻译的只有序章几百条

这个选择在当时看起来“聪明”:省时、复用已验证可用的数据。但后果是:

  • 翻译质量被花咲夜锁死(它的局限就是我的局限)
  • 引入了花咲夜的字符集问题(SJIS 只能表达 JIS X 0208 汉字)
  • 让我在后续反复“与汉化组的成品过不去”

第四阶段:封装

用 EscudeTools 把 GBK 简体的翻译写回 SQLite → 重新打包 → 生成 script.bin。这步技术上跑通了。

第五阶段:游戏加载(真正的泥潭开始)

这里开始了漫长的“让游戏显示简体”的攻坚战:

  1. 花咲夜 exe + Mai@KF.dll → 能跑,但 Mai@KF.dll 有 DRM(protect.dll/pkutil.dll),且用户目录被杀软隔离
  2. GPT-4o exe → 能跑(破解过 DRM),但翻译内嵌在 exe,无法替换
  3. 原版 exe → 有 SoftDenchi 启动检查(乱码弹窗跳网页)
  4. 自己写 version.dll 代理(hook GetACP/MultiByteToWideChar)→ 反复编译、反复失败
  5. 发现关键事实:引擎自解密(exe 启动时生成 femme_fatale.log 再运行)、引擎用自定义 SJIS 解码不走系统 API
  6. 终极发现:引擎原生支持多语言(LOC_CN),只需切换 language
  7. 生成 zh_cn 字库(PIL + 系统字体渲染 3097 个汉字,114 页)
  8. 卡死在 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.cput_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 的赋值:

  1. 编译期 #define LANGUAGE LOC_JP
  2. main.csystem->language = LANGUAGE
  3. 存档 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] 字段。这个思路我想到时已经太晚(清理阶段了)。


四、我的思考和决策过程(诚实版)

为什么我“跟汉化组的成品过不去”

用户问得尖锐:“你为什么非得跟汉化组的这个成品过不去?你自己翻译一份不行吗?”

真实原因(反思)

  1. 数据崇拜:我一开始就拿到了花咲夜的翻译数据,它“已验证可用”,让我产生了“复用 > 自研”的路径依赖。
  2. 时间压力:3.1 万条台词手动翻译在会话内不现实,我本能地找捷径。
  3. 技术聚焦错位:我把精力都放在“怎么让引擎显示”,而把“翻译本身”外包了——本末倒置。翻译才是这个任务的核心价值,显示只是最后一公里。

讽刺的是:如果我一开始就自己翻译(用 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 持久化关键发现

六、留下的有用成果

即使最终没产出简体补丁,这个项目留下了真正有价值的东西

  1. 完整的技术路径图:解包→SQLite→翻译→打包全流程跑通
  2. 引擎机制画像(已存 memory):
    • 双层脚本格式 + XOR 0x55 加密
    • 引擎自解密机制(femme_fatale.log)
    • 自定义 SJIS 解码 + system->language 多语言分支
    • 位图字体系统(gfx\font<lang>_,16×6 格/页)
    • 引擎原生支持简体中文(LOC_CN)——这是最值钱的发现
  3. zh_cn 字库生成方案:用 PIL + 系统字体渲染 GBK 字符到位图,格式已验证兼容
  4. 关键数据点SYSTEMDATA.version=1, language 在 offset 4、存档 game_data.sys 结构

七、给未来(如果重做这个项目)

正确路径(我现在的答案)

  1. 解包(EscudeTools,已验证)→ 导出纯日文台词到 SQLite
  2. 自己翻译:用 AI 逐条翻译日文→简体中文(可分批,保持术语表)
  3. 打包:GBK 简体 script.bin(已验证可打包)
  4. 显示简体——两条路:
    • 首选:hook malloc,定位 SYSTEMDATA,把 system->language 从 1 改成 3(LOC_CN),配合已生成的 zh_cn 字库 + 已打包的 GBK script.bin
    • 备选:patch exe 把 LANGUAGE 编译常量从 LOC_JP(1) 改成 LOC_CN(3)(需要精确定位 patch 点,可用调试器)

关键心态

  • 翻译是核心,显示是最后一公里,别本末倒置
  • 先诊断,再动手;一次只验证一个假设
  • 遇到字符集死结,先找“引擎是否原生支持”而不是硬绕

八、结语

这个项目我犯了很多错,但它们共同指向一个更根本的教训:当任务的核心是“做出更好的翻译”时,我不该把翻译这个核心外包出去,然后花全部精力去抠边缘的技术问题。

引擎的机制我摸得很清楚,简体显示的技术路径也基本打通(只差 language 切换这一步的可靠实现)。如果重来,我有把握按上面的正确路径走通。

这份复盘不是为了辩解,而是为了下次能真正做出那个“更优秀的简体翻译”。

—— 2026-08-08


相关文章

评论区