快讯发布

即时快讯 0

《FRAGMENTS BLUE》机翻汉化测试发布、与侦探小说
TODO:
目前汉化由Mimo-V2.5V2.5-pro翻译加校对。争取在27年春节前,人工校对一遍。

基于项目PS2Recomp,将目前运行于PS2模拟器/实机的游戏,Recomp成可运行在Windows的.exe。

0.感谢Opus4.8、4.6,Mimo-V2.5-pro、V2.5和项目GhidraMCP、pcsx2-mcp

本游戏的相关脚本、文本、文档(不含游戏本体和部分解包文件),已开源于Github,可自行构建ISO。汉化后的ISO下载链接在评论区发布。

目前提供的ISO仅为测试版,仅在Bangumi测试发布。
如在游玩过程中遇到问题,欢迎在评论区留言反馈或通过Bangumi私信我。

《FRAGMENTS BLUE》机翻汉化测试发布、与侦探小说
侦探笔记
(由各个session还原创作,作者:Mimo-V2.5-pro)

一份 PS2 游戏汉化项目的破案实录。
记录每一次误判、每一次推翻、每一次从"已确认"到"已作废"的跌宕。

――――――――――――――――――――――

序章:一具 2005 年的尸体

2005 年 ,日本开发商 ブリッジ 构建了 PS2 冒险游戏《FRAGMENTS BLUE》的最终版本。光盘编号 SLPM_662.03。此后这款游戏沉睡了二十一年。

2026 年 5 月,有人决定把它翻译成中文。

这不是一个普通的翻译项目。没有源代码,没有文档,没有 SDK。只有一张光盘镜像、一个反编译器(Ghidra)、一个模拟器(PCSX2),以及一个信念:只要搞懂数据是怎么存的,就能改

――――――――――――――――――――――

第一幕:虚假的确定性(会话 1–7)

第一条线索

最初的分析从 ELF 主程序 SLPM_662.03(1,148,848 字节)开始。Ghidra 的反编译器给出了第一批确定的事实:

• 文本渲染器 FUN_001aa360,处理控制码
• 字形分发器 FUN_001a1cd0,最大索引 3499,每字形 144 字节
• 像素渲染器 FUN_001a1d50,24 行 × 3 个 ushort,2bpp → 4bpp

然后是两个数据文件:

LT.BIN(505,856 字节)——字库。505856 ÷ 144 = 3512 个字形,刚好整除。
SC.BIN(1,966,080 字节)——剧本。指针表在 0x8E98,文本区从 0x100000 起。

一切看起来井井有条。

188 列公式——第一个"真相"

会话 1 从 Ghidra 反编译中提取出一个映射公式:字形索引与 Shift-JIS 编码之间存在 188 列的对应关系。


row = index // 188
col = index % 188
high = (row + 0x81) if row < 31 else (row - 31 + 0xE0)
low  = (col + 0x40) if col < 63 else (col + 0x41)

用这个公式解码 SC.BIN,得到了 6,156 个文本块、84,564 个字符。看起来像日文——至少大部分是。有 66% 的文本能正常解码,剩下的……大概是编码问题吧。

审判日还未到来。

"已确认"的幻觉

会话 3 判定 LT.BIN 格式为"128 字节头 + 3512 × 144 字节字形"。验证方法?505856 - 128 = 505728 = 3512 × 144。数学上完美。

会话 4 用 Ghidra 逐指令验证了像素渲染器的解码逻辑,结论:u2, u0, u1 顺序,LSB-first。渲染出的字形……有点怪。"文字边缘会混入其他图片",用户说。

会话 4 的回应:"前 282 个字形是小符号,所以模糊。" 结案。

会话 7 构建了四个 Python 工具:lt_bin_editor.py、sc_bin_repacker.py、glyph_mapping_builder.py、chinese_font_generator.py。报告结论:

"逆向工程完成,工具链就绪,等待人工翻译。"

报告的语气越来越自信。用词从"初步分析"变成了"已确认"、"已解决"、"已就绪"。

但有一件事从未被做过:在 PCSX2 里跑一次游戏。

――――――――――――――――――――――

第二幕:审计官的审判(stage_report.md)

2026 年 5 月 29 日,一份审计报告被写出来。它没有发现新的技术细节,但它问了一个更致命的问题:

你们到底验证过什么?

