资源分析
之前已经介绍过了整个游戏的汉化流程,我也提到过其实汉化的流程虽然简单,但是每一个步骤里面都包含了许多细节,甚至于有时候一个细节就会让整个汉化宣布失败。今天主要讲的就是第一个步骤,资源分析(包括资源的解包和封包),我用一个汉化实例的来说明,游戏是PS3下的ICO HD(又叫古堡迷踪,PS2下一款经典老游戏),之所以选择这个游戏,是因为它很简单,上手非常容易,这个游戏没有字库,所有的文本都是以图片形式存在的,游戏汉化的主要工作就是解包资源,改图,然后封包。
我们先来看一下这个游戏的整体目录结构:
熟悉PS3游戏的朋友应该知道PS3的目录结构,实际上游戏的主要文件都是放在PS_Game/USRDIR下的,汉化的时候,也是主要处理这个目录下的资源文件。USRDIR下的最重要的文件就是那个EBOOT.BIN,我之前说过,PS3是一个类Unix系统,EBOOT.BIN就是PS3的可执行文件,实际上这个文件本身是ELF(Executable and Linkable Format,Unix-Like系统的可执行文件),但是Sony对这个文件进行了加密,所以变成了BIN,可执行文件的解密工具可以在这里下载(注意是C的源文件,要使用的话,请自行make一下,另外,这里就不提供Key了,需要的朋友可以去Google一下,EBOOT的解密和加密一定要学会,因为有些游戏的文本就包含在这里面,比如我们之前汉化的一个游戏,托托莉的工作室)。关于EBOOT.BIN,以后在教程中逐步讲解,今天不会对这个文件进行任何处理。
继续来看文件,除了EBOOT.BIN,其他文件基本都是资源或者游戏脚本,而ICO只有一个文件,就是ICO/ico.psarc,实际上,游戏厂商为了提升游戏对文件读取的处理效率,使用了一个封包,将所有的资源都打包了(不大包的话,会有很多零散的小文件,PS3读取起来那叫一个慢啊),打包方式采用了Sony自家的PlayStation Archivie格式(在PS3下,读取这个包里面的内容就像读取本地内容一样轻松)。为什么我会知道它是采用的Sony的封包格式呢,其实最开始我也不知道psarc是什么格式,主要也是Google告诉我的。这里说下题外话,做程序,无论是不是搞汉化,学会用搜索引擎是个很重要的技能。另外,推荐一个国外专门研究游戏(是研究游戏结构、资源、破解)的论坛,有一些高手,而且各种资源解包的BMS脚本也多,网址是:http://forum.xentax.com/,在搜索的时候,可以这样来用Google(建议使用www.google.ph,速度比hk快的多),“site:forum.xentax.com psarc”,这样就可以搜索站内相关资源了。OK,继续,PSARC的解包要用到一个psarc工具,下面我详细说明下(很多游戏都用到了psarc格式)。
PSARC工具的使用
该工具是个命令行工具,运行后如下:
最重要的功能有三个,create,extract,list。使用方法为:psarc.exe extract ico.psarc。其他的选项不过多介绍了,help里面写的非常详细,主要这里说下list和-a选项。
list是列出包里所有文件,为什么说它重要呢,因为打包的时候要用到!一般来说,默认打包会按照文件路径顺序打包,但是部分游戏会出现异常,比如ICO,我们汉化的时候,只要打包放回游戏,就回出现死机的问题,测试了一下午,最后发现必须按照打包顺序放回去。那么list的功能就出来了,它回输出文件的原始顺序,而这个打包工具提供了一个功能,--inputfile的选项,可以让你指定一个文件,来描述要打包的文件路径。例如:psarc.exe create --inputfile=list.txt。这样就可以保证游戏不出问题了(所有游戏都可以采用这个方式打包)。
不过这里需要注意一点,在cmd下运行list命令时,要这样写:psarc.exe list ico.psarc >> list.txt才会输出到文件,否则直接打印在控制台窗口中。另外,输出的文件列表每一行都包含了文件的大小信息,需要写一个程序来统一处理,把里面的每一行都换成与psarc.exe的相对路径。例如下面这个list.txt:
说明下,因为我在mac下不能用我上面提到的那个工具,所以我找了个linux版本的psarc,输出的列表有些差异,不过大同小异。好了,列表里面,比如第一行,我们就要去掉1 469.09mb这些与路径无关的字符串。下面的代码就是我之前写的转换ICO的List的(当时赶时间,顺便写了下,可以专门写成一个通用处理程序):
namespace ListConverter { class Program { static void Main(string[] args) { string[] alltxts = File.ReadAllLines("ico.txt"); List<string> temp = new List<string>(); foreach (string s in alltxts) { string t = s.Substring(0,s.LastIndexOf("(")-1); t = t.Substring(1, t.Length - 1); temp.Add(t); } File.AppendAllLines("ico_m.txt", temp); Console.WriteLine("complete"); } } }
上面差不多就是psarc的使用方式和注意事项,接下来我们就要解压ico.psarc并开始进行正式的资源分析了。
文件格式的分析
解压ico.psarc后,我们得到如下的文件结构:
一共37704个文件,1.9G解压出来有4个多G。之前我说过,这个游戏没有文本和字库(关于如何确定游戏的文本,我会在下一节讲解)。但是我才拿到这个游戏的时候,是如何确定它没有文本的呢。首先我们翻一下游戏目录,在bp_precache/text/menu_pal_eg/_ps3下,我们发现了一系列ctxr文件,这个是什么文件?起初我也不知道,先不急分析,Google一下(能有现成的为啥不用)。最后,还是在xentax发现了一片帖子,说这种文件实际上是dds图片格式的封装,首先要把ctxr转换成gtf文件,然后将gtf文件转换为dds,并且还附带了对文件头信息描述的说明,于是,先搜索了gtf2dds,一个转换程序,然后需要写个批量处理程序来转换ctxr,转换之前,首先要确认ctxr的文件头信息,所以WinHex是汉化必不可少的工具,用它来查询16进制,并研究里面记录的数据。我先把ctxr的头文件数据贴上来,然后比照我的转换代码可以看出是如何来分析并转换的。
到偏移位置0x80以前,都是头文件信息,之后的就是图片的数据了。转换的代码如下(记住转换程序一定要写双向的啊,xentax上大部分脚本都是有去无回的):
#region 引用 using System; using System.IO; #endregion namespace CtxrProcessor { internal class Ctxr { public void ToGtf(FileInfo file) { string baseName = file.Name.Replace(file.Extension, ""); string rcdPath = string.Empty; string gtfPath = string.Empty; if (file.DirectoryName != null) { rcdPath = Path.Combine(file.DirectoryName, baseName + ".dat"); gtfPath = Path.Combine(file.DirectoryName, baseName + ".gtf"); } Console.WriteLine("正在处理:{0}", file.FullName); using (FileStream fileReader = file.OpenRead()) { using (BinaryReader binReader = new BinaryReader(fileReader)) { using (MemoryStream fileData = new MemoryStream()) { byte[] fixHead = binReader.ReadBytes(0x18); fixHead[1] = 1; fixHead[2] = 1; fileData.Write(fixHead, 0, fixHead.Length); //需要记录的数据 byte[] recordData = binReader.ReadBytes(0xc); File.WriteAllBytes(rcdPath, recordData); byte[] imgData = binReader.ReadBytes(0x14); fileData.Write(imgData, 0, imgData.Length); byte[] zeroData = new byte[0x80 - fileData.Length]; fileData.Write(zeroData, 0, zeroData.Length); //写入基础数据 fileReader.Seek(0x80, SeekOrigin.Begin); int bodyDataLength = (int)(fileReader.Length - 0x80); byte[] bodyData = binReader.ReadBytes(bodyDataLength); fileData.Write(bodyData, 0, bodyData.Length); File.WriteAllBytes(gtfPath, fileData.ToArray()); } } } file.Delete(); } public void ToCtxr(FileInfo file) { string baseName = file.Name.Replace(file.Extension, ""); string rcdPath = string.Empty; string cxtrPath = string.Empty; if (file.DirectoryName != null) { rcdPath = Path.Combine(file.DirectoryName, baseName + ".dat"); cxtrPath = Path.Combine(file.DirectoryName, baseName + ".ctxr"); } Console.WriteLine("正在处理:{0}", file.FullName); using (FileStream fileReader = file.OpenRead()) { using (BinaryReader binReader = new BinaryReader(fileReader)) { using (MemoryStream fileData = new MemoryStream()) { byte[] fixHead = binReader.ReadBytes(0x18); fixHead[1] = 0; fixHead[2] = 0; fileData.Write(fixHead, 0, fixHead.Length); byte[] recordData = File.ReadAllBytes(rcdPath); fileData.Write(recordData, 0, recordData.Length); byte[] imgData = binReader.ReadBytes(0x14); fileData.Write(imgData, 0, imgData.Length); byte[] zeroData = new byte[0x80 - fileData.Length]; fileData.Write(zeroData, 0, zeroData.Length); //写入基础数据 fileReader.Seek(0x80, SeekOrigin.Begin); int bodyDataLength = (int)(fileReader.Length - 0x80); byte[] bodyData = binReader.ReadBytes(bodyDataLength); fileData.Write(bodyData, 0, bodyData.Length); File.WriteAllBytes(cxtrPath, fileData.ToArray()); } } } file.Delete(); File.Delete(rcdPath); } } }
#region 引用 using System; using System.Collections.Generic; using System.IO; using System.Linq; #endregion namespace CtxrProcessor { internal class Program { private static void Main(string[] args) { if (args.Length != 2) { PrintUsage(); return; } string option = args[0]; string path = args[1]; bool isFile = File.Exists(path); bool isDirectory = false; if (!isFile) isDirectory = Directory.Exists(path); if (!isFile && !isDirectory) { Console.WriteLine("指定的文件或路径不存在"); Console.ReadKey(); return; } Ctxr c = new Ctxr(); if (isFile) { FileInfo file = new FileInfo(path); switch (option.ToLower()) { case "c2g": c.ToGtf(file); break; case "g2c": c.ToCtxr(file); break; default: PrintUsage(); break; } } else { switch (option.ToLower()) { case "c2g": string[] cFiles = GetAllFiles(new DirectoryInfo(path), ".ctxr"); foreach (string cSingle in cFiles) { FileInfo cFile = new FileInfo(cSingle); c.ToGtf(cFile); } break; case "g2c": string[] gFiles = GetAllFiles(new DirectoryInfo(path), ".gtf"); foreach (string gSingle in gFiles) { FileInfo gFile = new FileInfo(gSingle); c.ToCtxr(gFile); } break; default: PrintUsage(); break; } } Console.WriteLine("处理完成\r\n按任意键退出"); Console.ReadKey(); } private static void PrintUsage() { Console.WriteLine("CtxrProcessor.exe c2g[g2c] [file|path]"); Console.WriteLine("c2g: ctxr转换为gtf\r\ng2c: gtf转换为ctxr"); Console.WriteLine("可以指定文件或路径(指定路径为批量处理)"); } private static string[] GetAllFiles(DirectoryInfo directory, string extension) { List<string> allFiles = new List<string>(); DirectoryInfo[] allDirectory = directory.GetDirectories(); if (allDirectory.Length > 0) { foreach (string[] files in allDirectory.Select(single => GetAllFiles(single, extension))) { allFiles.AddRange(files); } FileInfo[] fileInfos = directory.GetFiles(); allFiles.AddRange(from file in fileInfos where file.Extension.ToLower().Equals(extension) select file.FullName); return allFiles.ToArray(); } else { FileInfo[] files = directory.GetFiles(); allFiles.AddRange(from file in files where file.Extension.ToLower().Equals(extension) select file.FullName); return allFiles.ToArray(); } } } }
因为要考虑转换回去,我在上面多生产了一个dat文件,用来记录固定不变的数据,转换回去的时候好写回原文件,程序写好了以后,记住测试一下,就针对原始文件来转换并转回,然后比较MD5值,如果相同,那么程序基本就没有什么问题了。
得到gtf文件后,就可以用下载的gtf2dds.exe,dds2gtf.exe来进行转换,最后用PS打开(dds图片需要去Nvidia官方网站下载一个插件),你就发现,上面例子中的图片原来是Sony的Logo,你可以尝试修改一下放回游戏~
这里特别说明下,本身gtf2dds.exe和dds2gtf.exe是支持批量处理的,但是需要一个文件列表,不过我们不可能去手写这个列表啊,所以,最简单的方式是利用windows的搜索功能,搜索"*.gtf",然后windows就把列表给你做好了,你要做的就是将这些文件拖动到gtf2dds.exe程序图标上即可。
最后我们大概预览下生成的图片,就发现了游戏文本,原来都在图片里,游戏开发商太懒了。然后果断的顺便修改几个图片,打包回去放回游戏,好了,中文正常显示,汉化成功,接下来就是美工和翻译下体力了。
结束语
这个游戏的汉化过程其实非常简单,当初唯一难住我们的就是psarc封包的时候的顺序问题,不过不轻易放弃,不断的尝试各种方式,总会成功的。另外,很大一部分游戏的资源解包没有那么简单,有些时候,你就是找遍了各大网站,都找不到相关的说明或者工具,这个时候,就只有自己来分析头文件并编写程序了,这个才是真正的挑战,我在后面的教程也会不断加强汉化难度来讲解。最后,上面的所有源代码可以在Github上下载。另外,需要其他的工具的可以在站内PM我,或者在我Blog留言(一般我登陆我的Blog较多,博客园都是要写文章的时候才来,欢迎到我Blog灌水!)。