朝花夕拾:一场从 2012 年开始的游戏汉化
(由 GPT-6 Astra 根据我的文章大纲和截图思路生成)
最近,我又在折腾 Wii 游戏《HOSPITAL. 6人の医師》的汉化。这次有了 AI 辅助,一些以前需要手动反复比对、试验的工作,可以更系统地做下去了。随着旧脚本和游戏里的实现被重新翻出来,我也发现,当年对一些格式的理解并不完全准确。
不过,比起纠正了多少个字段,更让我感慨的是时间:第一次尝试汉化这个游戏,已经是 2012 年的事情了。
当然,我并没有连续做十四年。中间很长一段时间,这个项目都是搁置的。只是到了不同的人生阶段,我又会回到同一个游戏面前,看看当年没有解决的问题,现在能不能再往前走一点。
想来想去,还是“朝花夕拾”这四个字最适合这段经历。
2024 年的汉化版。
2012:先让中文显示出来
2012 年开始尝试的时候,我对游戏汉化的认识还很简单:既然游戏里能显示日文,那应该也有办法把里面的文字换成中文吧。
真正动手之后才发现,找到文字和让游戏正确地显示修改后的文字,是两回事。文件里的内容也不会按照方便修改的方式摆好,等着我去替换。很多东西只能一边看、一边猜,改完之后再试。
那时确实做出了一些结果。我简单破解了部分文本,能够修改并导入回游戏。虽然离完整汉化还很远,但能看到自己的修改出现在游戏里,本身就很有意思。原本只能作为玩家去看的内容,居然可以通过自己的操作改变一点点。
图片就没有这么顺利了。画面上那些已经画进图片里的日文,不能像文本一样直接替换,而我当时没能把图片汉化做出来。只改了一部分文本,画面里仍然留着日文,这件事也就一直没有真正做完。
一方面是技术能力还不够,遇到一些问题时,我甚至不知道该从哪里继续查;另一方面,学业也不允许我无限地花时间试错。最后,项目还是搁置了。
现在回头看,这倒不是一个很戏剧化的“放弃”的决定。只是手头有更紧迫的事情,眼前的问题又暂时解决不了,于是先放下。至于以后什么时候再做,当时也没有答案。
2024:终于能接着往下做了
再次认真拾起来,是 2024 年。那时研究生已经差不多读完,我终于有了一些时间,也有了这些年学习计算机专业积累下来的能力,可以重新看看这个旧项目。
游戏还是那个游戏,文件也还是那些文件,但我看它们的方式已经不太一样了。
以前遇到一段看不懂的数据,更多是在文件之间找相似之处,尝试猜出规律。到了这一次,我可以沿着游戏的运行过程去找答案:用反编译和反汇编查看程序,在模拟器里下断点,观察内存,追踪文件被读进来之后,哪些代码取了哪些字段,又把这些数据交给了谁。
这并不意味着问题一下子都变简单了,但至少有了继续追下去的方法。某个数到底是长度、数量,还是偏移,可以去看程序怎样使用它;图片的数据为什么不像一张普通的图片,也可以沿着读取和显示的过程一点点拆开。
文本、图片、打包格式,我都重新尝试了一遍。除了把数据导出来,还要解决修改之后怎样放回去:文本变长了,相关的位置和长度怎么处理;图片改好了,怎样转回游戏接受的数据;包里的文件发生变化之后,又怎样重新组织和压缩。
最后,我完成了文本、图片和打包文件的导入导出脚本。这些工具后来整理在 hospital-mod-tools 仓库里。我也发了一段 汉化测试视频,记录当时已经能在游戏里看到的效果。
汉化后的剧情对白。
这里面最让我有成就感的,还是图片终于能够修改并导回去了。2012 年没能跨过去的那一步,到了 2024 年,终于可以用自己掌握的方法解决了。
学过的东西在这种时候会变得很具体。平时说“理解程序如何运行”,听起来像一个很大的目标;但在这里,它意味着我能找到以前看不懂的数据是怎么被读取的,能把一张图片正确地还原出来,再把修改后的图片放回游戏。过去积累的知识,真的帮我完成了一件惦记了很久的事。
文本:一句台词里面,为什么会藏着命令?
只说“破解了文本和图片”还是有点抽象。下面就挑几道具体的关讲讲。有些问题在 2024 年已经找到了解法,还有些细节,是到了 2026 年重新追踪代码时才弄明白的。
汉化最容易让人想到的操作,大概是搜索一句日文,然后替换成中文。但游戏里的文本不是一个大号的 TXT 文件。BM2 消息文件有消息头、标签、子消息和数据偏移;BF 则还包含控制流程的指令,消息块只是它的一个部分。有些 BF 甚至根本没有内嵌消息。
所以,“我找到了这句话”只解决了第一步。把它改长之后,后面的消息位置会不会移动?外层 BF 的字符串池又是不是紧接在后面?如果只按原位置写进去,很可能一句话看着改好了,别的数据却被覆盖了。
更麻烦的是,文字中间还混着控制信息。游戏显示一句话时,可能要播放语音、切换立绘、等待玩家输入。这些动作并不都写在另一份脚本里,有一部分就直接夹在文本字节之间。
先不要急着反编译,打开十六进制编辑器看看
不妨拿原版 ISO 中的一份文件,看看从字节到代码,可以怎样一步步找到线索。动手前先留一份原版备份,修改实验放在副本里进行。
第一步其实很朴素:看看目录和文件名,按扩展名、大小和所在章节分组,再用十六进制编辑器打开几个样本。先看头部有没有固定标记,右侧字符栏有没有可读的名字,几个同类文件之间又有哪些重复。直接在 ISO 或压缩包里搜索台词,搜不到也不能马上得出“文本加密了”的结论——它可能还压缩在包里,也可能用了另一种编码。
例如原版的 comic/msg/jpn/400_010.bm2,可以找到 ASCII 字符串 MSG_400_010_0010。这是一个很好的入口:名字看着像消息标签,数字又像章节和台词编号。但它只能说明标签用 ASCII,不能说明整份文件、尤其是正文也用同一种编码。
文件头的 MSG2 和 ASCII 消息标签,是识别结构的第一条线索。
接下来可以让编辑器切换右侧的解码方式,或把一小段候选字节拿出来,用不同编码试读。文件偏移 0x289 的这一段就是很小的例子:
1 | 文件偏移 原始字节 |
选区为 0x289—0x28E,共六字节。注意前一行的 FF 00 04 和后面的 FF 00 10:正文并不是孤立存放的。
切换编码,或载入码表,尝试把同一组字节还原成日文。
| 候选解码方式 | 能看到什么,能证明什么 |
|---|---|
| ASCII | 正文里的高位字节不可读;能认出标签,不等于能读正文。 |
| UTF-8 | 这几个字节不能组成合法的 UTF-8 序列。 |
| UTF-16 大端/小端 | 凑成双字节码位可能也能显示字符,但不对应这句日文;“没报错”不等于选对了。 |
| Shift-JIS/CP932 | 可以读出日文;按 CP932 解码是「ふ~…」,与这一幕的台词相符。 |
严格说,Shift-JIS 和 Windows 的 CP932 也不是完全相同的映射。例如这里的 81 60,前者通常解作波浪线 〜,后者是全角波浪号 ~。实际写工具时,应明确选用哪一套映射,不能只凭编辑器里看着差不多。
0x289 起的六字节按 CP932 解码,对应台词「ふ~…」。
找到一小段日文之后,再拿更长的句子、标点、假名和汉字交叉检查,才比较有把握。日文游戏让 Shift-JIS 成为值得优先尝试的候选,但不是可以直接跳过验证的答案。即便选对了编码,把整个 BM2 当文本打开仍然会有大量“乱码”:那些地方本来就是长度、偏移、标签和控制记录,并没有义务被解码成文字。
从“能读出来”到“敢写回去”
下一步不是立刻全文替换,而是把几条相邻消息并排看。台词前后哪些字节重复?00 出现在什么位置?有些四字节数,按大端或小端解释后,是否刚好落在文件里的另一个有效位置?相邻消息的起点之差,是否能和某个长度字段对上?先在副本或笔记中标注候选边界,再用更多样本推翻或保留这些猜测。
这份文件中,第一条消息的数据块从 0x270 开始,开头是:
1 | 00 00 00 01 00 00 00 08 |
现在知道,它表示一个子消息,子消息从这个块的 +8 开始;于是走到 0x278,先遇到控制记录,再走到 0x289 才是正文。可在最初观察时,“1 是不是数量、8 是不是偏移”都只是猜测。应当去找有多个子消息的样本,检查后面是否真的有相应数量的偏移,再看程序怎样取这些值。
在已经识别出正文边界后,一个合适的最小实验,可以是把「ふ」的 82 D3 换成同样占两字节的「は」82 CD,其他字节完全不动。这样先回答一个问题:游戏是否真的读取了这个文件、这个位置?然后才分别测试变长、换行、中文以及控制指令,不把几个未知因素混在一次修改里。
如果等长替换没有变化,先核对加载路径、是否有同名文件、是否需要重新打包;如果等长正常、变长异常,再查长度、偏移和槽位容量。如果中文显示为空白或错字,还要继续查字库的编码到字形映射。编辑器能显示中文,不代表游戏的 BRFNT 里已经有对应的字形。
还有两种测试很容易被忽略:一是不做任何翻译,导出后立即导入,再逐字节比较;二是修改后不只看一句话,还要继续到下一句、下一段,观察换行、等待和语音是否正常。前者检查工具有没有无意改坏数据,后者检查那些看不见的控制信息是否仍然有效。它们证明的事情不同,也都不能代替所有场景的测试。
断点地址不是凭空知道的
到这里,手上有的是文件偏移和几段认识的字节,还不是解析函数的地址。不能凭空说“去 0x80039480 下个断点”——找出这个地址,本身就是破解过程里很重要的一步。
如果从未知代码出发,一条可行的路线是先正常进入对应剧情,暂停后在模拟内存中搜索已经确认编码的台词字节。最好选较长、比较独特的句子;只有几个字节,可能在代码、旧缓冲区和其他消息里都碰巧出现。找到了候选位置,还要看前后有没有相同的标签、命令和相邻台词,确认它确实是目标资源。若搜不到,就回头检查加载时机、压缩、编码转换,以及文件是不是还没被读入。
这里需要区分两种“位置”:0x289 是文件里的偏移,不能直接拿去作为 CPU 地址下断点。在一次原版运行的 MEM1 中,整份 1319 字节的 400_010.bm2 在 0x80F01DC0 唯一、逐字节匹配。因此正文位于 0x80F01DC0 + 0x289 = 0x80F02049。内存地址会随运行情况变化,重进场景后还需要重新定位。
0x80F02049 处是正文首字节 82,后面紧跟着 D3 81 60 81 63 00。
接着,对候选正文地址设“读取时暂停”的内存断点,继续到游戏再次读取它的时刻。它回答的不是“程序走到哪儿了”,而是“谁在读取这个字节”。如果这句已经解析完,就需要重进场景或换一个尚未读取的消息,并核对重新分配后的地址。
对正文首字节设置仅在读取时暂停的内存断点,观察谁会读取它。
2026 年复查时,我先沿用已知的控制头断点,在 Maria 第一章解析途中确认缓冲区位置,再停用它,换成正文首字节的读取断点。
这个读取断点实际停在了下面的指令:
1 | 8003946C lbzx r0, r3, r31 |
绿色行为这次暂停的位置 0x8003946C。它从文本缓冲区加游标的位置取一个字节;随后紧接着的 cmplwi r0, 255 判断是否遇到 FF。
r3 + r31 = 0x80F02049,正好指向正文。此时 lbzx 尚待执行,r0 仍是旧值。
第一次命中也未必就是目标解析器。可能先撞到解压、复制、校验或测量文字的代码,应看寄存器指向哪里、游标怎样变化、调用栈来自谁,再继续筛选。纯复制循环和“识别 FF、分别读取命令头、按字符推进”的循环,行为是不同的。这里再结合调用栈中的 0x8003A8E0 和附近反汇编,可以把范围收敛到 0x80039410 这个扫描函数,而不是停在一个孤立地址上猜用途。
顺着读取动作,给字节找到含义
找到候选函数以后,才轮到在关键指令上设执行断点、单步读取字段。对这种格式,光靠肉眼看十六进制,很容易“看出”一个恰好适合几个样本的规律。读取代码则能告诉我:它一次拿走几个字节,把它当成什么。
控制头附近最关键的几条指令很短:
1 | 80039470 cmplwi r0, 255 ; 当前字节是不是 FF? |
在原版 400_010.bm2 的 0x278,也就是「ふ~…」前面,实际字节是:
1 | FF 0004 0002 0001 00000000 0001 00000001 |
进入 Maria 第一章「火の玉GIRL」,在 0x80039480 断下来时,这条指令还没有执行。单步执行后,r27 = 4;再执行 0x80039488 的指令,r26 = 2,下一条待执行指令位于 0x8003948C。
两条 lha 指令分别读出命令编号和参数数量。
于是字节结构就能和程序行为对上:一条控制记录以 FF 开始,随后是两个大端的 16 位数:命令编号 opcode 和参数数量 argc。这里命令号是 4,参数有两个;每个 0001 后各跟一个四字节数值,分别是 0、1。头部五字节,加两个六字节参数,共 17 字节,恰好从 0x278 走到 0x289 的正文。
参数也不都是整数。沿着 0x800391E0 的通用参数跳过逻辑,可以看到:类型 0001 消耗六字节,类型 0002 则读取四字节长度,再越过相应的字节串。因此不能只看到某个命令编号,就按猜好的固定长度往后跳,更不能遇到每个 FF 都强行切段。
把这一段 PowerPC 反汇编整理成 C 式伪代码,省略布局上下文和部分参数后,核心逻辑大致如下:
1 | // 依据 0x80039410 的读取逻辑整理,省略布局上下文。 |
这里还有两个不能略过的细节。第一,这一层主要扫描文本、测量并建立显示节点,不等于最终把像素画到屏幕;控制表也有解析阶段的回调,不能把所有命令都写成“扫到就立刻播放语音”。第二,在排除 FF 和 00 后,它简单地把小于 0x80 的字节视为单字节,其余视为双字节。这不是完整的 Shift-JIS 合法性检查,却意味着上层早已按照一字节或两字节拆好了文字。只把字库的编码标记改成 UTF-8,并不会让这条路径自动支持 UTF-8。
00 在这里结束的是当前这次扫描,不代表整份文件、甚至整条消息后面都没有数据了。外层还会根据消费字节数继续处理。于是,在十六进制里看到正文后还有控制记录,也就不再奇怪。
Maria 第一章的这段对白对应消息 MSG_400_010_0050。三行正文分别从文件偏移 0x3C4、0x3D9、0x3E8 开始,每行之后各有一个 00。
例如上面台词后面的等待控制,以及原版 operation2/data/en110/en110.bf 的 MSG_EN10_GAMEOVER 在文件偏移 0x1E1 的记录,都是:
1 | FF 0010 0001 0001 FFFFFFFF |
末尾 FFFFFFFF 是同一个数值参数,不是另一条命令。
这条记录总共 11 字节:一个参数,类型为整数,值按有符号数解释是 -1;五字节头部加六字节参数。它的后续处理器设置等待状态。这里的 0010 也不能和随盘消息源码里的 <wait> 混为一谈,后者对应的是另一条 FF 0000 0000。这段也正好解释了后面要讲的“旧工具切错边界,为什么仍然能回导”。
确认了这些边界,导出工具才能知道哪些内容交给翻译,哪些是必须保留的命令;导入时也才能按结构重新计算长度和偏移,而不是把整个文件当成一段可以任意替换的字符串。
图片:看起来像标准格式,却偏偏差了一点
图片是当年最让我惦记的一关。先看一个很直观的例子:游戏启动时的腕带安全提示。这里的日文已经画进了图片,改消息文本当然不会让它变成中文。
原版 title/hanken.tpx 中的 strapA_jp 纹理。
改成中文后的腕带安全提示。
先看看游戏实际用到了哪些纹理
要走到第二张图,首先得把第一张正确地取出来。不过,在完整理解文件格式之前,还可以先让游戏把资源加载起来,再借助 Dolphin 的 Dump Textures(导出纹理) 功能看看它实际用到了什么。在图形设置的高级选项中开启纹理导出,然后正常进入目标场景,模拟器就会把运行时遇到的纹理解码并保存成 PNG。本作可以到 Dolphin 用户目录下的 Dump/Textures/SPTJEB/ 查找结果。最好在进入场景前开启;如果目标纹理已经在缓存里,就重新进入场景,必要时重新启动游戏。这不是一次性扫描内存、导出游戏里的全部图片。
在 Advanced 页的 Texture Dumping 分组勾选 Enable,并保留 Dump Base Textures。它和上方的 Dump EFB Target、Dump XFB Target 不是同一个选项。
这些图很适合作为逆向时的参照。先按尺寸和内容寻找腕带提示、菜单文字等容易辨认的目标,观察透明区域、边缘和小图的排列,再回到磁盘资源里找对应的数据。屏幕上一块独立的按钮,导出后可能只是大图中的一角,这就提示还要寻找裁切坐标和精灵记录。导出文件名通常包含尺寸、哈希等信息,不会直接告诉我原来的 TPX 文件名,因此还得结合场景、资源目录和图像内容逐步对应。
这次实际运行原版游戏后,就能找到下面这一对腕带提示纹理。它们都是 640×480,文件名中的像素数据哈希同为 a436a57935fc2af0,后面的调色板哈希却不同。按这个共同哈希筛选,恰好留下两张图。
同一份索引像素,配上两份不同的调色板:文件名中分别出现 4b53f65ff3f67eea 和 b2c86521f4d01108。
第一张导出图。轮廓已经能认出腕带提示,但单独显示时还没有最终配色,部分文字的明暗也不同。
第二张导出图。两张都是灰度 RGB,却各自保留了不同的颜色与透明度分量;不是两份不同语言的图片。
但拿到 PNG,不等于已经知道原文件怎样存储。Dolphin 已经替我们完成了运行时纹理的解码,PNG 里没有 TPX 的目录、偏移和精灵信息;而且纹理还可能经过后续图形流水线组合,不能把单张导出图直接当成最终画面。本作后面提到的双调色板和 TEV 通道组合,就是需要继续追查的地方。比较靠谱的验证方式,是把自己写的解码器输出、Dolphin 导出的纹理和游戏实际画面放在一起对照:轮廓、分块、透明度或颜色究竟在哪一步出现差异,再带着这个问题去看读取和绘制代码。
有了这些参照,再回到游戏的 .tpx 文件。它并不是一张 PNG 换了扩展名,而是一份精灵和纹理集合:外层有头部、纹理目录和精灵记录,里面还可以放多张图。精灵记录描述的是从哪张纹理裁出哪一块、怎样定位和显示,不能因为只想改像素,就把这些数据一并丢掉。
沿着目录中的偏移往里看,又能遇到熟悉的 TPL 魔数 00 20 AF 30。但继续看调色板,格式值却是 0xFF,并不是照着标准 TPL 解码就能处理的那一套。这就是逆向里很容易卡住的地方:大半个结构都认识,剩下的一小块不对,整张图的颜色就都不对。
如果把这个过程展开,一开始可以先比较几张大小不同、用途已知的图片资源:头部哪些数随图片宽高变化,哪些数指向文件中的后半段,按候选宽高和像素位数计算,数据长度能不能对上。再写一个尽量简单的预览器,把像素排列和颜色解释分开试。轮廓出现了但颜色不对,就先查调色板;颜色似乎合理却被切成小块,就查分块顺序、块尺寸和块内排列。这些现象只是排查线索,最后仍要拿实际读取代码和多个样本核对,而不是“看着像图片了”就收工。
颜色表和像素排列,是两件不同的事
这些纹理的像素本身用的是 CI4 或 CI8,也就是“像素里存颜色表的索引”。CI4 每个像素占四位,CI8 占八位。它们还按小块存放:CI4 是 8×8 像素一块,CI8 是 8×4,一块都占 32 字节。读完一个小块,才走到右边的小块,不能把整个文件直接当成从左到右逐行排列的像素。
颜色表则是本作自己做的安排。通常说 RGBA,很自然会想到每种颜色连续存四个通道;这里却是先存所有颜色的 [G,R],再存所有颜色的 [A,B]。想恢复第 i 种颜色,就要从两个平面分别取值,再按 (R,G,B,A) 拼起来。
结构示意。颜色索引没有变成两套,拆开的是同一种颜色的四个通道。
到 2026 年重新追踪运行代码时,这种看起来奇怪的排列也有了更清楚的解释:游戏建立了两份 IA8 调色板,配合两套纹理对象,再通过 Wii 图形流水线里的 TEV 阶段重新组合通道。
这也解释了前面实际导出的两张灰度图:第一张 PNG 的灰度值对应原图的 R,透明度对应 G;第二张的灰度值对应 B,透明度对应 A。按这四个分量重新组合,就能得到本节开头的全彩腕带提示。这里还可以把“看着一样”推进到数值验证:两份真实导出图分别与原版 strapA_jp 的两份 IA8 解码结果逐像素一致,重组后的全部 640×480 个像素,其 RGBA 也与 TPX 解码图完全一致。这样,文件中的双平面调色板、模拟器看到的两份纹理和最后显示的颜色,才真正对应起来。
还有一个容易踩的坑是“文件偏移”和“内存地址”不能混用。TPL 绑定到运行时之后,会把一些相对偏移原地改成绝对指针。内存查看器里看到的数已经是绑定后的结果,如果直接把那段内存存回文件,下一次加载时就不对了。
把这些事情分开,导出就有了明确的步骤:读取 TPX 目录,找到图像块,还原调色板,按 tile 展开像素,最后保存成可以编辑的 PNG。导入再反过来,把修好的图量化到相应的颜色数量,生成索引像素和调色板,放回原来的结构。图片看着一样大,还不够;写入的数据长度不能踩到下一块,精灵的裁切和定位信息也要保留。
现在讲起来,可以归纳成几段文字。但对 2024 年的我来说,最有分量的还是结果:终于可以把图片上的日文改掉,再让游戏把修改后的图显示出来了。
打包:图和文字改好了,为什么游戏没有变化?
文本和图片之外,还有外面那层 PAK。游戏可以从已经挂载的资源包里找文件,找不到才回落到独立文件。于是,“我明明改了这个文件,为什么画面没变”不一定是导入算法错了,也可能是游戏根本没有读到我改的那一份。
要把修改真正送到游戏手里,就得把资源包也拆开、装回去。本作的 PAK 外层并不复杂:每项先有一个 252 字节的文件名槽,再跟四字节压缩长度,然后才是压缩数据。这里还有个小细节:BF、BM2、TPX 的许多字段是大端,PAK 的压缩长度却是小端。同一款游戏里的数据,也不能统一套一种字节序。
每个成员的压缩块以 10 开头,使用 LZ10。解开这一层,才拿到里面的 BF、TPX 或其他资源。成员结束后,还要把位置向上补到 64 字节边界,才能读下一项;少算这一步,后面的填充就可能被当成新文件头。
从未知包开始看时,文件名往往比压缩数据更容易给人线索。可以先搜索可读的名字,看它们是否隔着类似的头部反复出现,再尝试用紧邻文件名的长度跳到下一项。猜测某段是压缩流,也不能只看第一个 10:还要检查解压长度是否合理、能否完整解出数据、解出的内容有没有目标资源的头部。一个成员成功后,再换多成员的包验证对齐,最后做一次不修改内容的解包、重包测试。每一步都只增加一个新的假设,才知道出错时该回头查哪一层。
title/saveload.pak 的成员布局。
例如这个包的第一项是 k_saveload00.tpx,长度字段的四字节 70 EA 03 00 按小端解释是 0x3EA70。压缩数据从 0x100 开始,在 0x3EB70 结束,再补 16 字节,下一处目录头位于 0x3EB80。这个包只有一个成员,因此那里已不是第二个成员,而是全零的终止头。
重新打包时,修改后的数据压缩出来可能更长,也可能更短,不能只替换原位置的一段字节就结束。每一项的压缩长度、后面的填充以及整个成员顺序,都要重新处理。到这里,文本、图片和打包的导入导出才算接成了一条完整的路。
不完全正确,但已经很开心了
2024 年做这些工作的时候,我还没有用 AI 来辅助这次逆向。查资料、读代码、比对数据、写脚本、运行测试,主要还是自己一点点做。
现在重新检查,能发现当时有些地方理解得不够准确。比如,一个字段在几个样本中看起来符合某种规律,不代表所有文件都这样;某些文本控制信息虽然被原样保留下来了,也不代表我已经完整理解了它们。工具能处理手头的资源,与格式的每个细节都得到了解释,中间还有一段距离。
不过,我并不觉得这让当时的成果失去了意义。
那时候最直接的目标,就是把想修改的内容导出来,改好,再放回游戏里。这个过程能够跑通,文本和图片能够正常显示,就已经足够让我开心了。它不是一份覆盖所有情况的格式规范,却是我自己做出来、确实能用的工具。
病例说明也能正常显示中文了。
2026:这次多了 AI 辅助
到了 2026 年,再做同一个项目,工作方式又变了一次。
这次可以让 AI 帮忙搜索资源引用、整理反汇编线索、编写解析器和测试,也可以把一个猜测放到更多文件上验证。旧工具当时已经够用,但验证范围还有继续扩展的空间。现在,把整个资源目录扫一遍,找出不符合假设的例外,再回到程序里核对,变得更容易了。
这种变化带来的不只是速度。当一版工具已经能用了,我还有余力继续追问:为什么能用?是不是还有另一种版本?这个偏移究竟相对于哪里?导出再导入以后,没有修改过的部分能不能保持一致?
最近重新看 BF 和 BM2 时,这些追问也让我重新理解了旧工具:有些地方解释错了,为什么当年却能用?
但 AI 给出的解释也不能直接当成答案。有时候,它会根据几个现象给出一个很像那么回事的结论。我还是会追问:既然你说这里不对,为什么旧脚本导回去之后,游戏还能正常运行?
上面那条 FF 0010 就是一个很具体的答案。旧导出器把最后的整数拆成了另一条控制,但旧导入器又给每个块补回 FF,直接按顺序拼接。如果这些控制块没有被编辑,拼回去还是原来的 11 字节。对游戏来说,它收到的控制记录根本没有改变。错的是工具对记录的解释,不是这个例子回导之后的字节。
BF 的长度问题则是另一层。旧导入器在原来的槽位里覆盖消息块,文本变短时,后面的字符串池仍在原处。本作加载器又不是完全靠外层记录的长度去扫描文本,这能解释为什么某些没有超出旧槽容量的回导结果仍可运行。但如果变长超过旧槽容量,覆盖到后面真正会被读取的数据,就不能再靠“之前测试没问题”来保证安全。
这些区别很重要。不能看到一项长度没有同步,就直接断言游戏一定会崩;也不能因为当前画面正常,就断言这套写法能应付所有资源。最终还是要把字节、读取代码和实际执行路径对应起来。
所以,这一轮更准确的地方,并不是 AI 说得更有把握,而是我们能做更多交叉检查。哪些结论已经有读取代码的依据,哪些只是在文件样本中成立,哪些还需要进入游戏验证,可以分开记录。文件解析通过,也不会自动被当成修改后运行正常。
我花在机械操作上的时间少了,反而能多花一些精力理解问题。以前学过的知识并没有因此用不上:要判断一个解释是否合理、一个测试到底证明了什么,仍然需要那些基础。
拾起来的,也有当年的自己
两次重新开始,成就感其实有些不同。
2024 年更多是“我终于能做到了”。当年没有解决的图片、文本和打包问题,可以通过自己的分析和脚本一点点打通。即使结果还有粗糙的地方,它也很直接地让我看到了这些年自己的变化。
2026 年则多了一点“原来这里还能再弄明白一些”的乐趣。有了 AI 帮忙,我可以继续补那些以前没有精力细查的部分,也可以回头理解旧脚本为什么有效、又会在哪里遇到问题。这种成就感没有消失,只是从把一个功能做出来,延伸到了把它解释得更清楚。
当然,工具完成了,不等于整款游戏的汉化已经完成。文本翻译、图片修改、逐场景测试,仍然有各自需要投入的时间。对我来说,能够重新打开这个项目,继续做下去,就已经很好了。
“朝花夕拾”大概就是这种感觉。隔了很久,再把以前喜欢、却没能做完的事情拾起来,发现自己还愿意花时间折腾它,也确实比过去多懂了一点。
至于剩下的问题,就继续慢慢弄明白吧。


















