游戲漢化教程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灌水!)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM