Archived
0

Add dicore3-command

This commit is contained in:
Dico200
2018-07-25 01:53:23 +01:00
parent 5e168847c2
commit 44587e49ff
76 changed files with 6677 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
package io.dico.dicore.command;
import io.dico.dicore.command.predef.HelpCommand;
import java.util.*;
public class ChildCommandAddress extends ModifiableCommandAddress {
ModifiableCommandAddress parent;
final List<String> namesModifiable = new ArrayList<>(4);
List<String> names = namesModifiable;
Command command;
public ChildCommandAddress() {
}
public ChildCommandAddress(Command command) {
this.command = command;
}
public ChildCommandAddress(Command command, String name, String... aliases) {
this(command);
addNameAndAliases(name, aliases);
}
public static ChildCommandAddress newPlaceHolderCommand(String name, String... aliases) {
ChildCommandAddress rv = new ChildCommandAddress(null, name, aliases);
HelpCommand.registerAsChild(rv);
return rv;
}
@Override
public boolean isRoot() {
return false;
}
@Override
public ModifiableCommandAddress getParent() {
return parent;
}
@Override
public Command getCommand() {
return command;
}
@Override
public void setCommand(Command command) {
if (hasUserDeclaredCommand()) {
throw new IllegalStateException("Command is already set at address \"" + getAddress() + "\"");
}
this.command = command;
}
@Override
public List<String> getNames() {
return names;
}
public void addNameAndAliases(String name, String... aliases) {
names.add(name);
names.addAll(Arrays.asList(aliases));
}
@Override
public String getMainKey() {
return namesModifiable.isEmpty() ? null : namesModifiable.get(0);
}
@Override
public String getAddress() {
ICommandAddress address = this;
int depth = getDepth();
String[] keys = new String[depth];
for (int i = depth - 1; i >= 0; i--) {
keys[i] = address.getMainKey();
address = address.getParent();
}
return String.join(" ", keys);
}
public void finalizeNames() {
if (names instanceof ArrayList) {
names = Collections.unmodifiableList(namesModifiable);
}
}
Iterator<String> modifiableNamesIterator() {
return namesModifiable.iterator();
}
void setParent(ModifiableCommandAddress parent) {
finalizeNames();
this.parent = parent;
}
}

View File

@@ -0,0 +1,191 @@
package io.dico.dicore.command;
import io.dico.dicore.command.IContextFilter.Priority;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.IArgumentPreProcessor;
import io.dico.dicore.command.parameter.Parameter;
import io.dico.dicore.command.parameter.ParameterList;
import io.dico.dicore.command.parameter.type.ParameterType;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public abstract class Command {
private static final String[] EMPTY_DESCRIPTION = new String[0];
private final ParameterList parameterList = new ParameterList();
private final List<IContextFilter> contextFilters = new ArrayList<>(3);
private String[] description = EMPTY_DESCRIPTION;
private String shortDescription;
public Command addParameter(Parameter<?, ?> parameter) {
parameterList.addParameter(parameter);
return this;
}
public <TType> Command addParameter(String name, String description, ParameterType<TType, Void> type) {
return addParameter(Parameter.newParameter(name, description, type, null, false, null));
}
public <TType, TParamInfo> Command addParameter(String name, String description, ParameterType<TType, TParamInfo> type, TParamInfo paramInfo) {
return addParameter(Parameter.newParameter(name, description, type, paramInfo, false, null));
}
public <TType> Command addFlag(String name, String description, ParameterType<TType, Void> type) {
return addParameter(Parameter.newParameter('-' + name, description, type, null, true, null));
}
public <TType, TParamInfo> Command addFlag(String name, String description, ParameterType<TType, TParamInfo> type, TParamInfo paramInfo) {
return addParameter(Parameter.newParameter('-' + name, description, type, paramInfo, true, null));
}
public <TType> Command addAuthorizedFlag(String name, String description, ParameterType<TType, Void> type, String permission) {
return addParameter(Parameter.newParameter('-' + name, description, type, null, true, permission));
}
public <TType, TParamInfo> Command addAuthorizedFlag(String name, String description, ParameterType<TType, TParamInfo> type, TParamInfo paramInfo, String permission) {
return addParameter(Parameter.newParameter('-' + name, description, type, paramInfo, true, permission));
}
public Command requiredParameters(int requiredParameters) {
parameterList.setRequiredCount(requiredParameters);
return this;
}
public Command repeatFinalParameter() {
parameterList.setRepeatFinalParameter(true);
return this;
}
public Command setDescription(String... description) {
this.description = Objects.requireNonNull(description);
return this;
}
public Command setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
return this;
}
public Command preprocessArguments(IArgumentPreProcessor processor) {
parameterList.setArgumentPreProcessor(processor);
return this;
}
public final ParameterList getParameterList() {
return parameterList;
}
public final String[] getDescription() {
return description.length == 0 ? description : description.clone();
}
public String getShortDescription() {
return shortDescription;
}
/**
* ---- CONTEXT FILTERS ----
* Filter the contexts. For example, if the sender must be a player but it's the console,
* throw a CommandException describing the problem.
* <p>
* The index of the first element in contextFilters whose priority is POST_PARAMETERS
* Computed by {@link #computeContextFilterPostParameterIndex()}
*/
private transient int contextFilterPostParameterIndex;
public Command addContextFilter(IContextFilter contextFilter) {
Objects.requireNonNull(contextFilter);
if (!contextFilters.contains(contextFilter)) {
contextFilters.add(contextFilter);
contextFilters.sort(null);
computeContextFilterPostParameterIndex();
}
return this;
}
public List<IContextFilter> getContextFilters() {
return Collections.unmodifiableList(contextFilters);
}
public boolean removeContextFilter(IContextFilter contextFilter) {
boolean ret = contextFilters.remove(contextFilter);
if (ret) {
computeContextFilterPostParameterIndex();
}
return ret;
}
private void computeContextFilterPostParameterIndex() {
List<IContextFilter> contextFilters = this.contextFilters;
contextFilterPostParameterIndex = 0;
for (int i = contextFilters.size() - 1; i >= 0; i--) {
if (contextFilters.get(i).getPriority() != Priority.POST_PARAMETERS) {
contextFilterPostParameterIndex = i + 1;
}
}
}
// ---- CONTROL FLOW IN COMMAND TREES ----
public boolean isVisibleTo(CommandSender sender) {
return true;
}
public boolean takePrecedenceOverSubcommand(String subCommand, ArgumentBuffer buffer) {
return false;
}
// ---- EXECUTION ----
public void execute(CommandSender sender, ICommandAddress caller, ArgumentBuffer buffer) {
ExecutionContext executionContext = new ExecutionContext(sender, caller, this, buffer, false);
try {
//System.out.println("In Command.execute(sender, caller, buffer)#try{");
int i, n;
for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) {
contextFilters.get(i).filterContext(executionContext);
}
executionContext.parseParameters();
for (n = contextFilters.size(); i < n; i++) {
contextFilters.get(i).filterContext(executionContext);
}
//System.out.println("Post-contextfilters");
String message = execute(sender, executionContext);
caller.getChatController().sendMessage(sender, EMessageType.RESULT, message);
} catch (Throwable t) {
caller.getChatController().handleException(sender, executionContext, t);
}
}
public abstract String execute(CommandSender sender, ExecutionContext context) throws CommandException;
public List<String> tabComplete(CommandSender sender, ICommandAddress caller, Location location, ArgumentBuffer buffer) {
ExecutionContext executionContext = new ExecutionContext(sender, caller, this, buffer, true);
try {
int i, n;
for (i = 0, n = contextFilterPostParameterIndex; i < n; i++) {
contextFilters.get(i).filterContext(executionContext);
}
} catch (CommandException ex) {
return Collections.emptyList();
}
executionContext.parseParametersQuietly();
return tabComplete(sender, executionContext, location);
}
public List<String> tabComplete(CommandSender sender, ExecutionContext context, Location location) {
return context.getSuggestedCompletions(location);
}
}

View File

