寶藏獵人整合包(Vault Hunters)個人漢化筆記/VaultPatcher硬編碼漢化模組使用指南-上


寫在最前面
這篇專欄本意是記錄我在漢化過程中的一些收獲、問題和心得體會,此漢化版本僅供我個人使用,不會發(fā)布在公共平臺上。
寶藏獵人的官方授權(quán)漢化請參見VM漢化組的專欄。這里的個人漢化版本有部分內(nèi)容參考了VM的漢化文檔。
寶藏獵人大概是我第一個完整游玩、完整漢化的整合包,在這里要感謝VM漢化組的各位、KlparetlR和硬編碼漢化模組的作者3093FengMing(X209)。

第一次接觸寶藏獵人(以下簡稱VH)大概是22年的寒假期間,學(xué)校的幾個朋友推薦了這個整合包,三四個人一起靠著VH度過了整個寒假。那個時候VH差不多是剛出第三個,也就是基于1.18.2的版本(3.0),一直到現(xiàn)在更新到了Update 11(3.11)??上У氖悄壳癡H還屬于alpha測試階段,正式版可能還要等4-5個版本,而且測試階段也不會把核心的vault mod模組開源,這在一定程度上為漢化工作帶來了一些困難。

從零開始的漢化流程
由于VH不是國人做的整合包,從curseforge上下載打開,內(nèi)置漢化肯定是不用指望了。進去以后調(diào)成中文,首先就看到英文的文本按鈕

結(jié)合打開過程中自定義的加載ui和背景,一定是用了某個自定義主界面的mod,查過mod列表發(fā)現(xiàn)是packmenu。packmenu對于按鈕文本的存儲采用了lang的形式,在版本文件夾/packmenu/resources/assets/packmenu/lang下面有en_us.json,直接復(fù)制一份,重命名為zh_cn.json,并修改圖中所示的文本為中文即可。

lang部分
進存檔之后加載i18n的通用漢化資源包,確認可以正常生效后我選擇了提取VH中全部的modid進行篩選,把資源包中其它不需要的漢化文件刪去并重新打包,使得資源包大小縮減到了幾百KB,也方便后續(xù)的修改和管理。
VH的核心mod是作者團隊制作的vault mod,截至目前(3.11)有上百種物品和方塊,組成了整合包的大部分主線內(nèi)容。絕大部分物品的名稱可以通過lang文件的形式進行漢化。由于是專門定制的mod,必然不會被包含在i18n的通用包中。我從mod本體的jar包中提取出lang文件(assets/the_vault/lang/en_us.json),仿照通用資源包的原有格式加入到資源包中,這樣可以確保在不修改mod的前提下通過加載單一資源包實現(xiàn)全部漢化。
大概是幾個月后(也許是之前?),我偶然看到VM漢化組發(fā)布了VH的漢化補丁。當時還沒有和漢化組有什么聯(lián)系,只是查看了補丁內(nèi)容并對自己的翻譯做了一些調(diào)整(其實也發(fā)現(xiàn)了之前漢化組的一些紕漏www),想的是由于只是個人使用,所以借鑒一下他們的內(nèi)容也未嘗不可。
另外,也檢查到其它的一些模組有英文名稱的問題,比如ispawners和cagerium。查過之后發(fā)現(xiàn),這些模組都是為了VH而定制的,難怪不包括在通用漢化包中。我接下來的工作就是一點點把這些lang補全。過程其實很簡單,都是從對應(yīng)的模組jar文件中提取出英文lang文件,改成中文后丟進資源包中,就可以完成這部分的漢化。只是有些模組的key有點多,導(dǎo)致工作量還蠻大的(比如模塊化路由器的手冊是全文寫進了lang文件,到現(xiàn)在我還沒完全寫完對應(yīng)的內(nèi)容)。
當然,對于核心的vault mod,在寫完lang文件后還是存在一些物品名稱沒有漢化,比如傳奇寶藏、廢料升級吊墜等等。這些部分直到后來引入了新的工具才得以解決。
(另外,說到傳奇寶藏,這似乎是上個版本1.16時期的物品。由于VH3是在VH2的基礎(chǔ)上開發(fā)而來的,因此其實有相當一部分舊版本的遺留產(chǎn)物,這些舊的物品、配置和代碼到目前也只有一部分被加上了old的標識,需要體驗完VH3大部分的流程以后才能區(qū)分出來哪寫是舊版物品,希望VH3在正式版的時候可以把這些歷史遺留處理干凈。)

一個臨時的tooltip處理方式
我個人的初版漢化就僅限于上述的物品名稱部分。然而玩過VH的人都知道,游戲中還有大把的tooltip、gui等等均為英文。初版漢化連續(xù)用了幾個版本,直到有一天在b站偶然看到一個VH的直播,發(fā)現(xiàn)有人做了一些tooltip,主要是裝備詞條的漢化。詳細了解后,發(fā)現(xiàn)是一個專門的mod(原諒我找了很久都沒找到作者是誰)。下載后發(fā)現(xiàn),只是一個專門翻譯裝備詞條的mod,不過對于當時的漢化完成度來說已經(jīng)是很大的進步了。
但是我很快發(fā)現(xiàn)這個mod所漢化的內(nèi)容是封裝在mod內(nèi)部的,以我當時對于java和mcmod的理解還不能自由修改或增加內(nèi)容(本人以前從來沒學(xué)過java,后面關(guān)于代碼的一切操作基本都是充分發(fā)揮了大學(xué)時候?qū)W的計算機語法基礎(chǔ),以及英文閱讀水平。當然后來現(xiàn)學(xué)現(xiàn)用也了解了很多)。后來經(jīng)過多方面搜索,我獲取了在整個漢化流程中使用的第一個附加工具——Java bytecode?editor,簡稱jbe,翻譯成中文就是Java字節(jié)碼編輯器。后來我才了解到,字節(jié)碼編輯屬于jar包反編譯的一種形式,所有的mod都是以java源碼寫好,編譯后得到字節(jié)碼class文件并實際構(gòu)建成包使用的。這些涉及到模組開發(fā)方面的知識暫時不詳細展開。
jbe可以直接查看class文件的字節(jié)碼內(nèi)容,并對其中的字符串部分進行修改。我以壓縮文件的形式打開了那個裝備漢化mod,發(fā)現(xiàn)其中只有一個class文件。我用jbe打開了這個文件,并在replaceWord方法(當時甚至不知道什么是方法,只是隨便翻到了這個replaceword覺得應(yīng)該在這里就打開了)下找到了如下形式的字節(jié)碼:

這個形式一眼看懂,而且這里的文本是可編輯的形式,顯然只需要簡單的復(fù)制粘貼就可以在里面增加我想要的內(nèi)容了。我試著把游戲中剩余的英文字符串寫進去,并反復(fù)調(diào)整,最終發(fā)現(xiàn)可以漢化大約一半的tooltip、前面提到的傳奇寶藏(不過那個升級吊墜不行)名稱和一些奇怪的地方,不過各種gui內(nèi)的英文文本,以及不是vault mod模組的部分則完全不行。
另外,我還注意到這個mod對于重復(fù)字符串的處理方式。例如上圖中的"Mana Regen"和"Mana",這是兩個獨立的詞條,應(yīng)分別翻譯成魔力再生和魔力值,但其中的Mana字符串是共用的。這里具體表現(xiàn)為,如果我在jbe中先寫"Mana",再寫"Mana Regen",那實際游戲中就會錯誤地顯示為"魔力值 Regen"。推測其原因應(yīng)該是從代碼中讀取一個替換的字符串,替換后再讀取下一個,這樣替換過Mana后就找不到Mana Regen了。解決方法也很簡單,就是把具有重復(fù)部分的幾個字符串,較長的寫在前面,例如圖中先寫"Mana Regen",就不會對后面替換"Mana"造成影響。
為了深入了解此mod的替換原理,我獲取了第二個附加工具——Java Decompiler,簡寫為jd-gui。這個工具可以直接打開jar包,并方便地讀取其中包含的全部class文件反編譯后的源碼。調(diào)試好以后,我順利地打開了此mod的源碼,并在其中找到了如下字段

