宝藏猎人整合包(Vault Hunters)个人汉化笔记/VaultPatcher硬编码汉化模组使用指南

您所在的位置:网站首页 宝藏猎人装备推荐 宝藏猎人整合包(Vault Hunters)个人汉化笔记/VaultPatcher硬编码汉化模组使用指南

宝藏猎人整合包(Vault Hunters)个人汉化笔记/VaultPatcher硬编码汉化模组使用指南

2024-07-09 17:19| 来源: 网络整理| 查看: 265

Vault Hunters 3rd Edition协助写在最前面

这篇专栏本意是记录我在汉化过程中的一些收获、问题和心得体会,此汉化版本仅供我个人使用,不会发布在公共平台上。

宝藏猎人的官方授权汉化请参见VM汉化组的专栏。这里的个人汉化版本有部分内容参考了VM的汉化文档。

宝藏猎人大概是我第一个完整游玩、完整汉化的整合包,在这里要感谢VM汉化组的各位、KlparetlR和硬编码汉化模组的作者X209(3093FengMing)。

第一次接触宝藏猎人(以下简称VH)大概是22年的寒假期间,学校的几个朋友推荐了这个整合包,三四个人一起靠着VH度过了整个寒假。那个时候VH差不多是刚出第三个,也就是基于1.18.2的版本(3.0),一直到现在更新到了Update 11(3.11)。可惜的是目前VH还属于alpha测试阶段,正式版可能还要等4-5个版本,而且测试阶段也不会把核心的vault mod模组开源,这在一定程度上为汉化工作带来了一些困难。

从零开始的汉化流程

由于VH不是国人做的整合包,从curseforge上下载打开,内置汉化肯定是不用指望了。进去以后调成中文,首先就看到英文的文本按钮

结合打开过程中自定义的加载ui和背景,一定是用了某个自定义主界面的mod,查过mod列表发现是packmenu。packmenu对于按钮文本的存储采用了lang的形式,在版本文件夹/packmenu/resources/assets/packmenu/lang下面有en_us.json,直接复制一份,重命名为zh_cn.json,并修改图中所示的文本为中文即可。

lang部分

进存档之后加载i18n的通用汉化资源包,确认可以正常生效后我选择了提取VH中全部的modid进行筛选,把资源包中其它不需要的汉化文件删去并重新打包,使得资源包大小缩减到了几百KB,也方便后续的修改和管理。

VH的核心mod是作者团队制作的vault mod,截至目前(3.11)有上百种物品和方块,组成了整合包的大部分主线内容。绝大部分物品的名称可以通过lang文件的形式进行汉化。由于是专门定制的mod,必然不会被包含在i18n的通用包中。我从mod本体的jar包中提取出lang文件(assets/the_vault/lang/en_us.json),仿照通用资源包的原有格式加入到资源包中,这样可以确保在不修改mod的前提下通过加载单一资源包实现全部汉化。

大概是几个月后(也许是之前?),我偶然看到VM汉化组发布了VH的汉化补丁。当时还没有和汉化组有什么联系,只是查看了补丁内容并对自己的翻译做了一些调整(其实也发现了之前汉化组的一些纰漏www),想的是由于只是个人使用,所以借鉴一下他们的内容也未尝不可。

另外,也检查到其它的一些模组有英文名称的问题,比如ispawners和cagerium。查过之后发现,这些模组都是为了VH而定制的,难怪不包括在通用汉化包中。我接下来的工作就是一点点把这些lang补全。过程其实很简单,都是从对应的模组jar文件中提取出英文lang文件,改成中文后丢进资源包中,就可以完成这部分的汉化。只是有些模组的key有点多,导致工作量还蛮大的(比如模块化路由器的手册是全文写进了lang文件,到现在我还没完全写完对应的内容)。

当然,对于核心的vault mod,在写完lang文件后还是存在一些物品名称没有汉化,比如传奇宝藏、废料升级吊坠等等。这些部分直到后来引入了新的工具才得以解决。

(另外,说到传奇宝藏,这似乎是上个版本1.16时期的物品。由于VH3是在VH2的基础上开发而来的,因此其实有相当一部分旧版本的遗留产物,这些旧的物品、配置和代码到目前也只有一部分被加上了old的标识,需要体验完VH3大部分的流程以后才能区分出来哪写是旧版物品,希望VH3在正式版的时候可以把这些历史遗留处理干净。)

一个临时的tooltip处理方式

我个人的初版汉化就仅限于上述的物品名称部分。然而玩过VH的人都知道,游戏中还有大把的tooltip、gui等等均为英文。初版汉化连续用了几个版本,直到有一天在b站偶然看到一个VH的直播,发现有人做了一些tooltip,主要是装备词条的汉化。详细了解后,发现是一个专门的mod(原谅我找了很久都没找到作者是谁)。下载后发现,只是一个专门翻译装备词条的mod,不过对于当时的汉化完成度来说已经是很大的进步了。

但是我很快发现这个mod所汉化的内容是封装在mod内部的,以我当时对于java和mcmod的理解还不能自由修改或增加内容(本人以前从来没学过java,后面关于代码的一切操作基本都是充分发挥了大学时候学的计算机语法基础,以及英文阅读水平。当然后来现学现用也了解了很多)。后来经过多方面搜索,我获取了在整个汉化流程中使用的第一个附加工具——Java bytecode editor,简称jbe,翻译成中文就是Java字节码编辑器。后来我才了解到,字节码编辑属于jar包反编译的一种形式,所有的mod都是以java源码写好,编译后得到字节码class文件并实际构建成包使用的。这些涉及到模组开发方面的知识暂时不详细展开。

jbe可以直接查看class文件的字节码内容,并对其中的字符串部分进行修改。我以压缩文件的形式打开了那个装备汉化mod,发现其中只有一个class文件。我用jbe打开了这个文件,并在replaceWord方法(当时甚至不知道什么是方法,只是随便翻到了这个replaceword觉得应该在这里就打开了)下找到了如下形式的字节码:

这个形式一眼看懂,而且这里的文本是可编辑的形式,显然只需要简单的复制粘贴就可以在里面增加我想要的内容了。我试着把游戏中剩余的英文字符串写进去,并反复调整,最终发现可以汉化大约一半的tooltip、前面提到的传奇宝藏(不过那个升级吊坠不行)名称和一些奇怪的地方,不过各种gui内的英文文本,以及不是vault mod模组的部分则完全不行。

另外,我还注意到这个mod对于重复字符串的处理方式。例如上图中的"Mana Regen"和"Mana",这是两个独立的词条,应分别翻译成魔力再生和魔力值,但其中的Mana字符串是共用的。这里具体表现为,如果我在jbe中先写"Mana",再写"Mana Regen",那实际游戏中就会错误地显示为"魔力值 Regen"。推测其原因应该是从代码中读取一个替换的字符串,替换后再读取下一个,这样替换过Mana后就找不到Mana Regen了。解决方法也很简单,就是把具有重复部分的几个字符串,较长的写在前面,例如图中先写"Mana Regen",就不会对后面替换"Mana"造成影响。

为了深入了解此mod的替换原理,我获取了第二个附加工具——Java Decompiler(gui),简写为jd-gui。这个工具可以直接打开jar包,并方便地读取其中包含的全部class文件反编译后的源码。调试好以后,我顺利地打开了此mod的源码,并在其中找到了如下字段

