資源分析
之前已經介紹過了整個游戲的漢化流程,我也提到過其實漢化的流程雖然簡單,但是每一個步驟里面都包含了許多細節,甚至於有時候一個細節就會讓整個漢化宣布失敗。今天主要講的就是第一個步驟,資源分析(包括資源的解包和封包),我用一個漢化實例的來說明,游戲是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灌水!)。