Hey all, One of the things I've noticed about plugin development for Minecraft is that you can end up spending a lot of time trying to do string parsing for player commands. Especially if you want to do sub-commands. This wasted time adds up and eventually takes away from your productivity. I wrote this little utilitiy for the CivCraft project and its been such an amazing time saver. You guys have helped me out a lot in the past so I'm posting this here to give back. Apologies if something like this is has already been posted, I looked around and didn't find anything. The following is a new abstract class which wraps around CommandExecutor. It allows you to define a set of commands/syntax help quickly using a HashMap. Player commands are fed through a bit of reflection and the proper command function is found and executed. The automatic help for commands is great for players as it allows them to discover existing subcommands, their arguments, and what they do with relative ease. Here is the abstract "CommandBase" class. Code:java package com.avrgaming.command;/** The MIT License (MIT)** Copyright (c) 2013 AVRGAMING** Permission is hereby granted, free of charge, to any person obtaining a copy* of this software and associated documentation files (the "Software"), to deal* in the Software without restriction, including without limitation the rights* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell* copies of the Software, and to permit persons to whom the Software is* furnished to do so, subject to the following conditions:** The above copyright notice and this permission notice shall be included in* all copies or substantial portions of the Software.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN* THE SOFTWARE.*/ import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.text.DecimalFormat;import java.util.HashMap; import org.bukkit.Bukkit;import org.bukkit.OfflinePlayer;import org.bukkit.command.Command;import org.bukkit.command.CommandExecutor;import org.bukkit.command.CommandSender;import org.bukkit.entity.Player; public abstract class CommandBase implements CommandExecutor { protected HashMap<String, String> commands = new HashMap<String, String>(); protected String[] args; protected CommandSender sender; protected String command = "FIXME"; protected String displayName = "FIXME"; protected boolean sendUnknownToDefault = false; protected DecimalFormat df = new DecimalFormat(); public abstract void init(); /* Called when no arguments are passed. */ public abstract void doDefaultAction() throws CommandException; /* Called on syntax error. */ public abstract void showHelp(); /* Called before command is executed to check permissions. */ public abstract void permissionCheck() throws CommandException; /* Called after permission check succeeds. */ public abstract void doLogging() throws CommandException; private static final String HEADING_COLOR = "\u00A7e"; //Yellow private static final String BORDER_COLOR = "\u00A7b"; //LightBlue private static final String ARG_COLOR = "\u00A7e"; //Yellow private static final String INFO_COLOR = "\u00A77"; //light grey private static final String COMMAND_COLOR = "\u00A7d"; //light purple @Override public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) { init(); this.args = args; this.sender = sender; try { permissionCheck(); } catch (CommandException e1) { sender.sendMessage(e1.getMessage()); return false; } try { doLogging(); } catch (CommandException e2) { e2.printStackTrace(); } if (args.length == 0) { try { doDefaultAction(); } catch (CommandException e) { sender.sendMessage(e.getMessage()); } return false; } if (args[0].equalsIgnoreCase("help")) { showHelp(); return true; } for (String c : commands.keySet()) { if (c.equalsIgnoreCase(args[0])) { try { Method method = this.getClass().getMethod(args[0].toLowerCase()+"_cmd"); try { method.invoke(this); return true; } catch (IllegalAccessException | IllegalArgumentException e) { e.printStackTrace(); sender.sendMessage("Internal Command Error."); } catch (InvocationTargetException e) { if (e.getCause() instanceof CommandException) { sender.sendMessage(e.getCause().getMessage()); } else { sender.sendMessage("Internal Command Error."); e.getCause().printStackTrace(); } } } catch (NoSuchMethodException e) { if (sendUnknownToDefault) { try { doDefaultAction(); } catch (CommandException e1) { sender.sendMessage(e.getMessage()); } return false; } sender.sendMessage("Unknown method "+args[0]); } return true; } } if (sendUnknownToDefault) { try { doDefaultAction(); } catch (CommandException e) { sender.sendMessage(e.getMessage()); } return false; } sender.sendMessage("Unknown command "+args[0]); return false; } public void sendMessage(String message) { sender.sendMessage(message); } public void sendHeading(String title) { String line = "---------------------------------------------------------"; if (title.length() > line.length()) { title = title.substring(0, line.length()); } String insert = "["+HEADING_COLOR+title+BORDER_COLOR+"]"; int lineMidpoint = line.length() / 2; int insertMidPoint = insert.length() / 2; String heading = BORDER_COLOR+line.substring(0, Math.max(0, lineMidpoint - insertMidPoint)); heading += insert+line.substring(lineMidpoint + insertMidPoint); sender.sendMessage(heading); } public void showBasicHelp() { sendHeading(displayName+" Command Help"); for (String c : commands.keySet()) { String info = commands.get(c); info = info.replace("[", ARG_COLOR+"["); info = info.replace("]", "]"+INFO_COLOR); info = info.replace("(", ARG_COLOR+"("); info = info.replace(")", ")"+INFO_COLOR); sender.sendMessage(COMMAND_COLOR+command+" "+c+INFO_COLOR+" "+info); } } protected String[] stripArgs(String[] someArgs, int amount) { if (amount >= someArgs.length) { return new String[0]; } String[] argsLeft = new String[someArgs.length - amount]; for (int i = 0; i < argsLeft.length; i++) { argsLeft[i] = someArgs[i+amount]; } return argsLeft; } protected String combineArgs(String[] someArgs) { String combined = ""; for (String str : someArgs) { combined += str + " "; } combined = combined.trim(); return combined; } public Player getCurrentPlayer() throws CommandException { if (sender instanceof Player) { return (Player)sender; } throw new CommandException("Only players can do this."); } public Player getPlayer(int index) throws CommandException { if (args.length < (index+1)) { throw new CommandException("Enter a player name"); } Player player = Bukkit.getPlayer(args[index]); if (player == null) { throw new CommandException("Player named "+args[index]+" is not online."); } return player; } public String getString(int index) throws CommandException { if (args.length < (index+1)) { throw new CommandException("Missing argument."); } return args[index]; } protected Double getDouble(int index) throws CommandException { if (args.length < (index+1)) { throw new CommandException("Enter a number."); } try { Double number = Double.valueOf(args[index]); return number; } catch (NumberFormatException e) { throw new CommandException(args[index]+" is not a number."); } } protected Integer getInteger(int index) throws CommandException { if (args.length < (index+1)) { throw new CommandException("Enter a number."); } try { Integer number = Integer.valueOf(args[index]); return number; } catch (NumberFormatException e) { throw new CommandException(args[index]+" is not whole a number."); } } protected OfflinePlayer getNamedOfflinePlayer(int index) throws CommandException { if (args.length < (index+1)) { throw new CommandException("Enter a player name"); } OfflinePlayer offplayer = Bukkit.getOfflinePlayer(args[index]); if (offplayer == null) { throw new CommandException("No player named:"+args[index]); } return offplayer; } }[/i] Inside the "init" function the commands are defined. The key is the command's name, and the value is the help that is shown to players. Inside the help, anything in brackets is highlighted as an argument so go easy on the player's eyes. Once you've defined a command in the "commands" hashmap, simply add a function with the same command name, followed by _cmd() and the reflection will find it. These functions can throw a CommandException(message) which will display the message to the player. In order to do a subcommand, simply create another object that also inherits from CommandBase, create it, and call its onCommand function as shown above. Here is the example for the subcommand above: Code:java package com.avrgaming.command; import org.bukkit.Bukkit;import org.bukkit.Location;import org.bukkit.World;import org.bukkit.entity.Player; public class ExamplePlayerCommand extends CommandBase { @Overridepublic void init() {command = "/example player";displayName = "Example Player"; commands.put("kick", "[name] [reason] - Kicks this player from the server.");commands.put("setcompass", "[name] [world] [x] [y] [z] - Sets this players compass location to this world, x,y,z."); } public void kick_cmd() throws CommandException {Player player = this.getPlayer(1);String reason = this.combineArgs(this.stripArgs(args, 2));player.kickPlayer(reason);sender.sendMessage("Kicked "+player.getName());} public void setcompass_cmd() throws CommandException {Player player = this.getPlayer(1);String worldname = this.getString(2);int x = this.getInteger(3);int y = this.getInteger(4);int z = this.getInteger(5); World world = Bukkit.getWorld(worldname);if (world == null) {throw new CommandException("No world named "+worldname);} Location loc = new Location(world, x, y, z);player.setCompassTarget(loc);sender.sendMessage("Set "+player.getName()+" compass to "+loc.toString());} @Overridepublic void doDefaultAction() throws CommandException {showHelp();} @Overridepublic void showHelp() {showBasicHelp();} @Overridepublic void permissionCheck() throws CommandException {if (sender instanceof Player) {Player player = this.getCurrentPlayer();if (!player.isOp()) {throw new CommandException("Only OPs can use this command.");}}} @Overridepublic void doLogging() throws CommandException {System.out.println(sender.getName()+" issued command "+command+" "+this.combineArgs(args));} } Subcommands are now handled no differently than their base commands, and you can keep adding on as many sub commands and sub sub comands as you like. In order to cut down on boilerplate, I've also added a few shortcuts that grab particualr types from player arguments Code:java public Player getCurrentPlayer();public Player getPlayer(int index) ;public String getString(int index);protected Double getDouble(int index) ;protected Integer getInteger(int index);protected OfflinePlayer getNamedOfflinePlayer(int index); By providing the index of the argument where that type is supposed to be, these helper functions will get the required type and throw a CommandException if the argument wasn't there, or if the input provided by the player was incorrect. I've stripped this of a bunch of CivCraft specific stuff so it works better for you guys, but it hasn't been tested a whole lot as is. If it's broken let me know and I'll update it.
I'm not really a Java programmer, I spend most of my time in C so apologies for my ignorance. Integer.valueOf() throws a NumberFormatException when passed illegal arguments, so the goal was to do a similar thing with CommandException. Maybe it violates the PrincipleOfLeastAstonishment by catching the exception and displaying the message to the user then? I suppose it could just as easily return a String, but then there would need to be some magic string that meant the command succeeded. Is there a better solution?
This becomes quit handy actually as you can simply throw CommandExceptions when processing the individual command (as done in the example with the set-compass command). I don't really see a logical issue here either, after all you encounter some sort of error when handling the given command and you cannot continue to process it. Returning false is at least just as messy...
If your Example of how to use this. What are the commands? I see kick, setcompass and /example. I am wondering this because i don't exactly understand the way you have described how to use this. /example kick (player) (reason) ? /example setcompass (coords) ? /kick ? /setcompass ? /example ?
The class extends CommandBase which extends CommandExecutor, so you would create an instance of this class in exactly the same way as you would a normal CommandExecutor: Code:java @Overridepublic void onEnable() {// ... // This will throw a NullPointerException if you don't have the command defined in your plugin.yml file!getCommand("example").setExecutor(new ExamplePlayerCommand()); // ...} Then the commands can be used in-game like: Code:text /example kick netizen539 (this runs the kick_cmd() function in ExamplePlayerCommand() ) Code:text command = "/example";displayName = "Example Player"; These two variables are only used for display purposes to give the player command help. There is probably a better way to do this, but this was simple and allowed for a lot of flexibility. Not sure what you mean? EDIT by Moderator: merged posts, please use the edit button instead of double posting.
Netizen Code:java super(command, displayName, usage); super("kick", "Kick", "[user] [reason]");[/user] Edit: Im guessing