命令

命令

命令是一种文本操作,可以在聊天中以斜杠(/)开头输入消息触发,也可以从服务器控制台、命令方块或函数触发。命令在游戏中产生某种效果,例如修改世界、查询状态或提供调试工具。Mod 可以定义自己的命令,在调用时执行动态操作。

命令定义具有层级结构。Minecraft 使用 Mojang 的 Brigadier 库构建命令,将每条命令表示为一棵以分发器为根的节点树。字面量节点匹配固定关键字,例如 /mymod reload 中的 mymod 或 reload;参数节点匹配具有名称和类型的输入,例如数字或实体选择器。使用 #then 连接节点,使用 #executes 使节点可执行。节点可以持有执行逻辑、子节点,或同时持有两者。这种嵌套形成了子命令树。

参数用于两个位置:构建树时,提供名称和 ArgumentType 来声明参数;执行逻辑中,再使用相同名称从 CommandContext 获取解析后的值。名称是两者之间的约定。下面分别介绍这两部分。

注册命令

在 RegisterCommandsEvent 期间,将命令树传给 CommandDispatcher#register,例如:

@SubscribeEvent // on the game event bus
public static void registerCommands(RegisterCommandsEvent event) {
    event.getDispatcher().register(
        Commands.literal("mymod")
            .then(Commands.literal("reload")
                .executes(context -> {
                    // Perform the command logic here
                    return Command.SINGLE_SUCCESS;
                })
                .then(Commands.literal("force")
                    .executes(context -> {
                        // Perform the command logic here
                        return Command.SINGLE_SUCCESS;
                    })
                )
            )
    );
}

Commands#literal 创建字面量构建器,#then 连接子节点,#executes 提供 Command 回调。回调返回的 int 是向调用者报告的结果计数,通常表示影响了多少名玩家。上例注册了 /mymod reload 和 /mymod reload force。

也可以通过 RegisterClientCommandsEvent 而非 RegisterCommandsEvent 注册客户端命令。这些命令仅在客户端一端显示并运行,服务器无法使用。

提示 更高级的命令树如果需要访问注册表,可以使用 RegisterCommandsEvent#getBuildContext 提供的 CommandBuildContext,将其传给需要该上下文的参数类型。

权限

使用 #requires 添加权限条件,提供一个针对 CommandSourceStack 的谓词;调用者可以执行命令时返回 true。谓词在服务器上求值,因此可以检查调用者的权限等级或其他状态。

Commands.literal("op-me")
        .requires(Commands.hasPermission(Commands.LEVEL_OWNERS))
        .executes(context -> {
            // Perform the command logic here
            return Command.SINGLE_SUCCESS;
        })

提示 Commands#hasPermissions 是一个辅助方法,用于检查调用者权限等级是否达到指定阈值。等级在 Commands 中定义为 LEVEL_GAMEMASTERS、LEVEL_ADMINS 和 LEVEL_OWNERS。

也可以提供自定义 PermissionCheck。

参数

使用 Commands#argument 添加参数节点,提供参数名称和 ArgumentType。在 #executes 中,通过与所选类型配套的静态 getter,并以相同名称为键,读取解析后的值。

Commands.literal("give")
    .then(Commands.argument("count", IntegerArgumentType.integer(1))
        .executes(context -> {
            // Access the value declared above as "count"
            int count = IntegerArgumentType.getInteger(context, "count");

            // Use count...

            return count;
        }))

每个 ArgumentType 都提供声明参数时使用的构建器,通常还提供获取解析值的静态 getter。如果名称与当前路径上的已声明参数不符,getter 会抛出异常,因此声明名称与获取名称必须一致。

提示 如果 ArgumentType 没有提供静态 getter,可以通过 CommandContext#getArgument,传入参数名称和预期类,获取解析后的值。

参数并非通过某个标志变为可选。应当在命令树的多个深度附加 #executes:在父节点上处理没有参数的情况,在参数节点上处理提供参数的情况。

参数类型

下表列出常用参数类型。每一行都将声明参数的构建器与获取解析值的静态 getter 对应起来。

Brigadier

Brigadier 在 com.mojang.brigadier.arguments 中提供基础类型。

参数类型 声明(构建器) 获取(getter) 描述
BoolArgumentType #bool #getBool 布尔值。
IntegerArgumentType #integer(可选最小、最大值) #getInteger 32 位整数。
LongArgumentType #longArg(可选最小、最大值) #getLong 64 位整数。
FloatArgumentType #floatArg(可选最小、最大值) #getFloat 单精度小数。
DoubleArgumentType #doubleArg(可选最小、最大值) #getDouble 双精度小数。
StringArgumentType #word、#string 或 #greedyString #getString 文本,见下方说明。

提示 StringArgumentType 的三个构建器读取的文本范围不同。#word 读取一个不带引号的词,在第一个空格处结束。#string 也读取一个词,但如果输入用双引号包围(如 "hello world"),就会读取整段引号内的文本。#greedyString 读取参数后的全部内容,包括空格,因此后面不能再跟其他节点。

Minecraft 内置类型

Minecraft 在 net.minecraft.commands.arguments 中添加了游戏专用参数类型。以下并非完整列表。