@@ -0,0 +1,380 @@
package io.dico.dicore.command;
import io.dico.dicore.command.chat.IChatController;
import io.dico.dicore.command.parameter.type.IParameterTypeSelector;
import io.dico.dicore.command.parameter.type.MapBasedParameterTypeSelector;
import io.dico.dicore.command.parameter.type.ParameterType;
import io.dico.dicore.command.predef.HelpCommand;
import io.dico.dicore.command.predef.PredefinedCommand;
import io.dico.dicore.command.predef.SyntaxCommand;
import io.dico.dicore.command.registration.reflect.ReflectiveRegistration;
import java.util.HashSet;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Mimic of WorldEdit's CommandGraph
*/
public final class CommandBuilder {
private final RootCommandAddress root;
private ModifiableCommandAddress cur;
private IParameterTypeSelector selector;
/**
* Instantiate a new CommandBuilder with a new command root system
* Commands registered to this command builder might interfere with
* commands registered to other commands builders or by other plugins.
*/
public CommandBuilder() {
this(new RootCommandAddress());
}
/**
* Instantiate a new CommandBuilder with a specified root address.
* If the root address is identical to that of another command builder,
* they will modify the same tree.
*
* @param root the root address
*/
public CommandBuilder(RootCommandAddress root) {
this.root = Objects.requireNonNull(root);
this.cur = root;
this.selector = new MapBasedParameterTypeSelector(true);
}
/**
* Add a sub command at the current address
* The current address can be inspected using {@link #getAddress()}
*
* @param name the name of the command
* @param command the command executor
* @param aliases any aliases
* @return this
*/
public CommandBuilder addSubCommand(String name, Command command, String... aliases) {
ChildCommandAddress address = new ChildCommandAddress(command);
address.addNameAndAliases(name, aliases);
return addSubCommand(address);
}
/**
* Add a subcommand as an address at the current address
* The result of this call is the same as
* {@code addSubCommand(address.getMainKey(), address.getCommand(), address.getNames().sublist(1).toArray(new String[0]))}
*
* @param address the address
* @return this
* @throws IllegalArgumentException if {@code address.isRoot()}
*/
public CommandBuilder addSubCommand(ICommandAddress address) {
cur.addChild(address);
return this;
}
/**
* Search the given class for any (static) methods using command annotations
* The class gets a localized parameter type selector if it defines parameter types.
* Any commands found are registered as sub commands to the current address.
*
* @param clazz the clazz
* @return this
* @throws IllegalArgumentException if an exception occurs while parsing the methods of this class
* @see #registerCommands(Class, Object)
*/
public CommandBuilder registerCommands(Class<?> clazz) {
return registerCommands(clazz, null);
}
/**
* Search the given object's class for methods using command annotations.
* If the object is null, only static methods are checked. Otherwise, instance methods are also checked.
* The class gets a localized parameter type selector if it defines parameter types.
* Any commands found are registered as sub commands to the current address.
*
* @param object the object
* @return this
* @throws IllegalArgumentException if an exception occurs while parsing the methods of this class
* @see #registerCommands(Class, Object)
*/
public CommandBuilder registerCommands(Object object) {
return registerCommands(object.getClass(), object);
}
/**
* Search the given class for methods using command annotations.
* The class gets a localized parameter type selector if it defines parameter types.
* Any commands found are registered as sub commands to the current address.
* The instance is used to invoke non-static methods.
*
* @param clazz the class
* @param instance the instance, null if only static methods
* @return this
* @throws IllegalArgumentException if instance is not null and it's not an instance of the class
* @throws IllegalArgumentException if another exception occurs while parsing the methods of this class
*/
public CommandBuilder registerCommands(Class<?> clazz, Object instance) {
try {
ReflectiveRegistration.parseCommandGroup(cur, selector, clazz, instance);
return this;
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
/**
* register the {@link HelpCommand} as a sub command at the current address
*
* @return this
*/
public CommandBuilder registerHelpCommand() {
HelpCommand.registerAsChild(cur);
return this;
}
/**
* register the {@link SyntaxCommand} as a sub command a the current address
*
* @return this
*/
public CommandBuilder registerSyntaxCommand() {
SyntaxCommand.registerAsChild(cur);
return this;
}
/**
* Generate the predefined commands.
* These are presets.
* Examples include {@code help} and {@code syntax}.
* <p>
* Predefined commands can be registered through {@link PredefinedCommand#registerPredefinedCommandGenerator(String, Consumer)}
*
* @param commands the commands
* @return this
*/
public CommandBuilder generatePredefinedCommands(String... commands) {
for (String value : commands) {
Consumer<ICommandAddress> subscriber = PredefinedCommand.getPredefinedCommandGenerator(value);
if (subscriber == null) {
System.out.println("[Command Warning] generated command '" + value + "' could not be found");
} else {
subscriber.accept(cur);
}
}
return this;
}
/**
* Unregister any childs present at the given keys.
* <p>
* This method can be used to remove unwanted keys, that might have been added
* outside of your control. For example, because you didn't want all commands
* registered by {@link #registerCommands(Class, Object)}, or because you didn't
* want the help command registered by {@link #group(String, String...)}
*
* @param removeAliases true if any aliases of the children present at the keys should be removed
* @param keys a varargs array containing the keys
* @return this
* @throws IllegalArgumentException if keys array is empty
*/
public CommandBuilder unregisterCommands(boolean removeAliases, String... keys) {
cur.removeChildren(removeAliases, keys);
return this;
}
/**
* Jump to the sub-address with the given name as main key.
* If an address with the exact name as main key exists,
* that address becomes the current address.
* <p>
* Otherwise, a new addresses is registered with the name and aliases.
* New addresses registered by this command have a HelpCommand added by default.
* <p>
* After this call, any registered commands are registered as a sub command
* to the new address. To restore the previous state, a call to {@link #parent()}
* should be made.
* <p>
* If the address is the target of a command, it will provide information about its sub commands
* using the HelpCommand.
*
* @param name the main key
* @param aliases the aliases
* @return this
*/
public CommandBuilder group(String name, String... aliases) {
ChildCommandAddress address = cur.getChild(name);
if (address == null || !name.equals(address.getMainKey())) {
cur.addChild(address = ChildCommandAddress.newPlaceHolderCommand(name, aliases));
}
cur = address;
return this;
}
/**
* Sets the description of a group created by {@link #group(String, String...)}
* Can be called subsequently to making a call to {@link #group(String, String...)}
*
* @param shortDescription a short description
* @param description the lines of a full description.
* @return this
*/
public CommandBuilder setGroupDescription(String shortDescription, String... description) {
Command command = cur.getCommand();
cur.setCommand(command
.setShortDescription(shortDescription)
.setDescription(description));
return this;
}
/**
* Jump up a level in the address
*
* @return this
* @throws IllegalStateException if the address is empty
* // has a depth of 0 // is at level 0
*/
public CommandBuilder parent() {
if (cur.hasParent()) {
cur = cur.getParent();
return this;
}
throw new IllegalStateException("No parent exists at this address");
}
/**
* Jump to the root (empty) address,
* such that a subsequent call to {@link #parent()}
* will throw a {@link IllegalStateException}
*
* @return this
*/
public CommandBuilder root() {
cur = root;
return this;
}
/**
* Get the current address, as a space-separated string
*
* @return the current address
*/
public String getAddress() {
return cur.getAddress();
}
/**
* Get the depth of the current address.
* This is equivalent to {@code getAddress().split(" ").length}.
* If the address is empty, the depth is 0.
*
* @return the depth
*/
public int getDepth() {
return cur.getDepth();
}
/**
* Set the command at the current group. The command is set
* a level higher than it would be if this were a call to {@link #addSubCommand(String, Command, String...)}
* <p>
* If a call to {@link #setGroupDescription(String, String...)} was made at the same address before,
* the description is copied to the given executor.
*
* @param command the executor
* @return this
* @throws IllegalArgumentException if the command at the address is present and declared by the user,
* in other words, it's not a {@link PredefinedCommand}
*/
public CommandBuilder setCommand(Command command) {
Command current = cur.getCommand();
if (current instanceof HelpCommand && current != HelpCommand.INSTANCE) {
command.setShortDescription(current.getShortDescription());
command.setDescription(current.getDescription());
}
cur.setCommand(command);
return this;
}
/**
* Configure the chat controller at this address. The chat controller
* is used for all children down the tree if they don't explicitly have
* their own chat controller configured. If this isn't configured,
* {@code ChatControllers.defaultChat()} is used.
*
* @param chatController the chat controller
* @return this
*/
public CommandBuilder setChatController(IChatController chatController) {
cur.setChatController(chatController);
return this;
}
/**
* Add the parameter type to this builder's selector.
*
* @param type the type
* @param <T> the return type of the parameter type
* @return this
*/
public <T> CommandBuilder addParameterType(ParameterType<T, Void> type) {
selector.addType(false, type);
return this;
}
/**
* Add the parameter type to this builder's selector.
*
* @param infolessAlias whether to also register the type with an infoless alias.
* this increases the priority assigned to the type if no info object is present.
* @param type the type
* @param <T> the return type of the parameter type
* @param <C> the parameter config type (info object)
* @return this
*/
public <T, C> CommandBuilder addParameterType(boolean infolessAlias, ParameterType<T, C> type) {
selector.addType(infolessAlias, type);
return this;
}
/**
* Get the dispatcher for the root address.
* The dispatcher should be used to finally register all commands,
* after they are all declared.
*
* @return the dispatcher
*/
public ICommandDispatcher getDispatcher() {
return root;
}
/**
* Print debugging information about the current addresses and commands in this builder
* A StackTraceElement indicating where this was called from is also included
*
* @return this
*/
public CommandBuilder printDebugInformation() {
String address = cur == root ? "<root>" : cur.getAddress();
StackTraceElement caller = getCallsite();
StringBuilder message = new StringBuilder("### CommandBuilder dump ###");
message.append("\nCalled from ").append(caller);
message.append("\nPosition: ").append(address);
cur.appendDebugInformation(message, "", new HashSet<>());
System.out.println(message);
return this;
}
private static StackTraceElement getCallsite() {
// [0] Thread.currentThread()
// [1] CommandBuilder.getCallsite()
// [2] Calling method
// [3] Method calling the calling method
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
return trace.length > 3 ? trace[3] : null;
}
}

View File

@@ -0,0 +1,28 @@
package io.dico.dicore.command;
public class CommandException extends Exception {
public CommandException() {
}
public CommandException(String message) {
super(message);
}
public CommandException(String message, Throwable cause) {
super(message, cause);
}
public CommandException(Throwable cause) {
super(cause);
}
public static CommandException missingArgument(String parameterName) {
return new CommandException("Missing argument for " + parameterName);
}
public static CommandException invalidArgument(String parameterName, String syntaxHelp) {
return new CommandException("Invalid input for " + parameterName + ", should be " + syntaxHelp);
}
}

View File

@@ -0,0 +1,23 @@
package io.dico.dicore.command;
/**
* This enum is intended to provide some constants for default messages.
* Can be returned by a reflective command.
* Currently, no constants have an actual message.
* Prone to removal in the future because of lack of usefullness.
*/
public enum CommandResult {
SUCCESS(null),
QUIET_ERROR(null);
private final String message;
CommandResult(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,19 @@
package io.dico.dicore.command;
public enum EMessageType {
GOOD_NEWS,
BAD_NEWS,
NEUTRAL,
INFORMATIVE,
WARNING,
INSTRUCTION,
EXCEPTION,
RESULT,
CUSTOM,
DESCRIPTION,
SYNTAX,
HIGHLIGHT,
SUBCOMMAND,
NUMBER,
}

View File

@@ -0,0 +1,12 @@
package io.dico.dicore.command;
/**
* Override policies for registering to the command map
*/
public enum EOverridePolicy {
OVERRIDE_ALL,
MAIN_KEY_ONLY,
MAIN_AND_FALLBACK,
FALLBACK_ONLY,
OVERRIDE_NONE
}

View File

@@ -0,0 +1,352 @@
package io.dico.dicore.command;
import io.dico.dicore.command.chat.Formatting;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.ContextParser;
import io.dico.dicore.command.parameter.Parameter;
import io.dico.dicore.command.parameter.ParameterList;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.*;
/**
* The context of execution.
* <p>
* This class is responsible for the control flow of parameter parsing, as well as caching and providing the parsed parameter values.
* It is also responsible for keeping track of the parameter to complete in the case of a tab completion.
*/
public class ExecutionContext {
private final CommandSender sender;
private final ICommandAddress address;
private final Command command;
private final ArgumentBuffer originalBuffer;
private final ArgumentBuffer processedBuffer;
// caches the buffer's cursor before parsing. This is needed to provide the original input of the player.
private final int cursorStart;
// when the context starts parsing parameters, this flag is set, and any subsequent calls to #parseParameters() throw an IllegalStateException.
private boolean attemptedToParse;
// The parsed parameter values, mapped by parameter name.
// This also includes default values. All parameters from the parameter list are present if parsing was successful.
private Map<String, Object> parameterValueMap;
// this set contains the names of the parameters that were present in the command, and not given a default value.
private Set<String> parsedParameters;
// if this flag is set, this execution is only for completion purposes.
private boolean tabComplete;
// these fields store information required to provide completions.
// the parameter to complete is the parameter that threw an exception when it was parsing.
// the exception's message was discarded because it is a completion.
private Parameter<?, ?> parameterToComplete;
// this is the cursor that the ArgumentBuffer is reset to when suggested completions are requested.
private int parameterToCompleteCursor = -1;
// if this flag is set, any messages sent through the sendMessage methods are discarded.
private boolean muted;
/**
* Construct an execution context, making it ready to parse the parameter values.
*
* @param sender the sender
* @param address the address
* @param buffer the arguments
* @param tabComplete true if this execution is a tab-completion
*/
public ExecutionContext(CommandSender sender, ICommandAddress address, Command command, ArgumentBuffer buffer, boolean tabComplete) {
this.sender = Objects.requireNonNull(sender);
this.address = Objects.requireNonNull(address);
this.command = Objects.requireNonNull(command);
this.muted = tabComplete;
this.tabComplete = tabComplete;
// If its tab completing, keep the empty element that might be at the end of the buffer
// due to a space at the end of the command.
// This allows the parser to correctly identify the parameter to be completed in this case.
if (!tabComplete) {
buffer.dropTrailingEmptyElements();
}
this.originalBuffer = buffer;
this.processedBuffer = buffer.preprocessArguments(getParameterList().getArgumentPreProcessor());
this.cursorStart = buffer.getCursor();
}
/**
* Parse the parameters. If no exception is thrown, they were parsed successfully, and the command may continue post-parameter execution.
*
* @throws CommandException if an error occurs while parsing the parameters.
*/
public synchronized void parseParameters() throws CommandException {
if (attemptedToParse) {
throw new IllegalStateException();
}
attemptedToParse = true;
ContextParser parser = new ContextParser(this);
parameterValueMap = parser.getValueMap();
parsedParameters = parser.getParsedKeys();
parser.parse();
}
/**
* Attempts to parse parameters, without throwing an exception or sending any message.
* This method is typically used by tab completions.
* After calling this method, the context is ready to provide completions.
*/
public synchronized void parseParametersQuietly() {
if (attemptedToParse) {
throw new IllegalStateException();
}
attemptedToParse = true;
boolean before = muted;
muted = true;
try {
ContextParser parser = new ContextParser(this);
parameterValueMap = parser.getValueMap();
parsedParameters = parser.getParsedKeys();
parser.parse();
parameterToComplete = parser.getCompletionTarget();
parameterToCompleteCursor = parser.getCompletionCursor();
} catch (CommandException ignored) {
} finally {
muted = before;
}
}
/**
* Sender of the command
*
* @return the sender of the command
*/
public CommandSender getSender() {
return sender;
}
/**
* Command's address
*
* @return the command's address
*/
public ICommandAddress getAddress() {
return address;
}
/**
* The command
*
* @return the command
*/
public Command getCommand() {
return command;
}
/**
* The command's parameter definition.
*
* @return the parameter list
*/
public ParameterList getParameterList() {
return command.getParameterList();
}
/**
* Get the buffer as it was before preprocessing the arguments.
*
* @return the original buffer
*/
public ArgumentBuffer getOriginalBuffer() {
return originalBuffer;
}
/**
* The arguments
*
* @return the argument buffer
*/
public ArgumentBuffer getProcessedBuffer() {
return processedBuffer;
}
/**
* The cursor start, in other words, the buffer's cursor before parameters were parsed.
*
* @return the cursor start
*/
public int getCursorStart() {
return cursorStart;
}
/**
* The original arguments.
*
* @return original arguments.
*/
public String[] getOriginal() {
return originalBuffer.getArrayFromIndex(cursorStart);
}
public Formatting getFormat(EMessageType type) {
return address.getChatController().getChatFormatForType(type);
}
/**
* The full command as cached by the buffer. Might be incomplete depending on how it was dispatched.
*
* @return the full command
*/
public String getRawInput() {
return originalBuffer.getRawInput();
}
@SuppressWarnings("unchecked")
public <T> T get(String name) {
if (!parameterValueMap.containsKey(name)) {
throw new IllegalArgumentException();
}
try {
return (T) parameterValueMap.get(name);
} catch (ClassCastException ex) {
throw new IllegalArgumentException("Invalid type parameter requested for parameter " + name, ex);
}
}
@SuppressWarnings("unchecked")
public <T> T get(int index) {
return get(getParameterList().getIndexedParameterName(index));
}
public <T> T getFlag(String flag) {
return get("-" + flag);
}
/**
* Checks if the parameter by the name was provided in the command's arguments.
*
* @param name the parameter name
* @return true if it was provided
*/
public boolean isProvided(String name) {
return parsedParameters.contains(name);
}
/**
* Checks if the parameter by the index was provided in the command's arguments.
*
* @param index the parameter index
* @return true if it was provided
*/
public boolean isProvided(int index) {
return isProvided(getParameterList().getIndexedParameterName(index));
}
/**
* The parameter to complete.
* This parameter is requested suggestions
*
* @return the parameter to complete.
*/
public Parameter<?, ?> getParameterToComplete() {
return parameterToComplete;
}
/**
* @return true if this context is muted.
*/
public boolean isMuted() {
return muted;
}
/**
* @return true if this context is for a tab completion.
*/
public boolean isTabComplete() {
return tabComplete;
}
/**
* Get suggested completions.
*
* @param location The location as passed to {link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}, or null if requested in another way.
* @return completions.
*/
public List<String> getSuggestedCompletions(Location location) {
if (parameterToComplete != null) {
return parameterToComplete.complete(this, location, processedBuffer.getUnaffectingCopy().setCursor(parameterToCompleteCursor));
}
ParameterList parameterList = getParameterList();
List<String> result = new ArrayList<>();
for (String name : parameterValueMap.keySet()) {
if (parameterList.getParameterByName(name).isFlag() && !parsedParameters.contains(name)) {
result.add(name);
}
}
return result;
}
public void sendMessage(String message) {
sendMessage(true, message);
}
public void sendMessage(EMessageType messageType, String message) {
sendMessage(messageType, true, message);
}
public void sendMessage(boolean translateColours, String message) {
sendMessage(EMessageType.NEUTRAL, translateColours, message);
}
public void sendMessage(EMessageType messageType, boolean translateColours, String message) {
if (!muted) {
if (translateColours) {
message = Formatting.translateChars('&', message);
}
address.getChatController().sendMessage(this, messageType, message);
}
}
public void sendMessage(String messageFormat, Object... args) {
sendMessage(true, messageFormat, args);
}
public void sendMessage(EMessageType messageType, String messageFormat, Object... args) {
sendMessage(messageType, true, messageFormat, args);
}
public void sendMessage(boolean translateColours, String messageFormat, Object... args) {
sendMessage(EMessageType.NEUTRAL, translateColours, messageFormat, args);
}
public void sendMessage(EMessageType messageType, boolean translateColours, String messageFormat, Object... args) {
sendMessage(messageType, translateColours, String.format(messageFormat, args));
}
public void sendHelpMessage(int page) {
if (!muted) {
address.getChatController().sendHelpMessage(sender, this, address, page);
}
}
public void sendSyntaxMessage() {
if (!muted) {
address.getChatController().sendSyntaxMessage(sender, this, address);
}
}
}

View File

@@ -0,0 +1,52 @@
package io.dico.dicore.command;
import io.dico.dicore.command.parameter.IArgumentPreProcessor;
import io.dico.dicore.command.parameter.Parameter;
@SuppressWarnings("unchecked")
public abstract class ExtendedCommand<T extends ExtendedCommand<T>> extends Command {
protected final boolean modifiable;
public ExtendedCommand() {
this(true);
}
public ExtendedCommand(boolean modifiable) {
this.modifiable = modifiable;
}
protected T newModifiableInstance() {
return (T) this;
}
@Override
public T addParameter(Parameter<?, ?> parameter) {
return modifiable ? (T) super.addParameter(parameter) : newModifiableInstance().addParameter(parameter);
}
@Override
public T requiredParameters(int requiredParameters) {
return modifiable ? (T) super.requiredParameters(requiredParameters) : newModifiableInstance().requiredParameters(requiredParameters);
}
@Override
public T repeatFinalParameter() {
return modifiable ? (T) super.repeatFinalParameter() : newModifiableInstance().repeatFinalParameter();
}
@Override
public T setDescription(String... description) {
return modifiable ? (T) super.setDescription(description) : newModifiableInstance().setDescription(description);
}
@Override
public T setShortDescription(String shortDescription) {
return modifiable ? (T) super.setShortDescription(shortDescription) : newModifiableInstance().setShortDescription(shortDescription);
}
@Override
public T preprocessArguments(IArgumentPreProcessor processor) {
return modifiable ? (T) super.preprocessArguments(processor) : newModifiableInstance().preprocessArguments(processor);
}
}

View File

@@ -0,0 +1,152 @@
package io.dico.dicore.command;
import io.dico.dicore.command.chat.IChatController;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.predef.PredefinedCommand;
import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Map;
/**
* Interface for an address of a command.
* <p>
* The address holds what the name and aliases of a command are.
* The address also (optionally) holds a reference to a {@link Command}
* <p>
* One instance of {@link Command} can be held by multiple addresses,
* because the address decides what the command's name and aliases are.
* <p>
* The address holds children by key in a map. This map's keys include aliases for its children.
* This creates a tree of addresses. If a command is dispatches, the tree is traversed untill a command is found
* and no children deeper down match the command (there are exceptions to the later as defined by
* {@link Command#takePrecedenceOverSubcommand(String, ArgumentBuffer)}
* and {@link Command#isVisibleTo(CommandSender)}
*/
public interface ICommandAddress {
/**
* @return true if this address has a parent.
*/
boolean hasParent();
/**
* Get the parent of this address
*
* @return the parent of this address, or null if none exists.
*/
ICommandAddress getParent();
/**
* @return true if this address has a command.
*/
boolean hasCommand();
/**
* @return true if this address has a command that is not an instance of {@link PredefinedCommand}
*/
boolean hasUserDeclaredCommand();
/**
* @return Get the command of this address, or null if none exists.
*/
Command getCommand();
/**
* @return true if this address is an instance of {@link RootCommandAddress}
*/
boolean isRoot();
/**
* @return the root address of the tree which this address resides in.
*/
ICommandAddress getRoot();
/**
* A list of the names of this address, at the current level.
* The first entry is the main key, the subsequent ones are aliases.
* <p>
* Untill an address is assigned a parent, this list is mutable.
* <p>
* If {@link #isRoot()}, this returns an immutable, empty list.
*
* @return the list of names.
*/
List<String> getNames();
/**
* A list of the aliases of this address. That is, {@link #getNames()}
* without the first entry.
*
* @return a list of aliases
*/
List<String> getAliases();
/**
* @return The first element of {@link #getNames()}
*/
String getMainKey();
/**
* Get the address of this command.
* That is, the main keys of all commands leading up to this address, and this address itself, separated by a space.
* In other words, the command without the / that is required to target the command at this address.
*
* @return the address of this command.
*/
String getAddress();
/**
* Get the amount of addresses that separate this address from the root of the tree, + 1.
* The root of the tree has a depth of 0. Each subsequent child has its depth incremented by 1.
*
* @return The depth of this address
*/
int getDepth();
/**
* @return true if the depth of this address is larger than the argument.
*/
boolean isDepthLargerThan(int depth);
/**
* Get an unmodifiable view of the children of this address.
* Values might be duplicated for aliases.
*
* @return the children of this address.
*/
Map<String, ? extends ICommandAddress> getChildren();
/**
* Query for a child at the given key.
*
* @param key the key. The name or alias of a command.
* @return the child, or null if it's not found
*/
ICommandAddress getChild(String key);
/**
* Get the command dispatcher for this tree
*
* @return the command dispatcher
*/
ICommandDispatcher getDispatcherForTree();
/**
* @return The desired chatcontroller for use by commands at this address and any sub-addresses, if they define no explicit chat controller.
*/
IChatController getChatController();
static ICommandAddress newChild() {
return new ChildCommandAddress();
}
static ICommandAddress newChild(Command command) {
return new ChildCommandAddress(command);
}
static ICommandAddress newRoot() {
return new RootCommandAddress();
}
}

View File

@@ -0,0 +1,131 @@
package io.dico.dicore.command;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.registration.CommandMap;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Map;
public interface ICommandDispatcher {
/**
* Get a potentially indirect child of the root of this dispatcher
*
* @param buffer the argument buffer with the subsequent keys to traverse. Any keys beyond the first that isn't found are ignored.
* @return the child, or this same instance of no child is found.
*/
ICommandAddress getDeepChild(ArgumentBuffer buffer);
/**
* Similar to {@link #getDeepChild(ArgumentBuffer)},
* but this method incorporates checks on the command of traversed children:
* {@link Command#isVisibleTo(CommandSender)}
* and {@link Command#takePrecedenceOverSubcommand(String, ArgumentBuffer)}
* <p>
* The target of a command is never null, however, the same instance might be returned, and the returned address might not hold a command.
*
* @param sender the sender of the command
* @param buffer the command itself as a buffer.
* @return the address that is the target of the command.
*/
ICommandAddress getCommandTarget(CommandSender sender, ArgumentBuffer buffer);
/**
* dispatch the command
*
* @param sender the sender
* @param command the command
* @return true if a command has executed
*/
boolean dispatchCommand(CommandSender sender, String[] command);
/**
* dispatch the command
*
* @param sender the sender
* @param usedLabel the label (word after the /)
* @param args the arguments
* @return true if a command has executed
*/
boolean dispatchCommand(CommandSender sender, String usedLabel, String[] args);
/**
* dispatch the command
*
* @param sender the sender
* @param buffer the command
* @return true if a command has executed
*/
boolean dispatchCommand(CommandSender sender, ArgumentBuffer buffer);
/**
* suggest tab completions
*
* @param sender the sender as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param location the location as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param args the arguments as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* args must be sanitized such that it contains no empty elements, particularly at the last index.
* @return tab completions
*/
List<String> getTabCompletions(CommandSender sender, Location location, String[] args);
/**
* suggest tab completions
*
* @param sender the sender as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param usedLabel the label as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param location the location as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param args the arguments as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @return tab completions
*/
List<String> getTabCompletions(CommandSender sender, String usedLabel, Location location, String[] args);
/**
* suggest tab completions
*
* @param sender the sender as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param location the location as passed to {@link org.bukkit.command.Command#tabComplete(CommandSender, String, String[], Location)}
* @param buffer the arguments as a buffer
* @return tab completions
*/
List<String> getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer);
/**
* Register this dispatcher's commands to the command map
*
* @throws UnsupportedOperationException if this dispatcher is not the root of the tree
*/
default void registerToCommandMap() {
registerToCommandMap(null, CommandMap.getCommandMap(), EOverridePolicy.OVERRIDE_ALL);
}
/**
* Register this dispatcher's commands to the command map
*
* @param fallbackPrefix the fallback prefix to use, null if none
* @param overridePolicy the override policy
* @throws UnsupportedOperationException if this dispatcher is not the root of the tree
*/
default void registerToCommandMap(String fallbackPrefix, EOverridePolicy overridePolicy) {
registerToCommandMap(fallbackPrefix, CommandMap.getCommandMap(), overridePolicy);
}
/**
* Register this dispatcher's commands to the command map
*
* @param fallbackPrefix the fallback prefix to use, null if none
* @param map the command map
* @param overridePolicy the override policy
* @throws UnsupportedOperationException if this dispatcher is not the root of the tree
*/
void registerToCommandMap(String fallbackPrefix, Map<String, org.bukkit.command.Command> map, EOverridePolicy overridePolicy);
default void unregisterFromCommandMap() {
unregisterFromCommandMap(CommandMap.getCommandMap());
}
void unregisterFromCommandMap(Map<String, org.bukkit.command.Command> map);
}

View File

@@ -0,0 +1,322 @@
package io.dico.dicore.command;
import io.dico.dicore.exceptions.checkedfunctions.CheckedConsumer;
import io.dico.dicore.exceptions.checkedfunctions.CheckedRunnable;
import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Objects;
public interface IContextFilter extends Comparable<IContextFilter> {
/**
* Filter the given context by this filter's criteria.
* If the context does not match the criteria, an exception is thrown describing the problem.
*
* @param context the context to match
* @throws CommandException if it doesn't match
*/
void filterContext(ExecutionContext context) throws CommandException;
/**
* Filter an execution context for a direct or indirect sub command of the command that registered this filter.
*
* @param subContext the context for the execution
* @param path the path traversed from the command that registered this filter to the executed command
*/
default void filterSubContext(ExecutionContext subContext, String... path) throws CommandException {
filterContext(subContext);
}
/**
* Get the priority of this context filter.
* The priorities determine the order in which a command's context filters are executed.
*
* @return the priority
*/
Priority getPriority();
default boolean allowsContext(ExecutionContext context) {
try {
filterContext(context);
return true;
} catch (CommandException ex) {
return false;
}
}
/**
* Used to sort filters in execution order. That is, filters are ordered by {@link #getPriority()}
*
* @param o compared filter
* @return comparison value
*/
@Override
default int compareTo(IContextFilter o) {
return getPriority().compareTo(o.getPriority());
}
default boolean isInheritable() {
return false;
}
default IContextFilter inherit(String... components) {
if (!isInheritable()) {
throw new IllegalStateException("This IContextFilter cannot be inherited");
}
return this;
}
/**
* IContextFilter priorities. Executes from top to bottom.
*/
enum Priority {
/**
* This priority should have checks on the sender type.
* Any filters on this priority are tested before permissions are.
* This is the highest priority.
*/
VERY_EARLY, // sender type check
/**
* This priority is specific to permissions.
*/
PERMISSION,
/**
* Early priority. Post permissions, pre parameter-parsing.
*/
EARLY,
/**
* Normal priority. Post permissions, pre parameter-parsing.
*/
NORMAL,
/**
* Late priority. Post permissions, pre parameter-parsing.
*/
LATE,
/**
* Very late priority. Post permissions, pre parameter-parsing.
*/
VERY_LATE,
/**
* Post parameters priority. Post permissions, post parameter-parsing.
* This is the lowest priority.
*/
POST_PARAMETERS;
/**
* Get the context filter that inherits context filters from the parent of the same priority.
* If this filter is also present at the parent, it will do the same for the parent's parent, and so on.
*
* @return the inheritor
*/
public IContextFilter getInheritor() {
return inheritor;
}
private static String[] addParent(String[] path, String parent) {
String[] out = new String[path.length + 1];
System.arraycopy(path, 0, out, 0, path.length);
out[0] = parent;
return out;
}
final IContextFilter inheritor = new IContextFilter() {
@Override
public void filterContext(ExecutionContext context) throws CommandException {
ICommandAddress address = context.getAddress();
String[] traversedPath = new String[0];
do {
traversedPath = addParent(traversedPath, address.getMainKey());
address = address.getParent();
if (address != null && address.hasCommand()) {
boolean doBreak = true;
Command command = address.getCommand();
List<IContextFilter> contextFilterList = command.getContextFilters();
for (IContextFilter filter : contextFilterList) {
if (filter.getPriority() == Priority.this) {
if (filter == this) {
// do the same for next parent
// this method is necessary to keep traversedPath information
doBreak = false;
} else {
filter.filterSubContext(context, traversedPath);
}
}
}
if (doBreak) {
break;
}
}
} while (address != null);
}
@Override
public Priority getPriority() {
return Priority.this;
}
};
}
/**
* Ensures that only {@link org.bukkit.entity.Player} type senders can execute the command.
*/
IContextFilter PLAYER_ONLY = filterSender(Priority.VERY_EARLY, Validate::isPlayer);
/**
* Ensures that only {@link org.bukkit.command.ConsoleCommandSender} type senders can execute the command.
*/
IContextFilter CONSOLE_ONLY = filterSender(Priority.VERY_EARLY, Validate::isConsole);
/**
* This filter is not working as intended.
* <p>
* There is supposed to be a permission filter that takes a base, and appends the command's address to the base, and checks that permission.
*/
IContextFilter INHERIT_PERMISSIONS = Priority.PERMISSION.getInheritor();
static IContextFilter fromCheckedRunnable(Priority priority, CheckedRunnable<? extends CommandException> runnable) {
return new IContextFilter() {
@Override
public void filterContext(ExecutionContext context) throws CommandException {
runnable.checkedRun();
}
@Override
public Priority getPriority() {
return priority;
}
};
}
static IContextFilter filterSender(Priority priority, CheckedConsumer<? super CommandSender, ? extends CommandException> consumer) {
return new IContextFilter() {
@Override
public void filterContext(ExecutionContext context) throws CommandException {
consumer.checkedAccept(context.getSender());
}
@Override
public Priority getPriority() {
return priority;
}
};
}
static IContextFilter permission(String permission) {
Objects.requireNonNull(permission);
return filterSender(Priority.PERMISSION, sender -> Validate.isAuthorized(sender, permission));
}
static IContextFilter permission(String permission, String failMessage) {
Objects.requireNonNull(permission);
Objects.requireNonNull(failMessage);
return filterSender(Priority.PERMISSION, sender -> Validate.isAuthorized(sender, permission, failMessage));
}
/**
* Produce an inheritable permission context filter.
* A permission component is an element in {@code permission.split("\\.")}
*
* @param permission The permission that is required for the command that this is directly assigned to
* @param componentInsertionIndex the index where any sub-components are inserted. -1 for "at the end".
* @param failMessage the message to send if the permission is not met
* @return the context filter
* @throws IllegalArgumentException if componentInsertionIndex is out of range
*/
static IContextFilter inheritablePermission(String permission, int componentInsertionIndex, String failMessage) {
Objects.requireNonNull(permission);
Objects.requireNonNull(failMessage);
if (componentInsertionIndex > permission.split("\\.").length || componentInsertionIndex < -1) {
throw new IllegalArgumentException("componentInsertionIndex out of range");
}
return new IContextFilter() {
private String getInheritedPermission(String[] components) {
int insertedAmount = components.length;
String[] currentComponents = permission.split("\\.");
int currentAmount = currentComponents.length;
String[] targetArray = new String[currentAmount + insertedAmount];
int insertionIndex;
//int newInsertionIndex;
if (componentInsertionIndex == -1) {
insertionIndex = currentAmount;
//newInsertionIndex = -1;
} else {
insertionIndex = componentInsertionIndex;
//newInsertionIndex = insertionIndex + insertedAmount;
}
// copy the current components up to insertionIndex
System.arraycopy(currentComponents, 0, targetArray, 0, insertionIndex);
// copy the new components into the array at insertionIndex
System.arraycopy(components, 0, targetArray, insertionIndex, insertedAmount);
// copy the current components from insertionIndex + inserted amount
System.arraycopy(currentComponents, insertionIndex, targetArray, insertionIndex + insertedAmount, currentAmount - insertionIndex);
return String.join(".", targetArray);
}
@Override
public void filterContext(ExecutionContext context) throws CommandException {
Validate.isAuthorized(context.getSender(), permission, failMessage);
}
@Override
public void filterSubContext(ExecutionContext subContext, String... path) throws CommandException {
Validate.isAuthorized(subContext.getSender(), getInheritedPermission(path), failMessage);
}
@Override
public Priority getPriority() {
return Priority.PERMISSION;
}
@Override
public boolean isInheritable() {
return true;
}
@Override
public IContextFilter inherit(String... components) {
int insertedAmount = components.length;
String[] currentComponents = permission.split("\\.");
int currentAmount = currentComponents.length;
String[] targetArray = new String[currentAmount + insertedAmount];
int insertionIndex;
int newInsertionIndex;
if (componentInsertionIndex == -1) {
insertionIndex = currentAmount;
newInsertionIndex = -1;
} else {
insertionIndex = componentInsertionIndex;
newInsertionIndex = insertionIndex + insertedAmount;
}
// copy the current components up to insertionIndex
System.arraycopy(currentComponents, 0, targetArray, 0, insertionIndex);
// copy the new components into the array at insertionIndex
System.arraycopy(components, 0, targetArray, insertionIndex, insertedAmount);
// copy the current components from insertionIndex + inserted amount
System.arraycopy(currentComponents, insertionIndex, targetArray, insertionIndex + insertedAmount, currentAmount - insertionIndex);
return inheritablePermission(String.join(".", targetArray), newInsertionIndex, failMessage);
}
};
}
}

View File

@@ -0,0 +1,35 @@
package io.dico.dicore.command;
import io.dico.dicore.exceptions.checkedfunctions.CheckedBiFunction;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
public class LambdaCommand extends ExtendedCommand<LambdaCommand> {
private CheckedBiFunction<CommandSender, ExecutionContext, String, CommandException> executor;
private BiFunction<CommandSender, ExecutionContext, List<String>> completer;
public LambdaCommand executor(CheckedBiFunction<CommandSender, ExecutionContext, String, CommandException> executor) {
this.executor = Objects.requireNonNull(executor);
return this;
}
public LambdaCommand completer(BiFunction<CommandSender, ExecutionContext, List<String>> completer) {
this.completer = Objects.requireNonNull(completer);
return this;
}
@Override
public String execute(CommandSender sender, ExecutionContext context) throws CommandException {
return executor.checkedApply(sender, context);
}
@Override
public List<String> tabComplete(CommandSender sender, ExecutionContext context, Location location) {
return completer == null ? super.tabComplete(sender, context, location) : completer.apply(sender, context);
}
}

View File

@@ -0,0 +1,258 @@
package io.dico.dicore.command;
import io.dico.dicore.command.chat.ChatControllers;
import io.dico.dicore.command.chat.IChatController;
import io.dico.dicore.command.predef.HelpCommand;
import io.dico.dicore.command.predef.PredefinedCommand;
import java.util.*;
public abstract class ModifiableCommandAddress implements ICommandAddress {
Map<String, ChildCommandAddress> children;
// the chat controller as configured by the programmer
IChatController chatController;
// cache for the algorithm that finds the first chat controller going up the tree
transient IChatController chatControllerCache;
ModifiableCommandAddress helpChild;
public ModifiableCommandAddress() {
this.children = new LinkedHashMap<>(4);
}
@Override
public boolean hasParent() {
return getParent() != null;
}
@Override
public boolean hasCommand() {
return getCommand() != null;
}
@Override
public boolean hasUserDeclaredCommand() {
Command command = getCommand();
return command != null && !(command instanceof PredefinedCommand);
}
@Override
public Command getCommand() {
return null;
}
@Override
public boolean isRoot() {
return false;
}
@Override
public List<String> getNames() {
return null;
}
@Override
public List<String> getAliases() {
List<String> names = getNames();
if (names == null) {
return null;
}
if (names.isEmpty()) {
return Collections.emptyList();
}
return names.subList(1, names.size());
}
@Override
public String getMainKey() {
return null;
}
public void setCommand(Command command) {
throw new UnsupportedOperationException();
}
@Override
public abstract ModifiableCommandAddress getParent();
@Override
public RootCommandAddress getRoot() {
ModifiableCommandAddress out = this;
while (out.hasParent()) {
out = out.getParent();
}
return out.isRoot() ? (RootCommandAddress) out : null;
}
@Override
public int getDepth() {
int depth = 0;
ICommandAddress address = this;
while (address.hasParent()) {
address = address.getParent();
depth++;
}
return depth;
}
@Override
public boolean isDepthLargerThan(int value) {
int depth = 0;
ICommandAddress address = this;
do {
if (depth > value) {
return true;
}
address = address.getParent();
depth++;
} while (address != null);
return false;
}
@Override
public Map<String, ? extends ModifiableCommandAddress> getChildren() {
return Collections.unmodifiableMap(children);
}
@Override
public ChildCommandAddress getChild(String key) {
return children.get(key);
}
public void addChild(ICommandAddress child) {
if (!(child instanceof ChildCommandAddress)) {
throw new IllegalArgumentException("Argument must be a ChildCommandAddress");
}
ChildCommandAddress mChild = (ChildCommandAddress) child;
if (mChild.parent != null) {
throw new IllegalArgumentException("Argument already has a parent");
}
if (mChild.names.isEmpty()) {
throw new IllegalArgumentException("Argument must have names");
}
Iterator<String> names = mChild.modifiableNamesIterator();
children.put(names.next(), mChild);
while (names.hasNext()) {
String name = names.next();
if (children.putIfAbsent(name, mChild) != null) {
names.remove();
}
}
mChild.setParent(this);
if (mChild.hasCommand() && mChild.getCommand() instanceof HelpCommand) {
helpChild = mChild;
}
}
public void removeChildren(boolean removeAliases, String... keys) {
if (keys.length == 0) {
throw new IllegalArgumentException("keys is empty");
}
for (String key : keys) {
ChildCommandAddress keyTarget = getChild(key);
if (keyTarget == null) {
continue;
}
if (removeAliases) {
for (Iterator<String> iterator = keyTarget.namesModifiable.iterator(); iterator.hasNext(); ) {
String alias = iterator.next();
ChildCommandAddress aliasTarget = getChild(key);
if (aliasTarget == keyTarget) {
children.remove(alias);
iterator.remove();
}
}
continue;
}
children.remove(key);
keyTarget.namesModifiable.remove(key);
}
}
public boolean hasHelpCommand() {
return helpChild != null;
}
public ModifiableCommandAddress getHelpCommand() {
return helpChild;
}
@Override
public IChatController getChatController() {
if (chatControllerCache == null) {
if (chatController != null) {
chatControllerCache = chatController;
} else if (!hasParent()) {
chatControllerCache = ChatControllers.defaultChat();
} else {
chatControllerCache = getParent().getChatController();
}
}
return chatControllerCache;
}
public void setChatController(IChatController chatController) {
this.chatController = chatController;
resetChatControllerCache(new HashSet<>());
}
void resetChatControllerCache(Set<ModifiableCommandAddress> dejaVu) {
if (dejaVu.add(this)) {
chatControllerCache = chatController;
for (ChildCommandAddress address : children.values()) {
if (address.chatController == null) {
address.resetChatControllerCache(dejaVu);
}
}
}
}
@Override
public ICommandDispatcher getDispatcherForTree() {
return getRoot();
}
void appendDebugInformation(StringBuilder target, String linePrefix, Set<ICommandAddress> seen) {
target.append('\n').append(linePrefix);
if (!seen.add(this)) {
target.append("<duplicate of address '").append(getAddress()).append("'>");
return;
}
if (this instanceof ChildCommandAddress) {
List<String> namesModifiable = ((ChildCommandAddress) this).namesModifiable;
if (namesModifiable.isEmpty()) {
target.append("<no key>");
} else {
Iterator<String> keys = namesModifiable.iterator();
target.append(keys.next()).append(' ');
if (keys.hasNext()) {
target.append('(').append(keys.next());
while (keys.hasNext()) {
target.append(" ,").append(keys.next());
}
target.append(") ");
}
}
} else {
target.append("<root> ");
}
String commandClass = hasCommand() ? getCommand().getClass().getCanonicalName() : "<no command>";
target.append(commandClass);
for (ChildCommandAddress child : new HashSet<>(children.values())) {
child.appendDebugInformation(target, linePrefix + " ", seen);
}
}
}

View File

@@ -0,0 +1,218 @@
package io.dico.dicore.command;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.registration.BukkitCommand;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.*;
public class RootCommandAddress extends ModifiableCommandAddress implements ICommandDispatcher {
@Deprecated
public static final RootCommandAddress INSTANCE = new RootCommandAddress();
public RootCommandAddress() {
}
@Override
public Command getCommand() {
return null;
}
@Override
public boolean isRoot() {
return true;
}
@Override
public List<String> getNames() {
return Collections.emptyList();
}
@Override
public ModifiableCommandAddress getParent() {
return null;
}
@Override
public String getMainKey() {
return null;
}
@Override
public String getAddress() {
return "";
}
@Override
public void registerToCommandMap(String fallbackPrefix, Map<String, org.bukkit.command.Command> map, EOverridePolicy overridePolicy) {
Objects.requireNonNull(overridePolicy);
//debugChildren(this);
Map<String, ChildCommandAddress> children = this.children;
Map<ChildCommandAddress, BukkitCommand> wrappers = new IdentityHashMap<>();
for (ChildCommandAddress address : children.values()) {
if (!wrappers.containsKey(address)) {
wrappers.put(address, new BukkitCommand(address));
}
}
for (Map.Entry<String, ChildCommandAddress> entry : children.entrySet()) {
String key = entry.getKey();
ChildCommandAddress address = entry.getValue();
boolean override = overridePolicy == EOverridePolicy.OVERRIDE_ALL;
if (!override && key.equals(address.getMainKey())) {
override = overridePolicy == EOverridePolicy.MAIN_KEY_ONLY || overridePolicy == EOverridePolicy.MAIN_AND_FALLBACK;
}
registerMember(map, key, wrappers.get(address), override);
if (fallbackPrefix != null) {
key = fallbackPrefix + key;
override = overridePolicy != EOverridePolicy.OVERRIDE_NONE && overridePolicy != EOverridePolicy.MAIN_KEY_ONLY;
registerMember(map, key, wrappers.get(address), override);
}
}
}
private static void debugChildren(ModifiableCommandAddress address) {
for (ModifiableCommandAddress child : new HashSet<ModifiableCommandAddress>(address.getChildren().values())) {
System.out.println(child.getAddress());
debugChildren(child);
}
}
private static void registerMember(Map<String, org.bukkit.command.Command> map, String key, org.bukkit.command.Command value, boolean override) {
if (override) {
map.put(key, value);
} else {
map.putIfAbsent(key, value);
}
}
@Override
public void unregisterFromCommandMap(Map<String, org.bukkit.command.Command> map) {
Set<ICommandAddress> children = new HashSet<>(this.children.values());
Iterator<Map.Entry<String, org.bukkit.command.Command>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, org.bukkit.command.Command> entry = iterator.next();
org.bukkit.command.Command cmd = entry.getValue();
if (cmd instanceof BukkitCommand && children.contains(((BukkitCommand) cmd).getOrigin())) {
iterator.remove();
}
}
}
@Override
public ModifiableCommandAddress getDeepChild(ArgumentBuffer buffer) {
ModifiableCommandAddress cur = this;
ChildCommandAddress child;
while (buffer.hasNext()) {
child = cur.getChild(buffer.next());
if (child == null) {
buffer.rewind();
return cur;
}
cur = child;
}
return cur;
}
@Override
public ModifiableCommandAddress getCommandTarget(CommandSender sender, ArgumentBuffer buffer) {
//System.out.println("Buffer cursor upon getCommandTarget: " + buffer.getCursor());
ModifiableCommandAddress cur = this;
ChildCommandAddress child;
while (buffer.hasNext()) {
child = cur.getChild(buffer.next());
if (child == null
|| (child.hasCommand() && !child.getCommand().isVisibleTo(sender))
|| (cur.hasCommand() && cur.getCommand().takePrecedenceOverSubcommand(buffer.peekPrevious(), buffer.getUnaffectingCopy()))) {
buffer.rewind();
break;
}
cur = child;
}
/*
if (!cur.hasCommand() && cur.hasHelpCommand()) {
cur = cur.getHelpCommand();
} else {
while (!cur.hasCommand() && cur.hasParent()) {
cur = cur.getParent();
buffer.rewind();
}
}
*/
return cur;
}
@Override
public boolean dispatchCommand(CommandSender sender, String[] command) {
return dispatchCommand(sender, new ArgumentBuffer(command));
}
@Override
public boolean dispatchCommand(CommandSender sender, String usedLabel, String[] args) {
return dispatchCommand(sender, new ArgumentBuffer(usedLabel, args));
}
@Override
public boolean dispatchCommand(CommandSender sender, ArgumentBuffer buffer) {
ModifiableCommandAddress targetAddress = getCommandTarget(sender, buffer);
Command target = targetAddress.getCommand();
if (target == null) {
if (targetAddress.hasHelpCommand()) {
target = targetAddress.getHelpCommand().getCommand();
} else {
return false;
}
}
target.execute(sender, targetAddress, buffer);
return true;
}
@Override
public List<String> getTabCompletions(CommandSender sender, Location location, String[] args) {
return getTabCompletions(sender, location, new ArgumentBuffer(args));
}
@Override
public List<String> getTabCompletions(CommandSender sender, String usedLabel, Location location, String[] args) {
return getTabCompletions(sender, location, new ArgumentBuffer(usedLabel, args));
}
@Override
public List<String> getTabCompletions(CommandSender sender, Location location, ArgumentBuffer buffer) {
ICommandAddress target = getCommandTarget(sender, buffer);
List<String> out = target.hasCommand() ? target.getCommand().tabComplete(sender, target, location, buffer.getUnaffectingCopy()) : Collections.emptyList();
int cursor = buffer.getCursor();
String input;
if (cursor >= buffer.size()) {
input = "";
} else {
input = buffer.get(cursor).toLowerCase();
}
boolean wrapped = false;
for (String child : target.getChildren().keySet()) {
if (child.toLowerCase().startsWith(input)) {
if (!wrapped) {
out = new ArrayList<>(out);
wrapped = true;
}
out.add(child);
}
}
return out;
}
}

View File

@@ -0,0 +1,52 @@
package io.dico.dicore.command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import java.util.Optional;
public class Validate {
private Validate() {
}
//@Contract("false, _ -> fail")
public static void isTrue(boolean expression, String failMessage) throws CommandException {
if (!expression) {
throw new CommandException(failMessage);
}
}
//@Contract("null, _ -> fail")
public static void notNull(Object obj, String failMessage) throws CommandException {
Validate.isTrue(obj != null, failMessage);
}
public static void isAuthorized(CommandSender sender, String permission, String failMessage) throws CommandException {
Validate.isTrue(sender.hasPermission(permission), failMessage);
}
public static void isAuthorized(CommandSender sender, String permission) throws CommandException {
Validate.isAuthorized(sender, permission, "You do not have permission to use that command");
}
//@Contract("null -> fail")
public static void isPlayer(CommandSender sender) throws CommandException {
isTrue(sender instanceof Player, "That command can only be used by players");
}
//@Contract("null -> fail")
public static void isConsole(CommandSender sender) throws CommandException {
isTrue(sender instanceof ConsoleCommandSender, "That command can only be used by the console");
}
public static <T> T returnIfPresent(Optional<T> maybe, String failMessage) throws CommandException {
if (!maybe.isPresent()) {
throw new CommandException(failMessage);
}
return maybe.get();
}
}

View File

@@ -0,0 +1,52 @@
package io.dico.dicore.command.annotation;
import io.dico.dicore.command.parameter.type.ParameterConfig;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface BigRange {
Class<?> MEMORY_CLASS = Memory.class;
ParameterConfig<BigRange, Memory> CONFIG = ParameterConfig.getMemoryClassFromField(BigRange.class);
Memory DEFAULT = new Memory("MIN", "MAX", "0");
String min() default "MIN";
String max() default "MAX";
String defaultValue() default "0";
class Memory {
private final String min;
private final String max;
private final String defaultValue;
public Memory(BigRange range) {
this(range.min(), range.max(), range.defaultValue());
}
public Memory(String min, String max, String defaultValue) {
this.min = min;
this.max = max;
this.defaultValue = defaultValue;
}
public String min() {
return min;
}
public String max() {
return max;
}
public String defaultValue() {
return defaultValue;
}
}
}

View File

@@ -0,0 +1,16 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cmd {
String value();
String[] aliases() default {};
}

View File

@@ -0,0 +1,27 @@
package io.dico.dicore.command.annotation;
import io.dico.dicore.command.parameter.type.IParameterTypeSelector;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark methods that register a parameter type to the localized selector for use in reflective commands.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CmdParamType {
/**
* If this flag is set, the type is registered without its annotation type.
* As a result, the {@link IParameterTypeSelector} is more likely to select it (faster).
* This is irrelevant if there is no annotation type or param config.
*
* @return true if this parameter type should be registered without its annotation type too
*/
boolean infolessAlias() default false;
}

View File

@@ -0,0 +1,35 @@
package io.dico.dicore.command.annotation;
public class CommandAnnotationUtils {
/**
* Get the short description from a {@link Desc} annotation.
* If {@link Desc#shortVersion()} is given, returns that.
* Otherwise, returns the first element of {@link Desc#value()}
* If neither is available, returns null.
*
* @param desc the annotation
* @return the short description
*/
public static String getShortDescription(Desc desc) {
String descString;
if (desc == null) {
descString = null;
} else if (!desc.shortVersion().isEmpty()) {
descString = desc.shortVersion();
} else if (desc.value().length > 0) {
descString = desc.value()[0];
if (desc.value().length > 1) {
//System.out.println("[Command Warning] Multiline descriptions not supported here. Keep it short for: " + targetIdentifier);
}
if (descString != null && descString.isEmpty()) {
descString = null;
}
} else {
descString = null;
}
return descString;
}
}

View File

@@ -0,0 +1,27 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Desc {
/**
* Multiline description if {@link #shortVersion} is set.
* Otherwise, this should be an array with one element (aka, you don't have to add array brackets).
*
* @return the multiline description.
* @see CommandAnnotationUtils#getShortDescription(Desc)
*/
String[] value();
/**
* Short description, use if {@link #value} is multi-line.
* To get a short description from a {@link Desc}, you should use {@link CommandAnnotationUtils#getShortDescription(Desc)}
*
* @return short description
* @see CommandAnnotationUtils#getShortDescription(Desc)
*/
String shortVersion() default "";
}

View File

@@ -0,0 +1,16 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Flag {
String value() default "";
String permission() default "";
}

View File

@@ -0,0 +1,14 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GenerateCommands {
String[] value() default {"help"};
}

View File

@@ -0,0 +1,68 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to define sub-groups of the group registered reflectively from all methods in a class.
* <p>
* Commands are selected for grouping by matching their method's names to a regular expression.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface GroupMatchedCommands {
@Retention(RetentionPolicy.RUNTIME)
@interface GroupEntry {
/**
* Regular expression to match method names for this group
* Must be non-empty
*
* @return the regular expression
*/
String regex();
/**
* The name or main key of the sub-group or address
* Must be non-empty
*
* @return the group name
*/
String group();
/**
* The aliases for the sub-group
*
* @return the group aliases
*/
String[] groupAliases() default {};
/**
* Generated (predefined) commands for the sub-group
*/
String[] generatedCommands() default {};
/**
* @see Desc
*/
String[] description() default {};
/**
* @see Desc
*/
String shortDescription() default "";
}
/**
* The defined groups.
* If a method name matches the regex of multiple groups,
* groups are prioritized by the order in which they appear in this array.
*
* @return the defined groups
*/
GroupEntry[] value();
}

View File

@@ -0,0 +1,14 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface NamedArg {
String value();
}

View File

@@ -0,0 +1,16 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PreprocessArgs {
String tokens() default "\"\"";
char escapeChar() default '\\';
}

View File

@@ -0,0 +1,67 @@
package io.dico.dicore.command.annotation;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.Validate;
import io.dico.dicore.command.parameter.type.ParameterConfig;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Range {
Class<?> MEMORY_CLASS = Memory.class;
ParameterConfig<Range, Memory> CONFIG = ParameterConfig.getMemoryClassFromField(Range.class);
Memory DEFAULT = new Memory(-Double.MAX_VALUE, Double.MAX_VALUE, 0);
double min() default -Double.MAX_VALUE;
double max() default Double.MAX_VALUE;
double defaultValue() default 0;
class Memory {
private final double min;
private final double max;
private final double defaultValue;
public Memory(Range range) {
this(range.min(), range.max(), range.defaultValue());
}
public Memory(double min, double max, double defaultValue) {
this.min = min;
this.max = max;
this.defaultValue = defaultValue;
}
public double min() {
return min;
}
public double max() {
return max;
}
public double defaultValue() {
return defaultValue;
}
public void validate(Number x, String failMessage) throws CommandException {
Validate.isTrue(valid(x), failMessage);
}
public boolean valid(Number x) {
double d = x.doubleValue();
return min <= d && d <= max;
}
public boolean isDefault() {
return this == DEFAULT || (min == DEFAULT.min && max == DEFAULT.max && defaultValue == DEFAULT.defaultValue);
}
}
}

View File

@@ -0,0 +1,11 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequireConsole {
}

View File

@@ -0,0 +1,14 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequireParameters {
int value();
}

View File

@@ -0,0 +1,30 @@
package io.dico.dicore.command.annotation;
import io.dico.dicore.command.IContextFilter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequirePermissions {
/**
* Any permissions that must be present on the sender
*
* @return an array of permission nodes
*/
String[] value();
/**
* Whether permissions should (also) be inherited from the parent.
* This uses {@link IContextFilter#INHERIT_PERMISSIONS}
* This is true by default.
*
* @return true if permissions should be inherited.
*/
boolean inherit() default true;
}

View File

@@ -0,0 +1,11 @@
package io.dico.dicore.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequirePlayer {
}

View File

@@ -0,0 +1,86 @@
package io.dico.dicore.command.chat;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import org.bukkit.command.CommandSender;
public class AbstractChatController implements IChatController {
@Override
public void sendMessage(ExecutionContext context, EMessageType type, String message) {
sendMessage(context.getSender(), type, message);
}
@Override
public void sendMessage(CommandSender sender, EMessageType type, String message) {
if (message != null && !message.isEmpty()) {
sender.sendMessage(getMessagePrefixForType(type) + getChatFormatForType(type) + message);
}
}
@Override
public void handleCommandException(CommandSender sender, ExecutionContext context, CommandException exception) {
sendMessage(sender, EMessageType.EXCEPTION, exception.getMessage());
}
@Override
public void handleException(CommandSender sender, ExecutionContext context, Throwable exception) {
if (exception instanceof CommandException) {
handleCommandException(sender, context, (CommandException) exception);
} else {
sendMessage(sender, EMessageType.EXCEPTION, "An internal error occurred whilst executing this command");
exception.printStackTrace();
}
}
@Override
public void sendHelpMessage(CommandSender sender, ExecutionContext context, ICommandAddress address, int page) {
sendMessage(sender, EMessageType.INSTRUCTION, HelpCache.getHelpCache(address).getHelpPage(page));
}
@Override
public void sendSyntaxMessage(CommandSender sender, ExecutionContext context, ICommandAddress address) {
sendMessage(sender, EMessageType.INSTRUCTION, HelpCache.getHelpCache(address).getSyntax());
}
@Override
public Formatting getChatFormatForType(EMessageType type) {
switch (type) {
case EXCEPTION:
case BAD_NEWS:
return Formatting.RED;
case INSTRUCTION:
case NEUTRAL:
return Formatting.GRAY;
case CUSTOM:
return Formatting.WHITE;
case INFORMATIVE:
return Formatting.AQUA;
case RESULT:
default:
case GOOD_NEWS:
return Formatting.GREEN;
case WARNING:
return Formatting.YELLOW;
case DESCRIPTION:
return Formatting.GREEN;
case SYNTAX:
return Formatting.BLUE;
case HIGHLIGHT:
return Formatting.RED;
case SUBCOMMAND:
return Formatting.GRAY;
case NUMBER:
return Formatting.YELLOW;
}
}
@Override
public String getMessagePrefixForType(EMessageType type) {
return "";
}
}

View File

@@ -0,0 +1,52 @@
package io.dico.dicore.command.chat;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.help.IHelpComponent;
import io.dico.dicore.command.chat.help.IHelpTopic;
import io.dico.dicore.command.chat.help.IPageBuilder;
import io.dico.dicore.command.chat.help.IPageLayout;
import io.dico.dicore.command.chat.help.defaults.*;
import org.bukkit.command.CommandSender;
import java.util.Arrays;
import java.util.List;
/**
* Static factory methods for {@link IChatController}
*/
public class ChatControllers {
private static final IChatController defaultChat;
private ChatControllers() {
}
public static IChatController defaultChat() {
return defaultChat;
}
static {
defaultChat = new AbstractChatController() {
IPageBuilder pageBuilder = new DefaultPageBuilder();
IPageLayout pageLayout = new DefaultPageLayout();
List<IHelpTopic> topics = Arrays.asList(new DescriptionHelpTopic(), new SyntaxHelpTopic(), new SubcommandsHelpTopic());
@Override
public void sendHelpMessage(CommandSender sender, ExecutionContext context, ICommandAddress address, int page) {
sender.sendMessage(pageBuilder.getPage(topics, pageLayout, address, sender, context, page, 12));
}
@Override
public void sendSyntaxMessage(CommandSender sender, ExecutionContext context, ICommandAddress address) {
List<IHelpComponent> components = topics.get(1).getComponents(address, sender, context);
if (components.isEmpty()) {
sendHelpMessage(sender, context, address, 1);
} else {
sender.sendMessage(DefaultPageBuilder.combine(components));
}
}
};
}
}

View File

@@ -0,0 +1,278 @@
package io.dico.dicore.command.chat;
import gnu.trove.map.TCharObjectMap;
import gnu.trove.map.hash.TCharObjectHashMap;
public final class Formatting implements CharSequence {
public static final char FORMAT_CHAR = '\u00a7';
private static final TCharObjectMap<Formatting> singleCharInstances = new TCharObjectHashMap<>(16, .5F, '\0');
public static final Formatting
BLACK = from('0'),
DARK_BLUE = from('1'),
DARL_GREEN = from('2'),
CYAN = from('3'),
DARK_RED = from('4'),
PURPLE = from('5'),
ORANGE = from('6'),
GRAY = from('7'),
DARK_GRAY = from('8'),
BLUE = from('9'),
GREEN = from('a'),
AQUA = from('b'),
RED = from('c'),
PINK = from('d'),
YELLOW = from('e'),
WHITE = from('f'),
BOLD = from('l'),
STRIKETHROUGH = from('m'),
UNDERLINE = from('n'),
ITALIC = from('o'),
MAGIC = from('k'),
RESET = from('r'),
EMPTY = from('\0');
public static String stripAll(String value) {
return stripAll(FORMAT_CHAR, value);
}
public static String stripAll(char alternateChar, String value) {
int index = value.indexOf(alternateChar);
int max;
if (index == -1 || index == (max = value.length() - 1)) {
return value;
}
StringBuilder result = new StringBuilder();
int from = 0;
do {
if (isRecognizedChar(value.charAt(index + 1))) {
result.append(value, from, index);
from = index + 2;
} else {
result.append(value, from, from = index + 2);
}
index = value.indexOf(alternateChar, index + 1);
} while (index != -1 && index != max && from <= max);
if (from <= max) {
result.append(value, from, value.length());
}
return result.toString();
}
public static String stripFirst(String value) {
return stripFirst(FORMAT_CHAR, value);
}
public static String stripFirst(char alternateChar, String value) {
int index = value.indexOf(alternateChar);
int max;
if (index == -1 || index == (max = value.length() - 1)) {
return value;
}
StringBuilder result = new StringBuilder(value.length());
int from = 0;
if (isRecognizedChar(value.charAt(index + 1))) {
result.append(value, from, index);
from = index + 2;
} else {
result.append(value, from, from = index + 2);
}
if (from < max) {
result.append(value, from, value.length());
}
return result.toString();
}
public static Formatting from(char c) {
if (isRecognizedChar(c)) {
c = Character.toLowerCase(c);
Formatting res = singleCharInstances.get(c);
if (res == null) {
singleCharInstances.put(c, res = new Formatting(c));
}
return res;
}
return EMPTY;
}
public static Formatting from(String chars) {
return chars.length() == 1 ? from(chars.charAt(0)) : getFormats(chars, '\0');
}
public static Formatting getFormats(String input) {
return getFormats(input, FORMAT_CHAR);
}
public static Formatting getFormats(String input, char formatChar) {
return getFormats(input, 0, input.length(), formatChar);
}
public static Formatting getFormats(String input, int start, int end, char formatChar) {
if ((start < 0) || (start > end) || (end > input.length())) {
throw new IndexOutOfBoundsException("start " + start + ", end " + end + ", input.length() " + input.length());
}
boolean needsFormatChar = formatChar != '\0';
char[] formats = new char[6];
// just make sure it's not the same as formatChar
char previous = (char) (formatChar + 1);
for (int i = start; i < end; i++) {
char c = input.charAt(i);
if (previous == formatChar || !needsFormatChar) {
if (isColourChar(c) || isResetChar(c)) {
formats = new char[6];
formats[0] = Character.toLowerCase(c);
} else if (isFormatChar(c)) {
char format = Character.toLowerCase(c);
for (int j = 0; j < 6; j++) {
if (formats[j] == '\0') {
formats[j] = format;
break;
} else if (formats[j] == format) {
break;
}
}
}
}
previous = c;
}
return formats[1] == '\0' ? from(formats[0]) : new Formatting(formats);
}
public static String translate(String input) {
return translateChars('&', input);
}
public static String translateChars(char alternateChar, String input) {
return translateFormat(alternateChar, FORMAT_CHAR, input);
}
public static String revert(String input) {
return revertChars('&', input);
}
public static String revertChars(char alternateChar, String input) {
return translateFormat(FORMAT_CHAR, alternateChar, input);
}
public static String translateFormat(char fromChar, char toChar, String input) {
if (input == null) {
return null;
}
int n = input.length();
if (n < 2) {
return input;
}
char[] result = null;
char previous = input.charAt(0);
for (int i = 1; i < n; i++) {
char c = input.charAt(i);
if (previous == fromChar && isRecognizedChar(c)) {
if (result == null) {
result = input.toCharArray();
}
result[i - 1] = toChar;
}
previous = c;
}
return result == null ? input : String.valueOf(result);
}
public static void translate(StringBuilder input) {
translateChars('&', input);
}
public static void translateChars(char alternateChar, StringBuilder input) {
translateFormat(alternateChar, FORMAT_CHAR, input);
}
public static void revert(StringBuilder input) {
revertChars('&', input);
}
public static void revertChars(char alternateChar, StringBuilder input) {
translateFormat(FORMAT_CHAR, alternateChar, input);
}
public static void translateFormat(char fromChar, char toChar, StringBuilder input) {
if (input == null) {
return;
}
int n = input.length();
if (n < 2) {
return;
}
char previous = input.charAt(0);
for (int i = 1; i < n; i++) {
char c = input.charAt(i);
if (previous == fromChar && isRecognizedChar(c)) {
input.setCharAt(i - 1, toChar);
}
previous = c;
}
}
private static boolean isRecognizedChar(char c) {
return isColourChar(c) || isFormatChar(c) || isResetChar(c);
}
private static boolean isColourChar(char c) {
return "0123456789abcdefABCDEF".indexOf(c) > -1;
}
private static boolean isResetChar(char c) {
return c == 'r' || c == 'R';
}
private static boolean isFormatChar(char c) {
return "lmnokLMNOK".indexOf(c) > -1;
}
private final String format;
private Formatting(char[] formats) {
StringBuilder format = new StringBuilder(12);
for (char c : formats) {
if (c != '\0') {
format.append(FORMAT_CHAR).append(c);
} else {
break;
}
}
this.format = format.toString();
}
private Formatting(char c) {
this.format = (c != '\0') ? String.valueOf(new char[]{FORMAT_CHAR, c}) : "";
}
@Override
public int length() {
return format.length();
}
@Override
public char charAt(int index) {
return format.charAt(index);
}
@Override
public String subSequence(int start, int end) {
return format.substring(start, end);
}
@Override
public String toString() {
return format;
}
}

View File

@@ -0,0 +1,186 @@
package io.dico.dicore.command.chat;
import io.dico.dicore.command.Command;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.parameter.Parameter;
import io.dico.dicore.command.parameter.ParameterList;
import java.util.*;
import java.util.stream.Collectors;
public class HelpCache {
private static Map<ICommandAddress, HelpCache> caches = new IdentityHashMap<>();
private ICommandAddress address;
private String shortSyntax;
private String[] lines;
private int[] pageStarts;
public static HelpCache getHelpCache(ICommandAddress address) {
return caches.computeIfAbsent(address, HelpCache::new);
}
private HelpCache(ICommandAddress address) {
this.address = address;
}
private void loadHelp() {
List<String> lines = new ArrayList<>();
List<Integer> potentialPageStarts = new ArrayList<>();
int curLineIdx = 0;
potentialPageStarts.add(curLineIdx);
String curLine = address.getChatController().getMessagePrefixForType(EMessageType.INSTRUCTION);
curLine += address.getChatController().getChatFormatForType(EMessageType.INSTRUCTION);
curLine += getSyntax();
lines.add(curLine);
curLineIdx++;
if (address.hasCommand()) {
Command command = address.getCommand();
String[] description = command.getDescription();
if (description != null && description.length > 0) {
for (String line : description) {
curLine = address.getChatController().getChatFormatForType(EMessageType.INFORMATIVE).toString();
curLine += line;
lines.add(curLine);
curLineIdx++;
}
}
}
List<ICommandAddress> children = address.getChildren().values().stream()
.distinct()
.sorted(Comparator.comparing(ICommandAddress::getMainKey))
.collect(Collectors.toList());
for (ICommandAddress address : children) {
potentialPageStarts.add(curLineIdx);
curLine = this.address.getChatController().getChatFormatForType(EMessageType.INSTRUCTION) + "/";
if (address.isDepthLargerThan(2)) {
curLine += "... ";
}
curLine += address.getMainKey();
curLine += getHelpCache(address).getShortSyntax();
lines.add(curLine);
curLineIdx++;
if (address.hasCommand()) {
String shortDescription = address.getCommand().getShortDescription();
if (shortDescription != null) {
curLine = this.address.getChatController().getChatFormatForType(EMessageType.INFORMATIVE).toString();
curLine += shortDescription;
lines.add(curLine);
curLineIdx++;
}
}
}
this.lines = lines.toArray(new String[lines.size()]);
// compute where the pages start with a maximum page size of 10
List<Integer> pageStarts = new ArrayList<>();
pageStarts.add(0);
int maxLength = 10;
int curPageEndTarget = maxLength;
for (int i = 1, n = potentialPageStarts.size(); i < n; i++) {
int index = potentialPageStarts.get(i);
if (index == curPageEndTarget) {
pageStarts.add(curPageEndTarget);
curPageEndTarget += maxLength;
} else if (index > curPageEndTarget) {
curPageEndTarget = potentialPageStarts.get(i - 1);
pageStarts.add(curPageEndTarget);
curPageEndTarget += maxLength;
}
}
int[] pageStartsArray = new int[pageStarts.size()];
for (int i = 0, n = pageStartsArray.length; i < n; i++) {
pageStartsArray[i] = pageStarts.get(i);
}
this.pageStarts = pageStartsArray;
}
/**
* Get a help page
*
* @param page the 0-bound page number (first page is page 0)
* @return the help page
*/
public String getHelpPage(int page) {
if (lines == null) {
loadHelp();
}
//System.out.println(Arrays.toString(lines));
if (page >= pageStarts.length) {
//System.out.println("page >= pageStarts.length: " + Arrays.toString(pageStarts));
return "";
} else if (page < 0) {
throw new IllegalArgumentException("Page number is negative");
}
int start = pageStarts[page];
int end = page + 1 == pageStarts.length ? lines.length : pageStarts[page + 1];
//System.out.println("start = " + start);
//System.out.println("end = " + end);
return String.join("\n", Arrays.copyOfRange(lines, start, end));
}
public int getTotalPageCount() {
return pageStarts.length;
}
/**
* The latter syntax of the command, prefixed by a space.
*
* @return The latter part of the syntax for this command. That is, without the actual command name.
*/
public String getShortSyntax() {
if (shortSyntax != null) {
return shortSyntax;
}
StringBuilder syntax = new StringBuilder();
if (address.hasCommand()) {
Command command = address.getCommand();
ParameterList list = command.getParameterList();
Parameter<?, ?> repeated = list.getRepeatedParameter();
int requiredCount = list.getRequiredCount();
List<Parameter<?, ?>> indexedParameters = list.getIndexedParameters();
for (int i = 0, n = indexedParameters.size(); i < n; i++) {
syntax.append(i < requiredCount ? " <" : " [");
Parameter<?, ?> param = indexedParameters.get(i);
syntax.append(param.getName());
if (param == repeated) {
syntax.append("...");
}
syntax.append(i < requiredCount ? '>' : ']');
}
Map<String, Parameter<?, ?>> parametersByName = list.getParametersByName();
for (Parameter<?, ?> param : parametersByName.values()) {
if (param.isFlag()) {
syntax.append(" [").append(param.getName());
if (param.expectsInput()) {
syntax.append(" <>");
}
syntax.append(']');
}
}
} else {
syntax.append(' ');
}
this.shortSyntax = syntax.toString();
return this.shortSyntax;
}
public String getSyntax() {
return '/' + address.getAddress() + getShortSyntax();
}
}

View File

@@ -0,0 +1,28 @@
package io.dico.dicore.command.chat;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import org.bukkit.command.CommandSender;
//TODO add methods to send JSON messages
public interface IChatController {
void sendMessage(ExecutionContext context, EMessageType type, String message);
void sendMessage(CommandSender sender, EMessageType type, String message);
void handleCommandException(CommandSender sender, ExecutionContext context, CommandException exception);
void handleException(CommandSender sender, ExecutionContext context, Throwable exception);
void sendHelpMessage(CommandSender sender, ExecutionContext context, ICommandAddress address, int page);
void sendSyntaxMessage(CommandSender sender, ExecutionContext context, ICommandAddress address);
Formatting getChatFormatForType(EMessageType type);
String getMessagePrefixForType(EMessageType type);
}

View File

@@ -0,0 +1,24 @@
package io.dico.dicore.command.chat.help;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import org.bukkit.permissions.Permissible;
import java.util.List;
import java.util.Objects;
public abstract class HelpTopicModifier implements IHelpTopic {
private final IHelpTopic delegate;
public HelpTopicModifier(IHelpTopic delegate) {
this.delegate = Objects.requireNonNull(delegate);
}
@Override
public List<IHelpComponent> getComponents(ICommandAddress target, Permissible viewer, ExecutionContext context) {
return modify(delegate.getComponents(target, viewer, context), target, viewer, context);
}
protected abstract List<IHelpComponent> modify(List<IHelpComponent> components, ICommandAddress target, Permissible viewer, ExecutionContext context);
}

View File

@@ -0,0 +1,9 @@
package io.dico.dicore.command.chat.help;
public interface IHelpComponent {
int lineCount();
void appendTo(StringBuilder sb);
}

View File

@@ -0,0 +1,22 @@
package io.dico.dicore.command.chat.help;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import org.bukkit.permissions.Permissible;
import java.util.List;
public interface IHelpTopic {
/**
* Get the components of this help topic
*
* @param target The address of the command to provide help about
* @param viewer The permissible that the page will be shown to (null -> choose a default set).
* @param context Context of the command execution
* @return a mutable list of components to include in the help pages
*/
List<IHelpComponent> getComponents(ICommandAddress target, Permissible viewer, ExecutionContext context);
}

View File

@@ -0,0 +1,7 @@
package io.dico.dicore.command.chat.help;
public interface IPageBorder extends IHelpComponent {
void setPageCount(int pageCount);
}

View File

@@ -0,0 +1,13 @@
package io.dico.dicore.command.chat.help;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import org.bukkit.permissions.Permissible;
import java.util.List;
public interface IPageBuilder {
String getPage(List<IHelpTopic> helpTopics, IPageLayout pageLayout, ICommandAddress target, Permissible viewer, ExecutionContext context, int pageNum, int pageLen);
}

View File

@@ -0,0 +1,20 @@
package io.dico.dicore.command.chat.help;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import org.bukkit.permissions.Permissible;
public interface IPageLayout {
/**
* Get the page borders for a help page
*
* @param target the address that help is displayed for
* @param viewer the viewer of the help page, or null if irrelevant
* @param context the context of the execution
* @param pageNum the page number as displayed in the help page (so it's 1-bound and not 0-bound)
* @return the page borders.
*/
PageBorders getPageBorders(ICommandAddress target, Permissible viewer, ExecutionContext context, int pageNum);
}

View File

@@ -0,0 +1,76 @@
package io.dico.dicore.command.chat.help;
import java.util.Arrays;
public class PageBorders {
private final IPageBorder header, footer;
public PageBorders(IPageBorder header, IPageBorder footer) {
this.header = header;
this.footer = footer;
}
public IPageBorder getHeader() {
return header;
}
public IPageBorder getFooter() {
return footer;
}
public static IPageBorder simpleBorder(String... lines) {
return new SimplePageBorder(lines);
}
public static IPageBorder disappearingBorder(int pageNum, String... lines) {
return disappearingBorder(pageNum, 0, lines);
}
public static IPageBorder disappearingBorder(int pageNum, int keptLines, String... lines) {
return new DisappearingPageBorder(pageNum, keptLines, lines);
}
static class SimplePageBorder extends SimpleHelpComponent implements IPageBorder {
private final String replacedSequence;
public SimplePageBorder(String replacedSequence, String... lines) {
super(lines);
this.replacedSequence = replacedSequence;
}
public SimplePageBorder(String... lines) {
super(lines);
this.replacedSequence = "%pageCount%";
}
@Override
public void setPageCount(int pageCount) {
String[] lines = this.lines;
for (int i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace(replacedSequence, Integer.toString(pageCount));
}
}
}
static class DisappearingPageBorder extends SimpleHelpComponent implements IPageBorder {
private final int pageNum;
private final int keptLines;
public DisappearingPageBorder(int pageNum, int keptLines, String... lines) {
super(lines);
this.pageNum = pageNum;
this.keptLines = keptLines;
}
@Override
public void setPageCount(int pageCount) {
if (pageCount == pageNum) {
String[] lines = this.lines;
this.lines = Arrays.copyOfRange(lines, Math.max(0, lines.length - keptLines), lines.length);
}
}
}
}

View File

@@ -0,0 +1,27 @@
package io.dico.dicore.command.chat.help;
public class SimpleHelpComponent implements IHelpComponent {
String[] lines;
public SimpleHelpComponent(String... lines) {
this.lines = lines;
}
@Override
public int lineCount() {
return lines.length;
}
@Override
public void appendTo(StringBuilder sb) {
String[] lines = this.lines;
int len = lines.length;
if (0 < len) {
sb.append(lines[0]);
}
for (int i = 1; i < len; i++) {
sb.append('\n').append(lines[i]);
}
}
}

View File

@@ -0,0 +1,114 @@
package io.dico.dicore.command.chat.help.defaults;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.help.*;
import org.bukkit.permissions.Permissible;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
public class DefaultPageBuilder implements IPageBuilder {
@Override
public String getPage(List<IHelpTopic> helpTopics, IPageLayout pageLayout, ICommandAddress target, Permissible viewer, ExecutionContext context, int pageNum, int pageLen) {
if (pageLen <= 0 || pageNum < 0) {
throw new IllegalArgumentException();
}
List<IHelpComponent> components = new LinkedList<>();
for (IHelpTopic topic : helpTopics) {
components.addAll(topic.getComponents(target, viewer, context));
}
PageBorders pageBorders = null;
int componentStartIdx = -1;
int componentEndIdx = -1;
int totalPageCount = 0;
int curPageLines = 0;
ListIterator<IHelpComponent> iterator = components.listIterator();
while (iterator.hasNext()) {
if (curPageLines == 0) {
if (pageBorders != null) {
iterator.add(pageBorders.getFooter());
}
if (pageNum == totalPageCount) {
componentStartIdx = iterator.nextIndex();
} else if (pageNum + 1 == totalPageCount) {
componentEndIdx = iterator.nextIndex();
}
pageBorders = pageLayout.getPageBorders(target, viewer, context, totalPageCount + 1);
if (pageBorders != null) {
iterator.add(pageBorders.getHeader());
iterator.previous();
curPageLines += pageBorders.getFooter().lineCount();
}
totalPageCount++;
}
IHelpComponent component = iterator.next();
int lineCount = component.lineCount();
curPageLines += lineCount;
if (curPageLines >= pageLen) {
curPageLines = 0;
}
}
if (componentStartIdx == -1) {
// page does not exist
return "";
}
if (componentEndIdx == -1) {
componentEndIdx = components.size();
}
StringBuilder sb = new StringBuilder();
iterator = components.listIterator(componentStartIdx);
int count = componentEndIdx - componentStartIdx;
boolean first = true;
while (count-- > 0) {
IHelpComponent component = iterator.next();
if (component instanceof IPageBorder) {
((IPageBorder) component).setPageCount(totalPageCount);
}
if (first) {
first = false;
} else {
sb.append('\n');
}
component.appendTo(sb);
}
return sb.toString();
}
public static String combine(List<IHelpComponent> components) {
StringBuilder rv = new StringBuilder();
Iterator<IHelpComponent> iterator = components.iterator();
if (iterator.hasNext()) {
iterator.next().appendTo(rv);
}
while (iterator.hasNext()) {
rv.append('\n');
iterator.next().appendTo(rv);
}
return rv.toString();
}
}

View File

@@ -0,0 +1,40 @@
package io.dico.dicore.command.chat.help.defaults;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.ModifiableCommandAddress;
import io.dico.dicore.command.chat.Formatting;
import io.dico.dicore.command.chat.IChatController;
import io.dico.dicore.command.chat.help.IPageBorder;
import io.dico.dicore.command.chat.help.IPageLayout;
import io.dico.dicore.command.chat.help.PageBorders;
import org.bukkit.permissions.Permissible;
public class DefaultPageLayout implements IPageLayout {
@Override
public PageBorders getPageBorders(ICommandAddress target, Permissible viewer, ExecutionContext context, int pageNum) {
IChatController c = context.getAddress().getChatController();
String prefix = c.getMessagePrefixForType(EMessageType.INFORMATIVE);
Formatting informative = c.getChatFormatForType(EMessageType.INFORMATIVE);
Formatting number = c.getChatFormatForType(EMessageType.NEUTRAL);
String nextPageCommand;
ICommandAddress executor = context.getAddress();
if (((ModifiableCommandAddress) executor).hasHelpCommand()) {
nextPageCommand = ((ModifiableCommandAddress) executor).getHelpCommand().getAddress() + ' ' + (pageNum + 1);
} else {
nextPageCommand = executor.getAddress() + ' ' + (pageNum + 1);
}
String header = prefix + informative + "Help page " + number + pageNum + informative +
'/' + number + "%pageCount%" + informative + " for /" + target.getAddress();
String footer = informative + "Type /" + nextPageCommand + " for the next page";
IPageBorder headerBorder = PageBorders.simpleBorder("", header);
IPageBorder footerBorder = PageBorders.disappearingBorder(pageNum, footer);
return new PageBorders(headerBorder, footerBorder);
}
}

View File

@@ -0,0 +1,45 @@
package io.dico.dicore.command.chat.help.defaults;
import io.dico.dicore.command.Command;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.Formatting;
import io.dico.dicore.command.chat.help.IHelpComponent;
import io.dico.dicore.command.chat.help.IHelpTopic;
import io.dico.dicore.command.chat.help.SimpleHelpComponent;
import org.bukkit.permissions.Permissible;
import java.util.ArrayList;
import java.util.List;
public class DescriptionHelpTopic implements IHelpTopic {
@Override
public List<IHelpComponent> getComponents(ICommandAddress target, Permissible viewer, ExecutionContext context) {
List<IHelpComponent> out = new ArrayList<>();
Formatting format = context.getFormat(EMessageType.DESCRIPTION);
if (!target.hasCommand()) {
return out;
}
Command command = target.getCommand();
String[] description = command.getDescription();
if (description.length == 0) {
String shortDescription = command.getShortDescription();
if (shortDescription == null) {
return out;
}
description = new String[]{shortDescription};
}
for (int i = 0; i < description.length; i++) {
description[i] = format + description[i];
}
out.add(new SimpleHelpComponent(description));
return out;
}
}

View File

@@ -0,0 +1,58 @@
package io.dico.dicore.command.chat.help.defaults;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.Formatting;
import io.dico.dicore.command.chat.help.IHelpComponent;
import io.dico.dicore.command.chat.help.IHelpTopic;
import io.dico.dicore.command.chat.help.SimpleHelpComponent;
import io.dico.dicore.command.predef.PredefinedCommand;
import org.bukkit.command.CommandSender;
import org.bukkit.permissions.Permissible;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class SubcommandsHelpTopic implements IHelpTopic {
@Override
public List<IHelpComponent> getComponents(ICommandAddress target, Permissible viewer, ExecutionContext context) {
List<IHelpComponent> out = new ArrayList<>();
Map<String, ? extends ICommandAddress> children = target.getChildren();
if (children.isEmpty()) {
//System.out.println("No subcommands");
return out;
}
CommandSender sender = viewer instanceof CommandSender ? (CommandSender) viewer : context.getSender();
children.values().stream().distinct().forEach(child -> {
if ((!child.hasCommand() || child.getCommand().isVisibleTo(sender)) && !(child instanceof PredefinedCommand)) {
out.add(getComponent(child, viewer, context));
}
});
return out;
}
public IHelpComponent getComponent(ICommandAddress child, Permissible viewer, ExecutionContext context) {
Formatting subcommand = colorOf(context, EMessageType.SUBCOMMAND);
Formatting highlight = colorOf(context, EMessageType.HIGHLIGHT);
String address = subcommand + "/" + child.getParent().getAddress() + ' ' + highlight + child.getMainKey();
String description = child.hasCommand() ? child.getCommand().getShortDescription() : null;
if (description != null) {
Formatting descriptionFormat = colorOf(context, EMessageType.DESCRIPTION);
return new SimpleHelpComponent(address, descriptionFormat + description);
}
return new SimpleHelpComponent(address);
}
private static Formatting colorOf(ExecutionContext context, EMessageType type) {
return context.getAddress().getChatController().getChatFormatForType(type);
}
}

View File

@@ -0,0 +1,74 @@
package io.dico.dicore.command.chat.help.defaults;
import io.dico.dicore.command.Command;
import io.dico.dicore.command.EMessageType;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.Formatting;
import io.dico.dicore.command.chat.help.IHelpComponent;
import io.dico.dicore.command.chat.help.IHelpTopic;
import io.dico.dicore.command.chat.help.SimpleHelpComponent;
import io.dico.dicore.command.parameter.Parameter;
import io.dico.dicore.command.parameter.ParameterList;
import org.bukkit.permissions.Permissible;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class SyntaxHelpTopic implements IHelpTopic {
@Override
public List<IHelpComponent> getComponents(ICommandAddress target, Permissible viewer, ExecutionContext context) {
if (!target.hasCommand()) {
return Collections.emptyList();
}
String line = context.getFormat(EMessageType.SYNTAX) + "Syntax: "
+ context.getFormat(EMessageType.INSTRUCTION) + target.getAddress()
+ ' ' + getShortSyntax(target, context);
return Collections.singletonList(new SimpleHelpComponent(line));
}
private static String getShortSyntax(ICommandAddress target, ExecutionContext ctx) {
StringBuilder syntax = new StringBuilder();
if (target.hasCommand()) {
Formatting syntaxColor = ctx.getFormat(EMessageType.SYNTAX);
Formatting highlight = ctx.getFormat(EMessageType.HIGHLIGHT);
syntax.append(syntaxColor);
Command command = target.getCommand();
ParameterList list = command.getParameterList();
Parameter<?, ?> repeated = list.getRepeatedParameter();
int requiredCount = list.getRequiredCount();
List<Parameter<?, ?>> indexedParameters = list.getIndexedParameters();
for (int i = 0, n = indexedParameters.size(); i < n; i++) {
syntax.append(i < requiredCount ? " <" : " [");
Parameter<?, ?> param = indexedParameters.get(i);
syntax.append(param.getName());
if (param == repeated) {
syntax.append(highlight).append("...").append(syntaxColor);
}
syntax.append(i < requiredCount ? '>' : ']');
}
Map<String, Parameter<?, ?>> parametersByName = list.getParametersByName();
for (Parameter<?, ?> param : parametersByName.values()) {
if (param.isFlag()) {
syntax.append(" [").append(param.getName());
if (param.expectsInput()) {
syntax.append(" <").append(param.getName()).append(">");
}
syntax.append(']');
}
}
} else {
syntax.append(' ');
}
return syntax.toString();
}
}

View File

@@ -0,0 +1,29 @@
package io.dico.dicore.command.chat.help.insertion;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.help.IHelpComponent;
import org.bukkit.permissions.Permissible;
import java.util.List;
public enum EInsertionStage implements IInsertionFunction {
START {
@Override
public int insertionIndex(List<IHelpComponent> current, ICommandAddress target, Permissible viewer, ExecutionContext context) {
return 0;
}
},
CENTER {
@Override
public int insertionIndex(List<IHelpComponent> current, ICommandAddress target, Permissible viewer, ExecutionContext context) {
return current.size() / 2;
}
},
END {
@Override
public int insertionIndex(List<IHelpComponent> current, ICommandAddress target, Permissible viewer, ExecutionContext context) {
return current.size();
}
}
}

View File

@@ -0,0 +1,43 @@
package io.dico.dicore.command.chat.help.insertion;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.help.HelpTopicModifier;
import io.dico.dicore.command.chat.help.IHelpComponent;
import io.dico.dicore.command.chat.help.IHelpTopic;
import org.bukkit.permissions.Permissible;
import java.util.ArrayList;
import java.util.List;
public class HelpComponentInserter extends HelpTopicModifier {
private List<IInsertion> insertions = new ArrayList<>();
public HelpComponentInserter(IHelpTopic delegate) {
super(delegate);
}
@Override
protected List<IHelpComponent> modify(List<IHelpComponent> components, ICommandAddress target, Permissible viewer, ExecutionContext context) {
int componentCount = components.size();
for (int i = insertions.size() - 1; i >= 0; i--) {
IInsertion insertion = insertions.get(i);
int idx = insertion.insertionIndex(components, target, viewer, context);
List<IHelpComponent> inserted = insertion.getComponents(target, viewer, context);
components.addAll(idx, inserted);
}
return components;
}
public HelpComponentInserter insert(IInsertionFunction insertionFunction, IHelpTopic helpTopic) {
return insert(Insertions.combine(helpTopic, insertionFunction));
}
public HelpComponentInserter insert(IInsertion insertion) {
insertions.add(insertion);
return this;
}
}

View File

@@ -0,0 +1,7 @@
package io.dico.dicore.command.chat.help.insertion;
import io.dico.dicore.command.chat.help.IHelpTopic;
interface IInsertion extends IHelpTopic, IInsertionFunction {
}

View File

@@ -0,0 +1,14 @@
package io.dico.dicore.command.chat.help.insertion;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.help.IHelpComponent;
import org.bukkit.permissions.Permissible;
import java.util.List;
public interface IInsertionFunction {
int insertionIndex(List<IHelpComponent> current, ICommandAddress target, Permissible viewer, ExecutionContext context);
}

View File

@@ -0,0 +1,31 @@
package io.dico.dicore.command.chat.help.insertion;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.chat.help.IHelpComponent;
import io.dico.dicore.command.chat.help.IHelpTopic;
import org.bukkit.permissions.Permissible;
import java.util.List;
public class Insertions {
private Insertions() {
}
public static IInsertion combine(IHelpTopic topic, IInsertionFunction function) {
return new IInsertion() {
@Override
public List<IHelpComponent> getComponents(ICommandAddress target, Permissible viewer, ExecutionContext context) {
return topic.getComponents(target, viewer, context);
}
@Override
public int insertionIndex(List<IHelpComponent> current, ICommandAddress target, Permissible viewer, ExecutionContext context) {
return function.insertionIndex(current, target, viewer, context);
}
};
}
}

View File

@@ -0,0 +1,282 @@
package io.dico.dicore.command.parameter;
import io.dico.dicore.command.CommandException;
import java.util.*;
/**
* Buffer for the arguments.
* Easy to traverse for the parser.
*/
public class ArgumentBuffer extends AbstractList<String> implements Iterator<String>, RandomAccess {
private String[] array;
private int cursor = 0; // index of the next return value
private transient ArgumentBuffer unaffectingCopy = null; // see #getUnaffectingCopy()
public ArgumentBuffer(String label, String[] args) {
this(combine(label, args));
}
private static String[] combine(String label, String[] args) {
String[] result;
//if (args.length > 0 && "".equals(args[args.length - 1])) {
// // drop the last element of args if it is empty
// result = args;
//} else {
result = new String[args.length + 1];
//}
System.arraycopy(args, 0, result, 1, result.length - 1);
result[0] = Objects.requireNonNull(label);
return result;
}
/**
* Constructs a new ArgumentBuffer using the given array, without copying it first.
* None of the array its elements should be empty.
*
* @param array the array
*/
public ArgumentBuffer(String[] array) {
this.array = Objects.requireNonNull(array);
}
public int getCursor() {
return cursor;
}
public ArgumentBuffer setCursor(int cursor) {
if (cursor <= 0) {
cursor = 0;
} else if (size() <= cursor) {
cursor = size();
}
this.cursor = cursor;
return this;
}
@Override
public int size() {
return array.length;
}
@Override
public String get(int index) {
return array[index];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
public int remainingElements() {
return size() - nextIndex() - 1;
}
@Override
public boolean hasNext() {
return nextIndex() < size();
}
public boolean hasPrevious() {
return 0 <= previousIndex();
}
/**
* Unlike conventional ListIterator implementations, this returns null if there is no next element
*
* @return the next value, or null
*/
@Override
public String next() {
return hasNext() ? get(cursor++) : null;
}
public String requireNext(String parameterName) throws CommandException {
String next = next();
if (next == null) {
throw CommandException.missingArgument(parameterName);
}
return next;
}
// useful for completion code
public String nextOrEmpty() {
return hasNext() ? get(cursor++) : "";
}
/**
* Unlike conventional ListIterator implementations, this returns null if there is no previous element
*
* @return the previous value, or null
*/
public String previous() {
return hasPrevious() ? get(--cursor) : null;
}
public String peekNext() {
return hasNext() ? get(cursor) : null;
}
public String peekPrevious() {
return hasPrevious() ? get(cursor - 1) : null;
}
public ArgumentBuffer advance() {
return advance(1);
}
public ArgumentBuffer advance(int amount) {
cursor = Math.min(Math.max(0, cursor + amount), size());
return this;
}
public ArgumentBuffer rewind() {
return rewind(1);
}
public ArgumentBuffer rewind(int amount) {
return advance(-amount);
}
String[] getArray() {
return array;
}
public String[] getArrayFromCursor() {
return getArrayFromIndex(cursor);
}
public String[] getArrayFromIndex(int index) {
return Arrays.copyOfRange(array, index, array.length);
}
public String getRawInput() {
return String.join(" ", array);
}
public String[] toArray() {
return array.clone();
}
@Override
public Iterator<String> iterator() {
return this;
}
@Override
public ListIterator<String> listIterator() {
return new ListIterator<String>() {
@Override
public boolean hasNext() {
return ArgumentBuffer.this.hasNext();
}
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return ArgumentBuffer.this.next();
}
@Override
public boolean hasPrevious() {
return ArgumentBuffer.this.hasPrevious();
}
@Override
public String previous() {
if (!hasPrevious()) {
throw new NoSuchElementException();
}
return ArgumentBuffer.this.previous();
}
@Override
public int nextIndex() {
return ArgumentBuffer.this.nextIndex();
}
@Override
public int previousIndex() {
return ArgumentBuffer.this.previousIndex();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public void set(String s) {
throw new UnsupportedOperationException();
}
@Override
public void add(String s) {
throw new UnsupportedOperationException();
}
};
}
public void dropTrailingEmptyElements() {
int removeCount = 0;
String[] array = this.array;
for (int i = array.length - 1; i >= 0; i--) {
if ("".equals(array[i])) {
removeCount++;
}
}
if (removeCount > 0) {
String[] newArray = new String[array.length - removeCount];
System.arraycopy(array, 0, newArray, 0, newArray.length);
this.array = newArray;
if (cursor > newArray.length) {
cursor = newArray.length;
}
}
}
public ArgumentBuffer preprocessArguments(IArgumentPreProcessor preProcessor) {
String[] array = this.array;
// processor shouldn't touch any items prior to the cursor
if (array != (array = preProcessor.process(cursor, array))) {
return new ArgumentBuffer(array).setCursor(cursor);
}
return this;
}
/**
* Allows a piece of code to traverse this buffer without modifying its cursor.
* After this method has been called for the first time on this instance, if this method
* or the {@link #clone()} method are called, the operation carried out on the prior result has finished.
* As such, the same instance might be returned again.
*
* @return A view of this buffer that doesn't affect this buffer's cursor.
*/
public ArgumentBuffer getUnaffectingCopy() {
// the copy doesn't alter the cursor of this ArgumentBuffer when moved, but traverses the same array reference.
// there is only ever one copy of an ArgumentBuffer, the cursor of which is updated on every call to this method.
ArgumentBuffer unaffectingCopy = this.unaffectingCopy;
if (unaffectingCopy == null) {
this.unaffectingCopy = unaffectingCopy = new ArgumentBuffer(array);
}
unaffectingCopy.cursor = this.cursor;
return unaffectingCopy;
}
@SuppressWarnings("MethodDoesntCallSuperMethod")
public ArgumentBuffer clone() {
ArgumentBuffer result = getUnaffectingCopy();
this.unaffectingCopy = null;
return result;
}
}

View File

@@ -0,0 +1,270 @@
package io.dico.dicore.command.parameter;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.ExecutionContext;
import java.lang.reflect.Array;
import java.util.*;
public class ContextParser {
private final ExecutionContext m_context;
private final ArgumentBuffer m_buffer;
private final ParameterList m_paramList;
private final Parameter<?, ?> m_repeatedParam;
private final List<Parameter<?, ?>> m_indexedParams;
private final int m_maxIndex;
private final int m_requiredIndex;
private Map<String, Object> m_valueMap = new HashMap<>();
private Set<String> m_parsedKeys = new HashSet<>();
private int m_completionCursor = -1;
private Parameter<?, ?> m_completionTarget = null;
public ContextParser(ExecutionContext context) {
this.m_context = context;
this.m_buffer = context.getProcessedBuffer();
this.m_paramList = context.getParameterList();
this.m_repeatedParam = m_paramList.getRepeatedParameter();
this.m_indexedParams = m_paramList.getIndexedParameters();
this.m_maxIndex = m_indexedParams.size() - 1;
this.m_requiredIndex = m_paramList.getRequiredCount() - 1;
}
public ExecutionContext getContext() {
return m_context;
}
public Map<String, Object> getValueMap() {
return m_valueMap;
}
public Set<String> getParsedKeys() {
return m_parsedKeys;
}
public void parse() throws CommandException {
parseAllParameters();
}
public int getCompletionCursor() {
if (!m_done) {
throw new IllegalStateException();
}
return m_completionCursor;
}
public Parameter<?, ?> getCompletionTarget() {
if (!m_done) {
throw new IllegalStateException();
}
return m_completionTarget;
}
// ################################
// # PARSING METHODS #
// ################################
private boolean m_repeating = false;
private boolean m_done = false;
private int m_curParamIndex = -1;
private Parameter<?, ?> m_curParam = null;
private List<Object> m_curRepeatingList = null;
private void parseAllParameters() throws CommandException {
try {
do {
prepareStateToParseParam();
if (m_done) break;
parseCurParam();
} while (!m_done);
} finally {
m_curParam = null;
m_curRepeatingList = null;
assignDefaultValuesToUncomputedParams();
arrayifyRepeatedParamValue();
}
}
private void prepareStateToParseParam() throws CommandException {
boolean requireInput;
if (identifyFlag()) {
m_buffer.advance();
prepareRepeatedParameterIfSet();
requireInput = false;
} else if (m_repeating) {
m_curParam = m_repeatedParam;
requireInput = false;
} else if (m_curParamIndex < m_maxIndex) {
m_curParamIndex++;
m_curParam = m_indexedParams.get(m_curParamIndex);
prepareRepeatedParameterIfSet();
requireInput = m_curParamIndex <= m_requiredIndex;
} else if (m_buffer.hasNext()) {
throw new CommandException("Too many arguments");
} else {
m_done = true;
return;
}
if (!m_buffer.hasNext()) {
if (requireInput) {
reportParameterRequired(m_curParam);
}
if (m_repeating) {
m_done = true;
}
}
}
private boolean identifyFlag() {
String potentialFlag = m_buffer.peekNext();
Parameter<?, ?> target;
if (potentialFlag != null
&& potentialFlag.startsWith("-")
&& (target = m_paramList.getParameterByName(potentialFlag)) != null
&& target.isFlag()
&& !m_valueMap.containsKey(potentialFlag)
// Disabled because it's checked by {@link Parameter#parse(ExecutionContext, ArgumentBuffer)}
// && (target.getFlagPermission() == null || m_context.getSender().hasPermission(target.getFlagPermission()))
) {
m_curParam = target;
return true;
}
return false;
}
private void prepareRepeatedParameterIfSet() throws CommandException {
if (m_curParam != null && m_curParam == m_repeatedParam) {
if (m_curParam.isFlag() && m_curParamIndex < m_requiredIndex) {
Parameter<?, ?> requiredParam = m_indexedParams.get(m_curParamIndex + 1);
reportParameterRequired(requiredParam);
}
m_curRepeatingList = new ArrayList<>();
assignValue(m_curRepeatingList);
m_repeating = true;
}
}
private void reportParameterRequired(Parameter<?, ?> param) throws CommandException {
throw new CommandException("The argument '" + param.getName() + "' is required");
}
private void parseCurParam() throws CommandException {
if (!m_buffer.hasNext() && !m_curParam.isFlag()) {
assignDefaultValue();
return;
}
int cursorStart = m_buffer.getCursor();
if (m_context.isTabComplete() && "".equals(m_buffer.peekNext())) {
assignAsCompletionTarget(cursorStart);
return;
}
Object parseResult;
try {
parseResult = m_curParam.parse(m_context, m_buffer);
} catch (CommandException e) {
assignAsCompletionTarget(cursorStart);
throw e;
}
assignValue(parseResult);
m_parsedKeys.add(m_curParam.getName());
}
private void assignDefaultValue() throws CommandException {
assignValue(m_curParam.getDefaultValue(m_context, m_buffer));
}
private void assignAsCompletionTarget(int cursor) {
m_completionCursor = cursor;
m_completionTarget = m_curParam;
m_done = true;
}
private void assignValue(Object value) {
if (m_repeating) {
m_curRepeatingList.add(value);
} else {
m_valueMap.put(m_curParam.getName(), value);
}
}
private void assignDefaultValuesToUncomputedParams() throws CommandException {
// add default values for unset parameters
for (Map.Entry<String, Parameter<?, ?>> entry : m_paramList.getParametersByName().entrySet()) {
String name = entry.getKey();
if (!m_valueMap.containsKey(name)) {
if (m_repeatedParam == entry.getValue()) {
// below value will be turned into an array later
m_valueMap.put(name, Collections.emptyList());
} else {
m_valueMap.put(name, entry.getValue().getDefaultValue(m_context, m_buffer));
}
}
}
}
private void arrayifyRepeatedParamValue() {
if (m_repeatedParam != null) {
m_valueMap.computeIfPresent(m_repeatedParam.getName(), (k, v) -> {
List list = (List) v;
Class<?> returnType = m_repeatedParam.getType().getReturnType();
Object array = Array.newInstance(returnType, list.size());
ArraySetter setter = ArraySetter.getSetter(returnType);
for (int i = 0, n = list.size(); i < n; i++) {
setter.set(array, i, list.get(i));
}
return array;
});
}
}
private interface ArraySetter {
void set(Object array, int index, Object value);
static ArraySetter getSetter(Class<?> clazz) {
if (!clazz.isPrimitive()) {
return (array, index, value) -> ((Object[]) array)[index] = value;
}
switch (clazz.getSimpleName()) {
case "boolean":
return (array, index, value) -> ((boolean[]) array)[index] = (boolean) value;
case "int":
return (array, index, value) -> ((int[]) array)[index] = (int) value;
case "double":
return (array, index, value) -> ((double[]) array)[index] = (double) value;
case "long":
return (array, index, value) -> ((long[]) array)[index] = (long) value;
case "short":
return (array, index, value) -> ((short[]) array)[index] = (short) value;
case "byte":
return (array, index, value) -> ((byte[]) array)[index] = (byte) value;
case "float":
return (array, index, value) -> ((float[]) array)[index] = (float) value;
case "char":
return (array, index, value) -> ((char[]) array)[index] = (char) value;
case "void":
default:
throw new InternalError("This should not happen");
}
}
}
}

View File

@@ -0,0 +1,126 @@
package io.dico.dicore.command.parameter;
/**
* An interface to process tokens such as quotes
*/
public interface IArgumentPreProcessor {
/**
* Preprocess the arguments without modifying the array.
* Might return the same array (in which case no changes were made).
*
* @param argStart the index within the array where the given arguments start (the part before that identifies the command)
* @param args the arguments
* @return the arguments after preprocessing
*/
String[] process(int argStart, String[] args);
IArgumentPreProcessor NONE = (argStart, args) -> args;
/**
* Get an IArgumentPreProcessor that merges arguments between any two tokens
*
* @param tokens The tokens that the merged arguments should be enclosed by, in subsequent pairs.
* Example: []{}""
* This would mean the following would be merged: [ hello this is a merged argument]
* @param escapeChar the char that can be used to escape the given tokens
* @return The IArgumentPreProcessor
*/
static IArgumentPreProcessor mergeOnTokens(String tokens, char escapeChar) {
if (tokens.isEmpty() || (tokens.length() & 1) != 0) {
throw new IllegalArgumentException();
}
return (argStart, args) -> {
if (!(0 <= argStart && argStart <= args.length)) {
throw new IndexOutOfBoundsException();
}
args = args.clone();
int removeCount = 0;
int closingTokenIdx = 0;
int sectionStart = -1;
for (int i = argStart; i < args.length; i++) {
String arg = args[i];
if (arg == null || arg.isEmpty()) {
continue;
}
if (closingTokenIdx != 0) {
int idx = tokens.indexOf(arg.charAt(arg.length() - 1));
if (idx == closingTokenIdx) {
// count escape chars
int index = arg.length() - 1;
int count = 0;
while (index > 0 && arg.charAt(--index) == escapeChar) {
count++;
}
// remove the final char plus half the count, rounding upwards.
args[i] = arg.substring(0, args.length - 1 - (count + 1) / 2);
if ((count & 1) == 0) {
// not escaped
StringBuilder concat = new StringBuilder(args[sectionStart].substring(1));
for (int j = sectionStart + 1; j <= i; j++) {
concat.append(' ').append(args[j]);
args[j] = null;
removeCount++;
}
args[sectionStart] = concat.toString();
sectionStart = -1;
closingTokenIdx = 0;
} else {
// it's escaped
// add final char because it was escaped
args[i] += tokens.charAt(closingTokenIdx);
}
}
if (i == args.length - 1) {
// if the closing token isn't found, reset state and start from the index subsequent to the one where the opener was found
// it should also undo removal of any escapes... it doesn't do that
i = sectionStart + 1;
closingTokenIdx = 0;
sectionStart = -1;
}
continue;
}
int idx = tokens.indexOf(arg.charAt(0));
if (idx == -1 || (idx & 1) != 0) {
continue;
}
closingTokenIdx = idx | 1;
sectionStart = i;
// make sure to check from the current index for a closer
i--;
}
if (removeCount == 0) {
return args;
}
String[] result = new String[args.length - removeCount];
int i = 0;
for (String arg : args) {
if (arg != null) {
result[i++] = arg;
}
}
return result;
};
}
}

View File

@@ -0,0 +1,129 @@
package io.dico.dicore.command.parameter;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.Validate;
import io.dico.dicore.command.annotation.Range;
import io.dico.dicore.command.parameter.type.ParameterType;
import org.bukkit.Location;
import java.util.List;
import java.util.Objects;
/**
* IParameter object.
*
* @param <TResult> the parameter's type
* @param <TParamInfo> the parameter info object. Example: {@link Range.Memory}
*/
public class Parameter<TResult, TParamInfo> {
private final String name;
private final String description;
private final ParameterType<TResult, TParamInfo> parameterType;
private final TParamInfo paramInfo;
private final boolean flag;
private final String flagPermission;
public Parameter(String name, String description, ParameterType<TResult, TParamInfo> parameterType, TParamInfo paramInfo) {
this(name, description, parameterType, paramInfo, false, null);
}
public Parameter(String name, String description, ParameterType<TResult, TParamInfo> parameterType, TParamInfo paramInfo, boolean flag, String flagPermission) {
this.name = Objects.requireNonNull(name);
this.description = description == null ? "" : description;
this.parameterType = flag ? parameterType.asFlagParameter() : parameterType;
/*
if (paramInfo == null && parameterType.getParameterConfig() != null) {
paramInfo = parameterType.getParameterConfig().getDefaultValue();
}
*/
this.paramInfo = paramInfo;
this.flag = flag;
this.flagPermission = flagPermission;
if (flag && !name.startsWith("-")) {
throw new IllegalArgumentException("Flag parameter's name must start with -");
} else if (!flag && name.startsWith("-")) {
throw new IllegalArgumentException("Non-flag parameter's name may not start with -");
}
}
public static <TResult> Parameter<TResult, ?> newParameter(String name, String description, ParameterType<TResult, ?> type) {
return new Parameter<>(name, description, type, null);
}
public static <TResult, TParamInfo> Parameter<TResult, TParamInfo> newParameter(String name, String description, ParameterType<TResult, TParamInfo> type, TParamInfo info) {
return new Parameter<>(name, description, type, info);
}
public static <TResult, TParamInfo> Parameter<TResult, TParamInfo> newParameter(String name, String description, ParameterType<TResult, TParamInfo> parameterType, TParamInfo paramInfo, boolean flag, String flagPermission) {
return new Parameter<>(name, description, parameterType, paramInfo, flag, flagPermission);
}
public TResult parse(ExecutionContext context, ArgumentBuffer buffer) throws CommandException {
if (getFlagPermission() != null) {
Validate.isAuthorized(context.getSender(), getFlagPermission(), "You do not have permission to use the flag " + name);
}
return checkAllowed(context, parameterType.parseForContext(this, context, buffer));
}
public TResult getDefaultValue(ExecutionContext context, ArgumentBuffer buffer) throws CommandException {
return parameterType.getDefaultValueForContext(this, context, buffer);
}
public List<String> complete(ExecutionContext context, Location location, ArgumentBuffer buffer) {
return parameterType.completeForContext(this, context, location, buffer);
}
public TResult checkAllowed(ExecutionContext context, TResult result) throws CommandException {
return result;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public ParameterType<TResult, TParamInfo> getType() {
return parameterType;
}
public TParamInfo getParamInfo() {
return paramInfo;
}
public boolean isFlag() {
return flag;
}
// override with false for the flag parameter that simply must be present
public boolean expectsInput() {
return parameterType.getExpectedAmountOfConsumedArguments() > 0;
}
public String getFlagPermission() {
return flag ? flagPermission : null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Parameter)) return false;
/*
IParameter<?, ?> parameter = (IParameter<?, ?>) o;
return name.equals(parameter.name);
*/
return false;
}
@Override
public int hashCode() {
return name.hashCode();
}
}

View File

@@ -0,0 +1,128 @@
package io.dico.dicore.command.parameter;
import java.util.*;
/**
* IParameter definition for a command
*/
public class ParameterList {
private List<Parameter<?, ?>> indexedParameters;
private Map<String, Parameter<?, ?>> byName;
private IArgumentPreProcessor argumentPreProcessor = IArgumentPreProcessor.NONE;
private int requiredCount = -1;
private boolean repeatFinalParameter;
// if the final parameter is repeated and the command is implemented through reflection,
// the repeated parameter is simply the last parameter of the method, rather than the last
// indexed parameter. This might be a flag. As such, this field exists to ensure the correct
// parameter is taken for repeating
private boolean finalParameterMayBeFlag;
public ParameterList() {
this.indexedParameters = new ArrayList<>();
this.byName = new LinkedHashMap<>();
this.repeatFinalParameter = false;
}
public IArgumentPreProcessor getArgumentPreProcessor() {
return argumentPreProcessor;
}
public ParameterList setArgumentPreProcessor(IArgumentPreProcessor argumentPreProcessor) {
this.argumentPreProcessor = argumentPreProcessor == null ? IArgumentPreProcessor.NONE : argumentPreProcessor;
return this;
}
public boolean repeatFinalParameter() {
return repeatFinalParameter;
}
public ParameterList setRepeatFinalParameter(boolean repeatFinalParameter) {
this.repeatFinalParameter = repeatFinalParameter;
return this;
}
public boolean finalParameterMayBeFlag() {
return finalParameterMayBeFlag;
}
public ParameterList setFinalParameterMayBeFlag(boolean finalParameterMayBeFlag) {
this.finalParameterMayBeFlag = finalParameterMayBeFlag;
return this;
}
public int getRequiredCount() {
return requiredCount == -1 ? indexedParameters.size() : requiredCount;
}
public ParameterList setRequiredCount(int requiredCount) {
this.requiredCount = requiredCount;
return this;
}
public List<Parameter<?, ?>> getIndexedParameters() {
return Collections.unmodifiableList(indexedParameters);
}
public Parameter<?, ?> getParameterByName(String name) {
return byName.get(name);
}
public String getIndexedParameterName(int index) {
return indexedParameters.get(index).getName();
}
public Map<String, Parameter<?, ?>> getParametersByName() {
return Collections.unmodifiableMap(byName);
}
/**
* Add the given parameter to the end of this parameter list
* Can be a flag
*
* @param parameter the parameter
* @return this
*/
public ParameterList addParameter(Parameter<?, ?> parameter) {
return addParameter(-1, parameter);
}
/**
* Add the given parameter to this parameter list
* If the parameter is a flag, the index is ignored
*
* @param index parameter index number, -1 if end
* @param parameter the parameter
* @return this
* @throws NullPointerException if parameter is null
*/
public ParameterList addParameter(int index, Parameter<?, ?> parameter) {
//System.out.println("Added parameter " + parameter.getName() + ", flag: " + parameter.isFlag());
byName.put(parameter.getName(), parameter);
if (!parameter.isFlag()) {
indexedParameters.add(index == -1 ? indexedParameters.size() : index, parameter);
}
return this;
}
public Parameter<?, ?> getRepeatedParameter() {
if (!repeatFinalParameter) {
return null;
}
if (finalParameterMayBeFlag) {
Iterator<Parameter<?, ?>> iterator = byName.values().iterator();
Parameter<?, ?> result = null;
while (iterator.hasNext()) {
result = iterator.next();
}
return result;
}
if (indexedParameters.isEmpty()) {
return null;
}
return indexedParameters.get(indexedParameters.size() - 1);
}
}

View File

@@ -0,0 +1,44 @@
package io.dico.dicore.command.parameter.type;
import java.lang.annotation.Annotation;
/**
* An interface for an object that stores parameter types by {@link ParameterKey} and finds appropriate types for {@link ParameterKey parameterKeys}
*/
public interface IParameterTypeSelector {
<TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExact(ParameterKey key);
//<TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExactOrSubclass(ParameterKey key);
<TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectAny(ParameterKey key);
default <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExact(Class<?> returnType) {
return selectExact(returnType, null);
}
default <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExact(Class<?> returnType, Class<? extends Annotation> annotationClass) {
return selectExact(new ParameterKey(returnType, annotationClass));
}
/*
default <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExactOrSubclass(Class<?> returnType) {
return selectExactOrSubclass(returnType, null);
}
default <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExactOrSubclass(Class<?> returnType, Class<? extends Annotation> annotationClass) {
return selectExactOrSubclass(new ParameterKey(returnType, annotationClass));
}
*/
default <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectAny(Class<?> returnType) {
return selectAny(returnType, null);
}
default <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectAny(Class<?> returnType, Class<? extends Annotation> annotationClass) {
return selectAny(new ParameterKey(returnType, annotationClass));
}
void addType(boolean infolessAlias, ParameterType<?, ?> type);
}

View File

@@ -0,0 +1,109 @@
package io.dico.dicore.command.parameter.type;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
/**
* Map based implementation of {@link IParameterTypeSelector}
*/
public class MapBasedParameterTypeSelector implements IParameterTypeSelector {
static final MapBasedParameterTypeSelector defaultSelector = new MapBasedParameterTypeSelector(false);
private final Map<ParameterKey, ParameterType<?, ?>> parameterTypeMap;
private final boolean useDefault;
public MapBasedParameterTypeSelector(boolean useDefault) {
this.parameterTypeMap = new HashMap<>();
this.useDefault = useDefault;
}
@Override
public <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectExact(ParameterKey key) {
ParameterType<?, ?> out = parameterTypeMap.get(key);
if (useDefault && out == null) {
out = defaultSelector.selectExact(key);
}
return cast(out);
}
@Override
public <TReturn, TParamInfo> ParameterType<TReturn, TParamInfo> selectAny(ParameterKey key) {
ParameterType<TReturn, TParamInfo> exact = selectExact(key);
if (exact != null) {
return exact;
}
if (key.getAnnotationClass() != null) {
exact = selectExact(new ParameterKey(key.getReturnType()));
if (exact != null) {
return exact;
}
}
Class<?> returnType = key.getReturnType();
Class<? extends Annotation> annotationClass = key.getAnnotationClass();
ParameterType<?, ?> out = selectByReturnType(parameterTypeMap, returnType, annotationClass, false);
if (out == null && useDefault) {
out = selectByReturnType(defaultSelector.parameterTypeMap, returnType, annotationClass, false);
}
if (out == null) {
out = selectByReturnType(parameterTypeMap, returnType, annotationClass, true);
}
if (out == null && useDefault) {
out = selectByReturnType(defaultSelector.parameterTypeMap, returnType, annotationClass, true);
}
return cast(out);
}
private static ParameterType<?, ?> selectByReturnType(Map<ParameterKey, ParameterType<?, ?>> map, Class<?> returnType,
Class<? extends Annotation> annotationClass, boolean allowSubclass) {
ParameterType<?, ?> out = null;
if (allowSubclass) {
for (ParameterType<?, ?> type : map.values()) {
if (returnType.isAssignableFrom(type.getReturnType())) {
if (annotationClass == type.getAnnotationClass()) {
out = type;
break;
}
if (out == null) {
out = type;
}
}
}
} else {
for (ParameterType<?, ?> type : map.values()) {
if (returnType == type.getReturnType()) {
if (annotationClass == type.getAnnotationClass()) {
out = type;
break;
}
if (out == null) {
out = type;
}
}
}
}
return out;
}
private static <T> T cast(Object o) {
//noinspection unchecked
return (T) o;
}
@Override
public void addType(boolean infolessAlias, ParameterType<?, ?> type) {
parameterTypeMap.put(type.getTypeKey(), type);
if (infolessAlias) {
parameterTypeMap.putIfAbsent(type.getInfolessTypeKey(), type);
}
}
static {
// registers default parameter types
ParameterTypes.clinit();
}
}

View File

@@ -0,0 +1,55 @@
package io.dico.dicore.command.parameter.type;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.annotation.Range;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.Parameter;
import org.bukkit.command.CommandSender;
/**
* Abstraction for number parameter types which use {@link Range.Memory} as parameter info.
*
* @param <T> the Number subclass.
*/
public abstract class NumberParameterType<T extends Number> extends ParameterType<T, Range.Memory> {
public NumberParameterType(Class<T> returnType) {
super(returnType, Range.CONFIG);
}
protected abstract T parse(String input) throws NumberFormatException;
protected abstract T select(Number number);
@Override
public T parse(Parameter<T, Range.Memory> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
//System.out.println("In NumberParameterType:parse() for class " + getReturnType().toGenericString());
String input = buffer.next();
if (input == null) {
throw CommandException.missingArgument(parameter.getName());
}
T result;
try {
result = parse(input);
} catch (Exception ex) {
throw CommandException.invalidArgument(parameter.getName(), "a number");
}
Range.Memory memory = (Range.Memory) parameter.getParamInfo();
if (memory != null) {
memory.validate(result, "Argument " + parameter.getName() + " is out of range ["
+ select(memory.min()) + ", " + select(memory.max()) + "]: " + result);
}
return result;
}
@Override
public T getDefaultValue(Parameter<T, Range.Memory> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
Range.Memory memory = (Range.Memory) parameter.getParamInfo();
return select(memory != null ? memory.defaultValue() : 0);
}
}

View File

@@ -0,0 +1,80 @@
package io.dico.dicore.command.parameter.type;
import io.dico.dicore.Reflection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
/**
* This class serves the purpose of having annotated parameter configurations (such as ranges for number parameters).
* Such configurations must be possible to obtain without using annotations, and as such, there should be a class conveying the information
* that is separate from the annotation itself. This class acts as a bridge from the annotation to said class conveying the information.
*
* @param <TAnnotation> the annotation type for parameters
* @param <TParamInfo> the object type that holds the information required in memory
*/
public abstract class ParameterConfig<TAnnotation extends Annotation, TParamInfo> implements Comparable<ParameterConfig<?, ?>> {
private final Class<TAnnotation> annotationClass;
// protected final TParamInfo defaultValue;
public ParameterConfig(Class<TAnnotation> annotationClass/*, TParamInfo defaultValue*/) {
this.annotationClass = annotationClass;
//this.defaultValue = defaultValue;
}
public final Class<TAnnotation> getAnnotationClass() {
return annotationClass;
}
/*
public TParamInfo getDefaultValue() {
return defaultValue;
}*/
protected abstract TParamInfo toParameterInfo(TAnnotation annotation);
public TParamInfo getParameterInfo(Annotation annotation) {
//noinspection unchecked
return toParameterInfo((TAnnotation) annotation);
}
public static <TAnnotation extends Annotation, TParamInfo> ParameterConfig<TAnnotation, TParamInfo>
includeMemoryClass(Class<TAnnotation> annotationClass, Class<TParamInfo> memoryClass) {
Constructor<TParamInfo> constructor;
//TParamInfo defaultValue;
try {
constructor = memoryClass.getConstructor(annotationClass);
//defaultValue = Reflection.getStaticFieldValue(annotationClass, "DEFAULT");
} catch (NoSuchMethodException | IllegalArgumentException ex) {
throw new IllegalArgumentException(ex);
}
/*
if (defaultValue == null) try {
defaultValue = memoryClass.newInstance();
} catch (IllegalAccessException | InstantiationException ex) {
throw new IllegalArgumentException("Failed to get a default value for the param info", ex);
}*/
return new ParameterConfig<TAnnotation, TParamInfo>(annotationClass/*, defaultValue*/) {
@Override
public TParamInfo toParameterInfo(TAnnotation annotation) {
try {
return constructor.newInstance(annotation);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
};
}
public static <TAnnotation extends Annotation, TParamInfo> ParameterConfig<TAnnotation, TParamInfo> getMemoryClassFromField(Class<TAnnotation> annotationClass) {
return ParameterConfig.includeMemoryClass(annotationClass, Reflection.getStaticFieldValue(annotationClass, "MEMORY_CLASS"));
}
@Override
public int compareTo(ParameterConfig<?, ?> o) {
return 0;
}
}

View File

@@ -0,0 +1,46 @@
package io.dico.dicore.command.parameter.type;
import java.lang.annotation.Annotation;
import java.util.Objects;
/**
* More appropriate name: ParameterTypeKey
*/
public class ParameterKey {
private final Class<?> returnType;
private final Class<? extends Annotation> annotationClass;
public ParameterKey(Class<?> returnType) {
this(returnType, null);
}
public ParameterKey(Class<?> returnType, Class<? extends Annotation> annotationClass) {
this.returnType = Objects.requireNonNull(returnType);
this.annotationClass = annotationClass;
}
public Class<?> getReturnType() {
return returnType;
}
public Class<? extends Annotation> getAnnotationClass() {
return annotationClass;
}
@Override
public boolean equals(Object o) {
return this == o || (o instanceof ParameterKey && equals((ParameterKey) o));
}
public boolean equals(ParameterKey that) {
return returnType == that.returnType && annotationClass == that.annotationClass;
}
@Override
public int hashCode() {
int result = returnType.hashCode();
result = 31 * result + (annotationClass != null ? annotationClass.hashCode() : 0);
return result;
}
}

View File

@@ -0,0 +1,201 @@
package io.dico.dicore.command.parameter.type;
import io.dico.dicore.Reflection;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.ExecutionContext;
import io.dico.dicore.command.annotation.Range;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.Parameter;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* A parameter type.
* Takes care of parsing, default values as well as completions.
*
* @param <TReturn> type of the parameter
* @param <TParamInfo> the info object type for the parameter (Example: {@link Range.Memory}
*/
public abstract class ParameterType<TReturn, TParamInfo> {
private final Class<TReturn> returnType;
private final ParameterConfig<?, TParamInfo> parameterConfig;
protected final ParameterType<TReturn, TParamInfo> otherType; // flag or non-flag, depending on current
public ParameterType(Class<TReturn> returnType) {
this(returnType, null);
}
public ParameterType(Class<TReturn> returnType, ParameterConfig<?, TParamInfo> paramConfig) {
this.returnType = Objects.requireNonNull(returnType);
this.parameterConfig = paramConfig;
ParameterType<TReturn, TParamInfo> otherType = flagTypeParameter();
this.otherType = otherType == null ? this : otherType;
}
protected ParameterType(Class<TReturn> returnType, ParameterConfig<?, TParamInfo> parameterConfig, ParameterType<TReturn, TParamInfo> otherType) {
this.returnType = returnType;
this.parameterConfig = parameterConfig;
this.otherType = otherType;
}
public int getExpectedAmountOfConsumedArguments() {
return 1;
}
public boolean canBeFlag() {
return this == otherType;
}
public boolean isFlagExplicitly() {
return this instanceof FlagParameterType;
}
/**
* @return The return type
*/
public final Class<TReturn> getReturnType() {
return returnType;
}
public final Class<?> getAnnotationClass() {
return parameterConfig == null ? null : parameterConfig.getAnnotationClass();
}
public final ParameterConfig<?, TParamInfo> getParameterConfig() {
return parameterConfig;
}
public ParameterKey getTypeKey() {
return new ParameterKey(returnType, parameterConfig != null ? parameterConfig.getAnnotationClass() : null);
}
public ParameterKey getInfolessTypeKey() {
return new ParameterKey(returnType, null);
}
protected FlagParameterType<TReturn, TParamInfo> flagTypeParameter() {
return null;
}
public ParameterType<TReturn, TParamInfo> asFlagParameter() {
return canBeFlag() ? this : otherType;
}
public ParameterType<TReturn, TParamInfo> asNormalParameter() {
return isFlagExplicitly() ? otherType : this;
}
public abstract TReturn parse(Parameter<TReturn, TParamInfo> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException;
public TReturn parseForContext(Parameter<TReturn, TParamInfo> parameter, ExecutionContext context, ArgumentBuffer buffer) throws CommandException {
return parse(parameter, context.getSender(), buffer);
}
public TReturn getDefaultValue(Parameter<TReturn, TParamInfo> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return null;
}
public TReturn getDefaultValueForContext(Parameter<TReturn, TParamInfo> parameter, ExecutionContext context, ArgumentBuffer buffer) throws CommandException {
return getDefaultValue(parameter, context.getSender(), buffer);
}
public List<String> complete(Parameter<TReturn, TParamInfo> parameter, CommandSender sender, Location location, ArgumentBuffer buffer) {
return Collections.emptyList();
}
public List<String> completeForContext(Parameter<TReturn, TParamInfo> parameter, ExecutionContext context, Location location, ArgumentBuffer buffer) {
return complete(parameter, context.getSender(), location, buffer);
}
protected static abstract class FlagParameterType<TResult, TParamInfo> extends ParameterType<TResult, TParamInfo> {
protected FlagParameterType(ParameterType<TResult, TParamInfo> otherType) {
super(otherType.returnType, otherType.parameterConfig, otherType);
}
@Override
public int getExpectedAmountOfConsumedArguments() {
return otherType.getExpectedAmountOfConsumedArguments();
}
@Override
public boolean canBeFlag() {
return true;
}
@Override
protected final FlagParameterType<TResult, TParamInfo> flagTypeParameter() {
return this;
}
@Override
public ParameterType<TResult, TParamInfo> asFlagParameter() {
return this;
}
@Override
public ParameterType<TResult, TParamInfo> asNormalParameter() {
return otherType;
}
}
public @interface Reference {
/**
* The path to the static field holding the parameter type referred.
*
* @return The path
*/
String value();
}
public static class ReferenceUtil {
private ReferenceUtil() {
}
/**
* Get the ParameterType with the associated Reference
*
* @param ref the reference
* @return the parameter type object
* @throws IllegalArgumentException if the class is found, but the field doesn't exist.
* @throws IllegalStateException if this method fails to find the object for any other reason
*/
public static Object getReference(Reference ref) {
String[] path = ref.value().split("\\.");
if (path.length < 2) {
throw new IllegalStateException();
}
String fieldName = path[path.length - 1];
String className = String.join(".", Arrays.copyOfRange(path, 0, path.length - 1));
Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException ex) {
throw new IllegalArgumentException(ex);
}
Object result = Reflection.getStaticFieldValue(clazz, fieldName);
if (result == null) {
throw new IllegalStateException();
}
return result;
}
}
}

View File

@@ -0,0 +1,272 @@
package io.dico.dicore.command.parameter.type;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.Validate;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.Parameter;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/**
* Class providing default parameter types
*/
public class ParameterTypes {
public static IParameterTypeSelector getSelector() {
return MapBasedParameterTypeSelector.defaultSelector;
}
private static <T> T registerType(boolean infolessAlias, T obj) {
getSelector().addType(infolessAlias, (ParameterType<?, ?>) obj);
return obj;
}
static void clinit() {
// initializes class
}
public static final ParameterType<String, Void> STRING;
public static final ParameterType<Boolean, Void> BOOLEAN;
public static final NumberParameterType<Double> DOUBLE;
public static final NumberParameterType<Integer> INTEGER;
public static final NumberParameterType<Long> LONG;
public static final NumberParameterType<Short> SHORT;
public static final NumberParameterType<Float> FLOAT;
public static final ParameterType<Player, Void> PLAYER;
public static final ParameterType<OfflinePlayer, Void> OFFLINE_PLAYER;
//public static final ParameterType<Boolean, Void> PRESENCE;
//public static final NumberParameterType<BigDecimal> BIG_DECIMAL;
//public static final NumberParameterType<BigInteger> BIG_INTEGER;
private ParameterTypes() {
}
static {
STRING = registerType(false, new SimpleParameterType<String, Void>(String.class) {
@Override
protected String parse(Parameter<String, Void> parameter, CommandSender sender, String input) throws CommandException {
return input;
}
@Override
public String getDefaultValue(Parameter<String, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return "";
}
});
BOOLEAN = registerType(true, new ParameterType<Boolean, Void>(Boolean.TYPE) {
@Override
public Boolean parse(Parameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
String input = buffer.requireNext(parameter.getName());
switch (input.toLowerCase()) {
case "true":
case "yes":
return true;
case "false":
case "no":
return false;
default:
throw CommandException.invalidArgument(parameter.getName(), "true, false, yes or no");
}
}
@Override
public Boolean getDefaultValue(Parameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return false;
}
@Override
public List<String> complete(Parameter<Boolean, Void> parameter, CommandSender sender, Location location, ArgumentBuffer buffer) {
String input = buffer.next();
if (input != null) {
List<String> result = new ArrayList<>(1);
input = input.toLowerCase();
for (String value : new String[]{"true", "yes", "false", "no"}) {
if (value.startsWith(input)) {
result.add(value);
}
}
return result;
}
return Arrays.asList("true", "yes", "false", "no");
}
@Override
protected FlagParameterType<Boolean, Void> flagTypeParameter() {
return new FlagParameterType<Boolean, Void>(this) {
@Override
public Boolean parse(Parameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return true;
}
@Override
public Boolean getDefaultValue(Parameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return false;
}
@Override
public int getExpectedAmountOfConsumedArguments() {
return 0;
}
};
}
});
INTEGER = registerType(true, new NumberParameterType<Integer>(Integer.TYPE) {
@Override
protected Integer parse(String input) throws NumberFormatException {
return Integer.parseInt(input);
}
@Override
protected Integer select(Number number) {
return number.intValue();
}
});
DOUBLE = registerType(true, new NumberParameterType<Double>(Double.TYPE) {
@Override
protected Double parse(String input) throws NumberFormatException {
return Double.parseDouble(input);
}
@Override
protected Double select(Number number) {
return number.doubleValue();
}
});
LONG = registerType(true, new NumberParameterType<Long>(Long.TYPE) {
@Override
protected Long parse(String input) throws NumberFormatException {
return Long.parseLong(input);
}
@Override
protected Long select(Number number) {
return number.longValue();
}
});
SHORT = registerType(true, new NumberParameterType<Short>(Short.TYPE) {
@Override
protected Short parse(String input) throws NumberFormatException {
return Short.parseShort(input);
}
@Override
protected Short select(Number number) {
return number.shortValue();
}
});
FLOAT = registerType(true, new NumberParameterType<Float>(Float.TYPE) {
@Override
protected Float parse(String input) throws NumberFormatException {
return Float.parseFloat(input);
}
@Override
protected Float select(Number number) {
return number.floatValue();
}
});
PLAYER = registerType(true, new SimpleParameterType<Player, Void>(Player.class) {
@Override
protected Player parse(Parameter<Player, Void> parameter, CommandSender sender, String input) throws CommandException {
//System.out.println("In ParameterTypes#PLAYER.parse()");
Player player = Bukkit.getPlayer(input);
Validate.notNull(player, "A player by the name '" + input + "' could not be found");
return player;
}
@Override
public List<String> complete(Parameter<Player, Void> parameter, CommandSender sender, Location location, ArgumentBuffer buffer) {
String input = buffer.nextOrEmpty().toLowerCase();
List<String> result = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().toLowerCase().startsWith(input)) {
result.add(player.getName());
}
}
return result;
}
});
OFFLINE_PLAYER = registerType(true, new SimpleParameterType<OfflinePlayer, Void>(OfflinePlayer.class) {
@Override
protected OfflinePlayer parse(Parameter<OfflinePlayer, Void> parameter, CommandSender sender, String input) throws CommandException {
OfflinePlayer result = Bukkit.getPlayer(input);
if (result != null) {
return result;
}
input = input.toLowerCase(Locale.ROOT);
for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) {
if (offlinePlayer.getName().toLowerCase(Locale.ROOT).startsWith(input)) {
return offlinePlayer;
}
}
throw new CommandException("An offline player by the name '" + input + "' could not be found");
}
@Override
public List<String> complete(Parameter<OfflinePlayer, Void> parameter, CommandSender sender, Location location, ArgumentBuffer buffer) {
String input = buffer.nextOrEmpty().toLowerCase();
ArrayList<String> result = new ArrayList<>();
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
if (player.getName().toLowerCase().startsWith(input)) {
if (player.isOnline()) {
result.add(0, player.getName());
} else {
result.add(player.getName());
}
}
}
return result;
}
});
/*
PRESENCE = registerType(false, new ParameterType<Boolean, Void>(Boolean.class) {
@Override
public Boolean parse(IParameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return null;
}
@Override
protected FlagParameterType<Boolean, Void> flagTypeParameter() {
return new FlagParameterType<Boolean, Void>(this) {
@Override
public Boolean parse(IParameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return true;
}
@Override
public Boolean getDefaultValue(IParameter<Boolean, Void> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return false;
}
@Override
public int getExpectedAmountOfConsumedArguments() {
return 0;
}
};
}
});
*/
}
}

