diff options
author | Owen <[email protected]> | 2024-05-11 16:30:30 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2024-05-11 16:30:30 -0400 |
commit | b98d20a8ac9c21789d532652df86638a202093c7 (patch) | |
tree | cb3418675f0cb5c7ccd995d2c298fcf13c3729b0 /test-plugin | |
parent | 447f9a1e16f2273c656f4dc95f1ea838319d0fff (diff) | |
download | Paper-b98d20a8ac9c21789d532652df86638a202093c7.tar.gz Paper-b98d20a8ac9c21789d532652df86638a202093c7.zip |
Brigadier Command Support (#8235)
Adds the ability for plugins to register their own brigadier commands
---------
Co-authored-by: Jake Potrebic <[email protected]>
Co-authored-by: Jason Penilla <[email protected]>
Co-authored-by: Bjarne Koll <[email protected]>
Diffstat (limited to 'test-plugin')
9 files changed, 493 insertions, 1 deletions
diff --git a/test-plugin/build.gradle.kts b/test-plugin/build.gradle.kts index 3edf55f288..9f7d9da599 100644 --- a/test-plugin/build.gradle.kts +++ b/test-plugin/build.gradle.kts @@ -2,7 +2,6 @@ version = "1.0.0-SNAPSHOT" dependencies { compileOnly(project(":paper-api")) - compileOnly(project(":paper-mojangapi")) } tasks.processResources { diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java index 4e68423bb7..671c37fa40 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java @@ -8,5 +8,8 @@ public final class TestPlugin extends JavaPlugin implements Listener { @Override public void onEnable() { this.getServer().getPluginManager().registerEvents(this, this); + + // io.papermc.testplugin.brigtests.Registration.registerViaOnEnable(this); } + } diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java index e978b15f97..fe2b287b25 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java @@ -8,6 +8,7 @@ public class TestPluginBootstrap implements PluginBootstrap { @Override public void bootstrap(@NotNull BootstrapContext context) { + // io.papermc.testplugin.brigtests.Registration.registerViaBootstrap(context); } } diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java new file mode 100644 index 0000000000..cd24899f34 --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java @@ -0,0 +1,166 @@ +package io.papermc.testplugin.brigtests; + +import com.mojang.brigadier.Command; +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.range.DoubleRangeProvider; +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import io.papermc.testplugin.brigtests.example.ExampleAdminCommand; +import io.papermc.testplugin.brigtests.example.MaterialArgumentType; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +public final class Registration { + + private Registration() { + } + + public static void registerViaOnEnable(final JavaPlugin plugin) { + registerLegacyCommands(plugin); + registerViaLifecycleEvents(plugin); + } + + private static void registerViaLifecycleEvents(final JavaPlugin plugin) { + final LifecycleEventManager<Plugin> lifecycleManager = plugin.getLifecycleManager(); + lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS, event -> { + final Commands commands = event.registrar(); + // ensure plugin commands override + commands.register(Commands.literal("tag") + .executes(ctx -> { + ctx.getSource().getSender().sendPlainMessage("overriden command"); + return Command.SINGLE_SUCCESS; + }) + .build(), + null, + Collections.emptyList() + ); + }); + + lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> { + final Commands commands = event.registrar(); + commands.register(plugin.getPluginMeta(), Commands.literal("root_command") + .then(Commands.literal("sub_command") + .requires(source -> source.getSender().hasPermission("testplugin.test")) + .executes(ctx -> { + ctx.getSource().getSender().sendPlainMessage("root_command sub_command"); + return Command.SINGLE_SUCCESS; + })).build(), + null, + Collections.emptyList() + ); + + commands.register(plugin.getPluginMeta(), "example", "test", Collections.emptyList(), new BasicCommand() { + @Override + public void execute(@NotNull final CommandSourceStack commandSourceStack, final @NotNull String[] args) { + System.out.println(Arrays.toString(args)); + } + + @Override + public @NotNull Collection<String> suggest(final @NotNull CommandSourceStack commandSourceStack, final @NotNull String[] args) { + System.out.println(Arrays.toString(args)); + return List.of("apple", "banana"); + } + }); + + + commands.register(plugin.getPluginMeta(), Commands.literal("water") + .requires(source -> { + System.out.println("isInWater check"); + return source.getExecutor().isInWater(); + }) + .executes(ctx -> { + ctx.getSource().getExecutor().sendMessage("You are in water!"); + return Command.SINGLE_SUCCESS; + }).then(Commands.literal("lava") + .requires(source -> { + System.out.println("isInLava check"); + if (source.getExecutor() != null) { + return source.getExecutor().isInLava(); + } + return true; + }) + .executes(ctx -> { + ctx.getSource().getExecutor().sendMessage("You are in lava!"); + return Command.SINGLE_SUCCESS; + })).build(), + null, + Collections.emptyList()); + + + ExampleAdminCommand.register(plugin, commands); + }).priority(10)); + } + + private static void registerLegacyCommands(final JavaPlugin plugin) { + plugin.getServer().getCommandMap().register("fallback", new BukkitCommand("hi", "cool hi command", "<>", List.of("hialias")) { + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + sender.sendMessage("hi"); + return true; + } + }); + plugin.getServer().getCommandMap().register("fallback", new BukkitCommand("cooler-command", "cool hi command", "<>", List.of("cooler-command-alias")) { + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + sender.sendMessage("hi"); + return true; + } + }); + plugin.getServer().getCommandMap().getKnownCommands().values().removeIf((command) -> { + return command.getName().equals("hi"); + }); + } + + public static void registerViaBootstrap(final BootstrapContext context) { + final LifecycleEventManager<BootstrapContext> lifecycleManager = context.getLifecycleManager(); + lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS, event -> { + final Commands commands = event.registrar(); + commands.register(Commands.literal("material") + .then(Commands.literal("item") + .then(Commands.argument("mat", MaterialArgumentType.item()) + .executes(ctx -> { + ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name()); + return Command.SINGLE_SUCCESS; + }) + ) + ).then(Commands.literal("block") + .then(Commands.argument("mat", MaterialArgumentType.block()) + .executes(ctx -> { + ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name()); + return Command.SINGLE_SUCCESS; + }) + ) + ) + .build(), + null, + Collections.emptyList() + ); + }); + + lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> { + final Commands commands = event.registrar(); + commands.register(Commands.literal("heya") + .then(Commands.argument("range", ArgumentTypes.doubleRange()) + .executes((ct) -> { + ct.getSource().getSender().sendPlainMessage(ct.getArgument("range", DoubleRangeProvider.class).range().toString()); + return 1; + }) + ).build(), + null, + Collections.emptyList() + ); + }).priority(10)); + } +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/ComponentCommandExceptionType.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/ComponentCommandExceptionType.java new file mode 100644 index 0000000000..7b8d9db790 --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/ComponentCommandExceptionType.java @@ -0,0 +1,25 @@ +package io.papermc.testplugin.brigtests.example; + +import com.mojang.brigadier.ImmutableStringReader; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandExceptionType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.Component; + +public class ComponentCommandExceptionType implements CommandExceptionType { + + private final Message message; + + public ComponentCommandExceptionType(final Component message) { + this.message = MessageComponentSerializer.message().serialize(message); + } + + public CommandSyntaxException create() { + return new CommandSyntaxException(this, this.message); + } + + public CommandSyntaxException createWithContext(final ImmutableStringReader reader) { + return new CommandSyntaxException(this, this.message, reader.getString(), reader.getCursor()); + } +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/ExampleAdminCommand.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/ExampleAdminCommand.java new file mode 100644 index 0000000000..83f1ebb93e --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/ExampleAdminCommand.java @@ -0,0 +1,154 @@ +package io.papermc.testplugin.brigtests.example; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.SignedMessageResolver; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver; +import io.papermc.paper.math.BlockPosition; +import io.papermc.testplugin.TestPlugin; +import net.kyori.adventure.chat.ChatType; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ExampleAdminCommand { + + public static void register(JavaPlugin plugin, Commands commands) { + final LiteralArgumentBuilder<CommandSourceStack> adminBuilder = Commands.literal("admin") + .executes((ct) -> { + ct.getSource().getSender().sendPlainMessage("root admin"); + return 1; + }) + .then( + Commands.literal("tp") + .then( + Commands.argument("player", ArgumentTypes.player()).executes((source) -> { + CommandSourceStack sourceStack = source.getSource(); + Player resolved = source.getArgument("player", PlayerSelectorArgumentResolver.class).resolve(sourceStack).get(0); + + if (resolved == source.getSource().getExecutor()) { + source.getSource().getExecutor().sendMessage(Component.text("Can't teleport to self!")); + return 0; + } + Entity entity = source.getSource().getExecutor(); + if (entity != null) { + entity.teleport(resolved); + } + + return 1; + }) + ) + ) + .then( + Commands.literal("tp-self") + .executes((cmd) -> { + if (cmd.getSource().getSender() instanceof Player player) { + player.teleport(cmd.getSource().getLocation()); + } + + return com.mojang.brigadier.Command.SINGLE_SUCCESS; + }) + ) + .then( + Commands.literal("broadcast") + .then( + Commands.argument("message", ArgumentTypes.component()).executes((source) -> { + Component message = source.getArgument("message", Component.class); + Bukkit.broadcast(message); + return 1; + }) + ) + ) + .then( + Commands.literal("ice_cream").then( + Commands.argument("type", new IceCreamTypeArgument()).executes((context) -> { + IceCreamType argumentResponse = context.getArgument("type", IceCreamType.class); // Gets the raw argument + context.getSource().getSender().sendMessage(Component.text("You like: " + argumentResponse)); + return 1; + }) + ) + ) + .then( + Commands.literal("execute") + .redirect(commands.getDispatcher().getRoot().getChild("execute")) + ) + .then( + Commands.literal("signed_message").then( + Commands.argument("msg", ArgumentTypes.signedMessage()).executes((context) -> { + SignedMessageResolver argumentResponse = context.getArgument("msg", SignedMessageResolver.class); // Gets the raw argument + + // This is a better way of getting signed messages, includes the concept of "disguised" messages. + argumentResponse.resolveSignedMessage("msg", context) + .thenAccept((signedMsg) -> { + context.getSource().getSender().sendMessage(signedMsg, ChatType.SAY_COMMAND.bind(Component.text("STATIC"))); + }); + + return 1; + }) + ) + ) + .then( + Commands.literal("setblock").then( + Commands.argument("block", ArgumentTypes.blockState()) + .then(Commands.argument("pos", ArgumentTypes.blockPosition()) + .executes((context) -> { + CommandSourceStack sourceStack = context.getSource(); + BlockPosition position = context.getArgument("pos", BlockPositionResolver.class).resolve(sourceStack); + BlockState state = context.getArgument("block", BlockState.class); + + // TODO: better block state api here? :thinking: + Block block = context.getSource().getLocation().getWorld().getBlockAt(position.blockX(), position.blockY(), position.blockZ()); + block.setType(state.getType()); + block.setBlockData(state.getBlockData()); + + return 1; + }) + ) + ) + ); + commands.register(plugin.getPluginMeta(), adminBuilder.build(), "Cool command showcasing what you can do!", List.of("alias_for_admin_that_you_shouldnt_use", "a")); + + + Bukkit.getCommandMap().register( + "legacy", + new Command("legacy_command") { + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { + return List.of(String.join(" ", args)); + } + } + ); + + Bukkit.getCommandMap().register( + "legacy", + new Command("legacy_fail") { + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + return false; + } + + @Override + public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { + return List.of(String.join(" ", args)); + } + } + ); + } +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/IceCreamType.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/IceCreamType.java new file mode 100644 index 0000000000..cf63058fb9 --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/IceCreamType.java @@ -0,0 +1,9 @@ +package io.papermc.testplugin.brigtests.example; + +public enum IceCreamType { + VANILLA, + CHOCOLATE, + BLUE_MOON, + STRAWBERRY, + WHOLE_MILK +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/IceCreamTypeArgument.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/IceCreamTypeArgument.java new file mode 100644 index 0000000000..68df9e65a3 --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/IceCreamTypeArgument.java @@ -0,0 +1,47 @@ +package io.papermc.testplugin.brigtests.example; + +import com.mojang.brigadier.Message; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class IceCreamTypeArgument implements CustomArgumentType.Converted<IceCreamType, String> { + + @Override + public @NotNull IceCreamType convert(String nativeType) throws CommandSyntaxException { + try { + return IceCreamType.valueOf(nativeType.toUpperCase()); + } catch (Exception e) { + Message message = MessageComponentSerializer.message().serialize(Component.text("Invalid species %s!".formatted(nativeType), NamedTextColor.RED)); + + throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message); + } + } + + @Override + public @NotNull ArgumentType<String> getNativeType() { + return StringArgumentType.word(); + } + + @Override + public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) { + for (IceCreamType species : IceCreamType.values()) { + builder.suggest(species.name(), MessageComponentSerializer.message().serialize(Component.text("COOL! TOOLTIP!", NamedTextColor.GREEN))); + } + + return CompletableFuture.completedFuture( + builder.build() + ); + } +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/MaterialArgumentType.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/MaterialArgumentType.java new file mode 100644 index 0000000000..381be0e65b --- /dev/null +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/example/MaterialArgumentType.java @@ -0,0 +1,88 @@ +package io.papermc.testplugin.brigtests.example; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.Component.translatable; + +public class MaterialArgumentType implements CustomArgumentType.Converted<Material, NamespacedKey> { + + private static final ComponentCommandExceptionType ERROR_INVALID = new ComponentCommandExceptionType(translatable("argument.id.invalid")); + + private final Predicate<Material> check; + + private MaterialArgumentType(Predicate<Material> check) { + this.check = check; + } + + public static MaterialArgumentType item() { + return new MaterialArgumentType(Material::isItem); + } + + public static MaterialArgumentType block() { + return new MaterialArgumentType(Material::isBlock); + } + + @Override + public @NotNull Material convert(final @NotNull NamespacedKey nativeType) throws CommandSyntaxException { + final Material material = Registry.MATERIAL.get(nativeType); + if (material == null) { + throw ERROR_INVALID.create(); + } + if (!this.check.test(material)) { + throw ERROR_INVALID.create(); + } + return material; + } + + static boolean matchesSubStr(String remaining, String candidate) { + for(int i = 0; !candidate.startsWith(remaining, i); ++i) { + i = candidate.indexOf('_', i); + if (i < 0) { + return false; + } + } + + return true; + } + + @Override + public @NotNull ArgumentType<NamespacedKey> getNativeType() { + return ArgumentTypes.namespacedKey(); + } + + @Override + public @NotNull <S> CompletableFuture<Suggestions> listSuggestions(final @NotNull CommandContext<S> context, final @NotNull SuggestionsBuilder builder) { + final Stream<Material> stream = StreamSupport.stream(Registry.MATERIAL.spliterator(), false); + final String remaining = builder.getRemaining(); + boolean containsColon = remaining.indexOf(':') > -1; + stream.filter(this.check) + .map(Keyed::key) + .forEach(key -> { + final String keyAsString = key.asString(); + if (containsColon) { + if (matchesSubStr(remaining, keyAsString)) { + builder.suggest(keyAsString); + } + } else if (matchesSubStr(remaining, key.namespace()) || "minecraft".equals(key.namespace()) && matchesSubStr(remaining, key.value())) { + builder.suggest(keyAsString); + } + }); + return builder.buildFuture(); + } + +} |