diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/BrigadierMapping.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/BrigadierMapping.java new file mode 100644 index 00000000..b117c6ec --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/BrigadierMapping.java @@ -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, S> { + private final boolean cloudSuggestions; + private final BrigadierMappingBuilder.@Nullable SuggestionProviderSupplier suggestionsOverride; + private final @Nullable Function> mapper; + + BrigadierMapping( + final boolean cloudSuggestions, + final BrigadierMappingBuilder.@Nullable SuggestionProviderSupplier suggestionsOverride, + final @Nullable Function> mapper + ) { + this.cloudSuggestions = cloudSuggestions; + this.suggestionsOverride = suggestionsOverride; + this.mapper = mapper; + } + + public @Nullable Function> getMapper() { + return this.mapper; + } + + public @NonNull BrigadierMapping 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 makeSuggestionProvider(final K commandArgument) { + if (this.cloudSuggestions) { + return CloudBrigadierManager.delegateSuggestions(); + } + return this.suggestionsOverride == null + ? null + : (SuggestionProvider) this.suggestionsOverride.provide(commandArgument, CloudBrigadierManager.delegateSuggestions()); + } + + + static final class BuilderImpl, S> implements BrigadierMappingBuilder { + private Function> mapper; + private boolean cloudSuggestions = false; + private SuggestionProviderSupplier suggestionsOverride; + + @Override + public BrigadierMappingBuilder toConstant(final ArgumentType constant) { + return this.to(parser -> constant); + } + + @Override + public BrigadierMappingBuilder to(final Function> mapper) { + this.mapper = mapper; + return this; + } + + @Override + public BrigadierMappingBuilder nativeSuggestions() { + this.cloudSuggestions = false; + this.suggestionsOverride = null; + return this; + } + + @Override + public BrigadierMappingBuilder cloudSuggestions() { + this.cloudSuggestions = true; + this.suggestionsOverride = null; + return this; + } + + @Override + public BrigadierMappingBuilder suggestedByConstant(final SuggestionProvider provider) { + BrigadierMappingBuilder.super.suggestedByConstant(provider); + this.cloudSuggestions = false; + return this; + } + + @Override + public BrigadierMappingBuilder suggestedBy(final SuggestionProviderSupplier provider) { + this.suggestionsOverride = requireNonNull(provider, "provider"); + this.cloudSuggestions = false; + return this; + } + + public BrigadierMapping build() { + return new BrigadierMapping<>(this.cloudSuggestions, this.suggestionsOverride, this.mapper); + } + + } +} diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/BrigadierMappingBuilder.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/BrigadierMappingBuilder.java new file mode 100644 index 00000000..4c567c59 --- /dev/null +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/BrigadierMappingBuilder.java @@ -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 the Cloud argument parser type + * @param the brigadier-native sender type + */ +public interface BrigadierMappingBuilder, S> { + + /** + * Map the argument type parser to a constant Brigadier argument type instance. + * + * @param constant the argument type + * @return this builder + */ + BrigadierMappingBuilder toConstant(ArgumentType constant); + + /** + * Map the cloud argument parser to a variable Brigadier {@link ArgumentType}. + * + * @param mapper the mapper + * @return this builder + */ + BrigadierMappingBuilder to(Function> mapper); + + /** + * Use the default brigadier suggestions for this argument type. + * + *

This is the default option if a mapped type is specified.

+ * + * @return this builder + */ + BrigadierMappingBuilder nativeSuggestions(); + + /** + * Use the suggestions from Cloud's parser for this argument type. + * + *

This is not the default suggestions configuration.

+ * + *

Any previously set suggestion provider suppliers will not be used.

+ * + * @return this builder + */ + BrigadierMappingBuilder cloudSuggestions(); + + /** + * Use a custom Brigadier suggestion provider for this parser. + * + * @param provider the suggestions provider + * @return this builder + */ + default BrigadierMappingBuilder suggestedByConstant(final SuggestionProvider 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 suggestedBy(SuggestionProviderSupplier provider); + + @FunctionalInterface + interface SuggestionProviderSupplier, 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 provide(@NonNull K argument, SuggestionProvider useCloud); + + } + +} diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java index 276ec7dc..cf667923 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java @@ -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 { - private final Map, Pair, ? extends ArgumentType>, Boolean>> mappers; + private static final SuggestionProvider DELEGATE_TO_CLOUD = (c, b) -> b.buildFuture(); + + private final Map, BrigadierMapping> mappers; private final Map<@NonNull Class, @NonNull Supplier<@Nullable ArgumentType>> defaultArgumentTypeSuppliers; private final Supplier> dummyContextProvider; private final CommandManager commandManager; private Function brigadierCommandSenderMapper; private Function backwardsBrigadierCommandSenderMapper; + /** + * A sentinel value for declaring that suggestions should be delegated to cloud. + * + * @param the sender type + * @return a singleton sentinel suggestion provider + */ + static SuggestionProvider delegateSuggestions() { + return (SuggestionProvider) DELEGATE_TO_CLOUD; + } + /** * Create a new cloud brigadier manager * @@ -123,7 +136,7 @@ public final class CloudBrigadierManager { private void registerInternalMappings() { /* Map byte, short and int to IntegerArgumentType */ this.registerMapping(new TypeToken>() { - }, 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 { } else { return IntegerArgumentType.integer(); } - }); + })); this.registerMapping(new TypeToken>() { - }, 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 { } else { return IntegerArgumentType.integer(); } - }); + })); this.registerMapping(new TypeToken>() { - }, 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 { } else { return IntegerArgumentType.integer(); } - }); + })); /* Map float to FloatArgumentType */ this.registerMapping(new TypeToken>() { - }, 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 { } else { return FloatArgumentType.floatArg(); } - }); + })); /* Map double to DoubleArgumentType */ this.registerMapping(new TypeToken>() { - }, 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 { } else { return DoubleArgumentType.doubleArg(); } - }); + })); /* Map boolean to BoolArgumentType */ this.registerMapping(new TypeToken>() { - }, true, argument -> BoolArgumentType.bool()); + }, builder -> builder.toConstant(BoolArgumentType.bool())); /* Map String properly to StringArgumentType */ this.registerMapping(new TypeToken>() { - }, false, argument -> { + }, builder -> builder.cloudSuggestions().to(argument -> { switch (argument.getStringMode()) { case QUOTED: return StringArgumentType.string(); @@ -198,21 +211,16 @@ public final class CloudBrigadierManager { default: return StringArgumentType.word(); } - }); + })); /* Map flags to a greedy string */ this.registerMapping(new TypeToken>() { - }, false, argument -> StringArgumentType.greedyString()); + }, builder -> builder.cloudSuggestions().toConstant(StringArgumentType.greedyString())); /* Map String[] to a greedy string */ this.registerMapping(new TypeToken>() { - }, false, argument -> StringArgumentType.greedyString()); + }, builder -> builder.cloudSuggestions().toConstant(StringArgumentType.greedyString())); /* Map wrapped parsers to their native types */ - this.registerWrapperMapping(); - } - - private void registerWrapperMapping() { - /* a small hack to make type inference work properly... O doesn't behave as a wildcard */ - this.registerMapping(new TypeToken>() { - }, true, WrappedBrigadierParser::getNativeArgument); + this.registerMapping(new TypeToken>() { + }, builder -> builder.to(WrappedBrigadierParser::getNativeArgument)); } /** @@ -301,7 +309,7 @@ public final class CloudBrigadierManager { final @NonNull TypeToken argumentType, final boolean nativeSuggestions ) throws IllegalArgumentException { - final Pair, ? extends ArgumentType>, Boolean> pair = this.mappers.get( + final BrigadierMapping pair = this.mappers.get( GenericTypeReflector.erase(argumentType.getType()) ); if (pair == null) { @@ -313,27 +321,51 @@ public final class CloudBrigadierManager { } 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 cloud argument value type * @param cloud argument type * @param Brigadier argument type value + * @deprecated for removal since 1.4.0, use {@link #registerMapping(TypeToken, Consumer)} instead. */ + @Deprecated + @SuppressWarnings("unchecked") public , O> void registerMapping( final @NonNull TypeToken argumentType, final boolean nativeSuggestions, final @NonNull Function<@NonNull ? extends K, @NonNull ? extends ArgumentType> mapper ) { - this.mappers.put(GenericTypeReflector.erase(argumentType.getType()), Pair.of(mapper, nativeSuggestions)); + this.registerMapping(argumentType, builder -> { + builder.to((Function>) 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 cloud argument parser type + * @since 1.4.0 + */ + public > void registerMapping(final @NonNull TypeToken parserType, + final Consumer> configurer) { + final BrigadierMapping.BuilderImpl builder = new BrigadierMapping.BuilderImpl<>(); + configurer.accept(builder); + this.mappers.put(GenericTypeReflector.erase(parserType.getType()), builder.build()); + } /** @@ -350,23 +382,24 @@ public final class CloudBrigadierManager { } @SuppressWarnings("all") - private > @Nullable Pair<@NonNull ArgumentType, @NonNull Boolean> getArgument( + private > @Nullable Pair<@NonNull ArgumentType, @Nullable SuggestionProvider> getArgument( final @NonNull TypeToken valueType, final @NonNull TypeToken argumentType, final @NonNull K argument ) { final ArgumentParser commandArgument = (ArgumentParser) argument; - final Pair pair = this.mappers.get(GenericTypeReflector.erase(argumentType.getType())); - if (pair == null || pair.getFirst() == null) { + final BrigadierMapping mapping = (BrigadierMapping) 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 > @NonNull Pair<@NonNull ArgumentType, @NonNull Boolean> createDefaultMapper( + private @NonNull Pair<@NonNull ArgumentType, @Nullable SuggestionProvider> createDefaultMapper( final @NonNull TypeToken clazz ) { final Supplier> argumentTypeSupplier = this.defaultArgumentTypeSuppliers @@ -378,9 +411,9 @@ public final class CloudBrigadierManager { 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 { for (int i = parsers.length - 1; i >= 0; i--) { @SuppressWarnings("unchecked") final ArgumentParser parser = (ArgumentParser) parsers[i]; - final Pair, Boolean> pair = this.getArgument( + final Pair, SuggestionProvider> pair = this.getArgument( TypeToken.get((Class) types[i]), TypeToken.get(parser.getClass()), parser ); - final SuggestionProvider provider = pair.getSecond() ? null : suggestionProvider; + final SuggestionProvider provider = pair.getSecond() == delegateSuggestions() ? suggestionProvider + : pair.getSecond(); final ArgumentBuilder fragmentBuilder = RequiredArgumentBuilder .argument((String) names[i], (ArgumentType) pair.getFirst()) @@ -523,7 +557,7 @@ public final class CloudBrigadierManager { return argumentBuilders[0]; } - ArgumentBuilder argumentBuilder; + final ArgumentBuilder argumentBuilder; if (root.getValue() instanceof StaticArgument) { argumentBuilder = LiteralArgumentBuilder.literal(root.getValue().getName()) .requires(sender -> permissionChecker.test(sender, (CommandPermission) root.getNodeMeta() @@ -534,19 +568,18 @@ public final class CloudBrigadierManager { .executes(executor); } else { // Register argument - final Pair, Boolean> pair = this.getArgument( + final Pair, SuggestionProvider> pair = this.getArgument( root.getValue().getValueType(), TypeToken.get(root.getValue().getParser().getClass()), root.getValue().getParser() ); - final SuggestionProvider provider = pair.getSecond() - ? null - : (context, builder) -> this.buildSuggestions( + final SuggestionProvider provider = pair.getSecond() == delegateSuggestions() + ? (context, builder) -> this.buildSuggestions( context, root.getParent(), root.getValue(), builder - ); + ) : pair.getSecond(); argumentBuilder = RequiredArgumentBuilder .argument(root.getValue().getName(), (ArgumentType) pair.getFirst()) .suggests(provider) @@ -595,7 +628,7 @@ public final class CloudBrigadierManager { } /* Remove namespace */ - String leading = command.split(" ")[0]; + final String leading = command.split(" ")[0]; if (leading.contains(":")) { command = command.substring(leading.split(":")[0].length() + 1); }