Table of Contents

条款:本文中的代码适用于“Creative Commons Zero v1.0 Universial”条款,允许您将文中的代码示例用于自己的模组中。

注: 本文翻译自英文版commands

创建命令

创建命令允许模组开发者添加可以使用命令实现的功能。本教程将会教你如何注册命令,以及 Brigadier 的基本命令结构。

Brigadier 是什么?

Brigadier 是由 Mojang 写的用于 Minecraft 的命令解析器和派发器。Brigadier 是基于树的命令库,可以建立参数和命令的树。

这是 Brigadier 的源代码:https://github.com/Mojang/brigadier

Command 接口

在 Minecraft 中,com.mojang.brigadier.Command是函数型接口,会运行一些特定的内容,并在有些情况下抛出 CommandSyntaxException。命令有一个泛型 S,定义了命令的源(command source),命令源提供了命令运行的一些环境。在 Minecraft 中,命令源通常是 ServerCommandSource,代表一个服务器、命令方块、rcon 连接、玩家或者实体,有时也可以有 Event

Command 接口中的唯一方法,run(CommandContext<S>),接受一个 CommandContext<S> 作为唯一参数,并返回一个整数。命令环境(command context)存储 S 的命令源,并允许你从中获取参数、查询已解析的命令节点,并看到命令中的输入。

就像其他的函数型接口那样,命令通常用于匿名函数或者方法引用:

Command<ServerCommandSource> command = context -> {
    return 0;
};

这个整数相当于命令的结果。在 Minecraft 中。通常来说,负值表示命令执行失败,什么也不做,0 表示命令被略过,正数则表示命令执行成功并做了一些事情。

ServerCommandSource 是做什么的?

ServerCommandSource 提供了命令运行时的一些额外环境,这些环境拥有特定的实现,包括获取运行这个命令的实体、命令执行时所在的世界以及服务器。

  1. // 获取命令源。这总是生效。
  2. final ServerCommandSource source = ctx.getSource();
  3.  
  4. // 未经检查,如果是由控制台或命令方块执行的,则会是 null。
  5. final @Nullable Entity sender = source.getEntity();
  6.  
  7. // 如果命令执行者不是实体,就会抛出错误。
  8. // 这个的结果可能包含玩家,并且会发送反馈,告诉命令的发送者必须有一个实体。
  9. // 这个方法会要求你的方法能够抛出 CommandSyntaxException。
  10. // ServerCommandSource 中的 entity 选项可以返回一个 CommandBlock 实体、生物实体或者玩家。
  11. final @NotNull Entity sender2 = source.getEntityOrThrow();
  12.  
  13. // 如果命令执行者不是玩家,则为 null。
  14. final @Nullable ServerPlayerEntity player = source.getPlayer():
  15.  
  16. // 如果命令执行者不是玩家,抛出错误,并向命令的发送者发送反馈,告诉他必须有一个玩家。这个方法会要求你的方法能够抛出 CommandSyntaxException。
  17. final @NotNull ServerPlayerEntity player = source.getPlayerOrThrow();
  18.  
  19. // 获取命令发送时发送者的坐标,以 Vec3d 的形式。这可以是实体或命令方块的位置,若为控制台则为世界重生点。
  20. final Vec3d position = source.getPosition();
  21.  
  22. // 获取命令发送者所在的世界。控制台的世界就是默认重生的世界。
  23. final ServerWorld world = source.getWorld();
  24.  
  25. // 获取发送者的旋转角度,以 Vec2f 的形式。
  26. final Vec2f rotation = source.getRotation();
  27.  
  28. // 访问命令运行时的 MinecraftServer 实例。
  29. final MinecraftServer server = source.getServer();
  30.  
  31. // 命令源的名称,可以是实体、玩家、命令方块的名称,命令方块可以在放置之前命令,若为控制台则为“Console”
  32. final String name = source.getName();
  33.  
  34. // 如果命令源拥有特定的权限等级,则返回 true,这基于发送者的管理员级别。(在内置服务器上,玩家必须启用了作弊才能执行这些命令。)
  35. final boolean b = source.hasPermissionLevel(int level);

注册一个基本的命令

命令可以通过 Fabric APICommandRegistrationCallback 进行注册,关于如何注册回调,请参见 callbacks

这个事件必须在你的模组的初始化器中注册。这个回调有三个参数。CommmandDispatcher<S> 用于注册、解析和执行命令,S 是命令派发器支持的命令源的类型,通常是 ServerCommandSource。第二个参数提供了注册表的抽象化,可能传入了特定的命令参数方法中。第三个参数是 RegistrationEnvironment,识别命令将要注册到的服务器的类型。

为简化代码,建议静态导入 CommandManager 中的一些方法(参见静态导入):

import static net.minecraft.server.command.CommandManager.*;

