洞穴夜莺
本帖最后由 洞穴夜莺 于 2021-2-5 11:29 编辑

本帖计划补充一些常用的且在森林蝙蝠翻译后被修改的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
  1. Command<Object> command = context -> {
  2.     return 0;
  3. };
复制代码


用匿名类
  1. Command<Object> command = new Command<Object>() {
  2.     @Override
  3.     public int run(CommandContext<Object> context) {
  4.         return 0;
  5.     }
  6. }
复制代码


实现为一个类
  1. final class XYZCommand implements Command<Object> {
  2.     @Override
  3.     public int run(CommandContext<Object> context) {
  4.         return 0;
  5.     }
  6. }
复制代码



用方法引用
  1. void registerCommand() {
  2.     // 忽略这个,一会再解释
  3.     dispatcher.register(CommandManager.literal("foo"))
  4.         .executes(this::execute); // This refers to the "execute" method below.
  5. }
  6.      
  7. private int execute(CommandContext<Object> context) {
  8.     return 0;
  9. }
复制代码


这个run(CommandContext)方法可以抛出一个CommandSyntaxException,这被后面的内容覆盖。

可以认为这个整数(译注:指返回值)是这个命令运行的结果,在Minecraft中,这个结果可以等同与从命令方块接红石比较器所获取的红石信号强度或者传递给面向的连锁型命令方块。一般来说负数表示一个命令失败了并且没有做任何事情。运行结果0表示命令被传递。正数表示命令运行成功并做了一些事情。


一个基础的命令
下面是一个没有参数的命令:
  1. dispatcher.register(CommandManager.literal("foo").executes(context -> {
  2.     System.out.println("Called foo with no arguments");

  3.     return 1;
  4. }));
复制代码

CommandManager.literal("foo")告诉brigadier这个命令有一个节点,一个叫foo的字面量。必须运行/foo来执行这个命令。输入/Foo、/FoO、/FOO、 /fOO或/fooo则不行。

一个子命令
你要先正常注册一个这个命令的字面量节点来添加一个子命令。
  1. dispatcher.register(CommandManager.literal("foo")
复制代码

需要在已存在的节点后添加下一个节点来创建子命令,这是用接受一个ArgumentBuilder参数的then(ArgumentBuilder)方法完成的。

下面的代码创建了命令foo <bar>。
  1. dispatcher.register(CommandManager.literal("foo")
  2.     .then(CommandManager.literal("bar"))
  3. );
复制代码

建议在向命令添加节点时缩进你的代码,通常缩进深度等同于你的节点在命令树中的深度。换行使得添加另一个节点更加明显。本指南后面会使用这种对于树状命令的可选风格。

所以来试试执行命令
如果你在游戏中输入/foo bar,命令会执行失败,这是因为当处理了所有的参数却没有可供执行的代码。你需要用executes(Command)方法告诉游戏需要执行的内容来解决这个问题。下面是一个命令应该有的样子的例子:
  1. dispatcher.register(CommandManager.literal("foo")
  2.     .then(CommandManager.literal("bar")
  3.         .executes(context -> {
  4.             System.out.println("Called foo with bar");
  5.    
  6.             return 1;
  7.         })
  8.     )
  9. );
复制代码

注册这个命令
注册命令由注册CommandRegistrationCallback的回调来完成。关于注册回调的信息,参阅回调文章

这个事件应该在你的Mod主类中注册。这个回调有两个参数。CommmandDispatcher<S>是用来注册,解析和执行命令的。S是命令调度器支持的命令源。第二个参数是一个表示命令要注册的服务器类型的布尔值,true为专用服务器, false为内置服务器。
  1. public class ExampleCommandMod implements ModInitializer {
  2.     @Override
  3.     public void onInitialize() {
  4.         CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
  5.             ...
  6.         });
  7.     }
  8. }
复制代码

