This is an old revision of the document!
−Table of Contents
条款:本文中的代码适用于“Creative Commons Zero v1.0 Universial”条款,允许您将文中的代码示例用于自己的模组中。
注: 本文翻译自英文版commands。
创建命令
创建命令允许模组开发者添加可以使用命令实现的功能。本教程将会教你如何注册命令,以及Brigadier的基本命令结构。
注意:本文的所有代码都是用于 1.19.2 的。对于旧的版本,一些版本和映射可能不同。
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 中,命令通常用作方法引用,例如原版的 XXXCommand
类中名叫 register
的静态方法。
这个整数相当于命令的结果。在 Minecraft 中。通常来说,负值表示命令执行失败,什么也不做,0
表示命令被略过,正数则表示命令执行成功并做了一些事情。
ServerCommandSource 是做什么的?
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”。 context.getSource().sendMessage(Text.literal("调用 /foo,不带参数。")); // 对于 1.20 之后的版本,请使用以下代码,这是为了避免在不需要反馈时也创建了文本组件对象。 context.getSource().sendMessage(() -> Text.literal("调用 /foo,不带参数")); return 1; }))); } }
CommandManager.literal(“foo”)
会告诉 brigadier,命令有一个节点,foo
这个字面的节点。
要执行命令,必须输入 /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 自己的参数类型。
Below is an example of some static imports:
// 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
中的 literall
和 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(4)) .executes(ctx -> { ctx.getSource().sendFeedback(Text.literal("你是 OP"), false); return 1; });
此时命令只会在命令源为 4 级以上时运行,否则命令不会被注册。这样做的副作用就是,非 4 级管理员会看到命令不会被 tab 补全,这也就是为什么没有启用作弊时不能够 tab 补全大多数命令。
参数
Brigadier 中的参数会解析任何输入的参数,并检查错误。Minecraft 创建了一些特殊的参数类型以用于自己使用,例如 EntityArgumentType
,表示游戏内的实体选择器 @a, @r, @p, @e[type=!player, limit=1, distance=..2]
,又如 NbtTagArgumentType
,解析字符串化的 nbt(snbt)并验证输入的语法是否正确。
TODO:详细介绍如何使用参数
子命令
要添加子命令,你需要先照常注册注册第一个字面节点。
dispatcher.register(literal("foo"))
为拥有子命令,需要把下一个节点追加到已经存在的节点后面,这是利用 then(ArgumentBuilder)
实现的,该方法接收一个 ArgumentBuilder
。
如下所示,创建命令 foo <bar>
。
dispatcher.register(literal("foo") .then(literal("bar")) );
建议给命令添加节点时缩进你的代码,通常来说缩进对应了命令树中有多少节点的深度,每一次换行也可以看出添加了一个节点。本教程后面会展示格式化树状命令的几种可选样式。
那就开始尝试运行命令
通常如果你在游戏内输入了 /foo bar
,命令不会运行,这是因为当所有条件都满足时,游戏没有可执行的代码。要解决这个问题,你需要告诉游戏如何执行这个命令,方法就是使用 executes(Command)
方法。因此一个命令看起来应该像下面这样:
dispatcher.register(literal("foo") .then(literal("bar") .executes(context -> { // 对于 1.19 以下的版本,使用 ''new LiteralText''。 context.getSource().sendMessage(Text.literal("调用 foo 和 bar")); return 1; }) ) );
高级概念
以下是 brigadier 使用的更加复杂的概念的文章链接。
页面 | 描述 |
---|---|
Exceptions | 命令执行失败,并在特定的情况下留下描述性的消息。 |
Suggestions | 为客户端建议命令的输入。 |
Redirects | 允许在执行命令时使用别称或者重复元素。 |
Custom Argument Types | 在你自己的项目里面解析你自己的参数。 |
Examples | 一些示例命令 |
常见问题
命令为什么不编译
有两种可能的原因。
捕获或抛出 CommandSyntaxException
这个问题的解决方法就是让 run
或者 suggest
方法抛出 CommandSyntaxException
。Brigadier 会处理已检查的异常,并在游戏内向你提供适当的错误消息。
泛型问题
你可能有时出现了泛型问题,请检查你的静态导入里面用的是不是 CommandManager.literal(…)
或 CommandManager.argument(…)
而非 LiteralArgumentBuilder
或 RequiredArgumentBuilder
。
可以注册客户端命令吗?
Fabric 有个 ClientCommandManager,可以注册客户端命令。
黑科技
一些可行但是不推荐的做法:
可以运行时注册命令吗?
可以这么做但是不推荐,你可以从服务器中获取 CommandManager
并向里面添加你希望添加到 CommandDispatcher
中的任何内容。
然后你需要通过 CommandManager.sendCommandTree(ServerPlayerEntity)
向每个玩家再次发送命令树,之所以要这么做,是因为客户端已经缓存了命令树并在登录过程中(或发出管理员封包时)使用,以用于本地的补全和错误消息。
可以在运行时取消注册命令吗?
可以这么做,但是这更不稳定,并且可能造成未预料的副作用。为简化事情,你需要在 brigadier 中使用反射并移除这个节点,然后还需要再次使用 sendCommandTree(ServerPlayerEntity)
向每个玩家发送命令树。如果不发送更新的命令树,客户端可能还是会认为命令依然存在,即使服务器已经无法执行。