順便一提,從這個時期開始我算是接觸到了模組代碼的領(lǐng)域。整個VH都是基于Java運行的,其代碼自然是遵循了Java語法,而我也開始嘗試閱讀并理解這些代碼的用途。當然,即使到現(xiàn)在我也不可能獨立寫出哪怕一個單獨的java源碼文件。不過,我也可以直接以理解英文的角度去理解代碼的含義。我注意到這兩個if判斷語句,第一個是“如果注冊名為空,那么什么也不做并且返回”。return在各種語言中幾乎都有所出現(xiàn),代表當前函數(shù)結(jié)束的意思,那么我可以理解為是在說要獲取一個注冊名,如果注冊名為空可能代表獲取失敗了,那么什么也不做并結(jié)束當前代碼。而第二個判斷條件中,顯然!代表非,而后面的equals代表等于,這里應(yīng)該是檢查這個注冊名,如果不等于"the_vault"就什么都不做并返回,和前面的情況相同。
等等,"the_vault"不是vault mod的id嗎?由此,我推斷出此處是在讀取其它模組代碼中出現(xiàn)的字符串,并且篩選出只來自于注冊名(即modid)為"the_vault"的字符串。顯然這很好地解釋了前面提到的此mod只對vault mod的字符串生效的問題。很遺憾的是,jd-gui只是提供了查看功能,并不能對代碼進行任何修改操作;而jbe中也找不到這一字段,于是這里的修改只好作罷。
接下來的一行代碼我并不能理解,只能看出是定義了一個變量toolTip,其值為getToolTip的返回結(jié)果。由此,我大膽猜測這段內(nèi)容是此mod只能替換tooltip的原因,即實際的字符串只get了tooltip,因此只能替換tooltip中包含的內(nèi)容。不過為什么傳奇寶藏的物品名可以替換,到現(xiàn)在也未得而知。后面的所有代碼大概是在講用寫好的字符串去替換原來的英文字符串的實現(xiàn)過程,這里不展開描述了。

config處理階段
有一次,我在翻VH的文件時偶然發(fā)現(xiàn)其模組配置文件目錄下(版本文件夾/config/the_vault)有一個名為tooltip.json的文件??催^其中的內(nèi)容后我豁然開朗:原來我前面不能漢化的部分tooltips是被存到了這里,怪不得那個替換mod不起作用。

我立刻把其中的幾條修改成中文,并打開游戲測試效果。然后我傻眼了——被我修改過的部分全部變成了亂碼這里就不還原當時的截圖了。無奈,我以java、中文、亂碼等關(guān)鍵詞搜索,經(jīng)過一番艱苦的學(xué)習后我了解到,寫進json文件的中文字符在被java讀取時,默認會采取gbk編碼;而若想在游戲中正確顯示中文字符,需要在寫入json文件時直接使用中文的unicode碼存儲。


另外,以上對于文件的瀏覽和編輯均在Visual Studio Code(簡稱VSC)中進行。VSC對于大部分格式的文件都提供了對應(yīng)的擴展支持,可以方便排版和搜索等操作;還支持打開文件夾進行遍歷和全局搜索等功能,在稍后提到的步驟中為我節(jié)省了很多不必要的工作。在KlparetlR的建議下,我在VSC內(nèi)安裝了Native-ASCII Converter插件,可以通過設(shè)置固定的快捷鍵快速轉(zhuǎn)換中文與其對應(yīng)的unicode編碼。
處理完編碼問題后,vault mod的幾乎全物品的tooltip都解決了。受此過程啟發(fā),我開始遍歷版本文件夾/config/the_vault下面的所有文件。不過也不是漫無目的地遍歷,主要目標集中在display、name、tooltip、format、description等關(guān)鍵字相關(guān)的字符串上。同時,VM漢化組發(fā)布了適配新版本的漢化補丁(好像是U4-U6版本),在互相比對校驗之后我更加確定了前面關(guān)于編碼和config文件內(nèi)容顯示到游戲中的猜想。
經(jīng)過對config文件的遍歷,整個整合包的漢化完成度又前進了一大步,包括技能描述、寶庫修飾語(在VM發(fā)布的版本中對應(yīng)譯名為詞綴)、后來新版本的任務(wù)書描述等。

硬編碼處理階段
在上述工作完成后,漢化進度在很長一段時間之內(nèi)停滯不前。此時還有大批量的內(nèi)容沒有翻譯,包括各種gui,各個hud的文字部分,以及大多數(shù)聊天信息等。直到Update8更新后好久,我依然是偶然從b站上看到某個VH的直播,發(fā)現(xiàn)主播居然使用了大部分gui都是中文的漢化版本。然后我才注意到VM漢化組不知道什么時候更新了新版本的漢化適配,補充了對于大部分gui等信息的漢化。檢查了新的漢化補丁后,我發(fā)現(xiàn)他們引入了新的漢化mod——VaultPatcher(以下簡稱VP)。隨后,我在mcmod網(wǎng)站的相關(guān)鏈接處找到了該mod的github鏈接,并在其中詳細了解了VP的用法、原理以及配置示例。
VP的中文名為硬編碼漢化模組,為什么英文名為VaultPatcher呢?引用相關(guān)開發(fā)人員的話:
最開始這個mod是專門為了VH漢化開發(fā)的,于是起名VaultPatcher(保險庫補丁)。后來發(fā)現(xiàn)其潛力相當大,可以用于同mc版本下的大部分其它mod,于是改名硬編碼漢化;然而英文名和包名已經(jīng)決定好了就沒有再改。
總而言之,VP的工作原理是指定在某個mod或是全局搜索源碼中的指定字符串,并以對應(yīng)的中文譯名進行替換。詳細的原理和使用方法會在后面展開描述。
經(jīng)過此時的硬編碼漢化后,已經(jīng)有絕大多數(shù)英文內(nèi)容都處理完畢了。剩余的內(nèi)容,有些是由于當時VP的bug,有些是由于找不到對應(yīng)的源碼,或是由于其它原因,暫時還無法替換。
按照時間順序,我決定先進入此專欄的另一核心部分——VaultPatcher使用說明。

VaultPatcher 硬編碼漢化模組的使用說明
VP是國內(nèi)的一位模組作者,X209開發(fā)的用于替換模組源碼內(nèi)字符串的mod。在我剛接觸到此mod時,其版本為1.2.10(以下簡稱2.10)。
在這個時期,我已經(jīng)熟練掌握了用jd-gui打開mod的jar包并瀏覽其源碼,并簡單了解了一個mod所必需的代碼框架。我發(fā)現(xiàn),mod中對于游戲內(nèi)的顯示內(nèi)容大多數(shù)都以TextComponent("內(nèi)容字符串")的形式表示,而有另一部分會寫成TranslatableComponent("本地化key"),也就是可翻譯的形式。前者是直接填入要顯示的內(nèi)容,而后者是寫入一個lang文件中的key字符串,然后就可以在資源包中對對應(yīng)的內(nèi)容進行多語種的適配。后者的這種漢化方式被大部分模組作者所使用,也算是java為mod開發(fā)人員提供的通用的本地化接口;而前者是相當于把游戲內(nèi)顯示的內(nèi)容直接寫進了模組源碼中。這里就引入了硬編碼的概念:即顯示文本直接在源碼中,不能通過lang文件進行修改。


為了翻譯這部分源碼中的內(nèi)容,首先直接想到的方法是直接修改這部分內(nèi)容字符串。然而,大部分情況下,修改模組jar包都需要重新構(gòu)建,而這需要mod本身開源,才能獲取到必要的依賴庫等等。另外,部分模組作者規(guī)定了經(jīng)過修改的mod本體不允許公開分發(fā),這對于漢化組的工作來說是致命的。

1.2.10說明
我大概在今年的6月初加入了VP的討論群,然后才得以深入了解VP的種種。這里要強調(diào)一下,在從我接觸到VP開始的相當長的一段時間內(nèi),VP的可用最新版本為1.2.10,其工作原理與不久前更新的新版本VP(1.2.13/1.2.14)大相徑庭。我在這里貼一下模組作者給出的2.10的工作原理圖。

可以看到此時VP是在對應(yīng)的英文文本顯示到屏幕上時將其替換為事先配置好的中文翻譯。2.10版本的VP在配置時,會在版本文件夾/config下面生成名為vaultpatcher的文件夾,內(nèi)含兩個文件:config.json和mods.json。后者的文件名可以任意設(shè)置,只需要寫好對應(yīng)即可。其具體的使用方法,需要首先了解config.json的結(jié)構(gòu)。