顺便一提,从这个时期开始我算是接触到了模组代码的领域。整个VH都是基于Java运行的,其代码自然是遵循了Java语法,而我也开始尝试阅读并理解这些代码的用途。当然,即使到现在我也不可能独立写出哪怕一个单独的java源码文件。不过,我也可以直接以理解英文的角度去理解代码的含义。我注意到这两个if判断语句,第一个是“如果注册名为空,那么什么也不做并且返回”。return在各种语言中几乎都有所出现,代表当前函数结束的意思,那么我可以理解为是在说要获取一个注册名,如果注册名为空可能代表获取失败了,那么什么也不做并结束当前代码。而第二个判断条件中,显然!代表非,而后面的equals代表等于,这里应该是检查这个注册名,如果不等于"the_vault"就什么都不做并返回,和前面的情况相同。

等等,"the_vault"不是vault mod的id吗?由此,我推断出此处是在读取其它模组代码中出现的字符串,并且筛选出只来自于注册名(即modid)为"the_vault"的字符串。显然这很好地解释了前面提到的此mod只对vault mod的字符串生效的问题。很遗憾的是,jd-gui只是提供了查看功能,并不能对代码进行任何修改操作;而jbe中也找不到这一字段,于是这里的修改只好作罢。

接下来的一行代码我并不能理解,只能看出是定义了一个变量toolTip,其值为getToolTip的返回结果。由此,我大胆猜测这段内容是此mod只能替换tooltip的原因,即实际的字符串只get了tooltip,因此只能替换tooltip中包含的内容。不过为什么传奇宝藏的物品名可以替换,到现在也未得而知。后面的所有代码大概是在讲用写好的字符串去替换原来的英文字符串的实现过程,这里不展开描述了。

config处理阶段

有一次,我在翻VH的文件时偶然发现其模组配置文件目录下(版本文件夹/config/the_vault)有一个名为tooltip.json的文件。看过其中的内容后我豁然开朗:原来我前面不能汉化的部分tooltips是被存到了这里,怪不得那个替换mod不起作用。

我立刻把其中的几条修改成中文,并打开游戏测试效果。然后我傻眼了——被我修改过的部分全部变成了乱码这里就不还原当时的截图了。无奈,我以java、中文、乱码等关键词搜索,经过一番艰苦的学习后我了解到,写进json文件的中文字符在被java读取时,默认会采取gbk编码;而若想在游戏中正确显示中文字符,需要在写入json文件时直接使用中文的unicode码存储。

错误的写法(直接使用汉字存储)正确的写法(将汉字转化为对应的unicode编码)

另外,以上对于文件的浏览和编辑均在Visual Studio Code(简称VSC)中进行。VSC对于大部分格式的文件都提供了对应的扩展支持,可以方便排版和搜索等操作;还支持打开文件夹进行遍历和全局搜索等功能,在稍后提到的步骤中为我节省了很多不必要的工作。在KlparetlR的建议下,我在VSC内安装了Native-ASCII Converter插件,可以通过设置固定的快捷键快速转换中文与其对应的unicode编码。

处理完编码问题后,vault mod的几乎全物品的tooltip都解决了。受此过程启发,我开始遍历版本文件夹/config/the_vault下面的所有文件。不过也不是漫无目的地遍历,主要目标集中在display、name、tooltip、format、description等关键字相关的字符串上。同时,VM汉化组发布了适配新版本的汉化补丁(好像是U4-U6版本),在互相比对校验之后我更加确定了前面关于编码和config文件内容显示到游戏中的猜想。

经过对config文件的遍历,整个整合包的汉化完成度又前进了一大步,包括技能描述、宝库修饰语(在VM发布的版本中对应译名为词缀)、后来新版本的任务书描述等。

硬编码处理阶段

在上述工作完成后,汉化进度在很长一段时间之内停滞不前。此时还有大批量的内容没有翻译,包括各种gui,各个hud的文字部分,以及大多数聊天信息等。直到Update8更新后好久,我依然是偶然从b站上看到某个VH的直播,发现主播居然使用了大部分gui都是中文的汉化版本。然后我才注意到VM汉化组不知道什么时候更新了新版本的汉化适配,补充了对于大部分gui等信息的汉化。检查了新的汉化补丁后,我发现他们引入了新的汉化mod——VaultPatcher(以下简称VP)。随后,我在mcmod网站的相关链接处找到了该mod的github链接,并在其中详细了解了VP的用法、原理以及配置示例。

VP的中文名为保险库补丁,为什么英文名为VaultPatcher呢?引用相关开发人员的话:

最开始这个mod是专门为了VH汉化开发的,于是起名VaultPatcher(保险库补丁)。后来发现其潜力相当大,可以用于同mc版本下的大部分其它mod,于是改名硬编码汉化;然而英文名和包名已经决定好了就没有再改。

总而言之,VP的工作原理是指定在某个mod或是全局搜索源码中的指定字符串,并以对应的中文译名进行替换。详细的原理和使用方法会在后面展开描述。

经过此时的硬编码汉化后,已经有绝大多数英文内容都处理完毕了。剩余的内容,有些是由于当时VP的bug,有些是由于找不到对应的源码,或是由于其它原因,暂时还无法替换。

按照时间顺序,我决定先进入此专栏的另一核心部分——VaultPatcher使用说明。

VaultPatcher 硬编码汉化模组的使用说明

VP是国内的一位模组作者,X209开发的用于替换模组源码内字符串的mod。在我刚接触到此mod时,其版本为1.2.10(以下简称2.10)。

在这个时期,我已经熟练掌握了用jd-gui打开mod的jar包并浏览其源码,并简单了解了一个mod所必需的代码框架。我发现,mod中对于游戏内的显示内容大多数都以TextComponent("内容字符串")的形式表示,而有另一部分会写成TranslatableComponent("本地化key"),也就是可翻译的形式。前者是直接填入要显示的内容,而后者是写入一个lang文件中的key字符串,然后就可以在资源包中对对应的内容进行多语种的适配。后者的这种汉化方式被大部分模组作者所使用,也算是java为mod开发人员提供的通用的本地化接口;而前者是相当于把游戏内显示的内容直接写进了模组源码中。这里就引入了硬编码的概念:即显示文本直接在源码中,不能通过lang文件进行修改。

VH中宝库生命祭坛的相关文本,直接存储在源码中VH中技能祭坛gui的相关文本,可以在lang文件中进行修改

为了翻译这部分源码中的内容,首先直接想到的方法是直接修改这部分内容字符串。然而,大部分情况下,修改模组jar包都需要重新构建,而这需要mod本身开源,才能获取到必要的依赖库等等。另外,部分模组作者规定了经过修改的mod本体不允许公开分发,这对于汉化组的工作来说是致命的。

1.2.10说明

我大概在今年的6月初加入了VP的讨论群,然后才得以深入了解VP的种种。这里要强调一下,在从我接触到VP开始的相当长的一段时间内,VP的可用最新版本为1.2.10,其工作原理与不久前更新的新版本VP(1.2.13/1.2.14)大相径庭。我在这里贴一下模组作者给出的2.10的工作原理图。

VP(1.2.10)工作在第三阶段“加载到显示缓冲区”

可以看到此时VP是在对应的英文文本显示到屏幕上时将其替换为事先配置好的中文翻译。2.10版本的VP在配置时,会在版本文件夹/config下面生成名为vaultpatcher的文件夹,内含两个文件:config.json和mods.json。后者的文件名可以任意设置,只需要写好对应即可。其具体的使用方法,需要首先了解config.json的结构。

