本帖最后由 洞穴夜莺 于 2021-2-5 11:29 编辑
本帖计划补充一些常用的且在森林蝙蝠翻译后被修改的Fabric开发文档(侧重于在新版有变化的机制)
并不打算完整翻译Wiki
已完成的内容
森林蝙蝠原译文https://www.mcbbs.net/forum.php? ... eid%26typeid%3D2300
新人翻译,有些地方可能翻译得比较怪异
翻译状态
本帖计划补充一些常用的且在森林蝙蝠翻译后被修改的Fabric开发文档(侧重于在新版有变化的机制)
并不打算完整翻译Wiki
已完成的内容
森林蝙蝠原译文https://www.mcbbs.net/forum.php? ... eid%26typeid%3D2300
新人翻译,有些地方可能翻译得比较怪异
翻译状态
本帖最后由 洞穴夜莺 于 2020-8-28 16:50 编辑
创建命令(译者注:森林蝙蝠原译标题为"创建指令")
创建命令允许开发者添加可以通过命令使用的功能。这个指南会教你怎么注册命令以及Brigadier的一般命令结构。
注意:所有这里写的代码均面向1.14.4。一些映射可能已经在Yarn中改变,但所有的代码应该都适用。
啥是Brigadier?
Brigadier是一个由Mojang编写并在Minecraft中使用的解析和命令调度器。Brigadier是一个你可以创建一棵参数和命令的基于树的命令库。
Brigadier的源码可以在这里找到:https://github.com/Mojang/brigadier
啥是命令?
Brigadier需要你指定要运行的Command。一个"命令"在Brigadier中是一个宽松的词汇。但一般它指命令树的末端。这是你命令的代码被执行的地方。一个Command是一个函数式接口。这个命令有一个定义了命令源类型的泛型S。这个命令源在运行的提供了一些上下文。在Minecraft中,这一般是可能代表一个服务器、一个命令方块,一个远程控制台连接,一个玩家或一个实体的ServerCommandSource。
Command中唯一的方法,run(CommandContext<S>)接受一个CommandContext<S>参数并返回一个整数。这个命令上下文持有你的命令源并允许你获取命令参数,查看已被解析的命令节点以及查看此命令使用的输入。
一个命令可以被如下几种方式实现
用lambda
复制代码
用匿名类
复制代码
实现为一个类
复制代码
用方法引用
复制代码
这个run(CommandContext)方法可以抛出一个CommandSyntaxException,这被后面的内容覆盖。
可以认为这个整数(译注:指返回值)是这个命令运行的结果,在Minecraft中,这个结果可以等同与从命令方块接红石比较器所获取的红石信号强度或者传递给面向的连锁型命令方块。一般来说负数表示一个命令失败了并且没有做任何事情。运行结果0表示命令被传递。正数表示命令运行成功并做了一些事情。
一个基础的命令
下面是一个没有参数的命令:
复制代码
CommandManager.literal("foo")告诉brigadier这个命令有一个节点,一个叫foo的字面量。必须运行/foo来执行这个命令。输入/Foo、/FoO、/FOO、 /fOO或/fooo则不行。
一个子命令
你要先正常注册一个这个命令的字面量节点来添加一个子命令。
复制代码
需要在已存在的节点后添加下一个节点来创建子命令,这是用接受一个ArgumentBuilder参数的then(ArgumentBuilder)方法完成的。
下面的代码创建了命令foo <bar>。
复制代码
建议在向命令添加节点时缩进你的代码,通常缩进深度等同于你的节点在命令树中的深度。换行使得添加另一个节点更加明显。本指南后面会使用这种对于树状命令的可选风格。
所以来试试执行命令
如果你在游戏中输入/foo bar,命令会执行失败,这是因为当处理了所有的参数却没有可供执行的代码。你需要用executes(Command)方法告诉游戏需要执行的内容来解决这个问题。下面是一个命令应该有的样子的例子:
复制代码
注册这个命令
注册命令由注册CommandRegistrationCallback的回调来完成。关于注册回调的信息,参阅回调文章
这个事件应该在你的Mod主类中注册。这个回调有两个参数。CommmandDispatcher<S>是用来注册,解析和执行命令的。S是命令调度器支持的命令源。第二个参数是一个表示命令要注册的服务器类型的布尔值,true为专用服务器, false为内置服务器。
复制代码
在你的lambda中、方法引用或别的什么东西,你可以注册你的命令。
复制代码
你可以通过检查专用标志只在服务器注册一个命令
复制代码
反之亦然
复制代码
参数
参数用于在Brigadier中解析和检查任何输入的参数。Minecraft创建了一些特殊的参数类型用于自己使用,例如代表像@a, @r, @p, @e[type=!player, limit=1, distance=..2]这样的一个游戏内实体选择器的EntityArgumentType,或者一个解析字符串nbt(snbt)并检查输入语法的NbtTagArgumentType。
TODO:补充有关使用参数的更多信息
静态导入
你可以在创建一个字面量节点时每次都打CommandManager.literal("foo")。这是可行的,但是你可以静态导入方法来缩短对literal("foo")的使用。这也在获取参数的值时有用。把StringArgumentType.getString(ctx, "string")简写成getString(ctx, "string")。这也在Minecraft自己的参数类型起作用。
你的导入看起来像这样:
复制代码
注意:请确保你使用的literal和argument是CommandManager的,不然编译错误。
Brigadier的默认参数类型在com.mojang.brigadier.arguments
Minecraft的参数类型在net.minecraft.command.arguments。CommandManager在net.minecraft.server.command。
高级概念
TODO:一些段正在被移动到子类别中,并将在迁移时移入各自的文章。
FAQ
为啥我的命令不能编译
这里有两个可能导致这个问题的直接原因。
抛出或捕获一个CommandSyntaxException
这种问题的解决方案是让运行或建议方法抛出一个CommandSyntaxException。不用担心,brigadier会处理异常并输出错误信息。
泛型的问题
你可能在泛型这里有问题。确认你使用CommandManager.literal(…)或CommandManager.argument(…)而不是LiteralArgumentBuilder或 RequiredArgumentBuilder。
我可以注册客户端命令吗?
Fabric尚未支持客户端命令,这里有一个Cotton组编写的[url=https://github.com/CottonMC/ClientCommands]第三方Mod添加了此内容。
黑暗的艺术
一些东西不推荐,但是是可以做的。
我可以动态注册命令吗?
可以这么做但不推荐。你要获取服务器的CommandManager并添加你想要的命令到它的CommandDispacter。
完事之后你要用sendCommandTree(ServerPlayerEntity)给所有玩家发送一遍命令树。这是因为客户端缓存了在登录时收到的命令树(或者当他收到操作员变化网络包时)来实现本地补全和丰富错误信息。
我可以动态移除命令吗?
你也可以这么做,但是它比注册命令更不稳定且可能导致不想要的副作用。为了简化事情,你可以对brigadier反射并移除节点。在这之后,你需要再用sendCommandTree(ServerPlayerEntity)发命令树给每个玩家。如果你不发送命令树,客户端可能认为命令仍然存在,尽管服务器会执行失败。
条件
讨论只有操作员能执行的情况,这是requires方法的用处。这个方法有一个接受ServerCommandSource来检查和决定CommandSource是否能执行命令的Predicate<ServerCommandSource>参数。
例子如下:
复制代码
这个命令只会在有至少4级权限才执行,如果条件返回false,命令不会执行。并且也有不向没有4级权限的玩家展示Tab补全的副作用。
调用指定require块中权限实现不会被阻止。只是注意在权限变动时要重新发送命令树。
异常
Brigadier支持用于解析失败或执行失败抛出命令异常,也同时丰富错误信息。
所有Brigadier的异常基于CommandSyntaxException,这两种Brigadier提供的主要异常是动态和你需要调用create()来抛出的简单异常类型。
这些异常也允许你用创建输入命令错误信息的createWithContext(ImmutableStringReader)指定上下文。下面是个展示异常使用的抛硬币命令。
复制代码
你不会被限制在单个类型异常中,因为Brigadier也提供了可以接受额外上下文参数的动态异常。
复制代码
这里有接受不同数量参数的更多动态异常(Dynamic2CommandExceptionType、Dynamic3CommandExceptionType、 Dynamic4CommandExceptionType、 DynamicNCommandExceptionType)。你应该记住动态异常接受一个对象作为参数,所以你可能必须转换参数用于你自己的用途。
重定向(别名)
重定向是Brigadier的别名实现形式,下面是Minecraft怎么处理/msg的别名/tell和/w的代码。
复制代码
重定向告诉brigadier在另一个节点继续解析。
重定向(连锁)
像/execute as @e[type=player] in the_end run tp ~ ~ ~的命令是由重定向实现的。下面是一个连锁命令的例子:
复制代码
重定向也可以用redirect modifier修改命令源用于构建者命令。
复制代码
ServerCommandSource能干啥?
一个服务器命令源提供了一些额外的实现在一个命令执行时指定上下文。这包括获取执行命令的实体的能力,执行命令所位于的世界和执行命令的服务器。
复制代码
一些命令例子
广播一则消息
复制代码
/giveMeDiamond
首先我们注册"giveMeDiamond"为一个字面量节点然后执行块告诉调度器要运行的方法
复制代码
然后因为我们想要给玩家,所以我们要检查CommandSource是一个玩家,可以用getPlayer来在执行者不是玩家时出错。
复制代码
然后我们向玩家的物品栏添加物品,同时检查物品栏对象是否为空。
复制代码
安提阿
...将你安提阿的神圣手雷对向你的敌人,那敌人也就是那我眼中下流者,应当将它消除。
笑话的题外话,这个命令在指定位置或玩家的焦点处召唤一个点燃的TNT。
首先向CommandDispacther注册一个antioch字面量节点和一个可选的召唤位置参数。
复制代码
创建笑话背后的消息
复制代码
更多例子待补充
自定义参数类型
Brigadier支持自定义参数类型,这段会告诉你怎么创建一个简单的参数类型。
警告:自定义参数类型需要客户端正确安装Mod!如果你在编写服务器插件,考虑使用现有的参数类型并自定义建议提供器。
这里我们会创建一个UuidArgumentType。
首先创建一个继承ArgumentType的类,注意ArgumentType是个泛型类,所以定义你要返回的类型为泛型。
复制代码
ArgumentType需要你实现一个parse方法,它要返回泛型类型的对象。
复制代码
这个方法是你解析的发生的地方,这个方法的返回值给出一个对象参数或者抛出一个CommandSyntaxException解析失败。
接下来你要保存焦点当前位置,这样你可以求出指定参数的子串,这是始终在你要解析的参数的最前面。
复制代码
现在我们获取整个参数。基于你的参数类型,你需要有不同的标准或者像一些参数一样从{到}。对于一个UUID来说我们只是算出参数的结束焦点位置在哪。
复制代码
最后我们检查参数是否正确并解析出我们想要的参数,并在解析失败时抛出异常。
复制代码
这个ArgumentType就完成了,但是客户端会拒绝解析参数并抛出异常,这是因为服务器会告诉客户端命令节点的参数类型,客户端不会解析它不知道的类型。我们注册一个ArgumentSerializer来解决此问题。在你的ModInitializer中。对于更多复杂的参数类型,你可以创建你自己的ArgumentSerializer。
复制代码
然后这里是完整的ArgumentType:
UuidArgumentType.java
复制代码
创建命令(译者注:森林蝙蝠原译标题为"创建指令")
创建命令允许开发者添加可以通过命令使用的功能。这个指南会教你怎么注册命令以及Brigadier的一般命令结构。
注意:所有这里写的代码均面向1.14.4。一些映射可能已经在Yarn中改变,但所有的代码应该都适用。
啥是Brigadier?
Brigadier是一个由Mojang编写并在Minecraft中使用的解析和命令调度器。Brigadier是一个你可以创建一棵参数和命令的基于树的命令库。
Brigadier的源码可以在这里找到:https://github.com/Mojang/brigadier
啥是命令?
Brigadier需要你指定要运行的Command。一个"命令"在Brigadier中是一个宽松的词汇。但一般它指命令树的末端。这是你命令的代码被执行的地方。一个Command是一个函数式接口。这个命令有一个定义了命令源类型的泛型S。这个命令源在运行的提供了一些上下文。在Minecraft中,这一般是可能代表一个服务器、一个命令方块,一个远程控制台连接,一个玩家或一个实体的ServerCommandSource。
Command中唯一的方法,run(CommandContext<S>)接受一个CommandContext<S>参数并返回一个整数。这个命令上下文持有你的命令源并允许你获取命令参数,查看已被解析的命令节点以及查看此命令使用的输入。
一个命令可以被如下几种方式实现
用lambda
- Command<Object> command = context -> {
- return 0;
- };
用匿名类
- Command<Object> command = new Command<Object>() {
- @Override
- public int run(CommandContext<Object> context) {
- return 0;
- }
- }
实现为一个类
- final class XYZCommand implements Command<Object> {
- @Override
- public int run(CommandContext<Object> context) {
- return 0;
- }
- }
用方法引用
- void registerCommand() {
- // 忽略这个,一会再解释
- dispatcher.register(CommandManager.literal("foo"))
- .executes(this::execute); // This refers to the "execute" method below.
- }
-
- private int execute(CommandContext<Object> context) {
- return 0;
- }
这个run(CommandContext)方法可以抛出一个CommandSyntaxException,这被后面的内容覆盖。
可以认为这个整数(译注:指返回值)是这个命令运行的结果,在Minecraft中,这个结果可以等同与从命令方块接红石比较器所获取的红石信号强度或者传递给面向的连锁型命令方块。一般来说负数表示一个命令失败了并且没有做任何事情。运行结果0表示命令被传递。正数表示命令运行成功并做了一些事情。
一个基础的命令
下面是一个没有参数的命令:
- dispatcher.register(CommandManager.literal("foo").executes(context -> {
- System.out.println("Called foo with no arguments");
-
- return 1;
- }));
CommandManager.literal("foo")告诉brigadier这个命令有一个节点,一个叫foo的字面量。必须运行/foo来执行这个命令。输入/Foo、/FoO、/FOO、 /fOO或/fooo则不行。
一个子命令
你要先正常注册一个这个命令的字面量节点来添加一个子命令。
- dispatcher.register(CommandManager.literal("foo")
需要在已存在的节点后添加下一个节点来创建子命令,这是用接受一个ArgumentBuilder参数的then(ArgumentBuilder)方法完成的。
下面的代码创建了命令foo <bar>。
- dispatcher.register(CommandManager.literal("foo")
- .then(CommandManager.literal("bar"))
- );
建议在向命令添加节点时缩进你的代码,通常缩进深度等同于你的节点在命令树中的深度。换行使得添加另一个节点更加明显。本指南后面会使用这种对于树状命令的可选风格。
所以来试试执行命令
如果你在游戏中输入/foo bar,命令会执行失败,这是因为当处理了所有的参数却没有可供执行的代码。你需要用executes(Command)方法告诉游戏需要执行的内容来解决这个问题。下面是一个命令应该有的样子的例子:
- dispatcher.register(CommandManager.literal("foo")
- .then(CommandManager.literal("bar")
- .executes(context -> {
- System.out.println("Called foo with bar");
-
- return 1;
- })
- )
- );
注册这个命令
注册命令由注册CommandRegistrationCallback的回调来完成。关于注册回调的信息,参阅回调文章
这个事件应该在你的Mod主类中注册。这个回调有两个参数。CommmandDispatcher<S>是用来注册,解析和执行命令的。S是命令调度器支持的命令源。第二个参数是一个表示命令要注册的服务器类型的布尔值,true为专用服务器, false为内置服务器。
- public class ExampleCommandMod implements ModInitializer {
- @Override
- public void onInitialize() {
- CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
- ...
- });
- }
- }
在你的lambda中、方法引用或别的什么东西,你可以注册你的命令。
- public class ExampleCommandMod implements ModInitializer {
- @Override
- public void onInitialize() {
- CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
- dispatcher.register(CommandManager.literal("foo").executes(context -> {
- System.out.println("foo");
- return 1;
- }));
- });
- }
- }
你可以通过检查专用标志只在服务器注册一个命令
- public class ExampleCommandMod implements ModInitializer {
- @Override
- public void onInitialize() {
- CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
- if (dedicated) {
- TestDedicatedCommand.register(dispatcher);
- }
- });
- }
- }
反之亦然
- public class ExampleCommandMod implements ModInitializer {
- @Override
- public void onInitialize() {
- CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
- if (!dedicated) {
- TestDedicatedCommand.register(dispatcher);
- }
- });
- }
- }
参数
参数用于在Brigadier中解析和检查任何输入的参数。Minecraft创建了一些特殊的参数类型用于自己使用,例如代表像@a, @r, @p, @e[type=!player, limit=1, distance=..2]这样的一个游戏内实体选择器的EntityArgumentType,或者一个解析字符串nbt(snbt)并检查输入语法的NbtTagArgumentType。
TODO:补充有关使用参数的更多信息
静态导入
你可以在创建一个字面量节点时每次都打CommandManager.literal("foo")。这是可行的,但是你可以静态导入方法来缩短对literal("foo")的使用。这也在获取参数的值时有用。把StringArgumentType.getString(ctx, "string")简写成getString(ctx, "string")。这也在Minecraft自己的参数类型起作用。
你的导入看起来像这样:
- // getString(ctx, "string")
- import static com.mojang.brigadier.arguments.StringArgumentType.getString;
- // word()
- import static com.mojang.brigadier.arguments.StringArgumentType.word;
- // literal("foo")
- import static net.minecraft.server.command.CommandManager.literal;
- // argument("bar", word())
- import static net.minecraft.server.command.CommandManager.argument;
- // Import everything
- import static net.minecraft.server.command.CommandManager.*;
注意:请确保你使用的literal和argument是CommandManager的,不然编译错误。
Brigadier的默认参数类型在com.mojang.brigadier.arguments
Minecraft的参数类型在net.minecraft.command.arguments。CommandManager在net.minecraft.server.command。
高级概念
页面 | 描述 |
条件 | 只允许某些用户在一定情况下执行命令。 |
异常 | 在一定的上下文带有描述性信息地执行一个命令失败。 |
建议 | 要向客户端发送的命令建议。 |
重定向(别名)[/usl] | 允许你给命令创建别名。 |
重定向(连锁) | 允许命令有重复的元素的标志。 |
自定义参数类型 | 解析你自己的参数类型并反回你自己的类型。 |
TODO:一些段正在被移动到子类别中,并将在迁移时移入各自的文章。
FAQ
为啥我的命令不能编译
这里有两个可能导致这个问题的直接原因。
抛出或捕获一个CommandSyntaxException
这种问题的解决方案是让运行或建议方法抛出一个CommandSyntaxException。不用担心,brigadier会处理异常并输出错误信息。
泛型的问题
你可能在泛型这里有问题。确认你使用CommandManager.literal(…)或CommandManager.argument(…)而不是LiteralArgumentBuilder或 RequiredArgumentBuilder。
我可以注册客户端命令吗?
Fabric尚未支持客户端命令,这里有一个Cotton组编写的[url=https://github.com/CottonMC/ClientCommands]第三方Mod添加了此内容。
黑暗的艺术
一些东西不推荐,但是是可以做的。
我可以动态注册命令吗?
可以这么做但不推荐。你要获取服务器的CommandManager并添加你想要的命令到它的CommandDispacter。
完事之后你要用sendCommandTree(ServerPlayerEntity)给所有玩家发送一遍命令树。这是因为客户端缓存了在登录时收到的命令树(或者当他收到操作员变化网络包时)来实现本地补全和丰富错误信息。
我可以动态移除命令吗?
你也可以这么做,但是它比注册命令更不稳定且可能导致不想要的副作用。为了简化事情,你可以对brigadier反射并移除节点。在这之后,你需要再用sendCommandTree(ServerPlayerEntity)发命令树给每个玩家。如果你不发送命令树,客户端可能认为命令仍然存在,尽管服务器会执行失败。
条件
讨论只有操作员能执行的情况,这是requires方法的用处。这个方法有一个接受ServerCommandSource来检查和决定CommandSource是否能执行命令的Predicate<ServerCommandSource>参数。
例子如下:
- dispatcher.register(literal("foo")
- .requires(source -> source.hasPermissionLevel(4))
- .executes(ctx -> {
- ctx.getSource().sendFeedback(new LiteralText("You are an operator", false));
- return 1;
- });
这个命令只会在有至少4级权限才执行,如果条件返回false,命令不会执行。并且也有不向没有4级权限的玩家展示Tab补全的副作用。
调用指定require块中权限实现不会被阻止。只是注意在权限变动时要重新发送命令树。
异常
Brigadier支持用于解析失败或执行失败抛出命令异常,也同时丰富错误信息。
所有Brigadier的异常基于CommandSyntaxException,这两种Brigadier提供的主要异常是动态和你需要调用create()来抛出的简单异常类型。
这些异常也允许你用创建输入命令错误信息的createWithContext(ImmutableStringReader)指定上下文。下面是个展示异常使用的抛硬币命令。
- dispatcher.register(CommandManager.literal("coinflip")
- .executes(ctx -> {
- Random random = new Random();
-
- if(random.nextBoolean()) { // 正面成功
- ctx.getSource().sendMessage(new TranslateableText("coin.flip.heads"))
- return Command.SINGLE_SUCCESS;
- }
-
- throw new SimpleCommandExceptionType(new TranslateableText("coin.flip.tails")).create(); // 背面失败
- }));
你不会被限制在单个类型异常中,因为Brigadier也提供了可以接受额外上下文参数的动态异常。
- DynamicCommandExceptionType used_name = new DynamicCommandExceptionType(name -> {
- return new LiteralText("The name: " + (String) name + " has been used");
- });
这里有接受不同数量参数的更多动态异常(Dynamic2CommandExceptionType、Dynamic3CommandExceptionType、 Dynamic4CommandExceptionType、 DynamicNCommandExceptionType)。你应该记住动态异常接受一个对象作为参数,所以你可能必须转换参数用于你自己的用途。
重定向(别名)
重定向是Brigadier的别名实现形式,下面是Minecraft怎么处理/msg的别名/tell和/w的代码。
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- LiteralCommandNode node = registerMain(dispatcher); // 注册主命令
- dispatcher.register(literal("tell")
- .redirect(node)); // 别名1,重定向到主命令
- dispatcher.register(literal("w")
- .redirect(node)); // 别名2,重定向到主命令
- }
-
- public static LiteralCommandNode registerMain(CommandDispatcher<ServerCommandSource> dispatcher) {
- return dispatcher.register(literal("msg")
- .then(argument("targets", EntityArgumentType.players())
- .then(argument("message", MessageArgumentType.message())
- .executes(ctx -> {
- return execute(ctx.getSource(), getPlayers(ctx, "targets"), getMessage(ctx, "message"));
- }))));
- }
重定向告诉brigadier在另一个节点继续解析。
重定向(连锁)
像/execute as @e[type=player] in the_end run tp ~ ~ ~的命令是由重定向实现的。下面是一个连锁命令的例子:
- LiteralCommandNode<ServerCommandSource> root = dispatcher.register(literal("fabric_test"));
- LiteralCommandNode<ServerCommandSource> root1 = dispatcher.register(literal("fabric_test")
- // 你可以注册许多字面量,它只注册分支的新部分,如果注册重复的分支,则会在控制台中提示命令冲突,但是仍然可以使用。
- .then(literal("extra")
- .then(literal("long")
- .redirect(root, this::lengthen)) // 返回连锁的根节点
- .then(literal("short")
- .redirect(root, this::shorten))) // 返回连锁的根节点
- .then(literal("command")
- .executes(ctx -> {
- ctx.getSource().sendFeedback(new LiteralText("Chainable Command"), false);
- return Command.SINGLE_SUCCESS;
- })));
重定向也可以用redirect modifier修改命令源用于构建者命令。
- .redirect(rootNode, context -> {
- return ((ServerCommandSource) context.getSource()).withLookingAt(Vec3ArgumentType.getVec3(context, "pos"));
- })
ServerCommandSource能干啥?
一个服务器命令源提供了一些额外的实现在一个命令执行时指定上下文。这包括获取执行命令的实体的能力,执行命令所位于的世界和执行命令的服务器。
- final ServerCommandSource source = ctx.getSource();
- // 获取命令源,这始终有效
-
- final Entity sender = source.getEntity();
- // 未检查,如果执行者是控制台则为空
-
- final Entity sender2 = source.getEntityOrThrow();
- // 会在命令不是由实体执行时结束执行
- // 这个可能返回一个实体,也可能发反馈必须有一个实体
- // 这个方法需要你的方法能抛出CommandSyntaxException.
- // ServerCommandSource的实体选项可能返回一个CommandBlock实体,一个玩家,或一个生物
-
- final ServerPlayerEntity player = source.getPlayer();
- // 如果命令执行者不是一个玩家会结束命令。也会发送必须是玩家的反馈。这个方法需要你的方法能抛出CommandSyntaxException.
- source.getPosition();
- // 获取Vec3形式的执行坐标这可能是实体或命令方块坐标或者世界出生点(后台执行)
-
- source.getWorld();
- // 获取执行者的世界,或者出生世界(后台执行)
-
- source.getRotation();
- // 获取执行者的Vec2f形式面向方向
-
- source.getMinecraftServer();
- // 获取服务器实例
-
- source.getName();
- // 获取命令源的名字可能是玩家,实体,被重命名的命令方块名字或者"Console"(后台执行)
-
- source.hasPermissionLevel(int level);
- // 如果有该权限等级返回true,基于操作员的状态 (在内置服务端中,必须开启作弊来执行一些命令)
一些命令例子
广播一则消息
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher){
- dispatcher.register(literal("broadcast")
- .requires(source -> source.hasPermissionLevel(2)) // 运行命令必须是游戏管理员,命令不会被任何非操作员玩家和1级操作员补全和执行
- .then(argument("color", ColorArgumentType.color())
- .then(argument("message", greedyString())
- .executes(ctx -> broadcast(ctx.getSource(), getColor(ctx, "color"), getString(ctx, "message")))))); // 你可以处理参数并传递它们
- }
-
- public static int broadcast(ServerCommandSource source, Formatting formatting, String message) {
- final Text text = new LiteralText(message).formatting(formatting);
-
- source.getMinecraftServer().getPlayerManager().broadcastChatMessage(text, false);
- return Command.SINGLE_SUCCESS; // 成功
- }
/giveMeDiamond
首先我们注册"giveMeDiamond"为一个字面量节点然后执行块告诉调度器要运行的方法
- public static LiteralCommandNode register(CommandDispatcher<ServerCommandSource> dispatcher) { // You can also return a LiteralCommandNode for use with possible redirects
- return dispatcher.register(literal("giveMeDiamond")
- .executes(ctx -> giveDiamond(ctx)));
- }
然后因为我们想要给玩家,所以我们要检查CommandSource是一个玩家,可以用getPlayer来在执行者不是玩家时出错。
- public static int giveDiamond(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
- final ServerCommandSource source = ctx.getSource();
-
- final PlayerEntity self = source.getPlayer(); // 如果不是玩家则停止执行
然后我们向玩家的物品栏添加物品,同时检查物品栏对象是否为空。
- if(!player.inventory.insertStack(new ItemStack(Items.DIAMOND))){
- throw new SimpleCommandExceptionType(new TranslatableText("inventory.isfull")).create();
- }
-
- return 1;
- }
安提阿
...将你安提阿的神圣手雷对向你的敌人,那敌人也就是那我眼中下流者,应当将它消除。
笑话的题外话,这个命令在指定位置或玩家的焦点处召唤一个点燃的TNT。
首先向CommandDispacther注册一个antioch字面量节点和一个可选的召唤位置参数。
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- dispatcher.register(literal("antioch")
- .then(required("location", BlockPosArgumentType.blockPos()
- .executes(ctx -> antioch(ctx.getSource(), BlockPosArgument.getBlockPos(ctx, "location")))))
- .executes(ctx -> antioch(ctx.getSource(), null)));
- }
创建笑话背后的消息
- public static int antioch(ServerCommandSource source, BlockPos blockPos) throws CommandSyntaxException {
- if(blockPos == null) {
- // 没有位置参数的情况我们计算玩家的焦点坐标
- // 这只是个例子,实际上这个类不存在
- blockPos = LocationUtil.calculateCursorOrThrow(source, source.getRotation());
- }
-
- final TntEntity tnt = new TntEntity(source.getWorld(), blockPos.getX(), blockPos.getY(), blockPos.getZ(), null);
- tnt.setFuse(3);
-
- source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("...lobbest thou thy Holy Hand Grenade of Antioch towards thy foe").formatting(Formatting.RED), false);
- source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("who being naughty in My sight, shall snuff it.").formatting(Formatting.RED), false);
- source.getWorld().spawnEntity(tnt);
- return 1;
- }
更多例子待补充
自定义参数类型
Brigadier支持自定义参数类型,这段会告诉你怎么创建一个简单的参数类型。
警告:自定义参数类型需要客户端正确安装Mod!如果你在编写服务器插件,考虑使用现有的参数类型并自定义建议提供器。
这里我们会创建一个UuidArgumentType。
首先创建一个继承ArgumentType的类,注意ArgumentType是个泛型类,所以定义你要返回的类型为泛型。
- public class UuidArgumentType implements ArgumentType<UUID> {
ArgumentType需要你实现一个parse方法,它要返回泛型类型的对象。
- @Override
- public UUID parse(StringReader reader) throws CommandSyntaxException {
这个方法是你解析的发生的地方,这个方法的返回值给出一个对象参数或者抛出一个CommandSyntaxException解析失败。
接下来你要保存焦点当前位置,这样你可以求出指定参数的子串,这是始终在你要解析的参数的最前面。
- int argBeginning = reader.getCursor(); // 参数的开始位置
- if (!reader.canRead()) {
- reader.skip();
- }
现在我们获取整个参数。基于你的参数类型,你需要有不同的标准或者像一些参数一样从{到}。对于一个UUID来说我们只是算出参数的结束焦点位置在哪。
- while (reader.canRead() && reader.peek() != ' ') { // peek 提供了当前焦点处字符
- reader.skip(); // 让StringReader移动到下一个位置
- }
最后我们检查参数是否正确并解析出我们想要的参数,并在解析失败时抛出异常。
- try {
- UUID uuid = UUID.fromString(uuidString); // 这是我们的实际逻辑
- return uuid; // 我们在解析成功时返回类型并继续下一解析
- } catch (Exception ex) {
- // 当从字符串创建UUID时,可能会抛出异常,把它包装成CommandSyntaxException
- // 带有上下问可以使Brigadier向用户提供失败位置的信息
- // 一般的创建方法
- throw new SimpleCommandExceptionType(new LiteralText(ex.getMessage())).createWithContext(reader);
- }
这个ArgumentType就完成了,但是客户端会拒绝解析参数并抛出异常,这是因为服务器会告诉客户端命令节点的参数类型,客户端不会解析它不知道的类型。我们注册一个ArgumentSerializer来解决此问题。在你的ModInitializer中。对于更多复杂的参数类型,你可以创建你自己的ArgumentSerializer。
- ArgumentTypes.register("mymod:uuid", UuidArgumentType.class, new ConstantArgumentSerializer(UuidArgumentType::uuid));
- // 这个参数会创建ArgumentType
然后这里是完整的ArgumentType:
UuidArgumentType.java
- import com.mojang.brigadier.StringReader;
- import com.mojang.brigadier.arguments.ArgumentType;
- import com.mojang.brigadier.context.CommandContext;
- import com.mojang.brigadier.exceptions.CommandSyntaxException;
- import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
- import net.minecraft.text.LiteralText;
- import net.minecraft.util.SystemUtil;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.UUID;
- /**
- * Represents an ArgumentType that will return a UUID.
- */
- public class UuidArgumentType implements ArgumentType<UUID> {
- public static UuidArgumentType uuid() {
- return new UuidArgumentType();
- }
- public static <S> UUID getUuid(String name, CommandContext<S> context) {
- // 注意你应当永远假设CommandContext包装的CommandSource是个泛型
- // 如果你要用ServerCommandSource确保你在强制类型转换前类型检查
- return context.getArgument(name, UUID.class);
- }
- private static final Collection<String> EXAMPLES = SystemUtil.consume(new ArrayList<>(), list -> {
- list.add("765e5d33-c991-454f-8775-b6a7a394c097"); // i509VCB: Username The_1_gamers
- list.add("069a79f4-44e9-4726-a5be-fca90e38aaf5"); // Notch
- list.add("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"); // Dinnerbone
- });
- @Override
- public UUID parse(StringReader reader) throws CommandSyntaxException {
- int argBeginning = reader.getCursor(); // 你参数的开始位置
- if (!reader.canRead()) {
- reader.skip();
- }
- // 现在我们检查是否到了末尾(当canRead返回false时)
- // 或者到达下一个空格,它标志着下一个参数
- while (reader.canRead() && reader.peek() != ' ') { // peek 提供了当前焦点处字符
- reader.skip(); // 让StringReader移动到下一个位置
- }
- // 由这个参数的起始位置和下一个参数的起始位置获取这个命令的字串
- String uuidString = reader.getString().substring(argBeginning, reader.getCursor());
- try {
- UUID uuid = UUID.fromString(uuidString); // 这是我们的实际逻辑
- return uuid; // 我们在解析成功时返回类型并继续下一解析
- } catch (Exception ex) {
- // 当从字符串创建UUID时,可能会抛出异常,把它包装成CommandSyntaxException
- // 带有上下问可以使Brigadier向用户提供失败位置的信息
- // 一般的创建方法
- throw new SimpleCommandExceptionType(new LiteralText(ex.getMessage())).createWithContext(reader);
- }
- }
- @Override
- public Collection<String> getExamples() { // Brigadier对于命令参数的例子提供了支持,这个集合只应该包含能解析的内容。这通常用于模棱两个的命令参数的相同之处。
- return EXAMPLES;
- }
- }
比较长的代码建议使用复制代码代码折叠
- [spoiler][/spoiler]
本帖最后由 洞穴夜莺 于 2021-1-29 23:31 编辑
挖掘等级
简介
原版的挖掘等级系统很烂,它硬编码可挖掘的方块而且只支持镐却不支持其他挖掘工具。
设置一个方块的挖掘等级
使用FabricBlockSettings中的breakByTool方法来设置方块的挖掘等级,需要一个由Fabric在FabricToolTags中提供的物品标签。
复制代码
这里有一个挖掘等级的列表:
复制代码
处理方块材质问题
原版镐子对STONE, METAL, ANVIL有效。
原版斧头对WOOD, NETHER_WOOD, PLANT, REPLACEABLE_PLANT, BAMBOO, PUMPKIN有效。
工具能够在挖掘等级不够的情况下挖掘你这类材质的方块。
你必须创建你自己的材质拷贝来解决这个问题,讨论创建一个Material.STONE的拷贝的情况,看看Material.STONE的代码:
复制代码
把Material.Builder换成FabricMaterialBuilder得:
复制代码
当使用错误的工具时不掉落(面向1.15.x!)
你需要在方块的材质中设置requiresTool,因此需要创建你的材质的拷贝。
讨论创建一个Material.WOOD的拷贝来使你的木制方块只在使用正确的工具挖掘时掉落的情况,看看Material.STONE的代码:[原文如此,有误]
复制代码
把Material.Builder换成FabricMaterialBuilder并加入requiresTool()得:
复制代码
当使用错误的工具时不掉落(面向1.16.x!)
在方块的设置中设置requiresTool。
创建自定义工具
把你的工具加入Fabric工具标签来支持Mod方块。
向pickaxes标签添加一种镐的例子:
文件位置:/src/main/resources/data/fabric/tags/items/pickaxes.json
复制代码
挖掘等级
简介
原版的挖掘等级系统很烂,它硬编码可挖掘的方块而且只支持镐却不支持其他挖掘工具。
设置一个方块的挖掘等级
使用FabricBlockSettings中的breakByTool方法来设置方块的挖掘等级,需要一个由Fabric在FabricToolTags中提供的物品标签。
- settings.breakByTool(FabricToolTags.PICKAXES, 2)
这里有一个挖掘等级的列表:
- 0 -> 木 / 金 镐
- 1 -> 石镐
- 2 -> 铁镐
- 3 -> 钻石镐
- 4 -> 下界合金镐
处理方块材质问题
原版镐子对STONE, METAL, ANVIL有效。
原版斧头对WOOD, NETHER_WOOD, PLANT, REPLACEABLE_PLANT, BAMBOO, PUMPKIN有效。
工具能够在挖掘等级不够的情况下挖掘你这类材质的方块。
你必须创建你自己的材质拷贝来解决这个问题,讨论创建一个Material.STONE的拷贝的情况,看看Material.STONE的代码:
- new Material.Builder(MaterialColor.STONE).requiresTool().build()
把Material.Builder换成FabricMaterialBuilder得:
- new FabricMaterialBuilder(MaterialColor.STONE).requiresTool().build()
当使用错误的工具时不掉落(面向1.15.x!)
你需要在方块的材质中设置requiresTool,因此需要创建你的材质的拷贝。
讨论创建一个Material.WOOD的拷贝来使你的木制方块只在使用正确的工具挖掘时掉落的情况,看看Material.STONE的代码:[原文如此,有误]
- new Material.Builder(MaterialColor.WOOD).burnable().build()
把Material.Builder换成FabricMaterialBuilder并加入requiresTool()得:
- new FabricMaterialBuilder(MaterialColor.WOOD).burnable().requiresTool().build()
当使用错误的工具时不掉落(面向1.16.x!)
在方块的设置中设置requiresTool。
创建自定义工具
把你的工具加入Fabric工具标签来支持Mod方块。
向pickaxes标签添加一种镐的例子:
文件位置:/src/main/resources/data/fabric/tags/items/pickaxes.json
- {
- "replace": false,
- "values": [
- "examplemod:example_pickaxe"
- ]
- }
本帖最后由 洞穴夜莺 于 2020-8-28 19:54 编辑
命令建议
Brigadier允许指定自定义的参数建议,在Minecraft中,这些建议在玩家输入命令时被发送到客户端。(译注:bukkit的TabCompleter就是基于这个实现的)
建议提供器
SuggestionProvider用来创建一个发送给客户端的建议列表。一个建议提供器是一个接受CommandContext和一个SuggestionBuilder并返回一些Suggestion的函数式接口,因为可能建议没法马上确定,所以SuggestionProvider返回一个CompletableFuture。
建议可以是上下文有关的因为建议提供器允许你访问现在的命令上下文。
一个建议提供器的例子
例如你想要建议所有生物可能有的属性。
复制代码
因为我们拥有一个命令上下文,所以我们可以检查在此参数前的任何参数的值。
下面是一些样板代码
复制代码
为了在客户端显示建议,你要像构建器添加建议,可以用suggest方法实现。这些suggest方法一些处理数字和对小提示提供一些支持。
复制代码
你也可以结合用add(SuggestionBuilder)另一个建议构建者的结果。
最后,你要构建建议来返回,使用buildFuture方法完成。
复制代码
成品:
复制代码
使用建议提供器
现在你有了一个建议提供器,是时候使用它了。注意建议提供器不能对字面量节点使用。
当注册一个参数时,你可以用suggests(SuggestionProvider)设置建议提供器,像下面这样在RequiredArgumentBuilder上应用。
复制代码
内置建议提供器
Minecraft包含一些内置的建议提供器,如下:
CommandSource中的工具
CommandSource提供了少量帮助移除创建建议的样板代码的工具。很多工具方法包括返回从ID,位置或匹配的字符串的Stream或者Set获取完整的建议。
其他命令指南
点击这里(译注:已译,见2楼)查看其他命令指南。
命令建议
Brigadier允许指定自定义的参数建议,在Minecraft中,这些建议在玩家输入命令时被发送到客户端。(译注:bukkit的TabCompleter就是基于这个实现的)
建议提供器
SuggestionProvider用来创建一个发送给客户端的建议列表。一个建议提供器是一个接受CommandContext和一个SuggestionBuilder并返回一些Suggestion的函数式接口,因为可能建议没法马上确定,所以SuggestionProvider返回一个CompletableFuture。
建议可以是上下文有关的因为建议提供器允许你访问现在的命令上下文。
一个建议提供器的例子
例如你想要建议所有生物可能有的属性。
- class AttributeSuggestionProvider implements SuggestionProvider<ServerCommandSource> {
- @Override
- public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> context, SuggestionsBuilder builder) throws CommandSyntaxException {
- Identifier entityTypeId = context.getArgument("type", Identifier.class);
- ...
因为我们拥有一个命令上下文,所以我们可以检查在此参数前的任何参数的值。
下面是一些样板代码
- class AttributeSuggestionProvider implements SuggestionProvider<ServerCommandSource> {
- @Override
- public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> context, SuggestionsBuilder builder) {
- Identifier entityTypeId = context.getArgument("type", Identifier.class);
- EntityType<?> entityType = Registry.ENTITY_TYPE.getOrEmpty(entityTypeId).orElse(null);
-
- if (!DefaultAttributeContainer.hasDefinitionFor(entityType)) {
- // TODO: Fail
- }
-
- DefaultAttributeContainer attributeContainer = DefaultAttributeRegistry.get(entityType);
- // You will need mixin to get the 'instances map'. Lets assume we can just access it for the sake of the tutorial
- for (EntityAttribute attribute : attributeContainer.instances().keySet()) {
- Identifier attributeId = Registry.ATTRIBUTE.getId(attribute);
- if (attributeId != null) {
- ...
为了在客户端显示建议,你要像构建器添加建议,可以用suggest方法实现。这些suggest方法一些处理数字和对小提示提供一些支持。
- for (EntityAttribute attribute : attributeContainer.instances().keySet()) {
- Identifier attributeId = Registry.ATTRIBUTE.getId(attribute);
- if (attributeId != null) {
- builder.suggest(attributeId.toString());
- }
- }
你也可以结合用add(SuggestionBuilder)另一个建议构建者的结果。
最后,你要构建建议来返回,使用buildFuture方法完成。
- return builder.buildFuture();
成品:
- class AttributeSuggestionProvider implements SuggestionProvider<ServerCommandSource> {
- @Override
- public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> context, SuggestionsBuilder builder) {
- Identifier entityTypeId = context.getArgument("type", Identifier.class);
- EntityType<?> entityType = Registry.ENTITY_TYPE.getOrEmpty(entityTypeId).orElse(null);
-
- if (!DefaultAttributeContainer.hasDefinitionFor(entityType)) {
- // TODO: Fail
- }
-
- DefaultAttributeContainer attributeContainer = DefaultAttributeRegistry.get(entityType);
- // You will need mixin to get the 'instances map'. Lets assume we can just access it for the sake of the tutorial
- for (EntityAttribute attribute : attributeContainer.instances().keySet()) {
- Identifier attributeId = Registry.ATTRIBUTE.getId(attribute);
- if (attributeId != null) {
- builder.suggest(attributeId.toString());
- }
- }
-
- return builder.buildFuture();
- }
- }
使用建议提供器
现在你有了一个建议提供器,是时候使用它了。注意建议提供器不能对字面量节点使用。
当注册一个参数时,你可以用suggests(SuggestionProvider)设置建议提供器,像下面这样在RequiredArgumentBuilder上应用。
- argument(argumentName, word())
- .suggests(CompletionProviders.suggestedStrings())
- .then(/*Rest of the command*/));
内置建议提供器
Minecraft包含一些内置的建议提供器,如下:
类型 | 字段/方法 |
可生成的实体 | SuggestionProviders.SUMMONABLE_ENTITIES |
可用的声音 | SuggestionProviders.AVAILABLE_SOUNDS |
战利品表 | LootCommand.SUGGESTION_PROVIDER |
生物群系 | SuggestionProviders.ALL_BIOMES |
CommandSource中的工具
CommandSource提供了少量帮助移除创建建议的样板代码的工具。很多工具方法包括返回从ID,位置或匹配的字符串的Stream或者Set获取完整的建议。
其他命令指南
点击这里(译注:已译,见2楼)查看其他命令指南。
本帖最后由 洞穴夜莺 于 2021-3-27 17:58 编辑
访问扩展器
访问扩展器提供一种放宽类,方法或字段的访问限制的方式。访问扩展器比较类似于隔壁Forge的AccessTransformer。
访问扩展器只应该在mixin没有提供类似方法去做时使用。
一般只有2种mixin不够用的情况:
・需要访问一个(包)私有类,特别是用于@Shadow或者在mixin中访问字段或方法。
・需要重写终极方法或者继承终极类。
・在你考虑重写终极方法前先试试用mixin注入终极函数。
・如果你想要继承一个只有私有构造方法的类,扩展器是一个好选择。
依赖
・Fabric-loader >= 0.8.0
・Loom >= 0.2.7
文件格式
一种特殊的文件格式被用来定义你Mod的访问控制改变,最好用.accesswidener后缀来帮助IDE识别。
这个文件必须以下面的内容开始,namespace应该和你的Mod使用的映射表相匹配。通常是named,开发环境生成Mod时会将你的访问扩展器文件映射到intemediary表。如果你使用一个自定义的RemapJarTask,设置remapAccessWidener选项为true来确保这些发生。
复制代码
访问扩展器可以有空行和以#开头的注释
复制代码
任何空白字符都能在文件中用于分隔符,推荐用tab。
类名用 /分割而不是.
对于内部类,使用$而不是/
类
类访问限制可以通过指定访问限制和namespace中指定的映射的类名。
复制代码
1. access可以是accessibale或extendable
方法
方法访问限制可以通过指定访问限制,namespace中指定的映射的类名,方法名,方法描述符来改变。
复制代码
1.access可以是 accessible或extendable
2.className是方法所属的类
3.methodName是方法名
4.methodDesc是方法描述符
字段
方法访问限制可以通过指定访问限制,namespace中指定的映射的类名,方法名,方法描述符来改变。
复制代码
1.access可以是 accessible或mutable
2.className是字段所属的类
3.fieldName是字段名
4.fieldDesc是字段描述符
访问限制改变
Extendable
在你想要继承一个类或重写一个方法时使用Extendable。
・类会变成公开的并且终极关键字会被移除。
・方**变成至少是受保护的并且终极关键字被移除。
对方法使用extendable同时也会对类使用extendable。
Accessible
当你想要从另一个类访问一个类、字段或方法时使用Accessibale
・类会变成公开的
・方**变成公开的,如果这个方法是私有的,还会加上终极关键字
・字段会变成公开的
对方法或字段使用accessibale同时也会对类使用accessible。
Mutable
当你想要改变一个终极字段时使用Mutable。
・字段的终极关键字会被移除
指定文件位置
访问扩展器需要在你的build.gradle和fabric.mod.json中指定。它需要在导出的jar文件中包含所以需要保存在resources里。
复制代码复制代码
访问扩展器
访问扩展器提供一种放宽类,方法或字段的访问限制的方式。访问扩展器比较类似于隔壁Forge的AccessTransformer。
访问扩展器只应该在mixin没有提供类似方法去做时使用。
一般只有2种mixin不够用的情况:
・需要访问一个(包)私有类,特别是用于@Shadow或者在mixin中访问字段或方法。
・需要重写终极方法或者继承终极类。
・在你考虑重写终极方法前先试试用mixin注入终极函数。
・如果你想要继承一个只有私有构造方法的类,扩展器是一个好选择。
依赖
・Fabric-loader >= 0.8.0
・Loom >= 0.2.7
文件格式
一种特殊的文件格式被用来定义你Mod的访问控制改变,最好用.accesswidener后缀来帮助IDE识别。
这个文件必须以下面的内容开始,namespace应该和你的Mod使用的映射表相匹配。通常是named,开发环境生成Mod时会将你的访问扩展器文件映射到intemediary表。如果你使用一个自定义的RemapJarTask,设置remapAccessWidener选项为true来确保这些发生。
- accessWidener v1 <namespace>
访问扩展器可以有空行和以#开头的注释
- # 这样的注释是可以的,放在行尾也一样
任何空白字符都能在文件中用于分隔符,推荐用tab。
类名用 /分割而不是.
对于内部类,使用$而不是/
类
类访问限制可以通过指定访问限制和namespace中指定的映射的类名。
- <access> class <className>
1. access可以是accessibale或extendable
方法
方法访问限制可以通过指定访问限制,namespace中指定的映射的类名,方法名,方法描述符来改变。
- <access> method <className> <methodName> <methodDesc>
1.access可以是 accessible或extendable
2.className是方法所属的类
3.methodName是方法名
4.methodDesc是方法描述符
字段
方法访问限制可以通过指定访问限制,namespace中指定的映射的类名,方法名,方法描述符来改变。
- <access> field <className> <fieldName> <fieldDesc>
1.access可以是 accessible或mutable
2.className是字段所属的类
3.fieldName是字段名
4.fieldDesc是字段描述符
访问限制改变
Extendable
在你想要继承一个类或重写一个方法时使用Extendable。
・类会变成公开的并且终极关键字会被移除。
・方**变成至少是受保护的并且终极关键字被移除。
对方法使用extendable同时也会对类使用extendable。
Accessible
当你想要从另一个类访问一个类、字段或方法时使用Accessibale
・类会变成公开的
・方**变成公开的,如果这个方法是私有的,还会加上终极关键字
・字段会变成公开的
对方法或字段使用accessibale同时也会对类使用accessible。
Mutable
当你想要改变一个终极字段时使用Mutable。
・字段的终极关键字会被移除
指定文件位置
访问扩展器需要在你的build.gradle和fabric.mod.json中指定。它需要在导出的jar文件中包含所以需要保存在resources里。
- minecraft {
- accessWidener = file("src/main/resources/modid.accesswidener")
- }
- ...
- "accessWidener" : "modid.accesswidener",
- ...
注册表系统
你需要在你的游戏中出测大多数内容,这会对以下内容有帮助:
让游戏知道你的内容存在
在服务器和客户端之间验证游戏内容有效性
在存档中处理无效数据
抑制不同模组冲突
双端通信的压缩和数据保存
抽象和隐藏数字ID
当注册任意类型的内容时,你传递一个Identifier参数,它是你添加内容的参数。Identifier(缩写成ID)有一个命名空间和一个路径,在大多数情况下,这个命名空间是你的模组ID,然后路径是要注册内容的明智,例如,标准泥土方块的ID是minecraft:dirt。
使用没有注册的自定义内容可能会导致缺陷行为,例如材质丢失,世界保存问题,还有崩溃。游戏通常会在你忘记注册东西时让你知道。
注册表类型
当你注册内容时,你需要指定你要注册到的注册表,游戏提供的最基础的注册表能在Registry类中找到,两个注册表的例子是用于物品的Registry.ITEM和用于方块的Registry.BLOCK。
阅读注册表类型来获取深入的概述。
注册内容
使用Registry.register向注册表添加内容
复制代码
registry - 一个要添加内容的注册表实例。注册表类型Registry中的原版注册表清单。
entry - 一个你要注册的内容的实例。
注册表方法
get - 返回ID在注册表中对应的内容实例。如果不存在,DefaultedRegistry返回默认内容,SimpleRegistry返回空。
复制代码
getId - 返回注册表中内容实例对应的ID,如果不存在,DefaultedRegistry返回默认ID,SimpleRegistry返回空。
复制代码
getRawId - 返回注册表中内容实例对应的内部数字ID,如果不存在,DefaultedRegistry返回默认内容对应的内部数字ID,SimpleRegistry返回-1。
复制代码
你需要在你的游戏中出测大多数内容,这会对以下内容有帮助:
让游戏知道你的内容存在
在服务器和客户端之间验证游戏内容有效性
在存档中处理无效数据
抑制不同模组冲突
双端通信的压缩和数据保存
抽象和隐藏数字ID
当注册任意类型的内容时,你传递一个Identifier参数,它是你添加内容的参数。Identifier(缩写成ID)有一个命名空间和一个路径,在大多数情况下,这个命名空间是你的模组ID,然后路径是要注册内容的明智,例如,标准泥土方块的ID是minecraft:dirt。
使用没有注册的自定义内容可能会导致缺陷行为,例如材质丢失,世界保存问题,还有崩溃。游戏通常会在你忘记注册东西时让你知道。
注册表类型
当你注册内容时,你需要指定你要注册到的注册表,游戏提供的最基础的注册表能在Registry类中找到,两个注册表的例子是用于物品的Registry.ITEM和用于方块的Registry.BLOCK。
阅读注册表类型来获取深入的概述。
注册内容
使用Registry.register向注册表添加内容
- public static <T> T register(Registry<? super T> registry, Identifier id, T entry) { return ((MutableRegistry)registry).add(id, entry);}
registry - 一个要添加内容的注册表实例。注册表类型Registry中的原版注册表清单。
entry - 一个你要注册的内容的实例。
注册表方法
get - 返回ID在注册表中对应的内容实例。如果不存在,DefaultedRegistry返回默认内容,SimpleRegistry返回空。
- @Nullablepublic abstract T get(@Nullable Identifier id);
getId - 返回注册表中内容实例对应的ID,如果不存在,DefaultedRegistry返回默认ID,SimpleRegistry返回空。
- @Nullablepublic abstract Identifier getId(T entry);
getRawId - 返回注册表中内容实例对应的内部数字ID,如果不存在,DefaultedRegistry返回默认内容对应的内部数字ID,SimpleRegistry返回-1。
- public abstract int getRawId(@Nullable T entry);
okok yes go0d
本帖最后由 洞穴夜莺 于 2021-5-15 13:01 编辑
自定义热键
热键:直接从键盘上
Minecraft用热键处理多数外围设备的用户输入,例如鼠标键盘。当你按下W时向前移动,当你按下E键时打开背包。所有热键可以在控制选项中设置,所以你也可以用方向键代替WASD控制行动。
这个指南假设你有热键API,如果没有就加一个"fabric-key-binding-api-v1":"*"到你的fabric.mods.json的"depends"中去。
加入一个热键很容易,你需要:
这里有一个新的示例。
创建你的热键
在随便什么地方声明一个这样的玩意。
复制代码
FabricKeyBinding有一个Builder用于初始化,它需要一个Identifier,InputUtil.Type,按键编码,和一个分组。
复制代码
如果你想要一个粘性键,最后加一个()->true参数。
GLFW.GLFW_KEY_R可以被任何你想要使用的默认按键。分组会体现在你的设置页面上。
回应你的按键
这段代码会在游戏中输出“Key 1 was pressed!”。(译者注:注意不是isPressed,isPressed返回当前按键是否被按下,wasPressed返回此按键还有是否按下事件未被处理并移除一个未被处理的事件)
复制代码
注意这整个过程发生在客户端,如果你要服务端对热键作出反应,你需要使用自定义网络包并且单独在服务端做处理。
自定义热键
热键:直接从键盘上
Minecraft用热键处理多数外围设备的用户输入,例如鼠标键盘。当你按下W时向前移动,当你按下E键时打开背包。所有热键可以在控制选项中设置,所以你也可以用方向键代替WASD控制行动。
这个指南假设你有热键API,如果没有就加一个"fabric-key-binding-api-v1":"*"到你的fabric.mods.json的"depends"中去。
加入一个热键很容易,你需要:
- 创建一个KeyBinding对象
- 对按键按下作出反应
这里有一个新的示例。
创建你的热键
在随便什么地方声明一个这样的玩意。
- private static KeyBinding keyBinding;
FabricKeyBinding有一个Builder用于初始化,它需要一个Identifier,InputUtil.Type,按键编码,和一个分组。
- keyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
- "key.examplemod.spook", // 热键名的翻译键
- InputUtil.Type.KEYSYM, // 热键种类,键盘用KEYSYM,鼠标用MOUSE
- GLFW.GLFW_KEY_R, // 默认使用的按键的按键编码
- "category.examplemod.test" // 按键分组的翻译键
- ));
如果你想要一个粘性键,最后加一个()->true参数。
GLFW.GLFW_KEY_R可以被任何你想要使用的默认按键。分组会体现在你的设置页面上。
回应你的按键
这段代码会在游戏中输出“Key 1 was pressed!”。(译者注:注意不是isPressed,isPressed返回当前按键是否被按下,wasPressed返回此按键还有是否按下事件未被处理并移除一个未被处理的事件)
- ClientTickEvents.END_CLIENT_TICK.register(client -> {
- while (keyBinding.wasPressed()) {
- client.player.sendMessage(new LiteralText("Key 1 was pressed!"), false);
- }
- });
注意这整个过程发生在客户端,如果你要服务端对热键作出反应,你需要使用自定义网络包并且单独在服务端做处理。
有方向的方块
使方块有方向(朝向确定的方向)也是通过方块状态完成的,这个例子描述了一个竖半砖的例子。

复制代码
定义方块状态
polished_andesite_side_block.json
复制代码
定义方块模型
side.json
复制代码
polished_andesite_side_block.json
复制代码
使方块有方向(朝向确定的方向)也是通过方块状态完成的,这个例子描述了一个竖半砖的例子。

- public class PolishedAndesiteSideBlock extends HorizontalFacingBlock {
-
- public PolishedAndesiteSideBlock(Settings settings) {
- super(settings);
- setDefaultState(this.stateManager.getDefaultState().with(Properties.HORIZONTAL_FACING, Direction.NORTH));
- }
-
- @Override
- protected void appendProperties(StateManager.Builder<Block, BlockState> stateManager) {
- stateManager.add(Properties.HORIZONTAL_FACING);
- }
-
- @Override
- public VoxelShape getOutlineShape(BlockState state, BlockView view, BlockPos pos, EntityContext ctx) {
- Direction dir = state.get(FACING);
- switch(dir) {
- case NORTH:
- return VoxelShapes.cuboid(0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
- case SOUTH:
- return VoxelShapes.cuboid(0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f);
- case EAST:
- return VoxelShapes.cuboid(0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f);
- case WEST:
- return VoxelShapes.cuboid(0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 1.0f);
- default:
- return VoxelShapes.fullCube();
- }
- }
-
- public BlockState getPlacementState(ItemPlacementContext ctx) {
- return (BlockState)this.getDefaultState().with(FACING, ctx.getPlayerFacing());
- }
-
- }
定义方块状态
polished_andesite_side_block.json
- {
- "variants": {
- "facing=north": { "model": "bitmod:block/polished_andesite_side_block" },
- "facing=east": { "model": "bitmod:block/polished_andesite_side_block", "y": 90},
- "facing=south": { "model": "bitmod:block/polished_andesite_side_block", "y": 180 },
- "facing=west": { "model": "bitmod:block/polished_andesite_side_block", "y": 270 }
- }
- }
定义方块模型
side.json
- {
- "parent": "block/block",
- "textures": {
- "particle": "#side"
- },
- "elements": [
- { "from": [ 0, 0, 0 ],
- "to": [ 16, 16, 8 ],
- "faces": {
- "down": { "uv": [ 0, 8, 16, 16 ], "texture": "#bottom", "cullface": "down" },
- "up": { "uv": [ 0, 8, 16, 16 ], "texture": "#top", "cullface": "up" },
- "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#side", "cullface": "north" },
- "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#side" },
- "west": { "texture": "#side", "cullface": "west" },
- "east": { "texture": "#side", "cullface": "east" }
- }
- }
- ]
- }
polished_andesite_side_block.json
- {
- "parent": "bitmod:block/side",
- "textures": {
- "bottom": "block/polished_andesite",
- "top": "block/polished_andesite",
- "side": "block/polished_andesite"
- }
- }
本帖最后由 洞穴夜莺 于 2020-10-7 09:43 编辑
物品
物品是一种出现在物品栏的游戏内容。它们可以在你点击时执行动作,作为食物,或者生成实体。下面的文档会给你一些关于整个Item类的解释和所有有关的信息。你接下来可以阅读创建一个物品(本链接指向蝙蝠译文)。
物品设置
Item的构造函数需要一个Item.Settings的实例,这个构造器类定义了一些像物品堆大小,耐久和是否可食用。下面有一个完整的显示所有可用方法的表:[td][table]
maxDamageIfAbsent int 如果该物品没有设置耐久,则设置该物品的耐久
maxDamage int 设置物品耐久
recipeRemainder Item 设置当玩家在使用该物品合成其他物品时,返还何种物品
group ItemGroup 设置物品的物品组,在创造模式物品栏中体现
rarity Rarity 设置物品的稀有度,在物品名字的颜色中体现
食物
复制代码
foodComponent - FoodComponent的实例。当设置时物品会根据构造器提供的FoodComponent的设定变得可食用。深入解释请阅读FoodComponent页
最大物品堆叠数
复制代码
maxCount - 物品的最大物品堆叠属性。如果maxDamage()已经被调用将抛出RuntimeException,因为物品不可以又有耐久又能堆叠。建议将这个值设置在64及以下,因为更大的值可能带来不确定的问题。
最大耐久如果没有
复制代码
如果maxDamage()没被调用耐久会被设成传递进去的参数。这在一些像工具和护甲之类的场合使用,这里物品的耐久仅在没有被设置时设为工具材料的耐久。
最大耐久
复制代码
maxDamage - 物品的最大耐久
配方返还物品
复制代码
recipeRemainder - 当物品被用作配方的原材料时可以返还的物品。
当给一个物品设置一个配方返还物品时,所有使用它们的配方均会在合成时返还物品,这个设置一般用于桶(水,熔岩,牛奶)和瓶子(龙息、蜂蜜)在用于合成时返回它们对应的空物品。
物品组
复制代码
group - 欲将物品添加到的ItemGroup。
每个ItemGroup以创造模式标签栏的形式出现。向一个物品组添加物品会把它加到标签栏里,排序是注册表序。更多信息参阅物品组页。
稀有度
复制代码
rarity - 物品的稀有度。
如果设置了稀有度,物品会有一个自定义的名字颜色,物品的默认稀有度是common。
↓已修复
物品
物品是一种出现在物品栏的游戏内容。它们可以在你点击时执行动作,作为食物,或者生成实体。下面的文档会给你一些关于整个Item类的解释和所有有关的信息。你接下来可以阅读创建一个物品(本链接指向蝙蝠译文)。
物品设置
Item的构造函数需要一个Item.Settings的实例,这个构造器类定义了一些像物品堆大小,耐久和是否可食用。下面有一个完整的显示所有可用方法的表:[td][table]
方法
参数
描述
food
FoodComponent 基于给出的FoodComponent将食物变得可食用maxCount
int 设置物品的最大堆大小,不能和耐久一起用maxDamageIfAbsent int 如果该物品没有设置耐久,则设置该物品的耐久
maxDamage int 设置物品耐久
recipeRemainder Item 设置当玩家在使用该物品合成其他物品时,返还何种物品
group ItemGroup 设置物品的物品组,在创造模式物品栏中体现
rarity Rarity 设置物品的稀有度,在物品名字的颜色中体现
食物
- public Item.Settings food(FoodComponent foodComponent)
foodComponent - FoodComponent的实例。当设置时物品会根据构造器提供的FoodComponent的设定变得可食用。深入解释请阅读FoodComponent页
最大物品堆叠数
- public Item.Settings maxCount(int maxCount)
maxCount - 物品的最大物品堆叠属性。如果maxDamage()已经被调用将抛出RuntimeException,因为物品不可以又有耐久又能堆叠。建议将这个值设置在64及以下,因为更大的值可能带来不确定的问题。
最大耐久如果没有
- public Item.Settings maxDamageIfAbsent(int maxDamage)
如果maxDamage()没被调用耐久会被设成传递进去的参数。这在一些像工具和护甲之类的场合使用,这里物品的耐久仅在没有被设置时设为工具材料的耐久。
最大耐久
- public Item.Settings maxDamage(int maxDamage)
maxDamage - 物品的最大耐久
配方返还物品
- public Item.Settings recipeRemainder(Item recipeRemainder)
recipeRemainder - 当物品被用作配方的原材料时可以返还的物品。
当给一个物品设置一个配方返还物品时,所有使用它们的配方均会在合成时返还物品,这个设置一般用于桶(水,熔岩,牛奶)和瓶子(龙息、蜂蜜)在用于合成时返回它们对应的空物品。
物品组
- public Item.Settings group(ItemGroup group)
group - 欲将物品添加到的ItemGroup。
每个ItemGroup以创造模式标签栏的形式出现。向一个物品组添加物品会把它加到标签栏里,排序是注册表序。更多信息参阅物品组页。
稀有度
- public Item.Settings rarity(Rarity rarity)
rarity - 物品的稀有度。
如果设置了稀有度,物品会有一个自定义的名字颜色,物品的默认稀有度是common。
稀有度 | 颜色 |
Common | 白 |
Uncommon | 黄 |
Rare | 青 |
Epic | 品红 |
↓已修复
原来放在二楼了,我是说怎么看着这么少233
本帖最后由 洞穴夜莺 于 2021-1-24 10:42 编辑
颜色提供器
想知道树叶和草怎么根据生物群系改变颜色,或者皮甲怎么会有如此多种的颜色?
看看颜色提供器,它根据位置、NBT、方块状态等进行上色。
现有的例子
先来看看哪些原版内容使用颜色提供器。
颜色提供器很强大,但是Mojang坚持对混凝土、羊毛、玻璃等有色方块使用独立贴图。主要用在对生物群系的颜色调整和对纹理的小调整。例如药箭的末端。
颜色提供器的主要概念很简单,你向它们注册一个物品或者方块,当它们的模型被渲染时,颜色提供器给各层进行颜色调整。
颜色提供器允许你访问各层的纹理,这意味着你可以对纹理的各部分独立进行修改,像染色皮甲和药箭那样。这个特征在你想改变几个像素而不是整个纹理是很好用。
记住这玩意是仅客户端的,所以所有内容都需要只在客户端上注册。
注册一个方块颜色提供器
你需要用ColorProviderRegistry来注册一个方块颜色提供器。这个类有一个BLOCK和ITEM的提供器实例可以用来注册。这个注册方法需要一个颜色提供器实例,以及每个要着色的方块的变参。
复制代码
我们这里在说, “嗨,MY_BLOCK应该是0x3495eb色的”,即蓝色。 你有一个BlockState,World和BlockPos做上下文,你可以基于群系或者位置改变颜色。最后的整数是要上的颜色的tintindex。每个方块都会问一遍,但是在这个例子里我们始终回答蓝色。
模型也是很重要的:主要是你需要给所有要上色的部分定义一个tintindex,来看看leaves.json是个简单的例子,它是原版树叶使用的基础模型。下面是一个我们的方块的模型。
复制代码
在这个实例中,我们添加单个tintindex,它会在上文所述的tintIndex参数中出现(tint下标0)。
这是最终结果-注意原版模型使用white_concrete材质:
(夜莺觉得这里好像缺点东西,但是原文确实没了)
[size]注册一个物品颜色提供器
物品很类似,只是提供的上下文不同。你能访问ItemStack而不是方块状态,世界或位置。
复制代码
这个会像方块那样给你的物品染色。
限制
主要问题是物品提供的上下文太少,这是为啥原版草不根据你站立的位置改变物品栏里草的颜色。如果要实现多种颜色的方块(例如混凝土、玻璃、羊毛),建议你简单粗暴给每个颜色一张独立的贴图。
颜色提供器
想知道树叶和草怎么根据生物群系改变颜色,或者皮甲怎么会有如此多种的颜色?
看看颜色提供器,它根据位置、NBT、方块状态等进行上色。
现有的例子
先来看看哪些原版内容使用颜色提供器。
- 草
- 树叶
- 染色皮甲
- 红石线
- 西瓜、甘蔗、荷叶等植物
- 药箭
颜色提供器很强大,但是Mojang坚持对混凝土、羊毛、玻璃等有色方块使用独立贴图。主要用在对生物群系的颜色调整和对纹理的小调整。例如药箭的末端。
颜色提供器的主要概念很简单,你向它们注册一个物品或者方块,当它们的模型被渲染时,颜色提供器给各层进行颜色调整。
颜色提供器允许你访问各层的纹理,这意味着你可以对纹理的各部分独立进行修改,像染色皮甲和药箭那样。这个特征在你想改变几个像素而不是整个纹理是很好用。
记住这玩意是仅客户端的,所以所有内容都需要只在客户端上注册。
注册一个方块颜色提供器
你需要用ColorProviderRegistry来注册一个方块颜色提供器。这个类有一个BLOCK和ITEM的提供器实例可以用来注册。这个注册方法需要一个颜色提供器实例,以及每个要着色的方块的变参。
- ColorProviderRegistry.BLOCK.register((state, view, pos, tintIndex) -> 0x3495eb, MY_BLOCK);
我们这里在说, “嗨,MY_BLOCK应该是0x3495eb色的”,即蓝色。 你有一个BlockState,World和BlockPos做上下文,你可以基于群系或者位置改变颜色。最后的整数是要上的颜色的tintindex。每个方块都会问一遍,但是在这个例子里我们始终回答蓝色。
模型也是很重要的:主要是你需要给所有要上色的部分定义一个tintindex,来看看leaves.json是个简单的例子,它是原版树叶使用的基础模型。下面是一个我们的方块的模型。
- {
- "parent": "block/block",
- "textures": {
- "all": "block/white_concrete",
- "particle": "#all"
- },
- "elements": [
- { "from": [ 0, 0, 0 ],
- "to": [ 16, 16, 16 ],
- "faces": {
- "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "down" },
- "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "up" },
- "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "north" },
- "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "south" },
- "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "west" },
- "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "east" }
- }
- }
- ]
- }
在这个实例中,我们添加单个tintindex,它会在上文所述的tintIndex参数中出现(tint下标0)。
这是最终结果-注意原版模型使用white_concrete材质:
(夜莺觉得这里好像缺点东西,但是原文确实没了)
[size]注册一个物品颜色提供器
物品很类似,只是提供的上下文不同。你能访问ItemStack而不是方块状态,世界或位置。
- ColorProviderRegistry.ITEM.register((stack, tintIndex) -> 0x3495eb, COLORED_ITEM);
这个会像方块那样给你的物品染色。
限制
主要问题是物品提供的上下文太少,这是为啥原版草不根据你站立的位置改变物品栏里草的颜色。如果要实现多种颜色的方块(例如混凝土、玻璃、羊毛),建议你简单粗暴给每个颜色一张独立的贴图。
我饿大是大非
本帖最后由 洞穴夜莺 于 2020-10-31 08:49 编辑
工作台配方
添加一个基础的配方
确保你在看本帖之前添加了一个物品,我们要用到它。
目前,我们的物品可以通过创造模式或者命令获取,我们要给它加一个配方来让生存玩家获得它。这里我们会添加一个工作台配额放。
在resources/data/tutorial/recipes/文件夹下创建一个 fabric_item.json文件(把turtorial改成你的mod id)这是一个 fabric_item的例子
resources/data/tutorial/recipes/fabric_item.json复制代码
详细解释:
type: 这是一个有序配方。
result: 这是一个能合成4个tutorial:fabric_item的配方。count字段是可选的,如果你不写,它就默认是1。
pattern: 一个代表你工作台配方的图案,每个字母代表一个物品,一个空格代表不需要在该位置放置物品,每个字母所代表的物品在key中定义。
key: 每个字母所嗲表的物品是什么。W代表任何带minecraft:logs标签的物品(所有原木)。R特指红石,详细信息看这里(指向bilibili mcwiki)
总之你的物品看起来像这样
简单配方的详细信息看这里(指向bilibili mcwiki)
添加自定义配方
这个type字段可以修改来支持一个自定义的配方[需要补充](夜莺提示:阅读RecipeSerializer和CraftingRecipe这两个类)
工作台配方
添加一个基础的配方
确保你在看本帖之前添加了一个物品,我们要用到它。
目前,我们的物品可以通过创造模式或者命令获取,我们要给它加一个配方来让生存玩家获得它。这里我们会添加一个工作台配额放。
在resources/data/tutorial/recipes/文件夹下创建一个 fabric_item.json文件(把turtorial改成你的mod id)这是一个 fabric_item的例子
resources/data/tutorial/recipes/fabric_item.json
- {
- "type": "minecraft:crafting_shaped",
- "pattern": [
- "WWW",
- "WR ",
- "WWW"
- ],
- "key": {
- "W": {
- "tag": "minecraft:logs"
- },
- "R": {
- "item": "minecraft:redstone"
- }
- },
- "result": {
- "item": "tutorial:fabric_item",
- "count": 4
- }
- }
详细解释:
type: 这是一个有序配方。
result: 这是一个能合成4个tutorial:fabric_item的配方。count字段是可选的,如果你不写,它就默认是1。
pattern: 一个代表你工作台配方的图案,每个字母代表一个物品,一个空格代表不需要在该位置放置物品,每个字母所代表的物品在key中定义。
key: 每个字母所嗲表的物品是什么。W代表任何带minecraft:logs标签的物品(所有原木)。R特指红石,详细信息看这里(指向bilibili mcwiki)
总之你的物品看起来像这样
四个fabric_item的配方 | ||
木板 | 木板 | 木板 |
木板 | 红石 | 空气 |
木板 | 木板 | 木板 |
简单配方的详细信息看这里(指向bilibili mcwiki)
添加自定义配方
这个type字段可以修改来支持一个自定义的配方[需要补充](夜莺提示:阅读RecipeSerializer和CraftingRecipe这两个类)
维度概念
创建一个维度是一个需要一点功夫才能理解的高级概念,很容易就可以创建一个简单的维度,但是深入之后你会被成吨的难以掌握的新类所轰炸。这个指南是对创建维度的主要概念的概述,这里分类和类名密切相关。
维度(Dimension)
这个Dimension类是你的新维度的核心。它处理你维度环境的重要逻辑。玩家可以在那里用床睡觉吗?有世界边界吗?玩家能看见天空吗?它也用来创建你的ChunkGenerator来创建地图。
维度类型(DimensionType)
DimensionType是一个关于你的维度的注册表包装。它有一个用来注册注册表的ID。它也用来从文件加载维度或者保存文件到维度。
区块生成器(ChunkGenerator)
用来根据噪声函数放置方块,它一般不用来生成装饰性和选择具体的方块,大多数情况下你只根据你的选择放置石头和其他基础方块。如果我们此后没有生成装饰以及额外步骤的话,理论上就是完全由石头组成。
生成器类型(GeneratorType)
又是一样的,这是一个ChunkGenerator的注册表封装。但是它不能被正常注册,需要使用一些不合理手段,正在修复此问题。
生物群系(Biome)
生物群系是确定一片区域的样子的一个选择。它用来生成生物、植物、河流湖泊、洞穴、决定草的颜色和别的很多东西。在恰当的设置中,它也用来修改已经生成的石头为其他方块,例如草/泥土和矿石。
生物群系源(BiomeSource)
当创建一个维度时,你可以对所有地方使用同种生物群系或者使用一个生物群系源,生物群系源用来从几种生物群系中随机挑选。
表面构建器(SurfaceBuilder)
一个表面构建器用来改变上面提到的一些石头,每个生物群系都有一个对应的表面构建器。例如,平原&森林都使用DEFAULT SurfaceBuilder,因为它们都想要草方块和泥土在它们上面,它们仅仅因为它们上年的树和地形平坦程度不同----也就是说,一个SurfaceBuilder可以根据情况复用。
创建一个维度是一个需要一点功夫才能理解的高级概念,很容易就可以创建一个简单的维度,但是深入之后你会被成吨的难以掌握的新类所轰炸。这个指南是对创建维度的主要概念的概述,这里分类和类名密切相关。
维度(Dimension)
这个Dimension类是你的新维度的核心。它处理你维度环境的重要逻辑。玩家可以在那里用床睡觉吗?有世界边界吗?玩家能看见天空吗?它也用来创建你的ChunkGenerator来创建地图。
维度类型(DimensionType)
DimensionType是一个关于你的维度的注册表包装。它有一个用来注册注册表的ID。它也用来从文件加载维度或者保存文件到维度。
区块生成器(ChunkGenerator)
用来根据噪声函数放置方块,它一般不用来生成装饰性和选择具体的方块,大多数情况下你只根据你的选择放置石头和其他基础方块。如果我们此后没有生成装饰以及额外步骤的话,理论上就是完全由石头组成。
生成器类型(GeneratorType)
又是一样的,这是一个ChunkGenerator的注册表封装。但是它不能被正常注册,需要使用一些不合理手段,正在修复此问题。
生物群系(Biome)
生物群系是确定一片区域的样子的一个选择。它用来生成生物、植物、河流湖泊、洞穴、决定草的颜色和别的很多东西。在恰当的设置中,它也用来修改已经生成的石头为其他方块,例如草/泥土和矿石。
生物群系源(BiomeSource)
当创建一个维度时,你可以对所有地方使用同种生物群系或者使用一个生物群系源,生物群系源用来从几种生物群系中随机挑选。
表面构建器(SurfaceBuilder)
一个表面构建器用来改变上面提到的一些石头,每个生物群系都有一个对应的表面构建器。例如,平原&森林都使用DEFAULT SurfaceBuilder,因为它们都想要草方块和泥土在它们上面,它们仅仅因为它们上年的树和地形平坦程度不同----也就是说,一个SurfaceBuilder可以根据情况复用。
本帖最后由 洞穴夜莺 于 2020-11-28 18:50 编辑
生成矿石[1.16.3]
许多模组都有它们自己的矿石, 你需要一种方式把它们添加到存在的生物群系中使得玩家可以找到。在这篇指南中,我们会看看怎么像已存在的生物群系中添加矿石,这里有两个添加矿石所必须的步骤。
注意这个API是实验特性,如果他不能用,考虑用Mixin版。
我们假定你已经创建了你的矿石方块,下面将用羊毛作为演示,自己把羊毛改成你的矿石。
向主世界添加
在本章节,我们的目标是在主世界生成我们的矿石。
我们需要创建一个ConfiguredFeature,确保在onInitialize中注册你的ConfiguredFeature,自己根据需要改变这些参数来适合你的Mod。
复制代码
结果
你可以看到矿石已经在主世界生成了,用这个命令移除周围的石头。
复制代码

向下界添加
在本章节,在上面那段代码的基础上,我们给下界添加矿石。
我们把OreFeatureConfig.Rules.BASE_STONE_OVERWORLD 改成OreFeatureConfig.Rules.BASE_STONE_NETHER因为下界生物群系的基础方块和主世界是不同的。
复制代码
向末地添加
在本章节,在主世界那段代码的基础上,我们给末地添加矿石。
我们把OreFeatureConfig.Rules.BASE_STONE_OVERWORLD 改成 new BlockMatchRuleTest(Blocks.END_STONE)因为末地生物群系的基础方块是末地石。
复制代码
生成矿石[1.16.3]
许多模组都有它们自己的矿石, 你需要一种方式把它们添加到存在的生物群系中使得玩家可以找到。在这篇指南中,我们会看看怎么像已存在的生物群系中添加矿石,这里有两个添加矿石所必须的步骤。
- 创建一个ConfiguredFeatures,它定义你的矿石方块在哪里生成。
- 用生物群系修改API来向你的结构添加地物(feature,即装饰性方块生成)。
注意这个API是实验特性,如果他不能用,考虑用Mixin版。
我们假定你已经创建了你的矿石方块,下面将用羊毛作为演示,自己把羊毛改成你的矿石。
向主世界添加
在本章节,我们的目标是在主世界生成我们的矿石。
我们需要创建一个ConfiguredFeature,确保在onInitialize中注册你的ConfiguredFeature,自己根据需要改变这些参数来适合你的Mod。
- public class ExampleMod implements ModInitializer {
- private static ConfiguredFeature<?, ?> ORE_WOOL_OVERWORLD = Feature.ORE
- .configure(new OreFeatureConfig(
- OreFeatureConfig.Rules.BASE_STONE_OVERWORLD,
- Blocks.WHITE_WOOL.getDefaultState(),
- 9)) // 矿脉大小
- .decorate(Decorator.RANGE.configure(new RangeDecoratorConfig(
- 0, // 底部偏移量
- 0, // 最小生成高度
- 64))) // 最大生成高度
- .spreadHorizontally()
- .repeat(20); // 每区块矿脉数
-
- @Override
- public void onInitialize() {
- RegistryKey<ConfiguredFeature<?, ?>> oreWoolOverworld = RegistryKey.of(Registry.CONFIGURED_FEATURE_WORLDGEN,
- new Identifier("tutorial", "ore_wool_overworld"));
- Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, oreWoolOverworld.getValue(), ORE_WOOL_OVERWORLD);
- BiomeModifications.addFeature(BiomeSelectors.foundInOverworld(), GenerationStep.Feature.UNDERGROUND_ORES, oreWoolOverworld);
- }
- }
结果
你可以看到矿石已经在主世界生成了,用这个命令移除周围的石头。
- /fill ~-8 0 ~-8 ~8 ~ ~8 minecraft:air replace minecraft:stone