分為三個部分:mods、debug_mode和optimize_params。其中optimize_params的配置在通常情況下保持默認即可。mods中填寫的是config/vaultpatcher下的另一個json的文件名,該文件是用來存儲需要替換的字符串以及替換后的內(nèi)容的。這里我的另一個文件為3112.json,所以我填寫的是"3112"。注意這里不需要填寫后綴名.json,以及文件名需要用英文雙引號包括。(由于一些bug,2.10只能加載單個模塊,即mods部分只能填一個文件名。不過影響不大,全部內(nèi)容都塞進一個文件就行)
debug_mode部分下為開發(fā)人員所可能用到的配置。如果僅僅是使用該mod而不需要進行漢化工作,那么完全可以保持其默認配置不變(即圖中所示),其詳細用法會在后面調(diào)試部分說明。
然后來關(guān)注一下VP所漢化內(nèi)容的本體,即上文提到的3112.json,全部的翻譯內(nèi)容都會記錄到這里。該文件的內(nèi)容是分塊儲存的,每個塊對應(yīng)了一個要翻譯的字符串以及翻譯后的目標字符串。

文件開頭第一個塊與翻譯內(nèi)容無關(guān),用來記錄該文件的一些信息。從圖中可以看到一共包含了4個鍵值:authors(作者)、name(模塊名稱)、desc(模塊描述)和mods(適用模組)。需要注意的是,此部分內(nèi)容只用于顯示,是方便讓編輯人員看到此文件時了解基本信息所用,也可以在游戲中使用/vp list命令查看此部分信息。因此,這部分可以隨意填寫,甚至留空也不會有問題。

接下來的每個替換塊均描述了一個需要替換的字符串,其形式如圖所示??梢钥吹桨巳齻€鍵值:target_class、key和value。其中target_class中又包含三個子鍵值:name、method和stack_depth。簡單來說,target_class描述了所要替換字符串的位置,key是要替換的字符串,value是替換后的字符串。也就是,在target_class處找到key,并將其替換為value。多么簡單
首先說明key部分。通常來說,key部分填入的都是mod源碼中英文雙引號包括起來的內(nèi)容,這些部分一般會在各種代碼查看器(如VSC和jd-gui)中以特殊顏色標注,便于尋找,也為我后面的遍歷工作節(jié)省了不少時間。例如我們可以填入前文中圖示的Health Points,相當于VP在運行時就會搜索與你輸入相匹配的字符串并嘗試替換。注意這里需要區(qū)分大小寫,以及注意首尾的空格。
value和key類似,是存儲對應(yīng)譯名的地方,例如前面的例子中,我們填入生命值,就會在游戲中把Health?Points替換為生命值并顯示。不過,有一點不同的是,value在填寫時有兩種方式:一是直接填寫中文翻譯,被稱為完全匹配;也可以選擇在譯名前加一個@,如上圖示例所示,此時的匹配方式稱為半匹配。二者的區(qū)別在于,完全匹配要求key部分的內(nèi)容必須為完整的字符串,也就是在代碼編輯器中以特殊顏色標注的部分必須原封不動地復(fù)制到key部分,且源碼中不能有其它附加部分或是格式化的代碼;而半匹配方式是只要搜索到與key相匹配的字符串,就會直接進行替換。具體如何進行選擇,典型的例子像這樣:


另外,2.10版本不支持通配符。如果字符串中包含如%d、%s這樣的通配符,那么實際在寫字符串時需要從通配符隔開,也就是前后部分分別寫一個替換塊,分別進行翻譯,并且都必須使用半匹配的模式。
(不難發(fā)現(xiàn),半匹配在不考慮其余影響的情況下是兼容完全匹配的。所以有個小小的tips就是如果用完全匹配發(fā)現(xiàn)不能替換時,試試只是加一個簡單的@吧)
接下來說說target_class。將一個mod的jar包用壓縮文件打開,在除了assets、data、META-INF這三個文件夾之外有一個名字不確定的文件夾(默認為com,不過大部分mod中都改成了與mod名或作者名相關(guān)的名稱),其中存儲了該mod全部的源碼,此文件夾內(nèi)逐級展開,最終會發(fā)現(xiàn)全部是class類型的文件。那么,這里的target_class直譯就是目標class文件的意思,限定了目標字符串的搜索范圍,也就是說定義了前面所說的在target_class處對該塊內(nèi)的字符串進行替換。其中有三個子鍵值,這里一一說明。
name是必須填寫的部分,規(guī)定了詳細的class文件的路徑。其寫法是,獲取class文件在jar包中的路徑,各級文件夾之間用英文字符"."隔開。例如在

這一模塊中,"Hold "所處的位置是iskallia.vault.event.ClientEvents,則其實際路徑為

?而此class文件包括了"Hold "的部分為(使用jd-gui查看)

也就是說,這里的target_class要精確寫到具體的文件名及路徑,且需要刪去.class后綴名。那么,如果每個字符串都需要這樣寫class文件名稱,是不是有些過于繁瑣了呢?因此2.10提供了name鍵值的三種匹配方式:精確匹配、包匹配、類匹配。
前面所描述的為精確匹配方式,顧名思義就是需要寫出完整的路徑和文件名。
而包匹配是定位到對應(yīng)的package,其實現(xiàn)方法是在name的內(nèi)容前加上@。對于了解java語法的人應(yīng)該很容易理解;如果沒有接觸過java,那么可以簡單這樣理解:包匹配相當于文件夾匹配,例如上面的iskallia.vault.event.ClientEvents,用包匹配可以寫為@iskallia.vault.event,是不是省略了后面的class名稱從而達到縮減工作量的目的呢?當然,代價就是從搜索單一文件變成了搜索整個文件夾下的所有文件,一方面會使效率和性能有所下降,另一方面如果同一文件夾下的其它class文件中也有相同的字符串,那么就會出現(xiàn)誤匹配的問題。更進一步,上面的例子可以進一步縮寫為@iskallia.vault,甚至是@iskallia。當然也會使得搜索范圍一步步擴大。
類匹配,按照模組作者的說法是結(jié)尾(class名)匹配,實現(xiàn)方法是在name的內(nèi)容前加上#。也就是說,前面的iskallia.vault.event.ClientEvents理論上可以縮寫為#ClientEvents(其實我也不知道對不對。。。)。據(jù)不少人反饋,類匹配似乎有比較嚴重的bug,所以這里就不詳細說明了,總之是不推薦使用(
接下來看另外的兩個子鍵值method和stack_depth。
method是指方法名。方法名是java中的專有名詞(實際上我直到幾個星期之前才了解其概念并會在VP中使用),對于沒有java基礎(chǔ)的人來說可能難以理解。不過沒關(guān)系,方法名在大部分情況下可以留空!
(下面是一個具體例子,如果不需要用到method可以跳過)
填寫method鍵值意味著限定了所替換字符串的方法名,相當于進一步限制了目標字符串的范圍。當然,不填寫意味著此替換對全部方法名生效。那么,method處具體應(yīng)該填寫什么才能保證達到限制的目的又不會出問題呢?為了便于展示,我用VSC打開vault mod的源碼,并截取出一段典型代碼如下(VSC的顏色區(qū)分更明顯一些):

首先看下方紅色橫線的部分。這部分代碼在游戲中的作用是,當有人進入寶庫時,在聊天欄中打印信息:"<玩家名> entered a(n) <詞綴名稱> Vault!"。如果這段翻譯為中文,應(yīng)該是"<玩家名> 進入了一個 <詞綴名稱> 寶庫世界!"。這里標粗的文本是替換的部分,顯然和代碼中一一對應(yīng)。那么應(yīng)該分別寫兩(三)次替換(這里為了把a和an區(qū)分開):entered a(n)?替換為?進入了一個,而?Vault 替換為?寶庫世界。注意這里的后者,首先,根據(jù)前面完全或半匹配的原則,這里必須采取半匹配的方式才能成功替換;而Vault這個詞在VH中是個相當高頻的詞匯,就會出現(xiàn)一些問題。另外,半匹配有另外的更為嚴重的bug,會在后面說明。恰好,在同一個class文件靠前一點的位置還有這樣一段代碼:

這段代碼畫橫線的The Vault部分的作用是,在玩家進入寶庫時,在屏幕正中央打出"The Vault(換行)Good luck, <玩家名>"的歡迎信息。那么根據(jù)前面所說的半匹配替換的原則,如果后面寫了把?Vault?替換為?寶庫世界,那么這里的?Vault 也會被替換為?寶庫世界,那么在屏幕中實際就會打出"The 寶庫世界"。是不是有點類似于前面那個詞條mod中所提及的字符串重復(fù)的問題?可惜的是,VP并不支持那種寫在前面就先行替換(比如把The Vault整體先行替換成寶庫世界以避免沖突)的邏輯。當然,另一種處理方法是同時把"The "(注意這里的空格)替換為空字符串(value是可以留空的,效果就是替換為空字符串,相當于刪去了對應(yīng)的key值),實際顯示的居中歡迎信息也會是"寶庫世界"(好怪)。理論上當然可行,不過還有一些其余的問題,會在后面說明。
這時method的作用就體現(xiàn)出來了。注意到這兩個信息一個是在聊天欄顯示,另一個是在屏幕中央顯示,有java基礎(chǔ)的人通常就可以直接斷定這兩個字符串的方法名不同了。實際上,一個字符串所屬的方法名,以上述兩個Vault為例,從圖中頂端畫橫線的位置可以讀取到各自的方法名(在VSC中會被標識成淺黃色)。其實就是從字符串的位置向前追溯,查明其代碼位于哪個(我到現(xiàn)在也不知道叫什么,函數(shù)?還是模塊之類?)塊的內(nèi)部,在這個塊的開頭的淺黃色字符串就是對應(yīng)的方法名。例如,第一個聊天欄信息中的Vault對應(yīng)的方法名就是printJoinMessage,而第二個居中歡迎信息的方法名為onTeleport。(注意大小寫要正確)
顯然,這兩個分別屬于不同的方法名。于是,我在測試的時候只寫了這樣的替換塊:

并進入游戲測試,結(jié)果就是只有聊天欄中的Vault被替換為了寶庫世界,而居中顯示的歡迎信息依然維持英文不變。那么顯然我可以繼續(xù)在此基礎(chǔ)上寫一個新的替換塊將The Vault也替換為寶庫世界,且此時可以選擇method處留空,不會有影響。
這里說一下前面提到的一個嚴重bug。似乎是在value處使用半匹配的情況下,如果不對method或是下面提到的stack_depth進行限制,那么前面的name限制會完全失效,無論是采取精確匹配,還是類匹配或包匹配的方式。這個bug可能和半匹配本身的搜索邏輯有關(guān),模組作者X209似乎也提到過此bug難以修復(fù)。由于此bug的存在,前面在替換字符串Vault時使用了半匹配,那么如果不限制method或是stack_depth,那么游戲中所有的Vault都會被替換為寶庫世界,對于一個如此高頻的詞匯而言必定會出現(xiàn)一些問題。因此前面我有提到不能使用單獨把The替換為空字符串之類的解決方法。
接下來是stack_depth項(也沒什么用,想跳就跳吧)。從本質(zhì)上來講,這一項的功能和前面method的作用有點重復(fù),不過側(cè)重點有些許不同,因此在某些情況下還是有一些作用。stack_depth是指匹配替換時的堆棧深度(好吧又是一個專有名詞)。和前面一樣,實際并不需要詳細了解其原理,會用就行??梢钥吹缴厦娴乃刑鎿Q塊中stack_depth都填了默認值-1(這也說明了這一項有多不常用),-1就代表了不加限制。
要具體使用這一項進行限制,首先需要再次檢查前面的config.json

將debug_mode中的is_enable項改為true,同時檢查下方optimize_params中的disable_stacks項為false,后面的stack_min和stack_max項均為-1。
確認無誤后,在內(nèi)容json中準備至少一個替換塊,并需要知道怎么讓替換過的文本顯示在屏幕上。例如我寫了這樣一個替換塊

接下來,啟動游戲,進入一個世界并至少讓替換后的結(jié)果在屏幕上顯示一次。然后一定選擇保存并返回標題屏幕并通過主界面的按鈕退出游戲。成功關(guān)閉后,打開版本文件夾/logs/debug.log(這里要選擇GBK編碼打開,否則會有亂碼),在文本中搜索替換后的字符串,例如這里搜索生命值。搜索結(jié)果一般不止一個,不過大多數(shù)都為重復(fù)的語句。選擇其中正確替換的語句,單獨提取出來應(yīng)該類似這樣

這串語句的含義是將Health:替換為了生命值:,替換堆棧為后面[ ]的全部內(nèi)容。后面的堆棧列表中,兩個相鄰堆棧用英文逗號隔開。那么,這里的堆棧深度需要填的就是后面的總計堆棧數(shù),通常在20-100之間(如果不想一個一個手動查也可以搜索并統(tǒng)計英文逗號的數(shù)量,并+1就是實際的堆棧深度)。例如,這個語句中共有37個堆棧,那么前面的替換塊中的stack_depth項就應(yīng)該填寫37,就可以達到唯一篩選當前替換項的目的。(KlparetlR曾經(jīng)提到過,不同位置的同一字符串在替換時,其堆棧深度多多少少會有些浮動,因此限定堆棧深度也可以起到篩選作用,比如前面那個聊天欄中的Vault的堆棧深度為41,那么只填寫堆棧深度而留空方法名,也可以起到相同的篩選效果)
(堆棧深度的應(yīng)用方法、難度和不確定性比方法名難了不止一點,評價為能不用就不用)
最后,整個target_class項可以完全省略,也就是將替換塊簡化成為

是不是清爽了很多?只需要寫入源字符串和替換結(jié)果就好了。當然,這種一般只適用于比較長的字符串,或是很確定這個字符串在整個游戲中是唯一的(從而不會出現(xiàn)誤匹配或其它問題),以及無法定位代碼所在的class文件,或是這個字符串壓根就不在源碼中(沒錯,這種也能替換,參見專欄后面的補充),導(dǎo)致沒法寫target_class的情況。
接下來是1.2.10的另外一種常用手段和所引起的bug。1.2.10支持"二次替換"。這是什么意思呢?還是以前面的Vault為例,在不加限制的情況下我們看到居中的歡迎信息變成了The 寶庫世界。不考慮其它bug的情況下前面提到過的處理方法中有一種是將"The"替換為空字符串;同時也可以使用這樣的方法:將"The 寶庫世界"作為key的值,替換為"寶庫世界"。也就是這樣寫:

看起來很不可思議,但是如果只是考慮替換內(nèi)容的話,確實可行。看起來是一種不錯的重復(fù)字符串的處理方式,然而,這種操作會導(dǎo)致兩種bug:一是文字位置錯誤的問題。常發(fā)生在各種需要居中顯示或右對齊的文本處。例如上面的The 寶庫世界為在屏幕中央顯示,在二次替換后實際的文字內(nèi)容就會偏左。我沒研究明白其中的原理,不過根據(jù)實際測試發(fā)現(xiàn),大概是因為第二次替換的時候字符串的位置已經(jīng)定下來了,而二次替換過后的字符串默認為左對齊,因此在某些需要居中顯示的情形下進行二次替換都會發(fā)生一些排版問題。
如果說位置錯誤只是小bug的話,那不得不提到另一個困擾全部開發(fā)人員許久的bug:在二次替換的情形下,如果被替換的字符串后面有格式相關(guān)的代碼,例如改變顏色之類,那么對應(yīng)的字符串會直接消失。比較典型的例子是VH中有一個必須用半匹配替換的字符串Value:→經(jīng)驗值:,而很多vault mod物品的tooltip都有Soul Value:一項,后面會附帶具體的紫色的數(shù)值。而在二次替換Soul 經(jīng)驗值:為靈魂價值:的情況下,后面的數(shù)值會直接消失。顯然這樣的bug對于VH來說無法接受。因此在很長一段時間之內(nèi),都是直接舍棄了翻譯不那么重要的Value,而直接翻譯Soul Value整體。
不過二次替換總體來講還是方便了許多,這里給出一個很好的使用例。

這兩個替換塊是把VH中屏幕右上角的 X?unspent skill point(s) 替換為了 X?未用技能點。這里value使用的是lang中key的形式(沒錯,VP支持寫成本地化key的形式并通過加載資源包來替換,這個VP.modify.the_vault.10的內(nèi)容實際就是"未用技能點")。由于英文當中的復(fù)數(shù)形式問題,有一個技能點和多個技能點時實際的英文會有所區(qū)別。首先來看看源碼中是如何實現(xiàn)的:

是在?unspent skill point 后面加了一個條件判斷,如果前面顯示的數(shù)字為1就在后面加空字符串,如果不為1就加一個s。對于英文顯示來說完全沒問題,但在翻譯成中文時,如果只翻譯?unspent skill point?為?未用技能點?,必然會出現(xiàn)復(fù)數(shù)情況下顯示為?X?未用技能點s?的情況。當然,這里也肯定不能用半匹配的方式將s替換為空字符串,因為半匹配會替換所有的s,顯然會出現(xiàn)很多問題。于是,這里使用二次替換的方式,將 未用技能點s 作為一個新的替換key,而替換結(jié)果依然是 未用技能點 。而在二次替換生效時,由于這里的對齊的基準是以?未用技能點s?的左端點為錨點進行左對齊,因此實際的排版看起來只是刪去了后面多余的s,從而巧妙地利用了這一特性達到了想要的排版效果。
最后,三次或三次以上的替換基本不會生效(所以究竟是多么復(fù)雜的地方需要用到三次替換呢,不如考慮一下其它的替換方法吧)