config.json的全部内容示例

分为三个部分:mods、debug_mode和optimize_params。其中optimize_params的配置在通常情况下保持默认即可。mods中填写的是config/vaultpatcher下的另一个json的文件名,该文件是用来存储需要替换的字符串以及替换后的内容的。这里我的另一个文件为3112.json,所以我填写的是"3112"。注意这里不需要填写后缀名.json,以及文件名需要用英文双引号包括。(由于一些bug,2.10只能加载单个模块,即mods部分只能填一个文件名。不过影响不大,全部内容都塞进一个文件就行)

debug_mode部分下为开发人员所可能用到的配置。如果仅仅是使用该mod而不需要进行汉化工作,那么完全可以保持其默认配置不变(即图中所示),其详细用法会在后面调试部分说明。

然后来关注一下VP所汉化内容的本体,即上文提到的3112.json,全部的翻译内容都会记录到这里。该文件的内容是分块储存的,每个块对应了一个要翻译的字符串以及翻译后的目标字符串。

内容json的第一个块

文件开头第一个块与翻译内容无关,用来记录该文件的一些信息。从图中可以看到一共包含了4个键值:authors(作者)、name(模块名称)、desc(模块描述)和mods(适用模组)。需要注意的是,此部分内容只用于显示,是方便让编辑人员看到此文件时了解基本信息所用,也可以在游戏中使用/vp list命令查看此部分信息。因此,这部分可以随意填写,甚至留空也不会有问题。

其余部分的一个示例

接下来的每个替换块均描述了一个需要替换的字符串,其形式如图所示。可以看到包含了三个键值:target_class、key和value。其中target_class中又包含三个子键值:name、method和stack_depth。简单来说,target_class描述了所要替换字符串的位置,key是要替换的字符串,value是替换后的字符串。也就是,在target_class处找到key,并将其替换为value。多么简单

首先说明key部分。通常来说,key部分填入的都是mod源码中英文双引号包括起来的内容,这些部分一般会在各种代码查看器(如VSC和jd-gui)中以特殊颜色标注,便于寻找,也为我后面的遍历工作节省了不少时间。例如我们可以填入前文中图示的Health Points,相当于VP在运行时就会搜索与你输入相匹配的字符串并尝试替换。注意这里需要区分大小写,以及注意首尾的空格。

value和key类似,是存储对应译名的地方,例如前面的例子中,我们填入生命值,就会在游戏中把Health Points替换为生命值并显示。不过,有一点不同的是,value在填写时有两种方式:一是直接填写中文翻译,被称为完全匹配;也可以选择在译名前加一个@,如上图示例所示,此时的匹配方式称为半匹配。二者的区别在于,完全匹配要求key部分的内容必须为完整的字符串,也就是在代码编辑器中以特殊颜色标注的部分必须原封不动地复制到key部分,且源码中不能有其它附加部分或是格式化的代码;而半匹配方式是只要搜索到与key相匹配的字符串,就会直接进行替换。具体如何进行选择,典型的例子像这样:

只有TextComponent("内容")的,建议用全匹配这里的"XX Chest",形式为(rarity + "XX Chest"),必须用半匹配

另外,2.10版本不支持通配符。如果字符串中包含如%d、%s这样的通配符,那么实际在写字符串时需要从通配符隔开,也就是前后部分分别写一个替换块,分别进行翻译,并且都必须使用半匹配的模式。

(不难发现,半匹配在不考虑其余影响的情况下是兼容完全匹配的。所以有个小小的tips就是如果用完全匹配发现不能替换时,试试只是加一个简单的@吧)

接下来说说target_class。将一个mod的jar包用压缩文件打开,在除了assets、data、META-INF这三个文件夹之外有一个用于存储该mod全部的源码的文件夹(默认为com,不过大部分mod中都改成了对应的包名),此文件夹内逐级展开,最终会发现全部是class类型的文件。那么,这里的target_class直译就是目标class文件的意思,限定了目标字符串的搜索范围,也就是说定义了前面所说的在target_class处对该块内的字符串进行替换。其中有三个子键值,这里一一说明。

name是必须填写的部分,规定了详细的class文件的路径。其写法是,获取class文件在jar包中的路径,各级文件夹之间用英文字符"."隔开。例如在

这一模块中,"Hold "所处的位置是iskallia.vault.event.ClientEvents,则其实际路径为

注意图片顶端显示的路径

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

jd-gui中字符串部分均以蓝色标注,同时注意在这里左侧路径的表示方式

也就是说,这里的target_class要精确写到具体的文件名及路径,且需要删去.class后缀名。那么,如果每个字符串都需要这样写class文件名称,是不是有些过于繁琐了呢?因此2.10提供了name键值的三种匹配方式:精确匹配、包匹配、类匹配。

前面所描述的为精确匹配方式,顾名思义就是需要写出完整的路径和文件名。

而包匹配是定位到对应的package,其实现方法是在name的内容前加上@。对于了解java语法的人应该很容易理解;如果没有接触过java,那么可以简单这样理解:包匹配相当于文件夹匹配,例如上面的iskallia.vault.event.ClientEvents,用包匹配可以写为@iskallia.vault.event,是不是省略了后面的class名称从而达到缩减工作量的目的呢?当然,代价就是从搜索单一文件变成了搜索整个文件夹下的所有文件,一方面会使效率和性能有所下降,另一方面如果同一文件夹下的其它class文件中也有相同的字符串,那么就会出现误匹配的问题。更进一步,上面的例子可以进一步缩写为@iskallia.vault,甚至是@iskallia。当然也会使得搜索范围一步步扩大。

类匹配,按照模组作者的说法是结尾(class名)匹配,实现方法是在name的内容前加上#。也就是说,前面的iskallia.vault.event.ClientEvents理论上可以缩写为#ClientEvents(其实我也不知道对不对。。。)。据不少人反馈,类匹配似乎有比较严重的bug,所以这里就不详细说明了,总之是不推荐使用(

接下来看另外的两个子键值method和stack_depth。

method是指方法名。方法名是java中的专有名词(实际上我直到几个星期之前才了解其概念并会在VP中使用),对于没有java基础的人来说可能难以理解。不过没关系,方法名在大部分情况下可以留空!

(下面是一个具体例子,如果不需要用到method可以跳过)

填写method键值意味着限定了所替换字符串的方法名,相当于进一步限制了目标字符串的范围。当然,不填写意味着此替换对全部方法名生效。那么,method处具体应该填写什么才能保证达到限制的目的又不会出问题呢?为了便于展示,我用VSC打开vault mod的源码,并截取出一段典型代码如下(VSC的颜色区分更明显一些):

首先看下方红色横线的部分。这部分代码在游戏中的作用是,当有人进入宝库时,在聊天栏中打印信息:" entered a(n) Vault!"。如果这段翻译为中文,应该是" 进入了一个 宝库世界!"。这里标粗的文本是替换的部分,显然和代码中一一对应。那么应该分别写两(三)次替换(这里为了把a和an区分开):entered a(n) 替换为 进入了一个,而 Vault 替换为 宝库世界。注意这里的后者,首先,根据前面完全或半匹配的原则,这里必须采取半匹配的方式才能成功替换;而Vault这个词在VH中是个相当高频的词汇,就会出现一些问题。另外,半匹配有另外的更为严重的bug,会在后面说明。恰好,在同一个class文件靠前一点的位置还有这样一段代码:

