CommandFramework3 #94
82
SpigotCore_Main/src/de/steamwar/acommand/TestCommand.java
Normale Datei
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.acommand;
|
||||
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.command.TypeMapper;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class TestCommand extends SWCommand {
|
||||
|
||||
public TestCommand() {
|
||||
// Register this command as 'test'
|
||||
Lixfel
hat
Prinzipiell gut, aber insbesondere im Kampfsystem (nach refactoring) tausche ich die Implementierungen von Befehlen während der Serverlaufzeit mehrfach aus, je nachdem, welche Kampfphase gerade ist. Wäre toll, wenn das mit dem System gehen würde. Prinzipiell gut, aber insbesondere im Kampfsystem (nach refactoring) tausche ich die Implementierungen von Befehlen während der Serverlaufzeit mehrfach aus, je nachdem, welche Kampfphase gerade ist. Wäre toll, wenn das mit dem System gehen würde.
YoyoNow
hat
Kannst du mir hier zu ein Beispiel geben, was genau du brauchen wirst? Kannst du mir hier zu ein Beispiel geben, was genau du brauchen wirst?
YoyoNow
hat
Reicht dir die Implementierung von an und ausschalten von Befehlen mit dem enabled boolean? Oder brauchst du mehr als das? Reicht dir die Implementierung von an und ausschalten von Befehlen mit dem enabled boolean? Oder brauchst du mehr als das?
YoyoNow
hat
Wenn ja sollten wir mal am Wochenende darüber reden. Wenn ja sollten wir mal am Wochenende darüber reden.
|
||||
super("test");
|
||||
}
|
||||
|
||||
// One Help Command, the first Parameter should be some kind of CommandSender
|
||||
// The second argument can only be a varArgs string of what arguments were tried to map
|
||||
@Register(help = true)
|
||||
public void testHelp(Player player, String... args) {
|
||||
player.sendMessage("This is your help message");
|
||||
}
|
||||
|
||||
// One Command, the first Parameter should be some kind of CommandSender
|
||||
@Register
|
||||
public void test(Player player) {
|
||||
|
||||
}
|
||||
|
||||
// Another Command, subCommands can be implemented with the Register Annotation,
|
||||
// you can use custom Mappers by putting a Mapper Annotation on a Parameter
|
||||
Lixfel
hat
Liest sich arg hacky, ich fände es gut, wenn Mapperobjekte nur einmal erstellt werden und ggf. von mehreren Befehlen genutzt werden könnten. Evtl. eine Annotation @Mapper mit einem TypeMapper als Argument statt einem String? Dann haben wir auch nicht mehr die dreckige string-lösung. Liest sich arg hacky, ich fände es gut, wenn Mapperobjekte nur einmal erstellt werden und ggf. von mehreren Befehlen genutzt werden könnten. Evtl. eine Annotation @Mapper mit einem TypeMapper als Argument statt einem String? Dann haben wir auch nicht mehr die dreckige string-lösung.
YoyoNow
hat
Ich wollte die Mapper explizit von dem Parameter weghaben, damit diese explizit auch global erzeugt werden können. Das was du da liest ist eine abkürzung, womit du Mapper, welche du nur hier in diesem Befehl brauchst hier definierst und verwendest. Ab dem Moment wird es für alle Befehle, welche danach erzeugt werden auch existieren. Der String ist explizit, damit man ein Argument 'Material' haben kann, welches jedoch einen anderen Mapper haben kann. Der name ist dann auch so gut zu wählen wie nötig oder möglich. Ich wollte die Mapper explizit von dem Parameter weghaben, damit diese explizit auch global erzeugt werden können. Das was du da liest ist eine abkürzung, womit du Mapper, welche du nur hier in diesem Befehl brauchst hier definierst und verwendest. Ab dem Moment wird es für alle Befehle, welche danach erzeugt werden auch existieren. Der String ist explizit, damit man ein Argument 'Material' haben kann, welches jedoch einen anderen Mapper haben kann. Der name ist dann auch so gut zu wählen wie nötig oder möglich.
Die Mapper an sich werden auch nur einmal erzeugt und dann in eine Map intern gepackt. Außerdem kann man in Annotationen keine Interfaces als fields verwenden:
![image](/devlabs/attachments/7bc5f00b-475e-4314-bdd1-7fc6a4dbe5f0)
Deswegen geht deine Idee leider nicht, hatte ich auch schon als Idee.
|
||||
@Register({"two"})
|
||||
public void testTwo(Player player, int i, @Mapper("solidMaterial") Material material) {
|
||||
Lixfel
hat
Das das ganze mit Strings funktioniert, gefällt mir immer noch nicht (und dir wsl. auch nicht). Da anscheinend keine Interfaces, sondern nur konkrete Instanzen einer Klasse verwendet werden können: Mach doch eine Klasse TypeMapper, die als Parameter eine Funktion String -> Objekt nimmt. Dann ist das eine klare Klasse. Ggf. ist das dann erstmal ein Object (wenn Templating nicht geht) ansonsten spricht aber meines Wissens nach nix dagegen, oder? Das das ganze mit Strings funktioniert, gefällt mir immer noch nicht (und dir wsl. auch nicht). Da anscheinend keine Interfaces, sondern nur konkrete Instanzen einer Klasse verwendet werden können: Mach doch eine Klasse TypeMapper, die als Parameter eine Funktion String -> Objekt nimmt. Dann ist das eine klare Klasse. Ggf. ist das dann erstmal ein Object (wenn Templating nicht geht) ansonsten spricht aber meines Wissens nach nix dagegen, oder?
YoyoNow
hat
Ich verstehe dein Vorschlag nicht ganz. Du willst, dass ich in der Annotation eine Object angebe, welches dann eine Function<String, Object> beinhaltet. Wie soll das das Mapper zeug lösen. Kannst du versuchen das nochmal etwas genauer zu beschreiben? Ich verstehe dein Vorschlag nicht ganz. Du willst, dass ich in der Annotation eine Object angebe, welches dann eine Function\<String, Object> beinhaltet. Wie soll das das Mapper zeug lösen. Kannst du versuchen das nochmal etwas genauer zu beschreiben?
YoyoNow
hat
Das hier spricht dagegen (Allowed Types in Annotation):
Multidimensional arrays are forbidden. Arrays of type Class are forbidden. Hier warum letzteres verboten ist: 'Constant Expressions' ist das Stichwort ![image](/devlabs/attachments/3c90a023-f8d5-4ba7-827e-473af354ade4)
Das hier spricht dagegen (Allowed Types in Annotation):
- primitive
- String
- an Enum
- another Annotation
- Class
- an Array of the above
Multidimensional arrays are forbidden. Arrays of type Class are forbidden.
Hier warum letzteres verboten ist:
![image](/devlabs/attachments/2081e223-2da1-4f23-96ca-2cd5b0101bd7)
'Constant Expressions' ist das Stichwort
|
||||
|
||||
}
|
||||
|
||||
// Add Custom Mapper when this command is registered, all Mapper of you class will be
|
||||
// created first and than the Commands. Do not use this mapper outside your class, as
|
||||
// it can only create failures. Use on your own risk. Definition order should be considered.
|
||||
@Mapper("solidMaterial")
|
||||
public TypeMapper<Material> materialTypeMapper() {
|
||||
List<String> tabCompletes = Arrays.stream(Material.values())
|
||||
.filter(Material::isSolid)
|
||||
.map(Material::name)
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toList());
|
||||
return new TypeMapper<Material>() {
|
||||
@Override
|
||||
public Material map(String[] previous, String s) {
|
||||
return Material.valueOf(s.toUpperCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabCompletes(CommandSender commandSender, String[] previous, String s) {
|
||||
return tabCompletes;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
42
SpigotCore_Main/src/de/steamwar/command/CommandParseException.java
Normale Datei
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.command;
|
||||
|
||||
public class CommandParseException extends Exception {
|
||||
|
||||
public CommandParseException() {
|
||||
}
|
||||
|
||||
public CommandParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CommandParseException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public CommandParseException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CommandParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
224
SpigotCore_Main/src/de/steamwar/command/SWCommand.java
Normale Datei
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.command;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
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;
|
||||
|
||||
public abstract class SWCommand {
|
||||
Lixfel
hat
Das ist nicht so das, was ich fürs FightSystem gemeint habe. Ich möchte eigentlich nicht, dass sich der Command merkt, ob er jetzt enabled oder disabled ist, sondern den Command einfach Registrieren und aber auch wieder Entregistrieren können. Dann kann ich nämlich auch bestimmen, wass der Command macht, wenn er "disabled" ist, oder gar komplexere State-Machines umsetzen. Das ist nicht so das, was ich fürs FightSystem gemeint habe. Ich möchte eigentlich nicht, dass sich der Command merkt, ob er jetzt enabled oder disabled ist, sondern den Command einfach Registrieren und aber auch wieder Entregistrieren können. Dann kann ich nämlich auch bestimmen, wass der Command macht, wenn er "disabled" ist, oder gar komplexere State-Machines umsetzen.
YoyoNow
hat
Ok ich gucke, dass ich das eingebaut bekomme, an sich muss ich ja nur unregister können. Weril registerieren tust du ja mit einer Instanz erzeugen, Ok ich gucke, dass ich das eingebaut bekomme, an sich muss ich ja nur unregister können. Weril registerieren tust du ja mit einer Instanz erzeugen,
YoyoNow
hat
Dies sollte nun möglich sein. Dies sollte nun möglich sein.
|
||||
|
||||
Lixfel
hat
Da du glaube sowieso keinen Befehl doppelt einfügst und dann häufig drüberiterierst, wäre glaube ich eine ArrayList angebrachter. Da du glaube sowieso keinen Befehl doppelt einfügst und dann häufig drüberiterierst, wäre glaube ich eine ArrayList angebrachter.
YoyoNow
hat
Ich glaube eher eine LinkedList, weil ich nur drüber iteriere oder? Ich glaube eher eine LinkedList, weil ich nur drüber iteriere oder?
Lixfel
hat
Nein, der Vorteil einer Linkedlist ist eher nur bei häufigem Entfernen aus der Mitte gegeben. Die ArrayList ist auch beim Iterieren schneller, weil da ja einfach nur der index um eins erhöht werden muss (bessere Speicherpositionierung) Nein, der Vorteil einer Linkedlist ist eher nur bei häufigem Entfernen aus der Mitte gegeben. Die ArrayList ist auch beim Iterieren schneller, weil da ja einfach nur der index um eins erhöht werden muss (bessere Speicherpositionierung)
|
||||
private final Command command;
|
||||
private final List<SubCommand> commandSet = new ArrayList<>();
|
||||
private final List<SubCommand> commandHelpSet = new ArrayList<>();
|
||||
private final Map<String, TypeMapper<?>> localTypeMapper = new HashMap<>();
|
||||
|
||||
protected SWCommand(String command) {
|
||||
this(command, 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) {
|
||||
for (SubCommand subCommand : commandSet) {
|
||||
if (subCommand.invoke(sender, args)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (SubCommand subCommand : commandHelpSet) {
|
||||
if (subCommand.invoke(sender, args)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
|
||||
List<String> strings = new ArrayList<>();
|
||||
for (SubCommand subCommand : commandSet) {
|
||||
List<String> tabCompletes = subCommand.tabComplete(sender, args);
|
||||
if (tabCompletes != null) {
|
||||
strings.addAll(tabCompletes);
|
||||
}
|
||||
}
|
||||
strings = new ArrayList<>(strings);
|
||||
for (int i = strings.size() - 1; i >= 0; i--) {
|
||||
if (!strings.get(i).toLowerCase().startsWith(args[args.length - 1].toLowerCase())) {
|
||||
strings.remove(i);
|
||||
}
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
};
|
||||
register();
|
||||
|
||||
for (Method method : getClass().getDeclaredMethods()) {
|
||||
addMapper(Mapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> {
|
||||
if (anno.local()) {
|
||||
localTypeMapper.put(anno.value(), typeMapper);
|
||||
} else {
|
||||
SWCommandUtils.addMapper(anno.value(), typeMapper);
|
||||
}
|
||||
});
|
||||
addMapper(ClassMapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> {
|
||||
if (anno.local()) {
|
||||
localTypeMapper.put(anno.value().getTypeName(), typeMapper);
|
||||
} else {
|
||||
SWCommandUtils.addMapper(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;
|
||||
}
|
||||
commandHelpSet.add(new SubCommand(this, method, anno.value()));
|
||||
});
|
||||
}
|
||||
for (Method method : getClass().getDeclaredMethods()) {
|
||||
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 = clazz.getTypeName();
|
||||
if (mapper != null) {
|
||||
name = mapper.value();
|
||||
}
|
||||
if (!SWCommandUtils.MAPPER_FUNCTIONS.containsKey(name)) {
|
||||
Bukkit.getLogger().log(Level.WARNING, "The parameter '" + parameter.toString() + "' is using an unsupported Mapper of type '" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
commandSet.add(new SubCommand(this, method, anno.value(), localTypeMapper));
|
||||
});
|
||||
|
||||
this.commandSet.sort((o1, o2) -> {
|
||||
int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length);
|
||||
if (compare != 0) {
|
||||
return compare;
|
||||
} else {
|
||||
int i1 = o1.varArgType != null ? Integer.MAX_VALUE : o1.arguments.length;
|
||||
int i2 = o2.varArgType != null ? Integer.MAX_VALUE : o2.arguments.length;
|
||||
return Integer.compare(i1, i2);
|
||||
}
|
||||
});
|
||||
commandHelpSet.sort(Comparator.comparingInt(o -> -o.subCommand.length));
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Annotation> void add(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, Parameter[]> consumer) {
|
||||
T anno = SWCommandUtils.getAnnotation(method, annotation);
|
||||
if (anno == null) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
consumer.accept(anno, parameters);
|
||||
}
|
||||
|
||||
private <T extends Annotation> void addMapper(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, TypeMapper<?>> consumer) {
|
||||
add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> {
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
Object object = method.invoke(this);
|
||||
consumer.accept(anno, (TypeMapper<?>) object);
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException(e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void unregister() {
|
||||
SWCommandUtils.knownCommandMap.remove(command.getName());
|
||||
for (String alias : command.getAliases()) {
|
||||
SWCommandUtils.knownCommandMap.remove(alias);
|
||||
}
|
||||
command.unregister(SWCommandUtils.commandMap);
|
||||
}
|
||||
|
||||
protected void register() {
|
||||
SWCommandUtils.commandMap.register("steamwar", this.command);
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
protected @interface Register {
|
||||
String[] value() default {};
|
||||
|
||||
boolean help() default false;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.PARAMETER, ElementType.METHOD})
|
||||
protected @interface Mapper {
|
||||
String value();
|
||||
|
||||
boolean local() default false;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
protected @interface ClassMapper {
|
||||
Class<?> value();
|
||||
|
||||
boolean local() default false;
|
||||
}
|
||||
}
|
185
SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java
Normale Datei
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.command;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandMap;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.SimpleCommandMap;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SWCommandUtils {
|
||||
|
||||
private SWCommandUtils() {
|
||||
throw new IllegalStateException("Utility Class");
|
||||
}
|
||||
|
||||
static final Map<String, TypeMapper<?>> MAPPER_FUNCTIONS = new HashMap<>();
|
||||
|
||||
static final TypeMapper<?> ERROR_FUNCTION = createMapper(s -> {
|
||||
throw new SecurityException();
|
||||
}, s -> Collections.emptyList());
|
||||
|
||||
static final BiFunction<Class<Enum<?>>, String, Enum<?>> ENUM_MAPPER = (enumClass, s) -> {
|
||||
Enum<?>[] enums = enumClass.getEnumConstants();
|
||||
for (Enum<?> e : enums) {
|
||||
if (e.name().equalsIgnoreCase(s)) return e;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
Lixfel
hat
Wird wirklich irgendwo als Parameter "Boolean" benötigt? Oder "Float"? Oder... Wird wirklich irgendwo als Parameter "Boolean" benötigt? Oder "Float"? Oder...
YoyoNow
hat
Ich finde Boolean und Float ist benötigt, da man diese Mapper von außen nicht so schön erzeugen kann und wir sollten etwas Zukunftsicher an sich sache gehen, was benötigt wird. Ich finde Boolean und Float ist benötigt, da man diese Mapper von außen nicht so schön erzeugen kann und wir sollten etwas Zukunftsicher an sich sache gehen, was benötigt wird.
|
||||
|
||||
static {
|
||||
addMapper(boolean.class, Boolean.class, createMapper(Boolean::parseBoolean, s -> Arrays.asList("true", "false")));
|
||||
addMapper(float.class, Float.class, createMapper(Float::parseFloat, numberCompleter(Float::parseFloat)));
|
||||
addMapper(double.class, Double.class, createMapper(Double::parseDouble, numberCompleter(Double::parseDouble)));
|
||||
addMapper(int.class, Integer.class, createMapper(Integer::parseInt, numberCompleter(Integer::parseInt)));
|
||||
MAPPER_FUNCTIONS.put(String.class.getTypeName(), createMapper(s -> s, Collections::singletonList));
|
||||
MAPPER_FUNCTIONS.put(Player.class.getTypeName(), createMapper(Bukkit::getPlayer, s -> Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList())));
|
||||
MAPPER_FUNCTIONS.put(GameMode.class.getTypeName(), createMapper(s -> {
|
||||
s = s.toLowerCase();
|
||||
if (s.equals("s") || s.equals("survival") || s.equals("0")) return GameMode.SURVIVAL;
|
||||
if (s.equals("c") || s.equals("creative") || s.equals("1")) return GameMode.CREATIVE;
|
||||
if (s.equals("sp") || s.equals("spectator") || s.equals("3")) return GameMode.SPECTATOR;
|
||||
if (s.equals("a") || s.equals("adventure") || s.equals("2")) return GameMode.ADVENTURE;
|
||||
throw new SecurityException();
|
||||
}, s -> Arrays.asList("s", "survival", "0", "c", "creative", "1", "sp", "specator", "3", "a", "adventure", "2")));
|
||||
}
|
||||
|
||||
private static void addMapper(Class<?> clazz, Class<?> alternativeClazz, TypeMapper<?> mapper) {
|
||||
MAPPER_FUNCTIONS.put(clazz.getTypeName(), mapper);
|
||||
MAPPER_FUNCTIONS.put(alternativeClazz.getTypeName(), mapper);
|
||||
}
|
||||
|
||||
static final CommandMap commandMap;
|
||||
static final Map<String, Command> knownCommandMap;
|
||||
|
||||
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 registered.", exception);
|
||||
}
|
||||
try {
|
||||
final Field knownCommandsField = SimpleCommandMap.class.getDeclaredField("knownCommands");
|
||||
knownCommandsField.setAccessible(true);
|
||||
knownCommandMap = (Map<String, Command>) knownCommandsField.get(commandMap);
|
||||
} catch (NoSuchFieldException | IllegalAccessException exception) {
|
||||
Bukkit.shutdown();
|
||||
throw new SecurityException("Oh shit. Commands cannot be registered.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
static Object[] generateArgumentArray(TypeMapper<?>[] parameters, String[] args, Class<?> varArgType, String[] subCommand) throws CommandParseException {
|
||||
Object[] arguments = new Object[parameters.length + 1];
|
||||
int index = 0;
|
||||
while (index < subCommand.length) {
|
||||
if (!args[index].equalsIgnoreCase(subCommand[index])) {
|
||||
throw new CommandParseException();
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (varArgType != null && index > args.length - 1) {
|
||||
Object varArgument = Array.newInstance(varArgType, 0);
|
||||
arguments[arguments.length - 1] = varArgument;
|
||||
} else {
|
||||
for (int i = 0; i < parameters.length - (varArgType != null ? 1 : 0); i++) {
|
||||
arguments[i + 1] = parameters[i].map(Arrays.copyOf(args, index), args[index]);
|
||||
index++;
|
||||
if (arguments[i + 1] == null) {
|
||||
throw new CommandParseException();
|
||||
}
|
||||
}
|
||||
|
||||
if (varArgType != null) {
|
||||
int length = args.length - parameters.length - subCommand.length + 1;
|
||||
Object varArgument = Array.newInstance(varArgType, length);
|
||||
arguments[arguments.length - 1] = varArgument;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
Object value = parameters[parameters.length - 1].map(Arrays.copyOf(args, index), args[index]);
|
||||
if (value == null) {
|
||||
throw new CommandParseException();
|
||||
}
|
||||
Array.set(varArgument, i, value);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public static <T> void addMapper(Class<T> clazz, TypeMapper<T> mapper) {
|
||||
addMapper(clazz.getTypeName(), mapper);
|
||||
}
|
||||
|
||||
public static void addMapper(String name, TypeMapper<?> mapper) {
|
||||
if (MAPPER_FUNCTIONS.containsKey(name)) return;
|
||||
MAPPER_FUNCTIONS.put(name, mapper);
|
||||
}
|
||||
|
||||
public static <T> TypeMapper<T> createMapper(Function<String, T> mapper, Function<String, List<String>> tabCompleter) {
|
||||
return createMapper(mapper, (commandSender, s) -> tabCompleter.apply(s));
|
||||
}
|
||||
|
||||
public static <T> TypeMapper<T> createMapper(Function<String, T> mapper, BiFunction<CommandSender, String, List<String>> tabCompleter) {
|
||||
return new TypeMapper<T>() {
|
||||
@Override
|
||||
public T map(String[] previous, String s) {
|
||||
return mapper.apply(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabCompletes(CommandSender commandSender, String[] previous, String s) {
|
||||
return tabCompleter.apply(commandSender, s);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Function<String, List<String>> numberCompleter(Function<String, ?> mapper) {
|
||||
return s -> {
|
||||
try {
|
||||
mapper.apply(s);
|
||||
return Collections.singletonList(s);
|
||||
} catch (Exception e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static <T extends Annotation> T getAnnotation(Method method, Class<T> annotation) {
|
||||
if (method.getAnnotations().length != 1) return null;
|
||||
return method.getAnnotation(annotation);
|
||||
}
|
||||
}
|
139
SpigotCore_Main/src/de/steamwar/command/SubCommand.java
Normale Datei
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
class SubCommand {
|
||||
|
||||
private SWCommand swCommand;
|
||||
private Method method;
|
||||
String[] subCommand;
|
||||
TypeMapper<?>[] arguments;
|
||||
private Function<CommandSender, ?> commandSenderFunction;
|
||||
Class<?> varArgType = null;
|
||||
|
||||
public SubCommand(SWCommand swCommand, Method method, String[] subCommand) {
|
||||
this(swCommand, method, subCommand, new HashMap<>());
|
||||
}
|
||||
|
||||
public SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map<String, TypeMapper<?>> localTypeMapper) {
|
||||
this.swCommand = swCommand;
|
||||
this.method = method;
|
||||
|
||||
Parameter[] parameters = method.getParameters();
|
||||
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 && !SWCommandUtils.MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) {
|
||||
Class<Enum<?>> enumClass = (Class<Enum<?>>) clazz;
|
||||
List<String> tabCompletes = new ArrayList<>();
|
||||
for (Enum<?> enumConstant : enumClass.getEnumConstants()) {
|
||||
tabCompletes.add(enumConstant.name().toLowerCase());
|
||||
}
|
||||
arguments[i - 1] = SWCommandUtils.createMapper(s -> SWCommandUtils.ENUM_MAPPER.apply(enumClass, s), s -> tabCompletes);
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = clazz.getTypeName();
|
||||
if (mapper != null) {
|
||||
name = mapper.value();
|
||||
}
|
||||
if (localTypeMapper.containsKey(name)) {
|
||||
arguments[i - 1] = localTypeMapper.getOrDefault(name, SWCommandUtils.ERROR_FUNCTION);
|
||||
} else {
|
||||
arguments[i - 1] = SWCommandUtils.MAPPER_FUNCTIONS.getOrDefault(name, SWCommandUtils.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 {
|
||||
Object[] objects = SWCommandUtils.generateArgumentArray(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;
|
||||
Lixfel
hat
Ich denke mal, das throw new SecurityException aus der Zeile drüber wäre auch angebracht. Auf jeden Fall für "RuntimeException". Ich denke mal, das throw new SecurityException aus der Zeile drüber wäre auch angebracht. Auf jeden Fall für "RuntimeException".
|
||||
}
|
||||
|
||||
List<String> tabComplete(CommandSender commandSender, String[] args) {
|
||||
if (varArgType == null && args.length < arguments.length + subCommand.length - 1) {
|
||||
return null;
|
||||
}
|
||||
List<String> argsList = new LinkedList<>(Arrays.asList(args));
|
||||
for (String value : subCommand) {
|
||||
Lixfel
hat
Entweder du behandelst im SWCommand nicht den Fall null, oder du returnst immer eine leere Liste. Bitte nicht beides zeitgleich. Entweder du behandelst im SWCommand nicht den Fall null, oder du returnst immer eine leere Liste. Bitte nicht beides zeitgleich.
YoyoNow
hat
Denn fall null behandle ich eigentlich wegen unsauberen TabCompletern, die eigengeschrieben sind. Denn fall null behandle ich eigentlich wegen unsauberen TabCompletern, die eigengeschrieben sind.
Lixfel
hat
Dann nutze doch auch einfach immer null. Dann nutze doch auch einfach immer null.
|
||||
String s = argsList.remove(0);
|
||||
if (argsList.isEmpty()) return Collections.singletonList(value);
|
||||
if (!value.equalsIgnoreCase(s)) return null;
|
||||
}
|
||||
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(Arrays.copyOf(args, argsList.size()), s) == null) {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
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(Arrays.copyOf(args, argsList.size()), s) == null) {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
30
SpigotCore_Main/src/de/steamwar/command/TypeMapper.java
Normale Datei
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.command;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TypeMapper<T> {
|
||||
T map(String[] previousArguments, String s);
|
||||
|
||||
List<String> tabCompletes(CommandSender commandSender, String[] previousArguments, String s);
|
||||
}
|
Ich fände es besser, wenn es ein Implements sein würde, dann kann die Klasse auch noch anderes als ein Befehl sein.
Im Moment geht dies nicht, wegen dem Constructor, und den Sachen, welche dort drin passieren. Dementsprechend ist dies so im Moment nicht möglich. Wie würdest du das umsetzten?
Ich würde ungern mit defaults und so arbeiten, deswegen habe ich es im Moment so gelöst.