催更(?)
在使用了VP2.10后,根據(jù)VP群友的說法,他們沒能聯(lián)系到前面tooltip漢化的作者取得授權(quán)。因此VM的漢化補丁中沒有包括那個mod,而是改用VP承擔起了全部的翻譯工作。
然而VP2.10的種種bug導(dǎo)致了漢化進度一直卡在幾個無法處理的字符串上(比如前面提到過的soul value,以及cooldown reduction和mana與植物魔法中mana沖突的問題等)。于是我和KlparetlR等人在VP的討論群中持續(xù)地報告各種新發(fā)現(xiàn)的bug,同時一直在催更(
當時大概是今年的6月中旬到下旬,X209回復(fù)說要等到7月放假。于是這段時間漢化進度的推進被暫時擱置。
然后7月16日,X209發(fā)布了VP的1.2.13版本(以下簡稱2.13)。

1.2.13/1.2.14的說明
2.13完全重構(gòu)了字符串的匹配機制,并且將mod重置為core mod(core mod的具體定義我不太清楚,理解層面上大概是說,實際在游戲中增加內(nèi)容的,比如說ae、植魔、暮色森林都不是core mod,core mod加載的時間在進入游戲之前,有點類似于各種lib mod的形式,只在游戲啟動加載時生效,而對于實際游戲內(nèi)容沒有大體更改)。
2.13引入了ASM框架。
ASM 是一個 Java 字節(jié)碼操控框架。它能夠以二進制形式修改已有類或者動態(tài)生成類。ASM 可以直接產(chǎn)生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
——來自百度百科
看不懂也沒關(guān)系,我也看不懂
KlparetlR曾經(jīng)報告過一個bug,在使用VP2.10對大批量字符串進行替換時,會導(dǎo)致游戲性能顯著下降,包括加載時間延長,卡頓掉幀等。尤其是在存錯了大量物品的ae或是簡單存儲網(wǎng)絡(luò)(SSN)模組的存儲終端中,在使用按名稱排序的方式時會導(dǎo)致幀數(shù)驟降,甚至無法正常運行。
其原因不難理解。我們引用上面的一張圖

這里VP2.10的替換位置為源碼加載到顯示緩沖區(qū)時。然而,顯示緩沖區(qū)是逐幀實時更新的,也就是說每幀都需要對寫好的字符串進行一次匹配(如果開了debug模式,就會在debug.log中發(fā)現(xiàn)大量的重復(fù)替換語句)。顯然這種替換方式會占用大量運算性能。尤其是上面的按名稱排序問題,排序算法和VP2.10共同導(dǎo)致了甚至是逐幀刷新全部物品的排序方式。在存儲終端中有幾百上千的不同物品時,這必然會大量占用運算性能,導(dǎo)致極為嚴重的卡頓。
ASM框架的引入使得字符串的內(nèi)容可以在上圖中第二個步驟進行替換,從而完全避免了緩沖區(qū)刷新的問題。進一步來說,2.13的加載時間是在游戲啟動到加載完成進入主界面之前,其作用相當于在啟動加載時直接操作模組源碼,將對應(yīng)的字符串進行替換,這樣讀取進顯示緩沖區(qū)內(nèi)的為替換好的中文字符串,不會出現(xiàn)需要逐幀刷新的情況,當然不會出現(xiàn)影響性能的問題。當然,這會讓游戲的啟動加載時間略微延長,不過對實際游玩完全沒有影響,與性能下降相比幾乎不算問題。這里附上X209提供的一張2.14的工作原理圖(最新的圖是2.14版本,不過差別不大)。

2.13的配置文件結(jié)構(gòu)與2.10的區(qū)別不大,下面是2.13的一個典型匹配塊,主要改動如下。

首先,配置文件的儲存文件夾名稱從vaultpatcher改為了vaultpatcher_asm,其余結(jié)構(gòu)保持不變。
在替換塊方面,2.13取消了key原有的半匹配機制,只支持完全匹配,同時也可以替換原來2.10必須使用半匹配的場合。也就是說,這里的key必須精確地填寫與源碼中與目標字符串完全相同的內(nèi)容(jd-gui中顯示為藍色,VSC中顯示為淺棕色),包括字符串首或尾可能存在的空格,同時這也要求替換過后的字符串要考慮首尾空格匹配的問題,視具體情況而定(專欄后面會舉出例子)。
相應(yīng)地,2.13全面支持代碼中的通配符,如%s或%d等。因為替換過程發(fā)生在通配符沒有被替換為實際內(nèi)容之前,所以替換字符串中包含通配符(必須是源字符串中包含的通配符,不能手動添加,如下圖)不會引起問題。同時要注意key和value中的通配符類型、數(shù)量和順序都要一一對應(yīng)。


2.13的target_class部分僅支持精確匹配,也就是必須完整填寫目標字符串所在的class名稱及其路徑。同時要注意的是,與2.10不同,這里的class需要精確到內(nèi)部類。
我花了很長時間才研究明白什么是內(nèi)部類。
內(nèi)部類(Inner Class),是 Java 中對類的一種定義方式,是嵌套類的一個分類,即非靜態(tài)嵌套類(Non-Static Nested Class)。內(nèi)部類(非靜態(tài)嵌套類)分為成員內(nèi)部類、局部內(nèi)部類和匿名內(nèi)部類三種。
看不懂也沒關(guān)系,我也看不懂
起初是我在測試2.13版本時候發(fā)現(xiàn)有部分字符串明明寫的路徑看起來沒有問題,但是實際卻不能替換。觀察總結(jié)以后發(fā)現(xiàn),所有這類問題有一個共同點:源碼同時位于兩個class文件中,其中一個文件的文件名較短,但是文件內(nèi)容較長;而另一個class文件名相當于在前者的后面加上了一個$字符,同時后面跟了一些另外的東西,且其文件內(nèi)容會短一些,看起來像是從長文件中截取出的一部分。
一番搜索之下,我發(fā)現(xiàn)這個額外多出來的類似子文件一樣的文件稱為內(nèi)部類。mod在編寫時都是以java源碼的形式編寫(即此時后綴名為.java),然后整體丟進IDE中進行構(gòu)建和編譯,編譯過后,java文件會變成class文件,就是實際在mod的jar包中看到的那些。這個編譯的過程中,有時候一個java文件會被編譯為數(shù)個不同的class文件,其命名方式為作為主體的外部類名稱.class,和可能存在的外部類名稱$內(nèi)部類名稱.class(對不起,關(guān)于這部分的生成機制我實在是看不懂>﹏<)。后者是前者的一個子文件,稱為其內(nèi)部類,其代碼內(nèi)容同時存在于兩個文件內(nèi)。例如上面圖中的字符串"Difficulty: %s"同時存在于以下兩個文件中。

那么,如果有一個要替換的字符串恰好位于這個內(nèi)部類中,就會出現(xiàn)該字符串同時出現(xiàn)在兩個文件中的情況。此時target_class應(yīng)該精確定位到內(nèi)部類的名稱,也就是需要加上$后面的內(nèi)容,如上面的"Difficulty: %s"所示,如果僅定位到外部類則不能替換。
另外,2.13保留了方法名的限定,刪去了stack_depth字段都說過沒用了吧。方法名部分的用法與2.10版本相同。
在說明2.13的config.json結(jié)構(gòu)之前,考慮一個問題。2.13要求了必須寫target_class部分且必須精確匹配,這就導(dǎo)致每個匹配塊都需要寫很長的一段路徑,而且有些帶有內(nèi)部類的文件名長達四五十個字符,如果需要一個一個填寫,必然會增大配置文件的體積,也會降低編輯效率。在KlparetlR的反饋之下,X209在不久之后又更新了VP的2.14版本。
2.14是在2.13基礎(chǔ)上做了一個小改動,使得target_class字段變?yōu)?strong>可選,也就是替換塊可以像2.10那樣只寫key和value,適用于沒找到源碼位置的情況。但必須要在config.json中做出相應(yīng)配置,否則不能正常替換還是會崩潰來著。
可是你都知道了字符串在源碼中,為什么不用VSC的全局搜索呢
2.14可以完全替代2.13,因此這里直接說明2.14的config.json結(jié)構(gòu)。