在你的lambda中、方法引用或别的什么东西,你可以注册你的命令。
  1. public class ExampleCommandMod implements ModInitializer {
  2.     @Override
  3.     public void onInitialize() {
  4.         CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
  5.             dispatcher.register(CommandManager.literal("foo").executes(context -> {
  6.                 System.out.println("foo");
  7.                 return 1;
  8.             }));
  9.         });
  10.     }
  11. }
复制代码

你可以通过检查专用标志只在服务器注册一个命令
  1. public class ExampleCommandMod implements ModInitializer {
  2.     @Override
  3.     public void onInitialize() {
  4.         CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
  5.             if (dedicated) {
  6.                 TestDedicatedCommand.register(dispatcher);
  7.             }
  8.         });
  9.     }
  10. }
复制代码

反之亦然
  1. public class ExampleCommandMod implements ModInitializer {
  2.     @Override
  3.     public void onInitialize() {
  4.         CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
  5.             if (!dedicated) {
  6.                 TestDedicatedCommand.register(dispatcher);
  7.             }
  8.         });
  9.     }
  10. }
复制代码


参数
参数用于在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自己的参数类型起作用。

你的导入看起来像这样:
  1. // getString(ctx, "string")
  2. import static com.mojang.brigadier.arguments.StringArgumentType.getString;
  3. // word()
  4. import static com.mojang.brigadier.arguments.StringArgumentType.word;
  5. // literal("foo")
  6. import static net.minecraft.server.command.CommandManager.literal;
  7. // argument("bar", word())
  8. import static net.minecraft.server.command.CommandManager.argument;
  9. // Import everything
  10. 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>参数。

例子如下:
  1. dispatcher.register(literal("foo")
  2.         .requires(source -> source.hasPermissionLevel(4))
  3.                 .executes(ctx -> {
  4.                         ctx.getSource().sendFeedback(new LiteralText("You are an operator", false));
  5.                         return 1;
  6.                 });
复制代码

这个命令只会在有至少4级权限才执行,如果条件返回false,命令不会执行。并且也有不向没有4级权限的玩家展示Tab补全的副作用。

调用指定require块中权限实现不会被阻止。只是注意在权限变动时要重新发送命令树。

异常
Brigadier支持用于解析失败或执行失败抛出命令异常,也同时丰富错误信息。
所有Brigadier的异常基于CommandSyntaxException,这两种Brigadier提供的主要异常是动态和你需要调用create()来抛出的简单异常类型。
这些异常也允许你用创建输入命令错误信息的createWithContext(ImmutableStringReader)指定上下文。下面是个展示异常使用的抛硬币命令。
  1. dispatcher.register(CommandManager.literal("coinflip")
  2.     .executes(ctx -> {
  3.         Random random = new Random();

  4.         if(random.nextBoolean()) { // 正面成功
  5.             ctx.getSource().sendMessage(new TranslateableText("coin.flip.heads"))
  6.             return Command.SINGLE_SUCCESS;
  7.         }

  8.         throw new SimpleCommandExceptionType(new TranslateableText("coin.flip.tails")).create(); // 背面失败
  9.     }));
复制代码

你不会被限制在单个类型异常中,因为Brigadier也提供了可以接受额外上下文参数的动态异常。
  1. DynamicCommandExceptionType used_name = new DynamicCommandExceptionType(name -> {
  2.     return new LiteralText("The name: " + (String) name + " has been used");
  3. });
复制代码

这里有接受不同数量参数的更多动态异常(Dynamic2CommandExceptionType、Dynamic3CommandExceptionType、 Dynamic4CommandExceptionType、 DynamicNCommandExceptionType)。你应该记住动态异常接受一个对象作为参数,所以你可能必须转换参数用于你自己的用途。

重定向(别名)
重定向是Brigadier的别名实现形式,下面是Minecraft怎么处理/msg的别名/tell和/w的代码。
  1. public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
  2.     LiteralCommandNode node = registerMain(dispatcher); // 注册主命令
  3.     dispatcher.register(literal("tell")
  4.         .redirect(node)); // 别名1,重定向到主命令
  5.     dispatcher.register(literal("w")
  6.         .redirect(node)); // 别名2,重定向到主命令
  7. }

  8. public static LiteralCommandNode registerMain(CommandDispatcher<ServerCommandSource> dispatcher) {
  9.     return dispatcher.register(literal("msg")
  10.     .then(argument("targets", EntityArgumentType.players())
  11.         .then(argument("message", MessageArgumentType.message())
  12.             .executes(ctx -> {
  13.                 return execute(ctx.getSource(), getPlayers(ctx, "targets"), getMessage(ctx, "message"));
  14.             }))));
  15. }