罪状清单


罪状                                证据
──────────────────────────────────────────────────────────────────────
从未进行运行时验证                  PS2_PROJECT_STATE.md 记录 "PCSX2 MCP: NOT connected"
把渲染格式争议当作"已解决"          "文字边缘混入其他图片"从未被解释
过度相信静态分析                    "渲染代码与 Ghidra 完全一致"≠"渲染结果可信"
过早构建工具链                      4 个 Python 工具,没有一个被游戏验证过

审计报告的核心判断:

这个项目当前最大的风险不是技术难度,而是过度依赖静态分析导致的"虚假确定性"。

最尖锐的一刀

报告对 188 列公式提出了质疑:

"把'提取的文本用什么编码显示给人类看'和'游戏中字形索引到底对应什么字符'混为一谈了。"

翻译成白话:你用 SJIS 解码出了日文,但这不等于游戏引擎用的就是 SJIS。渲染器读的是 int16 字形索引,它根本不认识 SJIS。

"在 PCSX2 中跑一次游戏,显示已知文本,然后用 MCP 读取对应字形索引处的 VRAM 数据,就能一劳永逸地解决所有编码争议。这个步骤从未被执行。"

――――――――――――――――――――――

第三幕:连接死者(会话 8–12)

会话 8:第一次看见字库的真面目

审计之后,终于开始做该做的事:连接 PCSX2。

但第一件事不是验证编码——而是用正确的格式渲染字库。Ghidra 反编译已经说了渲染器用自然顺序 u0, u1, u2,但之前的工具用的是 u2, u0, u1。换回自然顺序后,水平环绕(wrap)从 98.8% 降到了 0.1%。

看起来修好了?

没有。 用户目视发现:每个字符左侧 8 像素块整体上移 1~2 行。 片假名 ア、カ 最明显。

会话 9:头部长度——差 2 字节的阴谋

这个"左侧上移"困扰了整整一个会话。最终,渲染器反编译 + 全字库统计双重证据锁定了根因:

LT.BIN 的头部是 126 字节,不是 128。

字模数据应该从 126 + N × 144 起算,而不是 128 + N × 144。差 2 字节,恰好是 6 字节行宽的非整数倍偏移 → 导致左 8px 块取到了上一行的第三个 ushort。

会话 2 为什么"看着对其实错"?因为它用 u2, u0, u1 行内置换来补偿 128 头的错误。水平 wrap 被压到了 0.1%,但代价是左块上移一行。单一指标达标 ≠ 模型正确。

全字库扫描给出了客观证据:


方案                  水平 wrap    质心偏移
────────────────────────────────────────────
头 128 + 自然序       98.8%        1.363
头 128 + u2,u0,u1     0.1%         0.42
头 126 + 自然序       0.0%         0.411    ← 最终被推翻

结论:HEADER_SIZE = 126,自然序 u0, u1, u2。

(这个结论在 7 天后被推翻。但此刻,它是"运行时验证"过的。)

会话 10–11:映射表的三重校正

审计报告说 188 列公式未验证。现在开始验证。

第一击(会话 10):用 montage 直读字库。把 LT.BIN 的字形批量渲染成带索引标注的拼图,人眼逐字辨认。结果:

• 490 = 亜(JIS 第一水准首个汉字)
• 491 = 唖(第二个)
• 2067 = 屈 ← (后来证明是假锚点)

汉字段被标定为 game 490..3454,2965 字 = JIS X 0208 第一水准全集。delta = 34。

第二击(会话 11):用新映射表直解 SC.BIN 真实剧本。

结果:全是乱码。

の 应该在索引 327,但解出来是别的字。は 应该在 328,也不对。整张表整体偏低了 1 槽

校正后:の = 253, は = 254, 亜 = 491, 俺 = 700, 中 = 2346, 腕 = 3455。delta 从 34 改为 33。

三重独立证据(非目视):
1. SC.BIN 字形频率 top15 在偏移 −1 下精确还原日语假名频率排序(の 排第一)
2. 运行时断点 700 = 俺(带亻旁,吻合)
3. 开场独白「俺の中に」修正索引在 SC.BIN 逐字节命中 4 处

第三击:推翻"屈 = 2067"。直读字模证明:2067 = 占,1147 = 屈。2067 恰好等于已推翻的旧 kuten 公式的输出值——它是从公式倒推的假锚点,从未被真正验证过。

