diff --git a/CommandFramework/build.gradle.kts b/CommandFramework/build.gradle.kts
new file mode 100644
index 00000000..880a4451
--- /dev/null
+++ b/CommandFramework/build.gradle.kts
@@ -0,0 +1,62 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2024 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+plugins {
+ id("base")
+ id("java")
+}
+
+group = "de.steamwar"
+version = ""
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+sourceSets {
+ main {
+ java {
+ srcDirs("src/")
+ }
+ resources {
+ srcDirs("src/")
+ exclude("**/*.java", "**/*.kt")
+ }
+ }
+ test {
+ java {
+ srcDirs("testsrc/")
+ }
+ resources {
+ srcDirs("testsrc/")
+ exclude("**/*.java", "**/*.kt")
+ }
+ }
+}
+
+dependencies {
+ compileOnly("org.projectlombok:lombok:1.18.32")
+ annotationProcessor("org.projectlombok:lombok:1.18.32")
+ testCompileOnly("org.projectlombok:lombok:1.18.32")
+ testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
+
+ testImplementation("junit:junit:4.13.2")
+ testImplementation("org.hamcrest:hamcrest:2.2")
+}
\ No newline at end of file
diff --git a/CommandFramework/src/de/steamwar/command/AbstractSWCommand.java b/CommandFramework/src/de/steamwar/command/AbstractSWCommand.java
new file mode 100644
index 00000000..65f6643c
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/AbstractSWCommand.java
@@ -0,0 +1,711 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+import java.lang.annotation.*;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public abstract class AbstractSWCommand {
+
+ private static final Map>, List>> dependencyMap = new HashMap<>();
+
+ private Class> clazz; // This is used in createMappings()
+
+ private boolean initialized = false;
+ protected final List> commandList = new ArrayList<>();
+ protected final List> helpCommandList = new ArrayList<>();
+
+ private final Map> localTypeMapper = new HashMap<>();
+ private final Map> localValidators = new HashMap<>();
+
+ protected AbstractSWCommand(Class clazz, String command) {
+ this(clazz, command, new String[0]);
+ }
+
+ protected AbstractSWCommand(Class clazz, String command, String... aliases) {
+ this.clazz = clazz;
+
+ PartOf partOf = this.getClass().getAnnotation(PartOf.class);
+ if (partOf != null) {
+ dependencyMap.computeIfAbsent((Class>) partOf.value(), k -> new ArrayList<>()).add(this);
+ return;
+ }
+
+ createAndSafeCommand(command, aliases);
+ unregister();
+ register();
+ }
+
+ protected abstract void createAndSafeCommand(String command, String[] aliases);
+
+ public abstract void unregister();
+
+ public abstract void register();
+
+ protected void commandSystemError(T sender, CommandFrameworkException e) {
+ e.printStackTrace();
+ }
+
+ protected void commandSystemWarning(Supplier message) {
+ System.out.println(message.get());
+ }
+
+ protected void sendMessage(T sender, String message, Object[] args) {
+ }
+
+ protected void initialisePartOf(AbstractSWCommand parent) {
+ }
+
+ protected final void execute(T sender, String alias, String[] args) {
+ initialize();
+ List errors = new ArrayList<>();
+ try {
+ if (commandList.stream().noneMatch(s -> s.invoke(errors::add, sender, alias, args))) {
+ errors.forEach(Runnable::run);
+ } else return;
+ if (errors.isEmpty() && helpCommandList.stream().noneMatch(s -> s.invoke(errors::add, sender, alias, args))) {
+ errors.forEach(Runnable::run);
+ }
+ } catch (CommandFrameworkException e) {
+ commandSystemError(sender, e);
+ throw e;
+ }
+ }
+
+ protected final List tabComplete(T sender, String alias, String[] args) throws IllegalArgumentException {
+ initialize();
+ String string = args[args.length - 1].toLowerCase();
+ return Stream.concat(commandList.stream(), helpCommandList.stream())
+ .filter(s -> !s.noTabComplete)
+ .map(s -> s.tabComplete(sender, args))
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .filter(s -> !s.isEmpty())
+ .filter(s -> s.toLowerCase().startsWith(string) || string.startsWith(s.toLowerCase()))
+ .distinct()
+ .collect(Collectors.toList());
+ }
+
+ private synchronized void initialize() {
+ if (initialized) return;
+ List methods = methods().stream()
+ .filter(this::validateMethod)
+ .collect(Collectors.toList());
+ for (Method method : methods) {
+ Cached cached = method.getAnnotation(Cached.class);
+ this.>add(Mapper.class, method, (anno, typeMapper) -> {
+ TabCompletionCache.add(typeMapper, cached);
+ (anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value(), typeMapper);
+ });
+ this.>add(ClassMapper.class, method, (anno, typeMapper) -> {
+ TabCompletionCache.add(typeMapper, cached);
+ (anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value().getName(), typeMapper);
+ });
+ this.>add(Validator.class, method, (anno, validator) -> {
+ (anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value(), validator);
+ });
+ this.>add(ClassValidator.class, method, (anno, validator) -> {
+ (anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value().getName(), validator);
+ });
+ }
+ for (Method method : methods) {
+ add(Register.class, method, true, (anno, parameters) -> {
+ for (int i = 1; i < parameters.length; i++) {
+ Parameter parameter = parameters[i];
+ Class> clazz = parameter.getType();
+ if (parameter.isVarArgs()) clazz = clazz.getComponentType();
+ Mapper mapper = parameter.getAnnotation(Mapper.class);
+ if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) {
+ continue;
+ }
+ String name = mapper != null ? mapper.value() : clazz.getTypeName();
+ if (!SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(name) && !localTypeMapper.containsKey(name)) {
+ commandSystemWarning(() -> "The parameter '" + parameter.toString() + "' is using an unsupported Mapper of type '" + name + "'");
+ return;
+ }
+ }
+ commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localValidators, anno.description(), anno.noTabComplete()));
+ });
+ }
+
+ if (dependencyMap.containsKey(this.getClass())) {
+ dependencyMap.get(this.getClass()).forEach(abstractSWCommand -> {
+ abstractSWCommand.localTypeMapper.putAll((Map) localTypeMapper);
+ abstractSWCommand.localValidators.putAll((Map) localValidators);
+ abstractSWCommand.initialisePartOf(this);
+ abstractSWCommand.initialize();
+ commandList.addAll((Collection) abstractSWCommand.commandList);
+ });
+ }
+
+ Collections.sort(commandList);
+ commandList.removeIf(subCommand -> {
+ if (subCommand.isHelp) {
+ helpCommandList.add(subCommand);
+ return true;
+ } else {
+ return false;
+ }
+ });
+ initialized = true;
+ }
+
+ private boolean validateMethod(Method method) {
+ if (!checkType(method.getAnnotations(), method.getReturnType(), false, annotation -> {
+ CommandMetaData.Method methodMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Method.class);
+ if (methodMetaData == null) return (aClass, varArg) -> true;
+ if (method.getParameterCount() > methodMetaData.maxParameterCount() || method.getParameterCount() < methodMetaData.minParameterCount())
+ return (aClass, varArg) -> false;
+ return (aClass, varArg) -> {
+ Class>[] types = methodMetaData.value();
+ if (types == null) return true;
+ for (Class> type : types) {
+ if (type.isAssignableFrom(aClass)) return true;
+ }
+ return false;
+ };
+ }, "The method '" + method + "'")) return false;
+ boolean valid = true;
+ for (Parameter parameter : method.getParameters()) {
+ if (!checkType(parameter.getAnnotations(), parameter.getType(), parameter.isVarArgs(), annotation -> {
+ CommandMetaData.Parameter parameterMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Parameter.class);
+ if (parameterMetaData == null) return (aClass, varArg) -> true;
+ Class> handler = parameterMetaData.handler();
+ if (BiPredicate.class.isAssignableFrom(handler)) {
+ try {
+ return (BiPredicate, Boolean>) handler.getConstructor().newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
+ NoSuchMethodException e) {
+ }
+ }
+ return (aClass, varArg) -> {
+ if (varArg) aClass = aClass.getComponentType();
+ Class>[] types = parameterMetaData.value();
+ if (types == null) return true;
+ for (Class> current : types) {
+ if (current.isAssignableFrom(aClass)) return true;
+ }
+ return false;
+ };
+ }, "The parameter '" + parameter + "'")) valid = false;
+ }
+ return valid;
+ }
+
+ private boolean checkType(Annotation[] annotations, Class> clazz, boolean varArg, Function, Boolean>> toApplicableTypes, String warning) {
+ boolean valid = true;
+ for (Annotation annotation : annotations) {
+ BiPredicate, Boolean> predicate = toApplicableTypes.apply(annotation);
+ if (!predicate.test(clazz, varArg)) {
+ commandSystemWarning(() -> warning + " is using an unsupported annotation of type '" + annotation.annotationType().getName() + "'");
+ valid = false;
+ }
+ }
+ return valid;
+ }
+
+ private void add(Class annotation, Method method, boolean firstParameter, BiConsumer consumer) {
+ T[] anno = SWCommandUtils.getAnnotation(method, annotation);
+ if (anno == null || anno.length == 0) return;
+
+ Parameter[] parameters = method.getParameters();
+ if (firstParameter && !clazz.isAssignableFrom(parameters[0].getType())) {
+ commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the first parameter of type '" + clazz.getTypeName() + "'");
+ return;
+ }
+ Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters));
+ }
+
+ private void add(Class annotation, Method method, BiConsumer consumer) {
+ add(annotation, method, false, (anno, parameters) -> {
+ try {
+ method.setAccessible(true);
+ consumer.accept(anno, (K) method.invoke(this));
+ } catch (Exception e) {
+ throw new SecurityException(e.getMessage(), e);
+ }
+ });
+ }
+
+ // TODO: Implement this when Message System is ready
+ /*
+ public void addDefaultHelpMessage(String message) {
+ defaultHelpMessages.add(message);
+ }
+ */
+
+ private List methods() {
+ List methods = new ArrayList<>();
+ Class> current = getClass();
+ while (current != AbstractSWCommand.class) {
+ methods.addAll(Arrays.asList(current.getDeclaredMethods()));
+ current = current.getSuperclass();
+ }
+ return methods;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.TYPE})
+ public @interface PartOf {
+ Class> value();
+ }
+
+ // --- Annotation for the command ---
+
+ /**
+ * Annotation for registering a method as a command
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ @Repeatable(Register.Registeres.class)
+ @CommandMetaData.Method(value = void.class, minParameterCount = 1)
+ protected @interface Register {
+
+ /**
+ * Identifier of subcommand
+ */
+ String[] value() default {};
+
+ @Deprecated
+ boolean help() default false;
+
+ String[] description() default {};
+
+ boolean noTabComplete() default false;
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = void.class, minParameterCount = 1)
+ @interface Registeres {
+ Register[] value();
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER, ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0)
+ @CommandMetaData.ImplicitTypeMapper(handler = Mapper.Handler.class)
+ protected @interface Mapper {
+ String value();
+
+ boolean local() default false;
+
+ class Handler implements AbstractTypeMapper {
+
+ private AbstractTypeMapper inner;
+
+ public Handler(AbstractSWCommand.Mapper mapper, Map> localTypeMapper) {
+ inner = (AbstractTypeMapper) SWCommandUtils.getTypeMapper(mapper.value(), localTypeMapper);
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ return inner.validate(sender, value, messageSender);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ return inner.tabCompletes(sender, previousArguments, s);
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0)
+ protected @interface ClassMapper {
+ Class> value();
+
+ boolean local() default false;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0)
+ protected @interface Cached {
+ long cacheDuration() default 5;
+
+ TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+ boolean global() default false;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER, ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0)
+ @CommandMetaData.ImplicitValidator(handler = Validator.Handler.class, order = 0)
+ protected @interface Validator {
+ String value() default "";
+
+ boolean local() default false;
+
+ boolean invert() default false;
+
+ class Handler implements AbstractValidator {
+
+ private AbstractValidator inner;
+ private boolean invert;
+
+ public Handler(AbstractSWCommand.Validator validator, Class> clazz, Map> localValidator) {
+ inner = (AbstractValidator) SWCommandUtils.getValidator(validator, clazz, localValidator);
+ invert = validator.invert();
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ return inner.validate(sender, value, messageSender) ^ invert;
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0)
+ protected @interface ClassValidator {
+ Class> value();
+
+ boolean local() default false;
+ }
+
+ // --- Implicit TypeMapper ---
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter({String.class, int.class, Integer.class, long.class, Long.class, boolean.class, Boolean.class})
+ @CommandMetaData.ImplicitTypeMapper(handler = StaticValue.Handler.class)
+ protected @interface StaticValue {
+ String[] value();
+
+ /**
+ * This is the short form for 'allowImplicitSwitchExpressions'
+ * and can be set to true if you want to allow int as well as boolean as annotated parameter types.
+ * The value array needs to be at least 2 long for this flag to be considered.
+ * While using an int, the value will represent the index into the value array.
+ * While using a boolean, the {@link #falseValues()} defines which indices are
+ * considered {@code false} or {@code true}.
+ */
+ boolean allowISE() default false;
+
+ int[] falseValues() default {0};
+
+ class Handler implements AbstractTypeMapper {
+
+ private AbstractTypeMapper inner;
+
+ public Handler(StaticValue staticValue, Class> clazz) {
+ if (clazz == String.class) {
+ inner = SWCommandUtils.createMapper(staticValue.value());
+ return;
+ }
+ if (!staticValue.allowISE()) {
+ throw new IllegalArgumentException("The parameter type '" + clazz.getTypeName() + "' is not supported by the StaticValue annotation");
+ }
+ if (clazz == boolean.class || clazz == Boolean.class) {
+ List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value()));
+ Set falseValues = new HashSet<>();
+ for (int i : staticValue.falseValues()) falseValues.add(i);
+ inner = SWCommandUtils.createMapper(s -> {
+ int index = tabCompletes.indexOf(s);
+ return index == -1 ? null : !falseValues.contains(index);
+ }, (commandSender, s) -> tabCompletes);
+ } else if (clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) {
+ List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value()));
+ inner = SWCommandUtils.createMapper(s -> {
+ Number index = tabCompletes.indexOf(s);
+ return index.longValue() == -1 ? null : index;
+ }, (commandSender, s) -> tabCompletes);
+ } else {
+ throw new IllegalArgumentException("The parameter type '" + clazz.getTypeName() + "' is not supported by the StaticValue annotation");
+ }
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ return inner.tabCompletes(sender, previousArguments, s);
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ protected @interface OptionalValue {
+ /**
+ * Will pe parsed against the TypeMapper specified by the parameter or annotation.
+ */
+ String value();
+
+ /**
+ * The method name stands for 'onlyUseIfNoneIsGiven'.
+ */
+ boolean onlyUINIG() default false;
+ }
+
+ // --- Implicit Validator ---
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.ImplicitValidator(handler = ErrorMessage.Handler.class, order = Integer.MAX_VALUE)
+ protected @interface ErrorMessage {
+ /**
+ * Error message to be displayed when the parameter is invalid.
+ */
+ String value();
+
+ /**
+ * This is the short form for 'allowEmptyArrays'.
+ */
+ boolean allowEAs() default true;
+
+ class Handler implements AbstractValidator {
+
+ private AbstractSWCommand.ErrorMessage errorMessage;
+
+ public Handler(AbstractSWCommand.ErrorMessage errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ if (value == null) messageSender.send(errorMessage.value());
+ if (!errorMessage.allowEAs() && value != null && value.getClass().isArray() && Array.getLength(value) == 0) {
+ messageSender.send(errorMessage.value());
+ return false;
+ }
+ return value != null;
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ protected @interface AllowNull {
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class})
+ @CommandMetaData.ImplicitValidator(handler = Min.Handler.class, order = 2)
+ protected @interface Min {
+ int intValue() default Integer.MIN_VALUE;
+
+ long longValue() default Long.MIN_VALUE;
+
+ float floatValue() default Float.MIN_VALUE;
+
+ double doubleValue() default Double.MIN_VALUE;
+
+ boolean inclusive() default true;
+
+ class Handler implements AbstractValidator {
+
+ private int value;
+ private Function comparator;
+
+ public Handler(AbstractSWCommand.Min min, Class> clazz) {
+ this.value = min.inclusive() ? 0 : 1;
+ this.comparator = createComparator("Min", clazz, min.intValue(), min.longValue(), min.floatValue(), min.doubleValue());
+ }
+
+ @Override
+ public boolean validate(T sender, Number value, MessageSender messageSender) {
+ if (value == null) return true;
+ return (comparator.apply(value).intValue()) >= this.value;
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class})
+ @CommandMetaData.ImplicitValidator(handler = Max.Handler.class, order = 2)
+ protected @interface Max {
+ int intValue() default Integer.MAX_VALUE;
+
+ long longValue() default Long.MAX_VALUE;
+
+ float floatValue() default Float.MAX_VALUE;
+
+ double doubleValue() default Double.MAX_VALUE;
+
+ boolean inclusive() default true;
+
+ class Handler implements AbstractValidator {
+
+ private int value;
+ private Function comparator;
+
+ public Handler(AbstractSWCommand.Max max, Class> clazz) {
+ this.value = max.inclusive() ? 0 : -1;
+ this.comparator = createComparator("Max", clazz, max.intValue(), max.longValue(), max.floatValue(), max.doubleValue());
+ }
+
+ @Override
+ public boolean validate(T sender, Number value, MessageSender messageSender) {
+ if (value == null) return true;
+ return (comparator.apply(value).intValue()) <= this.value;
+ }
+ }
+ }
+
+ private static Function createComparator(String type, Class> clazz, int iValue, long lValue, float fValue, double dValue) {
+ if (clazz == int.class || clazz == Integer.class) {
+ return number -> Integer.compare(number.intValue(), iValue);
+ } else if (clazz == long.class || clazz == Long.class) {
+ return number -> Long.compare(number.longValue(), lValue);
+ } else if (clazz == float.class || clazz == Float.class) {
+ return number -> Float.compare(number.floatValue(), fValue);
+ } else if (clazz == double.class || clazz == Double.class) {
+ return number -> Double.compare(number.doubleValue(), dValue);
+ } else {
+ throw new IllegalArgumentException(type + " annotation is not supported for " + clazz);
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.ImplicitTypeMapper(handler = Length.Handler.class)
+ protected @interface Length {
+ int min() default 0;
+
+ int max() default Integer.MAX_VALUE;
+
+ class Handler implements AbstractTypeMapper {
+
+ private int min;
+ private int max;
+ private AbstractTypeMapper inner;
+
+ public Handler(Length length, AbstractTypeMapper inner) {
+ this.min = length.min();
+ this.max = length.max();
+ this.inner = inner;
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ if (s.length() < min || s.length() > max) return null;
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ List tabCompletes = inner.tabCompletes(sender, previousArguments, s)
+ .stream()
+ .filter(str -> str.length() >= min)
+ .map(str -> str.substring(0, Math.min(str.length(), max)))
+ .collect(Collectors.toList());
+ if (s.length() < min) {
+ tabCompletes.add(0, s);
+ }
+ return tabCompletes;
+ }
+
+ @Override
+ public String normalize(T sender, String s) {
+ return inner.normalize(sender, s);
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter(handler = ArrayLength.Type.class)
+ @CommandMetaData.ImplicitTypeMapper(handler = ArrayLength.HandlerTypeMapper.class)
+ @CommandMetaData.ImplicitValidator(handler = ArrayLength.HandlerValidator.class, order = 1)
+ protected @interface ArrayLength {
+ int min() default 0;
+
+ int max() default Integer.MAX_VALUE;
+
+ class Type implements BiPredicate, Boolean> {
+ @Override
+ public boolean test(Class> clazz, Boolean isVarArgs) {
+ return clazz.isArray();
+ }
+ }
+
+ class HandlerTypeMapper implements AbstractTypeMapper {
+
+ private int max;
+ private AbstractTypeMapper inner;
+
+ public HandlerTypeMapper(ArrayLength arrayLength, AbstractTypeMapper inner) {
+ this.max = arrayLength.max();
+ this.inner = inner;
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ Object[] mapped = previousArguments.getMappedArg(0);
+ if (mapped.length >= max) return Collections.emptyList();
+ return inner.tabCompletes(sender, previousArguments, s);
+ }
+
+ @Override
+ public String normalize(T sender, String s) {
+ return inner.normalize(sender, s);
+ }
+ }
+
+ class HandlerValidator implements AbstractValidator {
+
+ private int min;
+ private int max;
+
+ public HandlerValidator(ArrayLength arrayLength) {
+ this.min = arrayLength.min();
+ this.max = arrayLength.max();
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ if (value == null) return true;
+ int length = Array.getLength(value);
+ return length >= min && length <= max;
+ }
+ }
+ }
+}
diff --git a/CommandFramework/src/de/steamwar/command/AbstractTypeMapper.java b/CommandFramework/src/de/steamwar/command/AbstractTypeMapper.java
new file mode 100644
index 00000000..040f3031
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/AbstractTypeMapper.java
@@ -0,0 +1,62 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+import java.util.Collection;
+
+public interface AbstractTypeMapper extends AbstractValidator {
+ /**
+ * The CommandSender can be null!
+ */
+ @Deprecated
+ default T map(K sender, String[] previousArguments, String s) {
+ throw new IllegalArgumentException("This method is deprecated and should not be used anymore!");
+ }
+
+ /**
+ * The CommandSender can be null!
+ */
+ default T map(K sender, PreviousArguments previousArguments, String s) {
+ return map(sender, previousArguments.userArgs, s);
+ }
+
+ @Override
+ default boolean validate(K sender, T value, MessageSender messageSender) {
+ return true;
+ }
+
+ @Deprecated
+ default Collection tabCompletes(K sender, String[] previousArguments, String s) {
+ throw new IllegalArgumentException("This method is deprecated and should not be used anymore!");
+ }
+
+ default Collection tabCompletes(K sender, PreviousArguments previousArguments, String s) {
+ return tabCompletes(sender, previousArguments.userArgs, s);
+ }
+
+ /**
+ * Normalize the cache key by sender and user provided argument.
+ * Note: The CommandSender can be null!
+ * Returning null and the empty string are equivalent.
+ */
+ default String normalize(K sender, String s) {
+ return null;
+ }
+}
diff --git a/CommandFramework/src/de/steamwar/command/AbstractValidator.java b/CommandFramework/src/de/steamwar/command/AbstractValidator.java
new file mode 100644
index 00000000..7dc0b72f
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/AbstractValidator.java
@@ -0,0 +1,96 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2022 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.function.BooleanSupplier;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+@FunctionalInterface
+public interface AbstractValidator {
+
+ /**
+ * Validates the given value.
+ *
+ * @param sender The sender of the command.
+ * @param value The value to validate or null if mapping returned null.
+ * @param messageSender The message sender to send messages to the player. Never send messages directly to the player.
+ * @return The result of the validation.
+ */
+ boolean validate(K sender, T value, MessageSender messageSender);
+
+ @Deprecated
+ default Validator validate(C value, MessageSender messageSender) {
+ return new Validator<>(value, messageSender);
+ }
+
+ @Deprecated
+ @RequiredArgsConstructor
+ class Validator {
+ private final C value;
+ private final MessageSender messageSender;
+
+ private boolean valid = true;
+
+ public Validator map(Function mapper) {
+ return new Validator<>(mapper.apply(value), messageSender).set(valid);
+ }
+
+ public Validator set(boolean value) {
+ this.valid = value;
+ return this;
+ }
+
+ public Validator and(Predicate predicate) {
+ valid &= predicate.test(value);
+ return this;
+ }
+
+ public Validator or(Predicate predicate) {
+ valid |= predicate.test(value);
+ return this;
+ }
+
+ public Validator errorMessage(String s, Object... args) {
+ if (!valid) messageSender.send(s, args);
+ return this;
+ }
+
+ public boolean result() {
+ return valid;
+ }
+ }
+
+ @FunctionalInterface
+ interface MessageSender {
+ void send(String s, Object... args);
+
+ default boolean send(boolean condition, String s, Object... args) {
+ if (condition) send(s, args);
+ return condition;
+ }
+
+ default boolean send(BooleanSupplier condition, String s, Object... args) {
+ return send(condition.getAsBoolean(), s, args);
+ }
+ }
+}
diff --git a/CommandFramework/src/de/steamwar/command/CommandFrameworkException.java b/CommandFramework/src/de/steamwar/command/CommandFrameworkException.java
new file mode 100644
index 00000000..ea3d8356
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/CommandFrameworkException.java
@@ -0,0 +1,110 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Executable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class CommandFrameworkException extends RuntimeException {
+
+ private Function causeMessage;
+ private Throwable cause;
+ private Function stackTraceExtractor;
+ private String extraStackTraces;
+
+ private String message;
+
+ static CommandFrameworkException commandPartExceptions(String type, Throwable cause, String current, Class> clazzType, Executable executable, int index) {
+ return new CommandFrameworkException(e -> {
+ return CommandFrameworkException.class.getTypeName() + ": Error while " + type + " (" + current + ") to type " + clazzType.getTypeName() + " with parameter index " + index;
+ }, cause, exception -> {
+ StackTraceElement[] stackTraceElements = exception.getStackTrace();
+ int last = 0;
+ for (int i = 0; i < stackTraceElements.length; i++) {
+ if (stackTraceElements[i].getClassName().equals(CommandPart.class.getTypeName())) {
+ last = i;
+ break;
+ }
+ }
+ return Arrays.stream(stackTraceElements).limit(last - 1);
+ }, executable.getDeclaringClass().getTypeName() + "." + executable.getName() + "(Unknown Source)");
+ }
+
+ CommandFrameworkException(InvocationTargetException invocationTargetException, String alias, String[] args) {
+ this(e -> {
+ StringBuilder st = new StringBuilder();
+ st.append(e.getCause().getClass().getTypeName());
+ if (e.getCause().getMessage() != null) {
+ st.append(": ").append(e.getCause().getMessage());
+ }
+ if (alias != null && !alias.isEmpty()) {
+ st.append("\n").append("Performed command: " + alias + " " + String.join(" ", args));
+ }
+ return st.toString();
+ }, invocationTargetException, e -> {
+ StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
+ return Arrays.stream(stackTraceElements).limit(stackTraceElements.length - e.getStackTrace().length);
+ }, null);
+ }
+
+ private CommandFrameworkException(Function causeMessage, T cause, Function> stackTraceExtractor, String extraStackTraces) {
+ super(causeMessage.apply(cause), cause);
+ this.causeMessage = causeMessage;
+ this.cause = cause;
+ this.stackTraceExtractor = stackTraceExtractor;
+ this.extraStackTraces = extraStackTraces;
+ }
+
+ public synchronized String getBuildStackTrace() {
+ if (message != null) {
+ return message;
+ }
+ StringBuilder st = new StringBuilder();
+ st.append(causeMessage.apply(cause)).append("\n");
+ ((Stream) stackTraceExtractor.apply(cause)).forEach(stackTraceElement -> {
+ st.append("\tat ").append(stackTraceElement.toString()).append("\n");
+ });
+ if (extraStackTraces != null) {
+ st.append("\tat ").append(extraStackTraces).append("\n");
+ }
+ message = st.toString();
+ return message;
+ }
+
+ @Override
+ public void printStackTrace() {
+ printStackTrace(System.err);
+ }
+
+ @Override
+ public void printStackTrace(PrintStream s) {
+ s.print(getBuildStackTrace());
+ }
+
+ @Override
+ public void printStackTrace(PrintWriter s) {
+ s.print(getBuildStackTrace());
+ }
+}
diff --git a/CommandFramework/src/de/steamwar/command/CommandMetaData.java b/CommandFramework/src/de/steamwar/command/CommandMetaData.java
new file mode 100644
index 00000000..33909ac7
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/CommandMetaData.java
@@ -0,0 +1,92 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2022 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+import java.lang.annotation.*;
+
+public @interface CommandMetaData {
+
+ /**
+ * This annotation is only for internal use.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface Method {
+ Class>[] value();
+ int minParameterCount() default 0;
+ int maxParameterCount() default Integer.MAX_VALUE;
+ }
+
+ /**
+ * This annotation denotes what types are allowed as parameter types the annotation annotated with can use.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface Parameter {
+ Class>[] value() default {};
+ Class> handler() default void.class;
+ }
+
+ /**
+ * This annotation can be used in conjunction with a class that implements {@link AbstractTypeMapper} to
+ * create a custom type mapper for a parameter. The class must have one of two constructors with the following
+ * types:
+ *
+ *
Annotation this annotation annotates
+ *
{@link Class}
+ *
{@link AbstractTypeMapper}, optional, if not present only one per parameter
+ *
{@link java.util.Map} with types {@link String} and {@link AbstractValidator}
+ *
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface ImplicitTypeMapper {
+ /**
+ * The validator class that should be used.
+ */
+ Class> handler();
+ }
+
+ /**
+ * This annotation can be used in conjunction with a class that implements {@link AbstractValidator} to
+ * create a custom validator short hands for commands. The validator class must have one constructor with
+ * one of the following types:
+ *
+ *
Annotation this annotation annotates
+ *
{@link Class}
+ *
{@link java.util.Map} with types {@link String} and {@link AbstractValidator}
+ *
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface ImplicitValidator {
+ /**
+ * The validator class that should be used.
+ */
+ Class> handler();
+
+ /**
+ * Defines when this validator should be processed. Negative numbers denote that this will be
+ * processed before {@link AbstractSWCommand.Validator} and positive numbers
+ * denote that this will be processed after {@link AbstractSWCommand.Validator}.
+ */
+ int order();
+ }
+}
diff --git a/CommandFramework/src/de/steamwar/command/CommandParseException.java b/CommandFramework/src/de/steamwar/command/CommandParseException.java
new file mode 100644
index 00000000..3d81ea69
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/CommandParseException.java
@@ -0,0 +1,26 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+public class CommandParseException extends RuntimeException {
+
+ public CommandParseException() {
+ }
+}
diff --git a/CommandFramework/src/de/steamwar/command/CommandPart.java b/CommandFramework/src/de/steamwar/command/CommandPart.java
new file mode 100644
index 00000000..02991b40
--- /dev/null
+++ b/CommandFramework/src/de/steamwar/command/CommandPart.java
@@ -0,0 +1,226 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.command;
+
+import lombok.AllArgsConstructor;
+import lombok.Setter;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+class CommandPart {
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
+
+ @AllArgsConstructor
+ private static class CheckArgumentResult {
+ private final boolean success;
+ private final Object value;
+ }
+
+ private AbstractSWCommand command;
+ private AbstractTypeMapper typeMapper;
+ private List> validators = new ArrayList<>();
+ private Class> varArgType;
+ private String optional;
+
+ private CommandPart next = null;
+
+ @Setter
+ private boolean ignoreAsArgument = false;
+
+ @Setter
+ private boolean onlyUseIfNoneIsGiven = false;
+
+ private Parameter parameter;
+ private int parameterIndex;
+
+ public CommandPart(AbstractSWCommand command, AbstractTypeMapper typeMapper, Class> varArgType, String optional, Parameter parameter, int parameterIndex) {
+ this.command = command;
+ this.typeMapper = typeMapper;
+ this.varArgType = varArgType;
+ this.optional = optional;
+ this.parameter = parameter;
+ this.parameterIndex = parameterIndex;
+
+ if (optional != null && varArgType != null) {
+ throw new IllegalArgumentException("A vararg part can't have an optional part! In method " + parameter.getDeclaringExecutable() + " with parameter " + parameterIndex);
+ }
+ }
+
+ void addValidator(AbstractValidator validator) {
+ if (validator == null) return;
+ validators.add(validator);
+ }
+
+ public void setNext(CommandPart next) {
+ if (varArgType != null) {
+ throw new IllegalArgumentException("There can't be a next part if this is a vararg part! In method " + parameter.getDeclaringExecutable() + " with parameter " + parameterIndex);
+ }
+ this.next = next;
+ }
+
+ public boolean isHelp() {
+ if (next == null) {
+ if (varArgType == null) {
+ return false;
+ }
+ if (varArgType != String.class) {
+ return false;
+ }
+ return typeMapper == SWCommandUtils.STRING_MAPPER;
+ } else {
+ return next.isHelp();
+ }
+ }
+
+ public void generateArgumentArray(Consumer errors, List