I was inspired by a recent thread to release my command system. This code is more than 4 years old, so it's not the greatest, but I also cleaned it up a little bit. It is fairly similar to Lithium's, but I think it's simpler and more flexible, though I don't remember too much about it. I just know they both use reflection at the core.
The Code:
client.commands.GMLevel:
PHP Code:
package client.commands;
public enum GMLevel {
PLAYER('@'),
DONOR('!'),
INTERN('!'),
GM('!'),
ADMIN('!');
char commandChar;
GMLevel(char commandChar) {
this.commandChar = commandChar;
}
public static GMLevel getByLevel(int level) {
return values()[level];
}
public static boolean isCommandChar(char a) {
for (GMLevel g : values()) {
if (g.getCommandChar() == a) {
return true;
}
}
return false;
}
public int getValue() {
return ordinal();
}
public char getCommandChar() {
return commandChar;
}
}
GMLevel is used universally throughout the source; all Characters should have one. Adapt your current GMLevel-equivalent or replace with this one, as this is the one I will use in the rest of the command system. It's pretty standard in implementation.
client.commands.CommandProcessor:
PHP Code:
package client.commands;
import client.MapleCharacter;
import client.MapleClient;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.*;
@Slf4j
public class CommandProcessor {
protected static Map<GMLevel, Map<String, Command>> commands = new EnumMap<>(GMLevel.class);
public static void load() {
for (GMLevel gmLevel : GMLevel.values()) {
commands.put(gmLevel, new LinkedHashMap<>());
}
Class<?>[] commandClasses = {AdminCommands.class, GMCommands.class, InternCommands.class, DonorCommands.class, PlayerCommands.class};
for (Class<?> abstractCommand : commandClasses) {
for (Class<?> command : abstractCommand.getClasses()) {
if (!Modifier.isAbstract(command.getModifiers())) {
try {
Command cm = (Command) command.newInstance();
commands.get(cm.getReqGMLevel()).put(cm.getName().toLowerCase(), cm);
} catch (Exception e) {
log.error("Error creating command " + command + " by reflection", e);
}
}
}
}
}
public enum ProcessCommandResponse {
ERR_NO_COMMAND_CHAR,
ERR_NOT_A_COMMAND,
ERR_INSUFFICIENT_PRIVILEGES,
SUCCESS
}
public static ProcessCommandResponse processCommand(MapleClient c, String text) {
char commandChar = text.charAt(0);
if (!GMLevel.isCommandChar(commandChar)) {
return ProcessCommandResponse.ERR_NO_COMMAND_CHAR;
}
String splitted[] = text.substring(1).split(" ");
GMLevel[] values = GMLevel.values();
MapleCharacter chr = c.getCharacter(); // this is important; it keeps a reference to the chr during cc commands
Command cm = null;
boolean isCommandCharAllowed = false;
// check for a valid command
for (int i = chr.getGMLevel(); i >= GMLevel.PLAYER.getValue(); i--) {
if (values[i].getCommandChar() == commandChar) {
isCommandCharAllowed = true;
Command cmd = commands.get(values[i]).get(splitted[0].toLowerCase());
if (cmd != null) {
cm = cmd;
break;
}
}
}
if (cm == null) {
// if they have a high enough GMLevel to use the commandChar, send NOT_A_COMMAND
// else, they have insufficient privileges, e.g. player (who can only use '@') used '!'
if (isCommandCharAllowed) {
return ProcessCommandResponse.ERR_NOT_A_COMMAND;
}
return ProcessCommandResponse.ERR_INSUFFICIENT_PRIVILEGES;
}
// remove the command name so the only strings passed to the command are the arguments, if any
String[] args = new String[splitted.length - 1];
System.arraycopy(splitted, 1, args, 0, splitted.length - 1);
try {
cm.run(c, args);
} catch (Exception e) {
chr.dropMessage("Error! Please check your syntax, or wait for the command to be fixed.");
log.error("Exception in command: " + commandChar + cm.getName(), e);
}
if (cm.getReqGMLevel().getValue() >= GMLevel.GM.getValue()) {
SimpleDateFormat logFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
GMCommandLog cmdLog = new GMCommandLog(text, chr.getMapId(), logFormatter.format(Calendar.getInstance().getTime()));
chr.addGMLog(cmdLog);
}
return ProcessCommandResponse.SUCCESS;
}
[MENTION=1333435839]Allar[/MENTION]gsConstructor
[MENTION=2000147484]Get[/MENTION]ter
public static class GMCommandLog {
private final String commandText;
private final int mapId;
private final String timestamp;
}
}
Remove the lombok annotations and adapt the GM command logging if your source has one.
Then call this on server startup:
PHP Code:
CommandProcessor.load();
client.commands.Command:
PHP Code:
package client.commands;
import client.MapleClient;
public interface Command {
GMLevel getReqGMLevel();
String getName();
String getDesc();
void run(MapleClient c, String[] commandArgs);
}
Main interface of the system. All commands will have a required GM level to use, a name, and a run method. The description is optional, but is used to auto-populate the help command for each GMLevel.
Now we can actually define commands. Let's start with the intern one, because it's an empty file. I never had interns, but I do have one now...
PHP Code:
package client.commands;
public abstract class InternCommands implements Command {
public GMLevel getReqGMLevel() {
return GMLevel.INTERN;
}
public String getDesc() {
return "";
}
}
Okay, so that's what the "root" abstract command classes will look like. They define an empty description and a GM level, which is inherited by all subclasses. The subclasses will be the actual commands. Let's take player commands, populated with some commands, as an example.
PHP Code:
package client.commands;
import client.MapleCharacter;
import client.MapleClient;
import scripting.npc.NPCScriptManager;
import scripting.quest.QuestScriptManager;
import tools.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public abstract class PlayerCommands implements Command {
public GMLevel getReqGMLevel() {
return GMLevel.PLAYER;
}
public String getDesc() {
return "";
}
public static class EnableActions extends PlayerCommands {
public String getName() {
return "dispose";
}
public void run(MapleClient c, String[] args) {
c.enableActions();
NPCScriptManager.getInstance().dispose(c);
QuestScriptManager.getInstance().dispose(c);
c.getCharacter().dropMessage("Disposed.");
}
}
public static class OpenCody extends PlayerCommands {
public String getName() {
return "cody";
}
public String getDesc() {
return "Opens Cody";
}
public void run(MapleClient c, String[] args) {
NPCScriptManager.getInstance().start(c, 9200000);
}
}
}
Note that each command is its own class, inside of the GMLevel's command "root" class. In this case we're talking about player commands, so they are inner classes defined inside PlayerCommands.java and they extend it. This should be similar to what most people are used to by now, I think. As you can see, each command can override the description if you want it to; it will be used later on for the help command.
Finally, wire it up to the chat handler:
PHP Code:
CommandProcessor.ProcessCommandResponse cmdResponse = CommandProcessor.processCommand(c, text);
switch (cmdResponse) {
case ERR_INSUFFICIENT_PRIVILEGES:
case ERR_NO_COMMAND_CHAR:
break;
case ERR_NOT_A_COMMAND:
c.getCharacter().dropMessage("That command does not exist.");
return;
case SUCCESS:
return;
}
Well, that's pretty much it for the system; I'll talk a little bit about ways to share functionality between commands and some examples of extra uses for commands.
Accessing Commands and Sharing Functionality
This isn't necessarily specific to this command system (though some of it is), but is just general good practices that I want to go over.
Example of sharing functionality through a separately defined private static function:
PHP Code:
package client.commands;
import client.MapleCharacter;
import client.MapleClient;
import client.MapleStat;
import constants.WzStrings;
import net.handler.channel.ChangeChannelHandler;
import scripting.npc.NPCScriptManager;
import scripting.quest.QuestScriptManager;
import server.DropEntryProvider;
import tools.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public abstract class PlayerCommands implements Command {
private static void addStat(MapleCharacter chr, MapleStat stat, int value) {
if (chr.getAp() < value) {
chr.dropMessage("Not enough AP.");
return;
} else if (value < 0) {
chr.dropMessage("Please enter a value greater than 0.");
return;
}
int old = 0;
if (stat == MapleStat.STR) {
old = chr.getStr();
chr.setStr(chr.getStr() + value);
} else if (stat == MapleStat.DEX) {
old = chr.getDex();
chr.setDex(chr.getDex() + value);
} else if (stat == MapleStat.INT) {
old = chr.getInt();
chr.setInt(chr.getInt() + value);
} else if (stat == MapleStat.LUK) {
old = chr.getLuk();
chr.setLuk(chr.getLuk() + value);
} else {
chr.dropMessage("Error!");
return;
}
chr.updateSingleStat(stat, old + value, true);
chr.setAp(chr.getAp() - value, true);
}
public GMLevel getReqGMLevel() {
return GMLevel.PLAYER;
}
public String getDesc() {
return "";
}
public static class AddStr extends PlayerCommands {
public String getName() {
return "str";
}
public String getDesc() {
return "Adds an amount to your " + getName() + ". Syntax: @" + getName() + " <value>";
}
public void run(MapleClient c, String[] args) {
addStat(c.getCharacter(), MapleStat.STR, Integer.parseInt(args[0]));
}
}
public static class AddDex extends PlayerCommands {
public String getName() {
return "dex";
}
public String getDesc() {
return "Adds an amount to your " + getName() + ". Syntax: @" + getName() + " <value>";
}
public void run(MapleClient c, String[] args) {
addStat(c.getCharacter(), MapleStat.DEX, Integer.parseInt(args[0]));
}
}
public static class AddInt extends PlayerCommands {
public String getName() {
return "int";
}
public String getDesc() {
return "Adds an amount to your " + getName() + ". Syntax: @" + getName() + " <value>";
}
public void run(MapleClient c, String[] args) {
addStat(c.getCharacter(), MapleStat.INT, Integer.parseInt(args[0]));
}
}
public static class AddLuk extends PlayerCommands {
public String getName() {
return "luk";
}
public String getDesc() {
return "Adds an amount to your " + getName() + ". Syntax: @" + getName() + " <value>";
}
public void run(MapleClient c, String[] args) {
addStat(c.getCharacter(), MapleStat.LUK, Integer.parseInt(args[0]));
}
}
As you can see, there is room for improvement here, even though we've moved most of the logic to its own private function.
We can improve this code by adding an extra layer of inheritance, as demonstrated by the following example:
Example of sharing functionality by adding a layer of inheritance:
PHP Code:
package client.commands;
import client.*;
import client.inventory.IItem;
import client.inventory.InventoryType;
import client.inventory.Item;
import client.inventory.MapleInventoryManipulator;
import client.status.PlayerDisease;
import client.status.PlayerDisease.PlayerDiseaseType;
import server.life.MobSkill;
import server.life.MobSkillFactory;
import server.maps.objects.MapObjectType;
import server.maps.objects.MapleMapItem.DropType;
import server.maps.objects.MapleMapObject;
import storage.dao.DatabaseConnection;
import tools.MaplePacketCreator;
import tools.Pair;
@Slf4j
public abstract class GMCommands implements Command {
public GMLevel getReqGMLevel() {
return GMLevel.GM;
}
public String getDesc() {
return "";
}
static abstract class GiveDisease extends GMCommands {
protected static void applyDisease(MapleCharacter chr, PlayerDiseaseType t, MobSkill m, boolean permanent) {
PlayerDisease disease;
if (permanent) {
disease = new PlayerDisease(t, m, true);
} else {
disease = new PlayerDisease(t, m, m.getDuration());
}
chr.giveDisease(disease);
}
public abstract String getName();
public abstract void run(MapleClient c, String[] args);
}
public static class GiveSeduce extends GiveDisease {
public String getName() {
return "seduce";
}
public void run(MapleClient c, String[] args) {
MapleCharacter chr = c.getChannelServer().getCharacter(args[0]);
if (chr != null) {
applyDisease(chr, PlayerDiseaseType.SEDUCE, MobSkillFactory.getSkill(128, 1), false);
}
}
}
public static class GiveStunMap extends GiveDisease {
public String getName() {
return "stunmap";
}
public void run(MapleClient c, String[] args) {
for (MapleCharacter chr : c.getCharacter().getMap().getCharacters()) {
if (chr.getGMLevel() < GMLevel.GM.getValue())
applyDisease(chr, PlayerDiseaseType.STUN, MobSkillFactory.getSkill(123, 12), true);
}
}
}
}
Applying the above concept to the stat code, we could remove the need for separate getName() and getDesc() implementations to each command.
Accessing commands:
Sometimes you want to access all commands for a certain GMLevel. This is how to do it, and this is what the help command(s) would look like:
PHP Code:
public static class Help extends PlayerCommands {
public String getName() {
return "help";
}
public void run(MapleClient c, String[] args) {
for (Command cm : CommandProcessor.commands.get(getReqGMLevel()).values()) {
StringBuilder cmBuilder = new StringBuilder();
cmBuilder.append(getReqGMLevel().getCommandChar()).append(cm.getName());
if (!cm.getDesc().isEmpty()) {
cmBuilder.append("\t-\t").append(cm.getDesc());
}
c.getCharacter().dropMessage(cmBuilder.toString());
}
}
}
This is for player commands. Similar thing for gm and admin commands, but a caveat is that the highest eligible GMLevel command will be ran if multiple commands have the same name. For example, if you have a !help in GMCommands and !help in AdminCommands, if an admin types it, the admin command will be ran. You can change names to get around this, like !gmcommands and !admincommands or !gmhelp and !adminhelp, etc.
Using commands anywhere:
Naturally, there is also the flexibility of using a command anywhere in the source. For example, to bandaid a bad skill/mastery issue, some sources max all skills at login. If you have code for this in a command, using it is as simple as this (though this likely isn't unique to this system):
PHP Code:
new GMCommands.MaxSkills().run(c, new String[0]);
Quote:
So, with that being said, some of you may like what your repack/source/whatever already uses, but I find my version mighty spiffy, and the least controversial of the attempts made by programmers-past to redo this (surprisingly) central feature.