西方 Project 幡紫龙 C74 版汉化(魔改)记录
✨小透明・宸✨
2024-01-19 23:37:09

西方 Project 是一个弹幕射击类游戏系列,前两作由东京电机大学的学生社团 Amusement Makers 制作,第三作由前 AM 成员组成的“被瞬杀之道?(瞬殺サレ道?)”制作。此系列的三个游戏为秋霜玉、稀翁玉、幡紫龙,是基于在概念上对应 AM 的先辈 ZUN(后来成立了个人社团上海爱丽丝幻乐团)的东方 Project 而制作的作品,而前两作也有 ZUN 的参与制作及东方 Project 的客串出场。

……嘛,以上的介绍是从 THBWiki 复制粘贴过来的。总之在最近通关了东方 Project 所有 Windows 整数作的 Normal 难度之后打算试点新东西,再加上之前也有通关秋霜玉 Normal 的经历,于是就找到了幡紫龙。(也许以后还会试试稀翁玉?)

不过这一作似乎没有汉化?

在查找角色设定和剧情的相关资料的时候,偶然看到了在国外的车万论坛 Maidens of the Kaleidoscope 上关于西方各作的英文补丁的讨论帖,其中有人提到制作英文版的最大阻碍是对游戏数据包的封包,大概也是这么久以来幡紫龙一直没有汉化版的原因之一,另一个原因也许是太冷门了。

解包工具早就已经有人写出来了,也就是 NamelessLegacy 的 pbg6ext。然后 Clb184 制作了 BSRtk,可以反编译和编译游戏中的二进制脚本,不过仍然没有实现封包功能。

