指令
在这一章,我将以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()))
可以在此之后开始一条新的指令,就像/excute
的run
一样。
-
.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
需要参数,那么有两种写法:
- 使用
.executes((ct) -> yourMethod(ct, <获取参数>))
,将指令参数获取到后作为形参传入; - 使用
.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);
}
别忘了在主类中注册DeferredRegister
:ModCommandArgumentRegistry.COMMAND_ARGUMENTS.register(modBus);
奇奇怪怪的注意事项
一个指令的解析过程
下面是简要的指令解析过程讲解:
指令的执行可以看作是从Commands.performPrefixedCommand
开始的。
CommandDispatcher.parse(StringReader command, S source)
接受传入的StringReader
和CommandSourceStack
。StringReader
含有这条指令的全部内容,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);
}
}
}
不过这样其实比较危险,判断条件的设置很重要。要是别人有什么指令恰好符合了你的判断条件,那就坏了。使用此方法则保修失效。