[Util] Easy Commands and Sub Commands

Discussion in 'Resources' started by Netizen, Jun 20, 2013.

Thread Status:
Not open for further replies.
  1. Offline

    Netizen

    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
    1. package com.avrgaming.command;
    2. /*
    3. * The MIT License (MIT)
    4. *
    5. * Copyright (c) 2013 AVRGAMING
    6. *
    7. * Permission is hereby granted, free of charge, to any person obtaining a copy
    8. * of this software and associated documentation files (the "Software"), to deal
    9. * in the Software without restriction, including without limitation the rights
    10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11. * copies of the Software, and to permit persons to whom the Software is
    12. * furnished to do so, subject to the following conditions:
    13. *
    14. * The above copyright notice and this permission notice shall be included in
    15. * all copies or substantial portions of the Software.
    16. *
    17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23. * THE SOFTWARE.
    24. */
    25.  
    26.  
    27. import java.lang.reflect.InvocationTargetException;
    28. import java.lang.reflect.Method;
    29. import java.text.DecimalFormat;
    30. import java.util.HashMap;
    31.  
    32. import org.bukkit.Bukkit;
    33. import org.bukkit.OfflinePlayer;
    34. import org.bukkit.command.Command;
    35. import org.bukkit.command.CommandExecutor;
    36. import org.bukkit.command.CommandSender;
    37. import org.bukkit.entity.Player;
    38.  
    39. public abstract class CommandBase implements CommandExecutor {
    40.  
    41. protected HashMap<String, String> commands = new HashMap<String, String>();
    42.  
    43. protected String[] args;
    44. protected CommandSender sender;
    45.  
    46. protected String command = "FIXME";
    47. protected String displayName = "FIXME";
    48. protected boolean sendUnknownToDefault = false;
    49. protected DecimalFormat df = new DecimalFormat();
    50.  
    51. public abstract void init();
    52.  
    53. /* Called when no arguments are passed. */
    54. public abstract void doDefaultAction() throws CommandException;
    55.  
    56. /* Called on syntax error. */
    57. public abstract void showHelp();
    58.  
    59. /* Called before command is executed to check permissions. */
    60. public abstract void permissionCheck() throws CommandException;
    61.  
    62. /* Called after permission check succeeds. */
    63. public abstract void doLogging() throws CommandException;
    64.  
    65. private static final String HEADING_COLOR = "\u00A7e"; //Yellow
    66. private static final String BORDER_COLOR = "\u00A7b"; //LightBlue
    67. private static final String ARG_COLOR = "\u00A7e"; //Yellow
    68. private static final String INFO_COLOR = "\u00A77"; //light grey
    69. private static final String COMMAND_COLOR = "\u00A7d"; //light purple
    70.  
    71. @Override
    72. public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) {
    73. init();
    74.  
    75. this.args = args;
    76. this.sender = sender;
    77.  
    78. try {
    79. permissionCheck();
    80. } catch (CommandException e1) {
    81. sender.sendMessage(e1.getMessage());
    82. return false;
    83. }
    84.  
    85. try {
    86. doLogging();
    87. } catch (CommandException e2) {
    88. e2.printStackTrace();
    89. }
    90.  
    91. if (args.length == 0) {
    92. try {
    93. doDefaultAction();
    94. } catch (CommandException e) {
    95. sender.sendMessage(e.getMessage());
    96. }
    97. return false;
    98. }
    99.  
    100. if (args[0].equalsIgnoreCase("help")) {
    101. showHelp();
    102. return true;
    103. }
    104.  
    105. for (String c : commands.keySet()) {
    106. if (c.equalsIgnoreCase(args[0])) {
    107. try {
    108. Method method = this.getClass().getMethod(args[0].toLowerCase()+"_cmd");
    109. try {
    110. method.invoke(this);
    111. return true;
    112. e.printStackTrace();
    113. sender.sendMessage("Internal Command Error.");
    114. if (e.getCause() instanceof CommandException) {
    115. sender.sendMessage(e.getCause().getMessage());
    116. } else {
    117. sender.sendMessage("Internal Command Error.");
    118. e.getCause().printStackTrace();
    119. }
    120. }
    121.  
    122.  
    123. } catch (NoSuchMethodException e) {
    124. if (sendUnknownToDefault) {
    125. try {
    126. doDefaultAction();
    127. } catch (CommandException e1) {
    128. sender.sendMessage(e.getMessage());
    129. }
    130. return false;
    131. }
    132. sender.sendMessage("Unknown method "+args[0]);
    133. }
    134. return true;
    135. }
    136. }
    137.  
    138. if (sendUnknownToDefault) {
    139. try {
    140. doDefaultAction();
    141. } catch (CommandException e) {
    142. sender.sendMessage(e.getMessage());
    143. }
    144. return false;
    145. }
    146.  
    147. sender.sendMessage("Unknown command "+args[0]);
    148. return false;
    149. }
    150.  
    151. public void sendMessage(String message) {
    152. sender.sendMessage(message);
    153. }
    154.  
    155. public void sendHeading(String title) {
    156. String line = "---------------------------------------------------------";
    157.  
    158. if (title.length() > line.length()) {
    159. title = title.substring(0, line.length());
    160. }
    161.  
    162. String insert = "["+HEADING_COLOR+title+BORDER_COLOR+"]";
    163. int lineMidpoint = line.length() / 2;
    164. int insertMidPoint = insert.length() / 2;
    165.  
    166. String heading = BORDER_COLOR+line.substring(0, Math.max(0, lineMidpoint - insertMidPoint));
    167. heading += insert+line.substring(lineMidpoint + insertMidPoint);
    168. sender.sendMessage(heading);
    169. }
    170.  
    171. public void showBasicHelp() {
    172. sendHeading(displayName+" Command Help");
    173. for (String c : commands.keySet()) {
    174. String info = commands.get(c);
    175.  
    176. info = info.replace("[", ARG_COLOR+"[");
    177. info = info.replace("]", "]"+INFO_COLOR);
    178. info = info.replace("(", ARG_COLOR+"(");
    179. info = info.replace(")", ")"+INFO_COLOR);
    180.  
    181. sender.sendMessage(COMMAND_COLOR+command+" "+c+INFO_COLOR+" "+info);
    182. }
    183. }
    184.  
    185.  
    186. protected String[] stripArgs(String[] someArgs, int amount) {
    187. if (amount >= someArgs.length) {
    188. return new String[0];
    189. }
    190.  
    191. String[] argsLeft = new String[someArgs.length - amount];
    192. for (int i = 0; i < argsLeft.length; i++) {
    193. argsLeft[i] = someArgs[i+amount];
    194. }
    195.  
    196. return argsLeft;
    197. }
    198.  
    199. protected String combineArgs(String[] someArgs) {
    200. String combined = "";
    201. for (String str : someArgs) {
    202. combined += str + " ";
    203. }
    204. combined = combined.trim();
    205. return combined;
    206. }
    207.  
    208.  
    209. public Player getCurrentPlayer() throws CommandException {
    210. if (sender instanceof Player) {
    211. return (Player)sender;
    212. }
    213. throw new CommandException("Only players can do this.");
    214. }
    215.  
    216. public Player getPlayer(int index) throws CommandException {
    217. if (args.length < (index+1)) {
    218. throw new CommandException("Enter a player name");
    219. }
    220.  
    221. Player player = Bukkit.getPlayer(args[index]);
    222. if (player == null) {
    223. throw new CommandException("Player named "+args[index]+" is not online.");
    224. }
    225. return player;
    226. }
    227.  
    228. public String getString(int index) throws CommandException {
    229. if (args.length < (index+1)) {
    230. throw new CommandException("Missing argument.");
    231. }
    232.  
    233. return args[index];
    234. }
    235.  
    236. protected Double getDouble(int index) throws CommandException {
    237. if (args.length < (index+1)) {
    238. throw new CommandException("Enter a number.");
    239. }
    240.  
    241. try {
    242. Double number = Double.valueOf(args[index]);
    243. return number;
    244. } catch (NumberFormatException e) {
    245. throw new CommandException(args[index]+" is not a number.");
    246. }
    247.  
    248. }
    249.  
    250. protected Integer getInteger(int index) throws CommandException {
    251. if (args.length < (index+1)) {
    252. throw new CommandException("Enter a number.");
    253. }
    254.  
    255. try {
    256. Integer number = Integer.valueOf(args[index]);
    257. return number;
    258. } catch (NumberFormatException e) {
    259. throw new CommandException(args[index]+" is not whole a number.");
    260. }
    261. }
    262.  
    263. protected OfflinePlayer getNamedOfflinePlayer(int index) throws CommandException {
    264. if (args.length < (index+1)) {
    265. throw new CommandException("Enter a player name");
    266. }
    267.  
    268. OfflinePlayer offplayer = Bukkit.getOfflinePlayer(args[index]);
    269. if (offplayer == null) {
    270. throw new CommandException("No player named:"+args[index]);
    271. }
    272.  
    273. return offplayer;
    274. }
    275.  
    276. }
    277. [/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
    1. package com.avrgaming.command;
    2.  
    3. import org.bukkit.Bukkit;
    4. import org.bukkit.Location;
    5. import org.bukkit.World;
    6. import org.bukkit.entity.Player;
    7.  
    8. public class ExamplePlayerCommand extends CommandBase {
    9.  
    10. @Override
    11. public void init() {
    12. command = "/example player";
    13. displayName = "Example Player";
    14.  
    15. commands.put("kick", "[name] [reason] - Kicks this player from the server.");
    16. commands.put("setcompass", "[name] [world] [x] [y] [z] - Sets this players compass location to this world, x,y,z.");
    17.  
    18. }
    19.  
    20. public void kick_cmd() throws CommandException {
    21. Player player = this.getPlayer(1);
    22. String reason = this.combineArgs(this.stripArgs(args, 2));
    23. player.kickPlayer(reason);
    24. sender.sendMessage("Kicked "+player.getName());
    25. }
    26.  
    27. public void setcompass_cmd() throws CommandException {
    28. Player player = this.getPlayer(1);
    29. String worldname = this.getString(2);
    30. int x = this.getInteger(3);
    31. int y = this.getInteger(4);
    32. int z = this.getInteger(5);
    33.  
    34. World world = Bukkit.getWorld(worldname);
    35. if (world == null) {
    36. throw new CommandException("No world named "+worldname);
    37. }
    38.  
    39. Location loc = new Location(world, x, y, z);
    40. player.setCompassTarget(loc);
    41. sender.sendMessage("Set "+player.getName()+" compass to "+loc.toString());
    42. }
    43.  
    44. @Override
    45. public void doDefaultAction() throws CommandException {
    46. showHelp();
    47. }
    48.  
    49. @Override
    50. public void showHelp() {
    51. showBasicHelp();
    52. }
    53.  
    54. @Override
    55. public void permissionCheck() throws CommandException {
    56. if (sender instanceof Player) {
    57. Player player = this.getCurrentPlayer();
    58. if (!player.isOp()) {
    59. throw new CommandException("Only OPs can use this command.");
    60. }
    61. }
    62. }
    63.  
    64. @Override
    65. public void doLogging() throws CommandException {
    66. System.out.println(sender.getName()+" issued command "+command+" "+this.combineArgs(args));
    67. }
    68.  
    69. }
    70.  


    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
    1. public Player getCurrentPlayer();
    2. public Player getPlayer(int index) ;
    3. public String getString(int index);
    4. protected Double getDouble(int index) ;
    5. protected Integer getInteger(int index);
    6. 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.
     
    AGuyWhoSkis and mkremins like this.
  2. I don't think its recommend to use exceptions for the code flow
     
    AlphartDev likes this.
  3. Offline

    Netizen


    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?
     
  4. Offline

    TheE

    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...
     
  5. Offline

    Goblom

    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 ?
     
  6. Offline

    Ultimate_n00b

    Hmm.. instead of setting variables, why not super them up?
     
  7. Offline

    Netizen

    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
    1.  
    2. @Override
    3. public void onEnable() {
    4. // ...
    5.  
    6. // This will throw a NullPointerException if you don't have the command defined in your plugin.yml file!
    7. getCommand("example").setExecutor(new ExamplePlayerCommand());
    8.  
    9. // ...
    10. }



    Then the commands can be used in-game like:
    Code:text
    1.  
    2. /example kick netizen539 (this runs the kick_cmd() function in ExamplePlayerCommand() )
    3.  


    Code:text
    1.  
    2. command = "/example";
    3. displayName = "Example Player";
    4.  


    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.
     
    Last edited by a moderator: Jun 2, 2016
  8. Offline

    Goblom

    Netizen
    Code:java
    1. super(command, displayName, usage);
    2.  
    3. super("kick", "Kick", "[user] [reason]");[/user]
    Edit: Im guessing
     
    Ultimate_n00b likes this.
Thread Status:
Not open for further replies.

Share This Page