【问题标题】:Designing a Bukkit plugin framework - Child command handling via annotations设计 Bukkit 插件框架 - 通过注解处理子命令
【发布时间】:2015-02-17 07:49:46
【问题描述】:

介绍一下情况。

上下文:为了简化我在编写 Bukkit 插件(在 Sponge 开始实施之前基本上是 Minecraft 服务器的实际 API)时的工作流程,我决定将一个“迷你-框架”让我自己不必一遍又一遍地重复相同的任务。 (另外,我正在尝试将其设计为不太依赖 Bukkit,因此我可以通过更改我的实现来继续在 Sponge 上使用它)

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

目标:抽象掉讨厌的 Bukkit 命令处理,并用干净的东西替换它。


朝着这个方向努力:

这将是我将解释 Bukkit 命令处理最初是如何实现的长段落,因为这将使对重要的命令参数等有更深入的了解。

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

举个例子,Minecraft 中的任何玩家都有一个生命条,默认上限为 10 颗心,并在受到伤害时耗尽。服务器可以随时设置最大和当前的“心”(阅读:健康)。

假设我们要定义这样的命令:

/sethealth <current/maximum> <player or * for all> <value>

开始实施这个......哦,男孩。如果你喜欢干净的代码,我会说跳过这个......我会评论解释,每当我觉得 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

主要插件类:

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());
    }
}

命令执行者:

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;
    }
}

现在你可能明白我为什么选择在我的框架中抽象出命令处理了。我不认为只有我一个人认为这种方式不是自我记录并且以这种方式处理子命令感觉不对


我的意图:

Bukkit Event System 的工作方式类似,我想开发一个框架/API 来将其抽象出来。

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

再次与事件 API 类似,命令方法将具有定义的签名。由于处理多个参数很烦人,我决定将它们全部打包在一个上下文接口中(另外,这样我就不会破坏所有以前的代码,以防我需要在上下文中添加一些东西!)。但是,我还需要一个返回类型,以防我想快速显示使用情况(但我不会选择布尔值,这是肯定的!),或者做一些其他的事情。所以,我的想法签名归结为CommandResult &lt;anyMethodName&gt;(CommandContext)

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

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

命令注册:

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);

}

命令结果枚举:

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?
}

命令上下文:

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();
}

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

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 {};
}

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;
    }
}

我相信我在这里代表大多数人说这种方式感觉更干净。

那么我在哪里问问题呢?

我被困的地方。

子命令处理。

在示例中,我能够基于第一个参数的两种情况使用一个简单的枚举。

在某些情况下,我必须创建许多类似于“当前/最大值”的子命令。一个很好的例子可能是处理将玩家作为一个团队一起加入的东西 - 我需要:

/team create ...
/team delete ...
/team addmember/join ...
/team removemember/leave ...

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

我究竟要如何引入一种简洁的方式来表达“嘿,当 this 的第一个参数匹配某些东西时,做这个和那个!” - 哎呀,“匹配”部分甚至不必是硬编码的字符串,我可能想要类似的东西

/team [player] info

同时,同时仍然匹配所有之前的子命令。

我不仅必须链接到子命令方法,还必须以某种方式链接所需的对象 - 毕竟,我的(未来)命令注册将采用实例化对象(在示例中为 BetterCommandExecutor)并注册它。我将如何告诉“使用这个子命令实例!”传入对象的时候去注册?

我一直在考虑说“**** 一切,链接到子命令类并实例化它的无参数构造函数”,但是虽然这可能会产生最少的代码,但它不会提供太多洞察力了解子命令实例是如何创建的。如果我决定这样做,我可能会在我的Command 注释中定义一个childs 参数,并使其采用某种@ChildCommand 注释列表(注释中的注释?你笨蛋,为什么不呢?) .


毕竟,问题是:有了这个设置,有没有办法可以干净地定义子命令,或者我必须完全改变我的立足点?我考虑过从某种抽象的 BaseCommand 扩展(使用抽象的 getChildCommands() 方法),但是注释方法的优点是能够处理来自一个类的多个命令。另外,就我到现在为止接触到的开源代码而言,我的印象是extends 是 2011 年,implements 是当年的味道,所以我可能不应该每次都强迫自己扩展一些东西'正在创建某种命令处理程序。

很抱歉发了这么长的帖子。这比我预期的要长:/


编辑#1:

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

【问题讨论】:

  • 这个问题听起来有点像软件设计或代码审查。如果是后者,则有一个SE site designed for those questions
  • 哇,stackoverflow 太强大了!我从 Google 搜索中了解到 meta 和 askubuntu,但从未听说过 codereview。如果我得到处理子命令的大脑火花,我一定会在那里发帖!感谢您的链接:)
  • 更正:堆栈 Exchange 很大。有a lot of sites,也可以propose your own
  • @gunr2171 由于代码还没有真正实现,这将是一个关于“代码尚未编写”的问题,这是代码审查的主题。
  • @ZKF 在阅读您的问题时,我想到了JCommander,您可能想查看它并从中汲取灵感,甚至在您的框架中使用它。

标签: java api frameworks command bukkit


【解决方案1】:

我唯一能想到的就是拆分您的注释。您将拥有一个将 Base Command 作为注释的类,然后在该类中使用不同的子命令来调用方法:

@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
    }
}

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

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

【讨论】:

  • 嗯,这样做的目的是让我可以将子命令拆分到它们自己的类中,这样我就可以清晰地了解我的所有命令...
  • 在这种情况下,您也可以只接受 @Command("/test sub") 之类的东西作为类/函数的注释,不是吗?
  • 我基本上不想“复制”我的代码,可以这么说。命令的名称是指当前“节点”的名称,可以这么说,所以在你的例子中,我的根命令上有一个@Command("test"),我的子命令上有一个@Command("sub"),现在我需要通过浏览方法注释/其他内容以立即阅读的方式将这两者联系起来。我只是有一个与命令注册界面有关的非常粗略的想法,如果它引导到我满意的地方,我会发布它作为我自己问题的答案,我猜......
  • 好吧,不,我无处可去。我考虑过在命令注册上使用Result register(Object root, Object child) 之类的东西,但是尝试为此编写示例代码再次归结为以一种方法构建某种对象图,这是我想要避免的。我想我需要一些时间来仔细考虑这个问题,也许我会在当天晚些时候得到一个随机的火花......
【解决方案2】:

standard Bukkit API for command handling 在我看来相当不错,为什么不使用它呢? 我认为你只是感到困惑,然后你避免它。 这是我的做法。

注册命令

创建一个名为 commands 的新部分,您将在其中将所有它们作为子节点。

commands:
  sethealth:

避免使用permission 键:我们稍后会检查。 避免使用usage 键:很难写出在每种情况下都有效的错误消息。 一般来说,我讨厌这些子键,所以将父节点留空。

在自己的类上处理

使用实现CommandExecutor 接口的单独类。

public class Sethealth implements CommandExecutor {
    @Override
    public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) {
        // ...
        return true;
    }
}

在主类的onEnable()方法下添加以下内容。

getCommand("sethealth").setExecutor(new Sethealth());

如果您仅将此类用于此命令,则无需检查 command.getName()。 让方法在任何情况下都返回true:你还没有定义错误信息,那你为什么要得到它?

确保安全

如果您在第一行处理sender,您将不再需要担心。 此外,您可以在此处检查任何通用权限。

if (!(sender instanceof Player)) {
    sender.sendMessage("You must be an in-game player.");
    return true;
}
Player player = (Player)sender;
if (!player.hasPermission("sethealth.use")) {
    player.sendMessage(ChatColor.RED + "Insufficient permissions.");
    return true;
}
// ...

您可以使用颜色使消息更具可读性。

处理参数

产生 100% 可靠的结果很简单。 这只是一个关于您应该如何工作的不完整示例。

if (args.length == 0) {
    player.sendMessage(ChatColor.YELLOW + "Please specify the target.");
    return true;
}
Player target = Server.getPlayer(args[0]);
if (target == null) {
    player.sendMessage(ChatColor.RED + "Target not found.");
    return true;
}
if (args.length == 1) {
    player.sendMessage(ChatColor.YELLOW + "Please specify the new health.");
    return true;
}
try {
    double value = Double.parseDouble(args[1]);
    if (value < 0D || value > 20D) {
        player.sendMessage(ChatColor.RED + "Invalid value.");
        return true;
    }
    target.setHealth(value);
    player.sendMessage(ChatColor.GREEN + target.getName() + "'s health set to " + value + ".");
} catch (NumberFormatException numberFormat) {
    player.sendMessage(ChatColor.RED + "Invalid number.");
}

使用guard clauses 规划您的代码,如果您需要子命令,请始终使用String.equalsIgnoreCase(String) 检查它们。

【讨论】:

  • 我决定抽象的原因是“标准”方式不能以任何方式扩展(我想添加另一个子命令?还有另一个?执行器很快就会变得一团糟。 ..),并将我的代码绑定到 YML 配置文件。我基本上想永远为自己节省所有“解析”,并能够专注于我在插件中真正在做什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多