向下界添加
在本章节,在上面那段代码的基础上,我们给下界添加矿石。
我们把OreFeatureConfig.Rules.BASE_STONE_OVERWORLD 改成OreFeatureConfig.Rules.BASE_STONE_NETHER因为下界生物群系的基础方块和主世界是不同的。
- public class ExampleMod implements ModInitializer {
- private static ConfiguredFeature<?, ?> ORE_WOOL_NETHER = Feature.ORE
- .configure(new OreFeatureConfig(
- OreFeatureConfig.Rules.BASE_STONE_NETHER, // We use OreFeatureConfig.Rules.BASE_STONE_NETHER for nether
- Blocks.WHITE_WOOL.getDefaultState(),
- 9))
- .decorate(Decorator.RANGE.configure(new RangeDecoratorConfig(
- 0,
- 0,
- 64)))
- .spreadHorizontally()
- .repeat(20);
-
- @Override
- public void onInitialize() {
- RegistryKey<ConfiguredFeature<?, ?>> oreWoolNether = RegistryKey.of(Registry.CONFIGURED_FEATURE_WORLDGEN,
- new Identifier("tutorial", "ore_wool_nether"));
- Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, oreWoolNether.getValue(), ORE_WOOL_NETHER);
- BiomeModifications.addFeature(BiomeSelectors.foundInTheNether(), GenerationStep.Feature.UNDERGROUND_ORES, oreWoolNether);
- }
- }
向末地添加
在本章节,在主世界那段代码的基础上,我们给末地添加矿石。
我们把OreFeatureConfig.Rules.BASE_STONE_OVERWORLD 改成 new BlockMatchRuleTest(Blocks.END_STONE)因为末地生物群系的基础方块是末地石。
- public class ExampleMod implements ModInitializer {
- private static ConfiguredFeature<?, ?> ORE_WOOL_END = Feature.ORE
- .configure(new OreFeatureConfig(
- new BlockMatchRuleTest(Blocks.END_STONE), // base block is endstone in the end biomes
- Blocks.WHITE_WOOL.getDefaultState(),
- 9))
- .decorate(Decorator.RANGE.configure(new RangeDecoratorConfig(
- 0,
- 0,
- 64)))
- .spreadHorizontally()
- .repeat(20);
-
- @Override
- public void onInitialize() {
- RegistryKey<ConfiguredFeature<?, ?>> oreWoolEnd = RegistryKey.of(Registry.CONFIGURED_FEATURE_WORLDGEN,
- new Identifier("tutorial", "ore_wool_end"));
- Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, oreWoolEnd.getValue(), ORE_WOOL_END);
- BiomeModifications.addFeature(BiomeSelectors.foundInTheEnd(), GenerationStep.Feature.UNDERGROUND_ORES, oreWoolEnd);
- }
- }
本帖最后由 洞穴夜莺 于 2021-5-15 13:10 编辑
生成地物[1.16.3]
石头,树,矿石和池塘都是地物的例子,它们简单地在世界上根据配置进行额外生成。在这个指南里,我们会尝试随机地生成螺旋状的石头地物。
有上那个添加地物到生物群系的步骤
注意这个API是实验特性,如果不能用,考虑用Mixin版。
创建一个地物
一个简单的地物像这样:
复制代码
这个Feature<DefaultFeatureConfig>构造函数使用一个Codec<DefaultFeatureConfig>,你可以传递DefaultFeatureConfig.CODEC来使用默认配置,或者声明时直接调用父类方法。
当区块决定生成地物的时候会调用generate。如果地物被配置成在每个区块生成,它就会在每个区块上调用一遍。这里要给地物一个合适的在每个生物群系的生成率。generate只在你想要生成的地方被调用。
在我们这个实现中,我们会构建一个15格高螺旋状石头在最顶上的方块上。
地物可以像其他东西一样通过注册表注册
复制代码
配置一个地物
我们要配置一个地物。确保它的配置也被注册。
复制代码
这个装饰器代表世界怎么选择你的地物的位置。查看类似生成风格的原版地物来确定你自己要填啥。装饰器将配置此分支,在CHANCE的情况下,你可以传入一个ChanceDecoratorConfig。
添加一个配置好的地物到生物群系
我们用生物群系API
复制代码
addFeature的第一个参数决定地物生成的生物群系。
第二个参数决定地物何时生成,对于地面上的房屋可以用SURFACE_STRUCTURES,对于洞穴,可以用RAW_GENERATION。
结果
[原文图挂]
生成地物[1.16.3]
石头,树,矿石和池塘都是地物的例子,它们简单地在世界上根据配置进行额外生成。在这个指南里,我们会尝试随机地生成螺旋状的石头地物。
有上那个添加地物到生物群系的步骤
- 创建一个地物
- 配置一个地物
- 用Fabric API生物群系API向生物群系添加地物
注意这个API是实验特性,如果不能用,考虑用Mixin版。
创建一个地物
一个简单的地物像这样:
- public class StoneSpiralFeature extends Feature<DefaultFeatureConfig> {
- public StoneSpiralFeature(Codec<DefaultFeatureConfig> config) {
- super(config);
- }
-
- @Override
- public boolean generate(StructureWorldAccess world, ChunkGenerator generator, Random random, BlockPos pos,
- DefaultFeatureConfig config) {
- BlockPos topPos = world.getTopPosition(Heightmap.Type.WORLD_SURFACE, pos);
- Direction offset = Direction.NORTH;
-
- for (int y = 1; y <= 15; y++) {
- offset = offset.rotateYClockwise();
- world.setBlockState(topPos.up(y).offset(offset), Blocks.STONE.getDefaultState(), 3);
- }
-
- return true;
- }
- }
这个Feature<DefaultFeatureConfig>构造函数使用一个Codec<DefaultFeatureConfig>,你可以传递DefaultFeatureConfig.CODEC来使用默认配置,或者声明时直接调用父类方法。
当区块决定生成地物的时候会调用generate。如果地物被配置成在每个区块生成,它就会在每个区块上调用一遍。这里要给地物一个合适的在每个生物群系的生成率。generate只在你想要生成的地方被调用。
在我们这个实现中,我们会构建一个15格高螺旋状石头在最顶上的方块上。
地物可以像其他东西一样通过注册表注册
- public class ExampleMod implements ModInitializer {
- private static final Feature<DefaultFeatureConfig> STONE_SPIRAL = new StoneSpiralFeature(DefaultFeatureConfig.CODEC);
-
- @Override
- public void onInitialize() {
- Registry.register(Registry.FEATURE, new Identifier("tutorial", "stone_spiral"), STONE_SPIRAL);
- }
- }
配置一个地物
我们要配置一个地物。确保它的配置也被注册。
- public class ExampleMod implements ModInitializer {
- public static final ConfiguredFeature<?, ?> STONE_SPIRAL_CONFIGURED = STONE_SPIRAL.configure(FeatureConfig.DEFAULT)
- .decorate(Decorator.CHANCE.configure(new ChanceDecoratorConfig(5)));
-
- @Override
- public void onInitialize() {
- [...]
-
- RegistryKey<ConfiguredFeature<?, ?>> stoneSpiral = RegistryKey.of(Registry.CONFIGURED_FEATURE_WORLDGEN,
- new Identifier("tutorial", "stone_spiral"));
- Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, stoneSpiral.getValue(), STONE_SPIRAL_CONFIGURED);
- }
- }
这个装饰器代表世界怎么选择你的地物的位置。查看类似生成风格的原版地物来确定你自己要填啥。装饰器将配置此分支,在CHANCE的情况下,你可以传入一个ChanceDecoratorConfig。
添加一个配置好的地物到生物群系
我们用生物群系API
- public class ExampleMod implements ModInitializer {
- [...]
-
- @Override
- public void onInitialize() {
- [...]
- BiomeModifications.addFeature(BiomeSelectors.all(), GenerationStep.Feature.UNDERGROUND_ORES, stoneSpiral);
- }
- }
addFeature的第一个参数决定地物生成的生物群系。
第二个参数决定地物何时生成,对于地面上的房屋可以用SURFACE_STRUCTURES,对于洞穴,可以用RAW_GENERATION。
结果
[原文图挂]
添加生物群系[1.16.3]
向世界添加一个生物群系需要3个步骤
- 创建一个生物群系
- 注册一个生物群系
- 向一个气候区添加生物群系
这个指南中我们会添加一个叫黑曜石大陆的生物群系,它的表面被黑曜石覆盖。
注意这个指南基于被标记为实验性特性的Farbic API中的生物群系API,如果它不能用。考虑用mixin版。
创建一个生物群系
我们用Biome.Builder配置选项来创建生物群系。少写一个选项很可能会让游戏崩溃。建议看一下DefaultBiomeCreator中的原版生物群系作为例子。
- public class ExampleMod implements ModInitializer {
- // SurfaceBuilder定义你的生物群系看起来如何
- // 我们用自定义的表面构建器来使我们的生物群系被黑曜石覆盖
- private static final ConfiguredSurfaceBuilder<TernarySurfaceConfig> OBSIDIAN_SURFACE_BUILDER = SurfaceBuilder.DEFAULT
- .withConfig(new TernarySurfaceConfig(
- Blocks.OBSIDIAN.getDefaultState(),
- Blocks.DIRT.getDefaultState(),
- Blocks.GRAVEL.getDefaultState()));
-
- private static final Biome OBSILAND = createObsiland();
-
- private static Biome createObsiland() {
- // 我们需要定义什么样的生物和地物在这个生物群系生成
- // 比如结构,数目,石头,植物和自定义实体,这些步骤对于不同群系来说大致相同
- // 原版生物群系在DefaultBiomeFeatures中定义这些
-
- SpawnSettings.Builder spawnSettings = new SpawnSettings.Builder();
- DefaultBiomeFeatures.addFarmAnimals(spawnSettings);
- DefaultBiomeFeatures.addMonsters(spawnSettings, 95, 5, 100);
-
- GenerationSettings.Builder generationSettings = new GenerationSettings.Builder();
- generationSettings.surfaceBuilder(OBSIDIAN_SURFACE_BUILDER);
- DefaultBiomeFeatures.addDefaultUndergroundStructures(generationSettings);
- DefaultBiomeFeatures.addLandCarvers(generationSettings);
- DefaultBiomeFeatures.addDefaultLakes(generationSettings);
- DefaultBiomeFeatures.addDungeons(generationSettings);
- DefaultBiomeFeatures.addMineables(generationSettings);
- DefaultBiomeFeatures.addDefaultOres(generationSettings);
- DefaultBiomeFeatures.addDefaultDisks(generationSettings);
- DefaultBiomeFeatures.addSprings(generationSettings);
- DefaultBiomeFeatures.addFrozenTopLayer(generationSettings);
-
- return (new Biome.Builder())
- .precipitation(Biome.Precipitation.RAIN)
- .category(Biome.Category.NONE)
- .depth(0.125F)
- .scale(0.05F)
- .temperature(0.8F)
- .downfall(0.4F)
- .effects((new BiomeEffects.Builder())
- .waterColor(0x3f76e4)
- .waterFogColor(0x050533)
- .fogColor(0xc0d8ff)
- .skyColor(0x77adff)
- .build())
- .spawnSettings(spawnSettings.build())
- .generationSettings(generationSettings.build())
- .build();
- }
- }
注册生物群系
我们在onInitialize中注册生物群系,如果你用自己的表面构造器,也要注册。
- public class ExampleMod implements ModInitializer {
- public static final RegistryKey<Biome> OBSILAND_KEY = RegistryKey.of(Registry.BIOME_KEY, new Identifier("tutorial", "obsiland"));
-
- @Override
- public void onInitialize() {
- Registry.register(BuiltinRegistries.CONFIGURED_SURFACE_BUILDER, new Identifier("tutorial", "obsidian"), OBSIDIAN_SURFACE_BUILDER);
- Registry.register(BuiltinRegistries.BIOME, OBSILAND_KEY.getValue(), OBSILAND);
- }
- }
你也要给它写一个翻译节点在你的en_us.json文件
src/main/resources/assets/modid/lang/en_us.json
- {
- "biome.tutorial.obsiland": "Obsiland"
- }
那么对应中文版就是
src/main/resources/assets/modid/lang/zh_cn.json
- {
- "biome.tutorial.obsiland": "黑曜石大陆"
- }
向气候区添加生物群系
我们要指定生物群系要加到哪个气候区,要加入哪个生物群系,和生物群系的权重(双精度浮点数)。权重是你生物群系被生成的概率。高权重对应高生成概率,相对于其他生物群系的权重。你会想要一个高权重来更加容易测试。在这个指南中,我们会向TEMPERATE和COOL气候添加生物群系。
- public class ExampleMod implements ModInitializer {
- @Override
- public void onInitialize() {
- [...]
-
- OverworldBiomes.addContinentalBiome(OBSILAND_KEY, OverworldClimate.TEMPERATE, 2D);
- OverworldBiomes.addContinentalBiome(OBSILAND_KEY, OverworldClimate.COOL, 2D);
- }
- }
结果
恭喜!你的生物群系应该已经在世界中生成了,你可以用下面这个指令来找到你的生物群系。
- /locatebiome tutorial:obsiland

