跳到主要内容

指令

在这一章,我将以MadParticle的/madparticle指令为例,向你讲解如何编写自己的指令。


创建指令

值得注意的是,/madparticle是一条相当极端的指令,正常情况下一条指令是不会需要如此多的参数的。

以下截选自MadParticle,MadParticleCommand,GPLv3:

public class MadParticleCommand {
private static final int COMMAND_LENGTH = 40;

public MadParticleCommand(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(
Commands.literal("madparticle")
.requires(commandSourceStack -> commandSourceStack.hasPermission(2))
.redirect(dispatcher.register(Commands.literal("mp")
.requires(commandSourceStack -> commandSourceStack.hasPermission(2))
.then(Commands.argument("targetParticle", ParticleArgument.particle(Commands.createValidationContext(VanillaRegistries.createLookup())))
.then(Commands.argument("spriteFrom", EnumArgument.enumArgument(SpriteFrom.class))
.then(Commands.argument("lifeTime", new InheritableIntegerArgument(0, Integer.MAX_VALUE, COMMAND_LENGTH))
.then(Commands.argument("alwaysRender", EnumArgument.enumArgument(InheritableBoolean.class))
.then(Commands.argument("amount", IntegerArgumentType.integer())
.then(Commands.argument("spawnPos", InheritableVec3Argument.inheritableVec3(COMMAND_LENGTH))
.then(Commands.argument("spawnDiffuse", Vec3Argument.vec3(false))
.then(Commands.argument("spawnSpeed", InheritableVec3Argument.inheritableVec3(COMMAND_LENGTH))
.then(Commands.argument("speedDiffuse", Vec3Argument.vec3(false))
.then(Commands.argument("collision", EnumArgument.enumArgument(InheritableBoolean.class))
.then(Commands.argument("bounceTime", InheritableIntegerArgument.inheritableInteger(0, Integer.MAX_VALUE, COMMAND_LENGTH))

以上是指令的核心部分:我们需要编写一个方法来处理传入的CommandDispatcher<CommandSourceStack> dispatcher

提示

在这里我用的是MadParticleCommand类的构造方法。我更建议使用一个静态方法来做这件事,将指令类本身更多地作为一个工具类。

你可能注意到了,一条指令是以dispatcher.register(...打头的,随后使用Commands类中的各种方法来按需接续,即注册子节点。

第一个子节点通常是Commands.literal。顾名思义,在游戏内执行指令时需要把literal的内容完整打进去,即Commands.literal("madparticle")在游戏中就会有/madparticle

随后你就需要按你自己的需求添加后面的内容。我们先来介绍操作子节点的几个常用方法:

  • .requires可以通过一个Predicate<CommandSourceStack>来判断一条指令能不能被执行。经常会用到的一种情形是判断玩家(或其他指令执行者,比如命令方块)是否有足够的权限等级来执行这条指令:如果你的这条指令可能会造成广泛的影响,或是可能会影响他人游戏,你就应该给它设置一个合适的执行权限等级。
信息

权限等级为0-4。0最低,4最高。

  • 允许作弊的单人游戏里,玩家的权限等级是4。
  • 多人游戏里,如果玩家是op,他的权限等级由ops.json中的值确定,默认是4。普通玩家的权限等级是0。
  • 命令方块的权限等级是2。
  • .redirect可以将指令重定向到其他指令。以下是几个典型场景。

    • Commands.literal("mytp").redirect(dispatcher.register(Commands.literal("teleport")))就可以让/mytp拥有和/teleport相同的效果。
    • 由此有一种Commands.literal("madparticle").redirect(dispatcher.register(Commands.literal("mp"),来为你的指令创建别名。
    • .redirect(pDispatcher.getRoot()))可以在此之后开始一条新的指令,就像/excuterun一样。
  • .then非常容易理解:开始一个新的子节点。

  • .executes即代表了玩家填写完这个节点后,如果敲下回车,会执行的内容。你需要填写一个Command<CommandContext<CommandSourceStack>>,类似一个Consumer<CommandContext<CommandSourceStack>>。最常见的用法是.executes((ct) -> yourMethod(ct)),通常这个yourMethod是你的Command类里面的一个静态方法。即使你需要执行的内容很短,我也不建议你以闭包的形式将其直接写在executes()的括号内——这么一串指令已经足够混乱了。

等等,指令参数呢?

.literal相对应地,你可以使用.argument来增加一个参数子节点。.argument的第一个形参是你的参数名称,它将会出现在对话框写指令的提示中,也是在后面获得对应值的关键字。

第二个形参则是这个参数的类型,你需要按需填入对应的参数类型实例。你可以打开ArgumentType<T>ctrl+H来查看所有可用的参数类型。值得注意的是,要想获得对应的参数类型实例,你可能需要使用其提供的静态工厂方法,而不是直接new。

要获得参数对应的值也很简单:使用context.getArgument(name, clazz),填入你的参数名称和参数值类型(比如Integer.class,而不是IntegerArgumentType)即可。

在少数情况下,参数值类型可能不直接是参数类型——例如实体选择器什么的。这种参数类型里通常提供一个get(CommandContext<CommandSourceStack> context, String name),你只需要传入context和上面填写的名称就可以了。

提示

如果你的执行方法yourMethod需要参数,那么有两种写法:

  1. 使用.executes((ct) -> yourMethod(ct, <获取参数>)),将指令参数获取到后作为形参传入;
  2. 使用.executes((ct) -> yourMethod(ct)),在方法内再获取参数。

一般两种做法都是可以的,但如果有EntityArgument.getPlayers(...) throws CommandSyntaxException这种会抛错误的,我建议使用第一种,将潜在的错误丢回去。

注册指令

指令写好之后也是需要注册的。不过很简单,监听RegisterCommandsEvent即可:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
public class ModCommandRegister {
@SubscribeEvent
public static void regCommand(RegisterCommandsEvent event) {
new MadParticleCommand(event.getDispatcher());
}
}

其实新指令就是写个方法把传进来的dispatcher折腾一番。


自定义参数类型

一般有两种情况你需要用到自定义参数类型:你发现现有的ArgumentType子类不够用,想要有新类型的参数值;你想修改某一个现有的ArgumentType实现。

信息

麻将在之前将MC的指令部分抽出来,独立成为了**Brigadier**。这使得你不能使用Mixin修改其中的内容,而继承后重写方法便成为了这种情况下的好选择。

Brigadier使用的是MIT许可证,所以尽管继承重写。

要编写自己的新参数类型,你只需要继承ArgumentType<T>即可。T即为参数解析后的返回值。随后你只需要完成一个方法:parse(StringReader reader)。由于我也不知道你想要解析出来啥,parse的内容就不在此赘述,翻看相近的原版实现即可。

要修改已有的参数类型实现,你只需要继承对应的参数类型,然后Override即可。

注册自定义参数类型

你自己定义的参数类型也需要注册,而与物品方块什么不同的是,参数类型需要在两个地方都注册:

节选自MadParticle, ModCommandArgumentRegistry:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModCommandArgumentRegistry {

private static SingletonArgumentInfo<InheritableIntegerArgument> inheritableIntegerArgumentInfo = SingletonArgumentInfo.contextFree(InheritableIntegerArgument::inheritableInteger);

@SubscribeEvent
public static void onCommandArgumentReg(FMLCommonSetupEvent event) {
event.enqueueWork(() -> {
ArgumentTypeInfos.registerByClass(InheritableIntegerArgument.class, inheritableIntegerArgumentInfo);
});
}


public static DeferredRegister<ArgumentTypeInfo<?, ?>> COMMAND_ARGUMENTS = DeferredRegister.create(ForgeRegistries.COMMAND_ARGUMENT_TYPES, MadParticle.MOD_ID);

public static RegistryObject<ArgumentTypeInfo<?, ?>> INHERITABLE_INT = COMMAND_ARGUMENTS.register("inheritable_integer", () -> inheritableIntegerArgumentInfo);
}

别忘了在主类中注册DeferredRegisterModCommandArgumentRegistry.COMMAND_ARGUMENTS.register(modBus);


奇奇怪怪的注意事项

一个指令的解析过程

下面是简要的指令解析过程讲解:

指令的执行可以看作是从Commands.performPrefixedCommand开始的。

CommandDispatcher.parse(StringReader command, S source)接受传入的StringReaderCommandSourceStackStringReader含有这条指令的全部内容,CommandSourceStack则有指令发起源的相关信息。

CommandDispatcher将会从指令的根节点开始,递归地逐渐深入解析,得到一个ParseResults,组装成为CommandContext<CommandSourceStack>,在需要时传递给你在execute中指定的方法。

CommandContext中有一个Map<String, ParsedArgument<S, ?>> arguments,存储着指令参数名称与对应的值。

注意

CommandContext<CommandSourceStack>是可嵌套的,所以如果你用的是上面获取参数的第二种“在方法内再获取参数”,则需要尤其注意。

如果有这样一条指令/excute as @p run yourcommand 10,你直接使用ct.getArgument("yourParameter", Integer.class)很可能会吃到一个No such argument的大比兜子。使用ct.getChild().getArgument("yourParameter", Integer.class)才能正常地获得这个10

不过Child一层又一层,要get到哪一层呢?一个简单的工具方法即可。MadParticle, CommandHelper:

public static <S, C> @Nullable CommandContext<S> getContextHasArgument(CommandContext<S> root, String argument, Class<C> clazz) {
CommandContext<S> now = root;
while (true) {
try {
now.getArgument(argument, clazz);
break;
} catch (IllegalArgumentException e) {
if (now.getChild() == null) {
return null;
} else {
now = now.getChild();
}
}
}
return now;
}

我本人很不喜欢递归,故在这里用的是循环。

两端与性能问题

MC原版提供的指令都是服务端指令,客户端输入的内容被传到服务端,解析并执行。Forge提供了方法来编写客户端指令,在此处我们不涉及。

显然服务端的性能是很宝贵的——但是指令的解析看起来也不怎么快。

如果你的指令执行方法可能会消耗更多时间,例如mp这样需要把指令重新解析一遍,然后读一堆东西来发包的,你可以利用CompletableFuture.runAsync来在其他线程上执行具体内容。不过你需要小心跨线程问题。

还有一种mp用到的方法,但是不太文明:直接在Commands.performPrefixedCommand就把指令执行截胡,如果是你的指令,就可以做一些优化。MadParticle, CommandsMixin:

@Mixin(Commands.class)
public class CommandsMixin {

@Final
@Shadow
private CommandDispatcher<CommandSourceStack> dispatcher;

@Inject(method = "performPrefixedCommand", at = @At(value = "HEAD"), cancellable = true)
private void madParticleOptimize(CommandSourceStack pSource, String pCommand, CallbackInfoReturnable<Integer> cir) {
if (pCommand.startsWith("mp ") || pCommand.contains(" mp ")) {
if (pSource.hasPermission(2)){
CompletableFuture.runAsync(() -> MadParticleCommand.fastSend(pCommand, pSource, pSource.getLevel().getPlayers(serverPlayer -> true), dispatcher));
}
cir.setReturnValue(Command.SINGLE_SUCCESS);
}
}
}

不过这样其实比较危险,判断条件的设置很重要。要是别人有什么指令恰好符合了你的判断条件,那就坏了。使用此方法则保修失效。