Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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 <T> custom type
* @param <N> type with an argument native to vanilla Minecraft
*/
public interface CustomArgumentType<T, N> extends ArgumentType<T> {

/**
* 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<N> 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<String> getExamples() {
return this.getNativeType().getExamples();
}

/**
* Provides a list of suggestions to show to the client.
*
* @param context command context
* @param builder suggestion builder
* @param <S> context type
* @return suggestions
*/
@Override
default <S> CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> 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.
*
* <p>The parsed native type will be converted via {@link #convert(Object)}.
* Implement {@link CustomArgumentType} if you want to handle parsing the type manually.
*
* @param <T> custom type
* @param <N> type with an argument native to vanilla Minecraft
*/
interface Converted<T, N> extends CustomArgumentType<T, N> {

@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,7 +87,7 @@ public void inject(final RootCommandNode<S> dest, final S source) {

final LiteralCommandNode<S> asLiteral = (LiteralCommandNode<S>) node;
final LiteralCommandNode<S> copy = asLiteral.createBuilder().build();
final VelocityArgumentCommandNode<S, ?> argsNode =
final CustomArgumentCommandNode<S, ?, ?> argsNode =
VelocityCommands.getArgumentsNode(asLiteral);
if (argsNode == null) {
// This literal is associated to a BrigadierCommand, filter normally.
Expand Down Expand Up @@ -117,7 +117,14 @@ public void inject(final RootCommandNode<S> dest, final S source) {
if (!node.canUse(source)) {
return null;
}
final ArgumentBuilder<S, ?> builder = node.createBuilder();

final ArgumentBuilder<S, ?> builder;
if (node instanceof CustomArgumentCommandNode<S, ?, ?> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -194,7 +194,7 @@ private CompletableFuture<Suggestions> provideArgumentsSuggestions(
final CommandContextBuilder<S> contextSoFar) {
final S source = contextSoFar.getSource();
final String fullInput = reader.getString();
final VelocityArgumentCommandNode<S, ?> argsNode = VelocityCommands.getArgumentsNode(alias);
final CustomArgumentCommandNode<S, ?, ?> argsNode = VelocityCommands.getArgumentsNode(alias);
if (argsNode == null) {
// This is a BrigadierCommand, fallback to regular suggestions
reader.setCursor(0);
Expand Down Expand Up @@ -252,7 +252,7 @@ private CompletableFuture<Suggestions> provideArgumentsSuggestions(
* @return a future that completes with the suggestions
*/
private CompletableFuture<Suggestions> getArgumentsNodeSuggestions(
final VelocityArgumentCommandNode<S, ?> node, final StringReader reader,
final CustomArgumentCommandNode<S, ?, ?> node, final StringReader reader,
final CommandContextBuilder<S> context) {
final int start = reader.getCursor();
final String fullInput = reader.getString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -70,34 +73,34 @@ public static CommandNode<CommandSource> wrap(final CommandNode<CommandSource> d
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
}

return switch (delegate) {
case LiteralCommandNode<CommandSource> lcn -> {
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
literalBuilder.executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
literalBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
yield literalBuilder.build();
}
case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand)
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
ArgumentBuilder<CommandSource, ?> argBuilder = switch (delegate) {
case LiteralCommandNode<CommandSource> node -> shallowCopyAsBuilder(node, delegate.getName(), true);
case CustomArgumentCommandNode<CommandSource, ?, ?> node -> node.createCustomArgBuilder();
case ArgumentCommandNode<CommandSource, ?> node -> {
var argBuilder = node.createBuilder().executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
argBuilder.then(wrap(child, registrant));
if (node.getType() instanceof CustomArgumentType<?, ?> type) {
yield CustomArgumentBuilder.<CommandSource, Object, Object>argument(node.getName(), (CustomArgumentType<Object, Object>) 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<CommandSource> child : delegate.getChildren()) {
argBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}

return argBuilder.build();
}

// Normalization
Expand Down Expand Up @@ -251,11 +254,11 @@ private static LiteralArgumentBuilder<CommandSource> shallowCopyAsBuilder(
* @param <S> the type of the command source
* @return the arguments node, or null if not present
*/
static <S> @Nullable VelocityArgumentCommandNode<S, ?> getArgumentsNode(
static <S> @Nullable CustomArgumentCommandNode<S, ?, ?> getArgumentsNode(
final LiteralCommandNode<S> alias) {
final CommandNode<S> node = alias.getChild(ARGS_NODE_NAME);
if (node instanceof VelocityArgumentCommandNode) {
return (VelocityArgumentCommandNode<S, ?>) node;
if (node instanceof CustomArgumentCommandNode) {
return (CustomArgumentCommandNode<S, ?, ?>) node;
}
return null;
}
Expand All @@ -267,7 +270,7 @@ private static LiteralArgumentBuilder<CommandSource> 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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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 <S> the type of the command source
* @param <T> the custom type of the argument to parse
* @param <N> the native type of the argument to parse
*/
public final class CustomArgumentBuilder<S, T, N> extends ArgumentBuilder<S, CustomArgumentBuilder<S, T, N>> {

/**
* 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 <S> the type of the command source
* @param <T> the type of the custom argument to parse
* @param <N> the type of the native argument to parse
* @return a builder
*/
public static <S, T, N> CustomArgumentBuilder<S, T, N> argument(final String name, final CustomArgumentType<T, N> argumentType) {
Preconditions.checkNotNull(name, "name");
Preconditions.checkNotNull(argumentType, "argument type");
return new CustomArgumentBuilder<>(name, argumentType);
}

private final String name;
private final CustomArgumentType<T, N> type;
private SuggestionProvider<S> suggestionsProvider = null;

private CustomArgumentBuilder(final String name, final CustomArgumentType<T, N> type) {
this.name = name;
this.type = type;
}

public CustomArgumentType<T, N> getType() {
return this.type;
}

public String getName() {
return this.name;
}

public CustomArgumentBuilder<S, T, N> suggests(final @Nullable SuggestionProvider<S> provider) {
this.suggestionsProvider = provider;
return this;
}

public SuggestionProvider<S> getSuggestionsProvider() {
return suggestionsProvider;
}

@Override
protected CustomArgumentBuilder<S, T, N> getThis() {
return this;
}

@Override
public CustomArgumentCommandNode<S, T, N> build() {
return new CustomArgumentCommandNode<>(this.name,
this.type,
getCommand(),
getRequirement(),
getContextRequirement(),
getRedirect(),
getRedirectModifier(),
isFork(),
this.suggestionsProvider);
}
}
Loading