这段代码画横线的The Vault部分的作用是,在玩家进入宝库时,在屏幕正中央打出"The Vault(subtitle)Good luck, "的欢迎信息。那么根据前面所说的半匹配替换的原则,如果后面写了把 Vault 替换为 宝库世界,那么这里的 Vault 也会被替换为 宝库世界,那么在屏幕中实际就会打出"The 宝库世界"。是不是有点类似于前面那个词条mod中所提及的字符串重复的问题?可惜的是,VP并不支持那种写在前面就先行替换(比如把The Vault整体先行替换成宝库世界以避免冲突)的逻辑。当然,另一种处理方法是同时把"The "(注意这里的空格)替换为空字符串(value是可以留空的,效果就是替换为空字符串,相当于删去了对应的key值),实际显示的居中欢迎信息也会是"宝库世界"(好怪)。理论上当然可行,不过还有一些其余的问题,会在后面说明。

这时method的作用就体现出来了。注意到这两个信息一个是在聊天栏显示,另一个是在屏幕中央显示,有java基础的人通常就可以直接断定这两个字符串所在方法的方法名不同了。实际上,一个字符串所属的方法名,以上述两个Vault为例,从图中顶端画横线的位置可以读取到各自的方法名(在VSC中会被标识成浅黄色)。其实就是从字符串的位置向前追溯,查明其代码位于哪个方法体的内部,在这个块的开头的浅黄色字符串就是对应的方法名。例如,第一个聊天栏信息中的Vault对应的方法名就是printJoinMessage,而第二个居中欢迎信息的方法名为onTeleport。(注意大小写要正确)

显然,这两个分别属于不同的方法名。于是,我在测试的时候只写了这样的替换块:

并进入游戏测试,结果就是只有聊天栏中的Vault被替换为了宝库世界,而居中显示的欢迎信息依然维持英文不变。那么显然我可以继续在此基础上写一个新的替换块将The Vault也替换为宝库世界,且此时可以选择method处留空,不会有影响。

这里说一下前面提到的一个严重bug。似乎是在value处使用半匹配的情况下,如果不对method或是下面提到的stack_depth进行限制,那么前面的name限制会完全失效,无论是采取精确匹配,还是类匹配或包匹配的方式。这个bug可能和半匹配本身的搜索逻辑有关,模组作者X209似乎也提到过此bug难以修复。由于此bug的存在,前面在替换字符串Vault时使用了半匹配,那么如果不限制method或是stack_depth,那么游戏中所有的Vault都会被替换为宝库世界,对于一个如此高频的词汇而言必定会出现一些问题。因此前面我有提到不能使用单独把The替换为空字符串之类的解决方法。

接下来是stack_depth项(也没什么用,想跳就跳吧)。从本质上来讲,这一项的功能和前面method的作用有点重复,不过侧重点有些许不同,因此在某些情况下还是有一些作用。stack_depth是指匹配替换时的堆栈深度(好吧又是一个专有名词)。和前面一样,实际并不需要详细了解其原理,会用就行。可以看到上面的所有替换块中stack_depth都填了默认值-1(这也说明了这一项有多不常用),-1就代表了不加限制。

要具体使用这一项进行限制,首先需要再次检查前面的config.json

将debug_mode中的is_enable项改为true,同时检查下方optimize_params中的disable_stacks项为false,后面的stack_min和stack_max项均为-1。

确认无误后,在内容json中准备至少一个替换块,并需要知道怎么让替换过的文本显示在屏幕上。例如我写了这样一个替换块

接下来,启动游戏,进入一个世界并至少让替换后的结果在屏幕上显示一次。然后一定选择保存并返回标题屏幕并通过主界面的按钮退出游戏。成功关闭后,打开版本文件夹/logs/debug.log(这里要选择GBK编码打开,否则会有乱码),在文本中搜索替换后的字符串,例如这里搜索生命值。搜索结果一般不止一个,不过大多数都为重复的语句。选择其中正确替换的语句,单独提取出来应该类似这样

这串语句的含义是将Health:替换为了生命值:,替换堆栈为后面[ ]的全部内容。后面的堆栈列表中,两个相邻堆栈用英文逗号隔开。那么,这里的堆栈深度需要填的就是后面的总计堆栈数,通常在20-100之间(如果不想一个一个手动查也可以搜索并统计英文逗号的数量,并+1就是实际的堆栈深度)。例如,这个语句中共有37个堆栈,那么前面的替换块中的stack_depth项就应该填写37,就可以达到唯一筛选当前替换项的目的。(KlparetlR曾经提到过,不同位置的同一字符串在替换时,其堆栈深度多多少少会有些浮动,因此限定堆栈深度也可以起到筛选作用,比如前面那个聊天栏中的Vault的堆栈深度为41,那么只填写堆栈深度而留空方法名,也可以起到相同的筛选效果)

(堆栈深度的应用方法、难度和不确定性比方法名难了不止一点,评价为能不用就不用)

最后,整个target_class项可以完全省略,也就是将替换块简化成为

是不是清爽了很多?只需要写入源字符串和替换结果就好了。当然,这种一般只适用于比较长的字符串,或是很确定这个字符串在整个游戏中是唯一的(从而不会出现误匹配或其它问题),以及无法定位代码所在的class文件,或是这个字符串压根就不在源码中(没错,这种也能替换,参见专栏后面的补充),导致没法写target_class的情况。

接下来是1.2.10的另外一种常用手段和所引起的bug。1.2.10支持"二次替换"。这是什么意思呢?还是以前面的Vault为例,在不加限制的情况下我们看到居中的欢迎信息变成了The 宝库世界。不考虑其它bug的情况下前面提到过的处理方法中有一种是将"The"替换为空字符串;同时也可以使用这样的方法:将"The 宝库世界"作为key的值,替换为"宝库世界"。也就是这样写:

看起来很不可思议,但是如果只是考虑替换内容的话,确实可行。看起来是一种不错的重复字符串的处理方式,然而,这种操作会导致两种bug:一是文字位置错误的问题。常发生在各种需要居中显示或右对齐的文本处。例如上面的The 宝库世界为在屏幕中央显示,在二次替换后实际的文字内容就会偏左。我没研究明白其中的原理,不过根据实际测试发现,大概是因为第二次替换的时候字符串的位置已经定下来了,而二次替换过后的字符串默认为左对齐,因此在某些需要居中显示的情形下进行二次替换都会发生一些排版问题。

如果说位置错误只是小bug的话,那不得不提到另一个困扰全部开发人员许久的bug:在二次替换的情形下,如果被替换的字符串后面有格式相关的代码,例如改变颜色之类,那么对应的字符串会直接消失。比较典型的例子是VH中有一个必须用半匹配替换的字符串Value:→经验值:,而很多vault mod物品的tooltip都有Soul Value:一项,后面会附带具体的紫色的数值。而在二次替换Soul 经验值:为灵魂价值:的情况下,后面的数值会直接消失。显然这样的bug对于VH来说无法接受。因此在很长一段时间之内,都是直接舍弃了翻译不那么重要的Value,而直接翻译Soul Value整体。

不过二次替换总体来讲还是方便了许多,这里给出一个很好的使用例。

该替换块来自VM汉化组

这两个替换块是把VH中屏幕右上角的 X unspent skill point(s) 替换为了 X 未用技能点。这里value使用的是lang中key的形式(没错,VP支持写成本地化key的形式并通过加载资源包来替换,这个VP.modify.the_vault.10的内容实际就是"未用技能点")。由于英文当中的复数形式问题,有一个技能点和多个技能点时实际的英文会有所区别。首先来看看源码中是如何实现的:

是在 unspent skill point 后面加了一个条件判断,如果前面显示的数字为1就在后面加空字符串,如果不为1就加一个s。对于英文显示来说完全没问题,但在翻译成中文时,如果只翻译 unspent skill point 为 未用技能点 ,必然会出现复数情况下显示为 X 未用技能点s 的情况。当然,这里也肯定不能用半匹配的方式将s替换为空字符串,因为半匹配会替换所有的s,显然会出现很多问题。于是,这里使用二次替换的方式,将 未用技能点s 作为一个新的替换key,而替换结果依然是 未用技能点 。而在二次替换生效时,由于这里的对齐的基准是以 未用技能点s 的左端点为锚点进行左对齐,因此实际的排版看起来只是删去了后面多余的s,从而巧妙地利用了这一特性达到了想要的排版效果。

最后,三次或三次以上的替换基本不会生效(所以究竟是多么复杂的地方需要用到三次替换呢,不如考虑一下其它的替换方法吧)

催更(?)

在使用了VP2.10后,根据VP群友的说法,他们没能联系到前面tooltip汉化的作者取得授权。因此VM的汉化补丁中没有包括那个mod,而是改用VP承担起了全部的翻译工作。

然而VP2.10的种种bug导致了汉化进度一直卡在几个无法处理的字符串上(比如前面提到过的soul value,以及cooldown reduction和mana与植物魔法中mana冲突的问题等)。于是我和KlparetlR等人在VP的讨论群中持续地报告各种新发现的bug,同时一直在催更(

当时大概是今年的6月中旬到下旬,X209回复说要等到7月放假。于是这段时间汉化进度的推进被暂时搁置。

然后7月16日,X209发布了VP的1.2.13版本(以下简称2.13)。

1.2.13/1.2.14的说明

2.13完全重构了字符串的匹配机制,并且将mod重置为core mod(core mod的具体定义我不太清楚,理解层面上大概是说,实际在游戏中增加内容的,比如说ae、植魔、暮色森林都不是core mod,core mod加载的时间在进入游戏之前,有点类似于各种lib mod的形式,只在游戏启动加载时生效,而对于实际游戏内容没有大体更改)。

2.13引入了ASM框架。

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

——来自百度百科

看不懂也没关系,我也看不懂

KlparetlR曾经报告过一个bug,在使用VP2.10对大批量字符串进行替换时,会导致游戏性能显著下降,包括加载时间延长,卡顿掉帧等。尤其是在存错了大量物品的ae或是简单存储网络(SSN)模组的存储终端中,在使用按名称排序的方式时会导致帧数骤降,甚至无法正常运行。

其原因不难理解。我们引用上面的一张图

这里VP2.10的替换位置为源码加载到显示缓冲区时。然而,显示缓冲区是逐帧实时更新的,也就是说每帧都需要对写好的字符串进行一次匹配(如果开了debug模式,就会在debug.log中发现大量的重复替换语句)。显然这种替换方式会占用大量运算性能。尤其是上面的按名称排序问题,排序算法和VP2.10共同导致了甚至是逐帧刷新全部物品的排序方式。在存储终端中有几百上千的不同物品时,这必然会大量占用运算性能,导致极为严重的卡顿。

ASM框架的引入使得字符串的内容可以在上图中第二个步骤进行替换,从而完全避免了缓冲区刷新的问题。进一步来说,2.13的加载时间是在游戏启动到加载完成进入主界面之前,其作用相当于在启动加载时直接操作模组源码,将对应的字符串进行替换,这样读取进显示缓冲区内的为替换好的中文字符串,不会出现需要逐帧刷新的情况,当然不会出现影响性能的问题。当然,这会让游戏的启动加载时间略微延长,不过对实际游玩完全没有影响,与性能下降相比几乎不算问题。这里附上X209提供的一张2.14的工作原理图(最新的图是2.14版本,不过差别不大)。

2.13的配置文件结构与2.10的区别不大,下面是2.13的一个典型匹配块,主要改动如下。

首先,配置文件的储存文件夹名称从vaultpatcher改为了vaultpatcher_asm,其余结构保持不变。

在替换块方面,2.13取消了key原有的半匹配机制,只支持完全匹配,同时也可以替换原来2.10必须使用半匹配的场合。也就是说,这里的key必须精确地填写与源码中与目标字符串完全相同的内容(jd-gui中显示为蓝色,VSC中显示为浅棕色),包括字符串首或尾可能存在的空格,同时这也要求替换过后的字符串要考虑首尾空格匹配的问题,视具体情况而定(专栏后面会举出例子)。

相应地,2.13全面支持代码中的通配符,如%s或%d等。因为替换过程发生在通配符没有被替换为实际内容之前,所以替换字符串中包含通配符(必须是源字符串中包含的通配符,不能手动添加,如下图)不会引起问题。同时要注意key和value中的通配符类型、数量和顺序都要一一对应。

源码部分替换块的写法

2.13的target_class部分仅支持精确匹配,也就是必须完整填写目标字符串所在的class名称及其路径。同时要注意的是,与2.10不同,这里的class需要精确到内部类。

我花了很长时间才研究明白什么是内部类。

内部类(Inner Class),是 Java 中对类的一种定义方式,是嵌套类的一个分类,即非静态嵌套类(Non-Static Nested Class)。内部类(非静态嵌套类)分为成员内部类、局部内部类和匿名内部类三种。

看不懂也没关系,我也看不懂

起初是我在测试2.13版本时候发现有部分字符串明明写的路径看起来没有问题,但是实际却不能替换。观察总结以后发现,所有这类问题有一个共同点:源码同时位于两个class文件中,其中一个文件的文件名较短,但是文件内容较长;而另一个class文件名相当于在前者的后面加上了一个$字符,同时后面跟了一些另外的东西,且其文件内容会短一些,看起来像是从长文件中截取出的一部分。

一番搜索之下,我发现这个额外多出来的类似子文件一样的文件称为内部类。mod在编写时都是以java源码的形式编写(即此时后缀名为.java),然后整体丢进IDE中进行构建和编译,编译过后,java文件会变成class文件,就是实际在mod的jar包中看到的那些。这个编译的过程中,有时候一个java文件会被编译为数个不同的class文件,其命名方式为作为主体的外部类名称.class,和可能存在的外部类名称$内部类名称.class(对不起,关于这部分的生成机制我实在是看不懂>﹏<)。后者是前者的一个子文件,称为其内部类,其代码内容同时存在于两个文件内。例如上面图中的字符串"Difficulty: %s"同时存在于以下两个文件中。

那么,如果有一个要替换的字符串恰好位于这个内部类中,就会出现该字符串同时出现在两个文件中的情况。此时target_class应该精确定位到内部类的名称,也就是需要加上$后面的内容,如上面的"Difficulty: %s"所示,如果仅定位到外部类则不能替换。

另外,2.13保留了方法名的限定,删去了stack_depth字段都说过没用了吧。方法名部分的用法与2.10版本相同。

在说明2.13的config.json结构之前,考虑一个问题。2.13要求了必须写target_class部分且必须精确匹配,这就导致每个匹配块都需要写很长的一段路径,而且有些带有内部类的文件名长达四五十个字符,如果需要一个一个填写,必然会增大配置文件的体积,也会降低编辑效率。在KlparetlR的反馈之下,X209在不久之后又更新了VP的2.14版本。

2.14是在2.13基础上做了一个小改动,使得target_class字段变为可选,也就是替换块可以像2.10那样只写key和value,适用于没找到源码位置的情况。但必须要在config.json中做出相应配置,否则不能正常替换还是会崩溃来着。

可是你都知道了字符串在源码中,为什么不用VSC的全局搜索呢

2.14可以完全替代2.13,因此这里直接说明2.14的config.json结构。

首先是和2.10一样的mods块。这里mods下面可以写多个文件,两个文件名之间使用英文逗号隔开,最后一个文件名后面不要有逗号(一定记住这一点,否则整个VP都会失效,不过如果是在VSC中编辑,会有相应的报错信息)。

这些文件需要并列地和config.json共同存储在版本文件夹/config/vaultpatcher_asm下。其实2.10是因为神秘bug导致仅支持单个替换文件,后面版本只是修了这个问题。

mods下面新增了名为apply_mods的块。其中填写的是版本文件夹/mods下面的jar文件名(不包括.jar后缀),也就是mod本体文件的文件名。

apply_mods是为了配合省略target_class的情况而配置的。也就是说,如果在替换块中省略了targrt_class段,那么对应字符串的搜索范围就是apply_mods下面的全部文件,也就是对应模组的源码。可以写多个,格式与前面mods段相同;也可以直接留空并删去整个apply_mods段,但是留空的情况下要求所有的替换块必须全部指定target_class。

后面的debug_mode与2.10配置方法相同。

debug mode/调试模式

(以下内容仅在2.10测试并应用过,2.13未知)

要开启调试模式,需要将config/vaultpatcher/config.json中debug_mode下的is_enable设置为true。

后面的output_format下支持占位符(相当于key)、(相当于value)、和。由于的显示内容实际包含了的内容,因此通常只使用前者即可。此处的配置实际上是定义了以下内容的写法

调试模式的用途其一就是上面提到过的统计堆栈深度。

另外,仔细观察上图中所显示的内容可以发现,堆栈列表中蓝色的部分(#之前)其实是该字符串所有出现过的target_class下的name,而浅黄色的部分(#之后)则是对应的method。实际2.10的工作原理是从此堆栈列表中挑选出所限制的堆栈位置并加以替换。

因此调试模式的另一个用途是辅助定位所替换字符串在源码中(或引用到源码中)的位置。通常只关注对应mod下的target_class(例如此处可以只关注以iskallia.vault开头的堆栈)。实际使用时,大概是先对未知位置的字符串不限制替换位置进行替换,然后在日志中找到对应的替换语句就可以精确定位了。

继续汉化流程

硬编码优化时期

2.14不支持二次替换。当然,2.14本来也从根本上解决了需要进行二次替换的问题,因为所有字符串都是完全精确匹配,且不会出现半匹配时候的name限制失效的问题。例如,在前面的未用技能点的问题部分,可以使用2.14将 unspent skill point 替换为 未用技能点,同时直接把 s 替换为空字符串,不会引起任何问题。

弄清楚2.14的工作原理后,我花了一段时间把原2.10的全部字符串转移到了2.14,同时删去了那些必须二次替换的部分,因为2.14实际是替换与key处全字匹配的字符串,除非同一个限定位置有两个完全相同的字符串,否则不会出现任何重复问题。

当然,因为后面要讲的另一些问题,有些字符串使用2.14替换会引起极其严重的错误,直接导致游戏崩溃。无奈之下,我只好放弃翻译那些字符串,这样在游戏中存留了少部分的英文内容,不过看起来无伤大雅,而且此时的整合包运行相当稳定且流畅。

当然我不可能止步于此。此时的汉化进度大概停留在85-90%,我需要继续推进。有一天,我在回想X209关于2.10以后版本VP的更新描述,记得他有说过2.13和2.14是完全更改了VP的整体架构,相当于是重写了一个mod。我就在想,能不能实现2.10和2.13的混用呢?这样,以2.14为主体,其余会导致游戏崩溃的汉化内容由2.10完成,应该可以在尽可能维持高性能的前提下提高汉化进度。

然后,在同时加载2.10和2.13时,游戏不出意外地在启动之前就崩溃了。仔细检查之下,发现其问题出现在二者的包名重复,也就是都是vaultpatcher。哪怕这两个mod的作用时间和原理完全不同,这样重复的包名也会导致冲突问题。当时向X209提出了issue,不过他那段时间在忙另外的工作没办法继续更新。于是我自己动手从github上拿到了2.10版本的源码,把所有的包名以及vaultpatcher相关的字符串都改成了vaultcompatcher好粗暴,取compat有兼容之意。改好后,我把源码包发给了KlparetlR并拜托他帮我构建编译我自己的电脑java环境一直有问题,困扰了我好久。赞美KlparetlR111111

后来,我顺利地用修改后的2.10版本补充了2.14无法替换的部分,使得汉化进度一度接近100%。

硬编码时期的问题

在继续完善汉化之前,这里着重说明一些在配置硬编码的时候遇到的各种问题。这些问题基本都以VP2.14版本为基础,因为2.14是目前汉化工作的主力。

部分文本内容不兼容显示中文

VH在Update 9更新中加入了新手任务系统,与其相关的文本有任务书的全部gui内容,以及每次完成一个任务时会在屏幕右上角弹出临时的提示信息。

任务书右侧各个任务的详细描述文本储存在版本文件夹/config/the_vault/quest/quest.json和sky_quest.json下的description字段中,分别对应了正常世界和空岛世界版本的任务书内容。这一部分可以通过修改为中文并转换为unicode编码的形式来成功汉化;然而左侧的各个任务标题,存储在上述两个文件的name字段中,当我尝试汉化这部分文本内容时,发现游戏中对应的标题可以成功替换,但会导致该任务无法完成。(后来我尝试把任务标题修改为任意的英文字符串,测试发现不会出现上述问题,说明问题在于不支持中文字符)

汉化后的quest.json的内容

而任务完成的提示文本"You have completed a quest!",当我在2.14下将其替换为中文后,在游戏中完成任务时会卡死并崩溃。具体的替换方法为

检查崩溃报告后我发现了如下的报错信息

翻译过来大概意思就是:任务完成提示的显示信息需要编码的部分长度过大(当前25字节,最大9字节)。这符合替换过后的字符串长度:"你完成了一个任务!",共8汉字+1英文感叹号,共计占用25字节(以GBK编码方式,每个汉字占用3字节)。首先我检查了对应源码的实现方式

然而以我粗浅的java基础,实在不清楚为什么这里限制了字符串的长度,以及原字符串的长度其实也超过了9字节(。于是我采用的解决方法是这部分内容使用2.10进行替换。

"You have completed a quest!"这段字符串使用2.10进行替换测试,一次就成功了,任务完成时的提示可以正常显示,且文本为中文。而且可以用完全匹配的方式(其实这段字符串在整个游戏中也是唯一的,使用半匹配也不会出现重复的问题)。

同理我尝试使用2.10汉化任务书左侧部分的标题(需要半匹配),居然也成功了,而且任务可以正常完成。有趣的是,可以发现这部分替换内容实际上不在源码中,而是存储在vault mod的配置文件下的相关json文件。游戏在运行时的处理逻辑是将标题的字符串从json文件读入到源码中,然后将其显示到屏幕。那么为什么2.10可以替换这部分文本呢?

使用VP2.10对任务书的标题内容进行替换

回想一下前面那张图

2.10的原理是将输入到显示缓冲区的文本进行替换。换句话说,2.10的本质工作原理不是硬编码汉化,而是显示文本汉化。也就是说对于任何显示到屏幕上的文本,无论其来源是不是模组源码,理论上都可以在2.10中进行替换(当然此时因为没有对应的target_class,所以替换块中只有key和value两个值)。而2.10对于显示缓冲区的修改与实际游戏的代码运行过程关系不大,例如那段"You have completed a quest!"字符串,使用VP2.14替换时是直接将代码替换为中文,那么实际运行时候就会检测到字符串长度过大,从而导致游戏报错并崩溃;而2.10是在这段字符串在内部处理完毕准备打印到屏幕上时进行替换,而在内部处理时仍然保持完全英文的状态,自然不会导致报错。这也算是2.14在修改模组源码时带来的一个弊端。

无法定位的字符串

其实这个问题与硬编码汉化没有太大关系,而是反编译过程出现的问题。起初是发生在VP2.14版本还没有更新的那段时间,由于2.13要求必须填写target_class,所以对于那些在源码中但无法精确定位的字符串,就只能被迫选用2.10进行替换。

最开始,我使用了jd-gui自带的反编译功能导出vault mod的源码包。但由于jd-gui开发时间较早年久失修,对vault mod的兼容性不是很好(vault mod是基于JAVA17开发的),于是偶尔会出现一些缺失代码的问题,包括直接在jd-gui中查看的代码可能也不全。所以,这里建议将mod的jar包使用完整的IDE环境(例如IntelliJ IDEA)进行反编译为java源码,并用VSC打开以搜索目标字符串的位置。我自己的电脑的IDE环境配置一直有点问题,还是要感谢KlparetlR发给我反编译好的源码

不过,这个问题在2.14中被新增的apply_mods功能解决了。只需要确定目标字符串属于哪个mod,直接填写mod文件名也可以匹配到。

一些直接使用变量名/id/有特殊用途的字符串

还有一部分显示的内容实际是直接打印了模组源码中的变量名、特殊命名id或是不允许替换的一些特殊字符串。这类问题往往是最难处理的,有些甚至直到现在还保留为英文的状态,并且没有任何办法。

首先是宝库之神的名字。这些名字在定义时被定义为了枚举类型

这里的name在源码中的许多地方被getName()方法所引用并和其它元素组合成为新的String,而后者通常仅支持ASCII字符集(即字母、数字和一些特殊符号等)。如果使用VP2.14对此处宝库之神的名字进行替换则会引发很多问题。典型例子的就是祭坛处获取一个眷顾后,再次进入宝库会导致无法打印对应宝库之神的聊天信息,获取对应的buff,以及在此次宝库全程后台一直在输出报错信息,导致大幅度卡顿和其它问题。然而,在属性统计界面还需要直接打印这个字符串

所以我选择使用2.10对这部分字符串进行替换。替换块的写法为

注意这里target_class的写法。前面提到这些宝库之神的名字需要在源码的不止一处被引用,且这里的字符串必须为半匹配才能替换。由于半匹配有name失效的bug存在,我需要限定method和stack_depth的至少一项才能使这个替换不会影响到其它引用该字符串的场合。

那么,name和method应该填什么呢?这些代码的来源是上面枚举类,其name值为iskallia.vault.core.vault.influence.VaultGod。显然如果直接从来源处进行修改,会影响到其它引用这些字符串的地方,那就和2.14版本的替换效果没有区别了。

由于2.10本质不会操作源码,所以我检查了上述统计页面打印此字符串的相关代码

此处代码的路径iskallia.vault.client.gui.screen.player.StatisticsElementContainerScreenData,也就是上面替换块中的写法。而this.getGodFavorTitle(VaultGod.VELARA)则负责显示上方游戏截图中所显示的内容,getGodFavorTitle为方法名,VaultGod.VELARA为引用来源,也就是代码运行时先将VaultGod.VELARA处(也就是上面枚举类中的name和title字符串)的代码引用到这里,然后输出到屏幕。以及getGodFavorTitle方法的具体代码:

可以看到确实是分别进行了getName和getTitle,与实际的显示内容相对应。获取到这里的目标字符串仅用于输出到屏幕,因此我实际可以在上述代码中的getGodFavorTitle和getTooltipExtendedVelara两个方法名中任选其一(因为getName到处都在用,可能还会导致误匹配)进行限定,都可以把匹配范围限定到这里。

另外举出一个更为极端逆天的例子,是包括了一些字符串的特殊处理方式。首先给出一张宝库中宝箱gui标题的截图(在不加载VP2.10的情况下)

宝库中宝箱分为四种稀有度:Common、Rare、Epic和Omega。在打开宝箱时,对应的稀有度信息会如图显示在标题处。负责标题显示的代码如下

显然宝箱名称的部分" Wooden Chest"可以直接使用2.14替换为"木质宝箱",实际截图中也没有问题。然而对于前面显示的Common,即使使用全局搜索也根本找不到完全匹配(大小写)的字符串。于是我继续分析这里的代码,此处实际显示的字符串是 变量rarity+" Wooden Chest"。rarity的定义在上图第三行给出:

String rarity = StringUtils.capitalize(this.rarity.name().toLowerCase());

其中实际的内容为this.rarity.name()。在同一class文件下可以继续追溯其来源

于是定位到名为VaultRarity.class的文件中。查看此文件中关于Common内容的定义怎么又是枚举类

好家伙,直接是变量名。注意到这里实际的变量名为COMMON(全部大写)。联想到前面的语句

String rarity = StringUtils.capitalize(this.rarity.name().toLowerCase());

不难发现Common的生成过程:先使用toLowerCase()将this.rarity.name()(即"COMMON")全部转换为小写(即"common"),然后用capitalize(common)大写其首字母c得到最终的输出字符串变量rarity = 'Common'。怪不得我根本搜不到完全匹配的字符串。

且不提枚举变量名本身就无法用2.14替换,即使是正常可以替换的字符串,通过这样的大小写转换后也无法直接通过搜索完全匹配。例如前面的宝库之神处的title="The Benevolent",实际在图中显示为"the Benevolent",也是通过decapitalize()方法把首字母转换为小写然后显示的。这时使用2.14匹配时要首先分析有没有对应的大小写转换算法,才能确定实际源码中存储的字符串的形式,然后才能确实使用2.14进行替换。

但是,我直接选择使用2.10。甚至由于大部分相关的字符串在源码处都已经被2.14替换为中文(显然2.14的优先级比2.10高出很多),我可以直接在2.10中写

并进入游戏测试,宝箱标题的gui可以正常替换,也没有出现任何其它问题。

相同的做法还有子悬赏的标题等,这里不再详细说明。

特殊使用例之一

Update 11重做了地牢结构,会在普通房间中随机生成地牢门,在打开地牢门时屏幕中央会显示一条提示地牢难度的信息

地牢共五种难度:Normal、Hard、Challenging、Extreme和Impossible。前面在提到通配符时也有涉及这条显示信息的代码

在使用2.14进行汉化时可以将"Difficulty"替换为"难度"。而后面的难度字符串部分,我最终在版本文件夹/config/the_vault/gen/1.0/palettes/generic/dungeon_door_placeholder.json中定位到了对于地牢难度的描述

但当我将这里的字符串替换成中文并用unicode编码保存后,进入游戏发现原本生成地牢的位置由于某种错误变成了

显然上述的改动会引起问题,于是我使用了VP2.10对这部分字符串进行处理。为了确保不会误匹配,我对替换块中的target_class和method都进行了限制,限制到上述"Difficulty: %s"的位置即可。由于源码中是以通配符%s的形式表达此字符串,因此需要使用半匹配的方式,具体的替换块写法如下:

其余四个难度字符串相同处理。经过实际测试,地牢门结构可以正常生成,且对应的提示信息可以正常汉化。

特殊使用例之二

这里提到的是VP2.10限定的一种特殊的替换方法。这一方法大概是KlparetlR发明的,是类似如下形式的替换块

这里MaZa_RinE是我自己的正版ID,使用了完全匹配的方式,替换结果为如上所示的字符串。这个替换的实际效果为:可以通过预先配置的形式将某个特定用户的ID进行替换,可以更改其ID的实际显示内容,也可以增加一些特殊效果。例如上述替换在游戏中实际的显示为

具体的效果实现需要参考mc自身提供的格式化代码。以及,这段代码的格式其实来自于VM汉化组内置的对于汉化组成员的ID替换,有兴趣的读者可以自行翻阅。

还有许多其它的特殊例子,限于篇幅这里就不一一描述了。不过,大体的替换思路是:在源码中全局搜索→在版本文件夹/config/the_vault下全局搜索→使用VP2.14尝试替换→使用VP2.10尝试替换。通过这四个步骤,几乎可以解决VH整合包的全部文本。

反编译汉化阶段

此方案大概依然由KlparetlR提出。他的出发点是由于硬编码的替换文件总计文本量过大(对于整个VH整合包来说大概有10000多行),撰写和维护的时间成本都较高,因此在寻找一种直接编辑mod源码的形式,对那些单个class文件中有大批量需要汉化的字符串的部分(例如存储全部装备模型名称的文件,单个文件内大概有接近100字符串)尝试直接修改mod源码为中文,从而简化后续的维护工作。

通常而言class文件不能直接编辑。class文件也可以称为字节码文件,是经过Java IDE编译后提交给机器运行的文件,因此其语法在设计上完全没有考虑可读性,且结构相当混乱。当然,前面有提到过VP2.10以后的版本均使用了ASM框架,其中文译名为Java字节码操控框架,实际上就是一种直接编辑这里的class文件的一种方式,也就是实现了硬编码汉化。

这里使用的工具为Recaf,是Col-E开发的一种java字节码编辑器。其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吗?没错,本质也是一种字节码编辑器。只是其应用范围相当狭窄,根据我的使用经验来看,大概只能成功打开只含有单个class文件,且构建时不需要额外依赖库的jar包。巧的是,前面提到的词条替换mod刚好满足这两个条件,所以我可以对mod源码进行编辑。然而,对于绝大多数mod文件来说,不可能只有一个class文件,而且稍复杂一些的mod都会有自己的依赖库,显然不能利用jbe进行编辑。例如我使用jbe打开任意vault mod内部的class文件时,jbe会报错

因此jbe在后期已经被我抛弃了。

Recaf支持直接打开整个jar包并浏览其中的源码,以及通过字节码编辑的方式对其内容进行修改。

首先讲一件趣事。Recaf目前的最新正式版版本为2.21.13,是基于Java开发的程序。我在下载并尝试打开时,由于vault mod是在Java 17环境下开发的,因此我必须使用Java 17打开Recaf,才能正常对vault mod的源码进行编辑。然而我的电脑的Java 17环境有些问题(至少当时我是这么认为的),导致我下载的Recaf只能使用Java 8打开,自然也就不能进行任何反编译的操作。当时想尽了各种办法,包括完全重新安装和配置Java,恢复系统,换另一台电脑,甚至是让KlparetlR帮我远程控制,最终也没能解决这个问题。

直到几天前的8月7日,我试着下载了Recaf的3.0测试版。然后,莫名其妙就顺利打开了当时我整个人就很无语。

不过这个时期我的汉化已经没有需要补充的东西了。我开始尝试反编译汉化的主要目的是考虑到VP2.10所带来的性能影响问题,于是希望可以尽可能减少2.10所负责的文本量。当时2.10替换文件中的一半内容(大概有300行)是用来汉化装备词条,也就是专栏开头提到的词条mod所负责的部分。这部分会同时显示在宝库装备的tooltip和属性统计页面右侧

tooltip部分属性统计页面

在源码中的表示方式为

看起来这种形式可以使用VP2.14进行替换。然而实际测试时发现并不起作用。KlparetlR说不生效的原因可能是因为这些名称的注册在定义的后面,也就是同一class文件的后半部分

的这些registry.register(XXX),导致2.14的匹配失效。当时的解决方案是改用2.10进行替换。大部分词条都顺利替换,但是由于在装备tooltip处的显示必须使用半匹配进行替换,会导致类似"Damage"和"Undead Damage"这样的重复词条问题(二次替换咯),以及"Mana"会同时替换到植魔mod中的"Mana"的问题,以及"Cooldown Reduction"完全不替换的神秘问题等等。另外,由于宝库装备是VH中几乎是最经常显示tooltip的部分,这样用2.10的频繁替换带来的性能影响不得不考虑。于是,在VP2.14不能用的情况下,我只好考虑反编译汉化的可行性。

使用Recaf修改字节码时有诸多限制,所以反编译汉化所能操作的代码部分相当有限其实就是碰运气。总的来说,能成功打开字节码编辑窗口的就代表着可以修改其中的内容

词条源字符串所在的字节码文件

由于字节码本身结构相当混乱,而且突出一个整体性,因此更改其中的语句逻辑或是增删语句之类的操作完全不现实。好在绝大部分字符串内容都是原封不动地从java源码编译进字节码中,编译后被冠以"ldc"或"ldc_w"前缀。实际在编辑字节码时只需要关注这一部分并进行适当修改就可以。

上述字节码文件中的中文不需要转码存储。修改好后,需要在Recaf内重新导出jar文件并替换原有的mod。然后打开游戏测试,完全没有问题。于是VP2.10的替换文本量最终被我缩减到了约350行,已经是十分令我满意的结果了。

后来我发现终端内按名称排序的掉帧问题只与是否加载VP2.10有关,而与实际的替换文本数量关系不大,悲

另外,字节码编辑方式实质上是修改了mod的源码内容,这对于某些mod来说可能违反了相关的分发协议,从而不能随意公开发布。如果是我个人使用的汉化版本就没有问题,不过我也没有了解过VM汉化组那边最新的汉化补丁有没有使用反编译汉化的形式(

一些汉化过程中的顽固分子

技能(Ability)名称

技能名称的字符串内容存储在版本文件夹/config/the_vault/abilities.json中的name字段中(不是id)。

这是汉化后的版本,实际是以unicode编码存储,这里为了显示方便转换为汉字,下同

起初我在修改这部分内容时,发现不会导致游戏报错但是不能替换。我也检查过相关的源码,确认过字符串的读取来源确实是这里。后来KlparetlR提到,VH会将技能名称的字符串写入到存档数据,也就是版本文件夹/saves/存档文件夹/data/the_vault_PlayerAbilities.dat中(据说是方便版本更新的检查),而dat文件并不能通过常规方法修改其中的特定字符串。因此实际上在配置文件中对技能名称的更改仅对于新存档生效。若是想在已有存档中汉化这部分内容,还是老老实实用VP2.10写对应的替换字符串吧。

后记:似乎Update 11对相关的存储机制做了更新,现在直接改配置文件应该也可以实时反馈到存档中。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3