这正是项目铁律警告的"静态推断的虚假确定性"。

会话 12:连接死者——SC.BIN RAM 基址

PCSX2 MCP 终于连通了。

第一个运行时发现:SC.BIN 整体连续加载进 EE 内存,ram_addr = file_offset + 0x007947C0。

验证方法:同一字形串「切り抜かれた空」在文件 20 处命中、RAM 也恰 20 处,逐一相减 base 全 = 0x7947C0。

然后是第一次尝试注入——失败了

把 RAM 里「千花はいつでも笑っていた」的「笑」字形索引改成「空」,回读确认字节已改,但屏幕该字未变

是缓存?是时序?还是改错了地址?(文件里同一段独白有多份近似文案,措辞略有不同。靠导出记录顺序猜"下一句",错了 4 次。)

汉化命门卡在"改了但没上屏"。

――――――――――――――――――――――

第四幕:命门打通(会话 13–16)

会话 14:SC.BIN 回注——第一个中文字符

会话 14 解决了会话 12 的谜题。答案简单得令人尴尬:

必须在文本显示之前修改 RAM,已显示的文本不会重绘。

游戏渲染分两阶段:① 字形位图 → 合成进文本缓冲区(仅一次);② 缓冲区 → GS VRAM(每帧)。合成之后,源数据就不再被读取。

测试:
• 「扉を開けて...」→「を開けて...」 ✅ 屏幕显示「中」
• 「白い壁と...」→「い壁と...」 ✅

汉化命门打通了。 自定义字形索引可以注入并显示。

同时确认了字形槽边界:渲染器检查 param_1 < 0xdab(3499),槽位 3500 超出有效范围。可用槽位 0–3499

会话 15:第一个中文字符出现在屏幕上

中文字形生成工具 chinese_glyph_injector.py 完成:从 TTF 字体渲染 24×24 2bpp 字形,编码为 LT.BIN 格式。

端到端测试:「紙飛行機はっけなく墜落した」→「紙飛行機はっけなく墜落した」

注入「测」字形到 slot 208/209 → 屏幕「あ」「っ」均变成「测」。

铁证:渲染器 PC 停在 0x1a1d80 时正执行 lhu (a0) 读字库位图。渲染器实时从 EE RAM 读位图,非 VRAM 缓存。

同时发现了字库注入时序铁律


改 LT.BIN 字库位图 → 必须在目标文字合成之前
改 SC.BIN 字形索引 → 必须在目标文字显示之前

实证:在「あ」合成中途暂停改字库 → 留下白线伪影(下半部用新数据、上半部用旧数据)。在「あ」合成改字库 → 屏幕不变。

这解释了会话 12 的所有"改了没变"现象。

会话 16:126 字节头——第二次推翻

正当一切看似就绪时,会话 16 用活断点给了 HEADER_SIZE = 126 致命一击。

在字形分发器 0x1a1cd0 命中,读寄存器:
• index = 0x0AFC = 2812
• a1 = 0x4B56C0(字库基址)

分发器算式:a0 = a1 + index × 144。反汇编确认:sll index, 3; addu index; sll 4; addu a1。无偏移。

算得 src = 0x518480 = disk 偏移 404928 = 2812 × 144(整除)。

读 0x518480 字模解码 = 。glyph_map_full.json[2812] = 扉。三方吻合。

但 (404928 − 126) / 144 = 2811.125 → header = 126 不对齐!

LT.BIN 没有头部。字形 index i 在文件偏移 i × 144。

会话 9 的"header = 126 运行时验证"为什么看着对?因为 126 是行宽 6 的整数倍,保持了水平对齐(wrap = 0%),掩盖了垂直 3 行位移 + 索引 off-by-one。居中启发式根本分不开 0 与 126——只有渲染器活算式 a1 + index × 144 能定夺。

又一次"单一指标达标 ≠ 模型正确"。 讽刺的是,会话 9 的教训正是"单一指标达标 ≠ 模型正确",但它自己也踩了同样的坑。

工具紧急修正:HEADER_SIZE 从 126 改为 0。所有已生成的 LT_patched.BIN 作废,需重生成。

但映射表 glyph_map_full.json 本身不受影响——它的键就是渲染器 index i,与头部无关。SC.BIN 解码/容量统计也不受影响。