View File

@@ -0,0 +1,31 @@
package io.dico.dicore.command.parameter.type;
import io.dico.dicore.command.CommandException;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.Parameter;
import org.bukkit.command.CommandSender;
/**
* An abstraction for parameter types that only parse a single argument
*
* @param <TReturn> the parameter type
* @param <TParamInfo> parameter info object type
*/
public abstract class SimpleParameterType<TReturn, TParamInfo> extends ParameterType<TReturn, TParamInfo> {
public SimpleParameterType(Class<TReturn> returnType) {
super(returnType);
}
public SimpleParameterType(Class<TReturn> returnType, ParameterConfig<?, TParamInfo> paramConfig) {
super(returnType, paramConfig);
}
protected abstract TReturn parse(Parameter<TReturn, TParamInfo> parameter, CommandSender sender, String input) throws CommandException;
@Override
public TReturn parse(Parameter<TReturn, TParamInfo> parameter, CommandSender sender, ArgumentBuffer buffer) throws CommandException {
return parse(parameter, sender, buffer.requireNext(parameter.getName()));
}
}

View File

@@ -0,0 +1,76 @@
package io.dico.dicore.command.predef;
import io.dico.dicore.command.*;
import io.dico.dicore.command.annotation.Range;
import io.dico.dicore.command.parameter.ArgumentBuffer;
import io.dico.dicore.command.parameter.Parameter;
import io.dico.dicore.command.parameter.type.NumberParameterType;
import org.bukkit.command.CommandSender;
/**
* The help command
*/
public class HelpCommand extends PredefinedCommand<HelpCommand> {
private static final Parameter<Integer, Range.Memory> pageParameter;
public static final HelpCommand INSTANCE;
private HelpCommand(boolean modifiable) {
super(modifiable);
getParameterList().addParameter(pageParameter);
getParameterList().setRequiredCount(0);
setDescription("Shows this help page");
}
@Override
protected HelpCommand newModifiableInstance() {
return new HelpCommand(true);
}
@Override
public String execute(CommandSender sender, ExecutionContext context) throws CommandException {
ICommandAddress target = context.getAddress();
if (context.getAddress().getCommand() == this) {
target = target.getParent();
}
context.getAddress().getChatController().sendHelpMessage(sender, context, target, context.<Integer>get("page") - 1);
return null;
}
public static void registerAsChild(ICommandAddress address) {
registerAsChild(address, "help");
}
public static void registerAsChild(ICommandAddress address, String main, String... aliases) {
((ModifiableCommandAddress) address).addChild(new ChildCommandAddress(INSTANCE, main, aliases));
}
static {
pageParameter = new Parameter<>("page", "the page number",
new NumberParameterType<Integer>(Integer.TYPE) {
@Override
protected Integer parse(String input) throws NumberFormatException {
return Integer.parseInt(input);
}
@Override
protected Integer select(Number number) {
return number.intValue();
}
@Override
public Integer parseForContext(Parameter<Integer, Range.Memory> parameter, ExecutionContext context, ArgumentBuffer buffer) throws CommandException {
if (context.getAddress().getCommand() == null || context.getAddress().getCommand().getClass() != HelpCommand.class) {
// An address was executed with its help command as target
buffer.next();
return 1;
}
return parse(parameter, context.getSender(), buffer);
}
},
new Range.Memory(1, Integer.MAX_VALUE, 1));
INSTANCE = new HelpCommand(false);
}
}

