前情提要
在上一篇教程中,我们了解了如何从零开始分析一个数据包
至此,一些必要的铺垫都已经准备完毕了
今天,我们正式开始实战——对Inventory的标题栏进行国际化
不过在这之前,我们还要先解决一下上次教程留下的一点小尾巴
作业答案
在wiki.vg上找到与ProtocolLib中
PacketType.Play.Server.OPEN_WINDOW
对应的数据包的名字,我们在下一篇教程中会用到它
这就是上次作业的内容
对于作业的答案,我相信一些读者应该是胸有成竹了
不过请容许我再啰嗦一下过程(我以构建版本#606为例,使用5.0.0-SNAPSHOT的可以进行类比参照,结果是一样的):
-
在
PacketType.class
中找到PacketType.Play.Server.OPEN_WINDOW
的定义OPEN_WINDOW = new PacketType(PacketType.Play.PROTOCOL, SENDER, 45, new String[]{"OpenWindow", "SPacketOpenWindow"});
可以看到其Packet ID为45,或者说0x2D
-
在wiki.vg中的Clientbound部分找到Packet ID为0x2D的数据包
没错,在wiki.vg上与ProtocolLib中PacketType.Play.Server.OPEN_WINDOW
对应的数据包就是Open Screen
分析Open Screen数据包
这个数据包的结构比较简单,有且只包含三部分数据
数据名称 | 数据类型 | 说明 |
---|---|---|
Window ID | VarInt | 窗体的ID(和上一篇教程中提到的Entity ID类似,都像一个“句柄”) |
Window Type | VarInt | 窗体的类型 |
Window Title | Chat | 窗体的标题 |
但是我们最关心的是最后一个Window Title,因为它正好包含了我们需要的Inventory标题
但是我们如何在ProtocolLib中获取这一项的值呢?
上代码!
读取数据包
又到了我们久违的代码时间了
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.OPEN_WINDOW) {
@Override
public void onPacketSending(PacketEvent event) {
event.getPlayer().sendMessage("OPEN_WINDOW");
}
}
);
上面展示的,是我们在第一篇教程中使用过的代码。我们将在它的基础上进行修改
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.OPEN_WINDOW) {
@Override
public void onPacketSending(PacketEvent event) {
event.getPlayer().sendMessage(
String.valueOf(event.getPacket().getChatComponents().read(0))
);
}
}
);
改动的地方不多,就是把"OPEN_WINDOW"
换成了String.valueOf(event.getPacket().getChatComponents().read(0))
接下来我们来仔细看看:
其实,核心部分就是event.getPacket().getChatComponents().read(0)
evevt.getPacket()
就是获取我们的数据包
getChatComponents()
方法返回的是StructureModifier<WrappedChatComponent>
,它包含了这个数据包中所有类型为WrappedChatComponent
(或者说ChatComponent类型,不在此深究,大家明白就好)的数据
read(0)
表示我们要读取第1个(索引为0)数据
那么在这个数据包中,第1个类型为WrappedChatComponent
的数据自然就是我们关心的Window Title了
让我们来看看实战的效果
可以看到,随着我们依次打开了工作台、箱子、投掷器三个Inventory,我们也依次收到了三条消息:
WrappedChatComponent[json={"translate":"container.crafting"}]
WrappedChatComponent[json={"translate":"container.chest"}]
WrappedChatComponent[json={"translate":"container.dropper"}]
虽然第一眼看上去可能一头雾水,并不是我们想要的“合成”、“箱子”、“投掷器”
但细细一琢磨,这"container.crafting", "container.chest", "container.dropper",结合前面的"translate",好像还是有点关系的
究竟是什么关系呢?
String.valueOf()迷思
我们前面实际上调用的是String.valueOf()
,或者说toString()
方法,返回的与其说是我们想要的标题栏文本,更不如说是这个对象的文字描述
好奇宝宝们或许已经发现了,在event.getPacket().getChatComponents().read(0)
后面多打一个".",触发IDE的自动补全,就能看到一个getJson()的补全项
看起来好像有点东西,不如运行试试
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.OPEN_WINDOW) {
@Override
public void onPacketSending(PacketEvent event) {
event.getPlayer().sendMessage(
event.getPacket().getChatComponents().read(0).getJson()
);
}
}
);
不出意外,我们收到了三个标准的JSON格式的消息:
"translate":"container.crafting"
"translate":"container.chest"
"translate":"container.dropper"
至此,我估计大家都知道我们要从这里入手了
有些老鸟这时坐不住了:“我看到WrappedChatComponent就有话要说了。”
没错,有经验的开发者看到JSON,又看到WrappedChatComponent和"translate",很难不联想到Spigot的ChatComponent API,以及“原始JSON文本”
WrappedChatComponent, ChatComponent API与“原始JSON文本”
要理清这三者,我还得倒过来说
先说说“原始JSON文本”吧
从某种意义上来说,这算是个专业术语了。如果要详细了解,可以到Fandom Minecraft Wiki上看看
我简单说一下,所谓“原始JSON文本”,就是用JSON描述的一段带格式的文本
相信大家在玩一些服务器时都见过一些花里胡哨的文字
这样的效果就是通过“原始JSON文本”来描述的
而ChatComponent API是Spigot提供的一套API,用以辅助我们实现如上的效果。不过与我们这篇教程无关,所以不涉及
WrappedChatComponent和ChatComponent API作用类似,只不过是ProtocolLib提供的,我们下面会稍加涉猎
我们前面看到的"translate":"container.crafting"
中的translate
,就是属于“翻译标识符”
它告诉客户端“去到语言文件里找到与container.crafting
对应的值,然后拿过来放到这里”
正是通过这样的方法,Minecraft实现了国际化翻译
我们找找Minecraft的简体中文语言文件zh_cn.json(以前的版本是.lang格式的),看看里面与container.crafting
对应的值是什么
没错,就是我们想要的“合成”
那么剩下的"translate":"container.chest"
和"translate":"container.dropper"
也就不言而喻了
谜团解开
修改数据包
既然我们知道了类似"translate":"container.crafting"
这样的字符串的意思,接下来就要偷天换日了
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.OPEN_WINDOW) {
@Override
public void onPacketSending(PacketEvent event) {
event.getPacket().getChatComponents().write(
0,
WrappedChatComponent.fromLegacyText("This is the title of the container")
);
}
}
);
和读取数据包类似,只不过把read()
换成了write()
。读到的是WrappedChatComponent
,写入的自然也是WrappedChatComponent
用人话来说,就是往类型为WrappedChatComponent
的第1个(索引为0)数据处写入WrappedChatComponent.fromLegacyText("This is the title of the container")
废话不多说,直接看效果
由于我们没有加入任何的条件判断,因此所有Inventory的标题栏都被修改为了"This is the title of the container"
虽然看起来有点笨笨的,不过这是我们成功的第一步,不是吗?
我怀疑你在搞颜色
原本深灰色的文字颜色未免显得有些单调了,我们不妨也来点花里胡哨的
这时候,我们就要请出格式化代码来助我们一臂之力了
当然,看到上面的超链接,就知道我又甩了一篇Wiki。按照惯例,想要详细了解的,可以去Wiki上看看
下面我来简单地说一说
所谓的格式化代码,就是由“§”(分节符)作为前缀,加上一个字符,共两个字符所组成的,用于表示特定格式的代码
用“§”开头来表示颜色的场合,相信大家多多少少也都听过或见过
我就把各个格式化代码的简要信息列出来,这个概念就过了
格式化代码 | 说明 |
---|---|
§0 | 黑色 |
§1 | 深蓝色 |
§2 | 深绿色 |
§3 | 湖蓝色 |
§4 | 深红色 |
§5 | 紫色 |
§6 | 金色 |
§7 | 灰色 |
§8 | 深灰色 |
§9 | 蓝色 |
§a | 绿色 |
§b | 天蓝色 |
§c | 红色 |
§d | 粉红色 |
§e | 黄色 |
§f | 白色 |
§g | 硬币金(仅限基岩版) |
§k | 随机字符(与原有字符的宽度相同) |
§l | 粗体 |
§m | |
§n | 下划线(仅限Java版) |
§o | 斜体 |
§r | 重置文字样式 |
OK,接下来,从理论到实践
我想要显示“xxx is saying hello”的效果。其中,"xxx"代表3个蓝色的随机字符,"is saying"是正常颜色的,"hello"是深绿色的
就应当如下书写:§9§kxxx §ris saying §2hello
稍微要注意一下的就是,当前后的格式冲突时,用§r重置一下样式,然后再继续书写新的格式
把上面这段套到之前的fromLegacyText()
方法中试一试
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.OPEN_WINDOW) {
@Override
public void onPacketSending(PacketEvent event) {
event.getPacket().getChatComponents().write(
0,
WrappedChatComponent.fromLegacyText("§9§kxxx §ris saying §2hello")
);
}
}
);
看看效果
简直完美
琐事三两件:Window Type
上面是Open Screen这个数据包的结构图,大家在之前都已经看过了
对于Window Title,我们已经门清了。那么我们现在要聊的是什么呢,还有什么问题吗?
那就是Window Type
有些人不以为意:不就是个VarInt吗,有啥了不起的,我用event.getPacket().getIntegers().read(1)
读一下,然后到对照表中查一下,不就就搞定了?
有经验的读者都会心一笑,既然我把它拎出来单独说说,当然是有坑的
抱着不撞南墙不回头的精神,我们就用event.getPacket().getIntegers().read(1)
来试一试
我擦,怎么有异常。你这ProtocolLib不讲武德啊,明明wiki.vg上写着有2个VarInt,我怎么一read(1)
你就告诉我总共只有1个Integer,良心属实大大的坏了
难道是ProtocolLib的问题?
当然也不是
不知道大家有没有听说过序列化和反序列化的概念
简单来说,要是我有一个Student
类,包含了一个学生的所有相关信息。现在,我要把这个存到数据库里
我总不可能直接让数据库真的保存Student
的对象,可能数据库根本不知道那是啥玩意儿
所以,我们就需要制定一种数据交换方式,然后通过这个交换方式来存入、读取数据,且这个数据交换方式必须双方都认识
最典型的就是JSON,大家都是认识的
结合上面的例子,如果使用JSON作为中介,那么存入数据的时候我就需要把Student
类“序列化”为JSON格式的字符串,然后送到数据库中;相反的,读取数据的时候,需要把JSON格式的字符串“反序列化”为Student
类
但是,对于Minecraft这样客户端和服务端的通信过程相对封闭的体系来说,就没必要用JSON了。因为JSON的易读性使其丧失了部分的传输效率
并且对于游戏来说,效率也是很重要的事。反正都是自己人,又不是给别人看的,怎么快怎么来
但是作为开发人员,直接拿0,1,2这样的“魔法值”去判断Inventory类型,效率高是高,简单是简单,但实在是太不优雅了,严重点说是犯忌的。既然Inventory类型就那几个,比较理想的就是用Enum来表示(当然这是理想情况,用public final class
也不是不行)
既要保证代码的可读性、容错性高,又要保证传输的效率。大家自然而然想到的方案就是:传递的时候把Enum转成int,接收的时候把int转成Enum
于是问题就产生了
wiki.vg作为Minecraft通信协议的百科全书,它注重的是“过程”,所以它把Window Type的类型标注成VarInt,说明Window Type在网络传输过程中是以VarInt的形式存在的
而ProtocolLib作为服务端插件,它更关注于“结果”。所以我们用ProtocolLib读取数据包的时候就不应当使用getIntegers()
方法了
那么我们应该用什么呢
这里就要提到另一个操蛋之处了。不像ChatTypes这种可以直接使用getChatTypes()
的ProtocolLib已经定义好的方法,我们需要自己手动去读取
这时候,我们需要用到getModifier()
方法
这个方法就像一个大熔炉,或者说像是泛型里的?
,任何类型的东西都可以用它来读取到
用了这个方法,我们就不用考虑它是什么类型的第几个数据了,直接看它在整个数据包中的顺序就行了
比如说,我们之前的Window Title,在整个数据包中它是第3条数据,所以我们使用getModifier().read(2)
可以读取到
当然,我并不是说大家一股脑儿都去用getModifier()
算了,因为getModifier().read()
返回的是Object,到头来你还是得把它转换成目标类型才能做一些操作
使用getModifier()
是一种下下策,或者说是fallback,是迫不得已的。如果ProtocolLib有提供相应的更方便的方法,请务必别用getModifier()
言归正传,回到Window Type。由于它在整个数据包中是第2条数据,所以我们使用getModifier().read(1)
进行读取
先直接套个String.valueOf()看看结果:
看来是类名+地址,没重写toString()
不过这个信息够我们判断一些东西了
我们可以看到,我们读取到的是net.minecraft.world.inventory.Containers
的一个对象
不妨去看看这个类
可是,怎么找不到呢?
没错,头疼的事情又来了,因为我们需要用到NMS
没办法,为了科学献身一把,到pom.xml里面把<artifactId>spigot-api</artifactId>
改成<artifactId>spigot</artifactId>
,刷新
然后进到net.minecraft.world.inventory.Containers
看看有啥东西
public class Containers<T extends Container> {
public static final Containers<ContainerChest> a = a("generic_9x1", ContainerChest::a);
public static final Containers<ContainerChest> b = a("generic_9x2", ContainerChest::b);
public static final Containers<ContainerChest> c = a("generic_9x3", ContainerChest::c);
public static final Containers<ContainerChest> d = a("generic_9x4", ContainerChest::d);
public static final Containers<ContainerChest> e = a("generic_9x5", ContainerChest::e);
public static final Containers<ContainerChest> f = a("generic_9x6", ContainerChest::f);
public static final Containers<ContainerDispenser> g = a("generic_3x3", ContainerDispenser::new);
public static final Containers<ContainerAnvil> h = a("anvil", ContainerAnvil::new);
public static final Containers<ContainerBeacon> i = a("beacon", ContainerBeacon::new);
public static final Containers<ContainerBlastFurnace> j = a("blast_furnace", ContainerBlastFurnace::new);
public static final Containers<ContainerBrewingStand> k = a("brewing_stand", ContainerBrewingStand::new);
public static final Containers<ContainerWorkbench> l = a("crafting", ContainerWorkbench::new);
public static final Containers<ContainerEnchantTable> m = a("enchantment", ContainerEnchantTable::new);
public static final Containers<ContainerFurnaceFurnace> n = a("furnace", ContainerFurnaceFurnace::new);
public static final Containers<ContainerGrindstone> o = a("grindstone", ContainerGrindstone::new);
public static final Containers<ContainerHopper> p = a("hopper", ContainerHopper::new);
public static final Containers<ContainerLectern> q = a("lectern", (i, playerinventory) -> {
return new ContainerLectern(i, playerinventory);
});
public static final Containers<ContainerLoom> r = a("loom", ContainerLoom::new);
public static final Containers<ContainerMerchant> s = a("merchant", ContainerMerchant::new);
public static final Containers<ContainerShulkerBox> t = a("shulker_box", ContainerShulkerBox::new);
public static final Containers<ContainerSmithing> u = a("smithing", ContainerSmithing::new);
public static final Containers<ContainerSmoker> v = a("smoker", ContainerSmoker::new);
public static final Containers<ContainerCartography> w = a("cartography_table", ContainerCartography::new);
public static final Containers<ContainerStonecutter> x = a("stonecutter", ContainerStonecutter::new);
private final Supplier<T> y;
private static <T extends Container> Containers<T> a(String s, Supplier<T> containers_supplier) {
return (Containers)IRegistry.a(IRegistry.ah, s, new Containers(containers_supplier));
}
private Containers(Supplier<T> containers_supplier) {
this.y = containers_supplier;
}
public T a(int i, PlayerInventory playerinventory) {
return this.y.create(i, playerinventory);
}
private interface Supplier<T extends Container> {
T create(int var1, PlayerInventory var2);
}
}
简直是难以忍受,不过我们还是看到了点有用的东西
public static final Containers<ContainerChest> a = a("generic_9x1", ContainerChest::a);
public static final Containers<ContainerChest> b = a("generic_9x2", ContainerChest::b);
public static final Containers<ContainerChest> c = a("generic_9x3", ContainerChest::c);
public static final Containers<ContainerChest> d = a("generic_9x4", ContainerChest::d);
public static final Containers<ContainerChest> e = a("generic_9x5", ContainerChest::e);
public static final Containers<ContainerChest> f = a("generic_9x6", ContainerChest::f);
public static final Containers<ContainerDispenser> g = a("generic_3x3", ContainerDispenser::new);
public static final Containers<ContainerAnvil> h = a("anvil", ContainerAnvil::new);
public static final Containers<ContainerBeacon> i = a("beacon", ContainerBeacon::new);
public static final Containers<ContainerBlastFurnace> j = a("blast_furnace", ContainerBlastFurnace::new);
public static final Containers<ContainerBrewingStand> k = a("brewing_stand", ContainerBrewingStand::new);
public static final Containers<ContainerWorkbench> l = a("crafting", ContainerWorkbench::new);
public static final Containers<ContainerEnchantTable> m = a("enchantment", ContainerEnchantTable::new);
public static final Containers<ContainerFurnaceFurnace> n = a("furnace", ContainerFurnaceFurnace::new);
public static final Containers<ContainerGrindstone> o = a("grindstone", ContainerGrindstone::new);
public static final Containers<ContainerHopper> p = a("hopper", ContainerHopper::new);
public static final Containers<ContainerLectern> q = a("lectern", (i, playerinventory) -> {
return new ContainerLectern(i, playerinventory);
});
public static final Containers<ContainerLoom> r = a("loom", ContainerLoom::new);
public static final Containers<ContainerMerchant> s = a("merchant", ContainerMerchant::new);
public static final Containers<ContainerShulkerBox> t = a("shulker_box", ContainerShulkerBox::new);
public static final Containers<ContainerSmithing> u = a("smithing", ContainerSmithing::new);
public static final Containers<ContainerSmoker> v = a("smoker", ContainerSmoker::new);
public static final Containers<ContainerCartography> w = a("cartography_table", ContainerCartography::new);
public static final Containers<ContainerStonecutter> x = a("stonecutter", ContainerStonecutter::new);
这些全部用public static final
声明的常量,好像有点搞头
不妨拿来试一试
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(this, PacketType.Play.Server.OPEN_WINDOW) {
@Override
public void onPacketSending(PacketEvent event) {
event.getPlayer().sendMessage(
String.valueOf(event.getPacket().getModifier().read(1).equals(Containers.l))
);
}
}
);
我们测试的目标是工作台
在Containers
类中,我们可以看到工作台对对应的应该是l
public static final Containers<ContainerWorkbench> l = a("crafting", ContainerWorkbench::new);
所以我们在getModifier().read(1)
后直接用equals()
比较一下
理想状态下,这段代码的效果是:只有当玩家打开工作台才会收到true,反之都是false
抱着忐忑的心情,我们来实战一下
努力没有白费,但只能算是惨胜
由于使用了NMS,算是破了戒,因此这种方法不推荐使用
但有没有稍微好一点的办法呢?
你可能会想到,既然工作台的Window Title是"translate":"container.crafting"
,箱子的Window Title是"translate":"container.chest"
,投掷器的Window Title是"translate":"container.dropper"
,那我们难道不能用Window Title来判断吗?
当然可以,这也确实比用NMS更优雅,但还是有一个小小的问题:
如果其他的插件也用了ProtocolLib,也监听了PacketType.Play.Server.OPEN_WINDOW
,也修改了Window Title,那岂不是有大问题?
虽然这个情况挺极端的,但我们既然考虑到了,不想办法克服,心里总是不好过的
于是,有请第三种方法
我们想,既然我们能用event.getPlayer()
获取到玩家,我们能不能从此着手呢?
Player
类下面有一个getOpenInventory()
方法,它返回的是InventoryView
(Inventory视图),说白了就是玩家的Inventory视野
由于Inventory是上下分层的,并且容器的Inventory是在上方的(比如说箱子,上方的Inventory是箱子的,下方的Inventory是玩家的),所以我们应该使用InventoryView
下面的getTopInventory()
方法获取容器的Inventory
既然这是Spigot API下的内容,相信大家都应该很熟悉了
如果要判断类型,直接getType()
就好了,甚至你还可以做一些其他的操作
比如说判断工作台,就是event.getPlayer().getOpenInventory().getTopInventory().getType() == InventoryType.CRAFTING
千算万算没想到,最后还是Spigot API救了我们一命,这也是我能想到的最优雅的方法
美中不足的是,如果你要通过ProtocolLib发PacketType.Play.Server.OPEN_WINDOW
包,还是得用到NMS
不过由于我们系列的教程用不到发包,我就偷个小懒。如果大家有兴趣,自己可以去研究研究
琐事三两件:WrappedChatComponent
接下来我们要聊的没有上面那么沉重,WrappedChatComponent
WrappedChatComponent下面有好几个方法,最常用三个的应该就是fromLegacyText()
, fromJson()
, fromText()
主要说说哥仨的区别
fromLegacyText()
顾名思义,是为了向前兼容的产物
只支持使用“§”(分节符)设置文字颜色、文字样式的基本功能
由于这次我们只需要修改文字的颜色和样式,所以fromLegacyText()
就够用了
fromJson()
运用了“原始JSON文本”
除了支持LegacyText使用“§”(分节符)设置文字颜色、文字样式的方法,还支持使用JSON格式设置文字颜色、文字样式,以及translate, clickEvent, hoverEvent, keybind等诸多高级玩法,是现在Minecraft普遍使用的格式
fromText()
底层调用的还是fromJson()
方法
如果只涉及到文本,不涉及“原始JSON文本”的高级玩法,可以用这个
结语
文本详细介绍了Open Screen数据包,读取、修改数据包的方法,WrappedChatComponent的用法,以及从数据包和WrappedChatComponent所延申出来的诸多概念和内容
这一篇教程涉及的知识不仅广,而且杂。事后可以对教程所涉及的概念多加了解,并且多加运用,熟能生巧
至此,你应该掌握了使用ProtocolLib读取、修改数据包的方法
所以接下来的教程中,对于读取、修改数据包部分,我将不再赘述。也正因此,将会容易很多
下一篇教程,我们来看一看“聊天内容”的国际化
相关链接
Fandom Wiki中对于“原始JSON文本”的介绍:https://minecraft.fandom.com/zh/wiki/%E5%8E%9F%E5%A7%8BJSON%E6%96%87%E6%9C%AC%E6%A0%BC%E5%BC%8F?variant=zh
Fandom Wiki中对于“格式化代码”的介绍:https://minecraft.fandom.com/zh/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81
wiki.vg上的 Inventory类型<->数值 对照表:https://wiki.vg/Inventory
This was exactly what I was looking for, thank you!