会话 16(续):字库容量可行性

同一次会话还解答了一个关键问题:字库够不够装中文?

对全剧本 6896 条文本做唯一字形槽统计:
• 全剧本仅用 1710 个唯一字形槽
• 唯一汉字仅 1436 个(占 JIS 第一水准 2965 字的不到一半)
1529 个汉字槽在字库里存在、但日文剧本从未使用 → 可直接重分配给中文

加上假名/未用区的 ~246 空槽,可用容量 ≈ 1775 槽。渲染器上限 3500 槽无需突破,无需改 ELF

――――――――――――――――――――――

第五幕:整段中文上屏(会话 17–21)

会话 17:松散文件的秘密

逆向 DVCI 文件系统后发现:DATA 目录下的文件是光盘上的松散文件,通过 CRI 中间件按文件名查找加载。直接替换 ISO 中的文件即可,无需重建归档容器。

这意味着汉化的最后一步——打包 ISO——变得极其简单:解包 → 替换 LT.BIN + SC.BIN → 重打包。

会话 19:离线管线建成

三个核心工具就绪:
• build_alloc.py:字集分析 + 槽分配
• chinese_glyph_injector.py:字形注入
• sc_bin_writer.py:SC.BIN 回写

回写策略:等 token 数就地替换。指针表 @0x8E98 有 6285 项,不指向记录起点(78% 落在正文 token 上),任何字节长度变化都须重建整张表。所以只替换对白字形 token,控制码全部原位不动,文件大小零变动。

小样 12 条闭环验证:12/12 逐字匹配 ✅

会话 20:第一句中文出现在游戏屏幕上

开场独白「切り抜かれた空を見る。」被替换为「看见切出的天空。」。

pcsx2_find_pattern 定位 RAM 真身:0x7A8128。write_memory 写入 11 个 token。屏幕成功显示中文。

随后注入了完整的 4 段开场独白,18 个新造字形(开/门/总/见/剪/墙/她/帘/插/时/柜/梦/窗/躺/还/间/卧/么),全部清晰可读。

唯一的问题:新造字笔画偏细——Pillow 渲染的 TTF 字形比原版像素字体瘦。但用户决定先不调字体。

会话 21:中文标点——30 个标点,0 缺形

一个容易被忽略但至关重要的发现:符号区 index 0..145 是完整的 JIS X0208 第 1-2 区符号集,是中文标点的超集。30 个中文常用标点全部命中:

• 28 个码点精确复用(。= 2 , = 3 「 = 53 … = 35)
• 2 个同形替换(— → idx28,· → idx5)

三层验证闭环:码点匹配 → 离线渲染 30/30 → 真实 SC 记录上屏。

――――――――――――――――――――――

第六幕:规模化翻译(会话 22–28)

会话 22:翻译工作台

translate.py 四子命令:template(拆单)/ validate(校验)/ merge(合成)/ stats(统计)。

TRANSLATION_GUIDE.md 成为规模化翻译的唯一交接文档:三条硬约束(行数/逐行 cap/一字一 token)、翻译上下文指令模板、与下游衔接。

会话 23:第一批全量翻译

6926 条翻译 → 2182 字 = 1185 复用 + 997 新造 → LT/PR 注入 → SC 回写。

闭环验证:6849/6926 逐字匹配(98.9%)。74 条溢出(中文文本超出原日文行宽 cap),保留原文。

会话 24–25:补丁 ISO 黑屏之谜

用户加载补丁 ISO 后,PCSX2 卡在黑屏。EE 停在 0x81fc0 内核空轮询。

根因:游戏校验文件完整性 hash。 修改文件内容后 hash 未更新。

hash 算法(Ghidra 反编译 FUN_001A8400):
• 两个 64-bit 累加器,初始值 0x1111111111111111
• 遍历文件 body,每 16 字节:hash_hi += word[0], hash_lo += word[1]
• 尾部 16 字节 = pack('<QQ', hash_hi, hash_lo)

修复:补丁管线最后一步对三个文件重算 hash 并写入尾部。

会话 26:8446 条被遗忘的记录

补丁 ISO 能加载了,但中文不显示

根因:export_text_v2.py 的 TEXT_START = 0x100000 跳过了偏移 0x3F48–0xFFFF 的 8446 个 FF84 记录。SC.BIN 的文本区不是从 0x100000 才开始——前半段也有大量对话,只是被导出脚本的硬编码常量跳过了。

