diff options
author | Dico200 <dico.karssiens@gmail.com> | 2018-07-25 01:53:23 +0100 |
---|---|---|
committer | Dico200 <dico.karssiens@gmail.com> | 2018-07-25 01:53:23 +0100 |
commit | 44587e49ff1840219d9bc44844d4a3a6cd8ac5de (patch) | |
tree | 276ae9625795e9d79fc7db8592dbcb3a1af60928 | |
parent | 5e168847c2624b767deb9da310ecfdf169e0f43c (diff) |
Add dicore3-command
76 files changed, 6677 insertions, 0 deletions
diff --git a/dicore3/command/build.gradle.kts b/dicore3/command/build.gradle.kts new file mode 100644 index 0000000..6077c91 --- /dev/null +++ b/dicore3/command/build.gradle.kts @@ -0,0 +1,4 @@ + +group = "io.dico.dicore3" +//name = "dicore3-command" +version = "1.2.5-mc-1.13" diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java new file mode 100644 index 0000000..7593492 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/Command.java b/dicore3/command/src/main/java/io/dico/dicore/command/Command.java new file mode 100644 index 0000000..0ba04b1 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/Command.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java b/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java new file mode 100644 index 0000000..59eebf9 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/CommandException.java b/dicore3/command/src/main/java/io/dico/dicore/command/CommandException.java new file mode 100644 index 0000000..b859952 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/CommandException.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/CommandResult.java b/dicore3/command/src/main/java/io/dico/dicore/command/CommandResult.java new file mode 100644 index 0000000..7c4a891 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/CommandResult.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/EMessageType.java b/dicore3/command/src/main/java/io/dico/dicore/command/EMessageType.java new file mode 100644 index 0000000..fba8780 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/EMessageType.java @@ -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, +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/EOverridePolicy.java b/dicore3/command/src/main/java/io/dico/dicore/command/EOverridePolicy.java new file mode 100644 index 0000000..83b0151 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/EOverridePolicy.java @@ -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 +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java b/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java new file mode 100644 index 0000000..4c014fb --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java @@ -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); + } + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ExtendedCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/ExtendedCommand.java new file mode 100644 index 0000000..602760c --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ExtendedCommand.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java new file mode 100644 index 0000000..d6cd350 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java new file mode 100644 index 0000000..b18694e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/IContextFilter.java b/dicore3/command/src/main/java/io/dico/dicore/command/IContextFilter.java new file mode 100644 index 0000000..a60c34e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/IContextFilter.java @@ -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); + } + }; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/LambdaCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/LambdaCommand.java new file mode 100644 index 0000000..71b5ca4 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/LambdaCommand.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java new file mode 100644 index 0000000..698eee8 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java @@ -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); + } + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java b/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java new file mode 100644 index 0000000..91dcc5b --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java @@ -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; + } +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/Validate.java b/dicore3/command/src/main/java/io/dico/dicore/command/Validate.java new file mode 100644 index 0000000..596ad08 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/Validate.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/BigRange.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/BigRange.java new file mode 100644 index 0000000..467ba4b --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/BigRange.java @@ -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; + } + + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Cmd.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Cmd.java new file mode 100644 index 0000000..109490a --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Cmd.java @@ -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 {}; + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/CmdParamType.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/CmdParamType.java new file mode 100644 index 0000000..ea51e44 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/CmdParamType.java @@ -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; + +} + diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/CommandAnnotationUtils.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/CommandAnnotationUtils.java new file mode 100644 index 0000000..868884c --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/CommandAnnotationUtils.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Desc.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Desc.java new file mode 100644 index 0000000..0011fb8 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Desc.java @@ -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 ""; + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Flag.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Flag.java new file mode 100644 index 0000000..31a47dd --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Flag.java @@ -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 ""; + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/GenerateCommands.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/GenerateCommands.java new file mode 100644 index 0000000..9b7164d --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/GenerateCommands.java @@ -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"}; + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/GroupMatchedCommands.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/GroupMatchedCommands.java new file mode 100644 index 0000000..53e3e9e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/GroupMatchedCommands.java @@ -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(); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/NamedArg.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/NamedArg.java new file mode 100644 index 0000000..fa42e6b --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/NamedArg.java @@ -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(); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/PreprocessArgs.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/PreprocessArgs.java new file mode 100644 index 0000000..40d6d73 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/PreprocessArgs.java @@ -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 '\\'; + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Range.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Range.java new file mode 100644 index 0000000..3fd4160 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/Range.java @@ -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); + } + + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireConsole.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireConsole.java new file mode 100644 index 0000000..362f05c --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireConsole.java @@ -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 { +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireParameters.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireParameters.java new file mode 100644 index 0000000..02f5548 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireParameters.java @@ -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(); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java new file mode 100644 index 0000000..0fbe9a4 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java @@ -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; + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePlayer.java b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePlayer.java new file mode 100644 index 0000000..2165e05 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePlayer.java @@ -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 { +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/AbstractChatController.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/AbstractChatController.java new file mode 100644 index 0000000..7d88d0e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/AbstractChatController.java @@ -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 ""; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/ChatControllers.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/ChatControllers.java new file mode 100644 index 0000000..709f791 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/ChatControllers.java @@ -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)); + } + } + + }; + } +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/Formatting.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/Formatting.java new file mode 100644 index 0000000..576dfb5 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/Formatting.java @@ -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; + } + +}
\ No newline at end of file diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/HelpCache.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/HelpCache.java new file mode 100644 index 0000000..ed91d69 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/HelpCache.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/IChatController.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/IChatController.java new file mode 100644 index 0000000..79a3d48 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/IChatController.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/HelpTopicModifier.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/HelpTopicModifier.java new file mode 100644 index 0000000..a44822b --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/HelpTopicModifier.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpComponent.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpComponent.java new file mode 100644 index 0000000..e1867b5 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpComponent.java @@ -0,0 +1,9 @@ +package io.dico.dicore.command.chat.help; + +public interface IHelpComponent { + + int lineCount(); + + void appendTo(StringBuilder sb); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpTopic.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpTopic.java new file mode 100644 index 0000000..618109e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpTopic.java @@ -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); + + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBorder.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBorder.java new file mode 100644 index 0000000..1ae3561 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBorder.java @@ -0,0 +1,7 @@ +package io.dico.dicore.command.chat.help; + +public interface IPageBorder extends IHelpComponent { + + void setPageCount(int pageCount); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBuilder.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBuilder.java new file mode 100644 index 0000000..86d9450 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBuilder.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageLayout.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageLayout.java new file mode 100644 index 0000000..3223e32 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageLayout.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/PageBorders.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/PageBorders.java new file mode 100644 index 0000000..43c0514 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/PageBorders.java @@ -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); + } + } + + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/SimpleHelpComponent.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/SimpleHelpComponent.java new file mode 100644 index 0000000..22707fd --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/SimpleHelpComponent.java @@ -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]); + } + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageBuilder.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageBuilder.java new file mode 100644 index 0000000..aaf4d1e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageBuilder.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageLayout.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageLayout.java new file mode 100644 index 0000000..e8f9bce --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageLayout.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DescriptionHelpTopic.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DescriptionHelpTopic.java new file mode 100644 index 0000000..d1a6445 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DescriptionHelpTopic.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SubcommandsHelpTopic.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SubcommandsHelpTopic.java new file mode 100644 index 0000000..1e9922f --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SubcommandsHelpTopic.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SyntaxHelpTopic.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SyntaxHelpTopic.java new file mode 100644 index 0000000..7c0bc9d --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SyntaxHelpTopic.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/EInsertionStage.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/EInsertionStage.java new file mode 100644 index 0000000..4f0026d --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/EInsertionStage.java @@ -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(); + } + } +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/HelpComponentInserter.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/HelpComponentInserter.java new file mode 100644 index 0000000..f8ae713 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/HelpComponentInserter.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertion.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertion.java new file mode 100644 index 0000000..757cb91 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertion.java @@ -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 { + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertionFunction.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertionFunction.java new file mode 100644 index 0000000..e99c246 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertionFunction.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/Insertions.java b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/Insertions.java new file mode 100644 index 0000000..042cd72 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/Insertions.java @@ -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); + } + }; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ArgumentBuffer.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ArgumentBuffer.java new file mode 100644 index 0000000..e063000 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ArgumentBuffer.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java new file mode 100644 index 0000000..8a3f236 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java @@ -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"); + } + } + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/IArgumentPreProcessor.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/IArgumentPreProcessor.java new file mode 100644 index 0000000..4ac9bd3 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/IArgumentPreProcessor.java @@ -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; + }; + + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/Parameter.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/Parameter.java new file mode 100644 index 0000000..ca66068 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/Parameter.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ParameterList.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ParameterList.java new file mode 100644 index 0000000..582d20a --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/ParameterList.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/IParameterTypeSelector.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/IParameterTypeSelector.java new file mode 100644 index 0000000..780ea0d --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/IParameterTypeSelector.java @@ -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); + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/MapBasedParameterTypeSelector.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/MapBasedParameterTypeSelector.java new file mode 100644 index 0000000..4e475fe --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/MapBasedParameterTypeSelector.java @@ -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(); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/NumberParameterType.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/NumberParameterType.java new file mode 100644 index 0000000..ed53cb0 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/NumberParameterType.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterConfig.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterConfig.java new file mode 100644 index 0000000..dbd7590 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterConfig.java @@ -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; + } + +} + diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterKey.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterKey.java new file mode 100644 index 0000000..67f86d4 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterKey.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterType.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterType.java new file mode 100644 index 0000000..d89fd10 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterType.java @@ -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; + } + + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterTypes.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterTypes.java new file mode 100644 index 0000000..98b7b77 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterTypes.java @@ -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; + } + }; + } + }); + */ + + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/SimpleParameterType.java b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/SimpleParameterType.java new file mode 100644 index 0000000..ecf19ce --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/SimpleParameterType.java @@ -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())); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/predef/HelpCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/predef/HelpCommand.java new file mode 100644 index 0000000..cadf4ae --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/predef/HelpCommand.java @@ -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); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/predef/PredefinedCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/predef/PredefinedCommand.java new file mode 100644 index 0000000..4e7ba07 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/predef/PredefinedCommand.java @@ -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); + } +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/predef/SyntaxCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/predef/SyntaxCommand.java new file mode 100644 index 0000000..4f26a7b --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/predef/SyntaxCommand.java @@ -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)); + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/BukkitCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/BukkitCommand.java new file mode 100644 index 0000000..b5346d0 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/BukkitCommand.java @@ -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); + } + } + */ + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/CommandMap.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/CommandMap.java new file mode 100644 index 0000000..780ec66 --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/CommandMap.java @@ -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; + } + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/CommandParseException.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/CommandParseException.java new file mode 100644 index 0000000..478c70c --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/CommandParseException.java @@ -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); + } +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java new file mode 100644 index 0000000..f033d9e --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java @@ -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; + } + +} diff --git a/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java new file mode 100644 index 0000000..84bb10b --- /dev/null +++ b/dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java @@ -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); + } + + } + +} |