From 6f0dba0bf00a9aa5a3a42ac847c38b3cb4aec753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Sun, 20 Sep 2020 19:49:25 +0200 Subject: [PATCH] Add asynchronous tab completion to the paper module --- .../commands/CommandManager.java | 4 +- .../commands/arguments/CommandArgument.java | 6 +- .../DelegatingSuggestionsProvider.java | 54 ++++++++++++++++ .../arguments/standard/StringArgument.java | 9 ++- .../commands/context/CommandContext.java | 21 +++++++ .../context/CommandContextFactory.java | 3 +- .../StandardCommandContextFactory.java | 7 ++- .../commands/CommandSuggestionsTest.java | 5 +- .../commands/BukkitTest.java | 9 ++- .../commands/bukkit/BukkitCommand.java | 11 +++- .../commands/bungee/BungeeCommand.java | 12 +++- .../AsyncCommandSuggestionsListener.java | 62 +++++++++++++++++++ .../commands/paper/PaperCommandManager.java | 8 +++ .../VelocityPluginRegistrationHandler.java | 13 +++- 14 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/arguments/DelegatingSuggestionsProvider.java create mode 100644 cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/AsyncCommandSuggestionsListener.java diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java index d2578caa..1f2cfa63 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java @@ -122,7 +122,7 @@ public abstract class CommandManager { */ @Nonnull public CompletableFuture> executeCommand(@Nonnull final C commandSender, @Nonnull final String input) { - final CommandContext context = this.commandContextFactory.create(commandSender); + final CommandContext context = this.commandContextFactory.create(false, commandSender); final LinkedList inputQueue = this.tokenize(input); try { if (this.preprocessContext(context, inputQueue) == State.ACCEPTED) { @@ -147,7 +147,7 @@ public abstract class CommandManager { */ @Nonnull public List suggest(@Nonnull final C commandSender, @Nonnull final String input) { - final CommandContext context = this.commandContextFactory.create(commandSender); + final CommandContext context = this.commandContextFactory.create(true, commandSender); final LinkedList inputQueue = this.tokenize(input); if (this.preprocessContext(context, inputQueue) == State.ACCEPTED) { return this.commandSuggestionProcessor.apply(new CommandPreprocessingContext<>(context, inputQueue), diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java index eee67478..0b0f55f7 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java @@ -131,7 +131,7 @@ public class CommandArgument implements Comparable> private static BiFunction, String, List> buildDefaultSuggestionsProvider( @Nonnull final CommandArgument argument) { - return (context, s) -> argument.getParser().suggestions(context, s); + return new DelegatingSuggestionsProvider<>(argument.getName(), argument.getParser()); } /** @@ -404,8 +404,8 @@ public class CommandArgument implements Comparable> this.parser = (c, i) -> ArgumentParseResult .failure(new UnsupportedOperationException("No parser was specified")); } - if (suggestionsProvider == null) { - suggestionsProvider = this.parser::suggestions; + if (this.suggestionsProvider == null) { + this.suggestionsProvider = new DelegatingSuggestionsProvider<>(this.name, this.parser); } return new CommandArgument<>(this.required, this.name, this.parser, this.defaultValue, this.valueType, this.suggestionsProvider); diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/DelegatingSuggestionsProvider.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/DelegatingSuggestionsProvider.java new file mode 100644 index 00000000..b2a1598b --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/DelegatingSuggestionsProvider.java @@ -0,0 +1,54 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.arguments; + +import com.intellectualsites.commands.arguments.parser.ArgumentParser; +import com.intellectualsites.commands.context.CommandContext; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.BiFunction; + +final class DelegatingSuggestionsProvider implements BiFunction, String, List> { + + private final String argumentName; + private final ArgumentParser parser; + + DelegatingSuggestionsProvider(@Nonnull final String argumentName, @Nonnull final ArgumentParser parser) { + this.argumentName = argumentName; + this.parser = parser; + } + + @Override + public List apply(final CommandContext context, final String s) { + return this.parser.suggestions(context, s); + } + + @Override + public String toString() { + return String.format("DelegatingSuggestionsProvider{name='%s',parser='%s'}", this.argumentName, + this.parser.getClass().getCanonicalName()); + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/StringArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/StringArgument.java index 45d0a712..509463b7 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/StringArgument.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/StringArgument.java @@ -215,6 +215,14 @@ public final class StringArgument extends CommandArgument { } if (this.stringMode == StringMode.SINGLE) { + if (commandContext.isSuggestions()) { + final List suggestions = this.suggestionsProvider.apply(commandContext, inputQueue.peek()); + if (!suggestions.isEmpty() && !suggestions.contains(input)) { + return ArgumentParseResult.failure(new IllegalArgumentException( + String.format("'%s' is not one of: %s", input, String.join(", ", suggestions)) + )); + } + } inputQueue.remove(); return ArgumentParseResult.success(input); } @@ -222,7 +230,6 @@ public final class StringArgument extends CommandArgument { final StringJoiner sj = new StringJoiner(" "); final int size = inputQueue.size(); - boolean started = false; boolean finished = false; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java b/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java index 53b6c7c7..ffa9dafa 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContext.java @@ -37,6 +37,7 @@ public final class CommandContext { private final Map internalStorage = new HashMap<>(); private final C commandSender; + private final boolean suggestions; /** * Create a new command context instance @@ -44,7 +45,18 @@ public final class CommandContext { * @param commandSender Sender of the command */ public CommandContext(@Nonnull final C commandSender) { + this(false, commandSender); + } + + /** + * Create a new command context instance + * + * @param suggestions Whether or not the context is created for command suggestions + * @param commandSender Sender of the command + */ + public CommandContext(final boolean suggestions, @Nonnull final C commandSender) { this.commandSender = commandSender; + this.suggestions = suggestions; } /** @@ -57,6 +69,15 @@ public final class CommandContext { return this.commandSender; } + /** + * Check if this context was created for tab completion purposes + * + * @return {@code true} if this context is requesting suggestions, else {@code false} + */ + public boolean isSuggestions() { + return this.suggestions; + } + /** * Store a value in the context map * diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContextFactory.java b/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContextFactory.java index c4a84b7c..5f82fbbf 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContextFactory.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/context/CommandContextFactory.java @@ -35,10 +35,11 @@ public interface CommandContextFactory { /** * Create a new command context * + * @param suggestions Whether or not the sender is requesting suggestions * @param sender Command sender * @return Command context */ @Nonnull - CommandContext create(@Nonnull C sender); + CommandContext create(boolean suggestions, @Nonnull C sender); } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/context/StandardCommandContextFactory.java b/cloud-core/src/main/java/com/intellectualsites/commands/context/StandardCommandContextFactory.java index 6b3ffedd..4f07e030 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/context/StandardCommandContextFactory.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/context/StandardCommandContextFactory.java @@ -30,13 +30,14 @@ public final class StandardCommandContextFactory implements CommandContextFac /** * Construct a new command context * - * @param sender Command sender + * @param suggestions Whether or not the sender is requesting suggestions + * @param sender Command sender * @return Created context */ @Nonnull @Override - public CommandContext create(@Nonnull final C sender) { - return new CommandContext<>(sender); + public CommandContext create(final boolean suggestions, @Nonnull final C sender) { + return new CommandContext<>(suggestions, sender); } } diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java b/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java index 124f9092..c52325c4 100644 --- a/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java @@ -73,6 +73,9 @@ public class CommandSuggestionsTest { final String input2 = "test "; final List suggestions2 = manager.suggest(new TestCommandSender(), input2); Assertions.assertEquals(Arrays.asList("alt", "comb", "one", "two", "var"), suggestions2); + final String input3 = "test a"; + final List suggestions3 = manager.suggest(new TestCommandSender(), input3); + Assertions.assertEquals(Collections.singletonList("alt"), suggestions3); } @Test @@ -82,7 +85,7 @@ public class CommandSuggestionsTest { Assertions.assertTrue(suggestions.isEmpty()); final String input2 = "test var one"; final List suggestions2 = manager.suggest(new TestCommandSender(), input2); - Assertions.assertTrue(suggestions2.isEmpty()); + Assertions.assertEquals(Collections.emptyList(), suggestions2); final String input3 = "test var one f"; final List suggestions3 = manager.suggest(new TestCommandSender(), input3); Assertions.assertEquals(Collections.singletonList("foo"), suggestions3); diff --git a/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java b/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java index e7dad8c0..6cf01e75 100644 --- a/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java +++ b/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java @@ -88,6 +88,12 @@ public final class BukkitTest extends JavaPlugin { getLogger().warning("Failed to initialize Brigadier support: " + e.getMessage()); } + try { + ((PaperCommandManager) mgr).registerAsynchronousCompletions(); + } catch (final Throwable e) { + getLogger().warning("Failed to register asynchronous command completions: " + e.getMessage()); + } + final AnnotationParser annotationParser = new AnnotationParser<>(mgr, CommandSender.class, p -> BukkitCommandMetaBuilder.builder().withDescription(p.get(StandardParameters.DESCRIPTION, @@ -180,7 +186,8 @@ public final class BukkitTest extends JavaPlugin { @CommandMethod(value = "annotation|a [number]", permission = "some.permission.node") private void annotatedCommand(@Nonnull final Player player, @Argument("input") @Completions("one,two,duck") @Nonnull final String input, - @Argument("number") @Range(min = "10", max = "100") final int number) { + @Argument(value = "number", defaultValue = "5") @Range(min = "10", max = "100") + final int number) { player.sendMessage(ChatColor.GOLD + "Your input was: " + ChatColor.AQUA + input + ChatColor.GREEN + " (" + number + ")"); } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java index c20de02e..90d88c57 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java @@ -37,6 +37,7 @@ import org.bukkit.plugin.Plugin; import javax.annotation.Nonnull; import java.util.List; +import java.util.concurrent.CompletionException; final class BukkitCommand extends org.bukkit.command.Command implements PluginIdentifiableCommand { @@ -76,6 +77,10 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi builder.toString()) .whenComplete(((commandResult, throwable) -> { if (throwable != null) { + if (throwable instanceof CompletionException) { + throwable = throwable.getCause(); + } + final Throwable finalThrowable = throwable; if (throwable instanceof InvalidSyntaxException) { this.manager.handleException(sender, InvalidSyntaxException.class, @@ -84,7 +89,7 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi ChatColor.RED + "Invalid Command Syntax. " + "Correct command syntax is: " + ChatColor.GRAY + "/" - + ((InvalidSyntaxException) throwable) + + ((InvalidSyntaxException) finalThrowable) .getCorrectSyntax()) ); } else if (throwable instanceof InvalidCommandSenderException) { @@ -92,7 +97,7 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi InvalidCommandSenderException.class, (InvalidCommandSenderException) throwable, (c, e) -> commandSender.sendMessage( - ChatColor.RED + throwable.getMessage()) + ChatColor.RED + finalThrowable.getMessage()) ); } else if (throwable instanceof NoPermissionException) { this.manager.handleException(sender, @@ -112,7 +117,7 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi (ArgumentParseException) throwable, (c, e) -> commandSender.sendMessage( ChatColor.RED + "Invalid Command Argument: " - + ChatColor.GRAY + throwable.getCause() + + ChatColor.GRAY + finalThrowable.getCause() .getMessage()) ); } else { diff --git a/cloud-minecraft/cloud-bungee/src/main/java/com/intellectualsites/commands/bungee/BungeeCommand.java b/cloud-minecraft/cloud-bungee/src/main/java/com/intellectualsites/commands/bungee/BungeeCommand.java index e13df31a..1ba28911 100644 --- a/cloud-minecraft/cloud-bungee/src/main/java/com/intellectualsites/commands/bungee/BungeeCommand.java +++ b/cloud-minecraft/cloud-bungee/src/main/java/com/intellectualsites/commands/bungee/BungeeCommand.java @@ -37,6 +37,7 @@ import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.TabExecutor; import javax.annotation.Nonnull; +import java.util.concurrent.CompletionException; public final class BungeeCommand extends Command implements TabExecutor { @@ -73,6 +74,10 @@ public final class BungeeCommand extends Command implements TabExecutor { builder.toString()) .whenComplete(((commandResult, throwable) -> { if (throwable != null) { + if (throwable instanceof CompletionException) { + throwable = throwable.getCause(); + } + final Throwable finalThrowable = throwable; if (throwable instanceof InvalidSyntaxException) { this.manager.handleException(sender, InvalidSyntaxException.class, @@ -82,16 +87,17 @@ public final class BungeeCommand extends Command implements TabExecutor { .color(ChatColor.RED) .append("/") .color(ChatColor.GRAY) - .append(((InvalidSyntaxException) throwable).getCorrectSyntax()) + .append(((InvalidSyntaxException) finalThrowable).getCorrectSyntax()) .color(ChatColor.GRAY) .create() ) ); } else if (throwable instanceof InvalidCommandSenderException) { + final Throwable finalThrowable1 = throwable; this.manager.handleException(sender, InvalidCommandSenderException.class, (InvalidCommandSenderException) throwable, (c, e) -> - commandSender.sendMessage(new ComponentBuilder(throwable.getMessage()) + commandSender.sendMessage(new ComponentBuilder(finalThrowable1.getMessage()) .color(ChatColor.RED) .create()) ); @@ -117,7 +123,7 @@ public final class BungeeCommand extends Command implements TabExecutor { (ArgumentParseException) throwable, (c, e) -> commandSender.sendMessage(new ComponentBuilder("Invalid Command Argument: ") .color(ChatColor.GRAY) - .append(throwable.getCause().getMessage()) + .append(finalThrowable.getCause().getMessage()) .create()) ); } else { diff --git a/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/AsyncCommandSuggestionsListener.java b/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/AsyncCommandSuggestionsListener.java new file mode 100644 index 00000000..0198d423 --- /dev/null +++ b/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/AsyncCommandSuggestionsListener.java @@ -0,0 +1,62 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.paper; + +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; +import org.bukkit.command.CommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +class AsyncCommandSuggestionsListener implements Listener { + + private static final long CACHE_EXPIRATION_TIME = 30L; + + private final PaperCommandManager paperCommandManager; + + AsyncCommandSuggestionsListener(@Nonnull final PaperCommandManager paperCommandManager) { + this.paperCommandManager = paperCommandManager; + } + + @EventHandler + void onTabCompletion(@Nonnull final AsyncTabCompleteEvent event) throws Exception { + if (event.getBuffer().isEmpty() || !event.getBuffer().startsWith("/")) { + return; + } + final String[] arguments = event.getBuffer().substring(1).split(" "); + if (paperCommandManager.getCommandTree().getNamedNode(arguments[0]) == null) { + return; + } + final CommandSender sender = event.getSender(); + final C cloudSender = this.paperCommandManager.getCommandSenderMapper().apply(sender); + final List suggestions = new ArrayList<>(this.paperCommandManager.suggest(cloudSender, + event.getBuffer().substring(1))); + event.setCompletions(suggestions); + event.setHandled(true); + } + +} diff --git a/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/PaperCommandManager.java b/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/PaperCommandManager.java index 4c2140d4..098b086f 100644 --- a/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/PaperCommandManager.java +++ b/cloud-minecraft/cloud-paper/src/main/java/com/intellectualsites/commands/paper/PaperCommandManager.java @@ -82,4 +82,12 @@ public class PaperCommandManager extends BukkitCommandManager { } } + /** + * Register asynchronous completions. This requires all argument parsers to be thread safe, and it + * is up to the caller to guarantee that such is the case + */ + public void registerAsynchronousCompletions() { + Bukkit.getServer().getPluginManager().registerEvents(new AsyncCommandSuggestionsListener<>(this), this.getOwningPlugin()); + } + } diff --git a/cloud-minecraft/cloud-velocity/src/main/java/com/intellectualsites/commands/velocity/VelocityPluginRegistrationHandler.java b/cloud-minecraft/cloud-velocity/src/main/java/com/intellectualsites/commands/velocity/VelocityPluginRegistrationHandler.java index b3687777..79ae1ee7 100644 --- a/cloud-minecraft/cloud-velocity/src/main/java/com/intellectualsites/commands/velocity/VelocityPluginRegistrationHandler.java +++ b/cloud-minecraft/cloud-velocity/src/main/java/com/intellectualsites/commands/velocity/VelocityPluginRegistrationHandler.java @@ -44,6 +44,7 @@ import javax.annotation.Nonnull; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletionException; final class VelocityPluginRegistrationHandler implements CommandRegistrationHandler { @@ -84,6 +85,10 @@ final class VelocityPluginRegistrationHandler implements CommandRegistrationH final C sender = this.manager.getCommandSenderMapper().apply(source); this.manager.executeCommand(sender, input).whenComplete((result, throwable) -> { if (throwable != null) { + if (throwable instanceof CompletionException) { + throwable = throwable.getCause(); + } + final Throwable finalThrowable = throwable; if (throwable instanceof InvalidSyntaxException) { this.manager.handleException(sender, InvalidSyntaxException.class, @@ -96,7 +101,7 @@ final class VelocityPluginRegistrationHandler implements CommandRegistrationH this.manager.handleException(sender, InvalidCommandSenderException.class, (InvalidCommandSenderException) throwable, (c, e) -> - source.sendMessage(TextComponent.of(throwable.getMessage()).color(NamedTextColor.RED)) + source.sendMessage(TextComponent.of(finalThrowable.getMessage()).color(NamedTextColor.RED)) ); } else if (throwable instanceof NoPermissionException) { this.manager.handleException(sender, @@ -114,8 +119,10 @@ final class VelocityPluginRegistrationHandler implements CommandRegistrationH this.manager.handleException(sender, ArgumentParseException.class, (ArgumentParseException) throwable, (c, e) -> - source.sendMessage(TextComponent.builder("Invalid Command Argument: ", NamedTextColor.RED) - .append(throwable.getCause().getMessage(), NamedTextColor.GRAY) + source.sendMessage(TextComponent.builder("Invalid Command Argument: ", + NamedTextColor.RED) + .append(finalThrowable.getCause().getMessage(), + NamedTextColor.GRAY) .build()) ); } else {