Fork me on GitHub

游戏汉化教程2-资源分析

资源分析


   之前已经介绍过了整个游戏的汉化流程,我也提到过其实汉化的流程虽然简单,但是每一个步骤里面都包含了许多细节,甚至于有时候一个细节就会让整个汉化宣布失败。今天主要讲的就是第一个步骤,资源分析(包括资源的解包和封包),我用一个汉化实例的来说明,游戏是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灌水!)。

 

posted @ 2013-02-23 16:57  sweetwxh  阅读(6989)  评论(0编辑  收藏  举报