条款:本文中的代码适用于“Creative Commons Zero v1.0 Universial”条款,允许您将文中的代码示例用于自己的模组中。 注: 本文翻译自英文版[[tutorial:commands]]。 ====== 创建命令 ====== 创建命令允许模组开发者添加可以使用命令实现的功能。本教程将会教你如何注册命令,以及 Brigadier 的基本命令结构。 ===== Brigadier 是什么? ===== Brigadier 是由 Mojang 写的用于 Minecraft 的命令解析器和派发器。Brigadier 是基于树的命令库,可以建立参数和命令的树。 这是 Brigadier 的源代码:https://github.com/Mojang/brigadier ===== Command 接口 ===== 在 Minecraft 中,''com.mojang.brigadier.Command''是函数型接口,会运行一些特定的内容,并在有些情况下抛出 ''CommandSyntaxException''。命令有一个泛型 ''S'',定义了命令的源(command source),命令源提供了命令运行的一些环境。在 Minecraft 中,命令源通常是 '''',代表一个服务器、命令方块、rcon 连接、玩家或者实体,有时也可以有 ''''。 ''Command'' 接口中的唯一方法,''run(CommandContext)'',接受一个 ''CommandContext'' 作为唯一参数,并返回一个整数。命令环境(command context)存储 ''S'' 的命令源,并允许你从中获取参数、查询已解析的命令节点,并看到命令中的输入。 就像其他的函数型接口那样,命令通常用于匿名函数或者方法引用: Command command = context -> { return 0; }; 这个整数相当于命令的结果。在 Minecraft 中。通常来说,负值表示命令执行失败,什么也不做,''0'' 表示命令被略过,正数则表示命令执行成功并做了一些事情。 ==== ServerCommandSource 是做什么的? ==== ''ServerCommandSource'' 提供了命令运行时的一些额外环境,这些环境拥有特定的实现,包括获取运行这个命令的实体、命令执行时所在的世界以及服务器。 // 获取命令源。这总是生效。 final ServerCommandSource source = ctx.getSource(); // 未经检查,如果是由控制台或命令方块执行的,则会是 null。 final @Nullable Entity sender = source.getEntity(); // 如果命令执行者不是实体,就会抛出错误。 // 这个的结果可能包含玩家,并且会发送反馈,告诉命令的发送者必须有一个实体。 // 这个方法会要求你的方法能够抛出 CommandSyntaxException。 // ServerCommandSource 中的 entity 选项可以返回一个 CommandBlock 实体、生物实体或者玩家。 final @NotNull Entity sender2 = source.getEntityOrThrow(); // 如果命令执行者不是玩家,则为 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” final String name = source.getName(); // 如果命令源拥有特定的权限等级,则返回 true,这基于发送者的管理员级别。(在内置服务器上,玩家必须启用了作弊才能执行这些命令。) final boolean b = source.hasPermissionLevel(int level); ===== 注册一个基本的命令 ===== 命令可以通过 Fabric API 的 ''CommandRegistrationCallback'' 进行注册,关于如何注册回调,请参见 [[callbacks]]。 这个事件必须在你的模组的初始化器中注册。这个回调有三个参数。''CommmandDispatcher'' 用于注册、解析和执行命令,''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'' 对象,因此请不要使用 ''Suppliers.ofInstance'' 或类似方法)。第二个参数决定了命令是否要将反馈的内容发送给其他的管理员。如果命令是//查询//一些内容,比如查询当前的时间或者某玩家的分数,则应该是 ''false''。如果命令实际上//做了些//事情,例如修改时间或者分数,那么则应该是 ''true''。如果游戏规则 ''sendCommandFeedback'' 是 false,那么你不会收到反馈。如果命令执行者被通过 ''/execute as ...'' 修改,反馈则会发送给原始的执行者。 如果命令失败,可以不必调用 ''sendFeedback'',而是直接抛出 ''CommandSyntaxException'' 或 ''''。具体请参见 [[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.field_25423) { ...; } }); } } ===== 静态导入 ===== 在上面的例子中,使用了静态导入以简化代码。对于字面值,语句会简化为 ''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'' 以检测 ''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 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 ''。 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 使用的更加复杂的概念的文章链接。 ^ 页面 ^ 描述 ^ | [[command_exceptions |Exceptions]] | 命令执行失败,并在特定的情况下留下描述性的消息。 | | [[command_suggestions|Suggestions]] | 为客户端建议命令的输入。 | | [[command_redirects|Redirects]] | 允许在执行命令时使用别称或者重复元素。 | | [[command_argument_types|Custom Argument Types]] | 在你自己的项目里面解析你自己的参数。 | | [[command_examples|Examples]] | 一些示例命令 | ====== 常见问题 ====== ===== 代码为什么不编译 ===== 此问题可能有一些常见的原因。 * **捕获或抛出 CommandSyntaxException:**''CommandSyntaxException'' 不是 ''RuntimeException'',如果抛出,则抛出的地方所在方法必须在方法签名中也抛出 ''CommandSyntaxException'',或者捕获。Brigadier 可以处理此异常,并在游戏内为你提供适当的错误消息。 * **泛型问题:**你可能遇到了泛型问题。如果你在注册服务器命令(大多数情况都是如此),确保你在静态导入中使用 ''CommandManager.literal(...)'' 或 ''CommandManager.argument(...)'' 而不是''LiteralArgumentBuilder.literal'' 或 ''RequiredArgumentBuilder.argument''。 * **检查 ''sendFeedback'' 方法:**你可能忘记了提供第二个参数(一个布尔值)。还需要注意,从 1.20 开始,第一个参数是 ''Supplier'' 而不是 ''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)'' 向每个玩家发送命令树。如果不发送更新的命令树,客户端可能还是会认为命令依然存在,即使服务器已经无法执行。