复制代码

重定向告诉brigadier在另一个节点继续解析。
重定向(连锁)
像/execute as @e[type=player] in the_end run tp ~ ~ ~的命令是由重定向实现的。下面是一个连锁命令的例子:
  1. LiteralCommandNode<ServerCommandSource> root = dispatcher.register(literal("fabric_test"));
  2. LiteralCommandNode<ServerCommandSource> root1 = dispatcher.register(literal("fabric_test")
  3. // 你可以注册许多字面量,它只注册分支的新部分,如果注册重复的分支,则会在控制台中提示命令冲突,但是仍然可以使用。
  4.     .then(literal("extra")
  5.         .then(literal("long")
  6.             .redirect(root, this::lengthen)) // 返回连锁的根节点
  7.         .then(literal("short")
  8.             .redirect(root, this::shorten))) // 返回连锁的根节点
  9.         .then(literal("command")
  10.             .executes(ctx -> {
  11.                 ctx.getSource().sendFeedback(new LiteralText("Chainable Command"), false);
  12.                 return Command.SINGLE_SUCCESS;
  13. })));
复制代码

重定向也可以用redirect modifier修改命令源用于构建者命令。
  1. .redirect(rootNode, context -> {
  2.     return ((ServerCommandSource) context.getSource()).withLookingAt(Vec3ArgumentType.getVec3(context, "pos"));
  3. })
复制代码


ServerCommandSource能干啥?
一个服务器命令源提供了一些额外的实现在一个命令执行时指定上下文。这包括获取执行命令的实体的能力,执行命令所位于的世界和执行命令的服务器。

  1. final ServerCommandSource source = ctx.getSource();
  2. // 获取命令源,这始终有效

  3. final Entity sender = source.getEntity();
  4. // 未检查,如果执行者是控制台则为空

  5. final Entity sender2 = source.getEntityOrThrow();
  6. // 会在命令不是由实体执行时结束执行
  7. // 这个可能返回一个实体,也可能发反馈必须有一个实体
  8. // 这个方法需要你的方法能抛出CommandSyntaxException.
  9. // ServerCommandSource的实体选项可能返回一个CommandBlock实体,一个玩家,或一个生物

  10. final ServerPlayerEntity player = source.getPlayer();
  11. // 如果命令执行者不是一个玩家会结束命令。也会发送必须是玩家的反馈。这个方法需要你的方法能抛出CommandSyntaxException.
  12. source.getPosition();
  13. // 获取Vec3形式的执行坐标这可能是实体或命令方块坐标或者世界出生点(后台执行)

  14. source.getWorld();
  15. // 获取执行者的世界,或者出生世界(后台执行)

  16. source.getRotation();
  17. // 获取执行者的Vec2f形式面向方向

  18. source.getMinecraftServer();
  19. // 获取服务器实例

  20. source.getName();
  21. // 获取命令源的名字可能是玩家,实体,被重命名的命令方块名字或者"Console"(后台执行)

  22. source.hasPermissionLevel(int level);
  23. // 如果有该权限等级返回true,基于操作员的状态 (在内置服务端中,必须开启作弊来执行一些命令)
复制代码


