diff --git a/SpigotCore_Main/src/de/steamwar/command/InternalCommand.java b/SpigotCore_Main/src/de/steamwar/command/InternalCommand.java new file mode 100644 index 0000000..bf0d759 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/InternalCommand.java @@ -0,0 +1,54 @@ +/* + * 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; + +class InternalCommand { + + private SWCommand swCommand; + private Method method; + private Parameter[] parameters; + + InternalCommand(SWCommand swCommand, Method method) { + this.swCommand = swCommand; + this.method = method; + parameters = method.getParameters(); + } + + boolean invoke(CommandSender commandSender, String[] args) { + if (args.length < parameters.length - 1) return false; + Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, parameters, args); + try { + method.setAccessible(true); + method.invoke(swCommand, objects); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new SecurityException(e.getMessage(), e); + } catch (InvocationTargetException e) { + return false; + } + return true; + } + +} diff --git a/SpigotCore_Main/src/de/steamwar/command/InternalTabComplete.java b/SpigotCore_Main/src/de/steamwar/command/InternalTabComplete.java new file mode 100644 index 0000000..b6f0d47 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/InternalTabComplete.java @@ -0,0 +1,53 @@ +/* + * 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; + +class InternalTabComplete { + + private SWCommand swCommand; + private Method method; + private Parameter[] parameters; + + InternalTabComplete(SWCommand swCommand, Method method) { + this.swCommand = swCommand; + this.method = method; + parameters = method.getParameters(); + } + + SWCommandUtils.TabComplete invoke(CommandSender commandSender, String[] args) { + if (args.length < parameters.length - 1) return null; + Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, parameters, args); + try { + method.setAccessible(true); + return (SWCommandUtils.TabComplete) method.invoke(swCommand, objects); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new SecurityException(e.getMessage(), e); + } catch (InvocationTargetException e) { + return null; + } + } + +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java new file mode 100644 index 0000000..725b891 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java @@ -0,0 +1,102 @@ +/* + * 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.Command; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.function.Consumer; + +public abstract class SWCommand { + + private final Set commandSet = new HashSet<>(); + private final Set tabCompleteSet = new HashSet<>(); + private Consumer helpMessage = sender -> {}; + + protected SWCommand(String command, String... aliases) { + SWCommandUtils.commandMap.register("steamwar", new Command(command, "", "/" + command, Arrays.asList(aliases)) { + @Override + public boolean execute(CommandSender sender, String alias, String[] args) { + for (InternalCommand internalCommand : commandSet) { + if (internalCommand.invoke(sender, args)) { + return false; + } + } + return false; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { + List strings = new ArrayList<>(); + for (InternalTabComplete internalTabComplete : tabCompleteSet) { + SWCommandUtils.TabComplete tabComplete = internalTabComplete.invoke(sender, args); + if (tabComplete != null) { + strings.addAll(tabComplete.tabCompletes); + } + } + return strings; + } + }); + } + + protected final void register(String methodName) { + if (methodName.equals("onCommand")) return; + if (methodName.equals("onTabComplete")) return; + Method[] methods = this.getClass().getDeclaredMethods(); + for (Method method : methods) { + if (!validMethod(method)) continue; + if (method.getReturnType() == Void.TYPE) { + commandSet.add(new InternalCommand(this, method)); + } + if (method.getReturnType() == SWCommandUtils.TabComplete.class) { + tabCompleteSet.add(new InternalTabComplete(this, method)); + } + } + } + + private boolean validMethod(Method method) { + Parameter[] parameters = method.getParameters(); + if (parameters.length == 0) return false; + if (!CommandSender.class.isAssignableFrom(parameters[0].getType())) return false; + 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(); + } + if (clazz.isEnum()) continue; + if (!SWCommandUtils.MAPPER_FUNCTIONS.containsKey(clazz)) return false; + } + return true; + } + + protected final void setHelpMessage(Consumer helpMessage) { + assert helpMessage != null; + this.helpMessage = helpMessage; + } + + protected final void sendHelpMessage(T sender) { + helpMessage.accept(sender); + } + +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java b/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java new file mode 100644 index 0000000..aaa194e --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java @@ -0,0 +1,130 @@ +/* + * 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.CommandMap; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; +import java.lang.reflect.Parameter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; + +class SWCommandUtils { + + private SWCommandUtils() { + throw new IllegalStateException("Utility Class"); + } + + static final Map, Function> MAPPER_FUNCTIONS = new HashMap<>(); + + static final Function ERROR_FUNCTION = s -> { + throw new SecurityException(s); + }; + + static final BiFunction>, String, Enum> ENUM_MAPPER = (enumClass, s) -> { + Enum[] enums = enumClass.getEnumConstants(); + for (Enum e : enums) { + if (e.name().equals(s)) return e; + } + return null; + }; + + static { + MAPPER_FUNCTIONS.put(boolean.class, Boolean::parseBoolean); + MAPPER_FUNCTIONS.put(Boolean.class, Boolean::parseBoolean); + MAPPER_FUNCTIONS.put(float.class, Float::parseFloat); + MAPPER_FUNCTIONS.put(Float.class, Float::parseFloat); + MAPPER_FUNCTIONS.put(double.class, Double::parseDouble); + MAPPER_FUNCTIONS.put(Double.class, Double::parseDouble); + MAPPER_FUNCTIONS.put(int.class, Integer::parseInt); + MAPPER_FUNCTIONS.put(Integer.class, Integer::parseInt); + MAPPER_FUNCTIONS.put(String.class, s -> s); + MAPPER_FUNCTIONS.put(StringBuilder.class, StringBuilder::new); + MAPPER_FUNCTIONS.put(Player.class, Bukkit::getPlayer); + MAPPER_FUNCTIONS.put(UUID.class, UUID::fromString); + } + + static final CommandMap commandMap; + + static { + try { + final Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); + commandMapField.setAccessible(true); + commandMap = (CommandMap) commandMapField.get(Bukkit.getServer()); + } catch (NoSuchFieldException | IllegalAccessException exception) { + Bukkit.shutdown(); + throw new SecurityException("Oh shit. Commands cannot be register.", exception); + } + } + + public static Object[] generateArgumentArray(CommandSender commandSender, Parameter[] parameters, String[] args) { + Object[] arguments = new Object[args.length + 1]; + boolean varArgs = false; + if (parameters[parameters.length - 1].isVarArgs()) { + varArgs = true; + arguments = new Object[parameters.length]; + } + arguments[0] = parameters[0].getType().cast(commandSender); + + for (int i = 1; i < parameters.length - (varArgs ? 1 : 0); i++) { + Class clazz = parameters[i].getType(); + arguments[i] = mapper(clazz).apply(args[i - 1]); + } + + if (varArgs) { + Object[] varArgument = new Object[args.length - parameters.length + 2]; + arguments[arguments.length - 1] = varArgument; + Function mapper = mapper(parameters[parameters.length - 1].getType().getComponentType()); + + int index = 0; + for (int i = parameters.length - 2; i < args.length; i++) { + varArgument[index++] = mapper.apply(args[i]); + } + } + + return arguments; + } + + @SuppressWarnings("unchecked") + private static Function mapper(Class clazz) { + if (clazz.isEnum()) { + Class> enumClass = (Class>) clazz; + return s -> ENUM_MAPPER.apply(enumClass, s); + } else { + return s -> MAPPER_FUNCTIONS.getOrDefault(clazz, ERROR_FUNCTION).apply(s); + } + } + + public static class TabComplete { + final List tabCompletes; + + public TabComplete(List tabCompletes) { + this.tabCompletes = tabCompletes; + } + } + +}