View File

@@ -0,0 +1,49 @@
package io.dico.dicore.command.predef;
import io.dico.dicore.command.CommandBuilder;
import io.dico.dicore.command.ExtendedCommand;
import io.dico.dicore.command.ICommandAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Marker class for commands that are generated. These commands can be replaced using methods in {@link CommandBuilder}
*/
public abstract class PredefinedCommand<T extends PredefinedCommand<T>> extends ExtendedCommand<T> {
static final Map<String, Consumer<ICommandAddress>> predefinedCommandGenerators = new HashMap<>();
/**
* Get a predefined command
*
* @param name the name
* @return the subscriber
*/
public static Consumer<ICommandAddress> getPredefinedCommandGenerator(String name) {
return predefinedCommandGenerators.get(name);
}
/**
* Register a predefined command
*
* @param name the name
* @param consumer the generator which adds the child to the address
* @return true if and only if the subscriber was registered (false if the name exists)
*/
public static boolean registerPredefinedCommandGenerator(String name, Consumer<ICommandAddress> consumer) {
return predefinedCommandGenerators.putIfAbsent(name, consumer) == null;
}
static {
registerPredefinedCommandGenerator("help", HelpCommand::registerAsChild);
registerPredefinedCommandGenerator("syntax", SyntaxCommand::registerAsChild);
}
public PredefinedCommand() {
}
public PredefinedCommand(boolean modifiable) {
super(modifiable);
}
}

