条款:本文中的代码适用于“Creative Commons Zero v1.0 Universial”条款,允许您将文中的代码示例用于自己的模组中。
注: 本文翻译自英文版commands。
创建命令允许模组开发者添加可以使用命令实现的功能。本教程将会教你如何注册命令,以及 Brigadier 的基本命令结构。
Brigadier 是由 Mojang 写的用于 Minecraft 的命令解析器和派发器。Brigadier 是基于树的命令库,可以建立参数和命令的树。
这是 Brigadier 的源代码:https://github.com/Mojang/brigadier
在 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
提供了命令运行时的一些额外环境,这些环境拥有特定的实现,包括获取运行这个命令的实体、命令执行时所在的世界以及服务器。
// 获取命令源。这总是生效。 final ServerCommandSource source = ctx.getSource(); // 未经检查,如果是由控制台或命令方块执行的,则会是 null。 // 如果命令执行者不是实体,就会抛出错误。 // 这个的结果可能包含玩家,并且会发送反馈,告诉命令的发送者必须有一个实体。 // 这个方法会要求你的方法能够抛出 CommandSyntaxException。 // ServerCommandSource 中的 entity 选项可以返回一个 CommandBlock 实体、生物实体或者玩家。 // 如果命令执行者不是玩家,则为 null。 final @Nullable ServerPlayerEntity player = source.getPlayer(): // 如果命令执行者不是玩家,抛出错误,并向命令的发送者发送反馈,告诉他必须有一个玩家。这个方法会要求你的方法能够抛出 CommandSyntaxException。 final @NotNull ServerPlayerEntity player = source.getPlayerOrThrow(); // 获取命令发送时发送者的坐标,以 Vec3d 的形式。这可以是实体或命令方块的位置,若为控制台则为世界重生点。 final Vec3d position = source.getPosition(); // 获取命令发送者所在的世界。控制台的世界就是默认重生的世界。 final ServerWorld world = source.getWorld(); // 获取发送者的旋转角度,以 Vec2f 的形式。 final Vec2f rotation = source.getRotation(); // 访问命令运行时的 MinecraftServer 实例。 final MinecraftServer server = source.getServer(); // 命令源的名称,可以是实体、玩家、命令方块的名称,命令方块可以在放置之前命令,若为控制台则为“Console” // 如果命令源拥有特定的权限等级,则返回 true,这基于发送者的管理员级别。(在内置服务器上,玩家必须启用了作弊才能执行这些命令。) final boolean b = source.hasPermissionLevel(int level);
命令可以通过 Fabric API 的 CommandRegistrationCallback
进行注册,关于如何注册回调,请参见 callbacks。
这个事件必须在你的模组的初始化器中注册。这个回调有三个参数。CommmandDispatcher<S>
用于注册、解析和执行命令,S
是命令派发器支持的命令源的类型,通常是 ServerCommandSource
。第二个参数提供了注册表的抽象化,可能传入了特定的命令参数方法中。第三个参数是 RegistrationEnvironment
,识别命令将要注册到的服务器的类型。
为简化代码,建议静态导入 CommandManager
中的一些方法(参见静态导入):
import static net.minecraft.server.command.CommandManager.*;
在模组初始化器中,注册最简单的命令:
public class ExampleMod implements ModInitializer { @Override public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("foo") .executes(context -> { // 对于 1.19 之前的版本,把“Text.literal”替换为“new LiteralText”。 // 对于 1.20 之前的版本,请移除“() ->”。 context.getSource().sendFeedback(() -> Text.literal("调用 /foo,不带参数"), false); return 1; }))); } }
请确保你导入了正确的静态方法。方法 literal
是 CommandManager.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
,而是直接抛出 CommandSyntaxException
或 CommandException
。具体请参见 command_exceptions。
要执行命令,必须输入 /foo
,这是大小写敏感的。如果输入 /Foo
、/FoO
、/FOO
、/fOO
或者 /fooo
,命令不会运行。
如有需要,你可以确保命令仅在一些特定情形下注册,例如仅在专用服务器上:
public class ExampleCommandMod implements ModInitializer { @Override public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { if (environment.dedicated) { ...; } }); } }
在上面的例子中,使用了静态导入以简化代码。对于字面值,语句会简化为 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 in the CommandManager import static net.minecraft.server.command.CommandManager.*;
注意:请确保使用了 CommandManager
中的 literal
和 argument
,而非其他类中的,否则编译时存在泛型擦除问题,因为类型参数 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
能否执行命令。
例如:
dispatcher.register(literal("foo") .requires(source -> source.hasPermissionLevel(2)) .executes(ctx -> { ctx.getSource().sendFeedback(() -> Text.literal("你是 OP"), false); return 1; });
此时命令只会在命令源为 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.integer
和 IntegerArgumentType.getInteger
可以替换为 integer
和 getInteger
同时使用静态导入。为了显得更加清楚,这个例子不使用静态导入。
然后我们添加可选的第二个参数:
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>
。
dispatcher.register(literal("foo") .then(literal("bar") .executes(context -> { // 对于 1.19 以下的版本,使用 ''new LiteralText''。 // 对于 1.20 以下的版本,直接使用 ''Text'' 对象而非 supplier。 context.getSource().sendFeedback(() -> Text.literal("调用 foo 和 bar"), false); return 1; }) ) );
建议给命令添加节点时缩进你的代码,通常来说缩进对应了命令树中有多少节点的深度,每一次换行也可以看出添加了一个节点。本教程后面会展示格式化树状命令的几种可选样式。
类似于参数,子命令节点也可以设置为可选的。在下面这个例子中,/foo
和 /foo bar
都是有效的。
dispatcher.register(literal("foo") .executes(context -> { context.getSource().sendFeedback(() -> Text.literal("调用 foo 不带 bar"), false); return 1; }) .then(literal("bar") .executes(context -> { context.getSource().sendFeedback(() -> Text.literal("调用 foo 带有 bar"), false); return 1; }) ) );
以下是 brigadier 使用的更加复杂的概念的文章链接。
页面 | 描述 |
---|---|
Exceptions | 命令执行失败,并在特定的情况下留下描述性的消息。 |
Suggestions | 为客户端建议命令的输入。 |
Redirects | 允许在执行命令时使用别称或者重复元素。 |
Custom Argument Types | 在你自己的项目里面解析你自己的参数。 |
Examples | 一些示例命令 |
此问题可能有一些常见的原因。
CommandSyntaxException
不是 RuntimeException
,如果抛出,则抛出的地方所在方法必须在方法签名中也抛出 CommandSyntaxException
,或者捕获。Brigadier 可以处理此异常,并在游戏内为你提供适当的错误消息。CommandManager.literal(…)
或 CommandManager.argument(…)
而不是LiteralArgumentBuilder.literal
或 RequiredArgumentBuilder.argument
。sendFeedback
方法:你可能忘记了提供第二个参数(一个布尔值)。还需要注意,从 1.20 开始,第一个参数是 Supplier<Text>
而不是 Text
。Command
应该返回整数:注册命令时,executes
方法接受一个 Command
对象,通常是 lambda。这个 lambda 应该返回整数,而不是其他的类型。
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)
向每个玩家发送命令树。如果不发送更新的命令树,客户端可能还是会认为命令依然存在,即使服务器已经无法执行。