一些命令例子
广播一则消息
  1. public static void register(CommandDispatcher<ServerCommandSource> dispatcher){
  2.     dispatcher.register(literal("broadcast")
  3.         .requires(source -> source.hasPermissionLevel(2)) // 运行命令必须是游戏管理员,命令不会被任何非操作员玩家和1级操作员补全和执行
  4.             .then(argument("color", ColorArgumentType.color())
  5.                 .then(argument("message", greedyString())
  6.                     .executes(ctx -> broadcast(ctx.getSource(), getColor(ctx, "color"), getString(ctx, "message")))))); // 你可以处理参数并传递它们
  7. }

  8. public static int broadcast(ServerCommandSource source, Formatting formatting, String message) {
  9.     final Text text = new LiteralText(message).formatting(formatting);

  10.     source.getMinecraftServer().getPlayerManager().broadcastChatMessage(text, false);
  11.     return Command.SINGLE_SUCCESS; // 成功
  12. }
复制代码


/giveMeDiamond
首先我们注册"giveMeDiamond"为一个字面量节点然后执行块告诉调度器要运行的方法
  1. public static LiteralCommandNode register(CommandDispatcher<ServerCommandSource> dispatcher) { // You can also return a LiteralCommandNode for use with possible redirects
  2.     return dispatcher.register(literal("giveMeDiamond")
  3.         .executes(ctx -> giveDiamond(ctx)));
  4. }
复制代码

然后因为我们想要给玩家,所以我们要检查CommandSource是一个玩家,可以用getPlayer来在执行者不是玩家时出错。
  1. public static int giveDiamond(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
  2.     final ServerCommandSource source = ctx.getSource();

  3.     final PlayerEntity self = source.getPlayer(); // 如果不是玩家则停止执行
复制代码

然后我们向玩家的物品栏添加物品,同时检查物品栏对象是否为空。
  1.     if(!player.inventory.insertStack(new ItemStack(Items.DIAMOND))){
  2.         throw new SimpleCommandExceptionType(new TranslatableText("inventory.isfull")).create();
  3.     }

  4.     return 1;
  5. }
复制代码

安提阿
...将你安提阿的神圣手雷对向你的敌人,那敌人也就是那我眼中下流者,应当将它消除。

笑话的题外话,这个命令在指定位置或玩家的焦点处召唤一个点燃的TNT。

首先向CommandDispacther注册一个antioch字面量节点和一个可选的召唤位置参数。
  1. public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
  2.     dispatcher.register(literal("antioch")
  3.         .then(required("location", BlockPosArgumentType.blockPos()
  4.             .executes(ctx -> antioch(ctx.getSource(), BlockPosArgument.getBlockPos(ctx, "location")))))
  5.         .executes(ctx -> antioch(ctx.getSource(), null)));
  6. }
复制代码

创建笑话背后的消息
  1. public static int antioch(ServerCommandSource source, BlockPos blockPos) throws CommandSyntaxException {
  2.     if(blockPos == null) {
  3.         // 没有位置参数的情况我们计算玩家的焦点坐标
  4.         // 这只是个例子,实际上这个类不存在
  5.         blockPos = LocationUtil.calculateCursorOrThrow(source, source.getRotation());
  6.     }

  7.     final TntEntity tnt = new TntEntity(source.getWorld(), blockPos.getX(), blockPos.getY(), blockPos.getZ(), null);
  8.     tnt.setFuse(3);

  9.     source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("...lobbest thou thy Holy Hand Grenade of Antioch towards thy foe").formatting(Formatting.RED), false);
  10.     source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("who being naughty in My sight, shall snuff it.").formatting(Formatting.RED), false);
  11.     source.getWorld().spawnEntity(tnt);
  12.     return 1;
  13. }
复制代码

更多例子待补充

自定义参数类型
Brigadier支持自定义参数类型,这段会告诉你怎么创建一个简单的参数类型。

警告:自定义参数类型需要客户端正确安装Mod!如果你在编写服务器插件,考虑使用现有的参数类型并自定义建议提供器。

这里我们会创建一个UuidArgumentType。

