Would have posted in snippets, however the guidelines said considerable amounts of code go here, so uh here goes. If somebody wants to move it then please do.
Before we get started: being a plugin developer for Bukkit I'm obviously used to rather different code style than most RSPS sources, what with plugins being loaded on startup/reload, etc. I admire how the command/plugin system works in Bukkit so I wondered "why not make something similar for an RSPS?" so here it is.
If anyone wishes to be critical of this post then I only request you try do it as constructively as possible being my first post here, as soon as I've made more then feel free to tear this thread apart.
Requirements
Matrix 718 source (I haven't used other bases, this may work for them, it may not.)
Patience
Knowledge of how to add .jar files as libraries in your preferred IDE.
What are the results of this tutorial?
The idea of this tutorial is to separate commands from being cluttered in a single file and to be able to add a command class and have the game automatically acknowledge that it exists.
How does this work?
The Reflections library is used to seek out every class that implements the RSCommand interface, a class called CommandManager then registers and processes commands whenever they're called.
Why should I use this?
Honestly, you don't have to. However I just prefer dealing with code in this sort of manner, some people may view it as messy but I'd much rather have a lot of class files lying around than a single class where you have to flick all over the place just to find what you want.
Spoiler for Media:
Command line view:
Example of output, handled entirely by CommandManager minus the Args output which is a command I setup to ensure arguments were received properly from WorldPacketsDecoder
Output when a user is denied access to a command or it returns CommandResult.SUCCESS
Let's get started!
First up add the Reflections library to your project - I use Maven with IntelliJ - links to the relevant files are available at the Reflections link above.
You're going to have to create a package to store the code, I used "com.rs.commands" however I expect that some of the programming veterans here will have a more appropriate to put this
* There is some manual problem solving you will have to do, for anyone with a brain it should be easy.
IF anybody is interested then I've included the Javadoc comments so that people can read how I intended it to work and laugh at how it actually works.
Once you've created that package create a new class called CommandManager and paste the following code into it.
Spoiler for CommandManager.java:
Code:
package com.rs.commands;
import com.rs.Settings;
import com.rs.game.player.Player;
import com.rs.utils.Logger;
import org.reflections.Reflections;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Set;
import static com.rs.commands.CommandResult.EXCEPTION;
import static com.rs.commands.CommandResult.NO_RIGHTS;
/**
* One class to rule them all.
* This class is responsible for loading and registration of
* all classes that implement the {@link RSCommand} interface.
*
* @author Connor Spencer Harries
*/
public final class CommandManager {
private static HashMap<String, Class<? extends RSCommand>> commandMap = new HashMap<>();
/**
* Register a new command and its associated class.
* @param name command name.
* @param command command class.
* @return true if the command was registered.
*/
public static boolean registerCommand(String name, Class<? extends RSCommand> command) {
if(commandExists(name))
return false;
Method method = null;
try {
method = command.getMethod("run", Player.class, String[].class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
if(method != null) {
if (!method.isAnnotationPresent(Command.class)) {
Logger.handle(new Exception("\"" + command.getSimpleName() + ".run()\" is Missing \"@Command\" Annotation."));
return false;
}
commandMap.put(name, command);
}
return commandMap.containsKey(name);
}
/**
* Test to see whether a command exists or not, useful for using before you call things.
* @param name
* @return
*/
public static boolean commandExists(String name) {
return commandMap.containsKey(name);
}
/**
* Try and process the command, very experimental right now as Reflection is
* heavily used.
*
* @param sender command sender.
* @param command command sent.
* @param args args sent.
* @return
*/
public static boolean processCommand(Player sender, String command, String[] args) {
if(commandExists(command)) {
Class<? extends RSCommand> commandClass = getCommand(command);
try {
Object commandInstance = commandClass.newInstance();
Method method = commandClass.getMethod("run", Player.class, String[].class);
Command commandParams = method.getAnnotation(Command.class);
if(sender.hasRights(commandParams.rights())) {
CommandResult result = (CommandResult) method.invoke(commandInstance, sender, args);
switch (result) {
case SUCCESS:
if(Settings.DEBUG) {
Logger.log(String.format("Player \"%s\" executed command \"%s\" successfully.", sender.getUsername(), command));
}
return true;
case INVALID_ARGS:
sender.sendMessage(String.format(result.getMessage(), commandParams.usage()));
break;
case EXCEPTION:
sender.sendMessage(result);
break;
default:
break;
}
} else {
sender.sendMessage(NO_RIGHTS);
if(Settings.DEBUG) {
Logger.log("\"" + sender.getUsername() + "\" was denied access to command: \"" + command + "\"");
}
}
} catch (Exception ex) {
sender.sendMessage(EXCEPTION);
if(sender.hasRights(Rights.DEVELOPER)) {
sender.sendMessage("<col=ff0000>See the console for more details.");
}
Logger.handle(ex);
}
} else {
sender.sendMessage("<col=ff0000>Sorry, that command does not exist.");
}
return false;
}
/**
* Get the class associated with a command.
* @param name command to lookup.
* @return null if the command has not yet been registered.
*/
private static Class<? extends RSCommand> getCommand(String name) {
return (commandMap.get(name) != null ? commandMap.get(name) : null);
}
/**
* Register a command alias to allow more than one command
* to trigger the code behind said command.
*
* @param existing name of command.
* @param alias alias of command.
* @return true if the command was registered.
*/
public static boolean registerAlias(String existing, String alias) {
Class<? extends RSCommand> command = getCommand(existing);
if(command != null && !commandMap.containsKey(alias)) {
commandMap.put(alias, command);
} else {
return false;
}
return (commandMap.get(alias) != null);
}
/**
* Replacement for the constructor.
*/
public static synchronized void init() {
Reflections ref = new Reflections(Settings.COMMAND_ROOT);
Set<Class<? extends RSCommand>> classes = ref.getSubTypesOf(RSCommand.class);
for(Class<? extends RSCommand> commandClass : classes) {
try {
Method run = commandClass.getMethod("run", Player.class, String[].class);
if(run.isAnnotationPresent(Command.class)) {
Command command = run.getAnnotation(Command.class);
String name = command.name();
if(registerCommand(name, commandClass)) {
int aliasCount = 0;
if (command.aliases().length > 0) {
String[] aliases = command.aliases();
for (String alias : aliases) {
if(registerAlias(name, alias))
aliasCount++;
}
}
if(aliasCount > 0) {
Logger.log("Registered command: \"" + name + "\" with " + aliasCount + " " + (aliasCount > 1 ? "aliases." : "alias."));
} else {
Logger.log("Registered command: \"" + name + "\" with no aliases.");
}
}
}
} catch (Exception e) {
Logger.handle(e);
}
}
}
}
If you go into your Launcher.java file add "CommandManager.init();" to the bottom of the main method.
Next up is the Command interface, all the command information comes from this so uh, try not to screw up. (There'll be an explanation of how everything works - including this - at the bottom of the thread)
Spoiler for Command.java:
Code:
package com.rs.commands;
import java.lang.annotation.*;
/**
* Annotation to mark methods as command handling methods.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Command {
/**
* Minimum rights required to use the command.
* Valid rights:
* <ol>
* <li>PLAYER</li>
* <li>MOD</li>
* <li>ADMIN</li>
* </ol>
*
* @return Required rights.
*/
Rights rights() default Rights.PLAYER;
/**
* @return command name.
*/
String name();
/**
* @return command usage.
*/
String usage();
/**
* @return list of aliases used to execute the command.
*/
String[] aliases() default {};
}
Up next is the enum that is responsible for sending response messages to players, it's possible to omit this however I like how simplistic this is and how practical it is.
Spoiler for CommandResult.java:
Code:
package com.rs.commands;
/**
* Simple enum that contains possible command execution results.
*
* @author Connor Spencer Harries
*/
public enum CommandResult {
SUCCESS,
NO_RIGHTS("<col=ff0000>You do not have permission to use that command."),
INVALID_ARGS("<col=ff3636>Invalid command usage, try \"%s\""),
EXCEPTION("<col=ff3636>An unknown exception occurred whilst executing the command.");
/**
* Message storage.
*/
private final String message;
/**
* Assign no value to message.
*/
private CommandResult() { message = null; }
/**
* Assign a custom message to the enum.
* @param message message to be displayed when toString() is called.
*/
private CommandResult(final String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
/**
* Lazy way to use the enum values.
* @return appropriate message.
*/
@Override
public String toString() {
return this.message;
}
}
This is the interface that all commands will have to implement in order to be recognised by the CommandManager init method.
Spoiler for RSCommand.java:
Code:
package com.rs.commands;
import com.rs.game.player.Player;
/**
* Superclass for all text based commands.
*
* @see com.rs.commands.CommandResult
* @author Connor Spencer Harries
*/
public interface RSCommand {
/**
* Code that gets called when the command is executed.
* @param args Arguments to pass.
* @return true if the command executed correctly.
*/
public CommandResult run(Player player, String[] args);
}
Finally another practical enum, you can easily alter this and have it update in numerous places at once, no more annoying tapping numbers in everywhere to check!
Spoiler for Rights.java:
Code:
package com.rs.commands;
/**
* Possible replacement for all right-based actions.
* @author Connor Spencer Harries
*/
public enum Rights {
PLAYER(0),
MOD(1),
ADMIN(2),
DEVELOPER(1337);
private final int rights;
private Rights(final int rights) {
this.rights = rights;
}
public Integer get() {
return this.rights;
}
public static Rights rightsFromInteger(Integer integer) {
for(Rights rights : values()) {
if(rights.get().equals(integer))
return rights;
}
return PLAYER;
}
}
That's it for the "com.rs.commands" package, now comes the problem solving.
BONUS:
Here is an example of what your commands will now look like, I went with a simple one just to give off the general idea. (This file goes in the "com.rs.commands.executors" package - that's the package I used for my COMMAND_ROOT)
Spoiler for Teleport.java:
Code:
package com.rs.commands.executors;
import com.rs.commands.Command;
import com.rs.commands.CommandResult;
import com.rs.commands.RSCommand;
import com.rs.commands.Rights;
import com.rs.game.WorldTile;
import com.rs.game.player.Player;
import static com.rs.commands.CommandResult.*;
/**
* Information:
* Created at: 13:22 on 19/07/2014
* Project: Runescape - Server
* Created by Connor Harries.
*/
public class Teleport implements RSCommand {
@Command(
name = "tele",
usage = "tele X Y",
rights = Rights.MOD,
aliases = { "tp", "tpto", "teleport" }
)
@Override
public CommandResult run(Player player, String[] args) {
try {
player.resetWalkSteps();
if (args.length < 2)
return INVALID_ARGS;
player.setNextWorldTile(new WorldTile(Integer.valueOf(args[0]), Integer.valueOf(args[1]), player.getPlane()));
} catch (NumberFormatException ex) {
return INVALID_ARGS;
} catch (Exception e) {
return EXCEPTION;
}
return SUCCESS;
}
}
Now that I've wasted enough of your time I feel like some explanation is due!
Scan Settings.COMMAND_ROOT for any classes implementing RSCommand
Test class one-by-one for the @Command annotation
Get name from @Command and call #registerCommand(), passing the #name() and class as arguments.
Check @Command for command aliases & register them if any are specified
Wait for the command to be called
When a command is called #get() is called on the HashMap and the #run(Player, String[]) method is called.
You can now replace the default code at line 1365 (for me at least) with this code in order to tell the CommandManager class to handle commands, there's another place which you will have to edit in order to make it handle console commands, however I currently have no implementation for differentiating between chat and console commands.
The Command annotation
You may or may not have seen annotations like this before, if you haven't I'll be surprised honestly unless you're a leech. Standard Java annotations include @Deprecated and @Override, these are the two I see most commonly at least.
But they don't have parameters?!
No, they don't require parameters to do their job. My Command annotation has parameters because without them the CommandManager class doesn't know how to deal with the command itself.
What data can the annotation contain?
The annotation has four fields, two of which have the most basic defaults available, the other two (name & usage) MUST be assigned to - though I'm sure you clever people can figure out how to make it so only the name is required.
Fields:
name - the main command itself
rights - the minimum rights required to actually trigger the run method
aliases - a String[] containing other commands which can be used to trigger the run method
usage - how to use the command, eg: "tele X Y"
Last edited by Frostcore; 07-20-2014 at 07:11 PM.
Reason: Updated information.
Eh, I tried at least
I think it could be because I wasn't sure about how to do it in the most efficient manner so I just fell back on what I already knew to do it.
I'm more than happy accepting edits and corrections