User Tools

Site Tools


zh_cn:tutorial:commands

This is an old revision of the document!


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

注: 本文翻译自英文版commands

创建命令

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

注意:本文的所有代码都是用于 1.19.2 的。对于旧的版本,一些版本和映射可能不同。

Brigadier 是什么?

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

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

''Command'' 接口

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

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

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

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

在原版 Minecraft 中,命令通常用作方法引用,例如原版的 XXXCommand 类中名叫 register 的静态方法。

这个整数相当于命令的结果。在 Minecraft 中,这个整数对应从命令方块中输出到红石比较器的红石能量,或者传入命令方块朝向的连锁命令方块。通常来说,负值表示命令执行失败,什么也不做,0 表示命令被略过,正数则表示命令执行成功并做了一些事情。

ServerCommandSource 是做什么的?

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

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

CommandManager.literal(“foo”) 会告诉 brigadier,命令有一个节点,foo这个字面的节点。

要执行命令,必须输入 /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 自己的参数类型。

Below is an example of some static imports:

  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 中的 literallargument,而非其他类中的,否则编译时存在泛型擦除问题。

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(4))
  3. .executes(ctx -> {
  4. ctx.getSource().sendFeedback(Text.literal("你是 OP"), false);
  5. return 1;
  6. });

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

参数

Brigadier 中的参数会解析任何输入的参数,并检查错误。Minecraft 创建了一些特殊的参数类型以用于自己使用,例如 EntityArgumentType,表示游戏内的实体选择器 @a, @r, @p, @e[type=!player, limit=1, distance=..2],又如 NbtTagArgumentType,解析字符串化的 nbt(snbt)并验证输入的语法是否正确。

TODO:详细介绍如何使用参数

子命令

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

  1. dispatcher.register(literal("foo"))

为拥有子命令,需要把下一个节点追加到已经存在的节点后面,这是利用 then(ArgumentBuilder) 实现的,该方法接收一个 ArgumentBuilder

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

  1. dispatcher.register(literal("foo")
  2. .then(literal("bar"))
  3. );

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

那就开始尝试运行命令

通常如果你在游戏内输入了 /foo bar,命令不会运行,这是因为当所有条件都满足时,游戏没有可执行的代码。要解决这个问题,你需要告诉游戏如何执行这个命令,方法就是使用 executes(Command) 方法。因此一个命令看起来应该像下面这样:

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

高级概念

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

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

FAQ

Why does my command not compile

There are two immediate possibilities for why this could occur.

Catch or throw a CommandSyntaxException

The solution to this issue is to make the run or suggest methods throw a CommandSyntaxException. Brigadier will handle the checked exceptions and forward the proper error message in game for you.

Issues with generics

You may have an issue with generic types once in a while. Verify you are using CommandManager.literal(…) or CommandManager.argument(…) instead LiteralArgumentBuilder or RequiredArgumentBuilder in your static imports.

Can I register client side commands?

Fabric has a ClientCommandManager that can be used to register client side commands.

Dark Arts

A few things we don't recommend, but are possible.

Can I register commands in runtime?

You can do this but it is not recommended. You would get the CommandManager from the server and add anything commands you wish to it's CommandDispatcher.

After that you need to send the command tree to every player again using CommandManager.sendCommandTree(ServerPlayerEntity). This is required because the client locally caches the command tree it receives during login (or when operator packets are sent) for local completions rich error messages.

Can I unregister commands in runtime?

You can also do this, however it is much less stable than registering commands and could cause unwanted side effects. To keep things simple, you need to use reflection on brigadier and remove the nodes. After this, you need to send the command tree to every player again using sendCommandTree(ServerPlayerEntity). If you don't send the updated command tree, the client may think a command still exists, even though the server will fail execution.

zh_cn/tutorial/commands.1662274430.txt.gz · Last modified: 2022/09/04 06:53 by solidblock