View File

@@ -0,0 +1,36 @@
package io.dico.dicore.command.predef;
import io.dico.dicore.command.*;
import org.bukkit.command.CommandSender;
/**
* The syntax command
*/
public class SyntaxCommand extends PredefinedCommand<SyntaxCommand> {
public static final SyntaxCommand INSTANCE = new SyntaxCommand(false);
private SyntaxCommand(boolean modifiable) {
super(modifiable);
setDescription("Describes how to use the command");
}
@Override
protected SyntaxCommand newModifiableInstance() {
return new SyntaxCommand(true);
}
@Override
public String execute(CommandSender sender, ExecutionContext context) throws CommandException {
context.getAddress().getChatController().sendSyntaxMessage(sender, context, context.getAddress().getParent());
return null;
}
public static void registerAsChild(ICommandAddress address) {
registerAsChild(address, "syntax");
}
public static void registerAsChild(ICommandAddress address, String main, String... aliases) {
((ModifiableCommandAddress) address).addChild(new ChildCommandAddress(INSTANCE, main, aliases));
}
}

View File

@@ -0,0 +1,122 @@
package io.dico.dicore.command.registration;
import io.dico.dicore.command.ICommandAddress;
import io.dico.dicore.command.ICommandDispatcher;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
/**
* This class extends the bukkit's command class.
* Instances are injected into the command map.
*/
public class BukkitCommand extends Command {
private ICommandDispatcher dispatcher;
private ICommandAddress origin;
public BukkitCommand(ICommandAddress address) {
super(validateTree(address).getNames().get(0), "", "", address.getNames().subList(1, address.getNames().size()));
this.dispatcher = address.getDispatcherForTree();
this.origin = address;
setTimingsIfNecessary(this);
}
private static ICommandAddress validateTree(ICommandAddress tree) {
if (!tree.hasParent()) {
throw new IllegalArgumentException();
}
if (tree.getNames().isEmpty()) {
throw new IllegalArgumentException();
}
return tree;
}
public ICommandAddress getOrigin() {
return origin;
}
@Override
public boolean execute(CommandSender sender, String label, String[] args) {
if (!dispatcher.dispatchCommand(sender, label, args)) {
//System.out.println("failed to dispatch command");
// target command not found, send a message in the future TODO
}
return true;
}
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
return this.tabComplete(sender, alias, args, null);
}
//@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException {
return dispatcher.getTabCompletions(sender, alias, location, args);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BukkitCommand that = (BukkitCommand) o;
return getName().equals(that.getName()) && dispatcher == that.dispatcher;
}
@Override
public int hashCode() {
return dispatcher.hashCode() | getName().hashCode();
}
private static void setTimingsIfNecessary(Command object) {
// with paper spigot, the timings are not set by super constructor but by CommandMap.register(), which is not invoked for this system
// I use reflection so that the project does not require paper spigot to build
try {
// public field
Field field = Command.class.getDeclaredField("timings");
if (field.get(object) != null) return;
Class<?> clazz = Class.forName("co.aikar.timings.TimingsManager");
// public method
Method method = clazz.getDeclaredMethod("getCommandTiming", String.class, Command.class);
Object timings = method.invoke(null, "", object);
field.set(object, timings);
} catch (Throwable ignored) {
}
}
/*
public static void registerToMap(ICommandAddress tree, Map<String, Command> map) {
BukkitCommand command = new BukkitCommand(tree);
Iterator<String> iterator = tree.getNames().iterator();
map.put(iterator.next(), command);
while (iterator.hasNext()) {
map.putIfAbsent(iterator.next(), command);
}
}
public static void unregisterFromMap(ICommandAddress tree, Map<String, Command> map) {
map.values().remove(new BukkitCommand(tree));
}
public static void registerChildrenToMap(ICommandAddress tree, Map<String, Command> map) {
for (Map.Entry<String, ? extends ICommandAddress> entry : tree.getChildren().entrySet()) {
ICommandAddress child = entry.getValue();
registerToMap(child, map);
}
}
public static void unregisterChildenFromMap(ICommandAddress tree, Map<String, Command> map) {
for (Map.Entry<String, ? extends ICommandAddress> entry : tree.getChildren().entrySet()) {
ICommandAddress child = entry.getValue();
unregisterFromMap(child, map);
}
}
*/
}

