【发布时间】: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 <anyMethodName>(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