这目录也太爱 了吧
本帖最后由 洞穴夜莺 于 2021-5-15 13:11 编辑
动态配方注册
动态配方注册是使用代码注册而不是JSON。这种方式可以用于在某个Mod加载时改变你的配方。或者改为使用其它Mod的标签。
我们要有一系列创建我们的配方的JSON对象的api来开始。
复制代码
主Mod文件
首先我们要先检查指定Mod到底安装没有。这样来检查
复制代码
custom_mod是我们要检查的Mod,如果mod加载,我们可以注册
复制代码
RecipeManager Mixin
最后我们要Mixin RecipeManager,这样我们就可以添加我们的配方到minecraft
动态配方注册
动态配方注册是使用代码注册而不是JSON。这种方式可以用于在某个Mod加载时改变你的配方。或者改为使用其它Mod的标签。
我们要有一系列创建我们的配方的JSON对象的api来开始。
- public static JsonObject createShapedRecipeJson(ArrayList<Character> keys, ArrayList<Identifier> items, ArrayList<String> type, ArrayList<String> pattern, Identifier output) {
- //创建一个用来保存配方的json对象
- JsonObject json = new JsonObject();
- //我们所需配方的"type",它是有序配方
- json.addProperty("type", "minecraft:crafting_shaped");
- //这相当于json
- //"type": "minecraft:crafting_shaped"
-
- //这里用来保存图案
- JsonArray jsonArray = new JsonArray();
- jsonArray.add(pattern.get(0));
- jsonArray.add(pattern.get(1));
- jsonArray.add(pattern.get(2));
- //然后向配方对象添加图案
- json.add("pattern", jsonArray);
- //相当于
- //"pattern": [
- // "###",
- // " | ",
- // " | "
- //]
-
- //然后我们要定义图案中的每一个键对应的物品,我们要给每个键定义一个JsonObject,然后要给一个主JsonObject对象添加
- JsonObject individualKey; //每个键
- JsonObject keyList = new JsonObject(); //包含所有键对象的主对象
-
- for (int i = 0; i < keys.size(); ++i) {
- individualKey = new JsonObject();
- individualKey.addProperty(type.get(i), items.get(i).toString()); //这会以 "type": "input"格式创建对象,其中type是item或者tag,input是我们输入的东西
- keyList.add(keys.get(i) + "", individualKey); //我们向主对象添加这个键
- //这就是
- //"#": { "tag": "c:copper_ingots" }
- //然后
- //"|": { "item": "minecraft:sticks" }
- //以此类推
- }
-
- json.add("key", keyList);
- //然后我们有了
- //"key": {
- // "#": {
- // "tag": "c:copper_ingots"
- // },
- // "|": {
- // "item": "minecraft:stick"
- // }
- //},
-
- //最终我们要完成json定义
- JsonObject result = new JsonObject();
- result.addProperty("item", output.toString());
- result.addProperty("count", 1);
- json.add("result", result);
- //就是
- //"result": {
- // "item": "modid:copper_pickaxe",
- // "count": 1
- //}
-
- return json;
- }
主Mod文件
首先我们要先检查指定Mod到底安装没有。这样来检查
- FabricLoader.getInstance().isModLoaded("custom_mod");
custom_mod是我们要检查的Mod,如果mod加载,我们可以注册
- public class ExampleMod implements ModInitializer {
-
- public static JsonObject COPPER_PICKAXE_RECIPE = null;
-
- @Override
- public void onInitialize() {
- if (FabricLoader.getInstance().isModLoaded("custom_mod")) {
- COPPER_PICKAXE_RECIPE = createShapedRecipeJson(
- Lists.newArrayList(
- '#',
- '|'
- ), //这两个键代表两种物品
- Lists.newArrayList(new Identifier("c", "copper_ingots"), new Identifier("stick")), //两个键代表的合成用到的tag/item
- Lists.newArrayList("tag", "item"), //我们的输入是tag还是item
- Lists.newArrayList(
- "###",
- " | ",
- " | "
- ), //配方图案
- new Identifier("examplemod:copper_pickaxe") //输出的物品
- );
- }
- }
-
- //之前那个函数
- public static JsonObject createShapedRecipeJson(...) {
- [...]
- }
-
- }
RecipeManager Mixin
最后我们要Mixin RecipeManager,这样我们就可以添加我们的配方到minecraft
动态模型生成
动态模型主要用户给生成的物品或者方块添加模型, 或者你想要自动大量物品或者方块时。像之前一样,我们先注册一个新物品。
复制代码
然后我们需要一个创建模型json的函数。
复制代码
注册生成的模型
最后,我们要注入模型到Minecraft,我们通过mixin ModelLoader#loadModelFromJson来实现。
复制代码
动态模型主要用户给生成的物品或者方块添加模型, 或者你想要自动大量物品或者方块时。像之前一样,我们先注册一个新物品。
- public class ExampleMod implements ModInitializer {
-
- public static final Item EXAMPLE_ITEM = new Item(new Item.Settings());
-
- @Override
- public void onInitialize() {
- Registry.register(Registry.ITEM, new Identifier("example_mod", "example_item"), EXAMPLE_ITEM);
- }
- }
然后我们需要一个创建模型json的函数。
- public static String createItemModelJson(String id, String type) {
- if ("generated".equals(type) || "handheld".equals(type) {
- //物品的两种类型,"handheld"多用于工具和工具类似物,"generated"用于所有别的
- return "{\n" +
- " "parent": "item/" + type + "",\n" +
- " "textures": {\n" +
- " "layer0": "example_mod:item/" + id + ""\n" +
- " }\n" +
- "}";
- } else if ("block".equals(type)) {
- //如果物品是一个BlockItem,它会有和前面两种不同的模型
- return "{\n" +
- " "parent": "example_mod:block/" + id + ""\n"
- "}";
- }
- else {
- //如果type未知,直接返回空json
- return "";
- }
- }
注册生成的模型
最后,我们要注入模型到Minecraft,我们通过mixin ModelLoader#loadModelFromJson来实现。
- @Mixin(ModelLoader.class)
- public class ModelLoaderMixin {
-
- @Inject(method = "loadModelFromJson", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourceManager;getResource(Lnet/minecraft/util/Identifier;)Lnet/minecraft/resource/Resource;"), cancellable = true)
- public void loadModelFromJson(Identifier id, CallbackInfoReturnable<JsonUnbakedModel> cir) {
- //首先我们检查我们的物品是不是我们的模组注册的
- if (!"example_mod".equals(id.getNamespace())) return;
- //我们使用各种方法测试物品是哪种
- //有很多方法可以检查,例如把它们放进一个集合或者列表里面
- //在这个指南中,我们只有一个物品,所以我们总是使用默认的"generated"
- String modelJson = ExampleMod.createItemModelJson(id, "generated");
- if ("".equals(modelJson)) return;
- //在继续之前检查我们的json字符串是否是合法的
- JsonUnbakedModel model = JsonUnbakedModel.deserialize(modelJson);
- model.id = id.toString();
- cir.setReturnValue(model);
- cir.cancel();
- }
- }
建议把自己发的楼层全部置顶
看着看着中间夹着一个水贴就很难受
看着看着中间夹着一个水贴就很难受
本帖最后由 洞穴夜莺 于 2021-5-15 13:13 编辑
添加状态效果
你需要这样来向你的模组添加状态效果:
创建自定义的状态效果类
我们添加一个每秒给你经验的状态效果
复制代码
注册状态效果
如下代码注册状态效果。
复制代码
添加翻译和材质
你需要添加一个你的状态效果的翻译。来给你的语言文件添加一个新的节点:
复制代码
你还需要添加一个材质。路径是:
复制代码
测试
可以运行/effect give @p tutorial:exp来测试你的状态效果
添加状态效果
你需要这样来向你的模组添加状态效果:
- 创建一个继承StatusEffect的类
- 注册你的状态效果
- 添加自定义的功能
- 添加你的状态效果的翻译和材质
创建自定义的状态效果类
我们添加一个每秒给你经验的状态效果
- public class ExpStatusEffect extends StatusEffect {
- public ExpStatusEffect() {
- super(
- StatusEffectType.BENEFICIAL, // 对实体有利还是有害
- 0x98D982); // RGB格式的颜色
- }
-
- // 这个方法每刻都被调用来确定它能否应用在实体上
- @Override
- public boolean canApplyUpdateEffect(int duration, int amplifier) {
- // 在这个情况下,我们只返回真来让它每刻都应用在实体上
- return true;
- }
-
- // 当实体被应用状态效果时,我们在这里实现自定义功能
- @Override
- public void applyUpdateEffect(LivingEntity entity, int amplifier) {
- if (entity instanceof PlayerEntity) {
- ((PlayerEntity) entity).addExperience(1 << amplifier); // 高等级给你更多经验
- }
- }
- }
注册状态效果
如下代码注册状态效果。
- public class ExampleMod implements ModInitializer {
- public static final StatusEffect EXP = new ExpStatusEffect();
-
- @Override
- public void onInitialize() {
- Registry.register(Registry.STATUS_EFFECT, new Identifier("tutorial", "exp"), EXP);
- }
- }
添加翻译和材质
你需要添加一个你的状态效果的翻译。来给你的语言文件添加一个新的节点:
- {
- "effect.tutorial.exp": "Experience"
- }
你还需要添加一个材质。路径是:
- .../resources/assets/tutorial/textures/mob_effect/exp.png
测试
可以运行/effect give @p tutorial:exp来测试你的状态效果
添加附魔
你需要做如下步骤来添加附魔到你的模组:
附魔可以通过独立实现来完成自定义功能,也可以使用已有的合适机制(比如DamageEnchantment),基础的Enchantment也有几个方法来实现功能,比如“敌人攻击”方法。
创建附魔类
我们这里的新附魔叫做Frost,它使怪物在被攻击时减速。缓慢效果,等级和时间会于附魔等级正相关。在我们的附魔类中,我们使用UNCOMMON作为附魔的稀有度,WEAPON作为附魔的目标,并且MAINHAND作为唯一哟中有效工具类型。
复制代码
我们现在重写少量的基础方法来实现基本功能。
getMinPower和能在附魔台上看见的最小等级有关,但不完全是1:1的比例,大多数附魔返回level * 10之类的东西,这和不同的放大倍数和附魔稀有度有关。我们返回1这样我们总是能看见附魔。注意最大经验默认是min(level) + 5,它代表附魔只会在很低等级的附魔台上出现,你需要自己处理你的附魔属性并且查看相似的附魔来选择合适的数字。
复制代码
getMaxLevel是附魔的最高等级。锋利有5级。
复制代码
最后,我们实现我们的缓慢效果于onTargetDamage方法中,它在你用你的附魔工具袭击敌人的时候。
复制代码
当被袭击的生物可以有状态效果时(LivingEntity可以,但Entity不行),给她缓慢效果,时间是2秒每级,效果等同于等级
注册附魔
注册附魔需要如下常见的步骤
复制代码
这个会以tutorial:frost注册我们的附魔,所有非宝藏附魔都会出现在附魔台,包括你注册的这个。
添加翻译&测试
你要添加翻译,添加如下内容到你的语言文件里面:
复制代码
如果你加入游戏
注:
1)当你注册附魔时,对应的附魔书会自动在游戏中添加。附魔的翻译名会显示在书上。
1)有多余一个等级的附魔会有罗马数字跟在附魔名称后面,如果附魔只有一个等级,啥都不会添加。
你需要做如下步骤来添加附魔到你的模组:
- 创建一个继承Enchantment的类或者继承其他Enchantment的子类(例如DamageEnchantment)
- 注册你的附魔
- 给你的附魔添加自定义的功能和机制
- 给你的附魔添加翻译
附魔可以通过独立实现来完成自定义功能,也可以使用已有的合适机制(比如DamageEnchantment),基础的Enchantment也有几个方法来实现功能,比如“敌人攻击”方法。
创建附魔类
我们这里的新附魔叫做Frost,它使怪物在被攻击时减速。缓慢效果,等级和时间会于附魔等级正相关。在我们的附魔类中,我们使用UNCOMMON作为附魔的稀有度,WEAPON作为附魔的目标,并且MAINHAND作为唯一哟中有效工具类型。
- public class FrostEnchantment extends Enchantment {
-
- public FrostEnchantment() {
- super(Enchantment.Rarity.UNCOMMON, EnchantmentTarget.WEAPON, new EquipmentSlot[] {EquipmentSlot.MAINHAND});
- }
- }
我们现在重写少量的基础方法来实现基本功能。
getMinPower和能在附魔台上看见的最小等级有关,但不完全是1:1的比例,大多数附魔返回level * 10之类的东西,这和不同的放大倍数和附魔稀有度有关。我们返回1这样我们总是能看见附魔。注意最大经验默认是min(level) + 5,它代表附魔只会在很低等级的附魔台上出现,你需要自己处理你的附魔属性并且查看相似的附魔来选择合适的数字。
- @Override
- public int getMinPower(int level) {
- return 1;
- }
getMaxLevel是附魔的最高等级。锋利有5级。
- @Override
- public int getMaxLevel() {
- return 3;
- }
最后,我们实现我们的缓慢效果于onTargetDamage方法中,它在你用你的附魔工具袭击敌人的时候。
- @Override
- public void onTargetDamaged(LivingEntity user, Entity target, int level) {
- if(target instanceof LivingEntity) {
- ((LivingEntity) target).addStatusEffect(new StatusEffectInstance(StatusEffects.SLOWNESS, 20 * 2 * level, level - 1));
- }
-
- super.onTargetDamaged(user, target, level);
- }
当被袭击的生物可以有状态效果时(LivingEntity可以,但Entity不行),给她缓慢效果,时间是2秒每级,效果等同于等级
注册附魔
注册附魔需要如下常见的步骤
- public class EnchantingExample implements ModInitializer {
-
- private static Enchantment FROST = Registry.register(
- Registry.ENCHANTMENT,
- new Identifier("tutorial", "frost"),
- new FrostEnchantment()
- );
-
- @Override
- public void onInitialize() {
-
- }
- }
这个会以tutorial:frost注册我们的附魔,所有非宝藏附魔都会出现在附魔台,包括你注册的这个。
添加翻译&测试
你要添加翻译,添加如下内容到你的语言文件里面:
- {
- "enchantment.tutorial.frost": "Frost"
- }
如果你加入游戏