首先创建一个继承ArgumentType的类,注意ArgumentType是个泛型类,所以定义你要返回的类型为泛型。
  1. public class UuidArgumentType implements ArgumentType<UUID> {
复制代码

ArgumentType需要你实现一个parse方法,它要返回泛型类型的对象。
  1. @Override
  2. public UUID parse(StringReader reader) throws CommandSyntaxException {
复制代码

这个方法是你解析的发生的地方,这个方法的返回值给出一个对象参数或者抛出一个CommandSyntaxException解析失败。

接下来你要保存焦点当前位置,这样你可以求出指定参数的子串,这是始终在你要解析的参数的最前面。
  1. int argBeginning = reader.getCursor(); // 参数的开始位置
  2. if (!reader.canRead()) {
  3.     reader.skip();
  4. }
复制代码

现在我们获取整个参数。基于你的参数类型,你需要有不同的标准或者像一些参数一样从{到}。对于一个UUID来说我们只是算出参数的结束焦点位置在哪。
  1. while (reader.canRead() && reader.peek() != ' ') { // peek 提供了当前焦点处字符
  2.     reader.skip(); // 让StringReader移动到下一个位置
  3. }
复制代码

最后我们检查参数是否正确并解析出我们想要的参数,并在解析失败时抛出异常。
  1. try {
  2.     UUID uuid = UUID.fromString(uuidString); // 这是我们的实际逻辑
  3.     return uuid; // 我们在解析成功时返回类型并继续下一解析
  4. } catch (Exception ex) {
  5.     // 当从字符串创建UUID时,可能会抛出异常,把它包装成CommandSyntaxException
  6.     // 带有上下问可以使Brigadier向用户提供失败位置的信息
  7.     // 一般的创建方法
  8.     throw new SimpleCommandExceptionType(new LiteralText(ex.getMessage())).createWithContext(reader);
  9. }
复制代码


这个ArgumentType就完成了,但是客户端会拒绝解析参数并抛出异常,这是因为服务器会告诉客户端命令节点的参数类型,客户端不会解析它不知道的类型。我们注册一个ArgumentSerializer来解决此问题。在你的ModInitializer中。对于更多复杂的参数类型,你可以创建你自己的ArgumentSerializer。
  1. ArgumentTypes.register("mymod:uuid", UuidArgumentType.class, new ConstantArgumentSerializer(UuidArgumentType::uuid));
  2. // 这个参数会创建ArgumentType
复制代码


然后这里是完整的ArgumentType:
UuidArgumentType.java
  1. import com.mojang.brigadier.StringReader;
  2. import com.mojang.brigadier.arguments.ArgumentType;
  3. import com.mojang.brigadier.context.CommandContext;
  4. import com.mojang.brigadier.exceptions.CommandSyntaxException;
  5. import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
  6. import net.minecraft.text.LiteralText;
  7. import net.minecraft.util.SystemUtil;

  8. import java.util.ArrayList;
  9. import java.util.Collection;
  10. import java.util.UUID;

  11. /**
  12. * Represents an ArgumentType that will return a UUID.
  13. */
  14. public class UuidArgumentType implements ArgumentType<UUID> {
  15.     public static UuidArgumentType uuid() {
  16.         return new UuidArgumentType();
  17.     }

  18.     public static <S> UUID getUuid(String name, CommandContext<S> context) {
  19.         // 注意你应当永远假设CommandContext包装的CommandSource是个泛型
  20.         // 如果你要用ServerCommandSource确保你在强制类型转换前类型检查
  21.         return context.getArgument(name, UUID.class);
  22.     }

  23.     private static final Collection<String> EXAMPLES = SystemUtil.consume(new ArrayList<>(), list -> {
  24.         list.add("765e5d33-c991-454f-8775-b6a7a394c097"); // i509VCB: Username The_1_gamers
  25.         list.add("069a79f4-44e9-4726-a5be-fca90e38aaf5"); // Notch
  26.         list.add("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"); // Dinnerbone
  27.     });

  28.     @Override
  29.     public UUID parse(StringReader reader) throws CommandSyntaxException {
  30.         int argBeginning = reader.getCursor(); // 你参数的开始位置
  31.         if (!reader.canRead()) {
  32.             reader.skip();
  33.         }

  34.         // 现在我们检查是否到了末尾(当canRead返回false时)
  35.         // 或者到达下一个空格,它标志着下一个参数
  36.         while (reader.canRead() && reader.peek() != ' ') { // peek 提供了当前焦点处字符
  37.             reader.skip(); // 让StringReader移动到下一个位置
  38.         }

  39.         // 由这个参数的起始位置和下一个参数的起始位置获取这个命令的字串
  40.         String uuidString = reader.getString().substring(argBeginning, reader.getCursor());
  41.         try {
  42.             UUID uuid = UUID.fromString(uuidString); // 这是我们的实际逻辑
  43.             return uuid; // 我们在解析成功时返回类型并继续下一解析
  44.         } catch (Exception ex) {
  45.             // 当从字符串创建UUID时,可能会抛出异常,把它包装成CommandSyntaxException
  46.             // 带有上下问可以使Brigadier向用户提供失败位置的信息
  47.             // 一般的创建方法
  48.             throw new SimpleCommandExceptionType(new LiteralText(ex.getMessage())).createWithContext(reader);
  49.         }
  50.     }

  51.     @Override
  52.     public Collection<String> getExamples() { // Brigadier对于命令参数的例子提供了支持,这个集合只应该包含能解析的内容。这通常用于模棱两个的命令参数的相同之处。
  53.         return EXAMPLES;
  54.     }
  55. }
复制代码

桥之影
比较长的代码建议使用
  1. [spoiler][/spoiler]
复制代码
代码折叠

洞穴夜莺
本帖最后由 洞穴夜莺 于 2021-1-29 23:31 编辑

挖掘等级

简介
原版的挖掘等级系统很烂,它硬编码可挖掘的方块而且只支持镐却不支持其他挖掘工具。

设置一个方块的挖掘等级
使用FabricBlockSettings中的breakByTool方法来设置方块的挖掘等级,需要一个由Fabric在FabricToolTags中提供的物品标签。
  1. settings.breakByTool(FabricToolTags.PICKAXES, 2)
复制代码

这里有一个挖掘等级的列表:
  1. 0 -> 木 / 金 镐
  2. 1 -> 石镐
  3. 2 -> 铁镐
  4. 3 -> 钻石镐
  5. 4 -> 下界合金镐
复制代码


处理方块材质问题
原版镐子对STONE, METAL, ANVIL有效。
原版斧头对WOOD, NETHER_WOOD, PLANT, REPLACEABLE_PLANT, BAMBOO, PUMPKIN有效。
工具能够在挖掘等级不够的情况下挖掘你这类材质的方块。
你必须创建你自己的材质拷贝来解决这个问题,讨论创建一个Material.STONE的拷贝的情况,看看Material.STONE的代码:
  1. new Material.Builder(MaterialColor.STONE).requiresTool().build()
复制代码

把Material.Builder换成FabricMaterialBuilder得:
  1. new FabricMaterialBuilder(MaterialColor.STONE).requiresTool().build()
复制代码


当使用错误的工具时不掉落(面向1.15.x!)

你需要在方块的材质中设置requiresTool,因此需要创建你的材质的拷贝。
讨论创建一个Material.WOOD的拷贝来使你的木制方块只在使用正确的工具挖掘时掉落的情况,看看Material.STONE的代码:[原文如此,有误]
  1. new Material.Builder(MaterialColor.WOOD).burnable().build()
复制代码

把Material.Builder换成FabricMaterialBuilder并加入requiresTool()得:
  1. new FabricMaterialBuilder(MaterialColor.WOOD).burnable().requiresTool().build()
复制代码


当使用错误的工具时不掉落(面向1.16.x!)

在方块的设置中设置requiresTool。

创建自定义工具

把你的工具加入Fabric工具标签来支持Mod方块。
向pickaxes标签添加一种镐的例子:
文件位置:/src/main/resources/data/fabric/tags/items/pickaxes.json
  1. {
  2.   "replace": false,
  3.   "values": [
  4.     "examplemod:example_pickaxe"
  5.   ]
  6. }
复制代码


第一页 上一页 下一页 最后一页