Skip to content

注解模块

defoli_ation edited this page Jul 17, 2019 · 19 revisions

使用Command注解来做一个命令

@Command("test")
public void test(String text) {
    //TODO
}

这就完成了命令的主要部分

如果你需要获得命令的发送者那么你可以这样做

@Command("test")
public void test(@Sender CommandSender sender,String text) {
    //TODO
}

为什么需要@Sender这个注解? -- 因为如果没有这个注解会产生歧义,AnnotationCommand系统会认为这个sender是用户需要输入的部分,但实际上不是。

返回值

在第一个例子中,方法返回了void,那么在默认情况下,只要方法不抛出异常,那么命令的执行就不会被认为失败
如果你希望这条命令执行失败,那么你可以返回boolean以告诉AnnotationCommand系统执行失败了

@Command("test")
public boolean test(String text) {
    return false;
}

但有时候仅仅靠boolean提示失败显然不够明确,这时你可以返回CommandResult以明确的提示执行者你的命令执行的结果

@Command("test")
public CommandResult test(String text) {
    return new CommandResult(false,"some reason");
}

同命令多参数

先看看下面这个命令

@Command("test")
public void test(String text) {
    //TODO
}

可以看到,这个命令的形参是比较单一的,这时我希望多加一些形参以执行更酷的指令,但又不希望添加更多的根命令,那么你只需要把你需要的往里加就好了

@Command("test")
public void test(String text,int index) {
    //TODO
}

假如用户输入了 "/test abc" 那么就会执行第一个命令,假如用户输入了"/test abc 123" 那么就会执行第二个命令

优先级

不同形参带来了更酷的指令执行方式,但是同时也带来了问题 先看以下两个命令

@Command("test")
public void test(String text) {
    //TODO
}


@Command("test")
public void test(int index) {
    //TODO
}

由于两个命令的形参不同,所以很显然这当用户输入文本或者数值时,会分别调用这两个方法。但...真的是这样么?
我们知道,在实际操作中,用户输入的数据永远是String,所以即使用户输入了数值,那么也只是以String方式表达的数值,这时就有可能造成预期的事情无法发生,例如用户输入了数值但调用了第一个方法。
我们的解决方法是优先级,在优先级的控制下,String永远是最后一个用于解析的类
即我们会先看看这个文本是不是int,是不是float,是不是boolean ... 在最后我们才会看他会不会是String,而这永远会是正确的。

@Sender带来的歧义

先看看这两个命令

@Command("test")
public void test(String text) {
    //TODO
}


@Command("test")
public void test(@Sender CommandSender sender,String text) {
    //TODO
}

可以明确的看出,第二个命令多了一个Sender,但当用户使用了这个命令 "/test abc",他会调用哪个方法呢?
答案是两个都有可能被调用,第一个方法不需要Sender,他只需要一个String,abc正好满足他的要求。第二个方法需要Sender,但只要用户调用命令,那么这个值永远会是被满足的,因为调用命令的家伙必须是CommandSender。所以结果是无法预期的。
所以在这种情况下,AnnotationCommand规定,假设这两个方法存在,那么一定会调用没有Sender的那个。

注册

现在我们终于做好了命令,我们希望他能够被用户使用,那么我们该怎么做呢?

AnnotationCommand内置了一个AnnotationCommandBuilder,我们使用它来完成命令的解析

获得AnnotationCommandBuilder并注册

AnnotationCommand
.getBuilder(CommandManager)
.addCommandHandler(commandHandler)
.addCommandHandler(otherHandler)
.register();

其中addCommandHandler就是那些拥有命令方法的类
可以看到,获得Builder必须要有给一个CommandManager,这是因为AnnotationCommand需要检查这个命令被注册了没有,以为其添加更多参数,避免覆盖。
需要注意的是,如果一个命令已被注册且不是被AnnotationCommandBuilder注册的,那么就会抛出异常

更多注解

@Command

在之前我们只是看到了Command的其中一个属性 他还有另外两个属性
String desc() default "";
String helpMessage() default "";
desc --- 解释这个命令干什么用的
helpMessage --- 当命令出错时的提示信息

@Permission

先看以下命令

@Command("test")
public void test(String text) {
    //TODO
}

我们希望这个命令只能被某些有权利的人执行,那么我们就需要这样做

@Command("test")
@Permission("command.test")
public void test(String text) {
    //TODO
}

请注意,Permission的属性是array即可以要求多个权限

@Command("test")
@Permission({"command.test","isOp"})
public void test(String text) {
    //TODO
}

@Sender

请看以下命令

@Command("test")
public void test(@Sender CommandSender sender) {
    //TODO
}

我们希望这个命令只能由Player发出,那么我们可以将CommandSender修改为Player

@Command("test")
public void test(@Sender Player sender) {
    //TODO
}

