brigadier: Allow specifying a custom brigadier suggestions provider

This commit is contained in:
Zach Levis 2021-01-05 12:20:30 -08:00 committed by Jason
parent 0af44e2406
commit 98aea50d3c
3 changed files with 315 additions and 44 deletions

View file

@ -0,0 +1,125 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.brigadier;
import cloud.commandframework.arguments.parser.ArgumentParser;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
final class BrigadierMapping<C, K extends ArgumentParser<C, ?>, S> {
private final boolean cloudSuggestions;
private final BrigadierMappingBuilder.@Nullable SuggestionProviderSupplier<K, S> suggestionsOverride;
private final @Nullable Function<K, ? extends ArgumentType<?>> mapper;
BrigadierMapping(
final boolean cloudSuggestions,
final BrigadierMappingBuilder.@Nullable SuggestionProviderSupplier<K, S> suggestionsOverride,
final @Nullable Function<K, ? extends ArgumentType<?>> mapper
) {
this.cloudSuggestions = cloudSuggestions;
this.suggestionsOverride = suggestionsOverride;
this.mapper = mapper;
}
public @Nullable Function<K, ? extends ArgumentType<?>> getMapper() {
return this.mapper;
}
public @NonNull BrigadierMapping<C, K, S> withNativeSuggestions(final boolean nativeSuggestions) {
if (nativeSuggestions && this.cloudSuggestions) {
return new BrigadierMapping<>(false, this.suggestionsOverride, this.mapper);
} else if (!nativeSuggestions && !this.cloudSuggestions) {
return new BrigadierMapping<>(true, this.suggestionsOverride, this.mapper);
}
return this;
}
@SuppressWarnings("unchecked")
public @Nullable SuggestionProvider<S> makeSuggestionProvider(final K commandArgument) {
if (this.cloudSuggestions) {
return CloudBrigadierManager.delegateSuggestions();
}
return this.suggestionsOverride == null
? null
: (SuggestionProvider<S>) this.suggestionsOverride.provide(commandArgument, CloudBrigadierManager.delegateSuggestions());
}
static final class BuilderImpl<C, K extends ArgumentParser<C, ?>, S> implements BrigadierMappingBuilder<K, S> {
private Function<K, ? extends ArgumentType<?>> mapper;
private boolean cloudSuggestions = false;
private SuggestionProviderSupplier<K, S> suggestionsOverride;
@Override
public BrigadierMappingBuilder<K, S> toConstant(final ArgumentType<?> constant) {
return this.to(parser -> constant);
}
@Override
public BrigadierMappingBuilder<K, S> to(final Function<K, ? extends ArgumentType<?>> mapper) {
this.mapper = mapper;
return this;
}
@Override
public BrigadierMappingBuilder<K, S> nativeSuggestions() {
this.cloudSuggestions = false;
this.suggestionsOverride = null;
return this;
}
@Override
public BrigadierMappingBuilder<K, S> cloudSuggestions() {
this.cloudSuggestions = true;
this.suggestionsOverride = null;
return this;
}
@Override
public BrigadierMappingBuilder<K, S> suggestedByConstant(final SuggestionProvider<S> provider) {
BrigadierMappingBuilder.super.suggestedByConstant(provider);
this.cloudSuggestions = false;
return this;
}
@Override
public BrigadierMappingBuilder<K, S> suggestedBy(final SuggestionProviderSupplier<K, S> provider) {
this.suggestionsOverride = requireNonNull(provider, "provider");
this.cloudSuggestions = false;
return this;
}
public BrigadierMapping<C, K, S> build() {
return new BrigadierMapping<>(this.cloudSuggestions, this.suggestionsOverride, this.mapper);
}
}
}

View file

