From 45ddd8cb5a8ae785b16f8c0a68aea1509ff605a5 Mon Sep 17 00:00:00 2001 From: yoyosource Date: Thu, 8 Jul 2021 22:07:11 +0200 Subject: [PATCH] CMDoS because Brigadier --- .../de/steamwar/command/Dispatcher_14.java | 31 ++ .../de/steamwar/command/Dispatcher_15.java | 32 ++ SpigotCore_Main/pom.xml | 18 + .../src/de/steamwar/command/SWCommand.java | 178 +++------- .../steamwar/command/SWCommandBrigadier.java | 333 ++++++++++++++++++ .../steamwar/command/SWCommandInterface.java | 25 ++ .../de/steamwar/command/SWCommandNormal.java | 290 +++++++++++++++ .../src/de/steamwar/command/SubCommand.java | 149 -------- 8 files changed, 780 insertions(+), 276 deletions(-) create mode 100644 SpigotCore_14/src/de/steamwar/command/Dispatcher_14.java create mode 100644 SpigotCore_15/src/de/steamwar/command/Dispatcher_15.java create mode 100644 SpigotCore_Main/src/de/steamwar/command/SWCommandBrigadier.java create mode 100644 SpigotCore_Main/src/de/steamwar/command/SWCommandInterface.java create mode 100644 SpigotCore_Main/src/de/steamwar/command/SWCommandNormal.java delete mode 100644 SpigotCore_Main/src/de/steamwar/command/SubCommand.java diff --git a/SpigotCore_14/src/de/steamwar/command/Dispatcher_14.java b/SpigotCore_14/src/de/steamwar/command/Dispatcher_14.java new file mode 100644 index 0000000..c2935b9 --- /dev/null +++ b/SpigotCore_14/src/de/steamwar/command/Dispatcher_14.java @@ -0,0 +1,31 @@ +/* + * 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 com.mojang.brigadier.CommandDispatcher; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_14_R1.CraftServer; + +public class Dispatcher_14 { + + static CommandDispatcher getDispatcher() { + return ((CraftServer) Bukkit.getServer()).getServer().getCommandDispatcher().a(); + } +} diff --git a/SpigotCore_15/src/de/steamwar/command/Dispatcher_15.java b/SpigotCore_15/src/de/steamwar/command/Dispatcher_15.java new file mode 100644 index 0000000..363ae82 --- /dev/null +++ b/SpigotCore_15/src/de/steamwar/command/Dispatcher_15.java @@ -0,0 +1,32 @@ +/* + * 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 com.mojang.brigadier.CommandDispatcher; +import net.minecraft.server.v1_15_R1.CommandListenerWrapper; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_15_R1.CraftServer; + +public class Dispatcher_15 { + + static CommandDispatcher getDispatcher() { + return ((CraftServer) Bukkit.getServer()).getServer().getCommandDispatcher().a(); + } +} diff --git a/SpigotCore_Main/pom.xml b/SpigotCore_Main/pom.xml index 0d23f6f..81c3416 100644 --- a/SpigotCore_Main/pom.xml +++ b/SpigotCore_Main/pom.xml @@ -19,6 +19,11 @@ codemc-snapshots https://repo.codemc.io/repository/maven-snapshots/ + + minecraft-libraries + Minecraft Libraries + https://libraries.minecraft.net + SpigotCore_Main @@ -50,6 +55,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + spigotcore @@ -124,5 +137,10 @@ 2.0 compile + + com.mojang + brigadier + 1.0.17 + \ No newline at end of file diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java index ab72fc5..e9f9914 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java @@ -19,151 +19,45 @@ package de.steamwar.command; -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; +import com.mojang.brigadier.CommandDispatcher; +import de.steamwar.core.VersionedCallable; import java.lang.annotation.*; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.IntPredicate; -import java.util.logging.Level; -import java.util.stream.Collectors; public abstract class SWCommand { - private final Command command; - private final List commandList = new ArrayList<>(); - private final List commandHelpList = new ArrayList<>(); - private final Map> localTypeMapper = new HashMap<>(); + static final CommandDispatcher dispatcher; - protected SWCommand(String command) { - this(command, new String[0]); + static { + dispatcher = VersionedCallable.call(new VersionedCallable<>(() -> null, 12), + new VersionedCallable<>(Dispatcher_14::getDispatcher, 14), + new VersionedCallable<>(Dispatcher_15::getDispatcher, 15)); + } + + private SWCommandInterface swCommandInterface; + + protected SWCommand(String command, boolean noBrigadier) { + this(command, noBrigadier, new String[0]); } protected SWCommand(String command, String... aliases) { - this.command = new Command(command, "", "/" + command, Arrays.asList(aliases)) { - @Override - public boolean execute(CommandSender sender, String alias, String[] args) { - if (commandList.stream().anyMatch(s -> s.invoke(sender, args))) return false; - commandHelpList.stream().anyMatch(s -> s.invoke(sender, args)); - return false; - } - - @Override - public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { - String string = args[args.length - 1].toLowerCase(); - return commandList.stream() - .map(s -> s.tabComplete(sender, args)) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(s -> !s.isEmpty()) - .filter(s -> s.toLowerCase().startsWith(string)) - .collect(Collectors.toList()); - } - }; - unregister(); - register(); - - Method[] methods = getClass().getDeclaredMethods(); - for (Method method : methods) { - addMapper(Mapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { - (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value(), typeMapper); - }); - addMapper(ClassMapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { - (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value().getTypeName(), typeMapper); - }); - add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { - if (!anno.help()) return; - if (parameters.length != 2) { - Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking parameters or has too many"); - } - if (!parameters[parameters.length - 1].isVarArgs()) { - Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the varArgs parameters as last Argument"); - } - if (parameters[parameters.length - 1].getType().getComponentType() != String.class) { - Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the varArgs parameters of type '" + String.class.getTypeName() + "' as last Argument"); - return; - } - commandHelpList.add(new SubCommand(this, method, anno.value(), new HashMap<>())); - }); - } - for (Method method : methods) { - add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { - if (anno.help()) return; - for (int i = 1; i < parameters.length; i++) { - Parameter parameter = parameters[i]; - Class clazz = parameter.getType(); - if (parameter.isVarArgs() && i == parameters.length - 1) { - clazz = parameter.getType().getComponentType(); - } - Mapper mapper = parameter.getAnnotation(Mapper.class); - if (clazz.isEnum() && mapper == null && !SWCommandUtils.MAPPER_FUNCTIONS.containsKey(clazz.getTypeName())) { - continue; - } - String name = mapper != null ? mapper.value() : clazz.getTypeName(); - if (!SWCommandUtils.MAPPER_FUNCTIONS.containsKey(name) && !localTypeMapper.containsKey(name)) { - Bukkit.getLogger().log(Level.WARNING, "The parameter '" + parameter.toString() + "' is using an unsupported Mapper of type '" + name + "'"); - return; - } - } - commandList.add(new SubCommand(this, method, anno.value(), localTypeMapper)); - }); - - this.commandList.sort((o1, o2) -> { - int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length); - if (compare != 0) { - return compare; - } else { - return Integer.compare(o1.varArgType != null ? Integer.MAX_VALUE : o1.arguments.length, - o2.varArgType != null ? Integer.MAX_VALUE : o2.arguments.length); - } - }); - commandHelpList.sort(Comparator.comparingInt(o -> -o.subCommand.length)); - } + this(command, false, aliases); } - private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { - T[] anno = SWCommandUtils.getAnnotation(method, annotation); - if (anno == null || anno.length == 0) return; - - Parameter[] parameters = method.getParameters(); - if (!parameterTester.test(parameters.length)) { - Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking parameters or has too many"); - return; + protected SWCommand(String command, boolean noBrigadier, String... aliases) { + if (dispatcher != null && !noBrigadier) { + swCommandInterface = new SWCommandBrigadier(this, command, aliases); + } else { + swCommandInterface = new SWCommandNormal(this, command, aliases); } - if (firstParameter && !CommandSender.class.isAssignableFrom(parameters[0].getType())) { - Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the first parameter of type '" + CommandSender.class.getTypeName() + "'"); - return; - } - if (returnType != null && method.getReturnType() != returnType) { - Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the desired return type '" + returnType.getTypeName() + "'"); - return; - } - Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters)); - } - - private void addMapper(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer> consumer) { - add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { - try { - method.setAccessible(true); - consumer.accept(anno, (TypeMapper) method.invoke(this)); - } catch (Exception e) { - throw new SecurityException(e.getMessage(), e); - } - }); } public void unregister() { - SWCommandUtils.knownCommandMap.remove(command.getName()); - command.getAliases().forEach(SWCommandUtils.knownCommandMap::remove); - command.unregister(SWCommandUtils.commandMap); + swCommandInterface.unregister(); } public void register() { - SWCommandUtils.commandMap.register("steamwar", this.command); + swCommandInterface.register(); } @Retention(RetentionPolicy.RUNTIME) @@ -196,4 +90,34 @@ public abstract class SWCommand { boolean local() default false; } + + + // Used for Brigadier, as parse hints and TabComplete hints + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + protected @interface IntRange { + int min(); + int max(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + protected @interface LongRange { + long min(); + long max(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + protected @interface FloatRange { + float min(); + float max(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + protected @interface DoubleRange { + double min(); + double max(); + } } diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommandBrigadier.java b/SpigotCore_Main/src/de/steamwar/command/SWCommandBrigadier.java new file mode 100644 index 0000000..ecad6b3 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandBrigadier.java @@ -0,0 +1,333 @@ +/* + * 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 com.mojang.brigadier.arguments.*; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.function.Predicate; +import java.util.logging.Level; + +import static de.steamwar.command.SWCommandUtils.*; + +class SWCommandBrigadier implements SWCommandInterface { + + private SWCommandNormal swCommandNormal = null; + private final SWCommand swCommand; + private final List commandList = new ArrayList<>(); + private final List commandHelpList = new ArrayList<>(); + private final Map> localTypeMapper = new HashMap<>(); + + protected SWCommandBrigadier(SWCommand swCommand, String command, String... aliases) { + this.swCommand = swCommand; + + Method[] methods = swCommand.getClass().getDeclaredMethods(); + for (Method method : methods) { + addMapper(SWCommand.Mapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { + (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value(), typeMapper); + }); + addMapper(SWCommand.ClassMapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { + (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value().getTypeName(), typeMapper); + }); + add(SWCommand.Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { + if (!anno.help()) return; + if (parameters.length != 2) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking parameters or has too many"); + } + if (!parameters[parameters.length - 1].isVarArgs()) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the varArgs parameters as last Argument"); + } + if (parameters[parameters.length - 1].getType().getComponentType() != String.class) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the varArgs parameters of type '" + String.class.getTypeName() + "' as last Argument"); + return; + } + commandHelpList.add(new SubCommand(swCommand, method, anno.value(), new HashMap<>())); + }); + } + for (Method method : methods) { + add(SWCommand.Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { + if (anno.help()) return; + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + Class clazz = parameter.getType(); + if (parameter.isVarArgs() && i == parameters.length - 1) { + clazz = parameter.getType().getComponentType(); + } + SWCommand.Mapper mapper = parameter.getAnnotation(SWCommand.Mapper.class); + if (clazz.isEnum() && mapper == null && !SWCommandUtils.MAPPER_FUNCTIONS.containsKey(clazz.getTypeName())) { + continue; + } + String name = mapper != null ? mapper.value() : clazz.getTypeName(); + if (!SWCommandUtils.MAPPER_FUNCTIONS.containsKey(name) && !localTypeMapper.containsKey(name)) { + Bukkit.getLogger().log(Level.WARNING, "The parameter '" + parameter + "' is using an unsupported Mapper of type '" + name + "'"); + return; + } + } + commandList.add(new SubCommand(swCommand, method, anno.value(), new HashMap<>())); + }); + } + + for (SubCommand subCommand : commandList) { + register(command, subCommand, command, aliases); + for (String s : aliases) { + register(s, subCommand, command, aliases); + } + } + for (SubCommand subCommand : commandHelpList) { + register(command, subCommand, command, aliases); + for (String s : aliases) { + register(s, subCommand, command, aliases); + } + } + } + + private void register(String name, SubCommand subCommand, String command, String... aliases) { + LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder.literal(name); + if (subCommand.argumentNode == null) { + return; + } + if (subCommand.normalTabCompleteNeeded) { + swCommandNormal = new SWCommandNormal(swCommand, command, aliases); + } + literalArgumentBuilder.then(subCommand.argumentNode); + SWCommand.dispatcher.register(literalArgumentBuilder); + } + + private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { + T[] anno = SWCommandUtils.getAnnotation(method, annotation); + if (anno == null || anno.length == 0) return; + + Parameter[] parameters = method.getParameters(); + if (!parameterTester.test(parameters.length)) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method + "' is lacking parameters or has too many"); + return; + } + if (firstParameter && !CommandSender.class.isAssignableFrom(parameters[0].getType())) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method + "' is lacking the first parameter of type '" + CommandSender.class.getTypeName() + "'"); + return; + } + if (returnType != null && method.getReturnType() != returnType) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method + "' is lacking the desired return type '" + returnType.getTypeName() + "'"); + return; + } + Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters)); + } + + private void addMapper(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer> consumer) { + add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { + try { + method.setAccessible(true); + consumer.accept(anno, (TypeMapper) method.invoke(swCommand)); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + }); + } + + @Override + public void unregister() { + if (swCommandNormal == null) return; + swCommandNormal.unregister(); + } + + @Override + public void register() { + if (swCommandNormal == null) return; + swCommandNormal.register(); + } + + static class SubCommand { + + ArgumentBuilder argumentNode = null; + boolean normalTabCompleteNeeded = false; + + private SWCommand swCommand; + private Method method; + String[] subCommand; + TypeMapper[] arguments; + private Predicate commandSenderPredicate; + private Function commandSenderFunction; + Class varArgType = null; + + SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper) { + this.swCommand = swCommand; + this.method = method; + + Parameter[] parameters = method.getParameters(); + commandSenderPredicate = sender -> parameters[0].getType().isAssignableFrom(sender.getClass()); + commandSenderFunction = sender -> parameters[0].getType().cast(sender); + this.subCommand = subCommand; + + arguments = new TypeMapper[parameters.length - 1]; + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + Class clazz = parameter.getType(); + if (parameter.isVarArgs()) { + clazz = clazz.getComponentType(); + varArgType = clazz; + } + + SWCommand.Mapper mapper = parameter.getAnnotation(SWCommand.Mapper.class); + if (clazz.isEnum() && mapper == null && !MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) { + Class> enumClass = (Class>) clazz; + List tabCompletes = new ArrayList<>(); + for (Enum enumConstant : enumClass.getEnumConstants()) { + tabCompletes.add(enumConstant.name().toLowerCase()); + } + arguments[i - 1] = SWCommandUtils.createMapper(s -> ENUM_MAPPER.apply(enumClass, s), s -> tabCompletes); + continue; + } + + String name = clazz.getTypeName(); + if (mapper != null) { + name = mapper.value(); + } + arguments[i - 1] = localTypeMapper.containsKey(name) + ? localTypeMapper.get(name) + : MAPPER_FUNCTIONS.getOrDefault(name, ERROR_FUNCTION); + } + + + ArgumentBuilder argumentBuilder = null; + for (String s : subCommand) { + LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder.literal(s); + if (argumentBuilder != null) { + argumentBuilder.then(literalArgumentBuilder); + } else { + argumentNode = literalArgumentBuilder; + } + argumentBuilder = literalArgumentBuilder; + } + + for (int i = 0; i < arguments.length - (varArgType != null ? 1 : 0); i++) { + Parameter parameter = parameters[i + 1]; + Class parameterType = parameter.getType(); + ArgumentType argumentType = getArgumentType(parameter, parameterType, arguments[i]); + + RequiredArgumentBuilder requiredArgumentBuilder = RequiredArgumentBuilder.argument(parameter.getName(), argumentType); + if (argumentBuilder != null) { + argumentBuilder.then(requiredArgumentBuilder); + } else { + argumentNode = requiredArgumentBuilder; + } + argumentBuilder = requiredArgumentBuilder; + if (i == arguments.length - 1) { + argumentBuilder.executes(commandContext -> { + invoke((CommandSender) commandContext.getCommand(), commandContext.getInput().split(" ")); + return 0; + }); + } + } + if (varArgType != null) { + // TODO: UNSUPORTED + /*Parameter parameter = parameters[parameters.length - 1]; + Class parameterType = parameter.getType(); + TypeMapper typeMapper = arguments[arguments.length - 1]; + ArgumentType argumentType = getArgumentType(parameter, parameterType, typeMapper); + + RequiredArgumentBuilder requiredArgumentBuilder = RequiredArgumentBuilder.argument(parameter.getName(), argumentType); + if (argumentBuilder != null) { + argumentBuilder.then(requiredArgumentBuilder); + } else { + argumentNode = requiredArgumentBuilder; + } + argumentBuilder = requiredArgumentBuilder; + argumentBuilder.executes(commandContext -> { + invoke((CommandSender) commandContext.getCommand(), commandContext.getInput().split(" ")); + return 0; + }); + CommandNode commandNode = argumentBuilder.build(); + argumentBuilder.redirect(commandNode);*/ + } + } + + boolean invoke(CommandSender commandSender, String[] args) { + if (args.length < arguments.length + subCommand.length - (varArgType != null ? 1 : 0)) { + return false; + } + if (varArgType == null && args.length > arguments.length + subCommand.length) { + return false; + } + try { + if (!commandSenderPredicate.test(commandSender)) { + return false; + } + Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, arguments, args, varArgType, subCommand); + objects[0] = commandSenderFunction.apply(commandSender); + method.setAccessible(true); + method.invoke(swCommand, objects); + } catch (IllegalAccessException | RuntimeException | InvocationTargetException e) { + throw new SecurityException(e.getMessage(), e); + } catch (CommandParseException e) { + return false; + } + return true; + } + + private ArgumentType getArgumentType(Parameter parameter, Class parameterType, TypeMapper typeMapper) { + ArgumentType argumentType; + if (parameterType == int.class || parameterType == Integer.class) { + SWCommand.IntRange intRange = parameter.getAnnotation(SWCommand.IntRange.class); + if (intRange != null) { + argumentType = IntegerArgumentType.integer(intRange.min(), intRange.max()); + } else { + argumentType = IntegerArgumentType.integer(); + } + } else if (parameterType == float.class || parameterType == Float.class) { + SWCommand.FloatRange floatRange = parameter.getAnnotation(SWCommand.FloatRange.class); + if (floatRange != null) { + argumentType = FloatArgumentType.floatArg(floatRange.min(), floatRange.max()); + } else { + argumentType = FloatArgumentType.floatArg(); + } + } else if (parameterType == long.class || parameterType == Long.class) { + SWCommand.LongRange longRange = parameter.getAnnotation(SWCommand.LongRange.class); + if (longRange != null) { + argumentType = LongArgumentType.longArg(longRange.min(), longRange.max()); + } else { + argumentType = LongArgumentType.longArg(); + } + } else if (parameterType == double.class || parameterType == Double.class) { + SWCommand.DoubleRange doubleRange = parameter.getAnnotation(SWCommand.DoubleRange.class); + if (doubleRange != null) { + argumentType = DoubleArgumentType.doubleArg(doubleRange.min(), doubleRange.max()); + } else { + argumentType = DoubleArgumentType.doubleArg(); + } + } else { + argumentType = StringArgumentType.string(); + normalTabCompleteNeeded = true; + } + return argumentType; + } + } +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommandInterface.java b/SpigotCore_Main/src/de/steamwar/command/SWCommandInterface.java new file mode 100644 index 0000000..4ba0844 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandInterface.java @@ -0,0 +1,25 @@ +/* + * 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 interface SWCommandInterface { + void unregister(); + void register(); +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommandNormal.java b/SpigotCore_Main/src/de/steamwar/command/SWCommandNormal.java new file mode 100644 index 0000000..144f867 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandNormal.java @@ -0,0 +1,290 @@ +/* + * 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 org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import static de.steamwar.command.SWCommandUtils.*; + +class SWCommandNormal implements SWCommandInterface { + + private final SWCommand swCommand; + private final Command command; + private final List commandList = new ArrayList<>(); + private final List commandHelpList = new ArrayList<>(); + private final Map> localTypeMapper = new HashMap<>(); + + protected SWCommandNormal(SWCommand swCommand, String command, String... aliases) { + this.swCommand = swCommand; + + this.command = new Command(command, "", "/" + command, Arrays.asList(aliases)) { + @Override + public boolean execute(CommandSender sender, String alias, String[] args) { + if (commandList.stream().anyMatch(s -> s.invoke(sender, args))) return false; + commandHelpList.stream().anyMatch(s -> s.invoke(sender, args)); + return false; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { + String string = args[args.length - 1].toLowerCase(); + return commandList.stream() + .map(s -> s.tabComplete(sender, args)) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(s -> !s.isEmpty()) + .filter(s -> s.toLowerCase().startsWith(string)) + .collect(Collectors.toList()); + } + }; + unregister(); + register(); + + Method[] methods = swCommand.getClass().getDeclaredMethods(); + for (Method method : methods) { + addMapper(SWCommand.Mapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { + (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value(), typeMapper); + }); + addMapper(SWCommand.ClassMapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { + (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value().getTypeName(), typeMapper); + }); + add(SWCommand.Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { + if (!anno.help()) return; + if (parameters.length != 2) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking parameters or has too many"); + } + if (!parameters[parameters.length - 1].isVarArgs()) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the varArgs parameters as last Argument"); + } + if (parameters[parameters.length - 1].getType().getComponentType() != String.class) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method.toString() + "' is lacking the varArgs parameters of type '" + String.class.getTypeName() + "' as last Argument"); + return; + } + commandHelpList.add(new SubCommand(swCommand, method, anno.value(), new HashMap<>())); + }); + } + for (Method method : methods) { + add(SWCommand.Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { + if (anno.help()) return; + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + Class clazz = parameter.getType(); + if (parameter.isVarArgs() && i == parameters.length - 1) { + clazz = parameter.getType().getComponentType(); + } + SWCommand.Mapper mapper = parameter.getAnnotation(SWCommand.Mapper.class); + if (clazz.isEnum() && mapper == null && !SWCommandUtils.MAPPER_FUNCTIONS.containsKey(clazz.getTypeName())) { + continue; + } + String name = mapper != null ? mapper.value() : clazz.getTypeName(); + if (!SWCommandUtils.MAPPER_FUNCTIONS.containsKey(name) && !localTypeMapper.containsKey(name)) { + Bukkit.getLogger().log(Level.WARNING, "The parameter '" + parameter + "' is using an unsupported Mapper of type '" + name + "'"); + return; + } + } + commandList.add(new SubCommand(swCommand, method, anno.value(), localTypeMapper)); + }); + + this.commandList.sort((o1, o2) -> { + int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length); + if (compare != 0) { + return compare; + } else { + return Integer.compare(o1.varArgType != null ? Integer.MAX_VALUE : o1.arguments.length, + o2.varArgType != null ? Integer.MAX_VALUE : o2.arguments.length); + } + }); + commandHelpList.sort(Comparator.comparingInt(o -> -o.subCommand.length)); + } + } + + private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { + T[] anno = SWCommandUtils.getAnnotation(method, annotation); + if (anno == null || anno.length == 0) return; + + Parameter[] parameters = method.getParameters(); + if (!parameterTester.test(parameters.length)) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method + "' is lacking parameters or has too many"); + return; + } + if (firstParameter && !CommandSender.class.isAssignableFrom(parameters[0].getType())) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method + "' is lacking the first parameter of type '" + CommandSender.class.getTypeName() + "'"); + return; + } + if (returnType != null && method.getReturnType() != returnType) { + Bukkit.getLogger().log(Level.WARNING, "The method '" + method + "' is lacking the desired return type '" + returnType.getTypeName() + "'"); + return; + } + Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters)); + } + + private void addMapper(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer> consumer) { + add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { + try { + method.setAccessible(true); + consumer.accept(anno, (TypeMapper) method.invoke(swCommand)); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + }); + } + + public void unregister() { + SWCommandUtils.knownCommandMap.remove(command.getName()); + command.getAliases().forEach(SWCommandUtils.knownCommandMap::remove); + command.unregister(SWCommandUtils.commandMap); + } + + public void register() { + SWCommandUtils.commandMap.register("steamwar", this.command); + } + + static class SubCommand { + + private SWCommand swCommand; + private Method method; + String[] subCommand; + TypeMapper[] arguments; + private Predicate commandSenderPredicate; + private Function commandSenderFunction; + Class varArgType = null; + + SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper) { + this.swCommand = swCommand; + this.method = method; + + Parameter[] parameters = method.getParameters(); + commandSenderPredicate = sender -> parameters[0].getType().isAssignableFrom(sender.getClass()); + commandSenderFunction = sender -> parameters[0].getType().cast(sender); + this.subCommand = subCommand; + + arguments = new TypeMapper[parameters.length - 1]; + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + Class clazz = parameter.getType(); + if (parameter.isVarArgs()) { + clazz = clazz.getComponentType(); + varArgType = clazz; + } + + SWCommand.Mapper mapper = parameter.getAnnotation(SWCommand.Mapper.class); + if (clazz.isEnum() && mapper == null && !MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) { + Class> enumClass = (Class>) clazz; + List tabCompletes = new ArrayList<>(); + for (Enum enumConstant : enumClass.getEnumConstants()) { + tabCompletes.add(enumConstant.name().toLowerCase()); + } + arguments[i - 1] = SWCommandUtils.createMapper(s -> ENUM_MAPPER.apply(enumClass, s), s -> tabCompletes); + continue; + } + + String name = clazz.getTypeName(); + if (mapper != null) { + name = mapper.value(); + } + arguments[i - 1] = localTypeMapper.containsKey(name) + ? localTypeMapper.get(name) + : MAPPER_FUNCTIONS.getOrDefault(name, ERROR_FUNCTION); + } + } + + boolean invoke(CommandSender commandSender, String[] args) { + if (args.length < arguments.length + subCommand.length - (varArgType != null ? 1 : 0)) { + return false; + } + if (varArgType == null && args.length > arguments.length + subCommand.length) { + return false; + } + try { + if (!commandSenderPredicate.test(commandSender)) { + return false; + } + Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, arguments, args, varArgType, subCommand); + objects[0] = commandSenderFunction.apply(commandSender); + method.setAccessible(true); + method.invoke(swCommand, objects); + } catch (IllegalAccessException | RuntimeException | InvocationTargetException e) { + throw new SecurityException(e.getMessage(), e); + } catch (CommandParseException e) { + return false; + } + return true; + } + + List tabComplete(CommandSender commandSender, String[] args) { + if (varArgType == null && args.length > arguments.length + subCommand.length) { + return null; + } + int index = 0; + List argsList = new LinkedList<>(Arrays.asList(args)); + for (String value : subCommand) { + String s = argsList.remove(0); + if (argsList.isEmpty()) return Collections.singletonList(value); + if (!value.equalsIgnoreCase(s)) return null; + index++; + } + for (TypeMapper argument : arguments) { + String s = argsList.remove(0); + if (argsList.isEmpty()) { + return argument.tabCompletes(commandSender, Arrays.copyOf(args, args.length - 1), s); + } + try { + if (argument.map(commandSender, Arrays.copyOf(args, index), s) == null) { + return null; + } + } catch (Exception e) { + return null; + } + index++; + } + if (varArgType != null && !argsList.isEmpty()) { + while (!argsList.isEmpty()) { + String s = argsList.remove(0); + if (argsList.isEmpty()) { + return arguments[arguments.length - 1].tabCompletes(commandSender, Arrays.copyOf(args, args.length - 1), s); + } + try { + if (arguments[arguments.length - 1].map(commandSender, Arrays.copyOf(args, index), s) == null) { + return null; + } + } catch (Exception e) { + return null; + } + index++; + } + } + return null; + } + } +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SubCommand.java b/SpigotCore_Main/src/de/steamwar/command/SubCommand.java deleted file mode 100644 index 15dad9a..0000000 --- a/SpigotCore_Main/src/de/steamwar/command/SubCommand.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 org.bukkit.command.CommandSender; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; - -import static de.steamwar.command.SWCommandUtils.*; - -class SubCommand { - - private SWCommand swCommand; - private Method method; - String[] subCommand; - TypeMapper[] arguments; - private Predicate commandSenderPredicate; - private Function commandSenderFunction; - Class varArgType = null; - - SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper) { - this.swCommand = swCommand; - this.method = method; - - Parameter[] parameters = method.getParameters(); - commandSenderPredicate = sender -> parameters[0].getType().isAssignableFrom(sender.getClass()); - commandSenderFunction = sender -> parameters[0].getType().cast(sender); - this.subCommand = subCommand; - - arguments = new TypeMapper[parameters.length - 1]; - for (int i = 1; i < parameters.length; i++) { - Parameter parameter = parameters[i]; - Class clazz = parameter.getType(); - if (parameter.isVarArgs()) { - clazz = clazz.getComponentType(); - varArgType = clazz; - } - - SWCommand.Mapper mapper = parameter.getAnnotation(SWCommand.Mapper.class); - if (clazz.isEnum() && mapper == null && !MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) { - Class> enumClass = (Class>) clazz; - List tabCompletes = new ArrayList<>(); - for (Enum enumConstant : enumClass.getEnumConstants()) { - tabCompletes.add(enumConstant.name().toLowerCase()); - } - arguments[i - 1] = SWCommandUtils.createMapper(s -> ENUM_MAPPER.apply(enumClass, s), s -> tabCompletes); - continue; - } - - String name = clazz.getTypeName(); - if (mapper != null) { - name = mapper.value(); - } - arguments[i - 1] = localTypeMapper.containsKey(name) - ? localTypeMapper.get(name) - : MAPPER_FUNCTIONS.getOrDefault(name, ERROR_FUNCTION); - } - } - - boolean invoke(CommandSender commandSender, String[] args) { - if (args.length < arguments.length + subCommand.length - (varArgType != null ? 1 : 0)) { - return false; - } - if (varArgType == null && args.length > arguments.length + subCommand.length) { - return false; - } - try { - if (!commandSenderPredicate.test(commandSender)) { - return false; - } - Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, arguments, args, varArgType, subCommand); - objects[0] = commandSenderFunction.apply(commandSender); - method.setAccessible(true); - method.invoke(swCommand, objects); - } catch (IllegalAccessException | RuntimeException | InvocationTargetException e) { - throw new SecurityException(e.getMessage(), e); - } catch (CommandParseException e) { - return false; - } - return true; - } - - List tabComplete(CommandSender commandSender, String[] args) { - if (varArgType == null && args.length > arguments.length + subCommand.length) { - return null; - } - int index = 0; - List argsList = new LinkedList<>(Arrays.asList(args)); - for (String value : subCommand) { - String s = argsList.remove(0); - if (argsList.isEmpty()) return Collections.singletonList(value); - if (!value.equalsIgnoreCase(s)) return null; - index++; - } - for (TypeMapper argument : arguments) { - String s = argsList.remove(0); - if (argsList.isEmpty()) { - return argument.tabCompletes(commandSender, Arrays.copyOf(args, args.length - 1), s); - } - try { - if (argument.map(commandSender, Arrays.copyOf(args, index), s) == null) { - return null; - } - } catch (Exception e) { - return null; - } - index++; - } - if (varArgType != null && !argsList.isEmpty()) { - while (!argsList.isEmpty()) { - String s = argsList.remove(0); - if (argsList.isEmpty()) { - return arguments[arguments.length - 1].tabCompletes(commandSender, Arrays.copyOf(args, args.length - 1), s); - } - try { - if (arguments[arguments.length - 1].map(commandSender, Arrays.copyOf(args, index), s) == null) { - return null; - } - } catch (Exception e) { - return null; - } - index++; - } - } - return null; - } -}