View File

@@ -0,0 +1,59 @@
package io.dico.dicore.command.registration;
import io.dico.dicore.Reflection;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.plugin.SimplePluginManager;
import java.util.*;
/**
* Provides access to bukkit's {@code Map<String, org.bukkit.command.Command>} command map.
*/
@SuppressWarnings("ConstantConditions")
public class CommandMap {
private static final Map<String, Command> commandMap = findCommandMap();
private CommandMap() {
}
public static Map<String, Command> getCommandMap() {
return Objects.requireNonNull(commandMap);
}
public static boolean isAvailable() {
return commandMap != null;
}
public static Command get(String key) {
return commandMap.get(key);
}
public static void put(String key, Command command) {
commandMap.put(key, command);
}
public static Collection<String> replace(Command command, Command replacement) {
List<String> result = new ArrayList<>();
for (Map.Entry<String, Command> entry : commandMap.entrySet()) {
if (entry.getValue() == command) {
entry.setValue(replacement);
result.add(entry.getKey());
}
}
return result;
}
private static Map<String, Command> findCommandMap() {
try {
return Reflection.getFieldValue(SimpleCommandMap.class, "knownCommands",
Reflection.getFieldValue(SimplePluginManager.class, "commandMap", Bukkit.getPluginManager()));
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}

View File

@@ -0,0 +1,27 @@
package io.dico.dicore.command.registration.reflect;
/**
* Thrown if an error occurs while 'parsing' a reflection command method
* Other errors can be thrown too in there that may not be directly relevant to a parsing error.
*/
public class CommandParseException extends Exception {
public CommandParseException() {
}
public CommandParseException(String message) {
super(message);
}
public CommandParseException(String message, Throwable cause) {
super(message, cause);
}
public CommandParseException(Throwable cause) {
super(cause);
}
public CommandParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,124 @@
package io.dico.dicore.command.registration.reflect;
import io.dico.dicore.command.*;
import io.dico.dicore.command.annotation.Cmd;
import io.dico.dicore.command.annotation.GenerateCommands;
import io.dico.dicore.command.parameter.type.IParameterTypeSelector;
import org.bukkit.command.CommandSender;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
final class ReflectiveCommand extends Command {
private final Method method;
private final Object instance;
private String[] parameterOrder;
private final int flags;
ReflectiveCommand(IParameterTypeSelector selector, Method method, Object instance) throws CommandParseException {
if (!method.isAnnotationPresent(Cmd.class)) {
throw new CommandParseException("No @Cmd present for the method " + method.toGenericString());
}
java.lang.reflect.Parameter[] parameters = method.getParameters();
if (!method.isAccessible()) try {
method.setAccessible(true);
} catch (Exception ex) {
throw new CommandParseException("Failed to make method accessible");
}
if (!Modifier.isStatic(method.getModifiers())) {
if (instance == null) {
try {
instance = method.getDeclaringClass().newInstance();
} catch (Exception ex) {
throw new CommandParseException("No instance given for instance method, and failed to create new instance", ex);
}
} else if (!method.getDeclaringClass().isInstance(instance)) {
throw new CommandParseException("Given instance is not an instance of the method's declaring class");
}
}
this.method = method;
this.instance = instance;
this.flags = ReflectiveRegistration.parseCommandAttributes(selector, method, this, parameters);
}
void setParameterOrder(String[] parameterOrder) {
this.parameterOrder = parameterOrder;
}
ICommandAddress getAddress() {
ChildCommandAddress result = new ChildCommandAddress();
result.setCommand(this);
Cmd cmd = method.getAnnotation(Cmd.class);
result.getNames().add(cmd.value());
for (String alias : cmd.aliases()) {
result.getNames().add(alias);
}
result.finalizeNames();
GenerateCommands generateCommands = method.getAnnotation(GenerateCommands.class);
if (generateCommands != null) {
ReflectiveRegistration.generateCommands(result, generateCommands.value());
}
return result;
}
@Override
public String execute(CommandSender sender, ExecutionContext context) throws CommandException {
//System.out.println("In ReflectiveCommand.execute()");
String[] parameterOrder = this.parameterOrder;
int start = Integer.bitCount(flags);
//System.out.println("start = " + start);
Object[] args = new Object[parameterOrder.length + start];
int i = 0;
if ((flags & 1) != 0) {
args[i++] = sender;
}
if ((flags & 2) != 0) {
args[i++] = context;
}
//System.out.println("i = " + i);
//System.out.println("parameterOrder = " + Arrays.toString(parameterOrder));
for (int n = args.length; i < n; i++) {
//System.out.println("n = " + n);
args[i] = context.get(parameterOrder[i - start]);
//System.out.println("context.get(parameterOrder[i - start]) = " + context.get(parameterOrder[i - start]));
//System.out.println("context.get(parameterOrder[i - start]).getClass() = " + context.get(parameterOrder[i - start]).getClass());
}
//System.out.println("args = " + Arrays.toString(args));
Object result;
try {
result = method.invoke(instance, args);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof CommandException) {
throw (CommandException) ex.getCause();
}
ex.printStackTrace();
throw new CommandException("An internal error occurred while executing this command.", ex);
} catch (Exception ex) {
ex.printStackTrace();
throw new CommandException("An internal error occurred while executing this command.", ex);
}
if (result instanceof String) {
return (String) result;
}
if (result instanceof CommandResult) {
return ((CommandResult) result).getMessage();
}
return null;
}
}

View File

@@ -0,0 +1,385 @@
package io.dico.dicore.command.registration.reflect;
import io.dico.dicore.command.*;
import io.dico.dicore.command.annotation.*;
import io.dico.dicore.command.annotation.GroupMatchedCommands.GroupEntry;
import io.dico.dicore.command.parameter.IArgumentPreProcessor;
import io.dico.dicore.command.parameter.Parameter;
import io.dico.dicore.command.parameter.ParameterList;
import io.dico.dicore.command.parameter.type.IParameterTypeSelector;
import io.dico.dicore.command.parameter.type.MapBasedParameterTypeSelector;
import io.dico.dicore.command.parameter.type.ParameterType;
import io.dico.dicore.command.parameter.type.ParameterTypes;
import io.dico.dicore.command.predef.PredefinedCommand;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Takes care of turning a reflection {@link Method} into a command and more.
*/
public class ReflectiveRegistration {
/**
* This object provides names of the parameters.
* Oddly, the AnnotationParanamer extensions require a 'fallback' paranamer to function properly without
* requiring ALL parameters to have that flag. This is weird because it should just use the AdaptiveParanamer on an upper level to
* determine the name of each individual flag. Oddly this isn't how it works, so the fallback works the same way as the AdaptiveParanamer does.
* It's just linked instead of using an array for that part. Then we can use an AdaptiveParanamer for the latest fallback, to get bytecode names
* or, finally, to get the Jvm-provided parameter names.
*/
//private static final Paranamer paranamer = new CachingParanamer(new BytecodeReadingParanamer());
@SuppressWarnings("StatementWithEmptyBody")
private static String[] lookupParameterNames(Method method, java.lang.reflect.Parameter[] parameters, int start) {
int n = parameters.length;
String[] out = new String[n - start];
//String[] bytecode;
//try {
// bytecode = paranamer.lookupParameterNames(method, false);
//} catch (Exception ex) {
// bytecode = new String[0];
// System.err.println("ReflectiveRegistration.lookupParameterNames failed to read bytecode");
// //ex.printStackTrace();
//}
//int bn = bytecode.length;
for (int i = start; i < n; i++) {
java.lang.reflect.Parameter parameter = parameters[i];
Flag flag = parameter.getAnnotation(Flag.class);
NamedArg namedArg = parameter.getAnnotation(NamedArg.class);
boolean isFlag = flag != null;
String name;
if (namedArg != null && !(name = namedArg.value()).isEmpty()) {
} else if (isFlag && !(name = flag.value()).isEmpty()) {
//} else if (i < bn && (name = bytecode[i]) != null && !name.isEmpty()) {
} else {
name = parameter.getName();
}
if (isFlag) {
name = '-' + name;
} else {
int idx = 0;
while (name.startsWith("-", idx)) {
idx++;
}
name = name.substring(idx);
}
out[i - start] = name;
}
return out;
}
public static void parseCommandGroup(ICommandAddress address, Class<?> clazz, Object instance) throws CommandParseException {
parseCommandGroup(address, ParameterTypes.getSelector(), clazz, instance);
}
public static void parseCommandGroup(ICommandAddress address, IParameterTypeSelector selector, Class<?> clazz, Object instance) throws CommandParseException {
boolean requireStatic = instance == null;
if (!requireStatic && !clazz.isInstance(instance)) {
throw new CommandParseException();
}
List<Method> methods = new LinkedList<>(Arrays.asList(clazz.getDeclaredMethods()));
Iterator<Method> it = methods.iterator();
for (Method method; it.hasNext(); ) {
method = it.next();
if (requireStatic && !Modifier.isStatic(method.getModifiers())) {
it.remove();
continue;
}
if (method.isAnnotationPresent(CmdParamType.class)) {
it.remove();
if (method.getReturnType() != ParameterType.class || method.getParameterCount() != 0) {
throw new CommandParseException("Invalid CmdParamType method: must return ParameterType and take no arguments");
}
ParameterType<?, ?> type;
try {
Object inst = Modifier.isStatic(method.getModifiers()) ? null : instance;
type = (ParameterType<?, ?>) method.invoke(inst);
Objects.requireNonNull(type, "ParameterType returned is null");
} catch (Exception ex) {
throw new CommandParseException("Error occurred whilst getting ParameterType from CmdParamType method '" + method.toGenericString() + "'", ex);
}
if (selector == ParameterTypes.getSelector()) {
selector = new MapBasedParameterTypeSelector(true);
}
selector.addType(method.getAnnotation(CmdParamType.class).infolessAlias(), type);
}
}
GroupMatcherCache groupMatcherCache = new GroupMatcherCache(clazz, address);
for (Method method : methods) {
if (method.isAnnotationPresent(Cmd.class)) {
ICommandAddress parsed = parseCommandMethod(selector, method, instance);
groupMatcherCache.getGroupFor(method).addChild(parsed);
}
}
}
private static final class GroupMatcherCache {
private ModifiableCommandAddress groupRootAddress;
private GroupEntry[] matchEntries;
private Pattern[] patterns;
private ModifiableCommandAddress[] addresses;
GroupMatcherCache(Class<?> clazz, ICommandAddress groupRootAddress) throws CommandParseException {
this.groupRootAddress = (ModifiableCommandAddress) groupRootAddress;
GroupMatchedCommands groupMatchedCommands = clazz.getAnnotation(GroupMatchedCommands.class);
GroupEntry[] matchEntries = groupMatchedCommands == null ? new GroupEntry[0] : groupMatchedCommands.value();
Pattern[] patterns = new Pattern[matchEntries.length];
for (int i = 0; i < matchEntries.length; i++) {
GroupEntry matchEntry = matchEntries[i];
if (matchEntry.group().isEmpty() || matchEntry.regex().isEmpty()) {
throw new CommandParseException("Empty group or regex in GroupMatchedCommands entry");
}
try {
patterns[i] = Pattern.compile(matchEntry.regex());
} catch (PatternSyntaxException ex) {
throw new CommandParseException(ex);
}
}
this.matchEntries = matchEntries;
this.patterns = patterns;
this.addresses = new ModifiableCommandAddress[this.matchEntries.length];
}
ModifiableCommandAddress getGroupFor(Method method) {
String name = method.getName();
GroupEntry[] matchEntries = this.matchEntries;
Pattern[] patterns = this.patterns;
ModifiableCommandAddress[] addresses = this.addresses;
for (int i = 0; i < matchEntries.length; i++) {
GroupEntry matchEntry = matchEntries[i];
if (patterns[i].matcher(name).matches()) {
if (addresses[i] == null) {
addresses[i] = ChildCommandAddress.newPlaceHolderCommand(matchEntry.group(), matchEntry.groupAliases());
groupRootAddress.addChild(addresses[i]);
generateCommands(addresses[i], matchEntry.generatedCommands());
setDescription(addresses[i], matchEntry.description(), matchEntry.shortDescription());
}
return addresses[i];
}
}
return groupRootAddress;
}
}
public static ICommandAddress parseCommandMethod(IParameterTypeSelector selector, Method method, Object instance) throws CommandParseException {
return new ReflectiveCommand(selector, method, instance).getAddress();
}
static int parseCommandAttributes(IParameterTypeSelector selector, Method method, ReflectiveCommand command, java.lang.reflect.Parameter[] parameters) throws CommandParseException {
ParameterList list = command.getParameterList();
boolean hasSenderParameter = false;
int start = 0;
Class<?> firstParameterType = null;
if (parameters.length > start && CommandSender.class.isAssignableFrom(firstParameterType = parameters[0].getType())) {
hasSenderParameter = true;
start++;
}
boolean hasContextParameter = false;
if (parameters.length > start && parameters[start].getType() == ExecutionContext.class) {
hasContextParameter = true;
start++;
}
String[] parameterNames = lookupParameterNames(method, parameters, start);
command.setParameterOrder(parameterNames);
for (int i = start, n = parameters.length; i < n; i++) {
Parameter<?, ?> parameter = parseParameter(selector, method, parameters[i], parameterNames[i - start]);
list.addParameter(parameter);
}
RequirePermissions cmdPermissions = method.getAnnotation(RequirePermissions.class);
if (cmdPermissions != null) {
for (String permission : cmdPermissions.value()) {
command.addContextFilter(IContextFilter.permission(permission));
}
if (cmdPermissions.inherit()) {
command.addContextFilter(IContextFilter.INHERIT_PERMISSIONS);
}
} else {
command.addContextFilter(IContextFilter.INHERIT_PERMISSIONS);
}
RequireParameters reqPar = method.getAnnotation(RequireParameters.class);
if (reqPar != null) {
list.setRequiredCount(reqPar.value() < 0 ? Integer.MAX_VALUE : reqPar.value());
} else {
list.setRequiredCount(list.getIndexedParameters().size());
}
PreprocessArgs preprocessArgs = method.getAnnotation(PreprocessArgs.class);
if (preprocessArgs != null) {
IArgumentPreProcessor preProcessor = IArgumentPreProcessor.mergeOnTokens(preprocessArgs.tokens(), preprocessArgs.escapeChar());
list.setArgumentPreProcessor(preProcessor);
}
Desc desc = method.getAnnotation(Desc.class);
if (desc != null) {
String[] array = desc.value();
if (array.length == 0) {
command.setDescription(desc.shortVersion());
} else {
command.setDescription(array);
}
} else {
command.setDescription();
}
if (hasSenderParameter && Player.class.isAssignableFrom(firstParameterType)) {
command.addContextFilter(IContextFilter.PLAYER_ONLY);
} else if (hasSenderParameter && ConsoleCommandSender.class.isAssignableFrom(firstParameterType)) {
command.addContextFilter(IContextFilter.CONSOLE_ONLY);
} else if (method.isAnnotationPresent(RequirePlayer.class)) {
command.addContextFilter(IContextFilter.PLAYER_ONLY);
} else if (method.isAnnotationPresent(RequireConsole.class)) {
command.addContextFilter(IContextFilter.CONSOLE_ONLY);
}
list.setRepeatFinalParameter(parameters.length > start && parameters[parameters.length - 1].isVarArgs());
list.setFinalParameterMayBeFlag(true);
return (hasSenderParameter ? 1 : 0) | (hasContextParameter ? 2 : 0);
}
public static int parseCommandAttributes(IParameterTypeSelector selector, Method method, ReflectiveCommand command) throws CommandParseException {
return parseCommandAttributes(selector, method, command, method.getParameters());
}
public static Parameter<?, ?> parseParameter(IParameterTypeSelector selector, Method method, java.lang.reflect.Parameter parameter, String name) throws CommandParseException {
Class<?> type = parameter.getType();
if (parameter.isVarArgs()) {
type = type.getComponentType();
}
Annotation[] annotations = parameter.getAnnotations();
Flag flag = null;
Annotation typeAnnotation = null;
Desc desc = null;
for (Annotation annotation : annotations) {
//noinspection StatementWithEmptyBody
if (annotation instanceof NamedArg) {
// do nothing
} else if (annotation instanceof Flag) {
if (flag != null) {
throw new CommandParseException("Multiple flags for the same parameter");
}
flag = (Flag) annotation;
} else if (annotation instanceof Desc) {
if (desc != null) {
throw new CommandParseException("Multiple descriptions for the same parameter");
}
desc = (Desc) annotation;
} else {
if (typeAnnotation != null) {
throw new CommandParseException("Multiple parameter type annotations for the same parameter");
}
typeAnnotation = annotation;
}
}
if (flag == null && name.startsWith("-")) {
throw new CommandParseException("Non-flag parameter's name starts with -");
} else if (flag != null && !name.startsWith("-")) {
throw new CommandParseException("Flag parameter's name doesn't start with -");
}
ParameterType<Object, Object> parameterType = selector.selectAny(type, typeAnnotation == null ? null : typeAnnotation.getClass());
if (parameterType == null) {
throw new CommandParseException("IParameter type not found for parameter " + name + " in method " + method.toGenericString());
}
Object parameterInfo;
if (typeAnnotation == null) {
parameterInfo = null;
} else try {
parameterInfo = parameterType.getParameterConfig() == null ? null : parameterType.getParameterConfig().getParameterInfo(typeAnnotation);
} catch (Exception ex) {
throw new CommandParseException("Invalid parameter config", ex);
}
String descString = desc == null ? null : CommandAnnotationUtils.getShortDescription(desc);
try {
//noinspection unchecked
return Parameter.newParameter(name, descString, parameterType, parameterInfo, name.startsWith("-"), flag == null ? null : flag.permission());
} catch (Exception ex) {
throw new CommandParseException("Invalid parameter", ex);
}
}
public static void generateCommands(ICommandAddress address, String[] input) {
for (String value : input) {
Consumer<ICommandAddress> consumer = PredefinedCommand.getPredefinedCommandGenerator(value);
if (consumer == null) {
System.out.println("[Command Warning] generated command '" + value + "' could not be found");
} else {
consumer.accept(address);
}
}
}
/*
Desired format
@Cmd({"tp", "tpto"})
@RequirePermissions("teleport.self")
public (static) String|void|CommandResult onCommand(Player sender, Player target, @Flag("force", permission = "teleport.self.force") boolean force) {
Validate.isTrue(force || !hasTpToggledOff(target), "Target has teleportation disabled. Use -force to ignore");
sender.teleport(target);
//return
}
parser needs to:
- see the @Cmd and create a CommandTree for it
- see that it must be a Player executing the command
- add an indexed IParameter for a Player type
- add a flag parameter named force, that consumes no arguments.
- see that setting the force flag requires a permission
*/
private static void setDescription(ICommandAddress address, String[] array, String shortVersion) {
if (!address.hasCommand()) {
return;
}
if (array.length == 0) {
address.getCommand().setDescription(shortVersion);
} else {
address.getCommand().setDescription(array);
}
}
}