参数类型 声明(构建器) 获取(getter) 描述
EntityArgument #entity、#entities、#player、#players #getEntity、#getEntities、#getOptionalEntities、#getPlayer、#getPlayers、#getOptionalPlayers 按名称、UUID 或 @p、@e 等选择器选取一个或多个实体或玩家。
BlockPosArgument #blockPos #getLoadedBlockPos、#getBlockPos 方块位置。
Vec3Argument #vec3 #getVec3 世界中的位置。
ItemArgument #item #getItem 包含数据组件的物品堆。
BlockStateArgument #block #getBlock 包含方块实体数据的方块状态。
ResourceArgument #resource #getResource 指向指定注册表条目的 Identifier。
ResourceKeyArgument #key #getRegistryKey 指定注册表的 ResourceKey。

NeoForge

NeoForge 在 net.neoforged.neoforge.server.command 中添加了额外参数类型。

参数类型 声明(构建器) 获取(getter) 描述
EnumArgument #enumArgument CommandContext#getArgument,传入对应名称和枚举类 枚举值。
ModIdArgument #modIdArgument CommandContext#getArgument,传入对应名称和 String.class Mod ID。

自定义参数类型

如果现有类型都无法解析所需值,可以实现 ArgumentType<T> 创建自定义参数类型,从 StringReader 读取原始命令文本并返回 T 类型的值。

// Spell is a custom type for the sake of this example.
public class SpellArgument implements ArgumentType<Spell> {
    private static final DynamicCommandExceptionType ERROR_UNKNOWN_SPELL = new DynamicCommandExceptionType(
            name -> Component.translatableEscape("commands.examplemod.spell.unknown", name)
    );

    // Follows the naming scheme of the vanilla argument types, used when declaring the argument
    public static SpellArgument spell() {
        return new SpellArgument();
    }

    // Consumes as much of the input as the argument needs
    @Override
    public Spell parse(StringReader reader) throws CommandSyntaxException {
        String name = reader.readUnquotedString();
        // Validate the supplied spell exists, throwing ERROR_UNKNOWN_SPELL#createWithContext if not
    }

    // Optional, defaults to no suggestions: the completions offered while typing the argument,
    // see the note below
    @Override
    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
        // Suggest the names of the available spells or none
        return Suggestions.empty();
    }

    // Optional, defaults to an empty list: example inputs used by Brigadier to detect
    // ambiguities between sibling nodes
    @Override
    public Collection<String> getExamples() {
        return List.of("fireball", "heal");
    }
}

提示 玩家输入时,客户端解析同步过来的命令树,并调用 listSuggestions。因此,它只能建议客户端已有的数据,例如已同步注册表中的条目。只有服务器知道的数据,必须先通过自定义负载发送给客户端,才能在这里建议;也可以在参数节点上通过 #suggests 由服务器提供建议。

新类型没有现成的静态 getter,因此通常在构建器旁添加一个,让访问方式与内置类型相似:

public static Spell getSpell(CommandContext<CommandSourceStack> context, String name) {
    return context.getArgument(name, Spell.class);
}

建议 如果现有参数类型已经能够解析目标值,只是建议需要不同,可以在参数节点上用 #suggests 替换建议,无需自定义类型。这种方式添加的建议会在玩家输入时向服务器请求。

同步

服务器会将命令树发送给每个客户端,使输入中的命令能够在本地解析和补全。每个参数节点由 ArgumentTypeInfo 描述,并根据 ArgumentType 的类从 COMMAND_ARGUMENT_TYPE 注册表中查找。上表中的类型都已具有对应描述;没有描述的自定义类型无法发送给客户端。

ArgumentTypeInfos#registerByClass 将 ArgumentType 类与其描述关联并返回描述,因此可以在同一条语句中注册:

public static final DeferredRegister<ArgumentTypeInfo<?, ?>> COMMAND_ARGUMENT_TYPES =
        DeferredRegister.create(Registries.COMMAND_ARGUMENT_TYPE, ExampleMod.MOD_ID);

public static final DeferredHolder<ArgumentTypeInfo<?, ?>, SingletonArgumentInfo<SpellArgument>> SPELL = COMMAND_ARGUMENT_TYPES.register(
        // The registry name of the argument type.
        "spell",
        () -> ArgumentTypeInfos.registerByClass(
                // The class of the argument type.
                SpellArgument.class,
                // The info describing it. 'contextFree' is used for argument types
                // constructed without parameters, 'contextAware' for those requiring
                // a CommandBuildContext.
                SingletonArgumentInfo.contextFree(SpellArgument::spell)
        )
);

如果参数类型本身持有参数(例如 IntegerArgumentType 的上下界),客户端也需要这些参数,因此不能使用 SingletonArgumentInfo。应直接实现 ArgumentTypeInfo,并实现持有参数的 ArgumentTypeInfo.Template:#serializeToNetwork 与 #deserializeFromNetwork 传输模板,#serializeToJson 将模板写入命令树的 JSON 表示,#unpack 从参数类型创建模板,Template#instantiate 从模板创建参数类型。EnumArgument.Info 是这种实现的一个简单示例。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。