教程/制作数据包/制作数据包的两个实例
阅读
2020-12-05更新
最新编辑:我是绵羊Yang_g
阅读:
更新日期:2020-12-05
最新编辑:我是绵羊Yang_g
此教程介绍两个Java版数据包制作的实例,阅读此教程前请先确保自己掌握教程/制作数据包和教程/制作资源包的知识。了解命令(尤其是扁平化后的命令/execute、命令/data)、记分板、原始JSON文本等页面也会有所帮助。
实例:蜜蜂助手
目标
动手制作一个数据包前,你应该有一个明确的目标。也许你已经阅读过很多命令教程,但是却不知道如何有机地整合这些知识。在本实例中,我们将制作一个数据包,使得玩家能够得知他视线所指的蜂巢内有多少只成年蜜蜂,有多少只幼年蜜蜂。
本实例的GitHub地址:点击这里
开始
让我们来分解一下需求吧。
“使得玩家能够得知他视线所指的蜂巢内有多少只成年蜜蜂,有多少只幼年蜜蜂。”
“视线所指”
注:如果你已经知道如何使用命令制作“视线追踪”“视线投射”,你可以使用自己的方式实现。
首先是视线所指。在17w50a中,Minecraft加入了局部坐标这一概念。局部坐标(玩家坐标系,或者摄像机坐标系)是一个以玩家头部为原点,玩家的视线(注意,和世界坐标系并不一定对齐)为“前轴”(局部坐标的第三个参数),三维中两个垂直于前轴,正方向分别指向玩家头部的左方和上方的轴为“左轴”和“上轴”(局部坐标的第一、二个参数)的空间系。举例来说,^ ^ ^1
就是玩家视线前方一个方块长度的位置。如果蜂巢就在玩家视线前方一个方块长度的位置,那么执行命令/data get block ^ ^ ^1
就可以得到这个蜂巢的数据了。但由于我们并不知道蜂巢到玩家头部的距离,因此我们不能直接写一个固定的坐标,而是需要以某种方式得到蜂巢的位置。
“多少”
通常情况下,一只蜂巢中至多只有3只蜜蜂。因此,在这种小数量级的情况下,我们可以进行穷举。
我们先看看蜂巢的方块实体数据。
观察到,蜂巢内的蜜蜂数据都保存在一个Bees
列表里面。因此,结合NBT路径的知识,我们在判定时只需要读取Bees[0]
、Bees[1]
、Bees[2]
就可以了。
“成年”“幼年”
我们再来看看蜜蜂的实体数据。由上,我们可以得知Bees
列表内的每一项都是一个蜜蜂的实体数据,因此直接在上述的NBT路径之后接上EntityData
(即Bees[n].EntityData
)再直接接上蜜蜂对应的NBT路径就可以了。
我们可以发现,蜜蜂拥有一个Age
标签。实际上,这个标签决定了这只蜜蜂是成年还是幼年,并且大于等于0为成年,小于0为幼年。
分析与小结
- 需要在数据包中实现视线投射。可以参考MCBBS上K_Bai的“超精准的射线追踪碰撞检测器”和MCBBS上Sethbling原版摄像机的解析。
- 由于蜂巢中最多三只蜜蜂,所以直接穷举能完成工作。对于制作数据包来说,穷举有时候也是唯一的方法。
- 要结合游戏内各个实体的NBT数据实现自己的功能。
编写与实现
初始化
注:以下文字中,#<命名空间>:<名字>
表示<命名空间>下的<名字>标签,<命名空间>:<名字>
函数表示<命名空间>下的<名字>函数,文件名对应data/<命名空间>/<名字>.mcfunction
。
新建一个文件夹,填写好pack.mcmeta
,建立data
文件夹。在data
文件夹下建立beeutility
(本实例就以此为主要的命名空间)和minecraft/tags/functions
(一些函数需要被每刻执行和在地图加载时执行)文件夹。
类似于编程中变量的概念,我们需要记分项储存成年蜜蜂的数量、幼年蜜蜂的数量、总的蜜蜂的数量和蜜蜂的年龄,以便之后用命令处理和显示出来。
在命名记分项的时候,为了避免和其他数据包冲突,我们通常在命名一个记分项的时候在前面会一个前缀。通常这个前缀是该数据包英文名的简写,本实例则以bu_
为例。
建立data/beeutility/functions
,建立load.mcfunction
函数,内容如下:
scoreboard objectives add bu_beeadult dummy scoreboard objectives add bu_beebaby dummy scoreboard objectives add bu_beeamount dummy scoreboard objectives add bu_beeage dummy
回顾一下/scoreboard
的语法。
scoreboard objectives add <记分项> <准则> [<显示名称>]
其中,这些记分项都不会直接以记分板的形式显示出来,所以没必要设置显示名称。准则设置为dummy
,使得该记分项只能被命令改变。
同样,你也可以建立一个unload.mcfunction
,在该函数中删除以上四个记分项。
建立data/minecraft/tags/functions/load.json
,内容如下:
{
"replace": false,
"values": [
"beeutility:load"
]
}
这样,将beeutility:load
函数设定为地图载入时执行。格式参见标签,下文不再赘述。
视线投射
现在,我们做“获取玩家所指的蜂巢”。由上,我们可以像下面这样一步一步尝试得出那个可能存在的蜂巢的坐标。
在beeutility
命名空间下,建立tick.mcfunction
和ray.mcfunction
函数。
tick.mcfunction
execute as @a at @s anchored eyes run function beeutility:ray
ray.mcfunction
execute if entity @s[distance=..4] if block ~ ~ ~ #minecraft:beehives run function beeutility:query execute if entity @s[distance=..4] unless block ~ ~ ~ #minecraft:beehives positioned ^ ^ ^0.005 run function beeutility:ray
其中beeutility:query
为接下来要建立的查询函数。
可以发现,beeutility:ray
函数在特定条件下会反复执行自己,也就是递归。这样这个函数在被执行的1刻内就会执行完毕,不需要担心效率问题。
将beeutility:tick
函数加入#minecraft:tick
,使得其每刻执行。beeutility:tick
中,/execute
先指定执行者为所有活着的玩家(使用更多目标选择器|语法来进一步筛选),将执行位置设定为选中的玩家,然后运用anchored
子命令,将执行位置对齐到玩家头部朝向,再运行beeutility:ray
函数。
在beeutility:ray
中,首先判断命令执行位置与执行实体(也就是一位选中的玩家)的距离是否小于4个方块长度。若是,则继续判断执行位置的方块是否在#minecraft:beehives
中,即这个方块是不是一个蜂巢。若是,那么执行beeutility:query
函数。以上过程有一个条件不满足就会跳过,执行下一条命令。假设这是游戏第一次执行这个函数,那么由于执行位置就是玩家的头部(通常不会与蜂巢重合),所以游戏不会执行查询。然后,依然判断命令执行位置与执行实体(还是那位选中的玩家)的距离是否小于4个方块长度。若是,那么判断执行位置的方块是否不满足在#minecraft:beehives
中(unless
可以理解为if not
)。若不满足,那么将执行位置向玩家视线方向移动0.005个方块长度,执行beeutility:ray
函数本身。
与一般编程语言不同的是,命令并没有提供非常方便的else
,因此我们需要重复验证很多条件。实际思路可能更类似于以下情况:
ray.mcfunction
- 如果执行位置到玩家头部的距离小于等于4
- 如果是(then)
- 如果执行位置所在的方块是一个蜂巢
- 如果是(then)
- 查询这个蜂巢
- 否则(else)
- 将执行位置向玩家视线方向移动0.005个方块长度,再次执行自己
- 如果是(then)
- 如果执行位置所在的方块是一个蜂巢
- 否则(else)
- 执行完毕
- 如果是(then)
将这个类流程图与实际的代码对比一下,看看哪些是因为命令的原因而被拆开的,可能有助于理解。
说了这么多,可能还是有人不理解这个函数在干什么,是吗?
想象一下,将玩家视线想象为一根实在的光滑杆子(杆子不与除下述小球以外的物体发生碰撞),将这根杆子截取为4个方块长度长,在杆子的末端有一个小挡板。在杆子的起点(玩家的眼睛)处套上一个小球,这个小球只会与蜂巢和杆子发生碰撞,然后让球沿杆向前运动。小球会不停往前运动,直到它在途中遇到了一个蜂巢(此时就会在这个位置执行beeutility:query
函数),或者它到达了杆子的最末端的挡板处(此时整个beeutility:ray
函数执行完毕)。
与一般的物理学运动不同的是,这个小球在移动时不是连续的而已(类似于以这个小球为执行实体,以玩家视线方向为小球视线方向,每次移动就是在执行/tp ^ ^ ^0.005
)。
这样,我们就实现了一个简单的“视线投射”,也就是获取玩家所看方块(本实例则是蜂巢)的坐标。
有人可能会问,如果碰不到蜂巢会怎么样呢?碰不到的意思就是玩家没有看着蜂巢,自然也就不存在要获取的蜂巢了。也许还会有人问0.005格是不是太慢了,见上,1刻内会把以上递归操作全部执行完毕,所以实际的平均速度非常快。
查询与显示
- 查询
在beeutility
命名空间下,建立beeutility:query
函数。
execute store result score @s bu_beeamount run data get block ~ ~ ~ Bees
这条命令将执行结果储存到玩家的记分项bu_beeamount
中,而执行的实际命令是获取执行位置方块数据中的Bees
列表。该命令的执行结果为该列表的长度,即总共有多少只蜜蜂。
#region query "Bee 1"'s age execute store result score @s bu_beeage if data block ~ ~ ~ Bees[0].EntityData.Age run data get block ~ ~ ~ Bees[0].EntityData.Age execute if data block ~ ~ ~ Bees[0].EntityData.Age if score @s bu_beeage matches 0.. run scoreboard players add @s bu_beeadult 1 execute if data block ~ ~ ~ Bees[0].EntityData.Age if score @s bu_beeage matches ..-1 run scoreboard players add @s bu_beebaby 1 #endregion
以上代码块重复三次,剩下两次分别将Bees[0]
替换为Bees[1]
和Bees[2]
即可。
以下将store
子命令放在每个分析过程的最后。
第一条命令,先判断执行位置的方块(蜂巢)数据中的第一只蜜蜂是否存在Age
标签,即判断是否存在第一只蜜蜂。若有,执行命令,获取第一只蜜蜂的Age
标签,该命令的执行结果即为这个标签的值,将结果储存到玩家的记分项bu_beeage
中。若无,则中断。
第二条命令,判断是否存在第一只蜜蜂,若有,判断玩家记分项bu_beeage
(也就是获取到的Age
)是否大于等于0,若是,则将玩家的记分项bu_beeadult
加1。
第三条命令,判断是否存在第一只蜜蜂,若有,判断玩家记分项bu_beeage
是否小于等于-1(即小于0),若是,则将玩家的记分项bu_beebaby
加1。
于是我们达成了目标——知道有多少只成年蜜蜂,有多少只幼年蜜蜂。现在要做的就是把结果显示出来了。
- 显示
这样一来,显示就简单很多了——我们有/title
,有/tellraw
,也许还可以用/bossbar
。此处以/title <选择器> actionbar
为例。
title @s actionbar ["",{"text":"[蜜蜂助手] ","color":"gold"},{"score":{"name":"*","objective":"bu_beeamount"}},"只蜜蜂,成年的",{"score":{"name":"*","objective":"bu_beeadult"}},"只,幼年的",{"score":{"name":"*","objective":"bu_beebaby"}},"只"]
此时,玩家的快捷栏上方就会显示蜂巢里有多少蜜蜂,有多少只成年蜜蜂,有多少只幼年蜜蜂了。
有关文本的格式,请见原始JSON文本。
- 清理
需要注意的是,以上四个记分项需要及时清理掉,否则会影响到下一次查询。
scoreboard players set @s bu_beeamount 0 scoreboard players set @s bu_beeadult 0 scoreboard players set @s bu_beebaby 0 scoreboard players set @s bu_beeage 0
润色与改进
目前这个数据包在加载地图后便会一直检测所有玩家有没有盯着蜂巢,有时这会导致极大的服务器计算资源浪费。而如果你在一个有多语需求的服务器使用这个数据包,也只有会中文的人能使用。下面介绍一下谓词和原始JSON文本。
筛选玩家
上面也提示了,说运用选择器参数可以筛选玩家。如果不是一直检测所有玩家的话,那么就只能检测有需要使用的人了。很容易想到,通常我们会使用玻璃瓶对蜂巢收集蜂蜜,因此可以只检测手(以副手为例)中拿着玻璃瓶的玩家。
建立data/beeutility/predicates/hold_glass_bottle.json
谓词文件,内容如下:
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"equipment":{
"offhand":{
"item": "minecraft:glass_bottle"
}
}
}
}
此谓词会检测应用该谓词的实体是否在副手拿着玻璃瓶。通过目标选择器参数,我们可以应用该谓词,筛选出有需要使用该数据包的玩家。
将beeutility:tick
函数前面改为:
execute as @a[predicate=beeutility:hold_glass_bottle] at @s anchored eyes ...
就可以筛选了。
多语言
细心的你也许用/data
获取过灾厄旗帜的数据,这个物品的名字是一个原始JSON文本,里面含有translate
属性,值为"block.minecraft.ominous_banner"
。这样的值被称之为本地化键名(有的地方也称翻译关键字或者翻译标识符)。如果你解压原版游戏的.jar
文件,你会发现上面的值就是assets/lang/en_us.json
里的键。游戏在解析translate
的时候先将值在资源包和外置资源文件的assets/lang/<当前语言的语言代码>.json
的键中寻找,再将对应的值(也就是实际的文本)实际显示出来。如果当前语言没有,就去en_us.json
里找。若都没有,就只能直接输出标识符。
原始JSON文本页面还提到了一个with
属性,就是和translate
搭配使用的。如果你自己翻过语言文件,你就会发现一些文本中含有%s
、%n$s
之类的变量。这个时候,with
属性里面的文本就会按顺序替代这些变量。
新建一个资源包,填写好pack.mcmeta
,建立assets/beeutility/lang
文件夹,在文件夹下建立语言文件,以下以zh_cn.json
和en_us.json
为例。
en_us.json
{
"beeutility.actionbar": "%s %s",
"beeutility.beeutility": "[Bee utility]",
"beeutility.result": "%d bee(s), %d adult bee(s), %d baby bee(s)"
}
zh_cn.json
{
"beeutility.actionbar": "%s %s",
"beeutility.beeutility": "[蜜蜂助手]",
"beeutility.result": "%d 只蜜蜂,成年的 %d 只,幼年的 %d 只"
}
也许你会很迷惑,为什么会有一个"%s %s"
?这是因为有一部分语言从右往左阅读,对于这些语言我们可以将其设为%2$s %1$s
来改变语序。
现在,将上述/title
命令的原始JSON文本写成这样:
{
"translate": "beeutility.actionbar",
"with": [
{
"translate": "beeutility.beeutility",
"color":"gold"
},
{
"translate": "beeutility.result",
"with": [
{"score":{"name":"*","objective":"bu_beeamount"}},
{"score":{"name":"*","objective":"bu_beeadult"}},
{"score":{"name":"*","objective":"bu_beebaby"}}
]
}
]
}
现在,将资源包通过服务器资源包分发出去就可以了。
更进一步?
既然已经用上资源包了,那么我们也可以另做一个调用CustomModelData
的物品模型并把CustomModelData
应用到一个不可用原版方法获得的自定义NBT的物品上,让查看蜂巢的人进一步减少。同样的,上述谓词也得做出相应的改变来检测。
或者,你也可以不仅仅只读取蜜蜂个数,可以顺便展示一些其他数据,等等等等。
下一步
这个数据包甚至没有用到一个盔甲架和效果区域云,也没有用到任何战利品表和结构。试着在数据包中应用这些功能也许是个好选择。
你也可以学习其他已经做成的数据包,或者自己定好一个目标,并把它做出来。
或者,继续看下面这个实例。
实例:指哪炸哪
目标
在本实例中,我们会解析如何制作一个数据包,使得玩家视线所指的地方发生爆炸。
这篇实例讲述的会十分含糊,甚至有一段干脆全部贴代码。这就需要你自己静下心来思考这些文件的关系了。
开始
上一个实例我们已经较为清楚的描述了“视线所指”如何实现,这里不再赘述。
本实例的GitHub地址:点击这里
爆炸
嗯,爆炸。
还能多简单呢?
为了达到爆炸的目的,我们可以在所指的地方召唤一个瞬间爆炸的TNT或者苦力怕。
但是,我们也可以做的更有趣一些,比如……一个小小的世界橡皮擦,又或是加一些粒子特效。
编写与实现
初始化
注:以下文字中,#<命名空间>:<名字>
表示<命名空间>下的<名字>标签,<命名空间>:<名字>
函数表示<命名空间>下的<名字>函数,文件名对应data/<命名空间>/<名字>.mcfunction
。资源包中也存在对应的关系。
新建一个文件夹,填写好pack.mcmeta
,建立data
文件夹。在data
文件夹下建立nuke
(本实例以此为主要的命名空间)和minecraft/tags/functions
(一些函数需要被每刻执行和在地图加载时执行)文件夹。
本实例中不使用队伍来侦测正在使用该功能的玩家,而是使用记分项。本实例的记分项前缀为nuke
。
建立data/nuke/functions
,建立load.mcfunction
函数,内容如下:
scoreboard objectives add nukePerson dummy scoreboard objectives add nukeUseCSt minecraft.used:minecraft.carrot_on_a_stick
好,你可能会问,“为什么这一次有一个不用dummy了”,对吧?
在原版中,胡萝卜钓竿应该算是最方便检测右键功能的了,原因就是有这么一个准则minecraft.used:minecraft.carrot_on_a_stick
,它可以自动记录玩家使用胡萝卜钓竿的次数。
没错,我们这次会使用胡萝卜钓竿作为本功能的开关(的本体)。记分项的“CSt”也就是“Carrot on a Stick”(胡萝卜钓竿)的缩写。
这一次胡萝卜钓竿的检测会参照更多的合成的某个版本编写,如果目录结构过于分散可以尝试自己简化。
建立data/minecraft/tags/functions/load.json
,内容如下:
{
"replace": false,
"values": [
"nuke:load"
]
}
这样,将nuke:load
函数设定为地图载入时执行。
视线投射
现在,我们要获取玩家所指的位置来生成我们的TNT。
在nuke
命名空间下,建立tick.mcfunction
和ray.mcfunction
函数。
tick.mcfunction
execute as @a run function nuke:entities/player execute as @a[scores={nukePerson=1..}] at @s anchored eyes run function nuke:ray
第一条命令用于后面的胡萝卜钓竿检测。
将nuke:tick
函数也加入#minecraft:tick
,使得其每刻执行。
ray.mcfunction
execute unless entity @s[distance=..20] positioned ^ ^ ^2 run function nuke:nuke execute if entity @s[distance=..20] unless block ~ ~ ~ #nuke:get_through run function nuke:nuke execute if entity @s[distance=..20] if block ~ ~ ~ #nuke:get_through positioned ^ ^ ^0.005 run function nuke:ray
好,一上来出现了很多东西。
#nuke:get_through
标签内储存的是视线追踪会忽略的方块,毕竟我们获取“视线所指”的方块并不想包含空气啊草啊花啊这些东西。这个标签是自创的,也就是你需要自己整理一份/抄我的标签列表,然后把列表放到data/nuke/tags/blocks/get_through.json
里面。
nuke:nuke
是实际执行爆炸的函数。
你会发现,这次出现了三个命令。第一条是因为,我们并不希望我们看着天空就不会发生爆炸(也就是并没有看着任何方块的时候),所以如果视线追踪往前20格都没有碰到任何方块,那就直接再前进两格然后执行爆炸。
其余的基本和第一个实例一致。
爆炸和消除
在nuke
命名空间下,建立nuke:nuke
函数。
summon minecraft:tnt ~ ~ ~
就这么简单。或者,如果你想做世界橡皮擦:
setblock ~ ~ ~ minecraft:air
基本功能就实现(但还未实装)完毕了。
胡萝卜钓竿开关
还记得上面提到的函数nuke:entities/player
吗?
nuke:entities/player
execute as @s[scores={nukeUseCSt=1..}] at @s run function nuke:use_carrot_on_a_stick/type
nuke:use_carrot_on_a_stick/type
execute as @s[nbt={SelectedItem:{id:"minecraft:carrot_on_a_stick"}}] run function nuke:use_carrot_on_a_stick/mainhand execute as @s[nbt=!{SelectedItem:{id:"minecraft:carrot_on_a_stick"}}] run function nuke:use_carrot_on_a_stick/offhand scoreboard players reset @s nukeUseCSt
nuke:use_carrot_on_a_stick/mainhand
execute as @s[nbt={SelectedItem:{tag:{id:"nuke:remote"}}}] run function nuke:use_carrot_on_a_stick/items/remote execute as @s[tag=!nuke_used,nbt={SelectedItem:{tag:{id:"nuke:remote_off"}}}] run function nuke:use_carrot_on_a_stick/items/remote_off tag @s remove nuke_used
nuke:use_carrot_on_a_stick/items/remote_off
loot replace entity @s[nbt=!{SelectedItem:{id:"minecraft:carrot_on_a_stick"}}] weapon.offhand 1 loot nuke:remote loot replace entity @s[nbt={SelectedItem:{id:"minecraft:carrot_on_a_stick"}}] weapon.mainhand 1 loot nuke:remote function nuke:start
nuke:use_carrot_on_a_stick/items/remote
loot replace entity @s[nbt=!{SelectedItem:{id:"minecraft:carrot_on_a_stick"}}] weapon.offhand 1 loot nuke:remote_off loot replace entity @s[nbt={SelectedItem:{id:"minecraft:carrot_on_a_stick"}}] weapon.mainhand 1 loot nuke:remote_off function nuke:stop tag @s add nuke_used
nuke:start
scoreboard players set @s nukePerson 1
nuke:stop
scoreboard players set @s nukePerson 0
东西开始变多了。
首先,我们得构想我们的这个胡萝卜钓竿开关——就叫遥控器好了,到底是怎么样的。
为了和普通的胡萝卜钓竿区别开来,我们会在物品的tag
中嵌入自己的id
标签,并把关闭状态的遥控器的id
的值设定为nuke:remote_off
,开启的设定为nuke:remote
。
检测玩家的SelectedItem
标签就可以检测主手上的物品了。此处我们检测tag
就是检测玩家手上有没有遥控器。
同时,因为遥控器肯定长得不像胡萝卜钓竿,所以我们需要通过CustomModelData
来重新指定遥控器的材质。
应该会有两个遥控器的图片(随便画的)。
新建一个资源包,填写好pack.mcmeta
,在assets/minecraft/models/item/carrot_on_a_stick.json
写入以下内容:
{
"parent": "item/handheld_rod",
"textures": {
"layer0": "item/carrot_on_a_stick"
},
"overrides": [
{ "predicate": { "custom_model_data": 13400000 }, "model": "nuke:item/remote"},
{ "predicate": { "custom_model_data": 13400001 }, "model": "nuke:item/remote_off"}
]
}
其中的overrides
就是来指定CustomModelData
的。我们指定了两个,13400000
和13400001
,分别对应两个模型。
assets/nuke/models/item/remote.json
{
"parent": "item/handheld",
"textures": {
"layer0": "nuke:item/remote"
}
}
其中nuke:item/remote
对应材质assets/nuke/textures/item/remote.png
,也就是开启的状态的材质。
另一个文件同理。
为了获取物品的方便,我们使用战利品表来预定义物品。此处以关闭的遥控器为例。
nuke:remote_off.json
{
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:carrot_on_a_stick",
"functions": [
{
"function": "minecraft:set_nbt",
"tag": "{id:'nuke:remote_off',display:{Name:'{\"translate\":\"item.nuke.remote_off\"}'},CustomModelData:13400001}"
}
]
}
]
}
]
}
掉落一份该战利品表就会获得关闭状态的遥控器。除了定义tag
中的id
,我们也自定义了物品的CustomModelData
和显示名称,和上面的多语言一样去写就行。此处在简体中文中显示的名称为“遥控器 (关闭)”。
同理,定义一份开启的。
现在我们需要回顾上面的代码。
基本思路是,循环检测钓竿,如果有人用了遥控器就切换遥控器的状态,并且根据遥控器的状态决定是开始爆炸还是停止爆炸。
切换遥控器的状态,可以直接替换玩家对应的栏位,比如像上方一样使用/loot replace
,然后调用定义好的战利品表,十分方便。
上方还预留了一个“offhand”,用于副手。除了使用和主手一样的使用目标选择器的nbt
参数以外(但是由于副手没有提供SelectedItem
这么方便的标签,我们需要读取玩家背包并且指定栏位),我们也可以使用1.15加入的(也就是上方蜜蜂助手所使用的)谓词文件去判断。这个谓词的编写就让你自己尝试写吧,绝对不是因为这玩意是在1.14写的所以我懒得写谓词。
至此,编写完毕。
另请参阅
- ruhuasiyu的数据包制作教程
- ruhuasiyu的更多的合成数据包,其中包含大量可学习借鉴的地方