注:
1)当你注册附魔时,对应的附魔书会自动在游戏中添加。附魔的翻译名会显示在书上。
1)有多余一个等级的附魔会有罗马数字跟在附魔名称后面,如果附魔只有一个等级,啥都不会添加。
本帖最后由 洞穴夜莺 于 2021-5-15 13:16 编辑
创建流体
概述
这里我们的内容会覆盖创建一个自定义流体(如果你打算创建好多流体,建议你弄个抽象类,这样你可以设置一些默认值,它们可以被子类所共享)我们还会让它在世界中像湖一样生成。
创建一个抽象流体
原版流体继承net.minecraft.fluid.FlowableFluid,那我们也这样干。
复制代码
实现
现在我们创建一个实际的流体,它有静止的和流动的两个变种。在这个指南里,我们叫它Acid(酸)缺失的引用会在段时间内填补。
复制代码
接下来,我们创建静止的和流动的酸的静态实例变量,和一个酸桶,在你的ModInitalizer里
复制代码
你小于要将它变加到一个合适的标签里来让它像水或熔岩一样工作,对于水来说,创建一个data/minecraft/tags/fluids/water.json文件并把你的流体id放在这里:
复制代码
创建一个流体方块
接下来我们创建一个代表酸的方块。
我们要用到net.minecraft.block.FluidBlock,但是由于他的构造函数是protected的,我们不能直接构造它,使用它的方法包括子类或者匿名子类,这里会稍后展示。在你的ModInitiazer中:
复制代码
现在我们有这些静态对象,我们可以回到AcidFluid并填充这些被重写的方法:
复制代码
开始渲染
你需要注册一个FluidRenderHandler因为你的流体有材质或者需要上色,这里,我们复用水的材质并仅仅改变颜色。为了让材质透明,你可以用Fabric的BlockRenderLayerMap
复制代码
如果你要你自己的流体材质,你可以参考原版的材质[1]
在世界中生成
为了让酸的湖泊在世界中生成,你创建一个net.minecraft.world.gen.feature.LakeFeature,并在你想要的群系中添加它:
复制代码
注:
1)assets/minecraft/blockstates/water.json
assets/minecraft/models/block/water.json
assets/minecraft/textures/block/water_still.png
assets/minecraft/textures/block/water_still.png.mcmeta
assets/minecraft/textures/block/water_flow.png
assets/minecraft/textures/block/water_flow.png.mcmeta
创建流体
概述
这里我们的内容会覆盖创建一个自定义流体(如果你打算创建好多流体,建议你弄个抽象类,这样你可以设置一些默认值,它们可以被子类所共享)我们还会让它在世界中像湖一样生成。
创建一个抽象流体
原版流体继承net.minecraft.fluid.FlowableFluid,那我们也这样干。
- public abstract class TutorialFluid extends FlowableFluid
- {
- /**
- * @return 给出的流体是否是这个流体的实例?
- */
- @Override
- public boolean matchesType(Fluid fluid)
- {
- return fluid == getStill() || fluid == getFlowing();
- }
-
- /**
- * @return 无限水?
- */
- @Override
- protected boolean isInfinite()
- {
- return false;
- }
-
- /**
- * 在流体流入可以破坏的方块时的动作,水掉落物品,熔岩播放block.lava.extinguish
- */
- @Override
- protected void beforeBreakingBlock(WorldAccess world, BlockPos pos, BlockState state)
- {
- final BlockEntity blockEntity = state.getBlock().hasBlockEntity() ? world.getBlockEntity(pos) : null;
- Block.dropStacks(state, world, pos, blockEntity);
- }
-
- /**
- * 岩浆返回true如果流入的是水且水够高
- *
- * @return 给出的流体是否能流入这个流体
- */
- @Override
- protected boolean canBeReplacedWith(FluidState fluidState, BlockView blockView, BlockPos blockPos, Fluid fluid, Direction direction)
- {
- return false;
- }
-
- /**
- * 或许和检查周围坑的距离有关?
- * 水返回4,熔岩在下界返回4,在主世界返回2
- */
- @Override
- protected int getFlowSpeed(WorldView worldView)
- {
- return 4;
- }
-
- /**
- * 水返回1,熔岩在下界返回1,在主世界返回2
- */
- @Override
- protected int getLevelDecreasePerBlock(WorldView worldView)
- {
- return 1;
- }
-
- /**
- * 水返回5,熔岩在下界返回10,在主世界返回30
- */
- @Override
- public int getTickRate(WorldView worldView)
- {
- return 5;
- }
-
- /**
- * 水和岩浆都返回100.0f
- */
- @Override
- protected float getBlastResistance()
- {
- return 100.0F;
- }
- }
实现
现在我们创建一个实际的流体,它有静止的和流动的两个变种。在这个指南里,我们叫它Acid(酸)缺失的引用会在段时间内填补。
- public abstract class AcidFluid extends TutorialFluid
- {
- @Override
- public Fluid getStill()
- {
- return <YOUR_STILL_FLUID_HERE>;
- }
-
- @Override
- public Fluid getFlowing()
- {
- return <YOUR_FLOWING_FLUID_HERE>;
- }
-
- @Override
- public Item getBucketItem()
- {
- return <YOUR_BUCKET_ITEM_HERE>;
- }
-
- @Override
- protected BlockState toBlockState(FluidState fluidState)
- {
- // method_15741 将LEVEL_1_8转换成我们用的LEVEL_15
- return <YOUR_FLUID_BLOCK_HERE>.getDefaultState().with(Properties.LEVEL_15, method_15741(fluidState));
- }
-
- public static class Flowing extends AcidFluid
- {
- @Override
- protected void appendProperties(StateManager.Builder<Fluid, FluidState> builder)
- {
- super.appendProperties(builder);
- builder.add(LEVEL);
- }
-
- @Override
- public int getLevel(FluidState fluidState)
- {
- return fluidState.get(LEVEL);
- }
-
- @Override
- public boolean isStill(FluidState fluidState)
- {
- return false;
- }
- }
-
- public static class Still extends AcidFluid
- {
- @Override
- public int getLevel(FluidState fluidState)
- {
- return 8;
- }
-
- @Override
- public boolean isStill(FluidState fluidState)
- {
- return true;
- }
- }
- }
接下来,我们创建静止的和流动的酸的静态实例变量,和一个酸桶,在你的ModInitalizer里
- // ...
-
- public static FlowableFluid STILL_ACID;
- public static FlowableFluid FLOWING_ACID;
-
- public static Item ACID_BUCKET;
-
- // ...
-
- @Override
- public void onInitialize()
- {
- // ...
-
- STILL_ACID = Registry.register(Registry.FLUID, new Identifier(MOD_ID, "acid"), new AcidFluid.Still());
-
- FLOWING_ACID = Registry.register(Registry.FLUID, new Identifier(MOD_ID, "flowing_acid"), new AcidFluid.Flowing());
-
- ACID_BUCKET = Registry.register(Registry.ITEM, new Identifier(MOD_ID, "acid_bucket"), new BucketItem(STILL_ACID, new Item.Settings().recipeRemainder(Items.BUCKET).maxCount(1)));
-
- // ...
- }
-
- // ...
你小于要将它变加到一个合适的标签里来让它像水或熔岩一样工作,对于水来说,创建一个data/minecraft/tags/fluids/water.json文件并把你的流体id放在这里:
- {
- "replace": false,
- "values":
- [
- "your_mod_id:acid",
- "your_mod_id:flowing_acid"
- ]
- }
创建一个流体方块
接下来我们创建一个代表酸的方块。
我们要用到net.minecraft.block.FluidBlock,但是由于他的构造函数是protected的,我们不能直接构造它,使用它的方法包括子类或者匿名子类,这里会稍后展示。在你的ModInitiazer中:
- // ...
-
- public static Block ACID;
-
- // ...
-
- @Override
- public void onInitialize()
- {
- // ...
-
- ACID = Registry.register(Registry.BLOCK, new Identifier(MOD_ID, "acid"), new FluidBlock(STILL_ACID, FabricBlockSettings.copy(Blocks.WATER)){});
-
- // ...
- }
-
- // ...
现在我们有这些静态对象,我们可以回到AcidFluid并填充这些被重写的方法:
- public abstract class AcidFluid extends TutorialFluid
- {
- @Override
- public Fluid getStill()
- {
- return TutorialMod.STILL_ACID;
- }
-
- @Override
- public Fluid getFlowing()
- {
- return TutorialMod.FLOWING_ACID;
- }
-
- @Override
- public Item getBucketItem()
- {
- return TutorialMod.ACID_BUCKET;
- }
-
- @Override
- protected BlockState toBlockState(FluidState fluidState)
- {
- // method_15741 将LEVEL_1_8转换成我们用的LEVEL_15
- return TutorialMod.ACID.getDefaultState().with(Properties.LEVEL_15, method_15741(fluidState));
- }
-
- // ...
- }
开始渲染
你需要注册一个FluidRenderHandler因为你的流体有材质或者需要上色,这里,我们复用水的材质并仅仅改变颜色。为了让材质透明,你可以用Fabric的BlockRenderLayerMap
- public class TutorialModClient implements ClientModInitializer
- {
- // ...
-
- @Override
- public void onInitializeClient()
- {
- // ...
-
- setupFluidRendering(TutorialMod.STILL_ACID, TutorialMod.FLOWING_ACID, new Identifier("minecraft", "water"), 0x4CC248);
- BlockRenderLayerMap.INSTANCE.putFluids(RenderLayer.getTranslucent(), TutorialMod.STILL_ACID, TutorialMod.FLOWING_ACID);
-
- // ...
- }
-
- // ...
-
- public static void setupFluidRendering(final Fluid still, final Fluid flowing, final Identifier textureFluidId, final int color)
- {
- final Identifier stillSpriteId = new Identifier(textureFluidId.getNamespace(), "block/" + textureFluidId.getPath() + "_still");
- final Identifier flowingSpriteId = new Identifier(textureFluidId.getNamespace(), "block/" + textureFluidId.getPath() + "_flow");
-
- // 如果他们没就绪,那就把方块贴图添加到图集(夜莺:原文If they're not already present, add the sprites to the block atlas,然而我没有看明白they指代什么,求翻译建议)
- ClientSpriteRegistryCallback.event(SpriteAtlasTexture.BLOCK_ATLAS_TEX).register((atlasTexture, registry) ->
- {
- registry.register(stillSpriteId);
- registry.register(flowingSpriteId);
- });
-
- final Identifier fluidId = Registry.FLUID.getId(still);
- final Identifier listenerId = new Identifier(fluidId.getNamespace(), fluidId.getPath() + "_reload_listener");
-
- final Sprite[] fluidSprites = { null, null };
-
- ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new SimpleSynchronousResourceReloadListener()
- {
- @Override
- public Identifier getFabricId()
- {
- return listenerId;
- }
-
- /**
- * 材质加载完毕之后从方块图集中获取贴图
- */
- @Override
- public void apply(ResourceManager resourceManager)
- {
- final Function<Identifier, Sprite> atlas = MinecraftClient.getInstance().getSpriteAtlas(SpriteAtlasTexture.BLOCK_ATLAS_TEX);
- fluidSprites[0] = atlas.apply(stillSpriteId);
- fluidSprites[1] = atlas.apply(flowingSpriteId);
- }
- });
-
- // FluidRenderer 渲染时从 FluidRenderHandler 获取贴图和颜色
- final FluidRenderHandler renderHandler = new FluidRenderHandler()
- {
- @Override
- public Sprite[] getFluidSprites(BlockRenderView view, BlockPos pos, FluidState state)
- {
- return fluidSprites;
- }
-
- @Override
- public int getFluidColor(BlockRenderView view, BlockPos pos, FluidState state)
- {
- return color;
- }
- };
-
- FluidRenderHandlerRegistry.INSTANCE.register(still, renderHandler);
- FluidRenderHandlerRegistry.INSTANCE.register(flowing, renderHandler);
- }
-
- // ...
- }
如果你要你自己的流体材质,你可以参考原版的材质[1]
在世界中生成
为了让酸的湖泊在世界中生成,你创建一个net.minecraft.world.gen.feature.LakeFeature,并在你想要的群系中添加它:
- // ...
-
- public static LakeFeature ACID_LAKE;
-
- // ...
-
- @Override
- public void onInitialize()
- {
- // ...
-
- ACID_LAKE = Registry.register(Registry.FEATURE, new Identifier(MOD_ID, "acid_lake"), new LakeFeature(SingleStateFeatureConfig::deserialize));
-
- // 在沼泽中像湖泊那样生成,但权重有40% (数值越大,生成概率越小)
- Biomes.SWAMP.addFeature(
- GenerationStep.Feature.LOCAL_MODIFICATIONS,
- ACID_LAKE.configure(new SingleStateFeatureConfig(ACID.getDefaultState()))
- .createDecoratedFeature(Decorator.WATER_LAKE.configure(new ChanceDecoratorConfig(40)))
- );
-
- // ...
- }
注:
1)assets/minecraft/blockstates/water.json
assets/minecraft/models/block/water.json
assets/minecraft/textures/block/water_still.png
assets/minecraft/textures/block/water_still.png.mcmeta
assets/minecraft/textures/block/water_flow.png
assets/minecraft/textures/block/water_flow.png.mcmeta
标准注册表
Minecraft有很多种对象的注册表,比如方块物品或者实体,下面是原版的注册表。
通用注册表
下面这些注册表对很多模组有用。
- Registry.REGISTRIES
包含所有的注册表本身。 - Registry.BLOCK
包含所有的方块。 - Registry.ITEM
包含所有的物品(包括方块物品)。 - Registry.BLOCK_ENTITY_TYPE
包含每种方块实体的BlockEntityType,它们用来反序列化方块实体并保存兼容的方块。 - Registry.STATUS_EFFECT
包含所有的状态效果,例如夜视和隐身。 - Registry.PARTICLE_TYPE
包含所有的粒子。 - Registry.FLUID
包含所有的流体。 - Registry.ENCHANTMENT
包含所有附魔。 - Registry.POTION
包含所有不同的药水类型,例如“加长版夜视药水”,“水瓶”,“幸运药水”。 - Registry.DIMENSION_TYPE
包含所有维度类型。 - Registry.SCREEN_HANDLER
包含所有的屏幕处理程序。它们用来在客户端和服务端之间同步GUI的状态。 - Registry.RECIPE_TYPE
包含所有配方的种类。 - Registry.RECIPE_SERIALIZER
包含所有的配方反序列化器,它们用来加载配方,可以对同一种类型的配方有多个反序列器(例如有序和无序都是工作台配方)。 - Registry.PAINTING_MOTIVE
包含所有的画的图片,或者其他画的类型。 - Registry.SOUND_EVENT
包含所有不同的声音事件,比如entity.item.pickup。 - Registry.STAT
包含所有类型的统计信息。统计使用一个泛型<T>,来获取不同的T值对应的数据。
不依赖于外部对象的统计信息(例:walk_one_cm)使用custom统计信息类型,它使用Identifier作为T
非custom类型的统计信息依赖于特定的方块或物品类型,例如Stats.MINED - Registry.CUSTOM_STAT
包含所有的“custom统计信息”,它们用来给那些不依赖于外部对象的统计信息。
实体
除了ENTITY_TYPE以外的注册表都和实体AI有关。
- Registry.ENTITY_TYPE
包含每种实体的EntityType,像BLOCK_ENETITY_TYPE那样,实体类型也被用来同步数据和反序列化。 - Registry.SCHEDULE
包含实体的时间表(通常是给村民设置的),时间表用来基于一天中的事件决定它们的行为。 - Registry.ACTIVITY
控制实体的活动(通常是村民)。像play或者work那样的活动控制实体的行为。 - Registry.SENSOR_TYPE
控制实体的敏感物,敏感物允许实体对不同的物品敏感并将它们记住。
和上面两个一样,这个也是给村民用的 - Registry.MEMORY_MODULE_TYPE
包含所有的记忆模块。 - Registry.VILLAGER_TYPE
包含每种生物群系的村民类型。 - Registry.VILLAGER_PROFESSION
包含所有的村民职业。 - Registry.POINT_OF_INTEREST_TYPE
包含所有生物的兴趣点。兴趣点让实体在世界中寻找不同的方块,例如村民寻**站点。这个机制也被用来寻找下界传送门。
世界生成
Minecraft的世界生成很复杂,有一大堆注册表是关于世界生成的。
- Registry.BIOME
包含所有的生物群系。 - Registry.FEATURE
包含所有的世界生成地物。 - Registry.STRUCTURE_FEATURE
包含所有的结构地物。 - Registry.DECORATOR
包含所有的装饰性生成器,它们用来放置地物。 - Registry.BIOME_SOURCE_TYPE
包含所有的生物群系源类型,它们用来决定在世界生成期间生物群系的位置。 - Registry.TREE_DECORATOR_TYPE
包含所有的树修饰器类型,这个可以用来添加像蜂巢之类的方块到树上。 - Registry.FOLIAGE_PLACER_TYPE
包含所有的树放置器类型,它们用来生成树叶。 - Registry.BLOCK_STATE_PROVIDER_TYPE
包含全部的方块状态提供者类型,它们用来基于一个位置可能随机地放置方块。 - Registry.BLOCK_PLACER_TYPE
包含所有的方块放置器类型,它们一般用于像RandomPatchFeature那样的地物中放置方块。 - Registry.CHUNK_GENERATOR_TYPE
包含所有的区块生成器类型。区块生成器定义地形的基本形状,比如surface的形状类似主世界,caves的形状类似下界,而floating_islands类似末地。 - Registry.CHUNK_STATUS
包含所有的区块状态类型,它们描述区块的生成进度。 - Registry.STRUCTURE_PIECE
包含所有的结构分片类型。结构分片是结构的一小部分,比如单个房间。 - Registry.RULE_TEST
包含所有的规则测试类型,它们用来在结构生成时匹配方块。 - Registry.STRUCTURE_PROCESSOR
包含所有的结构处理器类型。结构处理器用来在结构生成之后修改结构。 - Registry.STRUCTURE_POOL_ELEMENT
包含所有的结构池对象,结构池对象是结构更小的一部分,它们被结构分片所包含,它们用来基于数据包生成结构(例如使用链接方块生成的结构)。 - Registry.SURFACE_BUILDER
包含所有的表面构建器,表面构建器用来放置生物群系的表面方块,例如在很多主世界生物群系中的草方块。
创建一个自定义的传送门来进入你的维度
现在,你已经创建了你的维度,注册它,将它添加到生物群系并填充了生物和地物。
我们来创建一个能被生存玩家使用的传送门吧。
开始
Kyrptonaught创建了一个非常有用的库,它允许你很简单地创建一个自定义传送门连接你的维度与主世界或者其他维度。更多信息,看这个库的GH。
首先,像你的build.gradle添加如下仓库。
复制代码
然后添加如下依赖
复制代码
注意:这个库目前只在1.16能用,1.17很快会弄好。
注册你的传送门
来讨论用金块和打火石做得传送门,你可以在你的ModInitializer[1]中添加一个简单的方法。通过这种方式注册的传送门像下界传送门那样工作,最大可以去到23x23大小。
复制代码
现在你应该可以看到下面这样的传送门(自定义传送门在任何维度工作)
[原文图挂]
不管怎么说,这个API只能使用打火石开启传送门,让我们讨论一下怎么用岩浆桶来开启传送门吧。简单!用PortalIgnitionSource很容易就能完成。
复制代码
现在有一个酷的可以被岩浆激活的下界合金传送门和一个金传送门了!
杂项
这个传送门API支持自定义传送门方块,允许你创建它们的材质,但是目前它不支持方向性的传送门,但是很可能以后会支持。
这个API也支持事件,允许传送门在一些情况下不能被玩家激活,通过你代码中执行的事件。更多信息
现在,你已经创建了你的维度,注册它,将它添加到生物群系并填充了生物和地物。
我们来创建一个能被生存玩家使用的传送门吧。
开始
Kyrptonaught创建了一个非常有用的库,它允许你很简单地创建一个自定义传送门连接你的维度与主世界或者其他维度。更多信息,看这个库的GH。
首先,像你的build.gradle添加如下仓库。
- maven { url "https://dl.bintray.com/kyrptonaught/customportalapi" }
然后添加如下依赖
- modImplementation 'net.kyrptonaught:customportalapi:0.0.1-beta18-1.16'include 'net.kyrptonaught:customportalapi:0.0.1-beta18-1.16'
注意:这个库目前只在1.16能用,1.17很快会弄好。
注册你的传送门
来讨论用金块和打火石做得传送门,你可以在你的ModInitializer[1]中添加一个简单的方法。通过这种方式注册的传送门像下界传送门那样工作,最大可以去到23x23大小。
- // CustomPortalApiRegistry.addPortal(Block frameBlock, Identifier dimID, int r, int g, int b)CustomPortalApiRegistry.addPortal(Blocks.GOLD_BLOCK, new Identifier("my_mod_id", "my_dimension_id"), 234, 183, 8);
现在你应该可以看到下面这样的传送门(自定义传送门在任何维度工作)
[原文图挂]
不管怎么说,这个API只能使用打火石开启传送门,让我们讨论一下怎么用岩浆桶来开启传送门吧。简单!用PortalIgnitionSource很容易就能完成。
- // CustomPortalApiRegistry.addPortal(Block frameBlock, PortalIgnitionSource ignitionSource, Identifier dimID, int r, int g, int b) CustomPortalApiRegistry.addPortal(Blocks.NETHERITE_BLOCK, PortalIgnitionSource.FluidSource(Fluids.LAVA), new Identifier("my_mod_id", "my_dimension_id"), 51, 52, 49)
现在有一个酷的可以被岩浆激活的下界合金传送门和一个金传送门了!
杂项
这个传送门API支持自定义传送门方块,允许你创建它们的材质,但是目前它不支持方向性的传送门,但是很可能以后会支持。
这个API也支持事件,允许传送门在一些情况下不能被玩家激活,通过你代码中执行的事件。更多信息
MCBBS有你更精彩~
添加生成器类型
生成器类型是对区块生成器的封装并在世界创建菜单里面显示为世界类型。如果你不知道生成器类型是什么,建议你先看一看文末的结果段落。
添加生成器类型去要两个步骤
- 创建一个生成器类型
- 注册一个生成器类型
在这个指南里,我们会添加一个什么都不生成的生成器类型。
创建一个生成器类型
你要编写一个继承GeneratorType并实现getChunkGenerator方法的类。在这个只能中,我们复用已有的区块生成器FlatChunkGenerator来创建一个只有虚空的世界。你可以用你自己的区块生成器代替它。
- public class ExampleMod implements ModInitializer {
- private static final GeneratorType VOID = new GeneratorType("void") {
- protected ChunkGenerator getChunkGenerator(Registry<Biome> biomeRegistry,
- Registry<ChunkGeneratorSettings> chunkGeneratorSettingsRegistry, long seed) {
- FlatChunkGeneratorConfig config = new FlatChunkGeneratorConfig(
- new StructuresConfig(Optional.empty(), Collections.emptyMap()), biomeRegistry);
- config.updateLayerBlocks();
- return new FlatChunkGenerator(config);
- }
- };
- }
这个构造函数的参数是一个翻译键,它用于在你的世界创建菜单中的翻译。
Generator的构造函数是私有的,所以你要用访问扩展器kauai访问它。访问扩展器文件如下:
- accessWidener v1 named
- extendable method net/minecraft/client/world/GeneratorType <init> (Ljava/lang/String;)V
注册一个生成器类型
我们在onInitialize中注册一个生成器类型。
- public class ExampleMod implements ModInitializer {
- @Override
- public void onInitialize() {
- GeneratorTypeAccessor.getValues().add(VOID);
- }
- }
- @Mixin(GeneratorType.class)
- public interface GeneratorTypeAccessor {
- @Accessor("VALUES")
- public static List<GeneratorType> getValues() {
- throw new AssertionError();
- }
- }
你应该在你的en_us.json文件中添加
src/main/resources/assets/modid/lang/en_us.json
- {
- "generator.void": "Nothing but Void"
- }
zh_cn.json文件中添加
src/main/resources/assets/modid/lang/zh_cn.json
- {
- "generator.void": "只有虚空"
- }
结果
试试你的新生成器类型!


