Recently I started up a discussion about a different way to code your commands. There was some interesting chatter and I thought I'd let you guys take a look and decide if it is something you would use.
Features:
Variables inherited from super class. Ex: chr, client, world, channel, args, and more.
Uses Java Annotations to create descriptions, syntax, alias' and special tags for commands.
Generate command help.
Works well with Java IDE's Navigation. (Netbeans, Eclipse) Netbeans
Commands are cached in a HashMap therefore they are called quickly and are only loaded when they are called for the first time.
Overall more organized look.
Annotations can:
be used to generate help text for the users.
disable commands if they are not used. (EX: '@Command(false)')
specify alias'. (EX: @dispose has a alias of @fix or @unbug; All will call the same command.)
create tags for commands. So in code you can check if a command has that tag. (For example, you can only perform commands with the 'tutorial' tag when in the tutorial)
This is an example '@heal' command:
PHP Code:
@Command // States that this is a command. (Will not work without this) @Syntax("@heal (name)") // The syntax for the help generation. (Defaults to '@heal' if missing) @Description("Heals (name). '@help politics'") // The description in the help generation. @Tags({"tutorial", "cheat"}) // This command has a tag of 'tutorial' and 'cheat'. public void heal() { // The name of the command. (**Must be lower case**) if (args.length == 1) { chr.setHpMp(30000); } else if (args.length == 2) { String targetName = args[1]; MapleCharacter target = channel.getPlayerStorage().getCharacterByName(targetName); if (target != null) { target.setHpMp(30000); target.message("You been healed by " + chr.getName() + "."); chr.message("You have healed " + target.getName() + "."); } else { chr.message("Unable to find a player named " + targetName + "."); } } else { chr.message("Syntax: " + this.getCommand(label).syntax); } }
/* Other useful variables * If you use an IDE you can get a list of these variables by typing 'this.' * * chr - Gets the current character. * client - Gets the current character's client. * * channel - Gets the current character's channel server. * world - Gets the current character's world server. * server - Gets an instance of the server. * * label - The label of the command. Otherwise known as args[0]. * sub - The sub command. Otherwise known as args[1]. * args - The arguments of the command. * command - The full string of the command. (What the user entered) * handled - Used to determine whether a command has been handled. */
This is an example of how simple a command can be:
PHP Code:
@Command public void heal() { chr.setHpMp(30000); }
How to set up: (Note this tutorial uses MoopleDev. You may need to change some things.)
Spoiler:
First you need to find a place to put your command files. I think a new package called 'client.command' works well.
Once you have created a new package with the name you like, then you need to create a new Java Class. This class will hold the base command handler. Naturally I named the class 'CommandHandler.java'.
Copy and paste this code into you newly created base class.
/** * I don't suggest modifying this class unless you know what your doing. * If you know what your doing please let me know how you improved it. :) * @author Tagette aka Handsfree */ public class CommandHandler {
/** * Used by the command executer to tell if the method is a command or not. */ @Retention(RetentionPolicy.RUNTIME) protected @interface Command { boolean value() default true; }
/** * Used when creating help for the command handler. */ @Retention(RetentionPolicy.RUNTIME) protected @interface Syntax {
String value() default "NULL"; }
/** * Used when creating help for the command handler. */ @Retention(RetentionPolicy.RUNTIME) protected @interface Description {
String value() default "No description."; }
/** * Used when executing a command that has aliases. */ @Retention(RetentionPolicy.RUNTIME) protected @interface Alias { String[] value(); }
/** * Used for when commands need to be differentiated. */ @Retention(RetentionPolicy.RUNTIME) protected @interface Tags { String[] value(); }
public class CommandInfo {
public String name; public String syntax; public String description; public String[] alias; public String[] tags;
public CommandInfo() { name = ""; syntax = ""; description = ""; alias = null; tags = null; }
/** * Executes a command and checks for packet spamming. * * @return Returns false when the player isn't high enough gm level. */ public void execute() throws Exception { String key = getClass().getName() + "." + label; if(cachedCommands.containsKey(key)) { handled = true; // The command has already been stored so use that. cachedCommands.get(key).invoke(this); } else { // Finds the method the first time it has been run. for (Method method : getClass().getMethods()) { Command cmd = (Command) method.getAnnotation(Command.class); // The method is a command if it has a command annotation with the value true; if (cmd != null && cmd.value()) { if (method.getName().equalsIgnoreCase(label)) { // The method matches the commands name. handled = true; // Invoke the method. method.invoke(this); // Cache the method. cacheMethod(method); break; } else { // The method did not match the command's name so check for alias'. Alias alias = method.getAnnotation(Alias.class); if (alias != null) { // The method has alias'. for (String a : alias.value()) { if (a.equalsIgnoreCase(label)) { // The alias matched the command's name. handled = true; // Invoke the method. method.invoke(this); // Cache the method. cacheMethod(method); break; } } } } } } } }
/** * Executes a command for another player. * * @param client The client of the other player. * @param args The command to execute. */ public boolean executeFor(MapleClient client, String[] args) throws Exception { CommandHandler temp = new CommandHandler(client, header, args); temp.execute(); return temp.handled(); }
/** * Sets whether the command has been handled or not. * * @param handled True if the command has been handled. */ public void setHandled(boolean handled) { this.handled = handled; }
/** * Used to tell if the command was handled or not by this handler. * * @return Returns true if the command was handled during execution. */ public boolean handled() { return handled; }
/** * Caches a method and it's alias'. * * @param method The method to cache. */ private void cacheMethod(Method method) throws Exception { String name = method.getName(); if(!cachedCommands.containsKey(name)) { cachedCommands.put(getClass().getName() + "." + name, method); // Check for alias'. Alias alias = method.getAnnotation(Alias.class); if (alias != null) { // The method has alias'. for (String a : alias.value()) { cachedCommands.put(getClass().getName() + "." + a, method); } } } }
/** * Clears the command cache. */ public static void clearCache() { cachedCommands.clear(); }
/** * Returns true if this command handler has the specified command <code>label</code>. * * @param label The command label. * @return Returns true if this command handler has the command <code>label</code>. */ protected boolean hasCommand(String label) { boolean hasCommand = false; try { Method method = getClass().getMethod(label); Command cmd = (Command) method.getAnnotation(Command.class); if (cmd != null && cmd.value()) { hasCommand = true; } } catch (NoSuchMethodException nsme) { } return hasCommand; }
/** * Used to tell if a command has a tag or not. * * @param label The command to check. * @param tag The tag to confirm. * @return Returns true if the tag is on the command. */ protected boolean hasTag(String label, String tag) { boolean hasTag = false; try { Method method = getClass().getMethod(label); if (method != null) { Tags tags = (Tags) method.getAnnotation(Tags.class); if (tags != null) { for (String t : tags.value()) { if (t.equalsIgnoreCase(tag)) { hasTag = true; break; } } } } } catch (NoSuchMethodException nsme) { } return hasTag; }
/** * Gets a command's info. * * @param name The name of the command. * @return Returns info about the command or an empty class if not found. */ public CommandInfo getCommand(String name) { CommandInfo info = new CommandInfo(); try { Method method = getClass().getMethod(name); Command cmd = (Command) method.getAnnotation(Command.class); if (cmd != null && cmd.value()) { info.name = name; Syntax syntax = (Syntax) method.getAnnotation(Syntax.class); if (syntax != null) { info.syntax = syntax.value(); } Description desc = (Description) method.getAnnotation(Description.class); if (desc != null) { info.description = desc.value(); } Alias alias = (Alias) method.getAnnotation(Alias.class); if (alias != null) { info.alias = alias.value(); } Tags tags = (Tags) method.getAnnotation(Tags.class); if (tags != null) { info.tags = tags.value(); } } } catch (NoSuchMethodException nsme) { } return info; }
/** * Gets help for a command handler. * * @return Returns a list with each line of command help. */ public List<CommandInfo> getCommandInfo() { List<CommandInfo> info = new ArrayList<CommandInfo>(); try { for (Method method : getClass().getMethods()) { CommandInfo cmdInfo = new CommandInfo(); Command cmd = (Command) method.getAnnotation(Command.class); if (cmd != null && cmd.value()) { cmdInfo.name = method.getName(); Syntax syntax = (Syntax) method.getAnnotation(Syntax.class); if (syntax != null) { cmdInfo.syntax = syntax.value(); } else { cmdInfo.syntax = header + method.getName(); } Alias alias = (Alias) method.getAnnotation(Alias.class); if (alias != null) { cmdInfo.alias = alias.value(); } Tags tags = (Tags) method.getAnnotation(Tags.class); if (tags != null) { cmdInfo.tags = tags.value(); } Description desc = (Description) method.getAnnotation(Description.class); if (desc != null) { cmdInfo.description = desc.value(); info.add(cmdInfo); } } } } catch (SecurityException se) { se.printStackTrace(); } return info; } }
Now that you have your base class. We need to create a command handler to hold our commands.
For this example I will create a GM command handler named 'GMCmd.java'. (Same package)
Now that you have your class you will need to extend it from the base class like so:
Now you could start adding commands but we want to make sure the user is actually a GM.
To do this we need to override the execute method from the base class. ('CommandHandler.java')
@Override public void execute() throws Exception { if (chr.isGM()) { // Checks if the user is GM. super.execute(); // Executes the command. } }
Now that we made sure only GM's can use these commands, lets create our first command.
For this example we will create a command that disconnect's a user from the game.
This is what the code would look like: (Place this under the execute method)
PHP Code:
@Command @Syntax("!dc [name]") @Description("Disconnects [name] from the game.") @Alias({"kick"}) // User can type !kick or !dc. public void dc() { MapleCharacter p = world.getPlayerStorage().getCharacterByName(args[1]); if (this.chr.gmLevel() > p.gmLevel()) { p.getClient().disconnect(); } }
@Override public void execute() throws Exception { if (chr.isGM()) { // Checks if the user is GM. super.execute(); // Executes the command. } }
@Command @Syntax("!dc [name]") @Description("Disconnects [name] from the game.") @Alias({"kick"}) // User can type !kick or !dc. public void dc() { MapleCharacter p = world.getPlayerStorage().getCharacterByName(args[1]); if (this.chr.gmLevel() > p.gmLevel()) { p.getClient().disconnect(); } } }
You may need to change some things in your code to get it working. Some methods such as 'chr.isGM()' may not exist on your server.
It's that easy!
Alright now that we have our commands setup correctly we need to change our CommandProcessor.java to allow for the changes.
Every time someone performs a command a handler must be created with the client of the player so the variables can be populated.
Like so:
PHP Code:
GMCmd gmCmd = new GMCmd(c, heading, args);
Then all you need to do is call the execute method you created earlier.
Example of my PlayerCmd.java with @Commands:
This commands shows an example of tags and adding specific conditions when a command can be run.
@Commands is how the help would be listed to the user. (With page selection)
/** * Allows for any code to be run before the command is executed. Note: * super.execute() is required for the command to execute. * * @throws Exception Throws any exception that may occur while the command * is executed. */ @Override public void execute() throws Exception { // Checks to see if the character is in the correct group. (gm level) if (MapleGroup.PLAYER.atleast(chr)) { if ((!chr.canTalk() || chr.inJail()) && !chr.isGM()) { chr.message(1, "Your chat has been disabled."); setHandled(true); } else if (chr.inTutorial() && hasCommand(args[0]) && !hasTag(args[0], "tutorial") && !chr.isGM()) { chr.message("Please wait until you are out of the tutorial to " + "use that command. If you need help type @help or @callgm."); setHandled(true); } else { super.execute(); } } }
@Command @Tags({"tutorial"}) public void commands() { int pageMax = 8; int page = 1; try { page = Integer.parseInt(args[1]); } catch (Exception e) { } List<CommandInfo> commands = getCommandInfo(); Collections.sort(commands, new CmdSortASC()); int possiblePages = (int) (commands.size() / pageMax); if (commands.size() % pageMax > 0) { possiblePages++; } if (page > possiblePages) { page = possiblePages; } else if (page < 1) { page = 1; } if (commands.size() > 0) { int lastOfPage = (commands.size() - Math.max(0, commands.size() - (page * pageMax))) - 1; commands = commands.subList((page - 1) * pageMax, lastOfPage); chr.message("== Player Commands " + page + "/" + possiblePages + " =="); chr.message("For help with commands type '@help commands'."); for (CommandInfo info : commands) { if (info != null) { chr.message(info.toString()); } } } else { chr.message("Sorry no player command help yet."); } }
Change Log:
Added code to show how simple a command could be.
Made it so the pictures would show. (Fixed link too)
Added support for methods with capitals. (Commands ignore case for users anyways)
Removed the packet spam check I forgot to take out.
Thanks guys let me know what you think and how this worked for you!
That's not really the point though is it? The point is to be able to read through your code more easily and create additional commands more easily.
I don't know about "more easily" since I can easily read through my commands files ( still using the if-else way! ). I can also create additional commands "easily" in mine; so I don't see where you're getting at there. Sure it'd help "new" people, but they probably wouldn't know how Annotations work if they can't read simple if-else statements. ( Not implying you'd need to know how they work to use this, since you could just copy+paste the commnads you have and alter them. ) Also, another negative I see is not being able to use all cases of the command string. I.E: !hEaL. Just my opinion.
Anyways, IMO the best part about this is the saving the commands to a hashmap ( I think Moogra talked about doing that in some post ).
As far as retrieving the action to perform (what to put on the execution stack when someone does !xxx) it should be somewhat faster. Probably not really noticeable though.
Blue's post was pointless, its obvious (stated) that performance increase isn't the over arching goal of this. So, in that respect, I personally think it didn't achieve its goal, at least entirely; its aesthetically cleaner in certain ways but with annotations (as Veda eluded to) you sort of added a new set of complexities, somewhat defeating the purpose (I'm not suggesting annotations are the only "downside").
After more carefully analyzing the command architecture in aurasea, I've decided that (although the model I reviewed was sort of... eh... messy?) that, that was truly the ideal way to do it.
@ts
Good job otherwise, pretty cleanly written, good use of annotations... but yeah, I've made my criticism already, good job. You could always improve though, not gonna get onto that, you should be happy - not worried :)
Edit: another flaw in blue's post was that he said "probably". Now blue, this makes you look very stupid, you start out by saying "it may look cleaner" then proceed to insult it for no reason when you don't actually know what you're saying is true, then add "lol" to water it down. Why did you even post? Most ignorant and pathetic thing you've posted in a while. Just goes to show what you know, despite how you push that facade in everyone's face.
I'm sorry ragezone, that wasn't very merry to say. Have a nice day!
... Also, another negative I see is not being able to use all cases of the command string. I.E: !hEaL. Just my opinion.
...
The player in-game can use as many combinations as he wishes of capitals and lowercase. The string gets put to lowercase then it's compared with the methods in the class. If you wish (for some reason) to have a command that can only be called by having the exact case then you can go into the base class and remove ".tolowercase()".
A command only must have the '@Command' tag in order to work. So a command can be as simple as:
PHP Code:
@Command public void heal() { chr.setHpMp(30000); }
A problem with this system when coding is if you accidentally make your method have any capitals in it. It won't work. I will probably update it so that it is compatible for capitals.
I really like this. It seems a lot cleaner in my opinion, and the Tags and Alias idea is really useful. I will definatly use this!
Thanks a lot for releasing this. :)