diff --git a/api/src/main/java/com/velocitypowered/api/command/CustomArgumentType.java b/api/src/main/java/com/velocitypowered/api/command/CustomArgumentType.java new file mode 100644 index 0000000000..166912efb7 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/CustomArgumentType.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2026 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.command; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.ApiStatus; + +/** + * An argument type that wraps around a native-to-vanilla argument type. + * This argument receives special handling in that the native argument type will + * be sent to the client for possible client-side completions and syntax validation. + * + *

When implementing this class, you have to create your own parsing logic from a + * {@link StringReader}. If only want to convert from the native type ({@code N}) to the custom + * type ({@code T}), implement {@link Converted} instead. + * + * @param custom type + * @param type with an argument native to vanilla Minecraft + */ +public interface CustomArgumentType extends ArgumentType { + + /** + * Parses the argument into the custom type ({@code T}). Keep in mind + * that this parsing will be done on the server. This means that if + * you throw a {@link CommandSyntaxException} during parsing, this + * will only show up to the user after the user has executed the command + * not while they are still entering it. + * + * @param reader string reader input + * @return parsed value + * @throws CommandSyntaxException if an error occurs while parsing + */ + @Override + T parse(final StringReader reader) throws CommandSyntaxException; + + /** + * Gets the native type that this argument uses, + * the type that is sent to the client. + * + * @return native argument type + */ + ArgumentType getNativeType(); + + /** + * Cannot be controlled by the server. + * Returned in cases where there are multiple arguments in the same node. + * This helps differentiate and tell the player what the possible inputs are. + * + * @return client set examples + */ + @Override + @ApiStatus.NonExtendable + default Collection getExamples() { + return this.getNativeType().getExamples(); + } + + /** + * Provides a list of suggestions to show to the client. + * + * @param context command context + * @param builder suggestion builder + * @param context type + * @return suggestions + */ + @Override + default CompletableFuture listSuggestions(final CommandContext context, final SuggestionsBuilder builder) { + return ArgumentType.super.listSuggestions(context, builder); + } + + /** + * An argument type that wraps around a native-to-vanilla argument type. + * This argument receives special handling in that the native argument type will + * be sent to the client for possible client-side completions and syntax validation. + * + *

The parsed native type will be converted via {@link #convert(Object)}. + * Implement {@link CustomArgumentType} if you want to handle parsing the type manually. + * + * @param custom type + * @param type with an argument native to vanilla Minecraft + */ + interface Converted extends CustomArgumentType { + + @ApiStatus.NonExtendable + @Override + default T parse(final StringReader reader) throws CommandSyntaxException { + return this.convert(this.getNativeType().parse(reader)); + } + + /** + * Converts the value from the native type to the custom argument type. + * + * @param nativeType native argument provided value + * @return converted value + * @throws CommandSyntaxException if an exception occurs while parsing + */ + T convert(N nativeType) throws CommandSyntaxException; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java index 848d76d13b..8ff6eab4da 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java @@ -26,7 +26,7 @@ import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; -import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentCommandNode; import java.util.IdentityHashMap; import java.util.Map; import java.util.concurrent.locks.Lock; @@ -87,7 +87,7 @@ public void inject(final RootCommandNode dest, final S source) { final LiteralCommandNode asLiteral = (LiteralCommandNode) node; final LiteralCommandNode copy = asLiteral.createBuilder().build(); - final VelocityArgumentCommandNode argsNode = + final CustomArgumentCommandNode argsNode = VelocityCommands.getArgumentsNode(asLiteral); if (argsNode == null) { // This literal is associated to a BrigadierCommand, filter normally. @@ -117,7 +117,14 @@ public void inject(final RootCommandNode dest, final S source) { if (!node.canUse(source)) { return null; } - final ArgumentBuilder builder = node.createBuilder(); + + final ArgumentBuilder builder; + if (node instanceof CustomArgumentCommandNode customArgumentCommandNode) { + builder = customArgumentCommandNode.createCustomArgBuilder(); + } else { + builder = node.createBuilder(); + } + if (node.getRedirect() != null) { // Redirects to non-Brigadier commands are not supported. Luckily, // we don't expose the root node to API users, so they can't access diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java b/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java index 9c65c023ae..59b200e7f7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java @@ -34,7 +34,7 @@ import com.velocitypowered.api.command.CommandMeta; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentCommandNode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -194,7 +194,7 @@ private CompletableFuture provideArgumentsSuggestions( final CommandContextBuilder contextSoFar) { final S source = contextSoFar.getSource(); final String fullInput = reader.getString(); - final VelocityArgumentCommandNode argsNode = VelocityCommands.getArgumentsNode(alias); + final CustomArgumentCommandNode argsNode = VelocityCommands.getArgumentsNode(alias); if (argsNode == null) { // This is a BrigadierCommand, fallback to regular suggestions reader.setCursor(0); @@ -252,7 +252,7 @@ private CompletableFuture provideArgumentsSuggestions( * @return a future that completes with the suggestions */ private CompletableFuture getArgumentsNodeSuggestions( - final VelocityArgumentCommandNode node, final StringReader reader, + final CustomArgumentCommandNode node, final StringReader reader, final CommandContextBuilder context) { final int start = reader.getCursor(); final String fullInput = reader.getString(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java index 1e4a01ce21..8bb1849d5c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java @@ -19,6 +19,7 @@ import com.google.common.base.Preconditions; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; @@ -31,8 +32,10 @@ import com.velocitypowered.api.command.Command; import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.CustomArgumentType; import com.velocitypowered.api.command.InvocableCommand; -import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentBuilder; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentCommandNode; import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import java.util.List; import java.util.Locale; @@ -70,34 +73,34 @@ public static CommandNode wrap(final CommandNode d maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant); } - return switch (delegate) { - case LiteralCommandNode lcn -> { - var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true); - literalBuilder.executes(maybeCommand); - // we also need to wrap any children - for (final CommandNode child : delegate.getChildren()) { - literalBuilder.then(wrap(child, registrant)); - } - if (delegate.getRedirect() != null) { - literalBuilder.redirect(wrap(delegate.getRedirect(), registrant)); - } - yield literalBuilder.build(); - } - case VelocityArgumentCommandNode vacn -> vacn.withCommand(maybeCommand) - .withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null); + ArgumentBuilder argBuilder = switch (delegate) { + case LiteralCommandNode node -> shallowCopyAsBuilder(node, delegate.getName(), true); + case CustomArgumentCommandNode node -> node.createCustomArgBuilder(); case ArgumentCommandNode node -> { - var argBuilder = node.createBuilder().executes(maybeCommand); - // we also need to wrap any children - for (final CommandNode child : delegate.getChildren()) { - argBuilder.then(wrap(child, registrant)); + if (node.getType() instanceof CustomArgumentType type) { + yield CustomArgumentBuilder.argument(node.getName(), (CustomArgumentType) type) + .requires(node.getRequirement()) + .forward(node.getRedirect(), node.getRedirectModifier(), node.isFork()) + .suggests(node.getCustomSuggestions()) + .executes(node.getCommand()); } - if (delegate.getRedirect() != null) { - argBuilder.redirect(wrap(delegate.getRedirect(), registrant)); - } - yield argBuilder.build(); + + yield node.createBuilder(); } default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass()); }; + + argBuilder.executes(maybeCommand); + + // we also need to wrap any children + for (final CommandNode child : delegate.getChildren()) { + argBuilder.then(wrap(child, registrant)); + } + if (delegate.getRedirect() != null) { + argBuilder.redirect(wrap(delegate.getRedirect(), registrant)); + } + + return argBuilder.build(); } // Normalization @@ -251,11 +254,11 @@ private static LiteralArgumentBuilder shallowCopyAsBuilder( * @param the type of the command source * @return the arguments node, or null if not present */ - static @Nullable VelocityArgumentCommandNode getArgumentsNode( + static @Nullable CustomArgumentCommandNode getArgumentsNode( final LiteralCommandNode alias) { final CommandNode node = alias.getChild(ARGS_NODE_NAME); - if (node instanceof VelocityArgumentCommandNode) { - return (VelocityArgumentCommandNode) node; + if (node instanceof CustomArgumentCommandNode) { + return (CustomArgumentCommandNode) node; } return null; } @@ -267,7 +270,7 @@ private static LiteralArgumentBuilder shallowCopyAsBuilder( * @return true if the node is an arguments node; false otherwise */ public static boolean isArgumentsNode(final CommandNode node) { - return node instanceof VelocityArgumentCommandNode && node.getName().equals(ARGS_NODE_NAME); + return node instanceof CustomArgumentCommandNode && node.getName().equals(ARGS_NODE_NAME); } private VelocityCommands() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentBuilder.java new file mode 100644 index 0000000000..246f61d4bd --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentBuilder.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.command.brigadier; + +import com.google.common.base.Preconditions; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.velocitypowered.api.command.CustomArgumentType; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A builder for creating {@link ArgumentCommandNode}s. + * + * @param the type of the command source + * @param the custom type of the argument to parse + * @param the native type of the argument to parse + */ +public final class CustomArgumentBuilder extends ArgumentBuilder> { + + /** + * Creates a builder for creating {@link ArgumentCommandNode}s with the given name and + * type. + * + * @param name the name of the node + * @param argumentType the type of the argument to parse + * @param the type of the command source + * @param the type of the custom argument to parse + * @param the type of the native argument to parse + * @return a builder + */ + public static CustomArgumentBuilder argument(final String name, final CustomArgumentType argumentType) { + Preconditions.checkNotNull(name, "name"); + Preconditions.checkNotNull(argumentType, "argument type"); + return new CustomArgumentBuilder<>(name, argumentType); + } + + private final String name; + private final CustomArgumentType type; + private SuggestionProvider suggestionsProvider = null; + + private CustomArgumentBuilder(final String name, final CustomArgumentType type) { + this.name = name; + this.type = type; + } + + public CustomArgumentType getType() { + return this.type; + } + + public String getName() { + return this.name; + } + + public CustomArgumentBuilder suggests(final @Nullable SuggestionProvider provider) { + this.suggestionsProvider = provider; + return this; + } + + public SuggestionProvider getSuggestionsProvider() { + return suggestionsProvider; + } + + @Override + protected CustomArgumentBuilder getThis() { + return this; + } + + @Override + public CustomArgumentCommandNode build() { + return new CustomArgumentCommandNode<>(this.name, + this.type, + getCommand(), + getRequirement(), + getContextRequirement(), + getRedirect(), + getRedirectModifier(), + isFork(), + this.suggestionsProvider); + } +} \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentCommandNode.java similarity index 63% rename from proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java rename to proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentCommandNode.java index e5382d000d..d9f06cc27d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentCommandNode.java @@ -23,7 +23,6 @@ import com.mojang.brigadier.RedirectModifier; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.ArgumentType; -import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; @@ -34,6 +33,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; +import com.velocitypowered.api.command.CustomArgumentType; import java.util.Collection; import java.util.concurrent.CompletableFuture; import java.util.function.BiPredicate; @@ -47,33 +47,38 @@ * @param the type of the command source * @param the type of the argument to parse */ -public class VelocityArgumentCommandNode extends ArgumentCommandNode { - - private final ArgumentType type; - - VelocityArgumentCommandNode( - final String name, final ArgumentType type, final Command command, - final Predicate requirement, - final BiPredicate, ImmutableStringReader> contextRequirement, - final CommandNode redirect, final RedirectModifier modifier, final boolean forks, - final SuggestionProvider customSuggestions) { - super(name, StringArgumentType.greedyString(), command, requirement, contextRequirement, - redirect, modifier, forks, customSuggestions); +public class CustomArgumentCommandNode extends ArgumentCommandNode { + private final CustomArgumentType type; + + CustomArgumentCommandNode( + final String name, + final CustomArgumentType type, + final Command command, + final Predicate requirement, + final BiPredicate, ImmutableStringReader> contextRequirement, + final CommandNode redirect, + final RedirectModifier modifier, + final boolean forks, + final SuggestionProvider customSuggestions) { + super(name, + type.getNativeType(), + command, + requirement, + contextRequirement, + redirect, + modifier, + forks, + customSuggestions); this.type = Preconditions.checkNotNull(type, "type"); } @Override public void parse(final StringReader reader, final CommandContextBuilder contextBuilder) throws CommandSyntaxException { - // Same as super, except we use the rich ArgumentType final int start = reader.getCursor(); final T result = this.type.parse(reader); - if (reader.canRead()) { - throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException() - .createWithContext(reader, "Expected greedy ArgumentType to parse all input"); - } - final ParsedArgument parsed = new ParsedArgument<>(start, reader.getCursor(), result); + contextBuilder.withArgument(getName(), parsed); contextBuilder.withNode(this, parsed.getRange()); } @@ -83,34 +88,39 @@ public CompletableFuture listSuggestions( final CommandContext context, final SuggestionsBuilder builder) throws CommandSyntaxException { if (getCustomSuggestions() == null) { - return Suggestions.empty(); + return this.type.listSuggestions(context, builder); + } else { + return getCustomSuggestions().getSuggestions(context, builder); } - return getCustomSuggestions().getSuggestions(context, builder); } @Override - public RequiredArgumentBuilder createBuilder() { + public RequiredArgumentBuilder createBuilder() { throw new UnsupportedOperationException(); } - public VelocityArgumentCommandNode withCommand(Command command) { - return new VelocityArgumentCommandNode<>(getName(), type, command, getRequirement(), - getContextRequirement(), getRedirect(), getRedirectModifier(), isFork(), getCustomSuggestions()); - } - - public VelocityArgumentCommandNode withRedirect(CommandNode target) { - return new VelocityArgumentCommandNode<>(getName(), type, getCommand(), getRequirement(), - getContextRequirement(), target, getRedirectModifier(), isFork(), getCustomSuggestions()); - } - - @Override - public boolean isValidInput(final String input) { - return true; + /** + * Creates a new builder from this instance. + * + * @return the builder + */ + public CustomArgumentBuilder createCustomArgBuilder() { + return CustomArgumentBuilder.argument(getName(), this.type) + .suggests(getCustomSuggestions()) + .requires(getRequirement()) + .forward(getRedirect(), getRedirectModifier(), isFork()) + .executes(getCommand()); } @Override - public void addChild(final CommandNode node) { - throw new UnsupportedOperationException("Cannot add children to a greedy node"); + public boolean isValidInput(String input) { + try { + final StringReader reader = new StringReader(input); + this.type.parse(reader); + return !reader.canRead() || reader.peek() == ' '; + } catch (final CommandSyntaxException ignored) { + return false; + } } @Override @@ -118,7 +128,7 @@ public boolean equals(final Object o) { if (this == o) { return true; } - if (!(o instanceof VelocityArgumentCommandNode that)) { + if (!(o instanceof CustomArgumentCommandNode that)) { return false; } if (!super.equals(that)) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java index 4b19bb062f..1c0cc2a205 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java @@ -21,7 +21,9 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.velocitypowered.api.command.CustomArgumentType; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -30,7 +32,7 @@ * An argument type that parses the remaining contents of a {@link StringReader}, splitting the * input into words and placing the results in a string array. */ -public final class StringArrayArgumentType implements ArgumentType { +public final class StringArrayArgumentType implements CustomArgumentType { public static final StringArrayArgumentType INSTANCE = new StringArrayArgumentType(); public static final String[] EMPTY = new String[0]; @@ -52,6 +54,11 @@ public String[] parse(final StringReader reader) throws CommandSyntaxException { return WORD_SPLITTER.splitToList(text).toArray(EMPTY); } + @Override + public ArgumentType getNativeType() { + return StringArgumentType.greedyString(); + } + @Override public String toString() { return "stringArray()"; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentBuilder.java deleted file mode 100644 index 6badf80af3..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2021-2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.command.brigadier; - -import com.google.common.base.Preconditions; -import com.mojang.brigadier.arguments.ArgumentType; -import com.mojang.brigadier.builder.ArgumentBuilder; -import com.mojang.brigadier.suggestion.SuggestionProvider; -import com.mojang.brigadier.tree.CommandNode; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * A builder for creating {@link VelocityArgumentCommandNode}s. - * - * @param the type of the command source - * @param the type of the argument to parse - */ -public final class VelocityArgumentBuilder - extends ArgumentBuilder> { - - /** - * Creates a builder for creating {@link VelocityArgumentCommandNode}s with the given name and - * type. - * - * @param name the name of the node - * @param type the type of the argument to parse - * @param the type of the command source - * @param the type of the argument to parse - * @return a builder - */ - public static VelocityArgumentBuilder velocityArgument(final String name, - final ArgumentType type) { - Preconditions.checkNotNull(name, "name"); - Preconditions.checkNotNull(type, "type"); - return new VelocityArgumentBuilder<>(name, type); - } - - private final String name; - private final ArgumentType type; - private SuggestionProvider suggestionsProvider = null; - - private VelocityArgumentBuilder(final String name, final ArgumentType type) { - this.name = name; - this.type = type; - } - - public VelocityArgumentBuilder suggests(final @Nullable SuggestionProvider provider) { - this.suggestionsProvider = provider; - return this; - } - - @Override - public VelocityArgumentBuilder then(final ArgumentBuilder argument) { - throw new UnsupportedOperationException("Cannot add children to a greedy node"); - } - - @Override - public VelocityArgumentBuilder then(final CommandNode argument) { - throw new UnsupportedOperationException("Cannot add children to a greedy node"); - } - - @Override - protected VelocityArgumentBuilder getThis() { - return this; - } - - @Override - public VelocityArgumentCommandNode build() { - return new VelocityArgumentCommandNode<>(this.name, this.type, getCommand(), getRequirement(), - getContextRequirement(), getRedirect(), getRedirectModifier(), isFork(), - this.suggestionsProvider); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java index 847d67d161..8336ed233c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java @@ -19,19 +19,19 @@ import com.google.common.base.Preconditions; import com.mojang.brigadier.Command; -import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContextBuilder; -import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; import com.velocitypowered.api.command.CommandInvocation; import com.velocitypowered.api.command.CommandMeta; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.CustomArgumentType; import com.velocitypowered.api.command.InvocableCommand; import com.velocitypowered.proxy.command.VelocityCommandMeta; import com.velocitypowered.proxy.command.VelocityCommands; -import com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentBuilder; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentCommandNode; import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import com.velocitypowered.proxy.command.invocation.CommandInvocationFactory; import java.util.Iterator; @@ -43,14 +43,14 @@ * {@link InvocableCommand} in a root node. */ abstract class InvocableCommandRegistrar, - I extends CommandInvocation, A> extends AbstractCommandRegistrar { + I extends CommandInvocation, A, N> extends AbstractCommandRegistrar { private final CommandInvocationFactory invocationFactory; - private final ArgumentType argumentsType; + private final CustomArgumentType argumentsType; protected InvocableCommandRegistrar(final RootCommandNode root, final Lock lock, final CommandInvocationFactory invocationFactory, - final ArgumentType argumentsType) { + final CustomArgumentType argumentsType) { super(root, lock); this.invocationFactory = Preconditions.checkNotNull(invocationFactory, "invocationFactory"); this.argumentsType = Preconditions.checkNotNull(argumentsType, "argumentsType"); @@ -98,8 +98,8 @@ private LiteralCommandNode createLiteral(final T command, final C .executes(callback) .build(); - final ArgumentCommandNode arguments = VelocityArgumentBuilder - .velocityArgument(VelocityCommands.ARGS_NODE_NAME, argumentsType) + final CustomArgumentCommandNode arguments = CustomArgumentBuilder + .argument(VelocityCommands.ARGS_NODE_NAME, argumentsType) .requiresWithContext((context, reader) -> requirement.test(context)) .executes(callback) .suggests((context, builder) -> { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java index ad5a6cebd8..e84f7b2f01 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java @@ -17,9 +17,11 @@ package com.velocitypowered.proxy.command.registrar; +import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.tree.RootCommandNode; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.CustomArgumentType; import com.velocitypowered.api.command.RawCommand; import com.velocitypowered.proxy.command.invocation.RawCommandInvocation; import java.util.concurrent.locks.Lock; @@ -28,10 +30,22 @@ * Registers {@link RawCommand}s in a root node. */ public final class RawCommandRegistrar - extends InvocableCommandRegistrar { + extends InvocableCommandRegistrar { + + private static final CustomArgumentType.Converted nopCustomArgument = new CustomArgumentType.Converted<>() { + @Override + public String convert(String nativeType) { + return nativeType; + } + + @Override + public ArgumentType getNativeType() { + return StringArgumentType.greedyString(); + } + }; public RawCommandRegistrar(final RootCommandNode root, final Lock lock) { - super(root, lock, RawCommandInvocation.FACTORY, StringArgumentType.greedyString()); + super(root, lock, RawCommandInvocation.FACTORY, nopCustomArgument); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java index 67fd5f1f22..2f2e635d90 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java @@ -28,7 +28,7 @@ * Registers {@link SimpleCommand}s in a root node. */ public final class SimpleCommandRegistrar - extends InvocableCommandRegistrar { + extends InvocableCommandRegistrar { public SimpleCommandRegistrar(final RootCommandNode root, final Lock lock) { super(root, lock, SimpleCommandInvocation.FACTORY, StringArrayArgumentType.INSTANCE); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java index 6653d7633c..9f43c31a6e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java @@ -33,6 +33,8 @@ import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.proxy.command.brigadier.CustomArgumentCommandNode; +import com.velocitypowered.api.command.CustomArgumentType; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -43,6 +45,8 @@ import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; + +import java.lang.reflect.Method; import java.util.ArrayDeque; import java.util.Deque; import java.util.Iterator; @@ -154,10 +158,19 @@ private static void serializeNode(CommandNode node, ByteBuf buf, if (node instanceof LiteralCommandNode) { flags |= NODE_TYPE_LITERAL; - } else if (node instanceof ArgumentCommandNode) { + } else if (node instanceof ArgumentCommandNode argumentCommandNode) { flags |= NODE_TYPE_ARGUMENT; - if (((ArgumentCommandNode) node).getCustomSuggestions() != null) { + + if (argumentCommandNode.getCustomSuggestions() != null) { flags |= FLAG_HAS_SUGGESTIONS; + } else if (argumentCommandNode instanceof CustomArgumentCommandNode customArgumentCommandNode) { + try { + Method listSuggestionsMethod = customArgumentCommandNode.getType().getClass().getMethod("listSuggestions", CommandContext.class, SuggestionsBuilder.class); + if (listSuggestionsMethod.getDeclaringClass() != CustomArgumentType.class) { + flags |= FLAG_HAS_SUGGESTIONS; + } + } catch (NoSuchMethodException ignored) { + } } } else if (!(node instanceof RootCommandNode)) { throw new IllegalArgumentException("Unknown node type " + node.getClass().getName()); @@ -172,22 +185,24 @@ private static void serializeNode(CommandNode node, ByteBuf buf, ProtocolUtils.writeVarInt(buf, idMappings.getInt(node.getRedirect())); } - if (node instanceof ArgumentCommandNode) { + if (!(node instanceof RootCommandNode)) { ProtocolUtils.writeString(buf, node.getName()); - ArgumentPropertyRegistry.serialize(buf, - ((ArgumentCommandNode) node).getType(), protocolVersion); + } + + if (node instanceof ArgumentCommandNode argumentCommandNode) { + ArgumentPropertyRegistry.serialize(buf, argumentCommandNode.getType(), protocolVersion); - if (((ArgumentCommandNode) node).getCustomSuggestions() != null) { - SuggestionProvider provider = ((ArgumentCommandNode) node) - .getCustomSuggestions(); - String name = "minecraft:ask_server"; + if ((flags & FLAG_HAS_SUGGESTIONS) != 0) { + final SuggestionProvider provider = argumentCommandNode.getCustomSuggestions() != null + ? argumentCommandNode.getCustomSuggestions() + : argumentCommandNode::listSuggestions; + + String providerKey = "minecraft:ask_server"; if (provider instanceof ProtocolSuggestionProvider) { - name = ((ProtocolSuggestionProvider) provider).name; + providerKey = ((ProtocolSuggestionProvider) provider).name; } - ProtocolUtils.writeString(buf, name); + ProtocolUtils.writeString(buf, providerKey); } - } else if (node instanceof LiteralCommandNode) { - ProtocolUtils.writeString(buf, node.getName()); } } diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNodeTests.java b/proxy/src/test/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentCommandNodeTests.java similarity index 87% rename from proxy/src/test/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNodeTests.java rename to proxy/src/test/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentCommandNodeTests.java index 90420da02f..7cf1c4972f 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNodeTests.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/brigadier/CustomArgumentCommandNodeTests.java @@ -17,7 +17,6 @@ package com.velocitypowered.proxy.command.brigadier; -import static com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder.velocityArgument; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -36,10 +35,10 @@ import org.junit.jupiter.api.Test; /** - * Tests for {@link VelocityArgumentCommandNode}. + * Tests for {@link CustomArgumentCommandNode}. */ @SuppressWarnings("unchecked") -public class VelocityArgumentCommandNodeTests { +public class CustomArgumentCommandNodeTests { private static final StringArrayArgumentType STRING_ARRAY = StringArrayArgumentType.INSTANCE; @@ -54,8 +53,8 @@ void setUp() { @Test void testParse() throws CommandSyntaxException { - final VelocityArgumentCommandNode node = - velocityArgument("foo", STRING_ARRAY).build(); + final CustomArgumentCommandNode node = + CustomArgumentBuilder.argument("foo", STRING_ARRAY).build(); final StringReader reader = new StringReader("hello world"); node.parse(reader, this.contextBuilder); @@ -76,8 +75,8 @@ void testParse() throws CommandSyntaxException { @Test void testDefaultSuggestions() throws CommandSyntaxException { - final VelocityArgumentCommandNode node = - velocityArgument("foo", STRING_ARRAY).build(); + final CustomArgumentCommandNode node = + CustomArgumentBuilder.argument("foo", STRING_ARRAY).build(); final Suggestions result = node.listSuggestions( this.contextBuilder.build(""), new SuggestionsBuilder("", 0)).join(); @@ -88,8 +87,8 @@ void testDefaultSuggestions() throws CommandSyntaxException { // and filtering is already tested in Brigadier. @Test void testCustomSuggestions() throws CommandSyntaxException { - final VelocityArgumentCommandNode node = - velocityArgument("foo", STRING_ARRAY) + final CustomArgumentCommandNode node = + CustomArgumentBuilder.argument("foo", STRING_ARRAY) .suggests((context, builder) -> { builder.suggest("bar"); builder.suggest("baz");