首先是和2.10一樣的mods塊。這里mods下面可以寫多個文件,兩個文件名之間使用英文逗號隔開,最后一個文件名后面不要有逗號(一定記住這一點,否則整個VP都會失效,不過如果是在VSC中編輯,會有相應(yīng)的報錯信息)。
這些文件需要并列地和config.json共同存儲在版本文件夾/config/vaultpatcher_asm下。其實2.10是因為神秘bug導(dǎo)致僅支持單個替換文件,后面版本只是修了這個問題。
mods下面新增了名為apply_mods的塊。其中填寫的是版本文件夾/mods下面的jar文件名(不包括.jar后綴),也就是mod本體文件的文件名。
apply_mods是為了配合省略target_class的情況而配置的。也就是說,如果在替換塊中省略了targrt_class段,那么對應(yīng)字符串的搜索范圍就是apply_mods下面的全部文件,也就是對應(yīng)模組的源碼??梢詫懚鄠€,格式與前面mods段相同;也可以直接留空并刪去整個apply_mods段,但是留空的情況下要求所有的替換塊必須全部指定target_class。
后面的debug_mode與2.10配置方法相同。

debug mode/調(diào)試模式
(以下內(nèi)容僅在2.10測試并應(yīng)用過,2.13未知)
要開啟調(diào)試模式,需要將config/vaultpatcher/config.json中debug_mode下的is_enable設(shè)置為true。

后面的output_format下支持占位符<source>(相當于key)、<targret>(相當于value)、<class>和<stack>。由于<stack>的顯示內(nèi)容實際包含了<class>的內(nèi)容,因此通常只使用前者即可。此處的配置實際上是定義了以下內(nèi)容的寫法

