diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e34848b..df4f58aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - JDA Role argument parser - Bukkit: Implement parser for ProtoItemStack ([#257](https://github.com/Incendo/cloud/pull/257)) + - Bukkit: Implement parsers for ItemStackPredicate and BlockPredicate ([#259](https://github.com/Incendo/cloud/pull/259)) ### Changed - Use Command instead of TabCompleteEvent on Bukkit diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitBrigadierMapper.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitBrigadierMapper.java index 26d84990..ae6a6ef1 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitBrigadierMapper.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitBrigadierMapper.java @@ -27,8 +27,10 @@ import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.standard.UUIDArgument; import cloud.commandframework.brigadier.CloudBrigadierManager; import cloud.commandframework.bukkit.internal.CraftBukkitReflection; +import cloud.commandframework.bukkit.parsers.BlockPredicateArgument; import cloud.commandframework.bukkit.parsers.EnchantmentArgument; import cloud.commandframework.bukkit.parsers.ItemStackArgument; +import cloud.commandframework.bukkit.parsers.ItemStackPredicateArgument; import cloud.commandframework.bukkit.parsers.location.Location2DArgument; import cloud.commandframework.bukkit.parsers.location.LocationArgument; import cloud.commandframework.bukkit.parsers.selector.MultipleEntitySelectorArgument; @@ -90,9 +92,14 @@ public final class BukkitBrigadierMapper { /* Map Enchantment */ this.mapSimpleNMS(new TypeToken>() { }, "Enchantment"); - /* Map ItemStackArgument */ + /* Map Item arguments */ this.mapSimpleNMS(new TypeToken>() { }, "ItemStack"); + this.mapSimpleNMS(new TypeToken>() { + }, "ItemPredicate"); + /* Map Block arguments */ + this.mapSimpleNMS(new TypeToken>() { + }, "BlockPredicate"); /* Map Entity Selectors */ this.mapNMS(new TypeToken>() { }, this.entitySelectorArgumentSupplier(true, false)); @@ -105,9 +112,9 @@ public final class BukkitBrigadierMapper { /* Map Vec3 */ this.mapNMS(new TypeToken>() { }, this::argumentVec3); - /* Map Vec2I */ + /* Map Vec2 */ this.mapNMS(new TypeToken>() { - }, this::argumentVec2i); + }, this::argumentVec2); } /** @@ -141,11 +148,11 @@ public final class BukkitBrigadierMapper { } } - private @NonNull ArgumentType argumentVec2i() { + private @NonNull ArgumentType argumentVec2() { try { - return (ArgumentType) getNMSArgument("Vec2I").getDeclaredConstructor().newInstance(); + return (ArgumentType) getNMSArgument("Vec2").getDeclaredConstructor().newInstance(); } catch (final Exception e) { - this.commandManager.getOwningPlugin().getLogger().log(Level.INFO, "Failed to retrieve Vec2I argument", e); + this.commandManager.getOwningPlugin().getLogger().log(Level.INFO, "Failed to retrieve Vec2 argument", e); return fallbackType(); } } 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 fc49b404..7dae4987 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 @@ -33,8 +33,10 @@ import cloud.commandframework.bukkit.arguments.selector.SingleEntitySelector; import cloud.commandframework.bukkit.arguments.selector.SinglePlayerSelector; import cloud.commandframework.bukkit.data.ProtoItemStack; import cloud.commandframework.bukkit.internal.CraftBukkitReflection; +import cloud.commandframework.bukkit.parsers.BlockPredicateArgument; import cloud.commandframework.bukkit.parsers.EnchantmentArgument; import cloud.commandframework.bukkit.parsers.ItemStackArgument; +import cloud.commandframework.bukkit.parsers.ItemStackPredicateArgument; import cloud.commandframework.bukkit.parsers.MaterialArgument; import cloud.commandframework.bukkit.parsers.OfflinePlayerArgument; import cloud.commandframework.bukkit.parsers.PlayerArgument; @@ -62,6 +64,7 @@ import org.bukkit.plugin.Plugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.reflect.Method; import java.util.EnumSet; import java.util.Set; import java.util.function.Function; @@ -169,6 +172,12 @@ public class BukkitCommandManager extends CommandManager implements Brigad this.getParserRegistry().registerParserSupplier(TypeToken.get(MultiplePlayerSelector.class), parserParameters -> new MultiplePlayerSelectorArgument.MultiplePlayerSelectorParser<>()); + /* Register MC 1.13+ parsers */ + if (this.minecraftVersion >= BRIGADIER_MINIMUM_VERSION) { + this.registerParserSupplierFor(ItemStackPredicateArgument.class); + this.registerParserSupplierFor(BlockPredicateArgument.class); + } + /* Register suggestion and state listener */ this.owningPlugin.getServer().getPluginManager().registerEvents( new CloudBukkitListener<>(this), @@ -396,6 +405,24 @@ public class BukkitCommandManager extends CommandManager implements Brigad return this.backwardsCommandSenderMapper; } + /** + * Attempts to call the method on the provided class matching the signature + *

{@code private static void registerParserSupplier(BukkitCommandManager)}

+ * using reflection. + * + * @param argumentClass argument class + */ + private void registerParserSupplierFor(final @NonNull Class argumentClass) { + try { + final Method registerParserSuppliers = argumentClass + .getDeclaredMethod("registerParserSupplier", BukkitCommandManager.class); + registerParserSuppliers.setAccessible(true); + registerParserSuppliers.invoke(null, this); + } catch (final ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + final void lockIfBrigadierCapable() { if (this.minecraftVersion >= BRIGADIER_MINIMUM_VERSION) { this.lockRegistration(); diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/data/BlockPredicate.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/data/BlockPredicate.java new file mode 100644 index 00000000..76fc410e --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/data/BlockPredicate.java @@ -0,0 +1,55 @@ +// +// 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.data; + +import org.bukkit.World; +import org.bukkit.block.Block; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Predicate; + +/** + * A {@link Predicate} for {@link Block Blocks} in a {@link World}, parsed from user input. + * + *

By default, a parsed {@link BlockPredicate} will not load chunks to perform tests. It will simply + * return {@code false} when attempting to test a block in unloaded chunks.

+ * + *

To get a {@link BlockPredicate} which will load chunks, use {@link #loadChunks()}.

+ * + * @since 1.5.0 + */ +public interface BlockPredicate extends Predicate { + + /** + * Get a version of this {@link BlockPredicate} which will load chunks in order to perform + * tests. + * + *

If this {@link BlockPredicate} already loads chunks, it will simply return itself.

+ * + * @return a {@link BlockPredicate} which loads chunks + * @since 1.5.0 + */ + @NonNull BlockPredicate loadChunks(); + +} diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/data/ItemStackPredicate.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/data/ItemStackPredicate.java new file mode 100644 index 00000000..c1d6a9b3 --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/data/ItemStackPredicate.java @@ -0,0 +1,37 @@ +// +// 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.data; + +import org.bukkit.inventory.ItemStack; + +import java.util.function.Predicate; + +/** + * {@link Predicate} for {@link ItemStack ItemStacks}, parsed from user input. + * + * @since 1.5.0 + */ +public interface ItemStackPredicate extends Predicate { + +} diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/internal/CraftBukkitReflection.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/internal/CraftBukkitReflection.java index 2ab3d085..2dd06926 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/internal/CraftBukkitReflection.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/internal/CraftBukkitReflection.java @@ -28,6 +28,7 @@ import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -99,15 +100,18 @@ public final class CraftBukkitReflection { } } - public static boolean classExists(final @NonNull String className) { + public static @NonNull Constructor needConstructor(final @NonNull Class holder, final @NonNull Class... parameters) { try { - Class.forName(className); - return true; - } catch (ClassNotFoundException e) { - return false; + return holder.getDeclaredConstructor(parameters); + } catch (final NoSuchMethodException ex) { + throw new RuntimeException(ex); } } + public static boolean classExists(final @NonNull String className) { + return findClass(className) != null; + } + public static @NonNull Method needMethod( final @NonNull Class holder, final @NonNull String name, diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/BlockPredicateArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/BlockPredicateArgument.java new file mode 100644 index 00000000..ad60e7cc --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/BlockPredicateArgument.java @@ -0,0 +1,281 @@ +// +// 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; + +import cloud.commandframework.ArgumentDescription; +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; +import cloud.commandframework.bukkit.BukkitCommandManager; +import cloud.commandframework.bukkit.data.BlockPredicate; +import cloud.commandframework.bukkit.internal.CraftBukkitReflection; +import cloud.commandframework.context.CommandContext; +import com.mojang.brigadier.arguments.ArgumentType; +import io.leangen.geantyref.TypeToken; +import org.bukkit.block.Block; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Queue; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +/** + * Argument type for parsing a {@link BlockPredicate}. + * + *

This argument type is only usable on Minecraft 1.13+, as it depends on Minecraft internals added in that version.

+ * + *

This argument type only provides basic suggestions by default. Enabling Brigadier compatibility through + * {@link BukkitCommandManager#registerBrigadier()} will allow client side validation and suggestions to be utilized.

+ * + * @param Command sender type + * @since 1.5.0 + */ +public final class BlockPredicateArgument extends CommandArgument { + + private BlockPredicateArgument( + final boolean required, + final @NonNull String name, + final @NonNull String defaultValue, + final @Nullable BiFunction<@NonNull CommandContext, @NonNull String, + @NonNull List<@NonNull String>> suggestionsProvider, + final @NonNull ArgumentDescription defaultDescription + ) { + super(required, name, new Parser<>(), defaultValue, BlockPredicate.class, suggestionsProvider, defaultDescription); + } + + /** + * Create a new {@link Builder}. + * + * @param name Name of the argument + * @param Command sender type + * @return Created builder + * @since 1.5.0 + */ + public static BlockPredicateArgument.@NonNull Builder builder(final @NonNull String name) { + return new BlockPredicateArgument.Builder<>(name); + } + + /** + * Create a new required {@link BlockPredicateArgument}. + * + * @param name Argument name + * @param Command sender type + * @return Created argument + * @since 1.5.0 + */ + public static @NonNull BlockPredicateArgument of(final @NonNull String name) { + return BlockPredicateArgument.builder(name).build(); + } + + /** + * Create a new optional {@link BlockPredicateArgument}. + * + * @param name Argument name + * @param Command sender type + * @return Created argument + * @since 1.5.0 + */ + public static @NonNull BlockPredicateArgument optional(final @NonNull String name) { + return BlockPredicateArgument.builder(name).asOptional().build(); + } + + + /** + * Builder for {@link BlockPredicateArgument}. + * + * @param sender type + * @since 1.5.0 + */ + public static final class Builder extends TypedBuilder> { + + private Builder(final @NonNull String name) { + super(BlockPredicate.class, name); + } + + @Override + public @NonNull BlockPredicateArgument build() { + return new BlockPredicateArgument<>( + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getSuggestionsProvider(), + this.getDefaultDescription() + ); + } + + } + + /** + * Parser for {@link BlockPredicateArgument}. Only supported on Minecraft 1.13 and newer CraftBukkit based servers. + * + * @param sender type + * @since 1.5.0 + */ + public static final class Parser implements ArgumentParser { + + private static final Class TAG_REGISTRY_CLASS; + + static { + if (CraftBukkitReflection.MAJOR_REVISION >= 16) { + TAG_REGISTRY_CLASS = CraftBukkitReflection.needNMSClass("ITagRegistry"); + } else { + TAG_REGISTRY_CLASS = CraftBukkitReflection.needNMSClass("TagRegistry"); + } + } + + private static final Class CRAFT_WORLD_CLASS = CraftBukkitReflection.needOBCClass("CraftWorld"); + private static final Class MINECRAFT_SERVER_CLASS = CraftBukkitReflection.needNMSClass("MinecraftServer"); + private static final Class COMMAND_LISTENER_WRAPPER_CLASS = + CraftBukkitReflection.needNMSClass("CommandListenerWrapper"); + private static final Class ARGUMENT_BLOCK_PREDICATE_CLASS = + CraftBukkitReflection.needNMSClass("ArgumentBlockPredicate"); + private static final Class ARGUMENT_BLOCK_PREDICATE_RESULT_CLASS = + CraftBukkitReflection.needNMSClass("ArgumentBlockPredicate$b"); + private static final Class SHAPE_DETECTOR_BLOCK_CLASS = // BlockInWorld + CraftBukkitReflection.needNMSClass("ShapeDetectorBlock"); + private static final Class I_WORLD_READER_CLASS = CraftBukkitReflection.needNMSClass("IWorldReader"); + private static final Class BLOCK_POSITION_CLASS = CraftBukkitReflection.needNMSClass("BlockPosition"); + private static final Constructor BLOCK_POSITION_CTR = + CraftBukkitReflection.needConstructor(BLOCK_POSITION_CLASS, int.class, int.class, int.class); + private static final Constructor SHAPE_DETECTOR_BLOCK_CTR = CraftBukkitReflection + .needConstructor(SHAPE_DETECTOR_BLOCK_CLASS, I_WORLD_READER_CLASS, BLOCK_POSITION_CLASS, boolean.class); + private static final Method GET_HANDLE_METHOD = CraftBukkitReflection.needMethod(CRAFT_WORLD_CLASS, "getHandle"); + private static final Method CREATE_PREDICATE_METHOD = + CraftBukkitReflection.needMethod(ARGUMENT_BLOCK_PREDICATE_RESULT_CLASS, "create", TAG_REGISTRY_CLASS); + private static final Method GET_SERVER_METHOD = + CraftBukkitReflection.needMethod(COMMAND_LISTENER_WRAPPER_CLASS, "getServer"); + private static final Method GET_TAG_REGISTRY_METHOD = + CraftBukkitReflection.needMethod(MINECRAFT_SERVER_CLASS, "getTagRegistry"); + + private final ArgumentParser parser; + + /** + * Create a new {@link Parser}. + * + * @since 1.5.0 + */ + public Parser() { + try { + this.parser = this.createParser(); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException("Failed to initialize BlockPredicate parser.", ex); + } + } + + @SuppressWarnings("unchecked") + private ArgumentParser createParser() throws ReflectiveOperationException { + return new WrappedBrigadierParser( + (ArgumentType) ARGUMENT_BLOCK_PREDICATE_CLASS.getConstructor().newInstance() + ).map((ctx, result) -> { + final Object commandSourceStack = ctx.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER); + try { + final Object server = GET_SERVER_METHOD.invoke(commandSourceStack); + final Object tagRegistry = GET_TAG_REGISTRY_METHOD.invoke(server); + final Predicate predicate = (Predicate) CREATE_PREDICATE_METHOD.invoke(result, tagRegistry); + return ArgumentParseResult.success(new BlockPredicateImpl(predicate)); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + }); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull BlockPredicate> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + return this.parser.parse(commandContext, inputQueue); + } + + @Override + public @NonNull List<@NonNull String> suggestions( + final @NonNull CommandContext commandContext, + final @NonNull String input + ) { + return this.parser.suggestions(commandContext, input); + } + + private static final class BlockPredicateImpl implements BlockPredicate { + + private final Predicate predicate; + + BlockPredicateImpl(final @NonNull Predicate predicate) { + this.predicate = predicate; + } + + private boolean testImpl(final @NonNull Block block, final boolean loadChunks) { + try { + final Object blockInWorld = SHAPE_DETECTOR_BLOCK_CTR.newInstance( + GET_HANDLE_METHOD.invoke(block.getWorld()), + BLOCK_POSITION_CTR.newInstance(block.getX(), block.getY(), block.getZ()), + loadChunks + ); + return this.predicate.test(blockInWorld); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public boolean test(final @NonNull Block block) { + return this.testImpl(block, false); + } + + @Override + public @NonNull BlockPredicate loadChunks() { + return new BlockPredicate() { + @Override + public @NonNull BlockPredicate loadChunks() { + return this; + } + + @Override + public boolean test(final Block block) { + return BlockPredicateImpl.this.testImpl(block, true); + } + }; + } + + } + + } + + /** + * Called reflectively by {@link BukkitCommandManager}. + * + * @param commandManager command manager + * @param sender type + */ + @SuppressWarnings("unused") + private static void registerParserSupplier(final @NonNull BukkitCommandManager commandManager) { + commandManager.getParserRegistry() + .registerParserSupplier(TypeToken.get(BlockPredicate.class), params -> new BlockPredicateArgument.Parser<>()); + } + +} diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackArgument.java index f7c929f8..44934ef3 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackArgument.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackArgument.java @@ -92,7 +92,7 @@ public final class ItemStackArgument extends CommandArgument @NonNull CommandArgument of(final @NonNull String name) { + public static @NonNull ItemStackArgument of(final @NonNull String name) { return ItemStackArgument.builder(name).build(); } @@ -104,7 +104,7 @@ public final class ItemStackArgument extends CommandArgument @NonNull CommandArgument optional(final @NonNull String name) { + public static @NonNull ItemStackArgument optional(final @NonNull String name) { return ItemStackArgument.builder(name).asOptional().build(); } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackPredicateArgument.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackPredicateArgument.java new file mode 100644 index 00000000..a26b22d7 --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/parsers/ItemStackPredicateArgument.java @@ -0,0 +1,257 @@ +// +// 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; + +import cloud.commandframework.ArgumentDescription; +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; +import cloud.commandframework.bukkit.BukkitCommandManager; +import cloud.commandframework.bukkit.data.ItemStackPredicate; +import cloud.commandframework.bukkit.internal.CraftBukkitReflection; +import cloud.commandframework.context.CommandContext; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.StringRange; +import io.leangen.geantyref.TypeToken; +import org.bukkit.inventory.ItemStack; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +/** + * Argument type for parsing an {@link ItemStackPredicate}. + * + *

This argument type is only usable on Minecraft 1.13+, as it depends on Minecraft internals added in that version.

+ * + *

This argument type only provides basic suggestions by default. Enabling Brigadier compatibility through + * {@link BukkitCommandManager#registerBrigadier()} will allow client side validation and suggestions to be utilized.

+ * + * @param Command sender type + * @since 1.5.0 + */ +public final class ItemStackPredicateArgument extends CommandArgument { + + private ItemStackPredicateArgument( + final boolean required, + final @NonNull String name, + final @NonNull String defaultValue, + final @Nullable BiFunction<@NonNull CommandContext, @NonNull String, + @NonNull List<@NonNull String>> suggestionsProvider, + final @NonNull ArgumentDescription defaultDescription + ) { + super(required, name, new Parser<>(), defaultValue, ItemStackPredicate.class, suggestionsProvider, defaultDescription); + } + + /** + * Create a new {@link Builder}. + * + * @param name Name of the argument + * @param Command sender type + * @return Created builder + * @since 1.5.0 + */ + public static ItemStackPredicateArgument.@NonNull Builder builder(final @NonNull String name) { + return new ItemStackPredicateArgument.Builder<>(name); + } + + /** + * Create a new required {@link ItemStackPredicateArgument}. + * + * @param name Argument name + * @param Command sender type + * @return Created argument + * @since 1.5.0 + */ + public static @NonNull ItemStackPredicateArgument of(final @NonNull String name) { + return ItemStackPredicateArgument.builder(name).build(); + } + + /** + * Create a new optional {@link ItemStackPredicateArgument}. + * + * @param name Argument name + * @param Command sender type + * @return Created argument + * @since 1.5.0 + */ + public static @NonNull ItemStackPredicateArgument optional(final @NonNull String name) { + return ItemStackPredicateArgument.builder(name).asOptional().build(); + } + + + /** + * Builder for {@link ItemStackPredicateArgument}. + * + * @param sender type + * @since 1.5.0 + */ + public static final class Builder extends TypedBuilder> { + + private Builder(final @NonNull String name) { + super(ItemStackPredicate.class, name); + } + + @Override + public @NonNull ItemStackPredicateArgument build() { + return new ItemStackPredicateArgument<>( + this.isRequired(), + this.getName(), + this.getDefaultValue(), + this.getSuggestionsProvider(), + this.getDefaultDescription() + ); + } + + } + + /** + * Parser for {@link ItemStackPredicateArgument}. Only supported on Minecraft 1.13 and newer CraftBukkit based servers. + * + * @param sender type + * @since 1.5.0 + */ + public static final class Parser implements ArgumentParser { + + private static final Class CRAFT_ITEM_STACK_CLASS = + CraftBukkitReflection.needOBCClass("inventory.CraftItemStack"); + private static final Class ARGUMENT_ITEM_PREDICATE_CLASS = + CraftBukkitReflection.needNMSClass("ArgumentItemPredicate"); + private static final Class ARGUMENT_ITEM_PREDICATE_RESULT_CLASS = + CraftBukkitReflection.needNMSClass("ArgumentItemPredicate$b"); + private static final Method CREATE_PREDICATE_METHOD = CraftBukkitReflection.needMethod( + ARGUMENT_ITEM_PREDICATE_RESULT_CLASS, + "create", + com.mojang.brigadier.context.CommandContext.class + ); + private static final Method AS_NMS_COPY_METHOD = + CraftBukkitReflection.needMethod(CRAFT_ITEM_STACK_CLASS, "asNMSCopy", ItemStack.class); + + private final ArgumentParser parser; + + /** + * Create a new {@link Parser}. + * + * @since 1.5.0 + */ + public Parser() { + try { + this.parser = this.createParser(); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException("Failed to initialize ItemPredicate parser.", ex); + } + } + + @SuppressWarnings("unchecked") + private ArgumentParser createParser() throws ReflectiveOperationException { + return new WrappedBrigadierParser( + (ArgumentType) ARGUMENT_ITEM_PREDICATE_CLASS.getConstructor().newInstance() + ).map((ctx, result) -> { + final Object commandSourceStack = ctx.get(WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER); + final com.mojang.brigadier.context.CommandContext dummy = createDummyContext(ctx, commandSourceStack); + try { + final Predicate predicate = (Predicate) CREATE_PREDICATE_METHOD.invoke(result, dummy); + return ArgumentParseResult.success(new ItemStackPredicateImpl(predicate)); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + }); + } + + private static com.mojang.brigadier.context.@NonNull CommandContext createDummyContext( + final @NonNull CommandContext ctx, + final @NonNull Object commandSourceStack + ) { + return new com.mojang.brigadier.context.CommandContext<>( + commandSourceStack, + ctx.getRawInputJoined(), + Collections.emptyMap(), + null, + null, + Collections.emptyList(), + StringRange.at(0), + null, + null, + false + ); + } + + @Override + public @NonNull ArgumentParseResult<@NonNull ItemStackPredicate> parse( + @NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue + ) { + return this.parser.parse(commandContext, inputQueue); + } + + @Override + public @NonNull List<@NonNull String> suggestions( + final @NonNull CommandContext commandContext, + final @NonNull String input + ) { + return this.parser.suggestions(commandContext, input); + } + + private static final class ItemStackPredicateImpl implements ItemStackPredicate { + + private final Predicate predicate; + + ItemStackPredicateImpl(final @NonNull Predicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean test(final @NonNull ItemStack itemStack) { + try { + return this.predicate.test(AS_NMS_COPY_METHOD.invoke(null, itemStack)); + } catch (final ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + } + + } + + } + + /** + * Called reflectively by {@link BukkitCommandManager}. + * + * @param commandManager command manager + * @param sender type + */ + @SuppressWarnings("unused") + private static void registerParserSupplier(final @NonNull BukkitCommandManager commandManager) { + commandManager.getParserRegistry().registerParserSupplier( + TypeToken.get(ItemStackPredicate.class), + params -> new ItemStackPredicateArgument.Parser<>() + ); + } + +} diff --git a/examples/example-bukkit/build.gradle.kts b/examples/example-bukkit/build.gradle.kts index 1963fd7a..3a25d508 100644 --- a/examples/example-bukkit/build.gradle.kts +++ b/examples/example-bukkit/build.gradle.kts @@ -22,5 +22,5 @@ dependencies { implementation("me.lucko", "commodore", Versions.commodore) implementation("net.kyori", "adventure-platform-bukkit", Versions.adventurePlatform) /* Bukkit */ - compileOnly("org.bukkit", "bukkit", "1.8.8-R0.1-SNAPSHOT") + compileOnly("org.bukkit", "bukkit", Versions.bukkit) } diff --git a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java index 9391804c..ee39cb9e 100644 --- a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java +++ b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java @@ -26,12 +26,6 @@ package cloud.commandframework.examples.bukkit; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.Command; import cloud.commandframework.CommandTree; -import cloud.commandframework.arguments.compound.ArgumentPair; -import cloud.commandframework.bukkit.data.ProtoItemStack; -import cloud.commandframework.keys.SimpleCloudKey; -import cloud.commandframework.minecraft.extras.MinecraftExceptionHandler; -import cloud.commandframework.minecraft.extras.MinecraftExtrasMetaKeys; -import cloud.commandframework.minecraft.extras.MinecraftHelp; import cloud.commandframework.annotations.AnnotationParser; import cloud.commandframework.annotations.Argument; import cloud.commandframework.annotations.CommandDescription; @@ -50,7 +44,13 @@ import cloud.commandframework.arguments.standard.StringArrayArgument; import cloud.commandframework.bukkit.BukkitCommandManager; import cloud.commandframework.bukkit.CloudBukkitCapabilities; import cloud.commandframework.bukkit.arguments.selector.SingleEntitySelector; +import cloud.commandframework.bukkit.data.BlockPredicate; +import cloud.commandframework.bukkit.data.ItemStackPredicate; +import cloud.commandframework.bukkit.data.ProtoItemStack; +import cloud.commandframework.bukkit.parsers.BlockPredicateArgument; import cloud.commandframework.bukkit.parsers.EnchantmentArgument; +import cloud.commandframework.bukkit.parsers.ItemStackArgument; +import cloud.commandframework.bukkit.parsers.ItemStackPredicateArgument; import cloud.commandframework.bukkit.parsers.MaterialArgument; import cloud.commandframework.bukkit.parsers.WorldArgument; import cloud.commandframework.bukkit.parsers.selector.SingleEntitySelectorArgument; @@ -59,7 +59,10 @@ import cloud.commandframework.captions.SimpleCaptionRegistry; import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.extra.confirmation.CommandConfirmationManager; +import cloud.commandframework.keys.SimpleCloudKey; import cloud.commandframework.meta.CommandMeta; +import cloud.commandframework.minecraft.extras.MinecraftExceptionHandler; +import cloud.commandframework.minecraft.extras.MinecraftHelp; import cloud.commandframework.minecraft.extras.RichDescription; import cloud.commandframework.minecraft.extras.TextColorArgument; import cloud.commandframework.paper.PaperCommandManager; @@ -80,6 +83,8 @@ import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; import org.bukkit.command.CommandSender; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntityType; @@ -343,7 +348,7 @@ public final class ExamplePlugin extends JavaPlugin { .argument(IntegerArgument.of("level")) .handler(c -> this.manager.taskRecipe().begin(c).synchronous(ctx -> { final Player player = ((Player) ctx.getSender()); - player.getInventory().getItemInHand().addEnchantment(ctx.get("enchant"), ctx.get("level")); + player.getInventory().getItemInMainHand().addEnchantment(ctx.get("enchant"), ctx.get("level")); }).execute())); // @@ -381,6 +386,47 @@ public final class ExamplePlugin extends JavaPlugin { ))) ); + // MC 1.13+ commands + if (this.manager.queryCapability(CloudBukkitCapabilities.BRIGADIER)) { + this.manager.command(builder.literal("replace") + .senderType(Player.class) + .argument(BlockPredicateArgument.of("predicate")) + .literal("with") + .argument(MaterialArgument.of("block")) // todo: use BlockDataArgument + .argument(IntegerArgument.newBuilder("radius").withMin(1)) + .handler(ctx -> { + final BlockData block = ctx.get("block").createBlockData(); + final BlockPredicate predicate = ctx.get("predicate"); + final int radius = ctx.get("radius"); + + final Player player = (Player) ctx.getSender(); + final Location loc = player.getLocation(); + + this.manager.taskRecipe().begin(ctx).synchronous(context -> { + for (double x = loc.getX() - radius; x < loc.getX() + radius; x++) { + for (double y = loc.getY() - radius; y < loc.getY() + radius; y++) { + for (double z = loc.getZ() - radius; z < loc.getZ() + radius; z++) { + final Block blockAt = player.getWorld().getBlockAt((int) x, (int) y, (int) z); + if (predicate.test(blockAt)) { + blockAt.setBlockData(block); + } + } + } + } + }).execute(); + })); + this.manager.command(builder.literal("test_item") + .argument(ItemStackArgument.of("item")) + .literal("is") + .argument(ItemStackPredicateArgument.of("predicate")) + .handler(ctx -> { + final ProtoItemStack protoItemStack = ctx.get("item"); + final ItemStackPredicate predicate = ctx.get("predicate"); + ctx.getSender().sendMessage("result: " + predicate.test( + protoItemStack.createItemStack(1, true))); + })); + } + // // Create a Bukkit-like command // @@ -415,20 +461,21 @@ public final class ExamplePlugin extends JavaPlugin { ); } - // vanilla-like give command - this.manager.command(this.manager.commandBuilder("givetest") + // compound itemstack arg + this.manager.command(this.manager.commandBuilder("gib") .senderType(Player.class) - .argument(ArgumentPair.of( - this.manager, + .argumentPair( "itemstack", + TypeToken.get(ItemStack.class), Pair.of("item", "amount"), - Pair.of(ProtoItemStack.class, Integer.class) - ).withMapper(ItemStack.class, (sender, pair) -> { - final ProtoItemStack proto = pair.getFirst(); - final int amount = pair.getSecond(); - return proto.createItemStack(amount, true); - }), ArgumentDescription.of("The ItemStack to give")) - .meta(MinecraftExtrasMetaKeys.DESCRIPTION, text("Vanilla-like give command")) + Pair.of(ProtoItemStack.class, Integer.class), + (sender, pair) -> { + final ProtoItemStack proto = pair.getFirst(); + final int amount = pair.getSecond(); + return proto.createItemStack(amount, true); + }, + ArgumentDescription.of("The ItemStack to give") + ) .handler(ctx -> ((Player) ctx.getSender()).getInventory().addItem(ctx.get("itemstack")))); }