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..9405988 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 @@ -124,5 +129,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/CommandNode.java b/SpigotCore_Main/src/de/steamwar/command/CommandNode.java new file mode 100644 index 0000000..f8dff55 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/CommandNode.java @@ -0,0 +1,157 @@ +/* + * 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.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +class CommandNode { + + private final SWCommand swCommand; + private final TypeMapper typeMapper; + private boolean varArg = false; + private List commandNodeList = new ArrayList<>(); + + private Method executor; + private List helpMessages = new ArrayList<>(); + + private Predicate commandSenderPredicate; + private Function commandSenderObjectFunction; + + CommandNode(SWCommand swCommand, TypeMapper typeMapper) { + this.swCommand = swCommand; + this.typeMapper = typeMapper; + } + + public void addNode(CommandNode commandNode) { + commandNodeList.add(commandNode); + this.varArg = false; + } + + public void setExecutor(Method executor) { + if (this.executor != null) return; + this.executor = executor; + + Parameter parameter = executor.getParameters()[0]; + commandSenderPredicate = parameter.getType()::isInstance; + commandSenderObjectFunction = parameter.getType()::cast; + } + + public void addHelpMessage(String helpMessage) { + this.helpMessages.add(helpMessage); + } + + public void setVarArg(boolean varArg) { + if (commandNodeList.isEmpty()) { + this.varArg = varArg; + } else { + this.varArg = false; + } + } + + public List tabComplete(CommandSender commandSender, int index, String[] args) { + return internalTabCompleteAndSuggest((commandSender1, integer, strings) -> { + return typeMapper.tabCompletes(commandSender1, Arrays.copyOf(strings, integer), strings[strings.length - 1]); + }, commandSender, index, args); + } + + public boolean execute(CommandSender commandSender, int index, String[] args, List mappedObjects) { + try { + Object o; + if (varArg) { + o = Array.newInstance(Object.class, args.length - index); + for (int i = 0; i < Array.getLength(o); i++) { + Object current = typeMapper.map(commandSender, Arrays.copyOf(args, index + i), args[index + i]); + if (current == null) return false; + Array.set(o, i, current); + } + } else { + o = typeMapper.map(commandSender, Arrays.copyOf(args, index), args[index]); + if (o == null) return false; + } + mappedObjects.add(o); + } catch (Exception e) { + return false; + } + + if (index == args.length - 1) { + if (executor == null) { + return false; + } + if (!commandSenderPredicate.test(commandSender)) { + return false; + } + + List finalMappedObjects = new ArrayList<>(mappedObjects); + finalMappedObjects.add(0, commandSenderObjectFunction.apply(commandSender)); + + Object[] objects = finalMappedObjects.toArray(new Object[0]); + try { + executor.invoke(swCommand, objects); + return true; + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + } + + return commandNodeList.stream() + .map(commandNode -> commandNode.execute(commandSender, index + 1, args, new ArrayList<>(mappedObjects))) + .findFirst().orElse(false); + } + + public List suggest(CommandSender commandSender, int index, String[] args) { + return internalTabCompleteAndSuggest((commandSender1, integer, strings) -> helpMessages, commandSender, index, args); + } + + private List internalTabCompleteAndSuggest(TriFunction> returnFunction, CommandSender commandSender, int index, String[] args) { + try { + if (index == args.length - 1) { + return returnFunction.apply(commandSender, index, args); + } + if (typeMapper.map(commandSender, Arrays.copyOf(args, index), args[index]) == null) { + return Collections.emptyList(); + } + if (varArg) { + return internalTabCompleteAndSuggest(returnFunction, commandSender, index + 1, args); + } else { + return commandNodeList.stream() + .map(commandNode -> commandNode.internalTabCompleteAndSuggest(returnFunction, commandSender, index + 1, args)) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + } catch (Exception e) { + return Collections.emptyList(); + } + } + + private interface TriFunction { + O apply(I1 i1, I2 i2, I3 i3); + } +} diff --git a/SpigotCore_Main/src/de/steamwar/command/CommandPart.java b/SpigotCore_Main/src/de/steamwar/command/CommandPart.java new file mode 100644 index 0000000..27c5981 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/CommandPart.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 java.lang.reflect.Method; +import java.util.Map; + +public class CommandPart { + + public CommandPart(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper) { + + } + +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java index ab72fc5..fba9c11 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java @@ -19,151 +19,50 @@ 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(); + } + + interface SWCommandInterface { + void unregister(); + void register(); } @Retention(RetentionPolicy.RUNTIME) @@ -196,4 +95,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..4f7d386 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandBrigadier.java @@ -0,0 +1,213 @@ +/* + * 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 com.mojang.brigadier.tree.CommandNode; +import net.minecraft.server.v1_15_R1.CommandListenerWrapper; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class SWCommandBrigadier implements SWCommand.SWCommandInterface { + + private final String command; + private final String[] aliases; + + 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.command = command; + this.aliases = aliases; + + this.swCommand = swCommand; + SWCommandUtils.createList(swCommand, commandList, commandHelpList, localTypeMapper, this::createSubCommand); + Stream.of(commandList, commandHelpList).flatMap(List::stream).forEach(subCommand -> { + register(command, subCommand); + for (String s : aliases) { + register(s, subCommand); + } + }); + // unregister(); + // register(); + } + + private SubCommand createSubCommand(Method method, String[] strings) { + return new SubCommand(swCommand, method, strings, localTypeMapper, current -> { + List argumentBuilders = new ArrayList<>(); + + for (String s : current.subCommand) { + LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder.literal(s); + List currentBuilders = new ArrayList<>(); + argumentBuilders.forEach(currrentBuilder -> { + currentBuilders.add(currrentBuilder.then(literalArgumentBuilder)); + }); + if (argumentBuilders.isEmpty()) { + currentBuilders.add(literalArgumentBuilder); + if (current.argumentNode.isEmpty()) { + current.argumentNode.add(literalArgumentBuilder); + } + } + argumentBuilders = currentBuilders; + } + + for (int i = 0; i < current.arguments.length - (current.varArgType != null ? 1 : 0); i++) { + Parameter parameter = current.parameters[i + 1]; + Class parameterType = parameter.getType(); + ArgumentType argumentType = getArgumentType(parameter, parameterType); + List newBuilders = new ArrayList<>(); + if (argumentType == null) { + try { + current.arguments[i].tabCompletes(null, null, "").stream() + .map(LiteralArgumentBuilder::literal) + .forEach(newBuilders::add); + } catch (Exception e) { + newBuilders.add(RequiredArgumentBuilder.argument(parameter.getName(), StringArgumentType.string())); + } + } else { + newBuilders.add(RequiredArgumentBuilder.argument(parameter.getName(), argumentType)); + } + + List currentBuilders = new ArrayList<>(); + argumentBuilders.forEach(currrentBuilder -> { + newBuilders.forEach(argumentBuilder -> { + currentBuilders.add(currrentBuilder.then(argumentBuilder)); + }); + }); + if (argumentBuilders.isEmpty()) { + currentBuilders.addAll(newBuilders); + if (current.argumentNode.isEmpty()) { + current.argumentNode.addAll(newBuilders); + } + } + argumentBuilders = currentBuilders; + } + if (current.varArgType != null) { + Parameter parameter = current.parameters[current.parameters.length - 1]; + Class parameterType = parameter.getType(); + ArgumentType argumentType = getArgumentType(parameter, parameterType); + List newBuilders = new ArrayList<>(); + if (argumentType == null) { + try { + current.arguments[current.arguments.length - 1].tabCompletes(null, null, "").stream() + .map(LiteralArgumentBuilder::literal) + .forEach(newBuilders::add); + } catch (Exception e) { + newBuilders.add(RequiredArgumentBuilder.argument(parameter.getName(), StringArgumentType.string())); + } + } else { + newBuilders.add(RequiredArgumentBuilder.argument(parameter.getName(), argumentType)); + } + + // TODO: VarArgs + List currentBuilders = new ArrayList<>(); + argumentBuilders.forEach(currrentBuilder -> { + newBuilders.forEach(argumentBuilder -> { + currentBuilders.add(currrentBuilder.then(argumentBuilder)); + }); + }); + if (argumentBuilders.isEmpty()) { + currentBuilders.addAll(newBuilders); + if (current.argumentNode.isEmpty()) { + current.argumentNode.addAll(newBuilders); + } + } + currentBuilders.forEach(argumentBuilder -> argumentBuilder.executes(executes(current))); + argumentBuilders = currentBuilders; + } + current.argumentNodeEnd.addAll(argumentBuilders); + }); + } + + private com.mojang.brigadier.Command executes(SubCommand current) { + return commandContext -> { + CommandSender commandSender = ((CommandListenerWrapper) commandContext.getSource()).getBukkitSender(); + List stringList = Arrays.stream(commandContext.getInput().split(" ")).collect(Collectors.toList()); + stringList.remove(0); + String[] args = stringList.toArray(new String[0]); + current.invoke(commandSender, args); + return 0; + }; + } + + private void register(String name, SubCommand subCommand) { + LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder.literal(name); + if (subCommand.argumentNode == null) { + literalArgumentBuilder.executes(executes(subCommand)); + SWCommand.dispatcher.register(literalArgumentBuilder); + return; + } + subCommand.argumentNodeEnd.forEach(argumentBuilder -> argumentBuilder.executes(executes(subCommand))); + subCommand.argumentNode.forEach(literalArgumentBuilder::then); + SWCommand.dispatcher.register(literalArgumentBuilder); + } + + @Override + public void unregister() { + SWCommandUtils.knownCommandMap.remove(command); + Arrays.stream(aliases).forEach(SWCommandUtils.knownCommandMap::remove); + } + + @Override + public void register() { + Stream.of(commandList, commandHelpList).flatMap(List::stream).forEach(subCommand -> { + register(command, subCommand); + for (String s : aliases) { + register(s, subCommand); + } + }); + } + + private ArgumentType getArgumentType(Parameter parameter, Class parameterType) { + ArgumentType argumentType; + if (parameterType == boolean.class || parameterType == Boolean.class) { + argumentType = BoolArgumentType.bool(); + } else 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 { + return null; + } + return argumentType; + } +} 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..4017ec5 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandNormal.java @@ -0,0 +1,84 @@ +/* + * 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.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; + +class SWCommandNormal implements SWCommand.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(); + SWCommandUtils.createList(swCommand, commandList, commandHelpList, localTypeMapper, this::createSubCommand); + } + + private SubCommand createSubCommand(Method method, String[] strings) { + return new de.steamwar.command.SubCommand(swCommand, method, strings, localTypeMapper, current -> {}); + } + + 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); + } +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java b/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java index 64c735d..7313fba 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java @@ -32,9 +32,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.*; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.logging.Level; import java.util.stream.Collectors; public class SWCommandUtils { @@ -180,4 +184,83 @@ public class SWCommandUtils { if (method.getAnnotations().length != 1) return null; return method.getDeclaredAnnotationsByType(annotation); } + + static void createList(SWCommand swCommand, List commandList, List commandHelpList, Map> localTypeMapper, BiFunction creator) { + Method[] methods = swCommand.getClass().getDeclaredMethods(); + for (Method method : methods) { + addMapper(swCommand, SWCommand.Mapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { + (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value(), typeMapper); + }); + addMapper(swCommand, 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(creator.apply(method, anno.value())); + }); + } + 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(creator.apply(method, anno.value())); + }); + } + } + + private static 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 static void addMapper(SWCommand swCommand, 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); + } + }); + } } diff --git a/SpigotCore_Main/src/de/steamwar/command/SubCommand.java b/SpigotCore_Main/src/de/steamwar/command/SubCommand.java index 15dad9a..6dffc9b 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SubCommand.java +++ b/SpigotCore_Main/src/de/steamwar/command/SubCommand.java @@ -19,12 +19,14 @@ package de.steamwar.command; +import com.mojang.brigadier.builder.ArgumentBuilder; 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.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -32,19 +34,23 @@ import static de.steamwar.command.SWCommandUtils.*; class SubCommand { - private SWCommand swCommand; - private Method method; + List argumentNode = new ArrayList<>(); + List argumentNodeEnd = new ArrayList<>(); + + SWCommand swCommand; + Parameter[] parameters; + Method method; String[] subCommand; TypeMapper[] arguments; - private Predicate commandSenderPredicate; - private Function commandSenderFunction; + Predicate commandSenderPredicate; + Function commandSenderFunction; Class varArgType = null; - SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper) { + public SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper, Consumer consumer) { this.swCommand = swCommand; this.method = method; - Parameter[] parameters = method.getParameters(); + parameters = method.getParameters(); commandSenderPredicate = sender -> parameters[0].getType().isAssignableFrom(sender.getClass()); commandSenderFunction = sender -> parameters[0].getType().cast(sender); this.subCommand = subCommand; @@ -77,6 +83,8 @@ class SubCommand { ? localTypeMapper.get(name) : MAPPER_FUNCTIONS.getOrDefault(name, ERROR_FUNCTION); } + + consumer.accept(this); } boolean invoke(CommandSender commandSender, String[] args) {