summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDico200 <dico.karssiens@gmail.com>2018-07-25 01:53:23 +0100
committerDico200 <dico.karssiens@gmail.com>2018-07-25 01:53:23 +0100
commit44587e49ff1840219d9bc44844d4a3a6cd8ac5de (patch)
tree276ae9625795e9d79fc7db8592dbcb3a1af60928
parent5e168847c2624b767deb9da310ecfdf169e0f43c (diff)
Add dicore3-command
-rw-r--r--dicore3/command/build.gradle.kts4
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/ChildCommandAddress.java96
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/Command.java191
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/CommandBuilder.java380
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/CommandException.java28
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/CommandResult.java23
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/EMessageType.java19
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/EOverridePolicy.java12
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/ExecutionContext.java352
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/ExtendedCommand.java52
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/ICommandAddress.java152
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/ICommandDispatcher.java131
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/IContextFilter.java322
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/LambdaCommand.java35
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/ModifiableCommandAddress.java258
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/RootCommandAddress.java218
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/Validate.java52
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/BigRange.java52
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/Cmd.java16
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/CmdParamType.java27
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/CommandAnnotationUtils.java35
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/Desc.java27
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/Flag.java16
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/GenerateCommands.java14
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/GroupMatchedCommands.java68
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/NamedArg.java14
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/PreprocessArgs.java16
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/Range.java67
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireConsole.java11
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequireParameters.java14
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePermissions.java30
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/annotation/RequirePlayer.java11
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/AbstractChatController.java86
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/ChatControllers.java52
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/Formatting.java278
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/HelpCache.java186
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/IChatController.java28
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/HelpTopicModifier.java24
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpComponent.java9
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IHelpTopic.java22
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBorder.java7
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageBuilder.java13
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/IPageLayout.java20
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/PageBorders.java76
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/SimpleHelpComponent.java27
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageBuilder.java114
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DefaultPageLayout.java40
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/DescriptionHelpTopic.java45
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SubcommandsHelpTopic.java58
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/defaults/SyntaxHelpTopic.java74
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/EInsertionStage.java29
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/HelpComponentInserter.java43
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertion.java7
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/IInsertionFunction.java14
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/chat/help/insertion/Insertions.java31
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/ArgumentBuffer.java282
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/ContextParser.java270
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/IArgumentPreProcessor.java126
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/Parameter.java129
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/ParameterList.java128
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/IParameterTypeSelector.java44
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/MapBasedParameterTypeSelector.java109
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/NumberParameterType.java55
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterConfig.java80
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterKey.java46
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterType.java201
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/ParameterTypes.java272
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/parameter/type/SimpleParameterType.java31
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/predef/HelpCommand.java76
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/predef/PredefinedCommand.java49
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/predef/SyntaxCommand.java36
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/registration/BukkitCommand.java122
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/registration/CommandMap.java59
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/CommandParseException.java27
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveCommand.java124
-rw-r--r--dicore3/command/src/main/java/io/dico/dicore/command/registration/reflect/ReflectiveRegistration.java385
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);
+ }
+
+ }
+
+}