设计Bukkit插件框架 - 通过注释处理子命令

ZKF*_*ZKF 7 java api command frameworks bukkit

有些话介绍情况.

上下文:为了简化我的工作流程,同时编写Bukkit插件(Minecraft服务器基本上是事实上的API,直到Sponge实现它),我决定为自己整理一个"迷你框架",不必重复同样的任务一遍又一遍.(另外,我正在尝试将其设计为不太依赖Bukkit,所以我可以通过更改我的实现继续在Sponge上使用它)

意图:坦率地说,Bukkit中的命令处理是一团糟.你必须在YML文件中定义你的root命令(例如,你想运行/测试ingame,"test"是root)(而不是调用某种工厂?),子命令处理是不存在的,实现细节是隐藏所以产生100%可靠的结果很难.这是Bukkit唯一让我恼火的部分,它是我决定编写框架的主要发起人.

目标:抽象出令人讨厌的Bukkit命令处理,并将其替换为干净的东西.


努力:

这将是一个很长的段落,我将解释最初如何实现Bukkit命令处理,因为这将更深入地理解重要的命令参数等.

连接到Minecraft服务器的任何用户都可以使用"/"开始聊天消息,这将导致它被解析为命令.

举一个例子,Minecraft中的任何玩家都有一个生命栏,默认设置为10个心脏,并在受到伤害时耗尽.服务器可以随时设置最大和当前"心脏"(读取:运行状况).

让我们说我们想要定义这样的命令:

/sethealth <current/maximum> <player or * for all> <value>
Run Code Online (Sandbox Code Playgroud)

要开始实现这个......哦,小男孩.如果你喜欢干净的代码,我会说跳过这个...我会评论解释,每当我觉得Bukkit做错了.

强制性plugin.yml:

# Full name of the file extending JavaPlugin
# My best guess? Makes lazy-loading the plugin possible
# (aka: just load classes that are actually used by replacing classloader methods)
main: com.gmail.zkfreddit.sampleplugin.SampleJavaPlugin

# Name of the plugin.
# Why not have this as an annotation on the plugin class?
name: SamplePlugin

# Version of the plugin. Why is this even required? Default could be 1.0.
# And again, could be an annotation on the plugin class...
version: 1.0

# Command section. Instead of calling some sort of factory method...
commands:
    # Our '/sethealth' command, which we want to have registered.
    sethealth:
        # The command description to appear in Help Topics
        # (available via '/help' on almost any Bukkit implementation)
        description: Set the maximum or current health of the player

        # Usage of the command (will explain later)
        usage: /sethealth <current/maximum> <player/* for all> <newValue>

        # Bukkit has a simple string-based permission system, 
        # this will be the command permission
        # (and as no default is specified,
        # will default to "everybody has it")
        permission: sampleplugin.sethealth
Run Code Online (Sandbox Code Playgroud)

主插件类:

package com.gmail.zkfreddit.sampleplugin;

import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;

public class SampleJavaPlugin extends JavaPlugin {

    //Called when the server enables our plugin
    @Override
    public void onEnable() {
        //Get the command object for our "sethealth" command.
        //This basically ties code to configuration, and I'm pretty sure is considered bad practice...
        PluginCommand command = getCommand("sethealth");

        //Set the executor of that command to our executor.
        command.setExecutor(new SampleCommandExecutor());
    }
}
Run Code Online (Sandbox Code Playgroud)

命令执行程序:

package com.gmail.zkfreddit.sampleplugin;

import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class SampleCommandExecutor implements CommandExecutor {

    private static enum HealthOperationType {
        CURRENT,
        MAXIMUM;

        public void executeOn(Player player, double newHealth) {
            switch (this) {
                case CURRENT:
                    player.setHealth(newHealth);
                    break;
                case MAXIMUM:
                    player.setMaxHealth(newHealth);
                    break;
            }
        }
    }

    @Override
    public boolean onCommand(
            //The sender of the command - may be a player, but might also be the console
            CommandSender commandSender,

            //The command object representing this command
            //Why is this included? We know this is our SetHealth executor,
            //so why add this as another parameter?
            Command command,

            //This is the "label" of the command - when a command gets registered,
            //it's name may have already been taken, so it gets prefixed with the plugin name
            //(example: 'sethealth' unavailable, our command will be registered as 'SamplePlugin:sethealth')
            String label,

            //The command arguments - everything after the command name gets split by spaces.
            //If somebody would run "/sethealth a c b", this would be {"a", "c", "b"}.
            String[] args) {
        if (args.length != 3) {
            //Our command does not match the requested form {"<current/maximum>", "<player>", "<value>"},
            //returning false will, ladies and gentleman...

            //display the usage message defined in plugin.yml. Hooray for some documented code /s
            return false;
        }

        HealthOperationType operationType;
        double newHealth;

        try {
            //First argument: <current/maximum>
            operationType = HealthOperationType.valueOf(args[0].toUpperCase());
        } catch (IllegalArgumentException e) {
            return false;
        }

        try {
            //Third argument: The new health value
            newHealth = Double.parseDouble(args[2]);
        } catch (NumberFormatException e) {
            return false;
        }

        //Second argument: Player to operate on (or all)
        if (args[1].equalsIgnoreCase("*")) {
            //Run for all players
            for (Player player : Bukkit.getOnlinePlayers()) {
                operationType.executeOn(player, newHealth);
            }
        } else {
            //Run for a specific player
            Player player = Bukkit.getPlayerExact(args[1]);

            if (player == null) {
                //Player offline
                return false;
            }

            operationType.executeOn(player, newHealth);
        }

        //Handled successfully, return true to not display usage message
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以理解为什么我选择在我的框架中抽象出命令处理.我认为我并不孤单,认为这种方式不是自我记录,并且以这种方式处理子命令感觉不对.


我的本意:

Bukkit事件系统的工作方式类似,我想开发一个框架/ API来抽象它.

我的想法是使用包含所有必要信息的相应注释来注释命令方法,并使用某种注册器(在事件情况下:) Bukkit.getPluginManager().registerEvents(Listener, Plugin)来注册命令.

再次类似于Event API,命令方法将具有定义的签名.由于处理多个参数很烦人,我决定将它全部打包在一个上下文界面中(同样,这样我就不会破坏所有以前的代码,以防我需要在上下文中添加一些内容!).但是,我还需要一个返回类型,以防我想快速显示用法(但我不打算选择一个布尔值,这是肯定的!),或者做一些其他的事情.所以,我的想法签名归结为CommandResult <anyMethodName>(CommandContext).

然后,命令注册将为注释方法创建命令实例并注册它们.

我的基本大纲形成了.请注意,我还没有编写JavaDoc,我在非自编文档代码中添加了一些快速注释.

命令注册:

package com.gmail.zkfreddit.pluginframework.api.command;

public interface CommandRegistration {

    public static enum ResultType {
        REGISTERED,
        RENAMED_AND_REGISTERED,
        FAILURE
    }

    public static interface Result {
        ResultType getType();

        //For RENAMED_AND_REGISTERED
        Command getConflictCommand();

        //For FAILURE
        Throwable getException();

        //If the command got registered in some way
        boolean registered();
    }

    Result register(Object commandObject);

}
Run Code Online (Sandbox Code Playgroud)

命令结果枚举:

package com.gmail.zkfreddit.pluginframework.api.command;

public enum CommandResult {

    //Command executed and handlded
    HANDLED,

    //Show the usage for this command as some parameter is wrong
    SHOW_USAGE,

    //Possibly more?
}
Run Code Online (Sandbox Code Playgroud)

命令上下文:

package com.gmail.zkfreddit.pluginframework.api.command;

import org.bukkit.command.CommandSender;

import java.util.List;

public interface CommandContext {

    CommandSender getSender();

    List<Object> getArguments();

    @Deprecated
    String getLabel();

    @Deprecated
    //Get the command annotation of the executed command
    Command getCommand();
}
Run Code Online (Sandbox Code Playgroud)

要放在命令方法上的主命令注释:

package com.gmail.zkfreddit.pluginframework.api.command;

import org.bukkit.permissions.PermissionDefault;

public @interface Command {

    public static final String DEFAULT_STRING = "";

    String name();

    String description() default DEFAULT_STRING;

    String usageMessage() default DEFAULT_STRING;

    String permission() default DEFAULT_STRING;

    PermissionDefault permissionDefault() default PermissionDefault.TRUE;

    Class[] autoParse() default {};
}
Run Code Online (Sandbox Code Playgroud)

autoParse的意图是我可以快速定义一些东西,如果解析失败,它只显示命令的用法消息.

现在,一旦我编写了实现,我就可以将提到的"sethealth"命令执行器重写为:

package com.gmail.zkfreddit.sampleplugin;

import de.web.paulschwandes.pluginframework.api.command.Command;
import de.web.paulschwandes.pluginframework.api.command.CommandContext;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionDefault;

public class BetterCommandExecutor {

    public static enum HealthOperationType {
        CURRENT,
        MAXIMUM;

        public void executeOn(Player player, double newHealth) {
            switch (this) {
                case CURRENT:
                    player.setHealth(newHealth);
                    break;
                case MAXIMUM:
                    player.setMaxHealth(newHealth);
                    break;
            }
        }
    }

    @Command(
            name = "sethealth",
            description = "Set health values for any or all players",
            usageMessage = "/sethealth <current/maximum> <player/* for all> <newHealth>",
            permission = "sampleplugin.sethealth",
            autoParse = {HealthOperationType.class, Player[].class, Double.class} //Player[] as there may be multiple players matched
    )
    public CommandResult setHealth(CommandContext context) {
        HealthOperationType operationType = (HealthOperationType) context.getArguments().get(0);
        Player[] matchedPlayers = (Player[]) context.getArguments().get(1);
        double newHealth = (Double) context.getArguments().get(2);

        for (Player player : matchedPlayers) {
            operationType.executeOn(player, newHealth);
        }

        return CommandResult.HANDLED;
    }
}
Run Code Online (Sandbox Code Playgroud)

我相信我在这里说的最多,这种方式感觉更干净.

那我在哪里问一个问题呢?

我被困在哪里

子命令处理.

在这个例子中,基于第一个参数的两个案例,我能够得到一个简单的枚举.

在某些情况下,我必须创建许多类似于"当前/最大"的子命令.一个很好的例子可能是作为一个团队处理加入球员的事情 - 我需要:

/team create ...
/team delete ...
/team addmember/join ...
/team removemember/leave ...
Run Code Online (Sandbox Code Playgroud)

等 - 我希望能够为这些子命令创建单独的类.

我究竟要如何介绍一种简洁的方式来说"嘿,当这个问题的第一个参数符合某些内容时,就这样做吧!" - 哎呀,"匹配"的部分甚至不必是一个硬编码的字符串,我可能想要类似的东西

/team [player] info
Run Code Online (Sandbox Code Playgroud)

同时,仍然匹配所有以前的子命令.

我不仅要链接到子命令方法,我还必须以某种方式链接所需的对象 - 毕竟,我的(未来)命令注册将采用实例化对象(在示例情况下,BetterCommandExecutor)并注册它.我怎么告诉"使用这个子命令实例!" 在传递物体时注册?

我一直在考虑说"****所有内容,链接到一个子命令类,只是实例化它的no-args构造函数",但是虽然这可能会产生最少的代码,但它并不能让人深入了解子命令实例被创建.如果我决定采用这种方式,我可能只是childs在我的Command注释中定义一个参数,并使其采用某种@ChildCommand注释列表(注释中的注释?哟dawk,为什么不呢?).


所以在这之后,问题是:通过这种设置,有没有办法可以干净地定义子命令,还是我必须彻底改变我的立足点?我想过从某种抽象的BaseCommand(使用抽象的getChildCommands()方法)扩展,但是注释方法的优点是能够处理来自一个类的多个命令.此外,据我所知,直到现在我已经获得了开源代码,我得到了extends2011年的印象,并且implements是今年的风格,所以我应该不会强迫自己每次创建某种东西时都要扩展一些东西.命令处理程序

我很抱歉这篇长篇文章.这比我预期的要长:/


编辑#1:

我刚刚意识到我基本上创造的是某种......树?命令.然而,只是简单地使用某种CommandTreeBuilder就会失败,因为它违背了我想要的一个想法:能够在一个类中定义多个命令处理程序.回到头脑风暴.

mhl*_*hlz 1

我唯一能想到的就是拆分你的注释。您将拥有一个类,该类将基本命令作为注释,然后该类中的方法具有不同的子命令:

@Command("/test")
class TestCommands {

    @Command("sub1"// + more parameters and stuff)
    public Result sub1Command(...) {
        // do stuff
    }

    @Command("sub2"// + more parameters and stuff)
    public Result sub2Command(...) {
        // do stuff
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您想要更大的灵活性,您也可以考虑继承层次结构,但我不确定这将如何自我记录(因为部分命令将隐藏在父类中)。

虽然这个解决方案不能解决你的/team [player] info例子,但我认为这是一件小事。无论如何,让子命令出现在命令的不同参数中都会令人困惑。