这样在命令执行过程中,AnnotationCommand将为你自动检测。
但...这样似乎还不够酷,我们希望一个命令可以同时由两个及以上的类执行,例如可以被Player和Console执行,但其他的就不行,Sender一样能满足你的要求
你只需要按下面所做即可

@Command("test")
public void test(@Sender({Player.class,Console.class}) CommandSender sender) {
    //TODO
}

需要注意的是,形参需要是他们的共同父类,否则将不可避免的会出现问题。

@Required

先看以下命令

@Command("test")
public void test(String text) {
    if(text == "build")
        //TODO
    else if(text == "delete")
        //TODO
}

从这个方法可以看出,当text是某个特定的文本时就会做什么,但是这样太繁琐了,尤其当条件非常多时,代码将变得异常冗长和难看。
AnnotationCommand为你带来了新的解决方式,使用这个注解,以上的方法将变成

@Command("test")
public void test(@Required("build") String a) {
    //TODO
}

@Command("test")
public void test(@Required("delete") String a) {
    //TODO
}

@ArgumentHandler

AnnotationCommand是如何解析那些字符串并将他们准确转为你需要的类的?就是依靠Argument。
当你在注册时,ArgumentCommandBuilder就会生成一个默认的ArgumentManager以供使用 如果你需要自定义ArgumentManager,可以调用

ArgumentCommandBuilder.setArgumentManager(ArgumentManager)

方法。

你用以处理命令的方法其实就已经指定了用以处理的Argument,AnnotationCommand会用你的形参的类的Class在ArgumentManager中寻找对应的Argument。如果没有找到,那么就会报异常。

你当然可以通过ArgumentManager设置某个Class默认的Argument,但有时也许只有一个地方需要,而其他地方只需要用到默认的Argument,这时就可以使用我们的@ArgumentHandler

    @Command("command")
    public void command2(@ArgumentHandler("test") String a){

    }

通过这个注解,你可以告诉AnnotationCommand,使用名字来寻找Argument而非Class

自定义类的支持

我们有如下命令

    @Command("sendMessage")
    public void test(Player player,String message){
            player.sendMessage(message);
    }

假如你直接拿到AnnotatioCommand中注册,你会发现注册失败,并告诉你,AnnotationCommand并不认识Player这个玩意。
这是因为AnnotationCommand中默认的ArgumentManager并不认识Player。
你需要在AnnotationCommandBuilder时就为其设置ArgumentManager,并往ArgumentManager里添加你用以解析Player的Argument。 像这样

    SimpleArgumentManager argumentManager = new SimpleArgumentManager();

    argumentManager.appendArgument(new SimpleArgument(Player.class,"player") {
        @Override
        public Optional parse(String arg) {
            return Optional.ofNullable(Server.getPlayer(arg));
        }

        @Override
        public Completer getCompleter() {
            //Completer是一个复杂的系统,需要另开一页来讲解
    });
    
    AnnotationCommand
            .getBuilder(commandManager)
            .setArgumentManager(argumentManager)
            .addCommandHandler(handler)
            .register();

这样在注册命令时,AnnotationCommand就会正确识别你的类了。

多参数类

对于Player来说,一个参数就可以明确对象,使用Argument是最好的选择,但是很多时候,一个类是需要很多参数才能明确的,例如Location

Location中有这样一个构造方法

public class Location{

    public Location(World world,int x,int y,int z)

}

很明显,一个Argument是没法搞出一个Location的,除非你希望玩家输入蹩脚的/teleport world,x,y,z
而我们更希望玩家输入/teleport world x y z

但在代码层面相较于

    @Command("teleport")
    public void command1(@CommandSender Player player,World world,int x,int y,int z){
            Location location = new Location(world,x,y,z)
            //teleport
    }

显然我们更希望的是

    @Command("teleport")
    public void command1(@CommandSender Player player,Location location){
            //teleport
    }

现在我们可以在Location的构造方法前添加@Generator 如下

public class Location{
    @Generator
    public Location(World world,int x,int y,int z)
}

Annotation就会根据构造方法的形参自动构建对应的模型,然后将其传输给你的方法
但请注意,一个类只能有一个Generator 更要注意的是,这件事只会在Annotation找不到对应的Argument时执行,也就是说,如果Location被设定了一个Argument,那么Generator就会无效。

关于Genertor更小的细节

你会注意到Location中有一个World,假设World已在ArgumentManager中注册,那么这个类会被正确的识别,但如果没有,你可以继续为World添加Generator

public class World{
    @Generator
    public World(String worldName)
}

明确的说,Generator就是Argument的补充,对于一个不认识的类,AnnotationCommand就会去看看未知类的构造方法,如果有Generator,那么就会根据构造方法的形参利用已有的Argument生成相应的模型自动适配,例如Location可以被分解成WorldArgument和3个IntArgument,但这个过程并不是自动生成Argument,请注意这一点。