@ -0,0 +1,113 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.brigadier;
import cloud.commandframework.arguments.parser.ArgumentParser;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
/**
* A builder for a mapping between a Cloud parser and a Brigadier {@link com.mojang.brigadier.arguments.ArgumentType}
*
* @param <K> the Cloud argument parser type
* @param <S> the brigadier-native sender type
*/
public interface BrigadierMappingBuilder<K extends ArgumentParser<?, ?>, S> {
/**
* Map the argument type parser to a constant Brigadier argument type instance.
*
* @param constant the argument type
* @return this builder
*/
BrigadierMappingBuilder<K, S> toConstant(ArgumentType<?> constant);
/**
* Map the cloud argument parser to a variable Brigadier {@link ArgumentType}.
*
* @param mapper the mapper
* @return this builder
*/
BrigadierMappingBuilder<K, S> to(Function<K, ? extends ArgumentType<?>> mapper);
/**
* Use the default brigadier suggestions for this argument type.
*
* <p>This is the default option if a mapped type is specified.</p>
*
* @return this builder
*/
BrigadierMappingBuilder<K, S> nativeSuggestions();
/**
* Use the suggestions from Cloud's parser for this argument type.
*
* <p>This is not the default suggestions configuration.</p>
*
* <p>Any previously set suggestion provider suppliers will not be used.</p>
*
* @return this builder
*/
BrigadierMappingBuilder<K, S> cloudSuggestions();
/**
* Use a custom Brigadier suggestion provider for this parser.
*
* @param provider the suggestions provider
* @return this builder
*/
default BrigadierMappingBuilder<K, S> suggestedByConstant(final SuggestionProvider<S> provider) {
requireNonNull(provider, "provider");
return this.suggestedBy((argument, useCloud) -> provider);
}
/**
* Use a custom Brigadier suggestion provider for this parser.
*
* @param provider the suggestions provider
* @return this builder
*/
BrigadierMappingBuilder<K, S> suggestedBy(SuggestionProviderSupplier<K, S> provider);
@FunctionalInterface
interface SuggestionProviderSupplier<K extends ArgumentParser<?, ?>, S> {
/**
* Create a new suggestion provider based on the provided argument.
*
* @param argument Argument to create a specialized provider for
* @param useCloud A provider that can be returned to ask the server to use cloud suggestions
* @return A new provider, or {@code null} to use the default value for the mapped argument type
*/
@Nullable SuggestionProvider<? super S> provide(@NonNull K argument, SuggestionProvider<S> useCloud);
}
}

View file