于是,从“随便试试看”这样的想法开始,我对幡紫龙的主程序动手了…_φ(・ω・` )

然后顺便就把汉化做出来了,想要下载的话可以去这里

不过,由于我的日语水平大概只有 N95 级别(笑),所以这个汉化版大部分是复制粘贴了 THBWiki 上现有的翻译,剩余的文本则通过 DeepL 和 ChatGPT 和脑补完成翻译(可能有人会在意这个),略有改动。可能会有不准确的地方……在这里留言指出的话,我会尝试修正的!

汉化是基于 C74 v3.001 版的主程序进行的(SHA-256:07f2033073f1c890dcd25324dcf81778e88e8e0039a1a1d52e4889c17b9b8091)。幡紫龙还有一个更早的 C67 版,不过关卡设计、游戏系统、界面等都有很大的差别,基本上可以当成两个不同的游戏了。我暂时没有汉化 C67 版的打算。

顺便一提,使用更新补丁时可能会由于编码问题导致无法找到游戏主程序而出错,这种情况下需要把游戏主程序的文件名改为 敠巼棾.exe(用 Shift-JIS 编码的“幡紫竜.exe”按照 GBK 解码的效果)。

PBG6 数据包格式介绍

幡紫龙使用的数据包,也就是那些扩展名 .ac6 的文件,暂且根据文件头的 magic number 记作 PBG6。

通过阅读 pbg6ext 的代码(当然自己从头分析也不难)可知,整个 PBG6 文件一共由三个部分组成:

  • 文件头(固定 16 bytes)
    • uint8_t[4] 数据包格式的 magic number PBG6
    • uint32_t 目录数据在数据包文件中的位置
    • uint32_t 目录解密后的大小
    • uint32_t 目录解密后的 CRC32
  • 加密的各个文件数据
  • 加密的目录数据

目录数据的开头是一个 uint32_t,用于记录数据包中包含的文件数量,之后是若干段各个文件的信息,每一段由以下数据组成:

  • uint8_t[] Shift-JIS 编码的以 0x00 结束的文件名,以根目录的斜杠开始,例如:/foo.bar
  • uint32_t 文件解密前的大小
  • uint32_t 文件解密后的大小
  • uint32_t 文件数据在数据包文件中的位置
  • uint32_t 文件解密后的 CRC32

知道了 PBG6 的格式,就可以自己写一个简单的封包工具了:

import os
import sys
import zlib

srcPath = sys.argv[1] if len(sys.argv) > 1 else input('Input ac6 path: ').strip()
ac6Path = srcPath + '.ac6'

tocContent = b''
tocCount = 0

with open(ac6Path, 'wb') as f:
    f.write(b'PBG6')
    f.write(b'XXXX') # toc pos
    f.write(b'XXXX') # toc size
    f.write(b'XXXX') # toc crc32
    for root, dirs, files in os.walk(srcPath):
        for file in files:
            srcFile = os.path.join(root, file)
            ac6File = srcFile.removeprefix(srcPath).replace('\\', '/')
            print(ac6File)
            with open(srcFile, 'rb') as g:
                d = g.read()
            tocContent += ac6File.encode('shift_jis') + b'\x00'
            tocContent += len(d).to_bytes(4, 'little')
            tocContent += len(d).to_bytes(4, 'little')
            tocContent += f.tell().to_bytes(4, 'little')
            tocContent += zlib.crc32(d).to_bytes(4, 'little')
            f.write(d)
            tocCount += 1
    tocContent = tocCount.to_bytes(4, 'little') + tocContent
    tocPos = f.tell()
    f.write(tocContent)
    f.seek(4)
    f.write(tocPos.to_bytes(4, 'little'))
    f.write(len(tocContent).to_bytes(4, 'little'))
    f.write(zlib.crc32(tocContent).to_bytes(4, 'little'))

……如果不考虑加密的话。

之前提过,PBG6 是使用了某种加密(或者说是压缩,这里就不区分了)算法来存储目录和文件数据的,而且该算法似乎不是 Deflate 或 LZ 系列之类的任何一种标准算法。

pbg6ext 之所以能实现解包,那是因为原作者直接照抄了从幡紫龙主程序反编译出来的解包部分的汇编代码,所以这工具的变量名也都是各种寄存器名称……

Actually, I didn’t write it. I just copied the code from Banshiryuu’s assembly, which is made obvious by the variable names in the decrypt function. I have no clue what this is actually doing or if this is some known algorithm.

Writing a repacker would be the actually interesting part, but this game is bland anyway.

——pbg6ext 的 README

如果手上只有这堆汇编代码的话,写出对应的压缩代码当然就是十分困难的事情了。显而易见,无法实现压缩的话也就无法实现封包,把汉化过的脚本和图片资源导入游戏什么的完全没办法啊……

… so we still need a hardcore hacker to have the tools which will allow us to extract and repack the game data. :V

——Maidens of the Kaleidoscope 上的讨论帖“SEIHOU Project English Patch Question”

对读取数据包的分析和魔改

翻出 IDA 把主程序拖进去分析。根据 pbg6ext 的那段从汇编转写过来的 C++ 代码,试着搜索对应的汇编代码,在尝试了很多次之后找到了位于 sub_42D8D0 的解密算法:

和 pbg6ext 的代码对比一下还是能辨认出来是同一个东西的,当然因为这是反编译的产物所以看上去糟了很多。

然后,可以通过动态调试下断点看调用栈,或者用 Sysinternals 的 procmon 检查使用 ReadFile 读取 PBG6 的前 16 bytes(也就是文件头)的地方,从而找到 sub_42D2C0 这个读取 PBG6 的函数:

  • v9 != 910639696 这个常数就是用小端序表示的 magic number PBG6,即 0x36474250
  • v7 = sub_42DC90((int)v6, v5, dwBytes), (v2 = (void *)v7) != 0 调用了解密算法。
  • v8 = sub_42DB50(v7, dwBytes, 0), v12 == v8 计算解密后数据的 CRC32,并检查和文件中记录的值是否一致。

其实一开始只知道 PBG6 使用了校验和,但是不知道具体算法,点进 sub_42DB50 也没能认出来,只是用解包后的文件随便试了一下才发现是 CRC32。不过就算不知道,把校验过程直接去掉也是可以的。

继续查看 sub_42DC90,可以看到这里分配了一块内存区域用于保存解密后的数据(调用的就是 sub_42D8D0,不过不知道怎么被反编译成了函数指针的样子),解密成功则返回这个区域的地址。

暂时先分析到这里。那我是怎么使封包变得可行,从而实现汉化的呢?在给出答案之前,可以试着自己大胆猜想一下。

可以稍微提示一下……这个汉化是魔改了主程序的,而使封包变得可行的方法在前面的某个地方已经给出线索了。

“……如果不考虑加密的话。”

原来的主程序是从数据包读取加密的数据,解密后再使用。既然我们无法自行实现封包的加密,那把这一层直接去掉不就可以了吗?

一边是对数据包进行重新封包,保持 PBG6 的格式不变,但是不进行加密的步骤。另一边是修改 sub_42DC90 的代码,把解密操作去掉,改成直接使用数据包中的数据,也就是把这部分改成一个 memcpy。这样就没有什么加密解密的事情了。

sub_42DC90 对应的汇编如下:

而我的操作就是从 .text:0042DCA8 开始(这部分以上是 if (!v5) return 0;),用汇编的串传送指令(还好当时学的汇编语言没完全还给老师……)实现这个简单的 memcpy,再处理好返回值和栈平衡,剩下的部分用 nop 填充:

arg_0 = dword ptr 4 ; 解密前数据的地址
arg_4 = dword ptr 8 ; 解密前数据的大小
dwBytes = dword ptr 0Ch ; 解密后数据的大小

push ebx
mov  ebx, [esp+4+dwBytes]
push esi
push edi
push ebx ; dwBytes
push 0 ; uFlags
mov  edi, ecx
call ds:GlobalAlloc
mov  esi, eax ; 此时esi存储的就是v5
test esi, esi
jz   short loc_42DCC6 ; if (!v5) return 0;
; 从这里开始覆盖
; 从esi向edi复制ecx字节,通过rep movsb调用
push esi                   ; 56
mov  edi, esi              ; 89 F7
mov  esi, [esp+010h+arg_0] ; 8B 74 24 14
mov  ecx, [esp+010h+arg_4] ; 8B 4C 24 18
rep  movsb                 ; F3 A4
pop  esi                   ; 5E
mov  eax, esi              ; 89 F0
pop  edi                   ; 5F
pop  esi                   ; 5E
pop  ebx                   ; 5B
retn 0Ch                   ; C2 0C 00
nop                        ; 90

如此修改之后,IDA 也能认出它是 memcpy 了:

实际运行测试一下,主程序也能正常读取重新封过的去掉了加密的数据包。这样对于汉化来说最大的阻碍就解决了。

修改编码和字体

前面也提过,主程序使用的是 Shift-JIS 编码,汉化的话就需要改成 GBK 编码。创建字体使用的是 CreateFontA,其中参数 DWORD iCharSet 就是用来设定编码的。sub_4257B0 调用了这个函数:

中间的 0x80 (SHIFTJIS_CHARSET) 需要改成 0x86 (GB2312_CHARSET)。由于还没有修改游戏脚本,所以这个时候进入游戏会看到把 Shift-JIS 编码的文本当成 GBK 编码而出现的乱码。

有些游戏不仅需要改编码,还会进行字符边界检查,保证文本数据的每个字节都在 Shift-JIS 的范围内。不过幡紫龙没有添加边界检查。

另外,原来的无衬线体在这里变成了宋体,这也是因为修改了编码后就无法找到字体了。在这里下断点可以找到使用的字体名称:

  • .rdata:0046EF88 82 6C 82 72 20 82 6F 96 BE 92 A9(Shift-JIS 编码的“MS P明朝”)
  • .rdata:0046EF94 82 6C 82 72 20 82 6F 83 53 83 56 83 62 83 4E(Shift-JIS 编码的“MS Pゴシック”)
  • .rdata:0046EFA4 82 6C 82 72 20 96 BE 92 A9(Shift-JIS 编码的“MS 明朝”)
  • .rdata:0046EFB0 82 6C 82 72 20 83 53 83 56 83 62 83 4E(Shift-JIS 编码的“MS ゴシック”)

分别改成 GBK 编码的“宋体” CB CE CC E5 和“黑体” BA DA CC E5

类似地,可以通过搜索调用 CreateWindowExA 的地方和断点调试找到窗口标题的位置:.rdata:0046D464 94 A6 8E 87 97 B3 20 81 60 82 CE 82 F1 82 B5 82 E8 82 E3 82 A4 81 60 (Shift-JIS 编码的“幡紫竜 〜ばんしりゅう〜”),改成 GBK 编码的“幡紫龙” E1 A6 D7 CF C1 FA

之后就是修改脚本、改图嵌字之类的和主程序修改无关的琐碎工作了。

封面图:Pixiv ID: 110299012 「:p」 by 白河

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。不允许内容农场类网站、CSDN 用户和微信公众号转载。
本文作者:✨小透明・宸✨
本文链接:https://akarin.dev/2024/01/19/banshiryuu-modification/
下一篇 chevron_right