在模组初始化器中,注册最简单的命令:

  1. public class ExampleMod implements ModInitializer {
  2. @Override
  3. public void onInitialize() {
  4. CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("foo")
  5. .executes(context -> {
  6. // 对于 1.19 之前的版本,把“Text.literal”替换为“new LiteralText”。
  7. // 对于 1.20 之前的版本,请移除“() ->”。
  8. context.getSource().sendFeedback(() -> Text.literal("调用 /foo,不带参数"), false);
  9.  
  10. return 1;
  11. })));
  12. }
  13. }

请确保你导入了正确的静态方法。方法 literalCommandManager.literal。你也可以清楚地写 CommandManager.literal 而不是使用静态导入。CommandManager.literal(“foo”) 会告诉 brigadier,命令有一个节点,foo 这个字面的节点。

sendFeedback 方法中,第一个参数是需要发送的文本,在 1.20 之前的版本中是 Text,在 1.20 以及之后的版本是 Supplier<Text>(这是为了避免在不需要的时候实例化了 Text 对象,因此请不要使用 Suppliers.ofInstance 或类似方法)。第二个参数决定了命令是否要将反馈的内容发送给其他的管理员。如果命令是查询一些内容,比如查询当前的时间或者某玩家的分数,则应该是 false。如果命令实际上做了些事情,例如修改时间或者分数,那么则应该是 true。如果游戏规则 sendCommandFeedback 是 false,那么你不会收到反馈。如果命令执行者被通过 /execute as … 修改,反馈则会发送给原始的执行者。

如果命令失败,可以不必调用 sendFeedback,而是直接抛出 CommandSyntaxExceptionCommandException。具体请参见 command_exceptions

要执行命令,必须输入 /foo,这是大小写敏感的。如果输入 /Foo/FoO/FOO/fOO 或者 /fooo,命令不会运行。

注册环境

如有需要,你可以确保命令仅在一些特定情形下注册,例如仅在专用服务器上:

  1. public class ExampleCommandMod implements ModInitializer {
  2. @Override
  3. public void onInitialize() {
  4. CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
  5. if (environment.dedicated) {
  6. ...;
  7. }
  8. });
  9. }
  10. }

静态导入

在上面的例子中,使用了静态导入以简化代码。对于字面值,语句会简化为 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 in the CommandManager
  10. import static net.minecraft.server.command.CommandManager.*;

注意:请确保使用了 CommandManager 中的 literalargument,而非其他类中的,否则编译时存在泛型擦除问题,因为类型参数 S 应该是 ServerCommandSource。(对于客户端的命令,请使用 ClientCommandManager。)

Brigadier 的默认参数位于 com.mojang.brigadier.arguments

Minecraft 的参数位于 net.minecraft.command.arguments。CommandManager 位于包 net.minecraft.server.command 内。

添加条件

有时你希望命令只有管理员(OP)可以执行,这时就要用到 requires 方法。requires 方法有一个参数 Predicate<ServerCommandSource>,提供一个 ServerCommandSource 以检测 CommandSource 能否执行命令。

例如:

  1. dispatcher.register(literal("foo")
  2. .requires(source -> source.hasPermissionLevel(2))
  3. .executes(ctx -> {
  4. ctx.getSource().sendFeedback(() -> Text.literal("你是 OP"), false);
  5. return 1;
  6. });

此时命令只会在命令源为 2 级以上(包括命令方块)时运行,否则命令不会被注册。这样做的副作用就是,非 2 级管理员会看到命令不会被 tab 补全,这也就是为什么没有启用作弊时不能够 tab 补全大多数命令。

要创建只有 4 级管理员(不包括命令方块)可以执行的命令,请使用 source.hasPermissionLevel(4)

参数

大多数命令都使用了参数。一些参数是可选的,也就是说如果你不提供此参数,命令仍能运行。一个节点可以有多个参数类型,但是注意有可能出现二义性,这是需要避免的。

在这个例子中。我们添加一个整数参数。并计算整数的平方。

    dispatcher.register(literal("mul")
        .then(argument("value", IntegerArgumentType.integer())
            .executes(context -> {
              final int value = IntegerArgumentType.getInteger(context, "value");
              final int result = value * value;
              context.getSource().sendFeedback(() -> Text.literal("%s × %s = %s".formatted(value, value, result)), false);
              return result;
            })));

在这个例子中,在 /mul 之后,你需要输入一个整数。例如,如果你输入 /mul 3,会收到消息“3 × 3 = 9”。如果你输入 /mul 不带参数,命令无法正确解析。

注意:为了简便,IntegerArgumentType.integerIntegerArgumentType.getInteger 可以替换为 integergetInteger 同时使用静态导入。为了显得更加清楚,这个例子不使用静态导入。

然后我们添加可选的第二个参数:

    dispatcher.register(literal("mul")
        .then(argument("value", IntegerArgumentType.integer())
            .executes(context -> {
              final int value = IntegerArgumentType.getInteger(context, "value");
              final int result = value * value;
              context.getSource().sendFeedback(() -> Text.literal("%s × %s = %s".formatted(value, value, result)), false);
              return result;
            })
            .then(argument("value2", IntegerArgumentType.integer())
                .executes(context -> {
                  final int value = IntegerArgumentType.getInteger(context, "value");
                  final int value2 = IntegerArgumentType.getInteger(context, "value2");
                  final int result = value * value2;
                  context.getSource().sendFeedback(() -> Text.literal("%s × %s = %s".formatted(value, value2, result)), false);
                  return result;
                }))));

现在你可以输入一个或者两个整数了。如果给一个整数,会计算这个整数的平方。如果提供两个整数,会计算这两个整数的积。你可能发现,两次指定类似的执行内容有些不太必要。因此,我们可以创建一个在两个执行中都使用的方法。

public class ExampleMod implements ModInitializer {
  @Override
  public void onInitialize() {
    CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("mul")
        .then(argument("value", IntegerArgumentType.integer())
            .executes(context -> executeMultiply(IntegerArgumentType.getInteger(context, "value"), IntegerArgumentType.getInteger(context, "value"), context))
            .then(argument("value2", IntegerArgumentType.integer())
                .executes(context -> executeMultiply(IntegerArgumentType.getInteger(context, "value"), IntegerArgumentType.getInteger(context, "value2"), context))))));
  }
 
  private static int executeMultiply(int value, int value2, CommandContext<ServerCommandSource> context) {
    final int result = value * value2;
    context.getSource().sendFeedback(() -> Text.literal("%s × %s = %s".formatted(value, value2, result)), false);
    return result;
  }
}

