From 1d72fea8c1dbe178b322ad061962c0a6e518efcd Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Sat, 24 Apr 2021 15:50:31 +0200 Subject: [PATCH] Bungee lib --- Bungee/.classpath | 27 +++ Bungee/.gitignore | 1 + Bungee/.project | 23 ++ .../org.eclipse.core.resources.prefs | 3 + Bungee/.settings/org.eclipse.jdt.core.prefs | 8 + Bungee/.settings/org.eclipse.m2e.core.prefs | 4 + Bungee/pom.xml | 43 ++++ .../lib/bungee/commands/BrigadierCommand.java | 210 +++++++++++++++++ .../bungee/commands/BrigadierDispatcher.java | 222 ++++++++++++++++++ pom.xml | 2 +- 10 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 Bungee/.classpath create mode 100644 Bungee/.gitignore create mode 100644 Bungee/.project create mode 100644 Bungee/.settings/org.eclipse.core.resources.prefs create mode 100644 Bungee/.settings/org.eclipse.jdt.core.prefs create mode 100644 Bungee/.settings/org.eclipse.m2e.core.prefs create mode 100644 Bungee/pom.xml create mode 100644 Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierCommand.java create mode 100644 Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierDispatcher.java diff --git a/Bungee/.classpath b/Bungee/.classpath new file mode 100644 index 0000000..4559ca0 --- /dev/null +++ b/Bungee/.classpath @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bungee/.gitignore b/Bungee/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/Bungee/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/Bungee/.project b/Bungee/.project new file mode 100644 index 0000000..8f7612b --- /dev/null +++ b/Bungee/.project @@ -0,0 +1,23 @@ + + + pandalib-bungee + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/Bungee/.settings/org.eclipse.core.resources.prefs b/Bungee/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..e9441bb --- /dev/null +++ b/Bungee/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding/=UTF-8 diff --git a/Bungee/.settings/org.eclipse.jdt.core.prefs b/Bungee/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..2af1e7b --- /dev/null +++ b/Bungee/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/Bungee/.settings/org.eclipse.m2e.core.prefs b/Bungee/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/Bungee/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/Bungee/pom.xml b/Bungee/pom.xml new file mode 100644 index 0000000..8fb5861 --- /dev/null +++ b/Bungee/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + + fr.pandacube.lib + pandalib-parent + 1.0-SNAPSHOT + + + pandalib-bungee + + PandaLib-Bungee + + + + papermc + https://papermc.io/repo/repository/maven-public/ + + + + + + fr.pandacube.lib + pandalib-core + ${project.version} + compile + + + + + + + fr.pandacube.bungeecord + bungeecord-proxy + ${bungeecord.version} + compile + + + + + \ No newline at end of file diff --git a/Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierCommand.java b/Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierCommand.java new file mode 100644 index 0000000..5667822 --- /dev/null +++ b/Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierCommand.java @@ -0,0 +1,210 @@ +package fr.pandacube.lib.bungee.commands; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import fr.pandacube.lib.core.chat.ChatStatic; +import fr.pandacube.lib.core.commands.SuggestionsSupplier; +import fr.pandacube.lib.core.util.Log; +import fr.pandacube.lib.core.util.ReflexionUtil; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.TabExecutor; +import net.md_5.bungee.command.ConsoleCommandSender; + +public abstract class BrigadierCommand extends ChatStatic { + + private LiteralCommandNode commandNode; + + protected BrigadierDispatcher dispatcher; + + public BrigadierCommand() { + if (BrigadierDispatcher.getInstance() == null) { + throw new IllegalStateException("BrigadierDispatcher is not yet initialized."); + } + dispatcher = BrigadierDispatcher.getInstance(); + + LiteralArgumentBuilder builder = buildCommand(); + String[] aliases = getAliases(); + if (aliases == null) + aliases = new String[0]; + + commandNode = dispatcher.register(builder); + + // still have to be registered for console + BungeeCord.getInstance().getPluginManager().registerCommand(dispatcher.plugin, new CommandRelay(commandNode.getLiteral())); + + for (String alias : aliases) { + dispatcher.register(literal(alias) + .requires(commandNode.getRequirement()) + .executes(commandNode.getCommand()) + .redirect(commandNode) + ); + + BungeeCord.getInstance().getPluginManager().registerCommand(dispatcher.plugin, new CommandRelay(alias)); + } + + } + + private class CommandRelay extends Command implements TabExecutor { + private String alias; + public CommandRelay(String alias) { + super(alias); + this.alias = alias; + } + @Override + public void execute(CommandSender sender, String[] args) { + dispatcher.execute(sender, alias + (args.length == 0 ? "" : (" " + String.join(" ", args)))); + } + @Override + public Iterable onTabComplete(CommandSender sender, String[] args) { + + String cursor = "/" + alias + " " + String.join(" ", args); + + StringRange supportedRange = StringRange.between(cursor.lastIndexOf(' ') + 1, cursor.length()); + + Suggestions suggestions = dispatcher.getSuggestions(sender, cursor.substring(1)); + if (!suggestions.getRange().equals(supportedRange)) + return Collections.emptyList(); + + return suggestions.getList() + .stream() + .filter(s -> s.getRange().equals(supportedRange)) + .map(s -> s.getText()) + .collect(Collectors.toList()); + } + } + + protected abstract LiteralArgumentBuilder buildCommand(); + + protected String[] getAliases() { + return new String[0]; + } + + + + + public static LiteralArgumentBuilder literal(String name) { + return LiteralArgumentBuilder.literal(name); + } + + public static RequiredArgumentBuilder argument(String name, ArgumentType type) { + return RequiredArgumentBuilder.argument(name, type); + } + + public static Predicate hasPermission(String permission) { + return sender -> sender.hasPermission(permission); + } + + public static Predicate isPlayer() { + return sender -> isPlayer(sender); + } + + public static boolean isPlayer(CommandSender sender) { + return sender instanceof ProxiedPlayer; + } + + public static Predicate isConsole() { + return sender -> isConsole(sender); + } + + public static boolean isConsole(CommandSender sender) { + return sender instanceof ConsoleCommandSender; + } + + + public static boolean isLiteralParsed(CommandContext context, String literal) { + for (ParsedCommandNode node : context.getNodes()) { + if (!(node.getNode() instanceof LiteralCommandNode)) + continue; + if (((LiteralCommandNode)node.getNode()).getLiteral().equals(literal)) + return true; + } + return false; + } + + public static T tryGetArgument(CommandContext context, String argument, Class type) { + return tryGetArgument(context, argument, type, null); + } + + public static T tryGetArgument(CommandContext context, String argument, Class type, T deflt) { + try { + return context.getArgument(argument, type); + } catch (IllegalArgumentException e) { + return deflt; + } + } + + + + + protected static SuggestionProvider wrapSuggestions(SuggestionsSupplier suggestions) { + return (context, builder) -> { + CommandSender sender = context.getSource(); + String message = builder.getInput(); + try { + int tokenStartPos = builder.getStart(); + + List results = Collections.emptyList(); + + int firstSpacePos = message.indexOf(" "); + String[] args = (firstSpacePos + 1 > tokenStartPos - 1) ? new String[0] + : message.substring(firstSpacePos + 1, tokenStartPos - 1).split(" ", -1); + args = Arrays.copyOf(args, args.length + 1); + args[args.length - 1] = message.substring(tokenStartPos); + + results = suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args); + + for (String s : results) { + if (s != null) + builder.suggest(s); + } + } catch (Throwable e) { + Log.severe("Error while tab-completing '" + message + "' for " + sender.getName(), e); + } + return completableFutureSuggestionsKeepsOriginalOrdering(builder); + }; + } + + + + + public static CompletableFuture completableFutureSuggestionsKeepsOriginalOrdering(SuggestionsBuilder builder) { + return CompletableFuture.completedFuture( + BrigadierDispatcher.createSuggestionsOriginalOrdering(builder.getInput(), getSuggestionsFromSuggestionsBuilder(builder)) + ); + } + + @SuppressWarnings("unchecked") + private static List getSuggestionsFromSuggestionsBuilder(SuggestionsBuilder builder) { + try { + return (List) ReflexionUtil.getDeclaredFieldValue(SuggestionsBuilder.class, builder, "result"); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + + + + + +} \ No newline at end of file diff --git a/Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierDispatcher.java b/Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierDispatcher.java new file mode 100644 index 0000000..9954a17 --- /dev/null +++ b/Bungee/src/main/java/fr/pandacube/lib/bungee/commands/BrigadierDispatcher.java @@ -0,0 +1,222 @@ +package fr.pandacube.lib.bungee.commands; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.context.SuggestionContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import fr.pandacube.lib.core.chat.Chat; +import fr.pandacube.lib.core.util.Log; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.ChatEvent; +import net.md_5.bungee.api.event.CommandsDeclareEvent; +import net.md_5.bungee.api.event.TabCompleteRequestEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +public class BrigadierDispatcher implements Listener { + + private static BrigadierDispatcher instance = null; + + public static synchronized void init(Plugin plugin) { + instance = new BrigadierDispatcher(plugin); + } + + public static synchronized BrigadierDispatcher getInstance() { + return instance; + } + + + + + + private CommandDispatcher dispatcher; + /* package */ Plugin plugin; + + private BrigadierDispatcher(Plugin pl) { + plugin = pl; + dispatcher = new CommandDispatcher<>(); + ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); + } + + + + /* package */ LiteralCommandNode register(LiteralArgumentBuilder node) { + return dispatcher.register(node); + } + + + public CommandDispatcher getDispatcher() { + return dispatcher; + } + + + + @EventHandler + public void onCommandsDeclare(CommandsDeclareEvent event) { + dispatcher.getRoot().getChildren().forEach(node -> { + event.getRoot().getChildren().remove(event.getRoot().getChild(node.getName())); // may not work in the future + event.getRoot().addChild(node); + }); + } + + + + @EventHandler + public void onTabComplete(TabCompleteRequestEvent event) { + if (!event.getCursor().startsWith("/")) + return; + + String commandLine = event.getCursor().substring(1); + + String commandName = commandLine.split(" ", -1)[0]; + + if (dispatcher.getRoot().getChild(commandName) == null) + return; + + Suggestions suggestions = getSuggestions((ProxiedPlayer) event.getSender(), commandLine); + + // shift suggestion range 1 to the right to consider the / + suggestions = new Suggestions(new StringRange(suggestions.getRange().getStart() + 1, suggestions.getRange().getEnd() + 1), suggestions.getList()); + + event.setSuggestions(suggestions); + } + + + + @EventHandler + public void onChat(ChatEvent event) { + if (!event.getMessage().startsWith("/")) + return; + + String commandLine = event.getMessage().substring(1); + + String commandName = commandLine.split(" ", -1)[0]; + + if (dispatcher.getRoot().getChild(commandName) == null) + return; + + event.setCancelled(true); + + ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { + execute((ProxiedPlayer) event.getSender(), commandLine); + }); + + } + + + + + /* package */ int execute(CommandSender sender, String commandWithoutSlash) { + ParseResults parsed = dispatcher.parse(commandWithoutSlash, sender); + + try { + return dispatcher.execute(parsed); + } catch (CommandSyntaxException e) { + sender.sendMessage(Chat.failureText("Erreur d’utilisation de la commande : " + e.getMessage()).get()); + return 0; + } catch (Throwable e) { + sender.sendMessage(Chat.failureText("Erreur lors de l’exécution de la commande : " + e.getMessage()).get()); + Log.severe(e); + return 0; + } + + } + + /* package */ Suggestions getSuggestions(CommandSender sender, String buffer) { + ParseResults parsed = dispatcher.parse(buffer, sender); + try { + CompletableFuture futureSuggestions = buildSuggestionBrigadier(parsed); + return futureSuggestions.join(); + } catch (Throwable e) { + sender.sendMessage(Chat.failureText("Erreur d’exécution des suggestions :\n" + e.getMessage()).get()); + Log.severe(e); + return Suggestions.empty().join(); + } + } + + + + CompletableFuture buildSuggestionBrigadier(ParseResults parsed) { + int cursor = parsed.getReader().getTotalLength(); + final CommandContextBuilder context = parsed.getContext(); + + final SuggestionContext nodeBeforeCursor = context.findSuggestionContext(cursor); + final CommandNode parent = nodeBeforeCursor.parent; + final int start = Math.min(nodeBeforeCursor.startPos, cursor); + + final String fullInput = parsed.getReader().getString(); + final String truncatedInput = fullInput.substring(0, cursor); + @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[parent.getChildren().size()]; + int i = 0; + for (final CommandNode node : parent.getChildren()) { + CompletableFuture future = Suggestions.empty(); + try { + future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start)); + } catch (final CommandSyntaxException ignored) { + } + futures[i++] = future; + } + + final CompletableFuture result = new CompletableFuture<>(); + CompletableFuture.allOf(futures).thenRun(() -> { + final List suggestions = new ArrayList<>(); + for (final CompletableFuture future : futures) { + suggestions.add(future.join()); + } + result.complete(mergeSuggestionsOriginalOrdering(fullInput, suggestions)); + }); + return result; + } + + // inspired from com.mojang.brigadier.suggestion.Suggestions#merge, but without the sorting part + public static Suggestions mergeSuggestionsOriginalOrdering(String input, Collection suggestions) { + if (suggestions.isEmpty()) { + return new Suggestions(StringRange.at(0), new ArrayList<>(0)); + } else if (suggestions.size() == 1) { + return suggestions.iterator().next(); + } + + final List texts = new ArrayList<>(); + for (final Suggestions suggestions1 : suggestions) { + texts.addAll(suggestions1.getList()); + } + return createSuggestionsOriginalOrdering(input, texts); + } + + // inspired from com.mojang.brigadier.suggestion.Suggestions#create, but without the sorting part + public static Suggestions createSuggestionsOriginalOrdering(String command, Collection suggestions) { + if (suggestions.isEmpty()) { + return new Suggestions(StringRange.at(0), new ArrayList<>(0)); + } + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + for (final Suggestion suggestion : suggestions) { + start = Math.min(suggestion.getRange().getStart(), start); + end = Math.max(suggestion.getRange().getEnd(), end); + } + final StringRange range = new StringRange(start, end); + final List texts = new ArrayList<>(suggestions.size()); + for (final Suggestion suggestion : suggestions) { + texts.add(suggestion.expand(command, range)); + } + return new Suggestions(range, texts); + } + +} diff --git a/pom.xml b/pom.xml index af5b7e9..bab1ff9 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ Core - + Bungee Paper