添加盔甲
简介
实现盔甲比实现一般的方块和物品稍微复杂一点点,但是一旦你理解了,它就变得容易很多。我们先整个CustomArmorMaterial类并注册它的物品形式。也了解一下怎么给它们弄材质。有一个特殊的章节教你怎么在盔甲上添加击退,尽管这个方法只能从Mixin中访问。(在1.16.3中)
有个简单的例子的GH
添加自定义盔甲材质
我们需要添加一个新类来实现我们的盔甲材质因为新的盔甲需要新的名字(还有像耐久以及护甲值之类的东西)。
这个类实现ArmorMaterial。它的头部是护甲值的分配(变量名为PROTECTION_VALUES)。它接下来所有方**使用@Override
复制代码
接下来的方**像如下内容一样定义(不要担心它们的名字,你等会会看到怎么实现它):
还有一个1.16的新东西
我吧所有的变量写成了X或A,B,C,D的形式,你的代码现在应该是这样。
复制代码
现在你有了基础的盔甲材质类,让我们来在一个新类RegisterItems中注册你的盔甲物品。
创建盔甲材质
我们要创建一个叫RegisterItems的新类来实现你的新盔甲部分。这也是像注册工具等等的地方。如果你想要一个锭(自定义才螺),这个指南也会把物品加到创造栏里,但是你可以删掉这部分内容。
创造物品栏的语法是.group(YourModName.YOUR_MOD_NAME_BUT_IN_CAPS_GROUP)
我会在ExampleMod中引用它
复制代码
现在你的物品已经正确创建了,让我们注册它们并给它们一个合适的名字。第一个参数是你的命名空间,即你的模组ID,下一个是你想要给你的物品的名字。
我们把这个写进你那个的ArmorItem里
复制代码
你的盔甲物品已经弄好了,在主类中注册它吧,(还有新的创造模式物品组)。
复制代码
好啦,你的盔甲已经在游戏中存在了,虽然还没加材质。但是已经存在并可以/give了。
现在我们给每个盔甲加材质
加材质
我们需要你
这些步骤应该对所有的盔甲都使用,只改变我们使用的部分。我们用头盔做示例。
resources/assets/examplemod/models/item/custom_material_helmet.json
复制代码
对每个盔甲重复一遍。
放置layer_1.png和layer_2.png到resources/assets/minecraft/textures/models/armor就可以给穿在身上的盔甲加材质。
如果你遵守了所有东西,你现在会有一组完整的盔甲啦!
添加击退抗性
这个东西非常讨厌。
Mojang说他们不打算硬编码getKnockbackResistance,但是却准备让它们不可修改。xswl。
为了处理这个问题,我们整一个mixin到ArmorItem里面。如果你第一次用Mixin,如何在fabric.mod.json中添加Mixin。
我们创建一个叫ArmorItemMixin的类,然后写下。
复制代码
然后我们必须用一个@Shadow来修改knockbackResistance,它是一个EntityAttribute。
接下来我们@Inject我们的GENERIC_KNOCKBACK_RESISTANCE到我们的ArmorMaterial构造方法中。
复制代码
现在你的盔甲有你在CustomArmorMaterial中写的击退抗性了。
简介
实现盔甲比实现一般的方块和物品稍微复杂一点点,但是一旦你理解了,它就变得容易很多。我们先整个CustomArmorMaterial类并注册它的物品形式。也了解一下怎么给它们弄材质。有一个特殊的章节教你怎么在盔甲上添加击退,尽管这个方法只能从Mixin中访问。(在1.16.3中)
有个简单的例子的GH
添加自定义盔甲材质
我们需要添加一个新类来实现我们的盔甲材质因为新的盔甲需要新的名字(还有像耐久以及护甲值之类的东西)。
这个类实现ArmorMaterial。它的头部是护甲值的分配(变量名为PROTECTION_VALUES)。它接下来所有方**使用@Override
- public class CustomArmorMaterial implements ArmorMaterial {
- private static final int[] BASE_DURABILITY = new int[] {13, 15, 16, 11};
- private static final int[] PROTECTION_VALUES = new int[] {A, B, C, D};
-
- // In which A is helmet, B chestplate, C leggings and D boots.
- // For reference, Leather uses {1, 2, 3, 1}, and Diamond/Netherite {3, 6, 8, 3}
- }
接下来的方**像如下内容一样定义(不要担心它们的名字,你等会会看到怎么实现它):
- getDurability:它在损坏之前能够承受的攻击数,使用我们在BASE_DURABILITY中的数值计算。皮革为5,钻石为33,下界合金为37。
- getPretectionAmount:使用我们的PROTECTION_VALUES即可。
- getEnchantability:这个决定你有多大可能在附魔书[原文如此]中获得高等级或多个附魔。
- SoundEvent getEquipSound:原版使用的是SoundEvents.ITEM_ARMOR_EQUIP_X,X是盔甲的类型。
- Ingredient getRepairIngridient:在铁砧中修复消耗的物品,可以是原版的也可以是你自己的。
- String getName:盔甲的原材料是什么,钻石盔甲使用"diamond"。
- getToughness:这是第二个保护值,决定盔甲对抗高伤害的能力。一般使用"X.0F"。
还有一个1.16的新东西
- getKnockbackResistance:留个0在这。如果你想要实现它,写个'0.XF'(X是你想要多大的击退保护),然后我一会会你怎么让它工作。
我吧所有的变量写成了X或A,B,C,D的形式,你的代码现在应该是这样。
- public class CustomArmorMaterial implements ArmorMaterial {
- private static final int[] BASE_DURABILITY = new int[] {13, 15, 16, 11};
- private static final int[] PROTECTION_VALUES = new int[] {A, B, C, D};
-
- @Override
- public int getDurability(EquipmentSlot slot) {
- return BASE_DURABILITY[slot.getEntitySlotId()] * X;
- }
-
- @Override
- public int getProtectionAmount(EquipmentSlot slot) {
- return PROTECTION_VALUES[slot.getEntitySlotId()];
- }
-
- @Override
- public int getEnchantability() {
- return X;
- }
-
- @Override
- public SoundEvent getEquipSound() {
- return SoundEvents.ITEM_ARMOR_EQUIP_X;
- }
-
- @Override
- public Ingredient getRepairIngredient() {
- return Ingredient.ofItems(RegisterItems.X);
- }
-
- @Override
- public String getName() {
- return "name";
- }
-
- @Override
- public float getToughness() {
- return X.0F;
- }
-
- @Override
- public float getKnockbackResistance() {
- return 0.XF;
- }
- }
现在你有了基础的盔甲材质类,让我们来在一个新类RegisterItems中注册你的盔甲物品。
创建盔甲材质
我们要创建一个叫RegisterItems的新类来实现你的新盔甲部分。这也是像注册工具等等的地方。如果你想要一个锭(自定义才螺),这个指南也会把物品加到创造栏里,但是你可以删掉这部分内容。
创造物品栏的语法是.group(YourModName.YOUR_MOD_NAME_BUT_IN_CAPS_GROUP)
我会在ExampleMod中引用它
- public class RegisterItems {
-
- public static final ArmorMaterial customArmorMaterial = new CustomArmorMaterial();
- public static final Item CUSTOM_MATERIAL = new CustomMaterialItem(new Item.Settings().group(ExampleMod.EXAMPLE_MOD_GROUP));
- // If you made a new material, this is where you would note it.
- public static final Item CUSTOM_MATERIAL_HELMET = new ArmorItem(CustomArmorMaterial, EquipmentSlot.HEAD, new Item.Settings().group(ExampleMod.EXAMPLE_MOD_GROUP));
- public static final Item CUSTOM_MATERIAL_CHESTPLATE = new ArmorItem(CustomArmorMaterial, EquipmentSlot.CHEST, new Item.Settings().group(ExampleMod.EXAMPLE_MOD_GROUP));
- public static final Item CUSTOM_MATERIAL_LEGGINGS = new ArmorItem(CustomArmorMaterial, EquipmentSlot.LEGS, new Item.Settings().group(ExampleMod.EXAMPLE_MOD_GROUP));
- public static final Item CUSTOM_MATERIAL_BOOTS = new ArmorItem(CustomArmorMaterial, EquipmentSlot.FEET, new Item.Settings().group(ExampleMod.EXAMPLE_MOD_GROUP));
-
- }
现在你的物品已经正确创建了,让我们注册它们并给它们一个合适的名字。第一个参数是你的命名空间,即你的模组ID,下一个是你想要给你的物品的名字。
我们把这个写进你那个的ArmorItem里
- public static void register() {
- Registry.register(Registry.ITEM, new Identifier("examplemod", "custom_material"), CUSTOM_MATERIAL);
- Registry.register(Registry.ITEM, new Identifier("examplemod", "custom_material_helmet"), CUSTOM_MATERIAL_HELMET);
- Registry.register(Registry.ITEM, new Identifier("examplemod", "custom_material_chestplate"), CUSTOM_MATERIAL_CHESTPLATE);
- Registry.register(Registry.ITEM, new Identifier("examplemod", "custom_material_leggings"), CUSTOM_MATERIAL_LEGGINGS);
- Registry.register(Registry.ITEM, new Identifier("examplemod", "custom_material_boots"), CUSTOM_MATERIAL_BOOTS);
- }
你的盔甲物品已经弄好了,在主类中注册它吧,(还有新的创造模式物品组)。
- public static final ItemGroup EXAMPLE_MOD_GROUP = FabricItemGroupBuilder.create(
- new Identifier("examplemod", "example_mod_group"))
- .icon(() -> new ItemStack(RegisterItems.CUSTOM_MATERIAL)) // This uses the model of the new material you created as an icon, but you can reference to whatever you like
- .build();
-
- @Override
- public void onInitialize() {
- RegisterItems.register();
- }
好啦,你的盔甲已经在游戏中存在了,虽然还没加材质。但是已经存在并可以/give了。
现在我们给每个盔甲加材质
加材质
我们需要你
- 给每个物品加材质(x_helment.png,x_chestplate.png etc.)
- 给穿上的盔甲加材质(x_layer_1.png和x_layer_2.png)
这些步骤应该对所有的盔甲都使用,只改变我们使用的部分。我们用头盔做示例。
resources/assets/examplemod/models/item/custom_material_helmet.json
- {
- "parent": "item/generated",
- "textures": {
- "layer0": "examplemod:item/custom_material_helmet"
- }
- }
对每个盔甲重复一遍。
放置layer_1.png和layer_2.png到resources/assets/minecraft/textures/models/armor就可以给穿在身上的盔甲加材质。
如果你遵守了所有东西,你现在会有一组完整的盔甲啦!
添加击退抗性
这个东西非常讨厌。
Mojang说他们不打算硬编码getKnockbackResistance,但是却准备让它们不可修改。xswl。
为了处理这个问题,我们整一个mixin到ArmorItem里面。如果你第一次用Mixin,如何在fabric.mod.json中添加Mixin。
我们创建一个叫ArmorItemMixin的类,然后写下。
- [url=home.php?mod=space&uid=1927477]@mixin[/url] (ArmorItem.class)
- public abstract class ArmorItemMixin {
-
- }
然后我们必须用一个@Shadow来修改knockbackResistance,它是一个EntityAttribute。
接下来我们@Inject我们的GENERIC_KNOCKBACK_RESISTANCE到我们的ArmorMaterial构造方法中。
- @Mixin (ArmorItem.class)
- public abstract class ArmorItemMixin {
-
- [url=home.php?mod=space&uid=49144]@shadow[/url] [url=home.php?mod=space&uid=797972]@FINAL[/url] private static UUID[] MODIFIERS;
- @Shadow @Final @Mutable private Multimap<EntityAttribute, EntityAttributeModifier> attributeModifiers;
- @Shadow @Final protected float knockbackResistance;
-
- @Inject(method = "<init>", at = @At(value = "RETURN"))
- private void constructor(ArmorMaterial material, EquipmentSlot slot, Item.Settings settings, CallbackInfo ci) {
- UUID uUID = MODIFIERS[slot.getEntitySlotId()];
-
- if (material == RegisterItems.customArmorMaterial) {
- ImmutableMultimap.Builder<EntityAttribute, EntityAttributeModifier> builder = ImmutableMultimap.builder();
-
- this.attributeModifiers.forEach(builder::put);
-
- builder.put(
- EntityAttributes.GENERIC_KNOCKBACK_RESISTANCE,
- new EntityAttributeModifier(uUID,
- "Armor knockback resistance",
- this.knockbackResistance,
- EntityAttributeModifier.Operation.ADDITION
- )
- );
-
- this.attributeModifiers = builder.build();
- }
- }
-
- }
现在你的盔甲有你在CustomArmorMaterial中写的击退抗性了。
添加工具
添加一个工具材料
我们的工具材料定义以下行为:
换句话说,工具材料定义了那种工具的基础功能,工具可以选泽使用它们的材料的值,或者自己决定。
原版工具材料可以在ToolMaterials中找到。我们创建一个单独的类作为我们的工具材料。
复制代码
ToolMaterial有一系列你需要实现的方法。
耐久
getDurability定义了基础的此种材料工具的耐久。在原版中,所有的相同材料的工具的耐久是一样的。
复制代码
挖掘速度
getMiningSpeedMultiplier定义了挖方块是这种工具的速度。给个一般的感觉,木头的速度是2.0F,钻石的速度是8.0F。
复制代码
攻击伤害
getAttackDamage返回工具造成的基础伤害。注意大部分工具的构造函数需要一个整数伤害值,也就是说造成的伤害
是(float)materialDamage + (int) toolDamage + 1,如果你想要通过构造函数你完全控制整个伤害值,你可以让你的材料的伤害为0F。
复制代码
挖掘等级
getMiningLevel设置工具的挖掘等级,钻石的挖掘等级是3,3及以上的等级可以挖黑曜石。
复制代码
附魔能力
getEnchantability定义了一个工具的附魔能力如何。金有22的附魔能力,而钻石有10附魔能力。高的值代表附魔越好。
复制代码
修复材料
getRepairIngredient返回在铁砧中修复物品时需要的Ingredient。
复制代码
ToolMaterial不需要注册,传递它给工具的一个好方法是把它放在某个地方(然后当你需要的时候应用它)。在这种情况,我们把我们的实例放在工具材料类的顶部。
复制代码
PotatoToolMaterial现在可以用PotatoToolMaterial.INSTANCE来引用了。
所有有的基础工具类(PickaxeItem, ShovelItem, HoeItem, AxeItem, SwordItem)需要一个ToolMaterial,一个攻击速度(float),一个额外攻击伤害,和一个Item.Settings实例。
复制代码
`PickaxeItem`, `HoeItem`和`AxeItem`有protected的构造函数,意味着你需要创建自己的带public构造函数的子类。
复制代码
然后使用这些子类
复制代码
如果你需要给你的工具加任何特殊的属性,创建一个子类继承一个你的基础工具类,并重写需要的方法。
注册工具
注册物品看这里(物品篇:物品)
让你的工具对非原版方块有效
看物品篇:挖掘等级最后一节。
添加一个工具材料
我们的工具材料定义以下行为:
- 耐久
- 挖掘速度
- 攻击伤害
- 挖掘等级
- 附魔能力
- 修复材料
换句话说,工具材料定义了那种工具的基础功能,工具可以选泽使用它们的材料的值,或者自己决定。
原版工具材料可以在ToolMaterials中找到。我们创建一个单独的类作为我们的工具材料。
- public class PotatoToolMaterial implements ToolMaterial {
-
- [...]
- }
ToolMaterial有一系列你需要实现的方法。
耐久
getDurability定义了基础的此种材料工具的耐久。在原版中,所有的相同材料的工具的耐久是一样的。
- @Override
- public int getDurability() {
- return 500;
- }
挖掘速度
getMiningSpeedMultiplier定义了挖方块是这种工具的速度。给个一般的感觉,木头的速度是2.0F,钻石的速度是8.0F。
- @Override
- public float getMiningSpeedMultiplier() {
- return 5.0F;
- }
攻击伤害
getAttackDamage返回工具造成的基础伤害。注意大部分工具的构造函数需要一个整数伤害值,也就是说造成的伤害
是(float)materialDamage + (int) toolDamage + 1,如果你想要通过构造函数你完全控制整个伤害值,你可以让你的材料的伤害为0F。
- @Override
- public float getAttackDamage() {
- return 3.0F;
- }
挖掘等级
getMiningLevel设置工具的挖掘等级,钻石的挖掘等级是3,3及以上的等级可以挖黑曜石。
- @Override
- public int getMiningLevel() {
- return 2;
- }
附魔能力
getEnchantability定义了一个工具的附魔能力如何。金有22的附魔能力,而钻石有10附魔能力。高的值代表附魔越好。
- @Override
- public int getEnchantability() {
- return 15;
- }
修复材料
getRepairIngredient返回在铁砧中修复物品时需要的Ingredient。
- @Override
- public Ingredient getRepairIngredient() {
- return Ingredient.ofItems(Items.POTATO);
- }
ToolMaterial不需要注册,传递它给工具的一个好方法是把它放在某个地方(然后当你需要的时候应用它)。在这种情况,我们把我们的实例放在工具材料类的顶部。
- public class PotatoToolMaterial implements ToolMaterial {
-
- public static final PotatoToolMaterial INSTANCE = new PotatoToolMaterial();
-
- [...]
- }
PotatoToolMaterial现在可以用PotatoToolMaterial.INSTANCE来引用了。
所有有的基础工具类(PickaxeItem, ShovelItem, HoeItem, AxeItem, SwordItem)需要一个ToolMaterial,一个攻击速度(float),一个额外攻击伤害,和一个Item.Settings实例。
- public static ToolItem POTATO_SHOVEL = new ShovelItem(PotatoToolMaterial.INSTANCE, 1.5F, -3.0F, new Item.Settings().group(ItemGroup.TOOLS));
- public static ToolItem POTATO_SWORD = new SwordItem(PotatoToolMaterial.INSTANCE, 3, -2.4F, new Item.Settings().group(ItemGroup.COMBAT));
`PickaxeItem`, `HoeItem`和`AxeItem`有protected的构造函数,意味着你需要创建自己的带public构造函数的子类。
- public class CustomPickaxeItem extends PickaxeItem {
- public CustomPickaxeItem(ToolMaterial material, int attackDamage, float attackSpeed, Settings settings) {
- super(material, attackDamage, attackSpeed, settings);
- }
- }
然后使用这些子类
- public static ToolItem POTATO_PICKAXE = new CustomPickaxeItem(PotatoToolMaterial.INSTANCE, 1, -2.8F, new Item.Settings().group(ItemGroup.TOOLS));
- public static ToolItem POTATO_AXE = new CustomAxeItem(PotatoToolMaterial.INSTANCE, 7.0F, -3.2F, new Item.Settings().group(ItemGroup.TOOLS));
- public static ToolItem POTATO_HOE = new CustomHoeItem(PotatoToolMaterial.INSTANCE, 7, -3.2F, new Item.Settings().group(ItemGroup.TOOLS));
如果你需要给你的工具加任何特殊的属性,创建一个子类继承一个你的基础工具类,并重写需要的方法。
注册工具
注册物品看这里(物品篇:物品)
让你的工具对非原版方块有效
看物品篇:挖掘等级最后一节。
Mixin注入
引言
注入允许你在特定的已经存在的方法里面放置自定义的代码。例如帖子底部的实用例子。注入器的标准形式如下。
复制代码
这个注入点参考定义了自定义代码插入的位置。下面是选项的一小部分。
在注入点引用了语句或者成员时,目标值可以在@At中设置。目标值是根据JVM字节码描述符确定的。
Oracle定义了字段描述符:
一个方法描述服是由方法名、一对括号包起来的输入(参数)类型、一个输出(返回)类型。方法Object m(int i, double[] d, Thread t)的描述符是m(I[DLjava/lang/Thread;)Ljava/lang/Object;
@Inject方法始终返回void。方法名随便起;使用最能描述这个注入做的事情的名字就好,目标方法的参数放在注入方法参数列表的头部,然后是CallbackInfo,若目标方法有返回类型(T), 用CallbackInfoReturnable<T>代替CallbackInfo。
返回&取消
在一个方法中提前返回或取消,使用CallbackInfo#cancel或者CallbackInfoReturnable<T>#setReturnValue(T)。注意cancel不一定要在setReturnValue后调用,在两个实例中,cancellable都必须在@Inject中设置为true:
复制代码
注入构造方法
使用<init>()V作为目标方法来注入一个构造方法,括号内填写构造方法的参数列表,当在构造方法中注入时,@At只支持TAIL或者RETURN。暂时官方没有支持其他注入点。注意一些类有init方法,它和<init>是不同的,别搞错了!
注入一个静态构造函数,使用<clinit>作为方法名。
实用例子
下面这个注入了一个输出到TitleScreen#init(注意,init是一个正常的方法而不是构造方法)
复制代码
更多例子,看Fabric例子仓库
引言
注入允许你在特定的已经存在的方法里面放置自定义的代码。例如帖子底部的实用例子。注入器的标准形式如下。
- @Inject(method = "", at = @At("INJECTION POINT REFERENCE"))
- private void injectMethod(METHOD ARGS, CallbackInfo info) {
-
- }
这个注入点参考定义了自定义代码插入的位置。下面是选项的一小部分。
名称 | 描述 |
HEAD | 方法头部 |
RETURN | 在每个返回语句的前面 |
INVOKE | 调用方法时 |
TAIL | 在最后一个返回语句的签名 |
在注入点引用了语句或者成员时,目标值可以在@At中设置。目标值是根据JVM字节码描述符确定的。
Oracle定义了字段描述符:
描述符 | 类型 | 描述 |
B | byte | 单个字节 |
C | char | 基本多文种平面中的一个UTF-16编码的字符 |
D | double | 双精度浮点值 |
F | float | 单精度浮点值 |
I | int | 整型 |
J | long | 长整型 |
LClassName | reference | ClassName的一个引用 |
S | short | 短整型 |
Z | boolean | 真或假 |
[ | reference | 一个数组维度 |
一个方法描述服是由方法名、一对括号包起来的输入(参数)类型、一个输出(返回)类型。方法Object m(int i, double[] d, Thread t)的描述符是m(I[DLjava/lang/Thread;)Ljava/lang/Object;
@Inject方法始终返回void。方法名随便起;使用最能描述这个注入做的事情的名字就好,目标方法的参数放在注入方法参数列表的头部,然后是CallbackInfo,若目标方法有返回类型(T), 用CallbackInfoReturnable<T>代替CallbackInfo。
返回&取消
在一个方法中提前返回或取消,使用CallbackInfo#cancel或者CallbackInfoReturnable<T>#setReturnValue(T)。注意cancel不一定要在setReturnValue后调用,在两个实例中,cancellable都必须在@Inject中设置为true:
- @Inject(method = "...", at = @At("..."), cancellable = true)
注入构造方法
使用<init>()V作为目标方法来注入一个构造方法,括号内填写构造方法的参数列表,当在构造方法中注入时,@At只支持TAIL或者RETURN。暂时官方没有支持其他注入点。注意一些类有init方法,它和<init>是不同的,别搞错了!
注入一个静态构造函数,使用<clinit>作为方法名。
实用例子
下面这个注入了一个输出到TitleScreen#init(注意,init是一个正常的方法而不是构造方法)
- @Mixin(TitleScreen.class)
- public class ExampleMixin {
- @Inject(at = @At("HEAD"), method = "init()V")
- private void init(CallbackInfo info) {
- System.out.println("This line is printed by an example mod mixin!");
- }
- }
更多例子,看Fabric例子仓库
Mixin访问器
Mixin访问器使你能够访问不可见或者有final修饰的字段与方法。
访问器
@Accessor允许你访问字段。假设我们想要访问MinecraftClient的itemUseCooldown字段。
从字段中取值
复制代码
用法:
复制代码
给字段赋值
复制代码
用法:
复制代码
静态的访问器
假设我们要访问VanillaLayeredBiomeSource类的BIOMES字段。
从字段中取值
复制代码
用法:
复制代码
给字段赋值
复制代码
用法:
复制代码
调用器
@Invoker允许你访问方法,假设我们要访问EndermanEntity的teleportTo方法。
复制代码
用法:
复制代码
静态的调用器
假设我们要调用BrewingRecipeRegistry的registerPotionType方法。
复制代码
用法:
复制代码
Mixin访问器使你能够访问不可见或者有final修饰的字段与方法。
访问器
@Accessor允许你访问字段。假设我们想要访问MinecraftClient的itemUseCooldown字段。
从字段中取值
- @Mixin(MinecraftClient.class)
- public interface MinecraftClientAccessor {
- @Accessor("itemUseCooldown")
- public int getItemUseCooldown();
- }
用法:
- int itemUseCooldown = ((MinecraftClientAccessor) MinecraftClient.getInstance()).getItemUseCooldown();
给字段赋值
- @Mixin(MinecraftClient.class)
- public interface MinecraftClientAccessor {
- @Accessor("itemUseCooldown")
- public void setItemUseCooldown(int itemUseCooldown);
- }
用法:
- ((MinecraftClientAccessor) MinecraftClient.getInstance()).setItemUseCooldown(100);
静态的访问器
假设我们要访问VanillaLayeredBiomeSource类的BIOMES字段。
从字段中取值
- @Mixin(VanillaLayeredBiomeSource.class)
- public interface VanillaLayeredBiomeSourceAccessor {
- @Accessor("BIOMES")
- public static List<RegistryKey<Biome>> getBiomes() {
- throw new AssertionError();
- }
- }
用法:
- List<RegistryKey<Biome>> biomes = VanillaLayeredBiomeSourceAccessor.getBiomes();
给字段赋值
- @Mixin(VanillaLayeredBiomeSource.class)
- public interface VanillaLayeredBiomeSourceAccessor {
- @Accessor("BIOMES")
- public static void setBiomes(List<RegistryKey<Biome>> biomes) {
- throw new AssertionError();
- }
- }
用法:
- VanillaLayeredBiomeSourceAccessor.setBiomes(biomes);
调用器
@Invoker允许你访问方法,假设我们要访问EndermanEntity的teleportTo方法。
- @Mixin(EndermanEntity.class)
- public interface EndermanEntityInvoker {
- @Invoker("teleportTo")
- public boolean invokeTeleportTo(double x, double y, double z);
- }
用法:
- EndermanEntity enderman = ...;
- ((EndermanEntityInvoker) enderman).invokeTeleportTo(0.0D, 70.0D, 0.0D);
静态的调用器
假设我们要调用BrewingRecipeRegistry的registerPotionType方法。
- @Mixin(BrewingRecipeRegistry.class)
- public interface BrewingRecipeRegistryInvoker {
- @Invoker("registerPotionType")
- public static void invokeRegisterPotionType(Item item) {
- throw new AssertionError();
- }
- }
用法:
- BrewingRecipeRegistryInvoker.invokeRegisterPotionType(item);
重定向方法
方法重定向可以使用如下注入点。
INVOKE
这个INVOKE注入点引用是用于在method中调用target方法时用的,意味着它可以用来直接重定向这个方法调用。
重定向静态方法
静态的重定向器应该和target方法有一样的参数列表。
重定向SimpleInventory::readTags中的ItemStack::fromTag(ListTag)调用,使其返回null。
复制代码
重定向实例方法
实例方法重定向器和静态方法重定向器非常相似,但是它们在参数列表头部有一个额外的参数,代表调用的对象。
重定向Entity::dropItem(ItemConvertible)中的Entity::dropItem(ItemConvertible, int)调用来把钻石换成空气。
复制代码
INVOKE_STRING
INVOKE_STRING注入点用于匹配target方法当target只有一个String参数且传入的是字符串常量时。被捕获的String常量应该被At的args选项指定。
重定向MinecraftClient::render中参数为"tick"的Profiler::push调用,来修改传入的location。
复制代码
方法重定向可以使用如下注入点。
- INVOKE
- INVOKE_STRING
INVOKE
这个INVOKE注入点引用是用于在method中调用target方法时用的,意味着它可以用来直接重定向这个方法调用。
重定向静态方法
静态的重定向器应该和target方法有一样的参数列表。
重定向SimpleInventory::readTags中的ItemStack::fromTag(ListTag)调用,使其返回null。
- public static ToolItem POTATO_SHOVEL = new ShovelItem(PotatoToolMaterial.INSTANCE, 1.5F, -3.0F, new Item.Settings().group(ItemGroup.TOOLS));
- public static ToolItem POTATO_SWORD = new SwordItem(PotatoToolMaterial.INSTANCE, 3, -2.4F, new Item.Settings().group(ItemGroup.COMBAT)); @Mixin(SimpleInventory.class)
- abstract class SimpleInventoryMixin {
- @Redirect(method = "readTags",
- at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/item/ItemStack;fromTag(Lnet/minecraft/nbt/ListTag;)Lnet/minecraft/item/ItemStack;"))
- private static ItemStack returnNull(ListTag tag) {
- return null;
- }
- }
重定向实例方法
实例方法重定向器和静态方法重定向器非常相似,但是它们在参数列表头部有一个额外的参数,代表调用的对象。
重定向Entity::dropItem(ItemConvertible)中的Entity::dropItem(ItemConvertible, int)调用来把钻石换成空气。
- @Mixin(Entity.class)
- abstract class EntityMixin {
- @Redirect(method = "dropItem",
- at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;dropItem(Lnet/minecraft/item/ItemConvertible;I)Lnet/minecraft/entity/ItemEntity;"))
- private ItemEntity replaceDroppedItem(Entity droppingEntity, ItemConvertible item, int yOffset) {
- return droppingEntity.dropItem(item == Items.DIAMOND ? Items.AIR : item, yOffset);
- }
- }
INVOKE_STRING
INVOKE_STRING注入点用于匹配target方法当target只有一个String参数且传入的是字符串常量时。被捕获的String常量应该被At的args选项指定。
重定向MinecraftClient::render中参数为"tick"的Profiler::push调用,来修改传入的location。
- @Mixin(MinecraftClient.class)
- abstract class MinecraftClientMixin {
- @Redirect(method = "render",
- at = @At(value = "INVOKE_STRING",
- target = "Lnet/minecraft/util/profiler/Profiler;push(Ljava/lang/String;)V",
- args = "ldc=tick"))
- private void redirectPush(Profiler profiler, String location) {
- profiler.push("modified tick");
- System.out.println(location);
- }
- }
Mixin例子
这里是一些常见的Mixin用法的例子。
在方法的头部注入
Mixin:
复制代码
效果:
复制代码
在方法尾部注入
Mixin:
复制代码
效果:
复制代码
在返回前注入
Mixin:
复制代码
效果:
复制代码
在方法调用前注入
Mixin:
复制代码
效果:
复制代码
在一个片段中注入
Mixin:
复制代码
效果:
复制代码
注入和取消
Mixin:
复制代码
效果:
复制代码
注入和带返回值的取消
Mixin:
复制代码
效果:
复制代码
修改返回值
Mixin:
复制代码
效果:
复制代码
重定向方法调用
Mixin:
复制代码
效果:
复制代码
重定向字段取值
Mixin:
复制代码
效果:
复制代码
重定向字段赋值
Mixin:
复制代码
效果:
复制代码
修改调用方法时的一个参数
Mixin:
复制代码
效果:
复制代码
修改调用方法时的多个参数
Mixin:
复制代码
效果:
复制代码
修改被调用时的一个参数
Mixin:
复制代码
效果:
复制代码
在某处修改局部变量
Mixin:
复制代码
效果:
复制代码
这里是一些常见的Mixin用法的例子。
在方法的头部注入
Mixin:
- @Inject(method = "foo()V", at = @At("HEAD"))
- private void injected(CallbackInfo ci) {
- doSomething4();
- }
效果:
- public void foo() {
- + injected(new CallbackInfo("foo", false));
- doSomething1();
- doSomething2();
- doSomething3();
- }
在方法尾部注入
Mixin:
- @Inject(method = "foo()V", at = @At("TAIL"))
- private void injected(CallbackInfo ci) {
- doSomething4();
- }
效果:
- public void foo() {
- doSomething1();
- if (doSomething2()) {
- return;
- }
- doSomething3();
- + injected(new CallbackInfo("foo", false));
- }
在返回前注入
Mixin:
- @Inject(method = "foo()V", at = @At("RETURN"))
- private void injected(CallbackInfo ci) {
- doSomething4();
- }
效果:
- public void foo() {
- doSomething1();
- if (doSomething2()) {
- + injected(new CallbackInfo("foo", false));
- return;
- }
- doSomething3();
- + injected(new CallbackInfo("foo", false));
- }
在方法调用前注入
Mixin:
- @Inject(method = "foo()V", at = @At(value = "INVOKE", target = "La/b/c/Something;doSomething()V"))
- private void injected(CallbackInfo ci) {
- doSomething3();
- }
效果:
- public void foo() {
- doSomething1();
- Something something = new Something();
- + injected(new CallbackInfo("foo", false));
- something.doSomething();
- doSomething2();
- }
在一个片段中注入
Mixin:
- @Inject(
- method = "foo()V",
- at = @At(
- value = "INVOKE",
- target = "La/b/c/Something;doSomething()V"
- ),
- slice = @Slice(
- from = @At(value = "INVOKE", target = "La/b/c/Something;doSomething2()V"),
- to = @At(value = "INVOKE", target = "La/b/c/Something;doSomething3()V")
- )
- )
- private void injected(CallbackInfo ci) {
- doSomething5();
- }
效果:
- public class Something {
- public void foo() {
- this.doSomething1();
- + // 不会在这里注入因为在指定片段以外
- this.doSomething();
- this.doSomething2();
- + injected(new CallbackInfo("foo", false));
- this.doSomething();
- this.doSomething3();
- + // 不会在这里注入因为在指定片段以外
- this.doSomething();
- this.doSomething4();
- }
- }
注入和取消
Mixin:
- @Inject(method = "foo()V", at = @At("HEAD"), cancellable = true)
- private void injected(CallbackInfo ci) {
- ci.cancel();
- }
效果:
- public void foo() {
- + CallbackInfo ci = new CallbackInfo("foo", true);
- + injected(ci);
- + if (ci.isCancelled()) return;
- doSomething1();
- doSomething2();
- doSomething3();
- }
注入和带返回值的取消
Mixin:
- @Inject(method = "foo()I;", at = @At("HEAD"), cancellable = true)
- private void injected(CallbackInfoReturnable<Integer> cir) {
- cir.setReturnValue(3);
- }
效果:
- public int foo() {
- + CallbackInfoReturnable<Integer> cir = new CallbackInfoReturnable<Integer>("foo", true);
- + injected(cir);
- + if (cir.isCancelled()) return cir.getReturnValue();
- doSomething1();
- doSomething2();
- doSomething3();
- return 10;
- }
修改返回值
Mixin:
- @Inject(method = "foo()I;", at = @At("RETURN"), cancellable = true)
- private void injected(CallbackInfoReturnable<Integer> cir) {
- cir.setReturnValue(cir.getReturnValue() * 3);
- }
效果:
- public int foo() {
- doSomething1();
- doSomething2();
- - return doSomething3() + 7;
- + int i = doSomething3() + 7;
- + CallbackInfoReturnable<Integer> cir = new CallbackInfoReturnable<Integer>("foo", true, i);
- + injected(cir);
- + if (cir.isCancelled()) return cir.getReturnValue();
- + return i;
- }
重定向方法调用
Mixin:
- @Redirect(method = "foo()V", at = @At(value = "INVOKE", target = "La/b/c/Something;doSomething(I)I"))
- private int injected(Something something, int x) {
- return x + 3;
- }
效果:
- public void foo() {
- doSomething1();
- Something something = new Something();
- - int i = something.doSomething(10);
- + int i = injected(something, 10);
- doSomething2();
- }
重定向字段取值
Mixin:
- @Redirect(method = "foo()V", at = @At(value = "FIELD", target = "La/b/c/Something;aaa:I", opcode = Opcodes.GETFIELD))
- private int injected(Something something) {
- return 12345;
- }
效果:
- public class Something {
- public int aaa;
- public void foo() {
- doSomething1();
- - if (this.aaa > doSomething2()) {
- + if (injected(this) > doSomething2()) {
- doSomething3();
- }
- doSomething4();
- }
- }
重定向字段赋值
Mixin:
- @Redirect(method = "foo()V", at = @At(value = "FIELD", target = "La/b/c/Something;aaa:I", opcode = Opcodes.PUTFIELD))
- private void injected(Something something, int x) {
- something.aaa = x + doSomething5();
- }
效果:
- public class Something {
- public int aaa;
- public void foo() {
- doSomething1();
- - this.aaa = doSomething2() + doSomething3();
- + inject(this, doSomething2() + doSomething3());
- doSomething4();
- }
- }
修改调用方法时的一个参数
Mixin:
- @ModifyArg(method = "foo()V", at = @At(value = "INVOKE", target = "La/b/c/Something;doSomething(ZIII)V"), index = 2)
- private int injected(int x) {
- return x * 3;
- }
效果:
- public void foo() {
- doSomething1();
- Something something = new Something();
- - something.doSomething(true, 1, 4, 5);
- + something.doSomething(true, 1, injected(4), 5);
- doSomething2();
- }
修改调用方法时的多个参数
Mixin:
- @ModifyArgs(method = "foo()V", at = @At(value = "INVOKE", target = "La/b/c/Something;doSomething(IDZ)V"))
- private void injected(Args args) {
- int a0 = args.get(0);
- double a1 = args.get(1);
- boolean a2 = args.get(2);
- args.set(0, a0 + 3);
- args.set(1, a1 * 2.0D);
- args.set(2, !a2);
- }
效果:
- public void foo() {
- doSomething1();
- Something something = new Something();
- - something.doSomething(3, 2.5D, true);
- + // Actually, synthetic subclass of Args is generated at runtime,
- + // but we omit the details to make it easier to understand the concept.
- + Args args = new Args(new Object[] { 3, 2.5D, true });
- + injected(args);
- + something.doSomething(args.get(0), args.get(1), args.get(2));
- doSomething2();
- }
修改被调用时的一个参数
Mixin:
- @ModifyVariable(method = "foo(ZIII)V", at = @At("HEAD"), ordinal = 1)
- private int injected(int y) {
- return y * 3;
- }
效果:
- public void foo(boolean b, int x, int y, int z) {
- + y = injected(y);
- doSomething1();
- doSomething2();
- doSomething3();
- }
在某处修改局部变量
Mixin:
- @ModifyVariable(method = "foo()V", at = @At("STORE"), ordinal = 1)
- private double injected(double x) {
- return x * 1.5D;
- }
效果:
- public void foo() {
- int i0 = doSomething1();
- double d0 = doSomething2();
- - double d1 = doSomething3() + 0.8D;
- + double d1 = injected(doSomething3() + 0.8D);
- double d2 = doSomething4();
- }
热重载Mixin
Mixin不能在默认的Fabric环境中热重载。你需要指定在你的JVM参数中指定-javaagent参数来启用这个功能。
唯一的田间是有一个sponge mixin jar文件。Fabric默认下载了这个文件,所以你可以在Gradle缓存里找找。IDEA用户可以看看项目左侧的库列表。
复制jar文件的完整路径然后打开你的运行设置。在Minecraft Client那里,展开VM选项页,你要在那里设置-javaagent参数指向你的mixin jar。
启动你的游戏,你现在可以在下面的规则下重载Mixin。
注意
DCEVM貌似不支持Mixin热重载。
如果你能在DCEVM上进行Mixin热重载,欢迎留言!
Mixin不能在默认的Fabric环境中热重载。你需要指定在你的JVM参数中指定-javaagent参数来启用这个功能。
唯一的田间是有一个sponge mixin jar文件。Fabric默认下载了这个文件,所以你可以在Gradle缓存里找找。IDEA用户可以看看项目左侧的库列表。
复制jar文件的完整路径然后打开你的运行设置。在Minecraft Client那里,展开VM选项页,你要在那里设置-javaagent参数指向你的mixin jar。
启动你的游戏,你现在可以在下面的规则下重载Mixin。
- 不增删方法
- 不改方法参数
- 不增删字段
注意
DCEVM貌似不支持Mixin热重载。
- 在Java 1.8:181上,DCEVM和Mixin重载不起作用。
- 在DCEVM Java 11上运行时,你可以重载,但是热重载任务会卡死。
如果你能在DCEVM上进行Mixin热重载,欢迎留言!
导出Mixin类
在调试Mixin的时候,查看在你修改和注入后的最终加载的类是很有用的。Mixin提供了一个参数来完成这事。
-Dmixin.debug.export=true
他应该放在你的VM参数里。一旦你的类被加载,它们将出现在run/.mixin.out
注意
有些类可能会在游戏运行(或者世界加载)的时候才出现。
在调试Mixin的时候,查看在你修改和注入后的最终加载的类是很有用的。Mixin提供了一个参数来完成这事。
-Dmixin.debug.export=true
他应该放在你的VM参数里。一旦你的类被加载,它们将出现在run/.mixin.out
注意
有些类可能会在游戏运行(或者世界加载)的时候才出现。
注册Mixin
简介
在这个指南中,你会学到如何通过resources/fabric.mod.json注册你的Mixin.
fabric.mod.json应该在你的resources文件夹。
来这里看Fabric Example Mod的resources文件夹:Fabric Example Mod
在你的fabric.mod.json中定义了Fabric应该在哪里找你的mixins.json
在Fabric中注册Mixin
你需要告诉Fabric到哪里去找Mixin。这需要你向fabric.mod.json中的mixins添加一个元素。
复制代码
在mixins数组中的"<modid>.mixin.json"字符串告诉Fabric加载<modid>.mixin.json中的Mixin。
注册Mixin
在签名一节中,你学习了如何注册你的<modid>.mixins.json文件。
我们还有定义哪些mixin要加载和这些mixin在哪/
在你的<modid>.mixins.json中:
复制代码
你在新手时期主要需要考虑的四个字段是package字符串和mixins、client、server数组。
package字段定义Mixin所在的文件夹(包)。
mixins数组定义在双端加载的Mixin。
client数组定义只在客户端加载的Mixin。
server数组定义只在服务端加载的Mixin。
遵守这个逻辑:net.fabricmc.example.mixin.TitleScreenMixin是只需要在客户端加载的Mixin。
简介
在这个指南中,你会学到如何通过resources/fabric.mod.json注册你的Mixin.
fabric.mod.json应该在你的resources文件夹。
来这里看Fabric Example Mod的resources文件夹:Fabric Example Mod
在你的fabric.mod.json中定义了Fabric应该在哪里找你的mixins.json
在Fabric中注册Mixin
你需要告诉Fabric到哪里去找Mixin。这需要你向fabric.mod.json中的mixins添加一个元素。
- {
- "mixins": [
- "modid.mixins.json"
- ]
- }
在mixins数组中的"<modid>.mixin.json"字符串告诉Fabric加载<modid>.mixin.json中的Mixin。
注册Mixin
在签名一节中,你学习了如何注册你的<modid>.mixins.json文件。
我们还有定义哪些mixin要加载和这些mixin在哪/
在你的<modid>.mixins.json中:
- {
- "required": true,
- "minVersion": "0.8",
- "package": "net.fabricmc.example.mixin",
- "compatibilityLevel": "JAVA_8",
- "mixins": [],
- "client": [
- "TitleScreenMixin"
- ],
- "server": [],
- "injectors": {
- "defaultRequire": 1
- }
- }
你在新手时期主要需要考虑的四个字段是package字符串和mixins、client、server数组。
package字段定义Mixin所在的文件夹(包)。
mixins数组定义在双端加载的Mixin。
client数组定义只在客户端加载的Mixin。
server数组定义只在服务端加载的Mixin。
遵守这个逻辑:net.fabricmc.example.mixin.TitleScreenMixin是只需要在客户端加载的Mixin。
Mixin概述
Mixin是Fabric生态系统中一个强大而重要的工具。它们的基本用法是修改基本游戏中已经存在的代码。不管是通过注入自定义逻辑,移除机制,或者修改变量。
完整的Mixin功能、用法和机制,看Mixin官方Wiki。一些额外的文档可以在Mixin Javadoc中找到。Fabric Wiki也提供了几个含有实用例子和解释的文章。
Mixin是Fabric生态系统中一个强大而重要的工具。它们的基本用法是修改基本游戏中已经存在的代码。不管是通过注入自定义逻辑,移除机制,或者修改变量。
完整的Mixin功能、用法和机制,看Mixin官方Wiki。一些额外的文档可以在Mixin Javadoc中找到。Fabric Wiki也提供了几个含有实用例子和解释的文章。
添加一个物品
概述
添加一个基本的物品是编写模组的第一步之一。你需要创建一个Item对象,注册它,加材质。你需要一个自定义的Item类来实现额外功能。在这个以及以后的指南里,tutorial命名空间是一个展位服,如果你有一个独立的modid,请用它来代替。
注册一个物品
首先,创建一个物品的实例。我们在initializer类的顶部存放它。Item的构造方法需要一个Item.Settings(或者FabricItemSettings)实例,用来设置像创造模式分组,耐久,最大堆叠数。
复制代码
你需要用原版的注册表系统来注册新内容,基本的语法是Registry#register(Registry Type, Identifier, Content)。注册表类型保存在Registry类的静态字段里,并且identifer是你的内容的id,内容是你要加的东西的实例。可以在初始化截断的任何之后调用。
复制代码
你的物品已经被添加到Minecraft了,运行runClient来查看它。
‘
添加材质
给物品注册材质需要一个物品模型 .json文件和一个材质图片。你需要添加如下东西到你的resources目录:
复制代码
我们的实例材质可以在这里找到。
如果你正确注册了你的物品,你的游戏会像这样吐槽找不到材质的。
复制代码
它方便地告诉你他期望的材质应该放在哪里,当有疑惑的时候,看日志。
一个基础的物品模板是:
复制代码
物品的parent决定物品在受伤的渲染方式并对类似背包里的方块物品起作用。item/handheld用于需要从左下方握住的工具武器等。textures/layer0是你的材质位置。
最终效果:
创建一个物品类
为了给物品添加新的行为,你需要创建一个物品类。默认的构造方法需要一个Item.Settings对象。
复制代码
实用的使用情况是你要在物品使用时播放声音。
复制代码
用你的新类的实例代替所有的旧对象。
复制代码
如果你所有东西都做对了,使用这个物品就会播放声音。
怎么改变我的物品的最大堆叠数呢?
这里你应该使用FabricItemSettings中的maxCount(int size)在指最大堆叠数。注意如果你的物品是有耐久的,你将不能设置最大堆叠数,否则游戏会抛出RuntimeException。
复制代码
概述
添加一个基本的物品是编写模组的第一步之一。你需要创建一个Item对象,注册它,加材质。你需要一个自定义的Item类来实现额外功能。在这个以及以后的指南里,tutorial命名空间是一个展位服,如果你有一个独立的modid,请用它来代替。
注册一个物品
首先,创建一个物品的实例。我们在initializer类的顶部存放它。Item的构造方法需要一个Item.Settings(或者FabricItemSettings)实例,用来设置像创造模式分组,耐久,最大堆叠数。
- public class ExampleMod implements ModInitializer {
-
- // an instance of our new item
- public static final Item FABRIC_ITEM = new Item(new FabricItemSettings().group(ItemGroup.MISC));
- [...]
- }
你需要用原版的注册表系统来注册新内容,基本的语法是Registry#register(Registry Type, Identifier, Content)。注册表类型保存在Registry类的静态字段里,并且identifer是你的内容的id,内容是你要加的东西的实例。可以在初始化截断的任何之后调用。
- public class ExampleMod implements ModInitializer {
-
- // an instance of our new item
- public static final Item FABRIC_ITEM = new Item(new FabricItemSettings().group(ItemGroup.MISC));
-
- @Override
- public void onInitialize() {
- Registry.register(Registry.ITEM, new Identifier("tutorial", "fabric_item"), FABRIC_ITEM);
- }
- }
你的物品已经被添加到Minecraft了,运行runClient来查看它。
‘

添加材质
给物品注册材质需要一个物品模型 .json文件和一个材质图片。你需要添加如下东西到你的resources目录:
- Item model: .../resources/assets/tutorial/models/item/fabric_item.json
- Item texture: .../resources/assets/tutorial/textures/item/fabric_item.png
我们的实例材质可以在这里找到。
如果你正确注册了你的物品,你的游戏会像这样吐槽找不到材质的。
- [Server-Worker-1/WARN]: Unable to load model: 'tutorial:fabric_item#inventory' referenced from: tutorial:fabric_item#inventory: java.io.FileNotFoundException: tutorial:models/item/fabric_item.json
它方便地告诉你他期望的材质应该放在哪里,当有疑惑的时候,看日志。
一个基础的物品模板是:
- {
- "parent": "item/generated",
- "textures": {
- "layer0": "tutorial:item/fabric_item"
- }
- }
物品的parent决定物品在受伤的渲染方式并对类似背包里的方块物品起作用。item/handheld用于需要从左下方握住的工具武器等。textures/layer0是你的材质位置。
最终效果:

创建一个物品类
为了给物品添加新的行为,你需要创建一个物品类。默认的构造方法需要一个Item.Settings对象。
- public class FabricItem extends Item {
-
- public FabricItem(Settings settings) {
- super(settings);
- }
- }
实用的使用情况是你要在物品使用时播放声音。
- public class FabricItem extends Item {
-
- public FabricItem(Settings settings) {
- super(settings);
- }
-
- @Override
- public TypedActionResult<ItemStack> use(World world, PlayerEntity playerEntity, Hand hand) {
- playerEntity.playSound(SoundEvents.BLOCK_WOOL_BREAK, 1.0F, 1.0F);
- return TypedActionResult.success(playerEntity.getStackInHand(hand));
- }
- }
用你的新类的实例代替所有的旧对象。
- public class ExampleMod implements ModInitializer {
-
- // an instance of our new item
- public static final FabricItem FABRIC_ITEM = new FabricItem(new FabricItemSettings().group(ItemGroup.MISC));
- [...]
- }
如果你所有东西都做对了,使用这个物品就会播放声音。
怎么改变我的物品的最大堆叠数呢?
这里你应该使用FabricItemSettings中的maxCount(int size)在指最大堆叠数。注意如果你的物品是有耐久的,你将不能设置最大堆叠数,否则游戏会抛出RuntimeException。
- public class ExampleMod implements ModInitializer {
-
- // An instance of our new item, where the maximum stack size is 16
- public static final FabricItem FABRIC_ITEM = new FabricItem(new FabricItemSettings().group(ItemGroup.MISC).maxCount(16));
- [...]
- }
本帖最后由 zjw_qwq 于 2021-2-18 11:05 编辑
大佬nb,我一直卡在如何开始第一步这里QAQ(
---编辑---
发完看见只有我一个人回复,心梗复发.jpg,触犯版规的话请处罚我吧
大佬nb,我一直卡在如何开始第一步这里QAQ(
---编辑---
发完看见只有我一个人回复,心梗复发.jpg,触犯版规的话请处罚我吧
本帖最后由 洞穴夜莺 于 2021-5-15 12:52 编辑
配方类型概述
什么是配方类型?
配方类型和配方本身不同,我们举个例子:
复制代码这个文件本身定义了一个获取木炭的配方,这个配方的类型是熔炉配方,一个配方类型可以用来定义一组配方。这里这个配方类型是熔炉方块数用的,但是你也可以创建另外的熔炉也使用这种配方类型但不同的状态。
这东西怎么工作?
一个配方类型需要两个东西,一个Recipe类和一个RecipeSerializer类。
Recipe类定义输入,输出,如何用它们合成东西以及其他乱七八糟的东西。当创建像例子中的木炭配方一样的东西时,它根据注册的RecipeType创建一个你的Recipe实例。
一个RecipeSerializer用来序列化一个对象(这里就是你的定义配方的Recipe类)到一个网络包,并反序列化每一个需要的json对象到你的Recipe类。
如何使用RecipeType?
用RecipeManager类来测试物品栏中不同的物品是否在合适的输入时获得期望的输出。它有很多有用的方法,比如getAllOfType返回所有指定类型的配方和它们的Identifier,getAllMatches返回对于一个ScreenHandler中物品栏所有可用的配方。
最简单的添加一个RecipeType的方法是使用原版类类节省代码。例如这有很多关于烹饪的抽象类,它们用于熔炉,烟熏炉和高炉甚至是营火,如果你的RecipeType类似于原版的这些配方类型,那你应该尝试继承它们。(有时一个方块RecipeType没有可以继承的原版类,那你可以自己吃哦年头弄一个)
更加深入的指南
添加配方
添加烹饪配方
添加切石配方
配方类型概述
什么是配方类型?
配方类型和配方本身不同,我们举个例子:
- {
- "type": "minecraft:smelting",
- "ingredient": {
- "tag": "minecraft:logs_that_burn"
- },
- "result": "minecraft:charcoal",
- "experience": 0.15,
- "cookingtime": 200
- }
这东西怎么工作?
一个配方类型需要两个东西,一个Recipe类和一个RecipeSerializer类。
Recipe类定义输入,输出,如何用它们合成东西以及其他乱七八糟的东西。当创建像例子中的木炭配方一样的东西时,它根据注册的RecipeType创建一个你的Recipe实例。
一个RecipeSerializer用来序列化一个对象(这里就是你的定义配方的Recipe类)到一个网络包,并反序列化每一个需要的json对象到你的Recipe类。
如何使用RecipeType?
用RecipeManager类来测试物品栏中不同的物品是否在合适的输入时获得期望的输出。它有很多有用的方法,比如getAllOfType返回所有指定类型的配方和它们的Identifier,getAllMatches返回对于一个ScreenHandler中物品栏所有可用的配方。
最简单的添加一个RecipeType的方法是使用原版类类节省代码。例如这有很多关于烹饪的抽象类,它们用于熔炉,烟熏炉和高炉甚至是营火,如果你的RecipeType类似于原版的这些配方类型,那你应该尝试继承它们。(有时一个方块RecipeType没有可以继承的原版类,那你可以自己吃哦年头弄一个)
更加深入的指南
添加配方
添加烹饪配方
添加切石配方
请问如果是添加可以作为唱片放在唱片机里面播放的物品应该怎么写?

想问一下盔甲工具这些如何在代码里动态添加相关耐久或攻击力这些属性(除了添加附魔)
目前能想到的思路就是copy一堆一样的,不同的就是一些属性数值不一样...
有没有啥更好的方法,wiki也看不出该咋实现俺这
(纯萌新)
目前能想到的思路就是copy一堆一样的,不同的就是一些属性数值不一样...
有没有啥更好的方法,wiki也看不出该咋实现俺这
(纯萌新)