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