@ -70,6 +70,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -88,13 +89,25 @@ import java.util.stream.Stream;
@SuppressWarnings({"unchecked", "rawtypes"})
public final class CloudBrigadierManager<C, S> {
private final Map<Class<?>, Pair<Function<? extends ArgumentParser<C, ?>, ? extends ArgumentType<?>>, Boolean>> mappers;
private static final SuggestionProvider<?> DELEGATE_TO_CLOUD = (c, b) -> b.buildFuture();
private final Map<Class<?>, BrigadierMapping<C, ?, S>> mappers;
private final Map<@NonNull Class<?>, @NonNull Supplier<@Nullable ArgumentType<?>>> defaultArgumentTypeSuppliers;
private final Supplier<CommandContext<C>> dummyContextProvider;
private final CommandManager<C> commandManager;
private Function<S, C> brigadierCommandSenderMapper;
private Function<C, S> backwardsBrigadierCommandSenderMapper;
/**
* A sentinel value for declaring that suggestions should be delegated to cloud.
*
* @param <T> the sender type
* @return a singleton sentinel suggestion provider
*/
static <T> SuggestionProvider<T> delegateSuggestions() {
return (SuggestionProvider<T>) DELEGATE_TO_CLOUD;
}
/**
* Create a new cloud brigadier manager
*
@ -123,7 +136,7 @@ public final class CloudBrigadierManager<C, S> {
private void registerInternalMappings() {
/* Map byte, short and int to IntegerArgumentType */
this.registerMapping(new TypeToken<ByteArgument.ByteParser<C>>() {
}, true, argument -> {
}, builder -> builder.to(argument -> {
final boolean hasMin = argument.getMin() != Byte.MIN_VALUE;
final boolean hasMax = argument.getMax() != Byte.MAX_VALUE;
if (hasMin) {
@ -133,9 +146,9 @@ public final class CloudBrigadierManager<C, S> {
} else {
return IntegerArgumentType.integer();
}
});
}));
this.registerMapping(new TypeToken<ShortArgument.ShortParser<C>>() {
}, true, argument -> {
}, builder -> builder.to(argument -> {
final boolean hasMin = argument.getMin() != Short.MIN_VALUE;
final boolean hasMax = argument.getMax() != Short.MAX_VALUE;
if (hasMin) {
@ -145,9 +158,9 @@ public final class CloudBrigadierManager<C, S> {
} else {
return IntegerArgumentType.integer();
}
});
}));
this.registerMapping(new TypeToken<IntegerArgument.IntegerParser<C>>() {
}, true, argument -> {
}, builder -> builder.to(argument -> {
final boolean hasMin = argument.getMin() != Integer.MIN_VALUE;
final boolean hasMax = argument.getMax() != Integer.MAX_VALUE;
if (hasMin) {
@ -157,10 +170,10 @@ public final class CloudBrigadierManager<C, S> {
} else {
return IntegerArgumentType.integer();
}
});
}));
/* Map float to FloatArgumentType */
this.registerMapping(new TypeToken<FloatArgument.FloatParser<C>>() {
}, true, argument -> {
}, builder -> builder.to(argument -> {
final boolean hasMin = argument.getMin() != Float.NEGATIVE_INFINITY;
final boolean hasMax = argument.getMax() != Float.POSITIVE_INFINITY;
if (hasMin) {
@ -170,10 +183,10 @@ public final class CloudBrigadierManager<C, S> {
} else {
return FloatArgumentType.floatArg();
}
});
}));
/* Map double to DoubleArgumentType */
this.registerMapping(new TypeToken<DoubleArgument.DoubleParser<C>>() {
}, true, argument -> {
}, builder -> builder.to(argument -> {
final boolean hasMin = argument.getMin() != Double.NEGATIVE_INFINITY;
final boolean hasMax = argument.getMax() != Double.POSITIVE_INFINITY;
if (hasMin) {
@ -183,13 +196,13 @@ public final class CloudBrigadierManager<C, S> {
} else {
return DoubleArgumentType.doubleArg();
}
});
}));
/* Map boolean to BoolArgumentType */
this.registerMapping(new TypeToken<BooleanArgument.BooleanParser<C>>() {
}, true, argument -> BoolArgumentType.bool());
}, builder -> builder.toConstant(BoolArgumentType.bool()));
/* Map String properly to StringArgumentType */
this.registerMapping(new TypeToken<StringArgument.StringParser<C>>() {
}, false, argument -> {
}, builder -> builder.cloudSuggestions().to(argument -> {
switch (argument.getStringMode()) {
case QUOTED:
return StringArgumentType.string();
@ -198,21 +211,16 @@ public final class CloudBrigadierManager<C, S> {
default:
return StringArgumentType.word();
}
});
}));
/* Map flags to a greedy string */
this.registerMapping(new TypeToken<FlagArgument.FlagArgumentParser<C>>() {
}, false, argument -> StringArgumentType.greedyString());
}, builder -> builder.cloudSuggestions().toConstant(StringArgumentType.greedyString()));
/* Map String[] to a greedy string */
this.registerMapping(new TypeToken<StringArrayArgument.StringArrayParser<C>>() {
}, false, argument -> StringArgumentType.greedyString());
}, builder -> builder.cloudSuggestions().toConstant(StringArgumentType.greedyString()));
/* Map wrapped parsers to their native types */
this.registerWrapperMapping();
}
private <O> void registerWrapperMapping() {
/* a small hack to make type inference work properly... O doesn't behave as a wildcard */
this.registerMapping(new TypeToken<WrappedBrigadierParser<C, O>>() {
}, true, WrappedBrigadierParser::getNativeArgument);
this.registerMapping(new TypeToken<WrappedBrigadierParser<C, ?>>() {
}, builder -> builder.to(WrappedBrigadierParser::getNativeArgument));
}
/**
@ -301,7 +309,7 @@ public final class CloudBrigadierManager<C, S> {
final @NonNull TypeToken<K> argumentType,
final boolean nativeSuggestions
) throws IllegalArgumentException {
final Pair<Function<? extends ArgumentParser<C, ?>, ? extends ArgumentType<?>>, Boolean> pair = this.mappers.get(
final BrigadierMapping<C, ?, S> pair = this.mappers.get(
GenericTypeReflector.erase(argumentType.getType())
);
if (pair == null) {
@ -313,27 +321,51 @@ public final class CloudBrigadierManager<C, S> {
}
this.mappers.put(
GenericTypeReflector.erase(argumentType.getType()),
Pair.of(pair.getFirst(), nativeSuggestions)
pair.withNativeSuggestions(nativeSuggestions)
);
}
/**
* Register a cloud-Brigadier mapping
*
* @param argumentType cloud argument type
* @param argumentType cloud argument parser type
* @param nativeSuggestions Whether or not Brigadier suggestions should be used
* @param mapper mapper function
* @param <T> cloud argument value type
* @param <K> cloud argument type
* @param <O> Brigadier argument type value
* @deprecated for removal since 1.4.0, use {@link #registerMapping(TypeToken, Consumer)} instead.
*/
@Deprecated
@SuppressWarnings("unchecked")
public <T, K extends ArgumentParser<C, T>, O> void registerMapping(
final @NonNull TypeToken<K> argumentType,
final boolean nativeSuggestions,
final @NonNull Function<@NonNull ? extends K,
@NonNull ? extends ArgumentType<O>> mapper
) {
this.mappers.put(GenericTypeReflector.erase(argumentType.getType()), Pair.of(mapper, nativeSuggestions));
this.registerMapping(argumentType, builder -> {
builder.to((Function<K, ? extends ArgumentType<?>>) mapper);
if (!nativeSuggestions) {
builder.cloudSuggestions();
}
});
}
/**
* Register a cloud-Brigadier mapping.
*
* @param parserType The cloud argument parser type
* @param configurer a callback that will configure the mapping attributes
* @param <K> cloud argument parser type
* @since 1.4.0
*/
public <K extends ArgumentParser<C, ?>> void registerMapping(final @NonNull TypeToken<K> parserType,
final Consumer<BrigadierMappingBuilder<K, S>> configurer) {
final BrigadierMapping.BuilderImpl<C, K, S> builder = new BrigadierMapping.BuilderImpl<>();
configurer.accept(builder);
this.mappers.put(GenericTypeReflector.erase(parserType.getType()), builder.build());
}
/**
@ -350,23 +382,24 @@ public final class CloudBrigadierManager<C, S> {
}
@SuppressWarnings("all")
private <T, K extends ArgumentParser<?, ?>> @Nullable Pair<@NonNull ArgumentType<?>, @NonNull Boolean> getArgument(
private <T, K extends ArgumentParser<C, ?>> @Nullable Pair<@NonNull ArgumentType<?>, @Nullable SuggestionProvider<S>> getArgument(
final @NonNull TypeToken<?> valueType,
final @NonNull TypeToken<T> argumentType,
final @NonNull K argument
) {
final ArgumentParser<C, ?> commandArgument = (ArgumentParser<C, ?>) argument;
final Pair pair = this.mappers.get(GenericTypeReflector.erase(argumentType.getType()));
if (pair == null || pair.getFirst() == null) {
final BrigadierMapping<C, K, S> mapping = (BrigadierMapping<C, K, S>) this.mappers
.get(GenericTypeReflector.erase(argumentType.getType()));
if (mapping == null || mapping.getMapper() == null) {
return this.createDefaultMapper(valueType);
}
return Pair.of(
(ArgumentType<?>) ((Function) pair.getFirst()).apply(commandArgument),
(boolean) pair.getSecond()
(ArgumentType<?>) ((Function) mapping.getMapper()).apply(commandArgument),
mapping.makeSuggestionProvider(argument)
);
}
private <T, K extends ArgumentParser<C, T>> @NonNull Pair<@NonNull ArgumentType<?>, @NonNull Boolean> createDefaultMapper(
private @NonNull Pair<@NonNull ArgumentType<?>, @Nullable SuggestionProvider<S>> createDefaultMapper(
final @NonNull TypeToken<?> clazz
) {
final Supplier<ArgumentType<?>> argumentTypeSupplier = this.defaultArgumentTypeSuppliers
@ -378,9 +411,9 @@ public final class CloudBrigadierManager<C, S> {
defaultType = null;
}
if (defaultType != null) {
return Pair.of(argumentTypeSupplier.get(), true);
return Pair.of(argumentTypeSupplier.get(), null);
}
return Pair.of(StringArgumentType.string(), false);
return Pair.of(StringArgumentType.string(), delegateSuggestions());
}
/**
@ -486,12 +519,13 @@ public final class CloudBrigadierManager<C, S> {
for (int i = parsers.length - 1; i >= 0; i--) {
@SuppressWarnings("unchecked") final ArgumentParser<C, ?> parser = (ArgumentParser<C, ?>) parsers[i];
final Pair<ArgumentType<?>, Boolean> pair = this.getArgument(
final Pair<ArgumentType<?>, SuggestionProvider<S>> pair = this.getArgument(
TypeToken.get((Class<?>) types[i]),
TypeToken.get(parser.getClass()),
parser
);
final SuggestionProvider<S> provider = pair.getSecond() ? null : suggestionProvider;
final SuggestionProvider<S> provider = pair.getSecond() == delegateSuggestions() ? suggestionProvider
: pair.getSecond();
final ArgumentBuilder<S, ?> fragmentBuilder = RequiredArgumentBuilder
.<S, Object>argument((String) names[i], (ArgumentType<Object>) pair.getFirst())
@ -523,7 +557,7 @@ public final class CloudBrigadierManager<C, S> {
return argumentBuilders[0];
}
ArgumentBuilder<S, ?> argumentBuilder;
final ArgumentBuilder<S, ?> argumentBuilder;
if (root.getValue() instanceof StaticArgument) {
argumentBuilder = LiteralArgumentBuilder.<S>literal(root.getValue().getName())
.requires(sender -> permissionChecker.test(sender, (CommandPermission) root.getNodeMeta()
@ -534,19 +568,18 @@ public final class CloudBrigadierManager<C, S> {
.executes(executor);
} else {
// Register argument
final Pair<ArgumentType<?>, Boolean> pair = this.getArgument(
final Pair<ArgumentType<?>, SuggestionProvider<S>> pair = this.getArgument(
root.getValue().getValueType(),
TypeToken.get(root.getValue().getParser().getClass()),
root.getValue().getParser()
);
final SuggestionProvider<S> provider = pair.getSecond()
? null
: (context, builder) -> this.buildSuggestions(
final SuggestionProvider<S> provider = pair.getSecond() == delegateSuggestions()
? (context, builder) -> this.buildSuggestions(
context,
root.getParent(),
root.getValue(),
builder
);
) : pair.getSecond();
argumentBuilder = RequiredArgumentBuilder
.<S, Object>argument(root.getValue().getName(), (ArgumentType<Object>) pair.getFirst())
.suggests(provider)
@ -595,7 +628,7 @@ public final class CloudBrigadierManager<C, S> {
}
/* Remove namespace */
String leading = command.split(" ")[0];
final String leading = command.split(" ")[0];
if (leading.contains(":")) {
command = command.substring(leading.split(":")[0].length() + 1);
}