diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java index ace9ee22..8f2b0ab1 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/argument/WrappedBrigadierParser.java @@ -39,7 +39,9 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import static java.util.Objects.requireNonNull; @@ -56,6 +58,7 @@ public final class WrappedBrigadierParser implements ArgumentParser private final Supplier> nativeType; private final int expectedArgumentCount; + private final @Nullable ParseFunction parse; /** * Create an argument parser based on a brigadier command. @@ -101,10 +104,28 @@ public final class WrappedBrigadierParser implements ArgumentParser public WrappedBrigadierParser( final Supplier> nativeType, final int expectedArgumentCount + ) { + this(nativeType, expectedArgumentCount, null); + } + + /** + * Create an argument parser based on a brigadier command. + * + * @param nativeType the native command type provider, calculated lazily + * @param expectedArgumentCount the number of arguments the brigadier type is expected to consume + * @param parse special function to replace {@link ArgumentType#parse(StringReader)} (for CraftBukkit weirdness) + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public WrappedBrigadierParser( + final Supplier> nativeType, + final int expectedArgumentCount, + final @Nullable ParseFunction parse ) { requireNonNull(nativeType, "brigadierType"); this.nativeType = nativeType; this.expectedArgumentCount = expectedArgumentCount; + this.parse = parse; } /** @@ -135,7 +156,10 @@ public final class WrappedBrigadierParser implements ArgumentParser // Then try to parse try { - return ArgumentParseResult.success(this.nativeType.get().parse(reader)); + final T result = this.parse != null + ? this.parse.apply(this.nativeType.get(), reader) + : this.nativeType.get().parse(reader); + return ArgumentParseResult.success(result); } catch (final CommandSyntaxException ex) { return ArgumentParseResult.failure(ex); } finally { @@ -191,4 +215,24 @@ public final class WrappedBrigadierParser implements ArgumentParser public int getRequestedArgumentCount() { return this.expectedArgumentCount; } + + /** + * Function which can call {@link ArgumentType#parse(StringReader)} or another method. + * + * @param result type + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + @FunctionalInterface + public interface ParseFunction { + /** + * Apply the parse function. + * + * @param type argument type + * @param reader string reader + * @return result + * @throws CommandSyntaxException on failure + */ + T apply(ArgumentType type, StringReader reader) throws CommandSyntaxException; + } } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionKeys.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionKeys.java index 407527af..0422f145 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionKeys.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionKeys.java @@ -27,6 +27,7 @@ import cloud.commandframework.captions.Caption; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -58,7 +59,11 @@ public final class BukkitCaptionKeys { public static final Caption ARGUMENT_PARSE_FAILURE_WORLD = of("argument.parse.failure.world"); /** * Variables: {input} + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final Caption ARGUMENT_PARSE_FAILURE_SELECTOR_MALFORMED = of("argument.parse.failure.selector.malformed"); /** * Variables: None @@ -66,17 +71,29 @@ public final class BukkitCaptionKeys { public static final Caption ARGUMENT_PARSE_FAILURE_SELECTOR_UNSUPPORTED = of("argument.parse.failure.selector.unsupported"); /** * Variables: None + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final Caption ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_PLAYERS = of( "argument.parse.failure.selector.too_many_players"); /** * Variables: None + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final Caption ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_ENTITIES = of( "argument.parse.failure.selector.too_many_entities"); /** * Variables: None + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final Caption ARGUMENT_PARSE_FAILURE_SELECTOR_NON_PLAYER = of( "argument.parse.failure.selector.non_player_in_player_selector"); /** diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionRegistry.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionRegistry.java index 066c8d60..de3afa02 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionRegistry.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCaptionRegistry.java @@ -24,6 +24,7 @@ package cloud.commandframework.bukkit; import cloud.commandframework.captions.SimpleCaptionRegistry; +import org.apiguardian.api.API; /** * Caption registry that uses bi-functions to produce messages @@ -54,7 +55,11 @@ public class BukkitCaptionRegistry extends SimpleCaptionRegistry { public static final String ARGUMENT_PARSE_FAILURE_WORLD = "'{input}' is not a valid Minecraft world"; /** * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_SELECTOR_MALFORMED} + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final String ARGUMENT_PARSE_FAILURE_SELECTOR_MALFORMED = "Selector '{input}' is malformed."; /** * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_SELECTOR_UNSUPPORTED} @@ -63,15 +68,27 @@ public class BukkitCaptionRegistry extends SimpleCaptionRegistry { "Entity selector argument type not supported below Minecraft 1.13."; /** * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_PLAYERS} + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final String ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_PLAYERS = "More than 1 player selected in single player selector"; /** * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_ENTITIES} + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final String ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_ENTITIES = "More than 1 entity selected in single entity selector."; /** * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_SELECTOR_NON_PLAYER} + * + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated public static final String ARGUMENT_PARSE_FAILURE_SELECTOR_NON_PLAYER = "Non-player(s) selected in player selector."; /** * Default caption for {@link BukkitCaptionKeys#ARGUMENT_PARSE_FAILURE_LOCATION_INVALID_FORMAT} @@ -106,6 +123,7 @@ public class BukkitCaptionRegistry extends SimpleCaptionRegistry { "Invalid input '{input}', requires an explicit namespace."; + @SuppressWarnings("deprecation") protected BukkitCaptionRegistry() { super(); this.registerMessageFactory( diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java index b0df94b0..56b45baa 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java @@ -29,6 +29,7 @@ import cloud.commandframework.CommandTree; import cloud.commandframework.arguments.parser.ParserParameters; import cloud.commandframework.brigadier.BrigadierManagerHolder; import cloud.commandframework.brigadier.CloudBrigadierManager; +import cloud.commandframework.bukkit.annotation.specifier.AllowEmptySelection; import cloud.commandframework.bukkit.annotation.specifier.DefaultNamespace; import cloud.commandframework.bukkit.annotation.specifier.RequireExplicitNamespace; import cloud.commandframework.bukkit.argument.NamespacedKeyArgument; @@ -157,15 +158,28 @@ public class BukkitCommandManager extends CommandManager implements Brigad new Location2DArgument.Location2DParser<>()); this.parserRegistry().registerParserSupplier(TypeToken.get(ProtoItemStack.class), parserParameters -> new ItemStackArgument.Parser<>()); + /* Register Entity Selector Parsers */ this.parserRegistry().registerParserSupplier(TypeToken.get(SingleEntitySelector.class), parserParameters -> new SingleEntitySelectorArgument.SingleEntitySelectorParser<>()); this.parserRegistry().registerParserSupplier(TypeToken.get(SinglePlayerSelector.class), parserParameters -> new SinglePlayerSelectorArgument.SinglePlayerSelectorParser<>()); - this.parserRegistry().registerParserSupplier(TypeToken.get(MultipleEntitySelector.class), parserParameters -> - new MultipleEntitySelectorArgument.MultipleEntitySelectorParser<>()); - this.parserRegistry().registerParserSupplier(TypeToken.get(MultiplePlayerSelector.class), parserParameters -> - new MultiplePlayerSelectorArgument.MultiplePlayerSelectorParser<>()); + this.parserRegistry().registerAnnotationMapper( + AllowEmptySelection.class, + (annotation, type) -> ParserParameters.single(BukkitParserParameters.ALLOW_EMPTY_SELECTOR_RESULT, annotation.value()) + ); + this.parserRegistry().registerParserSupplier( + TypeToken.get(MultipleEntitySelector.class), + parserParameters -> new MultipleEntitySelectorArgument.MultipleEntitySelectorParser<>( + parserParameters.get(BukkitParserParameters.ALLOW_EMPTY_SELECTOR_RESULT, true) + ) + ); + this.parserRegistry().registerParserSupplier( + TypeToken.get(MultiplePlayerSelector.class), + parserParameters -> new MultiplePlayerSelectorArgument.MultiplePlayerSelectorParser<>( + parserParameters.get(BukkitParserParameters.ALLOW_EMPTY_SELECTOR_RESULT, true) + ) + ); if (CraftBukkitReflection.classExists("org.bukkit.NamespacedKey")) { this.registerParserSupplierFor(NamespacedKeyArgument.class); diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitParserParameters.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitParserParameters.java index 6bd4c8f8..e27a28fd 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitParserParameters.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitParserParameters.java @@ -25,6 +25,7 @@ package cloud.commandframework.bukkit; import cloud.commandframework.arguments.parser.ParserParameter; import io.leangen.geantyref.TypeToken; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -32,11 +33,23 @@ import org.checkerframework.checker.nullness.qual.NonNull; * * @since 1.7.0 */ +@API(status = API.Status.STABLE, since = "1.7.0") public final class BukkitParserParameters { private BukkitParserParameters() { } + /** + * Used to specify if an empty result is allowed for + * {@link cloud.commandframework.bukkit.parsers.selector.MultiplePlayerSelectorArgument} and + * {@link cloud.commandframework.bukkit.parsers.selector.MultipleEntitySelectorArgument}. + * + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public static final ParserParameter ALLOW_EMPTY_SELECTOR_RESULT = + create("allow_empty_selector_result", TypeToken.get(Boolean.class)); + /** * Sets to require explicit namespaces for {@link cloud.commandframework.bukkit.argument.NamespacedKeyArgument} * (i.e. 'test' will be rejected but 'test:test' will pass). diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/annotation/specifier/AllowEmptySelection.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/annotation/specifier/AllowEmptySelection.java new file mode 100644 index 00000000..be7de109 --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/annotation/specifier/AllowEmptySelection.java @@ -0,0 +1,28 @@ +package cloud.commandframework.bukkit.annotation.specifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apiguardian.api.API; + +/** + * Annotation used to specify if an empty result is allowed for + * {@link cloud.commandframework.bukkit.parsers.selector.MultiplePlayerSelectorArgument} and + * {@link cloud.commandframework.bukkit.parsers.selector.MultipleEntitySelectorArgument}. + * + * @since 1.8.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@API(status = API.Status.STABLE, since = "1.8.0") +public @interface AllowEmptySelection { + + /** + * Whether to allow empty results. + * + * @return value + */ + boolean value() default true; + +} diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultipleEntitySelectorArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultipleEntitySelectorArgument.java index 5e6f9f14..da73809e 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultipleEntitySelectorArgument.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultipleEntitySelectorArgument.java @@ -25,17 +25,12 @@ package cloud.commandframework.bukkit.parsers.selector; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.arguments.CommandArgument; -import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; -import cloud.commandframework.bukkit.BukkitCommandContextKeys; -import cloud.commandframework.bukkit.CloudBukkitCapabilities; import cloud.commandframework.bukkit.arguments.selector.MultipleEntitySelector; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.exceptions.parsing.NoInputProvidedException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import java.util.List; -import java.util.Queue; import java.util.function.BiFunction; -import org.bukkit.Bukkit; +import org.apiguardian.api.API; import org.bukkit.entity.Entity; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -43,6 +38,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public final class MultipleEntitySelectorArgument extends CommandArgument { private MultipleEntitySelectorArgument( + final boolean allowEmpty, final boolean required, final @NonNull String name, final @NonNull String defaultValue, @@ -50,8 +46,8 @@ public final class MultipleEntitySelectorArgument extends CommandArgument> suggestionsProvider, final @NonNull ArgumentDescription defaultDescription ) { - super(required, name, new MultipleEntitySelectorParser<>(), defaultValue, MultipleEntitySelector.class, - suggestionsProvider, defaultDescription + super(required, name, new MultipleEntitySelectorParser<>(allowEmpty), defaultValue, + MultipleEntitySelector.class, suggestionsProvider, defaultDescription ); } @@ -61,9 +57,25 @@ public final class MultipleEntitySelectorArgument extends CommandArgument Command sender type * @return Created builder + * @deprecated prefer {@link #builder(String)} */ - public static MultipleEntitySelectorArgument.@NonNull Builder newBuilder(final @NonNull String name) { - return new MultipleEntitySelectorArgument.Builder<>(name); + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated + public static Builder newBuilder(final @NonNull String name) { + return builder(name); + } + + /** + * Create a new {@link Builder}. + * + * @param name argument name + * @param sender type + * @return new builder + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public static @NonNull Builder builder(final @NonNull String name) { + return new Builder<>(name); } /** @@ -73,8 +85,8 @@ public final class MultipleEntitySelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument of(final @NonNull String name) { - return MultipleEntitySelectorArgument.newBuilder(name).asRequired().build(); + public static @NonNull MultipleEntitySelectorArgument of(final @NonNull String name) { + return MultipleEntitySelectorArgument.builder(name).asRequired().build(); } /** @@ -84,8 +96,8 @@ public final class MultipleEntitySelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional(final @NonNull String name) { - return MultipleEntitySelectorArgument.newBuilder(name).asOptional().build(); + public static @NonNull MultipleEntitySelectorArgument optional(final @NonNull String name) { + return MultipleEntitySelectorArgument.builder(name).asOptional().build(); } /** @@ -96,20 +108,35 @@ public final class MultipleEntitySelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional( + public static @NonNull MultipleEntitySelectorArgument optional( final @NonNull String name, final @NonNull String defaultEntitySelector ) { - return MultipleEntitySelectorArgument.newBuilder(name).asOptionalWithDefault(defaultEntitySelector).build(); + return MultipleEntitySelectorArgument.builder(name).asOptionalWithDefault(defaultEntitySelector).build(); } - public static final class Builder extends CommandArgument.Builder { + public static final class Builder extends CommandArgument.TypedBuilder> { + + private boolean allowEmpty = true; private Builder(final @NonNull String name) { super(MultipleEntitySelector.class, name); } + /** + * Set whether to allow empty results. + * + * @param allowEmpty whether to allow empty results + * @return builder instance + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public @NonNull Builder allowEmpty(final boolean allowEmpty) { + this.allowEmpty = allowEmpty; + return this; + } + /** * Builder a new argument * @@ -117,51 +144,51 @@ public final class MultipleEntitySelectorArgument extends CommandArgument build() { - return new MultipleEntitySelectorArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(), - this.getSuggestionsProvider(), this.getDefaultDescription() + return new MultipleEntitySelectorArgument<>( + this.allowEmpty, + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getSuggestionsProvider(), + this.getDefaultDescription() ); } } - public static final class MultipleEntitySelectorParser implements ArgumentParser { + public static final class MultipleEntitySelectorParser extends SelectorUtils.EntitySelectorParser { + + private final boolean allowEmpty; + + /** + * Creates a new {@link MultipleEntitySelectorParser}. + * + * @param allowEmpty Whether to allow an empty result + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public MultipleEntitySelectorParser(final boolean allowEmpty) { + super(false); + this.allowEmpty = allowEmpty; + } + + /** + * Creates a new {@link MultipleEntitySelectorParser}. + */ + public MultipleEntitySelectorParser() { + this(true); + } @Override - public @NonNull ArgumentParseResult parse( - final @NonNull CommandContext commandContext, - final @NonNull Queue<@NonNull String> inputQueue - ) { - if (!commandContext.get(BukkitCommandContextKeys.CLOUD_BUKKIT_CAPABILITIES).contains( - CloudBukkitCapabilities.BRIGADIER)) { - return ArgumentParseResult.failure(new SelectorParseException( - "", - commandContext, - SelectorParseException.FailureReason.UNSUPPORTED_VERSION, - MultipleEntitySelectorParser.class - )); + public MultipleEntitySelector mapResult( + final @NonNull String input, + final SelectorUtils.@NonNull EntitySelectorWrapper wrapper + ) throws Exception { + final List entities = wrapper.entities(); + if (entities.isEmpty() && !this.allowEmpty) { + throw ((SimpleCommandExceptionType) NO_ENTITIES_EXCEPTION_TYPE.get()).create(); } - final String input = inputQueue.peek(); - if (input == null) { - return ArgumentParseResult.failure(new NoInputProvidedException( - MultipleEntitySelectorParser.class, - commandContext - )); - } - - List entities; - try { - entities = Bukkit.selectEntities(commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER), input); - } catch (IllegalArgumentException e) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.MALFORMED_SELECTOR, - MultipleEntitySelectorParser.class - )); - } - - inputQueue.remove(); - return ArgumentParseResult.success(new MultipleEntitySelector(input, entities)); + return new MultipleEntitySelector(input, entities); } } } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultiplePlayerSelectorArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultiplePlayerSelectorArgument.java index 351ee3b6..d9863842 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultiplePlayerSelectorArgument.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/MultiplePlayerSelectorArgument.java @@ -26,21 +26,17 @@ package cloud.commandframework.bukkit.parsers.selector; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; -import cloud.commandframework.bukkit.BukkitCommandContextKeys; -import cloud.commandframework.bukkit.CloudBukkitCapabilities; import cloud.commandframework.bukkit.arguments.selector.MultiplePlayerSelector; import cloud.commandframework.bukkit.parsers.PlayerArgument; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.exceptions.parsing.NoInputProvidedException; import com.google.common.collect.ImmutableList; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.function.BiFunction; +import org.apiguardian.api.API; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -48,6 +44,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public final class MultiplePlayerSelectorArgument extends CommandArgument { private MultiplePlayerSelectorArgument( + final boolean allowEmpty, final boolean required, final @NonNull String name, final @NonNull String defaultValue, @@ -55,7 +52,7 @@ public final class MultiplePlayerSelectorArgument extends CommandArgument> suggestionsProvider, final @NonNull ArgumentDescription defaultDescription ) { - super(required, name, new MultiplePlayerSelectorParser<>(), defaultValue, MultiplePlayerSelector.class, + super(required, name, new MultiplePlayerSelectorParser<>(allowEmpty), defaultValue, MultiplePlayerSelector.class, suggestionsProvider, defaultDescription ); } @@ -66,9 +63,25 @@ public final class MultiplePlayerSelectorArgument extends CommandArgument Command sender type * @return Created builder + * @deprecated prefer {@link #builder(String)} */ - public static MultiplePlayerSelectorArgument.Builder newBuilder(final @NonNull String name) { - return new MultiplePlayerSelectorArgument.Builder<>(name); + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated + public static Builder newBuilder(final @NonNull String name) { + return builder(name); + } + + /** + * Create a new {@link Builder}. + * + * @param name argument name + * @param sender type + * @return new builder + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public static @NonNull Builder builder(final @NonNull String name) { + return new Builder<>(name); } /** @@ -78,8 +91,8 @@ public final class MultiplePlayerSelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument of(final @NonNull String name) { - return MultiplePlayerSelectorArgument.newBuilder(name).asRequired().build(); + public static @NonNull MultiplePlayerSelectorArgument of(final @NonNull String name) { + return MultiplePlayerSelectorArgument.builder(name).asRequired().build(); } /** @@ -89,8 +102,8 @@ public final class MultiplePlayerSelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional(final @NonNull String name) { - return MultiplePlayerSelectorArgument.newBuilder(name).asOptional().build(); + public static @NonNull MultiplePlayerSelectorArgument optional(final @NonNull String name) { + return MultiplePlayerSelectorArgument.builder(name).asOptional().build(); } /** @@ -101,20 +114,35 @@ public final class MultiplePlayerSelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional( + public static @NonNull MultiplePlayerSelectorArgument optional( final @NonNull String name, final @NonNull String defaultEntitySelector ) { - return MultiplePlayerSelectorArgument.newBuilder(name).asOptionalWithDefault(defaultEntitySelector).build(); + return MultiplePlayerSelectorArgument.builder(name).asOptionalWithDefault(defaultEntitySelector).build(); } - public static final class Builder extends CommandArgument.Builder { + public static final class Builder extends CommandArgument.TypedBuilder> { + + private boolean allowEmpty = true; private Builder(final @NonNull String name) { super(MultiplePlayerSelector.class, name); } + /** + * Set whether to allow empty results. + * + * @param allowEmpty whether to allow empty results + * @return builder instance + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public @NonNull Builder allowEmpty(final boolean allowEmpty) { + this.allowEmpty = allowEmpty; + return this; + } + /** * Builder a new argument * @@ -122,83 +150,66 @@ public final class MultiplePlayerSelectorArgument extends CommandArgument build() { - return new MultiplePlayerSelectorArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(), - this.getSuggestionsProvider(), this.getDefaultDescription() + return new MultiplePlayerSelectorArgument<>( + this.allowEmpty, + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getSuggestionsProvider(), + this.getDefaultDescription() ); } } - public static final class MultiplePlayerSelectorParser implements ArgumentParser { + public static final class MultiplePlayerSelectorParser extends SelectorUtils.PlayerSelectorParser { + + private final boolean allowEmpty; + + /** + * Creates a new {@link MultiplePlayerSelectorParser}. + * + * @param allowEmpty Whether to allow an empty result + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public MultiplePlayerSelectorParser(final boolean allowEmpty) { + super(false); + this.allowEmpty = allowEmpty; + } + + /** + * Creates a new {@link MultiplePlayerSelectorParser}. + */ + public MultiplePlayerSelectorParser() { + this(true); + } @Override - public @NonNull ArgumentParseResult parse( + public MultiplePlayerSelector mapResult( + final @NonNull String input, + final SelectorUtils.@NonNull EntitySelectorWrapper wrapper + ) throws Exception { + final List players = wrapper.players(); + if (players.isEmpty() && !this.allowEmpty) { + throw ((SimpleCommandExceptionType) NO_PLAYERS_EXCEPTION_TYPE.get()).create(); + } + return new MultiplePlayerSelector(input, new ArrayList<>(players)); + } + + @Override + protected @NonNull ArgumentParseResult legacyParse( final @NonNull CommandContext commandContext, final @NonNull Queue<@NonNull String> inputQueue ) { final String input = inputQueue.peek(); - if (input == null) { - return ArgumentParseResult.failure(new NoInputProvidedException( - MultiplePlayerSelectorParser.class, - commandContext - )); + @SuppressWarnings("deprecation") final @Nullable Player player = Bukkit.getPlayer(input); + + if (player == null) { + return ArgumentParseResult.failure(new PlayerArgument.PlayerParseException(input, commandContext)); } - - if (!commandContext.get(BukkitCommandContextKeys.CLOUD_BUKKIT_CAPABILITIES).contains( - CloudBukkitCapabilities.BRIGADIER)) { - @SuppressWarnings("deprecation") - Player player = Bukkit.getPlayer(input); - - if (player == null) { - return ArgumentParseResult.failure(new PlayerArgument.PlayerParseException(input, commandContext)); - } - inputQueue.remove(); - return ArgumentParseResult.success(new MultiplePlayerSelector(input, ImmutableList.of(player))); - } - - List entities; - try { - entities = Bukkit.selectEntities(commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER), input); - } catch (IllegalArgumentException e) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.MALFORMED_SELECTOR, - MultiplePlayerSelectorParser.class - )); - } - - for (Entity e : entities) { - if (!(e instanceof Player)) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.NON_PLAYER_IN_PLAYER_SELECTOR, - MultiplePlayerSelectorParser.class - )); - } - } - inputQueue.remove(); - return ArgumentParseResult.success(new MultiplePlayerSelector(input, entities)); - } - - @Override - public @NonNull List<@NonNull String> suggestions( - final @NonNull CommandContext commandContext, - final @NonNull String input - ) { - List output = new ArrayList<>(); - - for (Player player : Bukkit.getOnlinePlayers()) { - final CommandSender bukkit = commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER); - if (bukkit instanceof Player && !((Player) bukkit).canSee(player)) { - continue; - } - output.add(player.getName()); - } - - return output; + return ArgumentParseResult.success(new MultiplePlayerSelector(input, ImmutableList.of(player))); } } } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorParseException.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorParseException.java index 63293aa7..75651480 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorParseException.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorParseException.java @@ -28,6 +28,7 @@ import cloud.commandframework.captions.Caption; import cloud.commandframework.captions.CaptionVariable; import cloud.commandframework.context.CommandContext; import cloud.commandframework.exceptions.parsing.ParserException; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -73,7 +74,10 @@ public final class SelectorParseException extends ParserException { } /** - * Get the reason of failure for the selector parser + * Get the reason of failure for the selector parser. + * + *

Note: The only type currently used is {@link FailureReason#UNSUPPORTED_VERSION}, other exceptions + * are now handled by Brigadier in the form of {@link com.mojang.brigadier.exceptions.CommandSyntaxException}.

* * @return Failure reason * @since 1.2.0 @@ -90,9 +94,29 @@ public final class SelectorParseException extends ParserException { public enum FailureReason { UNSUPPORTED_VERSION(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_SELECTOR_UNSUPPORTED), + /** + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. + */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated MALFORMED_SELECTOR(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_SELECTOR_MALFORMED), + /** + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. + */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated TOO_MANY_PLAYERS(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_PLAYERS), + /** + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. + */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated TOO_MANY_ENTITIES(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_SELECTOR_TOO_MANY_ENTITIES), + /** + * @deprecated parsing is now handled by Brigadier and will throw {@link com.mojang.brigadier.exceptions.CommandSyntaxException} instead. + */ + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated NON_PLAYER_IN_PLAYER_SELECTOR(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_SELECTOR_NON_PLAYER); diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorUtils.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorUtils.java new file mode 100644 index 00000000..317563c8 --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SelectorUtils.java @@ -0,0 +1,472 @@ +// +// MIT License +// +// Copyright (c) 2021 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.bukkit.parsers.selector; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; +import cloud.commandframework.bukkit.BukkitCommandContextKeys; +import cloud.commandframework.bukkit.internal.CraftBukkitReflection; +import cloud.commandframework.bukkit.internal.MinecraftArgumentTypes; +import cloud.commandframework.context.CommandContext; +import com.google.common.base.Suppliers; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import io.leangen.geantyref.GenericTypeReflector; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +final class SelectorUtils { + + private SelectorUtils() { + } + + private static @Nullable ArgumentParser createModernParser( + final boolean single, + final boolean playersOnly, + final SelectorMapper mapper + ) { + if (CraftBukkitReflection.MAJOR_REVISION < 13) { + return null; + } + final ArgumentParser wrappedBrigParser = new WrappedBrigadierParser<>( + () -> createEntityArgument(single, playersOnly), + ArgumentParser.DEFAULT_ARGUMENT_COUNT, + EntityArgumentParseFunction.INSTANCE + ); + return new ModernSelectorParser<>(wrappedBrigParser, mapper); + } + + @SuppressWarnings("unchecked") + private static ArgumentType createEntityArgument(final boolean single, final boolean playersOnly) { + final Constructor constructor = + MinecraftArgumentTypes.getClassByKey(NamespacedKey.minecraft("entity")).getDeclaredConstructors()[0]; + constructor.setAccessible(true); + try { + return (ArgumentType) constructor.newInstance(single, playersOnly); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + + private static final class EntityArgumentParseFunction implements WrappedBrigadierParser.ParseFunction { + static final EntityArgumentParseFunction INSTANCE = new EntityArgumentParseFunction(); + + @Override + public Object apply( + final ArgumentType type, + final StringReader reader + ) throws CommandSyntaxException { + final @Nullable Method specialParse = CraftBukkitReflection.findMethod( + type.getClass(), + "parse", + StringReader.class, + boolean.class + ); + if (specialParse == null) { + return type.parse(reader); + } + try { + return specialParse.invoke( + type, + reader, + true // CraftBukkit overridePermissions param + ); + } catch (final InvocationTargetException ex) { + final Throwable cause = ex.getCause(); + if (cause instanceof CommandSyntaxException) { + throw (CommandSyntaxException) cause; + } + throw new RuntimeException(ex); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + } + + private abstract static class SelectorParser implements ArgumentParser, SelectorMapper { + protected static final Supplier NO_PLAYERS_EXCEPTION_TYPE = + Suppliers.memoize(() -> findExceptionType("argument.entity.notfound.player")); + protected static final Supplier NO_ENTITIES_EXCEPTION_TYPE = + Suppliers.memoize(() -> findExceptionType("argument.entity.notfound.entity")); + + private final @Nullable ArgumentParser modernParser; + + protected SelectorParser( + final boolean single, + final boolean playersOnly + ) { + this.modernParser = createModernParser(single, playersOnly, this); + } + + protected ArgumentParseResult legacyParse( + final CommandContext commandContext, + final Queue inputQueue + ) { + return ArgumentParseResult.failure(new SelectorParseException( + "", + commandContext, + SelectorParseException.FailureReason.UNSUPPORTED_VERSION, + this.getClass() + )); + } + + protected List legacySuggestions( + final CommandContext commandContext, + final String input + ) { + return Collections.emptyList(); + } + + @Override + public ArgumentParseResult parse( + final CommandContext commandContext, + final Queue inputQueue + ) { + if (this.modernParser != null) { + return this.modernParser.parse(commandContext, inputQueue); + } + return this.legacyParse(commandContext, inputQueue); + } + + @Override + public List suggestions( + final CommandContext commandContext, + final String input + ) { + if (this.modernParser != null) { + return this.modernParser.suggestions(commandContext, input); + } + return this.legacySuggestions(commandContext, input); + } + + // returns SimpleCommandExceptionType, does not reference in signature for ABI with pre-1.13 + private static Object findExceptionType(final String type) { + final Field[] fields = MinecraftArgumentTypes.getClassByKey(NamespacedKey.minecraft("entity")).getDeclaredFields(); + return Arrays.stream(fields) + .filter(field -> Modifier.isStatic(field.getModifiers()) && field.getType() == SimpleCommandExceptionType.class) + .map(field -> { + try { + final @Nullable Object fieldValue = field.get(null); + if (fieldValue == null) { + return null; + } + final Field messageField = SimpleCommandExceptionType.class.getDeclaredField("message"); + messageField.setAccessible(true); + if (messageField.get(fieldValue).toString().contains(type)) { + return fieldValue; + } + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + return null; + }) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Could not find exception type '" + type + "'")); + } + } + + abstract static class EntitySelectorParser extends SelectorParser { + protected EntitySelectorParser(final boolean single) { + super(single, false); + } + } + + abstract static class PlayerSelectorParser extends SelectorParser { + protected PlayerSelectorParser(final boolean single) { + super(single, true); + } + + @Override + protected List legacySuggestions( + final CommandContext commandContext, + final String input + ) { + final List suggestions = new ArrayList<>(); + + for (final Player player : Bukkit.getOnlinePlayers()) { + final CommandSender bukkit = commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER); + if (bukkit instanceof Player && !((Player) bukkit).canSee(player)) { + continue; + } + suggestions.add(player.getName()); + } + + return suggestions; + } + } + + private static class ModernSelectorParser implements ArgumentParser { + private final ArgumentParser wrappedBrigadierParser; + private final SelectorMapper mapper; + + ModernSelectorParser( + final ArgumentParser wrapperBrigParser, + final SelectorMapper mapper + ) { + this.wrappedBrigadierParser = wrapperBrigParser; + this.mapper = mapper; + } + + @Override + public ArgumentParseResult parse( + final CommandContext commandContext, + final Queue inputQueue + ) { + final List originalInputQueue = new ArrayList<>(inputQueue); + + final ArgumentParseResult result = this.wrappedBrigadierParser.parse(commandContext, inputQueue); + if (result.getFailure().isPresent()) { + return ArgumentParseResult.failure(result.getFailure().get()); + } else if (result.getParsedValue().isPresent()) { + try { + final int consumed = originalInputQueue.size() - inputQueue.size(); + final String input = String.join(" ", originalInputQueue.subList(0, consumed)); + return ArgumentParseResult.success(this.mapper.mapResult( + input, + new EntitySelectorWrapper(commandContext, result.getParsedValue().get()) + )); + } catch (final CommandSyntaxException ex) { + inputQueue.clear(); + inputQueue.addAll(originalInputQueue); + return ArgumentParseResult.failure(ex); + } catch (final Exception ex) { + throw rethrow(ex); + } + } + throw new IllegalStateException(); + } + + @Override + public List suggestions( + final CommandContext commandContext, + final String input + ) { + return this.wrappedBrigadierParser.suggestions(commandContext, input); + } + } + + static final class EntitySelectorWrapper { + private static volatile @Nullable Methods methods; + + private final CommandContext commandContext; + private final Object selector; + + private static final class Methods { + private Method getBukkitEntity; + private Method entity; + private Method player; + private Method entities; + private Method players; + + Methods(final CommandContext commandContext, final Object selector) { + final Object nativeSender = commandContext.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER); + final Class nativeSenderClass = nativeSender.getClass(); + for (final Method method : selector.getClass().getDeclaredMethods()) { + if (method.getParameterCount() != 1 || !method.getParameterTypes()[0].equals(nativeSenderClass)) { + continue; + } + + final Class returnType = method.getReturnType(); + if (List.class.isAssignableFrom(returnType)) { + final ParameterizedType stringListType = (ParameterizedType) method.getGenericReturnType(); + Type listType = stringListType.getActualTypeArguments()[0]; + while (listType instanceof WildcardType) { + listType = ((WildcardType) listType).getUpperBounds()[0]; + } + final Class clazz = listType instanceof Class + ? (Class) listType + : GenericTypeReflector.erase(listType); + final @Nullable Method getBukkitEntity = findGetBukkitEntityMethod(clazz); + if (getBukkitEntity == null) { + continue; + } + final Class bukkitType = getBukkitEntity.getReturnType(); + if (Player.class.isAssignableFrom(bukkitType)) { + if (this.players != null) { + throw new IllegalStateException(); + } + this.players = method; + } else { + if (this.entities != null) { + throw new IllegalStateException(); + } + this.entities = method; + } + } else if (returnType != Void.TYPE) { + final @Nullable Method getBukkitEntity = findGetBukkitEntityMethod(returnType); + if (getBukkitEntity == null) { + continue; + } + final Class bukkitType = getBukkitEntity.getReturnType(); + if (Player.class.isAssignableFrom(bukkitType)) { + if (this.player != null) { + throw new IllegalStateException(); + } + this.player = method; + } else { + if (this.entity != null || this.getBukkitEntity != null) { + throw new IllegalStateException(); + } + this.entity = method; + this.getBukkitEntity = getBukkitEntity; + } + } + } + Objects.requireNonNull(this.getBukkitEntity); + Objects.requireNonNull(this.player); + Objects.requireNonNull(this.entity); + Objects.requireNonNull(this.players); + Objects.requireNonNull(this.entities); + } + + private static @Nullable Method findGetBukkitEntityMethod(final Class returnType) { + @Nullable Method getBukkitEntity; + try { + getBukkitEntity = returnType.getDeclaredMethod("getBukkitEntity"); + } catch (final ReflectiveOperationException ex) { + try { + getBukkitEntity = returnType.getMethod("getBukkitEntity"); + } catch (final ReflectiveOperationException ex0) { + getBukkitEntity = null; + } + } + return getBukkitEntity; + } + } + + EntitySelectorWrapper( + final CommandContext commandContext, + final Object selector + ) { + this.commandContext = commandContext; + this.selector = selector; + } + + private static Methods methods(final CommandContext commandContext, final Object selector) { + if (methods == null) { + synchronized (Methods.class) { + if (methods == null) { + methods = new Methods(commandContext, selector); + } + } + } + return methods; + } + + private Methods methods() { + return methods(this.commandContext, this.selector); + } + + Entity singleEntity() { + return reflectiveOperation(() -> (Entity) this.methods().getBukkitEntity.invoke(this.methods().entity.invoke( + this.selector, + this.commandContext.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER) + ))); + } + + Player singlePlayer() { + return reflectiveOperation(() -> (Player) this.methods().getBukkitEntity.invoke(this.methods().player.invoke( + this.selector, + this.commandContext.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER) + ))); + } + + @SuppressWarnings("unchecked") + List entities() { + return reflectiveOperation(() -> ((List) this.methods().entities.invoke( + this.selector, + this.commandContext.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER) + )) + .stream() + .map(o -> reflectiveOperation(() -> (Entity) this.methods().getBukkitEntity.invoke(o))) + .collect(Collectors.toList())); + } + + @SuppressWarnings("unchecked") + List players() { + return reflectiveOperation(() -> ((List) this.methods().players.invoke( + this.selector, + this.commandContext.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER) + )) + .stream() + .map(o -> reflectiveOperation(() -> (Player) this.methods().getBukkitEntity.invoke(o))) + .collect(Collectors.toList())); + } + + @FunctionalInterface + interface ReflectiveOperation { + T run() throws ReflectiveOperationException; + } + + private static T reflectiveOperation(final ReflectiveOperation op) { + try { + return op.run(); + } catch (final InvocationTargetException ex) { + if (ex.getCause() instanceof CommandSyntaxException) { + throw rethrow(ex.getCause()); + } + throw new RuntimeException(ex); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + } + + @FunctionalInterface + interface SelectorMapper { + T mapResult(String input, EntitySelectorWrapper wrapper) throws Exception; // throws CommandSyntaxException + } + + @SuppressWarnings("unchecked") + private static RuntimeException rethrow(final Throwable t) throws X { + throw (X) t; + } +} diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SingleEntitySelectorArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SingleEntitySelectorArgument.java index 427c7392..9dcceb1a 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SingleEntitySelectorArgument.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SingleEntitySelectorArgument.java @@ -25,18 +25,12 @@ package cloud.commandframework.bukkit.parsers.selector; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.arguments.CommandArgument; -import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; -import cloud.commandframework.bukkit.BukkitCommandContextKeys; -import cloud.commandframework.bukkit.CloudBukkitCapabilities; import cloud.commandframework.bukkit.arguments.selector.SingleEntitySelector; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.exceptions.parsing.NoInputProvidedException; +import java.util.Collections; import java.util.List; -import java.util.Queue; import java.util.function.BiFunction; -import org.bukkit.Bukkit; -import org.bukkit.entity.Entity; +import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -67,9 +61,25 @@ public final class SingleEntitySelectorArgument extends CommandArgument Command sender type * @return Created builder + * @deprecated prefer {@link #builder(String)} */ - public static SingleEntitySelectorArgument.@NonNull Builder newBuilder(final @NonNull String name) { - return new SingleEntitySelectorArgument.Builder<>(name); + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated + public static @NonNull Builder newBuilder(final @NonNull String name) { + return builder(name); + } + + /** + * Create a new {@link Builder}. + * + * @param name argument name + * @param sender type + * @return new builder + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public static @NonNull Builder builder(final @NonNull String name) { + return new Builder<>(name); } /** @@ -79,8 +89,8 @@ public final class SingleEntitySelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument of(final @NonNull String name) { - return SingleEntitySelectorArgument.newBuilder(name).asRequired().build(); + public static @NonNull SingleEntitySelectorArgument of(final @NonNull String name) { + return SingleEntitySelectorArgument.builder(name).asRequired().build(); } /** @@ -90,8 +100,8 @@ public final class SingleEntitySelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional(final @NonNull String name) { - return SingleEntitySelectorArgument.newBuilder(name).asOptional().build(); + public static @NonNull SingleEntitySelectorArgument optional(final @NonNull String name) { + return SingleEntitySelectorArgument.builder(name).asOptional().build(); } /** @@ -102,15 +112,15 @@ public final class SingleEntitySelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional( + public static @NonNull SingleEntitySelectorArgument optional( final @NonNull String name, final @NonNull String defaultEntitySelector ) { - return SingleEntitySelectorArgument.newBuilder(name).asOptionalWithDefault(defaultEntitySelector).build(); + return SingleEntitySelectorArgument.builder(name).asOptionalWithDefault(defaultEntitySelector).build(); } - public static final class Builder extends CommandArgument.Builder { + public static final class Builder extends CommandArgument.TypedBuilder> { private Builder(final @NonNull String name) { super(SingleEntitySelector.class, name); @@ -123,60 +133,32 @@ public final class SingleEntitySelectorArgument extends CommandArgument build() { - return new SingleEntitySelectorArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(), - this.getSuggestionsProvider(), this.getDefaultDescription() + return new SingleEntitySelectorArgument<>( + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getSuggestionsProvider(), + this.getDefaultDescription() ); } } - public static final class SingleEntitySelectorParser implements ArgumentParser { + public static final class SingleEntitySelectorParser extends SelectorUtils.EntitySelectorParser { + + /** + * Creates a new {@link SingleEntitySelectorParser}. + */ + public SingleEntitySelectorParser() { + super(true); + } @Override - public @NonNull ArgumentParseResult parse( - final @NonNull CommandContext commandContext, - final @NonNull Queue<@NonNull String> inputQueue + public SingleEntitySelector mapResult( + final @NonNull String input, + final SelectorUtils.@NonNull EntitySelectorWrapper wrapper ) { - if (!commandContext.get(BukkitCommandContextKeys.CLOUD_BUKKIT_CAPABILITIES).contains( - CloudBukkitCapabilities.BRIGADIER)) { - return ArgumentParseResult.failure(new SelectorParseException( - "", - commandContext, - SelectorParseException.FailureReason.UNSUPPORTED_VERSION, - SingleEntitySelectorParser.class - )); - } - final String input = inputQueue.peek(); - if (input == null) { - return ArgumentParseResult.failure(new NoInputProvidedException( - SingleEntitySelectorParser.class, - commandContext - )); - } - - List entities; - try { - entities = Bukkit.selectEntities(commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER), input); - } catch (IllegalArgumentException e) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.MALFORMED_SELECTOR, - SingleEntitySelectorParser.class - )); - } - - if (entities.size() > 1) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.TOO_MANY_ENTITIES, - SingleEntitySelectorParser.class - )); - } - - inputQueue.remove(); - return ArgumentParseResult.success(new SingleEntitySelector(input, entities)); + return new SingleEntitySelector(input, Collections.singletonList(wrapper.singleEntity())); } } } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SinglePlayerSelectorArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SinglePlayerSelectorArgument.java index 2767c839..ee5e209f 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SinglePlayerSelectorArgument.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/selector/SinglePlayerSelectorArgument.java @@ -26,21 +26,16 @@ package cloud.commandframework.bukkit.parsers.selector; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; -import cloud.commandframework.bukkit.BukkitCommandContextKeys; -import cloud.commandframework.bukkit.CloudBukkitCapabilities; import cloud.commandframework.bukkit.arguments.selector.SinglePlayerSelector; import cloud.commandframework.bukkit.parsers.PlayerArgument; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.exceptions.parsing.NoInputProvidedException; import com.google.common.collect.ImmutableList; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Queue; import java.util.function.BiFunction; +import org.apiguardian.api.API; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -72,9 +67,25 @@ public final class SinglePlayerSelectorArgument extends CommandArgument Command sender type * @return Created builder + * @deprecated prefer {@link #builder(String)} */ - public static SinglePlayerSelectorArgument.@NonNull Builder newBuilder(final @NonNull String name) { - return new SinglePlayerSelectorArgument.Builder<>(name); + @API(status = API.Status.DEPRECATED, since = "1.8.0") + @Deprecated + public static @NonNull Builder newBuilder(final @NonNull String name) { + return builder(name); + } + + /** + * Create a new {@link Builder}. + * + * @param name argument name + * @param sender type + * @return new builder + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public static @NonNull Builder builder(final @NonNull String name) { + return new Builder<>(name); } /** @@ -84,8 +95,8 @@ public final class SinglePlayerSelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument of(final @NonNull String name) { - return SinglePlayerSelectorArgument.newBuilder(name).asRequired().build(); + public static @NonNull SinglePlayerSelectorArgument of(final @NonNull String name) { + return SinglePlayerSelectorArgument.builder(name).asRequired().build(); } /** @@ -95,8 +106,8 @@ public final class SinglePlayerSelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional(final @NonNull String name) { - return SinglePlayerSelectorArgument.newBuilder(name).asOptional().build(); + public static @NonNull SinglePlayerSelectorArgument optional(final @NonNull String name) { + return SinglePlayerSelectorArgument.builder(name).asOptional().build(); } /** @@ -107,15 +118,15 @@ public final class SinglePlayerSelectorArgument extends CommandArgument Command sender type * @return Created argument */ - public static @NonNull CommandArgument optional( + public static @NonNull SinglePlayerSelectorArgument optional( final @NonNull String name, final @NonNull String defaultEntitySelector ) { - return SinglePlayerSelectorArgument.newBuilder(name).asOptionalWithDefault(defaultEntitySelector).build(); + return SinglePlayerSelectorArgument.builder(name).asOptionalWithDefault(defaultEntitySelector).build(); } - public static final class Builder extends CommandArgument.Builder { + public static final class Builder extends CommandArgument.TypedBuilder> { private Builder(final @NonNull String name) { super(SinglePlayerSelector.class, name); @@ -128,91 +139,48 @@ public final class SinglePlayerSelectorArgument extends CommandArgument build() { - return new SinglePlayerSelectorArgument<>(this.isRequired(), this.getName(), this.getDefaultValue(), - this.getSuggestionsProvider(), this.getDefaultDescription() + return new SinglePlayerSelectorArgument<>( + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getSuggestionsProvider(), + this.getDefaultDescription() ); } } - public static final class SinglePlayerSelectorParser implements ArgumentParser { + public static final class SinglePlayerSelectorParser extends SelectorUtils.PlayerSelectorParser { + + /** + * Creates a new {@link SinglePlayerSelectorParser}. + */ + public SinglePlayerSelectorParser() { + super(true); + } @Override - public @NonNull ArgumentParseResult parse( + public SinglePlayerSelector mapResult( + final @NonNull String input, + final SelectorUtils.@NonNull EntitySelectorWrapper wrapper + ) { + return new SinglePlayerSelector(input, Collections.singletonList(wrapper.singlePlayer())); + } + + @Override + protected @NonNull ArgumentParseResult legacyParse( final @NonNull CommandContext commandContext, final @NonNull Queue<@NonNull String> inputQueue ) { final String input = inputQueue.peek(); - if (input == null) { - return ArgumentParseResult.failure(new NoInputProvidedException( - SinglePlayerSelectorParser.class, - commandContext - )); - } + @SuppressWarnings("deprecation") final @Nullable Player player = Bukkit.getPlayer(input); - if (!commandContext.get(BukkitCommandContextKeys.CLOUD_BUKKIT_CAPABILITIES).contains( - CloudBukkitCapabilities.BRIGADIER)) { - @SuppressWarnings("deprecation") - Player player = Bukkit.getPlayer(input); - - if (player == null) { - return ArgumentParseResult.failure(new PlayerArgument.PlayerParseException(input, commandContext)); - } - inputQueue.remove(); - return ArgumentParseResult.success(new SinglePlayerSelector(input, ImmutableList.of(player))); + if (player == null) { + return ArgumentParseResult.failure(new PlayerArgument.PlayerParseException(input, commandContext)); } - - List entities; - try { - entities = Bukkit.selectEntities(commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER), input); - } catch (IllegalArgumentException e) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.MALFORMED_SELECTOR, - SinglePlayerSelectorParser.class - )); - } - - for (Entity e : entities) { - if (!(e instanceof Player)) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.NON_PLAYER_IN_PLAYER_SELECTOR, - SinglePlayerSelectorParser.class - )); - } - } - if (entities.size() > 1) { - return ArgumentParseResult.failure(new SelectorParseException( - input, - commandContext, - SelectorParseException.FailureReason.TOO_MANY_PLAYERS, - SinglePlayerSelectorParser.class - )); - } - inputQueue.remove(); - return ArgumentParseResult.success(new SinglePlayerSelector(input, entities)); + return ArgumentParseResult.success(new SinglePlayerSelector(input, ImmutableList.of(player))); } - @Override - public @NonNull List<@NonNull String> suggestions( - final @NonNull CommandContext commandContext, - final @NonNull String input - ) { - List output = new ArrayList<>(); - - for (Player player : Bukkit.getOnlinePlayers()) { - final CommandSender bukkit = commandContext.get(BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER); - if (bukkit instanceof Player && !((Player) bukkit).canSee(player)) { - continue; - } - output.add(player.getName()); - } - - return output; - } } }