首先想提醒大家的是,根据现行法律,未经著作权人等许可的游戏汉化是一种侵权行为,我曾经听说过一种说法,即只要不以营利为目的进行汉化则没问题,但我认为这是个有问题的说法。当然自我学习之用则可以。本心得以psv文字游戏《可塑性记忆》为例,向大家展示我汉化的流程。以下内容仅为个人观点,若有欠妥之处我会及时修改。下面是干货。
汉化总步骤:
|---- 解包
| (然后分析解包出来的文件,你至少得搞清楚要汉化的东西都在哪些文件里吧)
| |--- 提取文本
| |---文本汉化- | 翻译
| | |---替换文本
|---- |
| | |---P图
| |---图片汉化-|
| |---保存(是的,这一步非常重要而且让人掉头发)
|
|---- 制作字库
|
|
|---- 视频汉化(如果你想的话)
|
|---- 打包
- 首先准备一个mai版的游戏文件,一般是压缩包,解压后游戏文件尽收眼底。
接着初步分析目录:
- mai_moe文件夹:不用管,是安装mai用的
- movie文件夹:无需多说,该游戏内的一些视频
- sce_module文件夹:不知道干什么用的,里面文件后缀为.suprx,是psv的系统文件
- sce_sys文件夹:里面有些图片,随便看看你就知道是什么了,psv标志的贴纸和奖杯等
下面的文件都是未知格式xxx_body.bin和xxx_info.psb.bin等,而且是成组的,可以看到有几个占了很大内存,猜测游戏本体就是由这些文件组成。
注:解包这些未知格式文件就是最棘手的问题了,你可能需要大量时间在网上寻找是否有前辈提供解包工具,或者自己造轮子,但是我想大部分人是没有能力自己造的,所以只能碰运气去找,找到了就有汉化的可能,找不到就只能放弃。
这次运气好,在github上找到了某大神的一个工具FreeMote,貌似该工具的初衷并非用于汉化,但客观上确实能解包该游戏的重要文件,其详细信息在此不做赘述。(请正确上网,不FQ,github无需FQ)
2. 用FreeMote解包xxx_body.bin和xxx_info.psb.bin文件(解包方法略)
注:需要说明的是解包这类文件需要一个key,若想获得该游戏的key需要在未解包时的bin文件源码中找,总之挺麻烦。
以font_body.bin和font_info.psb.bin为例:解出来一个文件夹和两个json文件
文件夹内也是json文件。先在psv上打开游戏看一下游戏第一句话,记下来。用notepad随便打开一个json文件,search一下全文件(指定搜索路径可以提高效率)是否存在第一句话,有的话则说明文本已经解出来了。接下来只要找到文本文件即可。
虽可以直接在源文件上翻译文本,但效率过低,如能提取出来做成excel就可以批量翻译了,我选择python解决此问题。对于翻译这种小项目,无需使用面向对象的方式编程,简单的函数足以应付:
1. 提取文本小程序
1 import json #用于解析json文件 2 import csv #用于保存csv文件 3 4 def savefile(filename,words,name,nikname): 5 path2 = r"C:\Users\xxxxxx\scenariotest\get_word\{0}".format(filename) 6 with open(path2,'a+',encoding='utf-8') as csvfile: 7 writer = csv.writer(csvfile) 8 writer.writerow([name,nikname,words]) 9 csvfile.close() #该函数用于写入csv文件并保存 10 11 def get_word(path): 12 file = path.split("\\")[-1] 13 change = file.split(".")[0] 14 filename = change + ".csv" 15 fp = open(path,encoding='utf-8') #注意解码用utf-8 16 json_data = fp.read() #读取json文件 17 data = json.loads(json_data) 18 data01 = data["scenes"] #定位到一级标签 19 for m in data01: 20 if ('texts' in m.keys()): #定位到text标签 21 print("loading.......................") 22 for i in m['texts']: 23 words = i[2] #获取文本 24 name = i[0] 25 nikname = i[1] 26 if name == 'NULL': 27 name = ' ' 28 if nikname == 'NULL': 29 nikname = ' ' 30 savefile(filename,words,name,nikname) 31 else: 32 print("NULL") 33 continue 34 35 if __name__ == '__main__': 36 file_path = r"C:\Users\xxxxxx\pm1-01.txt.scn.m.json" # 改文件名 37 get_word(file_path)
思路:观察得知每个文件中“text”标签后即一堆数组,文本就是数组中的一个元素,用json包直接定位到文本即可,然后保存为csv格式。
使用方法:改一下程序中要提取的文件名后运行
运行结果:以示例文件名为例,生成一个文件:pm1-01.csv,用excel打开即可
2. 翻译小程序
虽可逐句手动翻译,但奈何我日语5级都难过,不如先让度娘翻译一遍,再修修补补即可:
1 import requests 2 import hashlib 3 import json 4 import csv 5 import time 6 # python3.0 已经取消了MD5,需要使用hashlib 7 # 该程序最后输出的文件为xxxfanyi.csv 文件只包含翻译结果,每隔一句空一行 8 9 # 获取csv中文本 10 def getword(path): 11 csvfile = csv.reader(open(path,'r',encoding='utf-8')) 12 words = [] 13 i = 1 14 for line in csvfile: 15 if i%2 == 1: 16 words.append(line[2]) 17 else: 18 pass 19 i = i + 1 20 return words 21 22 # 翻译函数,具体使用方法在api申请页面有教程 23 def fanyi(str): 24 q = str # 句子 25 ifrom = 'jp' # 翻译源:日语 26 to = 'zh' # 翻译到:中文 27 appid = 'xxxxxxxx' # id号 28 key = 'xxxxxxxx' # 密钥 29 salt = 'xxxxxxxx' # salt随便取一个 30 # appid+q+salt+密钥 的MD5值 31 sign_1 = appid + q + salt + key 32 sign = hashlib.md5(sign_1.encode(encoding='utf-8')).hexdigest() 33 # 产生url 34 url = 'https://fanyi-api.baidu.com/api/trans/vip/translate?q={0}&from={1}&to={2}&appid={3}&salt={4}&sign={5}'.format( 35 q, ifrom, to, appid, salt, sign) 36 try: 37 res = requests.get(url=url) 38 words = json.loads(res.text) 39 words = words['trans_result'] 40 words = words[0] 41 words = words['dst'] 42 return words 43 except BaseException as e: 44 print(e) 45 46 # 最后输出 47 def pack(list01): 48 path = r'C:\Users\xxxxxxxx\pm1-01fanyi.csv' # 改文件名,这是最终生成的文件名 49 f = open(path,'w+',encoding='utf-8') 50 writer = csv.writer(f) 51 for n in list01: 52 writer.writerow([n]) 53 54 if __name__ == '__main__': 55 # 获取word 56 path = r'C:\Users\xxxxxx\pm1-01.csv' # 也改一下文件名,这是之前生成的那个csv文件 57 list01 = [] 58 m = getword(path) 59 num = 0 # 计数用于调试 60 for j in m: 61 print('waiting.......') 62 a = fanyi(j) 63 list01.append(a) 64 time.sleep(1.5) 65 num = num+1 66 pack(list01)
思路:百度翻译有个api接口,自己申请一个就可以用了,实名后貌似可以每秒翻译10句,但网速实在不给力,而且我也懒得做错误处理了,就写成个半成品。直接利用之前生成的csv翻译再生成一个翻译版csv,就这么简单。
使用方法:在程序中改需要翻译的文件名后运行
运行结果:以示例文件名为例,生成一个文件:pm1-01fanyi.csv,用excel打开即可
3. 导入小程序
1 import csv 2 import json 3 # 本文件需要03.csv和03mid.csv两个文件 4 # 生成文件即03.csv,需要翻译名字 5 6 def csv_read_word(path): 7 strword01 = [] 8 flag = 1 9 file = csv.reader(open(path, 'r', encoding='gbk')) 10 for line in file: 11 if flag%2 == 1: 12 # print(line) 13 strword01.append(line[0]) 14 flag = flag + 1 15 return strword01 16 17 def csv_read_name(path,h): 18 strword02 = [] 19 flag = 1 20 flag2 = 0 21 file = csv.reader(open(path, 'r', encoding='utf-8')) 22 for line in file: 23 if flag%2 == 1: 24 strword02.append(line[h]) 25 flag = flag + 1 26 return strword02 27 28 def recsv(word,name,nikname,csv_path): 29 with open(csv_path,'w+',encoding='utf-8') as csvfile: 30 writer = csv.writer(csvfile) 31 for i in range(0,len(word)-1): 32 # print([name[i],nikname[i],word[i]]) 33 writer.writerow([name[i],nikname[i],word[i]]) 34 csvfile.close() 35 36 if __name__ == '__main__': 37 mid_path = r'C:\Users\xxxxxxx\pm1-01mid.csv' # 改之前的pm1-01fanyi.csv文件名为这里的文件名 38 csv_path = r'C:\Users\xxxxxxx\pm1-01.csv' # 改文件名,为源文件名 39 word = csv_read_word(mid_path) 40 name = csv_read_name(csv_path,0) 41 nikname = csv_read_name(csv_path,1) 42 recsv(word,name,nikname,csv_path)
思路:这个程序就有点无厘头了,不知道我当时怎么想的。。。总之有很大的修改空间。具体来说就是将之前翻译好的pm1-01fanyi.csv导入到源文件中,即第一个程序的逆向,没有什么技术难度。
注:这个程序一个大问题是,因为galgame的文本有对应的人物名字,我在第二个翻译程序中忘了翻译人名,导致这里想再翻译人名就很麻烦。
使用方法:修修补补完成pm1-01fanyi.csv文件翻译后,将该文件的文件名改为pm1-01mid.csv,然后改一下程序中的文件名,运行
运行结果:导入完成。
至此,文本翻译告一段落。
1. 该游戏中,游戏菜单等内容并非以文本形式存储,而是图片,因此还要翻译图片。和解包文本相同,解包图片后是一堆png格式图片,用photoshop进行修图翻译即可。(这里要感谢FreeMote工具的大佬帮忙更新了工具,能够打包了,否则只能解包,图片汉化就不可能了)
2. 保存:保存之所以成为一个问题,是因为如果保存格式不正确,放回游戏后显示会有问题,这就好比你用txt打开一个exe文件,改几下后再保存,虽然它可能还是一个exe后缀的文件名,但电脑可能完全运行不了它了。同理,虽然你用ps保存为png格式,但与原来的那个png已经完全不同了。这个问题我暂时没有找到好的解决办法,只能尝试改变保存时的各种参数来碰运气。最难过的是,游戏中不同的图片,保存格式也不同,而有一些图片你试完了所有ps提供的可变项后,还是无法正确运行,好在只有少数不重要的图片,不然你可能要气炸了(忙了大半个月了就因为这玩意,毁了所有努力啊有木有!!!)有了解图片格式的大佬可以自行研究解决。
关于视频汉化,就是加字幕了,该游戏的一些剧情是以视频形式表现的,但没有字幕,日语听力还是饶了我吧。。。我还未进行实践,自然不知道结果如何,用pr加字幕是没有问题,但是不是和图片一样有保存的问题就不得而知了。
字库也一度让我止步不前,但只要理解了原理,造轮子不是问题。
1. 字库是什么:游戏中的文本本质上是图片,字库就是一堆字的图片拼在一起而组成一张大图,有点像排好的活字印刷术的活字。当需要显示时,将映射表中的文字映射到大图中的某个坐标从而调取到字图,得以显示。
2. 怎么做字库:我找了很多生成字库的工具,但总有各种问题,如字图大小不一,顺序混乱等,最终只能自己解决了。
- 到网上找常用字集合,我找了一个3500字集合,做成txt
注:应当为一个矩形,不要一行多一行少
- 在解包的文件中找到字库,该游戏字库在font文件夹中,且有好几个字号的字库,不知道为什么要这么多字号,实际只需改其中一个即可,直观感受一下最终需要达到的成果
- 编程生成字库
字库小程序:
1 import os 2 from PIL import ImageFont,ImageDraw,Image 3 4 def Read(): 5 fp = open(r'F:\pycharm\xxxxxx\3500字_new.txt','r',True,encoding='utf-8-sig') 6 while True: 7 word = fp.read(1) 8 if not word: 9 break 10 yield word 11 fp.close() 12 13 if __name__ == '__main__': 14 m = 5 # x轴 15 n = 5 # y轴 16 wordlist = [] 17 text = Read() 18 im = Image.new("RGB", (2048, 2048), (255, 255, 255)) 19 dr = ImageDraw.Draw(im) 20 font = ImageFont.truetype(os.path.join("fonts", "goodfont01.ttf"), 22) 21 for word in text: 22 wordlist.append(word) 23 # 逐字写入 24 for i in range(1, 46): # i列 25 for j in range(1, 79): # j行 26 flag = (i-1)*78+j # 第flag个字 27 if flag <= 3500: 28 dr.text((m, n), text=wordlist[flag-1], fill="#000000", font=font) 29 m = m + 26 30 else: 31 break 32 print("{0}层完成".format(i)) 33 m = 5 # 换行定位 34 n = n + 26 35 im.show() 36 im.save('font_new.png')
思路:制作字库就三步,读取文字、定位、写入,读取文字即读取txt文件中的3500字中的一个,定位即定位到这个文字应该放在哪里,写入不说了。我使用了PIL包,先生成一张底图,再逐字写入即可,这个包还可以设定底图的颜色,字号,字体颜色等。但问题是我需要透明底图,PIL貌似没有透明这个选项。因此只能先白底黑字再ps反色抠图解决了,实际效果强差人意,因为我不知如何将字调为单色透明度,如该游戏字库图实际上是白色字且有透明度的,抠图的结果是一堆狗牙,但至少做到了排列整齐。
- 修改映射表
你可以在和字库图一起解包的json文件中找到,有很明显的规律,只要将里面文字的部分用3500字重新写一遍,覆盖原内容即可,程序很简单就不做解释了。
映射表小程序:
1 if __name__ == '__main__': 2 alljs = [] 3 jstext = [] 4 x = 1 5 y = 213 6 7 # get words 8 fp = open(r'F:\pycharm\xxxxxxx\3500字_new.txt','r',True,encoding='utf-8-sig') 9 while True: 10 word = fp.read(1) 11 if not word: 12 break 13 unit = '"first": {"a": 0,"b": 20,"d": 24.0,"h": 24,"height": 24,"id": 0,"w": 24.0,"width": 24.0,"x": second,"y": third}' 14 unit2 = unit.replace('first',word) 15 jstext.append(unit2) 16 fp.close() 17 18 # change x,y 19 for i in range(1,46): 20 for j in range(1,79): 21 flag = (i-1)*78+j 22 if flag <= 3500: 23 str01 = jstext[flag-1].replace('second',str(x)) 24 str02 = str01.replace('third',str(y)) 25 alljs.append(str02) 26 x = x + 26 27 print('完成{0}行'.format(i)) 28 x = 1 29 y = y + 26 30 print('完成') 31 32 # write file 33 fp02 = open(r'F:\pycharm\xxxxxxx\font24.txt','w+',encoding='utf-8') 34 for m in alljs: 35 m = m + ',' + '\n' 36 fp02.write(m) 37 fp02.close()
最终结果无需追求源码的缩进,只要符合json格式即可,如可以做成这样:
- 如果翻译中有些字在3500字中没有怎么办?先在映射表里找一个不常用的字,改掉它。再到字库图里把相应的字图改掉。(或者重新生成)
打包
用FreeMote反向操作一下即可。这一步的问题是为了方便安装,究竟是制作汉化补丁,还是整体打包替换,但该问题已经超出我想讨论的话题了。
结语
若完成以上所有工作,该游戏即汉化完成。但可知这其中仍有很多问题有待解决,如对于人名的汉化我并未完成自动化,图片的汉化尚存漏洞,字库的颜色问题,当然还有丑的不行的代码 T _ T。总之尽善尽美是很困难的。另外,上述方法也不具有通用性。最后希望这些看似已经脱离时代的东西能帮助到有需要的人吧。
最终效果: