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就会失败,因为它违背了我想要的一个想法:能够在一个类中定义多个命令处理程序.回到头脑风暴.
我唯一能想到的就是拆分你的注释。您将拥有一个类,该类将基本命令作为注释,然后该类中的方法具有不同的子命令:
@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例子,但我认为这是一件小事。无论如何,让子命令出现在命令的不同参数中都会令人困惑。
| 归档时间: |
|
| 查看次数: |
1741 次 |
| 最近记录: |