子命令

要添加子命令,你需要先照常注册第一个字面节点。

dispatcher.register(literal("foo"))

为拥有子命令,需要把下一个节点追加到已经存在的节点后面。

如下所示,创建命令 foo <bar>

  1. dispatcher.register(literal("foo")
  2. .then(literal("bar")
  3. .executes(context -> {
  4. // 对于 1.19 以下的版本,使用 ''new LiteralText''。
  5. // 对于 1.20 以下的版本,直接使用 ''Text'' 对象而非 supplier。
  6. context.getSource().sendFeedback(() -> Text.literal("调用 foo 和 bar"), false);
  7.  
  8. return 1;
  9. })
  10. )
  11. );

建议给命令添加节点时缩进你的代码,通常来说缩进对应了命令树中有多少节点的深度,每一次换行也可以看出添加了一个节点。本教程后面会展示格式化树状命令的几种可选样式。

类似于参数,子命令节点也可以设置为可选的。在下面这个例子中,/foo/foo bar 都是有效的。

  1. dispatcher.register(literal("foo")
  2. .executes(context -> {
  3. context.getSource().sendFeedback(() -> Text.literal("调用 foo 不带 bar"), false);
  4. return 1;
  5. })
  6. .then(literal("bar")
  7. .executes(context -> {
  8. context.getSource().sendFeedback(() -> Text.literal("调用 foo 带有 bar"), false);
  9. return 1;
  10. })
  11. )
  12. );

高级概念

以下是 brigadier 使用的更加复杂的概念的文章链接。

页面 描述
Exceptions 命令执行失败,并在特定的情况下留下描述性的消息。
Suggestions 为客户端建议命令的输入。
Redirects 允许在执行命令时使用别称或者重复元素。
Custom Argument Types 在你自己的项目里面解析你自己的参数。
Examples 一些示例命令

常见问题

代码为什么不编译

此问题可能有一些常见的原因。

可以注册客户端命令吗?

Fabric API 有个 ClientCommandManager,可以注册客户端命令。代码应该仅存在于客户端的代码中。例子:

    ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("foo_client")
        .executes(context -> {
              context.getSource().sendFeedback(Text.literal("此命令是客户端执行的!"));
              return 1;
            }
        )));

如果你需要在客户端命令执行过程中打开屏幕,不要直接调用 client.setScreen(…),你应该调用 client.execute(() -> client.setScreen(...)),其中变量 client 可以通过 context.getSource().getClient() 获得。

可以运行时注册命令吗?

可以这么做但是不推荐,你可以从服务器中获取 CommandManager 并向里面添加你希望添加到 CommandDispatcher 中的任何内容。

然后你需要通过 CommandManager.sendCommandTree(ServerPlayerEntity) 向每个玩家再次发送命令树,之所以要这么做,是因为客户端已经缓存了命令树并在登录过程中(或发出管理员封包时)使用,以用于本地的补全和错误消息。

可以在运行时取消注册命令吗?

可以这么做,但是这更不稳定,并且可能造成未预料的副作用。为简化事情,你需要在 brigadier 中使用反射并移除这个节点,然后还需要再次使用 sendCommandTree(ServerPlayerEntity) 向每个玩家发送命令树。如果不发送更新的命令树,客户端可能还是会认为命令依然存在,即使服务器已经无法执行。