調(diào)試模式的用途其一就是上面提到過的統(tǒng)計堆棧深度。
另外,仔細觀察上圖中所顯示的內(nèi)容可以發(fā)現(xiàn),堆棧列表中藍色的部分(#之前)其實是該字符串所有出現(xiàn)過的target_class下的name,而淺黃色的部分(#之后)則是對應(yīng)的method。實際2.10的工作原理是從此堆棧列表中挑選出所限制的堆棧位置并加以替換。
因此調(diào)試模式的另一個用途是輔助定位所替換字符串在源碼中(或引用到源碼中)的位置。通常只關(guān)注對應(yīng)mod下的target_class(例如此處可以只關(guān)注以iskallia.vault開頭的堆棧)。實際使用時,大概是先對未知位置的字符串不限制替換位置進行替換,然后在日志中找到對應(yīng)的替換語句就可以精確定位了。

繼續(xù)漢化流程
硬編碼優(yōu)化時期
2.14不支持二次替換。當然,2.14本來也從根本上解決了需要進行二次替換的問題,因為所有字符串都是完全精確匹配,且不會出現(xiàn)半匹配時候的name限制失效的問題。例如,在前面的未用技能點的問題部分,可以使用2.14將?unspent skill point?替換為 未用技能點,同時直接把 s 替換為空字符串,不會引起任何問題。
弄清楚2.14的工作原理后,我花了一段時間把原2.10的全部字符串轉(zhuǎn)移到了2.14,同時刪去了那些必須二次替換的部分,因為2.14實際是替換與key處全字匹配的字符串,除非同一個限定位置有兩個完全相同的字符串,否則不會出現(xiàn)任何重復(fù)問題。
當然,因為后面要講的另一些問題,有些字符串使用2.14替換會引起極其嚴重的錯誤,直接導(dǎo)致游戲崩潰。無奈之下,我只好放棄翻譯那些字符串,這樣在游戲中存留了少部分的英文內(nèi)容,不過看起來無傷大雅,而且此時的整合包運行相當穩(wěn)定且流暢。
當然我不可能止步于此。此時的漢化進度大概停留在85-90%,我需要繼續(xù)推進。有一天,我在回想X209關(guān)于2.10以后版本VP的更新描述,記得他有說過2.13和2.14是完全更改了VP的整體架構(gòu),相當于是重寫了一個mod。我就在想,能不能實現(xiàn)2.10和2.13的混用呢?這樣,以2.14為主體,其余會導(dǎo)致游戲崩潰的漢化內(nèi)容由2.10完成,應(yīng)該可以在盡可能維持高性能的前提下提高漢化進度。
然后,在同時加載2.10和2.13時,游戲不出意外地在啟動之前就崩潰了。仔細檢查之下,發(fā)現(xiàn)其問題出現(xiàn)在二者的包名重復(fù),也就是都是vaultpatcher。哪怕這兩個mod的作用時間和原理完全不同,這樣重復(fù)的包名也會導(dǎo)致沖突問題。當時向X209提出了issue,不過他那段時間在忙另外的工作沒辦法繼續(xù)更新。于是我自己動手從github上拿到了2.10版本的源碼,把所有的包名以及vaultpatcher相關(guān)的字符串都改成了vaultcompatcher好粗暴,取compat有兼容之意。改好后,我把源碼包發(fā)給了KlparetlR并拜托他幫我構(gòu)建編譯我自己的電腦java環(huán)境一直有問題,困擾了我好久。贊美KlparetlR111111
后來,我順利地用修改后的2.10版本補充了2.14無法替換的部分,使得漢化進度一度接近100%。

硬編碼時期的問題
在繼續(xù)完善漢化之前,這里著重說明一些在配置硬編碼的時候遇到的各種問題。這些問題基本都以VP2.14版本為基礎(chǔ),因為2.14是目前漢化工作的主力。
部分文本內(nèi)容不兼容顯示中文
VH在Update 9更新中加入了新手任務(wù)系統(tǒng),與其相關(guān)的文本有任務(wù)書的全部gui內(nèi)容,以及每次完成一個任務(wù)時會在屏幕右上角彈出臨時的提示信息。


任務(wù)書右側(cè)各個任務(wù)的詳細描述文本儲存在版本文件夾/config/the_vault/quest/quest.json和sky_quest.json下的description字段中,分別對應(yīng)了正常世界和空島世界版本的任務(wù)書內(nèi)容。這一部分可以通過修改為中文并轉(zhuǎn)換為unicode編碼的形式來成功漢化;然而左側(cè)的各個任務(wù)標題,存儲在上述兩個文件的name字段中,當我嘗試漢化這部分文本內(nèi)容時,發(fā)現(xiàn)游戲中對應(yīng)的標題可以成功替換,但會導(dǎo)致該任務(wù)無法完成。(后來我嘗試把任務(wù)標題修改為任意的英文字符串,測試發(fā)現(xiàn)不會出現(xiàn)上述問題,說明問題在于不支持中文字符)

而任務(wù)完成的提示文本"You have completed a quest!",當我在2.14下將其替換為中文后,在游戲中完成任務(wù)時會卡死并崩潰。具體的替換方法為

檢查崩潰報告后我發(fā)現(xiàn)了如下的報錯信息

翻譯過來大概意思就是:任務(wù)完成提示的顯示信息需要編碼的部分長度過大(當前25字節(jié),最大9字節(jié))。這符合替換過后的字符串長度:"你完成了一個任務(wù)!",共8漢字+1英文感嘆號,共計占用25字節(jié)(以GBK編碼方式,每個漢字占用3字節(jié))。首先我檢查了對應(yīng)源碼的實現(xiàn)方式

然而以我粗淺的java基礎(chǔ),實在不清楚為什么這里限制了字符串的長度,以及原字符串的長度其實也超過了9字節(jié)(。于是我采用的解決方法是這部分內(nèi)容使用2.10進行替換。
"You have completed a quest!"這段字符串使用2.10進行替換測試,一次就成功了,任務(wù)完成時的提示可以正常顯示,且文本為中文。而且可以用完全匹配的方式(其實這段字符串在整個游戲中也是唯一的,使用半匹配也不會出現(xiàn)重復(fù)的問題)。
同理我嘗試使用2.10漢化任務(wù)書左側(cè)部分的標題(需要半匹配),居然也成功了,而且任務(wù)可以正常完成。有趣的是,可以發(fā)現(xiàn)這部分替換內(nèi)容實際上不在源碼中,而是存儲在vault mod的配置文件下的相關(guān)json文件。游戲在運行時的處理邏輯是將標題的字符串從json文件讀入到源碼中,然后將其顯示到屏幕。那么為什么2.10可以替換這部分文本呢?

回想一下前面那張圖

2.10的原理是將輸入到顯示緩沖區(qū)的文本進行替換。換句話說,2.10的本質(zhì)工作原理不是硬編碼漢化,而是顯示文本漢化。也就是說對于任何顯示到屏幕上的文本,無論其來源是不是模組源碼,理論上都可以在2.10中進行替換(當然此時因為沒有對應(yīng)的target_class,所以替換塊中只有key和value兩個值)。而2.10對于顯示緩沖區(qū)的修改與實際游戲的代碼運行過程關(guān)系不大,例如那段"You have completed a quest!"字符串,使用VP2.14替換時是直接將代碼替換為中文,那么實際運行時候就會檢測到字符串長度過大,從而導(dǎo)致游戲報錯并崩潰;而2.10是在這段字符串在內(nèi)部處理完畢準備打印到屏幕上時進行替換,而在內(nèi)部處理時仍然保持完全英文的狀態(tài),自然不會導(dǎo)致報錯。這也算是2.14在修改模組源碼時帶來的一個弊端。

無法定位的字符串
其實這個問題與硬編碼漢化沒有太大關(guān)系,而是反編譯過程出現(xiàn)的問題。起初是發(fā)生在VP2.14版本還沒有更新的那段時間,由于2.13要求必須填寫target_class,所以對于那些在源碼中但無法精確定位的字符串,就只能被迫選用2.10進行替換。
最開始,我使用了jd-gui自帶的反編譯功能導(dǎo)出vault mod的源碼包。但由于jd-gui開發(fā)時間較早年久失修,對vault mod的兼容性不是很好(vault mod是基于JAVA17開發(fā)的),于是偶爾會出現(xiàn)一些缺失代碼的問題,包括直接在jd-gui中查看的代碼可能也不全。所以,這里建議將mod的jar包使用完整的IDE環(huán)境(例如IntelliJ IDEA)進行反編譯為java源碼,并用VSC打開以搜索目標字符串的位置。我自己的電腦的IDE環(huán)境配置一直有點問題,還是要感謝KlparetlR發(fā)給我反編譯好的源碼
不過,這個問題在2.14中被新增的apply_mods功能解決了。只需要確定目標字符串屬于哪個mod,直接填寫mod文件名也可以匹配到。

一些直接使用變量名/id/有特殊用途的字符串
還有一部分顯示的內(nèi)容實際是直接打印了模組源碼中的變量名、特殊命名id或是不允許替換的一些特殊字符串。這類問題往往是最難處理的,有些甚至直到現(xiàn)在還保留為英文的狀態(tài),并且沒有任何辦法。
首先是寶庫之神的名字。這些名字在定義時被定義為了枚舉類型

這里的name在源碼中的許多地方被getName()方法所引用并和其它元素組合成為新的String,而后者通常僅支持ASCII字符集(即字母、數(shù)字和一些特殊符號等)。如果使用VP2.14對此處寶庫之神的名字進行替換則會引發(fā)很多問題。典型例子的就是祭壇處獲取一個眷顧后,再次進入寶庫會導(dǎo)致無法打印對應(yīng)寶庫之神的聊天信息,獲取對應(yīng)的buff,以及在此次寶庫全程后臺一直在輸出報錯信息,導(dǎo)致大幅度卡頓和其它問題。然而,在屬性統(tǒng)計界面還需要直接打印這個字符串

所以我選擇使用2.10對這部分字符串進行替換。替換塊的寫法為

注意這里target_class的寫法。前面提到這些寶庫之神的名字需要在源碼的不止一處被引用,且這里的字符串必須為半匹配才能替換。由于半匹配有name失效的bug存在,我需要限定method和stack_depth的至少一項才能使這個替換不會影響到其它引用該字符串的場合。
那么,name和method應(yīng)該填什么呢?這些代碼的來源是上面枚舉類,其name值為iskallia.vault.core.vault.influence.VaultGod。顯然如果直接從來源處進行修改,會影響到其它引用這些字符串的地方,那就和2.14版本的替換效果沒有區(qū)別了。
由于2.10本質(zhì)不會操作源碼,所以我檢查了上述統(tǒng)計頁面打印此字符串的相關(guān)代碼

此處代碼的路徑iskallia.vault.client.gui.screen.player.StatisticsElementContainerScreenData,也就是上面替換塊中的寫法。而this.getGodFavorTitle(VaultGod.VELARA)則負責顯示上方游戲截圖中所顯示的內(nèi)容,getGodFavorTitle為方法名,VaultGod.VELARA為引用來源,也就是代碼運行時先將VaultGod.VELARA處(也就是上面枚舉類中的name和title字符串)的代碼引用到這里,然后輸出到屏幕。以及getGodFavorTitle方法的具體代碼:

可以看到確實是分別進行了getName和getTitle,與實際的顯示內(nèi)容相對應(yīng)。獲取到這里的目標字符串僅用于輸出到屏幕,因此我實際可以在上述代碼中的getGodFavorTitle和getTooltipExtendedVelara兩個方法名中任選其一(因為getName到處都在用,可能還會導(dǎo)致誤匹配)進行限定,都可以把匹配范圍限定到這里。
另外舉出一個更為極端逆天的例子,是包括了一些字符串的特殊處理方式。首先給出一張寶庫中寶箱gui標題的截圖(在不加載VP2.10的情況下)

寶庫中寶箱分為四種稀有度:Common、Rare、Epic和Omega。在打開寶箱時,對應(yīng)的稀有度信息會如圖顯示在標題處。負責標題顯示的代碼如下

顯然寶箱名稱的部分" Wooden Chest"可以直接使用2.14替換為"木質(zhì)寶箱",實際截圖中也沒有問題。然而對于前面顯示的Common,即使使用全局搜索也根本找不到完全匹配(大小寫)的字符串。于是我繼續(xù)分析這里的代碼,此處實際顯示的字符串是 變量rarity+"?Wooden Chest"。rarity的定義在上圖第三行給出:
String rarity = StringUtils.capitalize(this.rarity.name().toLowerCase());
其中實際的內(nèi)容為this.rarity.name()。在同一class文件下可以繼續(xù)追溯其來源

于是定位到名為VaultRarity.class的文件中。查看此文件中關(guān)于Common內(nèi)容的定義怎么又是枚舉類

好家伙,直接是變量名。注意到這里實際的變量名為COMMON(全部大寫)。聯(lián)想到前面的語句
String?rarity?=?StringUtils.capitalize(this.rarity.name().toLowerCase());
不難發(fā)現(xiàn)Common的生成過程:先使用toLowerCase()將this.rarity.name()(即"COMMON")全部轉(zhuǎn)換為小寫(即"common"),然后用capitalize(common)大寫其首字母c得到最終的輸出字符串變量rarity?=?'Common'。怪不得我根本搜不到完全匹配的字符串。
且不提枚舉變量名本身就無法用2.14替換,即使是正??梢蕴鎿Q的字符串,通過這樣的大小寫轉(zhuǎn)換后也無法直接通過搜索完全匹配。例如前面的寶庫之神處的title="The Benevolent",實際在圖中顯示為"the Benevolent",也是通過decapitalize()方法把首字母轉(zhuǎn)換為小寫然后顯示的。這時使用2.14匹配時要首先分析有沒有對應(yīng)的大小寫轉(zhuǎn)換算法,才能確定實際源碼中存儲的字符串的形式,然后才能確實使用2.14進行替換。
但是,我直接選擇使用2.10。甚至由于大部分相關(guān)的字符串在源碼處都已經(jīng)被2.14替換為中文(顯然2.14的優(yōu)先級比2.10高出很多),我可以直接在2.10中寫

并進入游戲測試,寶箱標題的gui可以正常替換,也沒有出現(xiàn)任何其它問題。
相同的做法還有子懸賞的標題等,這里不再詳細說明。

特殊使用例之一
Update 11重做了地牢結(jié)構(gòu),會在普通房間中隨機生成地牢門,在打開地牢門時屏幕中央會顯示一條提示地牢難度的信息

地牢共五種難度:Normal、Hard、Challenging、Extreme和Impossible。前面在提到通配符時也有涉及這條顯示信息的代碼

在使用2.14進行漢化時可以將"Difficulty"替換為"難度"。而后面的難度字符串部分,我最終在版本文件夾/config/the_vault/gen/1.0/palettes/generic/dungeon_door_placeholder.json中定位到了對于地牢難度的描述

但當我將這里的字符串替換成中文并用unicode編碼保存后,進入游戲發(fā)現(xiàn)原本生成地牢的位置由于某種錯誤變成了

顯然上述的改動會引起問題,于是我使用了VP2.10對這部分字符串進行處理。為了確保不會誤匹配,我對替換塊中的target_class和method都進行了限制,限制到上述"Difficulty: %s"的位置即可。由于源碼中是以通配符%s的形式表達此字符串,因此需要使用半匹配的方式,具體的替換塊寫法如下:

其余四個難度字符串相同處理。經(jīng)過實際測試,地牢門結(jié)構(gòu)可以正常生成,且對應(yīng)的提示信息可以正常漢化。

特殊使用例之二
這里提到的是VP2.10限定的一種特殊的替換方法。這一方法大概是KlparetlR發(fā)明的,是類似如下形式的替換塊

這里MaZa_RinE是我自己的正版ID,使用了完全匹配的方式,替換結(jié)果為如上所示的字符串。這個替換的實際效果為:可以通過預(yù)先配置的形式將某個特定用戶的ID進行替換,可以更改其ID的實際顯示內(nèi)容,也可以增加一些特殊效果。例如上述替換在游戲中實際的顯示為

具體的效果實現(xiàn)需要參考mc自身提供的格式化代碼。以及,這段代碼的格式其實來自于VM漢化組內(nèi)置的對于漢化組成員的ID替換,有興趣的讀者可以自行翻閱。

還有許多其它的特殊例子,限于篇幅這里就不一一描述了。不過,大體的替換思路是:在源碼中全局搜索→在版本文件夾/config/the_vault下全局搜索→使用VP2.14嘗試替換→使用VP2.10嘗試替換。通過這四個步驟,幾乎可以解決VH整合包的全部文本。

反編譯漢化階段
此方案大概依然由KlparetlR提出。他的出發(fā)點是由于硬編碼的替換文件總計文本量過大(對于整個VH整合包來說大概有10000多行),撰寫和維護的時間成本都較高,因此在尋找一種直接編輯mod源碼的形式,對那些單個class文件中有大批量需要漢化的字符串的部分(例如存儲全部裝備模型名稱的文件,單個文件內(nèi)大概有接近100字符串)嘗試直接修改mod源碼為中文,從而簡化后續(xù)的維護工作。
通常而言class文件不能直接編輯。class文件也可以稱為字節(jié)碼文件,是經(jīng)過Java IDE編譯后提交給機器運行的文件,因此其語法在設(shè)計上完全沒有考慮可讀性,且結(jié)構(gòu)相當混亂。當然,前面有提到過VP2.10以后的版本均使用了ASM框架,其中文譯名為Java字節(jié)碼操控框架,實際上就是一種直接編輯這里的class文件的一種方式,也就是實現(xiàn)了硬編碼漢化。
這里使用的工具為Recaf,是Col-E開發(fā)的一種java字節(jié)碼編輯器。其github頁面上的描述如下:
An easy to use modern Java bytecode editor that abstracts away the complexities of Java programs. Recaf abstracts away:
Constant pool
Stack frames
Wide instructions
And more!
等等,還記得我在上面提到的jbe嗎?沒錯,本質(zhì)也是一種字節(jié)碼編輯器。只是其應(yīng)用范圍相當狹窄,根據(jù)我的使用經(jīng)驗來看,大概只能成功打開只含有單個class文件,且構(gòu)建時不需要額外依賴庫的jar包。巧的是,前面提到的詞條替換mod剛好滿足這兩個條件,所以我可以對mod源碼進行編輯。然而,對于絕大多數(shù)mod文件來說,不可能只有一個class文件,而且稍復(fù)雜一些的mod都會有自己的依賴庫,顯然不能利用jbe進行編輯。例如我使用jbe打開任意vault mod內(nèi)部的class文件時,jbe會報錯

因此jbe在后期已經(jīng)被我拋棄了。
Recaf支持直接打開整個jar包并瀏覽其中的源碼,以及通過字節(jié)碼編輯的方式對其內(nèi)容進行修改。
首先講一件趣事。Recaf目前的最新正式版版本為2.21.13,是基于Java開發(fā)的程序。我在下載并嘗試打開時,由于vault mod是在Java 17環(huán)境下開發(fā)的,因此我必須使用Java 17打開Recaf,才能正常對vault mod的源碼進行編輯。然而我的電腦的Java 17環(huán)境有些問題(至少當時我是這么認為的),導(dǎo)致我下載的Recaf只能使用Java 8打開,自然也就不能進行任何反編譯的操作。當時想盡了各種辦法,包括完全重新安裝和配置Java,恢復(fù)系統(tǒng),換另一臺電腦,甚至是讓KlparetlR幫我遠程控制,最終也沒能解決這個問題。
直到幾天前的8月7日,我試著下載了Recaf的3.0測試版。然后,莫名其妙就順利打開了當時我整個人就很無語。
不過這個時期我的漢化已經(jīng)沒有需要補充的東西了。我開始嘗試反編譯漢化的主要目的是考慮到VP2.10所帶來的性能影響問題,于是希望可以盡可能減少2.10所負責的文本量。當時2.10替換文件中的一半內(nèi)容(大概有300行)是用來漢化裝備詞條,也就是專欄開頭提到的詞條mod所負責的部分。這部分會同時顯示在寶庫裝備的tooltip和屬性統(tǒng)計頁面右側(cè)


在源碼中的表示方式為

看起來這種形式可以使用VP2.14進行替換。然而實際測試時發(fā)現(xiàn)并不起作用。KlparetlR說不生效的原因可能是因為這些名稱的注冊在定義的后面,也就是同一class文件的后半部分

的這些registry.register(XXX),導(dǎo)致2.14的匹配失效。當時的解決方案是改用2.10進行替換。大部分詞條都順利替換,但是由于在裝備tooltip處的顯示必須使用半匹配進行替換,會導(dǎo)致類似"Damage"和"Undead Damage"這樣的重復(fù)詞條問題(二次替換咯),以及"Mana"會同時替換到植魔mod中的"Mana"的問題,以及"Cooldown Reduction"完全不替換的神秘問題等等。另外,由于寶庫裝備是VH中幾乎是最經(jīng)常顯示tooltip的部分,這樣用2.10的頻繁替換帶來的性能影響不得不考慮。于是,在VP2.14不能用的情況下,我只好考慮反編譯漢化的可行性。
使用Recaf修改字節(jié)碼時有諸多限制,所以反編譯漢化所能操作的代碼部分相當有限其實就是碰運氣??偟膩碚f,能成功打開字節(jié)碼編輯窗口的就代表著可以修改其中的內(nèi)容

由于字節(jié)碼本身結(jié)構(gòu)相當混亂,而且突出一個整體性,因此更改其中的語句邏輯或是增刪語句之類的操作完全不現(xiàn)實。好在絕大部分字符串內(nèi)容都是原封不動地從java源碼編譯進字節(jié)碼中,編譯后被冠以"ldc"或"ldc_w"前綴。實際在編輯字節(jié)碼時只需要關(guān)注這一部分并進行適當修改就可以。Recaf為什么不支持文本搜索
上述字節(jié)碼文件中的中文不需要轉(zhuǎn)碼存儲。修改好后,需要在Recaf內(nèi)重新導(dǎo)出jar文件并替換原有的mod。然后打開游戲測試,完全沒有問題。于是VP2.10的替換文本量最終被我縮減到了約350行,已經(jīng)是十分令我滿意的結(jié)果了。
后來我發(fā)現(xiàn)終端內(nèi)按名稱排序的掉幀問題只與是否加載VP2.10有關(guān),而與實際的替換文本數(shù)量關(guān)系不大,悲
另外,字節(jié)碼編輯方式實質(zhì)上是修改了mod的源碼內(nèi)容,這對于某些mod來說可能違反了相關(guān)的分發(fā)協(xié)議,從而不能隨意公開發(fā)布。如果是我個人使用的漢化版本就沒有問題,不過我也沒有了解過VM漢化組那邊最新的漢化補丁有沒有使用反編譯漢化的形式(

一些漢化過程中的頑固分子
技能(Ability)名稱

技能名稱的字符串內(nèi)容存儲在版本文件夾/config/the_vault/abilities.json中的name字段中(不是id)。

起初我在修改這部分內(nèi)容時,發(fā)現(xiàn)不會導(dǎo)致游戲報錯但是不能替換。我也檢查過相關(guān)的源碼,確認過字符串的讀取來源確實是這里。后來KlparetlR提到,VH會將技能名稱的字符串寫入到存檔數(shù)據(jù),也就是版本文件夾/saves/存檔文件夾/data/the_vault_PlayerAbilities.dat中(據(jù)說是方便版本更新的檢查),而dat文件并不能通過常規(guī)方法修改其中的特定字符串。因此實際上在配置文件中對技能名稱的更改僅對于新存檔生效。若是想在已有存檔中漢化這部分內(nèi)容,還是老老實實用VP2.10寫對應(yīng)的替換字符串吧。
后記:似乎Update 11對相關(guān)的存儲機制做了更新,現(xiàn)在直接改配置文件應(yīng)該也可以實時反饋到存檔中。