修复后全量导出:15375 条(含前 8446 条 + 后 6929 条)。

会话 27:全量翻译完成

区 1(id 0–8445):7261 条中文翻译,34 个 Agent 子代理并行。
区 2(id 8446–15374):6926 条旧译沿用。

合计 14133 条翻译。build_alloc.py 空槽池扩展至 0–490(假名区和符号区在全量中文翻译后可安全回收)。

会话 28:三个视觉问题

用户在 PCSX2 中目视验证,发现三个问题:

1. 残留原文——90 条日文未被翻译(溢出保留原文的 + 导出遗漏的)
2. 字体偏细——SimSun TTF 笔画太细,与原版像素字体不协调
3. 错体字——glyph_map 大面积错误(如 498 ≠ 开),导致字形与字符不对应

解决方案:
1. 逐条修正,0 溢出
2. 字体换 SimHei(黑体)
3. 改用 --alloc 全槽 SimHei TTF 覆盖(2597 字形),不再依赖有错的 glyph_map

――――――――――――――――――――――

第七幕:隐藏的格式(会话 29–31)

会话 29–30:选择画面的幽灵

对话全译了,ELF 文本也译了。但选择画面(分支选项)仍显示日文

会话 29 推测"RAM 有两份 SC.BIN,选择 UI 读原版 0x95xxxx"。

会话 30 推翻了这个推测。 经运行时实证,RAM 0x95E8C0 与 disk file 0x1CA100 逐字节匹配。0x95xxxx 就是同一份 SC.BIN 的尾部,不是第二份。

选择文本显示日文的真因:该区域从未被翻译——它是与 FF84 对话不同的独立格式,export_text_v2.py(只认 FF84 记录)整块跳过了。

格式解析:FFFF 0000 &lt;16bit setid&gt; [可选 FFFF] &lt;字形 token…&gt;,无 FF84 头。setid 22..897 不连续,共 200 条记录。2 条多选项菜单块(setid 333/889)内部子选项再用 FFFF 分隔。

导出工具 export_choices.py → 209 个干净文本段。回写工具 choices_writer.py。

会话 31:alloc 错配——v6 的灾难

选择区翻译完成后构建了 Pieta_ZH_v6.iso。用户反馈:字形全错。 么 显成 举,个 显成 丧。

根因:choices_writer.py 用了 build/alloc.json,但 v5 的 LT.BIN 是用根目录 alloc.json 烤的。 两份 alloc 索引在 ~600 处错位,选择 token 落到了邻字。

这是一条铁律的代价:

基于某 ISO 只改 SC 时,回写使用的 alloc 必须与该 ISO 的 LT.BIN 同源。

用根 alloc.json 重写 choices,choices_writer.py --verify 达到 207/207。Pieta_ZH_v7.iso 构建完成。

同时补进了第三种文本 framing:FFFB/FF99 孤立正文「谁说我是可爱女孩?」——既非 FF84 也非 FFFF-choice,两个导出器都跳过的漏网之鱼。

――――――――――――――――――――――

终章:v8_1——传统汉化完成

全量译文经 LLM 校对,质量提升。Pieta_ZH_v8_1.iso 构建完成。

最终覆盖范围:
SC.BIN 对话:15375 条全量翻译
ELF 文本:116 条全量翻译
选择画面:207 段全量翻译
字形注入:SimHei 全槽覆盖(2597 字形)
文件 hash:自动重算

――――――――――――――――――――――

后记:侦探守则

这个项目最深刻的教训,不是任何一条技术细节,而是一条方法论:

"单一指标达标 ≠ 模型正确。"

188 列公式能解出 66% 的"可读"日文。header = 126 能让水平 wrap 降到 0%。u2,u0,u1 能消除环绕。每一个都"看起来对"。每一个都是错的。

真正的确定性只来自一个地方:在目标机器上跑一次。

Ghidra 反编译告诉你代码可能做什么。Python 渲染告诉你数据可能是什么。但只有 PCSX2 加载游戏、显示文本、按下按钮的那一刻,你才知道真相是什么

Game reversing 的铁律:在目标机器上跑一次,比在反编译器里看一百次更有说服力。

来自:Bangumi

发表评论 取消回复
图片 链接