命令
命令是一种文本操作,可以在聊天中以斜杠(/)开头输入消息触发,也可以从服务器控制台、命令方块或函数触发。命令在游戏中产生某种效果,例如修改世界、查询状态或提供调试工具。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。