关于psv某文字游戏汉化的研究

关于psv某文字游戏汉化的研究

首先想提醒大家的是,根据现行法律,未经著作权人等许可的游戏汉化是一种侵权行为,我曾经听说过一种说法,即只要不以营利为目的进行汉化则没问题,但我认为这是个有问题的说法。当然自我学习之用则可以。本心得以psv文字游戏《可塑性记忆》为例,向大家展示我汉化的流程。以下内容仅为个人观点,若有欠妥之处我会及时修改。下面是干货。汉化总步骤: |---- 解包 | (然后分析解包出来的文件,你至少得搞清楚要汉化的东西都在哪些文件里吧) | |--- 提取文本 | |---文本汉化- | 翻译 | | |---替换文本 |---- | | | |---P图 | |---图片汉化-| | |---保存(是的,这一步非常重要而且让人掉头发) | |---- 制作字库 | | |---- 视频汉化(如果你想的话) | |---- 打包

解包:想必第一步就让好多人望而却步。就一个psv游戏而言:

首先准备一个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。总之尽善尽美是很困难的。另外,上述方法也不具有通用性。最后希望这些看似已经脱离时代的东西能帮助到有需要的人吧。

最终效果: