vault spigot plugin playerpoints placeholders holographic displays commands authme java api frameworks command bukkit

java - spigot - Diseño de un marco de plugin Bukkit-Manejo de comandos secundarios a través de anotaciones



playerpoints spigot (2)

Algunas palabras para presentar la situación.

Contexto: para facilitar mi flujo de trabajo mientras escribo los complementos de Bukkit (la API de facto básicamente para el Servidor de Minecraft hasta que Sponge logre su implementación), he decidido armar un "mini-framework" para no tener que repetir el las mismas tareas una y otra vez. (Además, estoy intentando diseñarlo para que no dependa demasiado de Bukkit, así que puedo seguir usándolo en Sponge simplemente cambiando mi implementación)

Intención: el manejo de comandos en Bukkit es, francamente, un desastre. Debe definir su comando raíz (por ejemplo, desea ejecutar / probar en el juego, "prueba" es la raíz) en un archivo YML (en lugar de llamar a algún tipo de fábrica), el manejo de comandos secundarios es inexistente y los detalles de implementación son oculto para producir resultados 100% confiables es difícil. Es la única parte de Bukkit que me ha molestado, y fue el principal iniciador de mi decisión de escribir un marco.

Objetivo: Resuma el desagradable manejo de comandos de Bukkit y reemplácelo por algo que esté limpio.

Trabajando para lograrlo:

Este va a ser el largo párrafo en el que explicaré cómo se implementó originalmente el manejo de comandos Bukkit, ya que eso permitirá una comprensión más profunda de los parámetros de comando importantes y demás.

Cualquier usuario conectado a un servidor de Minecraft puede iniciar un mensaje de chat con ''/'', lo que dará como resultado que se lo analice como un comando.

Para ofrecer una situación de ejemplo, cualquier jugador en Minecraft tiene una barra de vida, que por defecto alcanza un límite de 10 corazones, y se agota al recibir daño. Los "corazones" máximos y actuales (léase: salud) pueden ser configurados por el servidor en cualquier momento.

Digamos que queremos definir un comando como este:

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

Para comenzar a implementar esto ... oh chico. Si te gusta el código limpio, yo diría que saltea esto ... Voy a comentar para explicar, y cada vez que sienta que Bukkit cometió un error.

El plugin.yml obligatorio :

# 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

La clase de plugin principal:

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

El ejecutor del comando:

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

Ahora puedes entender por qué estoy eligiendo abstraer el manejo de comandos en mi marco. No creo que esté solo al pensar que esta manera no es auto-documentada, y manejar los comandos secundarios de esta manera no me parece correcto .

Mi intención:

Similar a cómo funciona el sistema de eventos Bukkit , quiero desarrollar un marco / API para abstraerlo.

Mi idea es anotar métodos de comando con una anotación respectiva que incluya toda la información de neccasario, y usar algún tipo de registrador (en el caso de caso: Bukkit.getPluginManager().registerEvents(Listener, Plugin) ) para registrar el comando.

De nuevo, de forma similar a la API de eventos, los métodos de comando tendrían una firma definida. Como tratar con múltiples parámetros es molesto, decidí empacar todo en una interfaz de contexto (¡también, de esta manera no rompo todos los códigos anteriores en caso de que necesite agregar algo al contexto!). Sin embargo, también necesitaba un tipo de devolución en caso de que quiera mostrar el uso rápidamente (pero no voy a elegir un booleano, eso seguro!), O hacer otras cosas. Entonces, la firma de mi idea se reduce a CommandResult <anyMethodName>(CommandContext) .

El registro del comando crearía las instancias de comando para los métodos anotados y los registraría.

Mi esquema básico tomó forma. Tenga en cuenta que aún no me he acostumbrado a escribir JavaDoc, agregué algunos comentarios rápidos sobre el código que no se auto-documenta.

Registro de comando:

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

La enumeración del resultado del comando:

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

El contexto de comando:

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

La anotación de comando principal que se pondrá en los métodos de comando:

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

La intención de autoParse es que puedo definir algo rápido, y si el análisis falla, simplemente muestra el mensaje de uso del comando.

Ahora, una vez que tengo mi implementación escrita, puedo volver a escribir el ejecutable del comando "sethealth" mencionado a algo como esto:

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

Creo que hablo por la mayoría aquí que de esta manera se siente más limpio.

Entonces, ¿dónde estoy haciendo una pregunta aquí?

Donde estoy estancado

Manejo de comando infantil.

En el ejemplo, pude salirme con una enumeración simple basada en los dos casos para el primer argumento.

Puede haber casos en los que deba crear muchos comandos secundarios similares a "actual / máximo". Un buen ejemplo puede ser algo que maneja la unión de jugadores en equipo: necesitaría:

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

etc. - Quiero poder crear clases separadas para estos comandos secundarios.

¿Cómo exactamente voy a presentar una forma limpia de decir "¡Oye, cuando el primer argumento de esto coincida con algo, haz esto y lo otro!" - diablos, la parte "emparejada" ni siquiera tiene que ser una cadena codificada, es posible que desee algo así como

/team [player] info

al mismo tiempo, sin dejar de coincidir con todos los comandos secundarios anteriores.

No solo tengo que vincular los métodos de comando secundarios, también tengo que vincular de algún modo el objeto requerido; después de todo, mi (futuro) registro de comando tomará un objeto instanciado (en el caso de ejemplo de BetterCommandExecutor) y lo registrará. ¿Cómo voy a decir "Use this child command instance!" al registro cuando se pasa el objeto?

He estado pensando en decir "**** todo, enlazar a una clase de comando hija y solo crear una instancia del constructor no-args", pero aunque esto probablemente aprobara el menor código, no daría mucha información sobre cómo exactamente Se crean instancias de comando hijo. Si decido ir por ese camino, probablemente simplemente definiré un parámetro de childs en mi anotación de Command , y lo haré tomar algún tipo de lista de anotación @ChildCommand (anotaciones en las anotaciones? Yo dawk, ¿por qué no?).

Entonces, después de todo esto, la pregunta es: Con esta configuración, ¿hay alguna manera de poder definir claramente los comandos secundarios, o tendré que cambiar completamente mi equilibrio? Pensé en extenderme desde algún tipo de BaseCommand abstracto (con un método abstracto de getChildCommands ()), pero el método de anotación tiene la ventaja de poder manejar múltiples comandos de una clase. Además, en lo que respecta al código de código abierto hasta ahora, tengo la impresión de que se extends 2011 e implements el sabor del año, por lo que probablemente no me obligue a extender algo cada vez que estoy creando tipo de controlador de comando.

Lo siento por la larga publicación. Esto fue más de lo esperado:

Editar # 1:

Me acabo de dar cuenta de que lo que básicamente estoy creando es una especie de ... árbol? de comandos. Sin embargo, simplemente usar algún tipo de CommandTreeBuilder se desvanece ya que va en contra de una de las cosas que quería de esta idea: ser capaz de definir múltiples manejadores de comandos en una clase. De vuelta a la lluvia de ideas.


Lo único que puedo pensar es dividir tus anotaciones. Tendría una clase que tiene el Comando Base como anotación y luego los métodos en esa clase con los diferentes subcomandos:

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

Si desea más flexibilidad, también puede tener en cuenta la jerarquía de herencia, pero no estoy seguro de qué tan auto-documentada sería entonces (ya que parte de los comandos quedarían ocultos en las clases principales).

Sin embargo, esta solución no resuelve el ejemplo de /team [player] info tu /team [player] info , pero creo que es algo menor. Sería confuso de todos modos tener subcomandos aparecer en diferentes parámetros de su comando.


La API estándar de Bukkit para el manejo de comandos es bastante buena en mi opinión, ¿por qué no usarla? Creo que estás confundido, entonces lo evitas. Así es como lo hago.

Registre el comando

Cree una nueva sección llamada commands , donde los colocará todos como nodos secundarios.

commands: sethealth:

Evite usar la clave de permission : lo revisaremos más tarde. Evite usar la tecla de usage : es difícil escribir un gran mensaje de error válido en cada caso. En general, odio estas claves secundarias, así que deja el nodo principal vacío.

Manejarlo en su propia clase

Use una clase separada que implemente la interfaz CommandExecutor .

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

Agregue lo siguiente bajo el método onEnable() en la clase principal.

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

No necesita verificar command.getName() si usa esta clase solo para este comando. Haga que el método devuelva true en cualquier caso: no ha definido el mensaje de error, entonces ¿por qué debería obtenerlo?

Que sea seguro

Ya no tendrá que preocuparse si procesa el sender en la primera línea. Además, puede verificar los permisos genéricos aquí.

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

Puede usar colores para hacer que los mensajes sean más legibles.

Lidiando con argumentos

Es simple producir resultados 100% confiables. Este es solo un ejemplo incompleto de cómo debes trabajar.

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."); }

Planifica tu código usando cláusulas de guardia y si quieres subcomandos, siempre String.equalsIgnoreCase(String) con String.equalsIgnoreCase(String) .