From aef966a8afd18f2726a396d99cd321f52a7464c6 Mon Sep 17 00:00:00 2001 From: vaperion Date: Sat, 24 Feb 2024 19:51:47 +0100 Subject: [PATCH] Add proper brigadier support This commit adds proper brigadier support to Blade. In previous versions commands registered via Blade would sometimes not show up in-game, and some commands would not execute at all. This is a non-breaking change, and it should automatically start working once the dependency is updated. --- build.gradle | 2 +- bukkit-brigadier/build.gradle | 14 ++ .../brigadier/BladeBrigadierSupport.java | 183 ++++++++++++++++++ .../bukkit/brigadier/BladeNodeDiscovery.java | 56 ++++++ .../bukkit/brigadier/SimpleBladeNode.java | 24 +++ bukkit/build.gradle | 3 +- .../blade/bukkit/BladeBukkitPlatform.java | 46 +++++ .../bukkit/container/BukkitContainer.java | 4 +- .../main/java/me/vaperion/blade/Blade.java | 1 + .../me/vaperion/blade/command/Command.java | 2 + .../blade/platform/BladePlatform.java | 11 +- .../blade/service/CommandRegistrar.java | 4 + settings.gradle | 3 +- 13 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 bukkit-brigadier/build.gradle create mode 100644 bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeBrigadierSupport.java create mode 100644 bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeNodeDiscovery.java create mode 100644 bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/SimpleBladeNode.java diff --git a/build.gradle b/build.gradle index ef1a0a1..ef20b13 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ allprojects { } group = 'me.vaperion.blade' - version = '3.0.10' + version = '3.0.11' // workaround for gradle issue: https://github.com/gradle/gradle/issues/17236#issuecomment-894385386 tasks.withType(Copy).configureEach { diff --git a/bukkit-brigadier/build.gradle b/bukkit-brigadier/build.gradle new file mode 100644 index 0000000..35b95aa --- /dev/null +++ b/bukkit-brigadier/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java-library' +} + +repositories { + maven { url 'https://repo.papermc.io/repository/maven-public/' } +} + +dependencies { + implementation project(":core") + + compileOnly 'io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-mojangapi:1.20.4-R0.1-SNAPSHOT' +} diff --git a/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeBrigadierSupport.java b/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeBrigadierSupport.java new file mode 100644 index 0000000..5242902 --- /dev/null +++ b/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeBrigadierSupport.java @@ -0,0 +1,183 @@ +package me.vaperion.blade.bukkit.brigadier; + +import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; +import com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.*; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.vaperion.blade.Blade; +import me.vaperion.blade.command.Parameter; +import me.vaperion.blade.context.Context; +import me.vaperion.blade.context.WrappedSender; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; +import java.util.function.Predicate; + +@SuppressWarnings("UnstableApiUsage") +public final class BladeBrigadierSupport implements Listener { + + private final Blade blade; + private final BladeNodeDiscovery nodeDiscovery; + private final Function> wrappedSenderFunction; + + public BladeBrigadierSupport(@NotNull Blade blade, + @NotNull Function> wrappedSenderFunction) throws ClassNotFoundException { + Class.forName("com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent"); + + this.blade = blade; + this.nodeDiscovery = new BladeNodeDiscovery(blade); + this.wrappedSenderFunction = wrappedSenderFunction; + + Bukkit.getPluginManager().registerEvents(this, (Plugin) blade.getPlatform().getPluginInstance()); + } + + @EventHandler + public void onCommandRegistered(CommandRegisteredEvent event) { + SimpleBladeNode node = nodeDiscovery.discoverCommand(event.getCommandLabel()); + if (node == null) return; + + event.setLiteral(buildLiteral( + node, + event.getCommandLabel(), + event.getBrigadierCommand(), + event.getBrigadierCommand() + )); + } + + @NotNull + private LiteralCommandNode buildLiteral( + @NotNull SimpleBladeNode node, + @NotNull String label, + @NotNull SuggestionProvider suggestionProvider, + @NotNull Command brigadierCommand) { + LiteralArgumentBuilder builder = LiteralArgumentBuilder.literal(label) + .requires(createPermissionPredicate(node)) + .executes(brigadierCommand); + + LiteralCommandNode root = builder.build(); + + registerParams(node, root, suggestionProvider, brigadierCommand); + + for (SimpleBladeNode subCommand : node.getSubCommands()) { + if (subCommand.isStub()) continue; + + String subLabel = subCommand.getCommand().getUsageAlias(); + String[] parts = subLabel.split(" "); + String rest = subLabel.substring(parts[0].length() + 1); + + registerSubCommand(subCommand, rest, suggestionProvider, brigadierCommand, root); + } + + return root; + } + + private void registerSubCommand(@NotNull SimpleBladeNode subCommand, + @NotNull String label, + @NotNull SuggestionProvider suggestionProvider, + @NotNull Command brigadierCommand, + @NotNull LiteralCommandNode root) { + if (subCommand.isStub()) return; + + if (label.contains(" ")) { + String[] parts = label.split(" "); + + String stubName = parts[0]; + String rest = label.substring(stubName.length() + 1); + + CommandNode subCommandNode = root.getChild(stubName); + + if (subCommandNode == null) { + subCommandNode = LiteralArgumentBuilder.literal(stubName) + .requires(createPermissionPredicate(subCommand)) + .executes(brigadierCommand) + .build(); + } + + root.addChild(subCommandNode); + registerSubCommand(subCommand, rest, suggestionProvider, brigadierCommand, (LiteralCommandNode) subCommandNode); + } else { + CommandNode subCommandNode = root.getChild(label); + + if (subCommandNode == null) { + subCommandNode = LiteralArgumentBuilder.literal(label) + .requires(createPermissionPredicate(subCommand)) + .executes(brigadierCommand) + .build(); + } + + root.addChild(subCommandNode); + registerParams(subCommand, subCommandNode, suggestionProvider, brigadierCommand); + } + } + + private void registerParams(@NotNull SimpleBladeNode node, + @NotNull CommandNode commandNode, + @NotNull SuggestionProvider suggestionProvider, + @NotNull Command brigadierCommand) { + if (node.isStub()) return; + + for (Parameter.CommandParameter parameter : node.getCommand().getCommandParameters()) { + RequiredArgumentBuilder builder = RequiredArgumentBuilder + .argument(parameter.getName(), mapBrigadierType(parameter.getType())) + .suggests(suggestionProvider) + .requires(createPermissionPredicate(node)) + .executes(brigadierCommand); + + CommandNode argument = builder.build(); + commandNode.addChild(argument); + commandNode = argument; + } + } + + // This is a bit weird. Brigadier on newer versions literally HIDES commands if you don't have permissions / enter invalid args + // so instead of seeing the usage, you see unknown command. To fix this, we just tell brigadier that everyone has permission + // to execute the command, which then properly delegates to Blade's handler. + @NotNull + private Predicate createPermissionPredicate(@NotNull SimpleBladeNode node) { + return sender -> { + WrappedSender wrappedSender = wrappedSenderFunction.apply(sender.getBukkitSender()); + Context context = new Context(blade, wrappedSender, "", new String[0]); + + if (node.getCommand() != null && node.getCommand().isHidden()) { + boolean result = blade.getPermissionTester().testPermission(context, node.getCommand()); + if (!result) return false; + } + + for (SimpleBladeNode subCommand : node.getSubCommands()) { + if (!subCommand.getCommand().isHidden()) continue; + + boolean result = blade.getPermissionTester().testPermission(context, subCommand.getCommand()); + if (!result) return false; + } + + return true; + }; + } + + @NotNull + private ArgumentType mapBrigadierType(@NotNull Class clazz) { + if (clazz == String.class) return objectifyArgument(StringArgumentType.string()); + if (clazz == int.class || clazz == Integer.class) return objectifyArgument(IntegerArgumentType.integer()); + if (clazz == float.class || clazz == Float.class) return objectifyArgument(FloatArgumentType.floatArg()); + if (clazz == double.class || clazz == Double.class) return objectifyArgument(DoubleArgumentType.doubleArg()); + if (clazz == boolean.class || clazz == Boolean.class) return objectifyArgument(BoolArgumentType.bool()); + if (clazz == long.class || clazz == Long.class) return objectifyArgument(LongArgumentType.longArg()); + return objectifyArgument(StringArgumentType.string()); // Everything else becomes a string + } + + @SuppressWarnings("unchecked") + @NotNull + private ArgumentType objectifyArgument(@NotNull ArgumentType type) { + return (ArgumentType) type; + } +} diff --git a/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeNodeDiscovery.java b/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeNodeDiscovery.java new file mode 100644 index 0000000..8ee71b1 --- /dev/null +++ b/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/BladeNodeDiscovery.java @@ -0,0 +1,56 @@ +package me.vaperion.blade.bukkit.brigadier; + +import me.vaperion.blade.Blade; +import me.vaperion.blade.command.Command; +import me.vaperion.blade.util.Tuple; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class BladeNodeDiscovery { + + private final Blade blade; + + public BladeNodeDiscovery(@NotNull Blade blade) { + this.blade = blade; + } + + @NotNull + private String removeCommandQualifier(@NotNull String input) { + String[] parts = input.split(" "); + + if (parts[0].contains(":")) + parts[0] = parts[0].split(":")[1]; + + return String.join(" ", parts); + } + + @Nullable + public SimpleBladeNode discoverCommand(@NotNull String label) { + label = removeCommandQualifier(label); + Tuple bladeCommand = blade.getResolver().resolveCommand(new String[]{label}); + + if (bladeCommand != null) { + // This is the simple case: if a command is registered with that exact label (e.g. "/hello"), we can just return it + return new SimpleBladeNode(false, bladeCommand.getLeft(), List.of()); + } + + // If no command was found, we have to search for "stub" parent commands, e.g. if you register "/hello world", "hello" would be a stub + List commands = blade.getAliasToCommands().get(label); + if (commands == null || commands.isEmpty()) return null; + + List resolved = new ArrayList<>(); + for (Command command : commands) { + SimpleBladeNode subcommand = discoverCommand(command.getAliases()[0]); + + if (subcommand == null) + resolved.add(new SimpleBladeNode(false, command, List.of())); + else + resolved.add(subcommand); + } + + return new SimpleBladeNode(true, null, resolved); + } +} diff --git a/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/SimpleBladeNode.java b/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/SimpleBladeNode.java new file mode 100644 index 0000000..9a7ee55 --- /dev/null +++ b/bukkit-brigadier/src/main/java/me/vaperion/blade/bukkit/brigadier/SimpleBladeNode.java @@ -0,0 +1,24 @@ +package me.vaperion.blade.bukkit.brigadier; + +import lombok.Getter; +import lombok.ToString; +import me.vaperion.blade.command.Command; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +@Getter +@ToString +public class SimpleBladeNode { + + private final boolean isStub; + private final Command command; + private final List subCommands; + + public SimpleBladeNode(boolean isStub, @Nullable Command command, @NotNull List subCommands) { + this.isStub = isStub; + this.command = command; + this.subCommands = subCommands; + } +} diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 3ba50c5..da47c93 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -9,9 +9,10 @@ repositories { dependencies { implementation project(":core") + implementation project(":bukkit-brigadier") compileOnly 'org.jetbrains:annotations:23.0.0' - compileOnly 'org.spigotmc:spigot-api:1.19.3-R0.1-SNAPSHOT' + compileOnly 'org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT' compileOnly 'com.comphenix.protocol:ProtocolLib:4.6.0' } diff --git a/bukkit/src/main/java/me/vaperion/blade/bukkit/BladeBukkitPlatform.java b/bukkit/src/main/java/me/vaperion/blade/bukkit/BladeBukkitPlatform.java index 81cfe7b..7259c55 100644 --- a/bukkit/src/main/java/me/vaperion/blade/bukkit/BladeBukkitPlatform.java +++ b/bukkit/src/main/java/me/vaperion/blade/bukkit/BladeBukkitPlatform.java @@ -5,7 +5,9 @@ import me.vaperion.blade.Blade.Builder.Binder; import me.vaperion.blade.bukkit.argument.OfflinePlayerArgument; import me.vaperion.blade.bukkit.argument.PlayerArgument; +import me.vaperion.blade.bukkit.brigadier.BladeBrigadierSupport; import me.vaperion.blade.bukkit.container.BukkitContainer; +import me.vaperion.blade.bukkit.context.BukkitSender; import me.vaperion.blade.bukkit.platform.BukkitHelpGenerator; import me.vaperion.blade.bukkit.platform.ProtocolLibTabCompleter; import me.vaperion.blade.container.ContainerCreator; @@ -18,11 +20,31 @@ import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Method; import java.util.Locale; @RequiredArgsConstructor public final class BladeBukkitPlatform implements BladePlatform { + private static final Method SYNC_COMMANDS; + + static { + Method syncCommands = null; + + try { + Class craftServerClass = Bukkit.getServer().getClass(); + syncCommands = craftServerClass.getDeclaredMethod("syncCommands"); + syncCommands.setAccessible(true); + } catch (NoSuchMethodException ignored) { + // Doesn't exist in 1.8 + } catch (Exception ex) { + System.err.println("Failed to grab CraftServer#syncCommands method."); + ex.printStackTrace(); + } + + SYNC_COMMANDS = syncCommands; + } + private final JavaPlugin plugin; @Override @@ -46,4 +68,28 @@ public void configureBlade(Blade.@NotNull Builder builder, @NotNull BladeConfigu binder.bind(Player.class, new PlayerArgument()); binder.bind(OfflinePlayer.class, new OfflinePlayerArgument()); } + + @Override + public void ingestBlade(@NotNull Blade blade) { + try { + new BladeBrigadierSupport(blade, BukkitSender::new); + } catch (ClassNotFoundException | NoClassDefFoundError ignored) { + // No paper / brigadier not supported + } catch (Throwable t) { + System.err.println("Blade failed to initialize Brigadier support."); + t.printStackTrace(); + } + } + + @Override + public void postCommandMapUpdate() { + if (SYNC_COMMANDS != null) { + try { + SYNC_COMMANDS.invoke(Bukkit.getServer()); + } catch (Throwable t) { + System.err.println("Blade failed to invoke CraftServer#syncCommands method, Brigadier may not recognize new commands."); + t.printStackTrace(); + } + } + } } diff --git a/bukkit/src/main/java/me/vaperion/blade/bukkit/container/BukkitContainer.java b/bukkit/src/main/java/me/vaperion/blade/bukkit/container/BukkitContainer.java index 342c5f7..85bbdd2 100644 --- a/bukkit/src/main/java/me/vaperion/blade/bukkit/container/BukkitContainer.java +++ b/bukkit/src/main/java/me/vaperion/blade/bukkit/container/BukkitContainer.java @@ -90,7 +90,9 @@ private BukkitContainer(@NotNull Blade blade, @NotNull me.vaperion.blade.command } } - simpleCommandMap.register(blade.getConfiguration().getFallbackPrefix(), this); + if (!simpleCommandMap.register(blade.getConfiguration().getFallbackPrefix(), this)) { + System.err.println("Blade failed to register the command \"" + alias + "\". This could lead to issues."); + } } private boolean doesBukkitCommandConflict(@NotNull Command bukkitCommand, @NotNull String alias, @NotNull me.vaperion.blade.command.Command command) { diff --git a/core/src/main/java/me/vaperion/blade/Blade.java b/core/src/main/java/me/vaperion/blade/Blade.java index d795a22..b753971 100644 --- a/core/src/main/java/me/vaperion/blade/Blade.java +++ b/core/src/main/java/me/vaperion/blade/Blade.java @@ -77,6 +77,7 @@ private Blade(Builder builder) { } configuration.getTabCompleter().init(this); + platform.ingestBlade(this); } @NotNull diff --git a/core/src/main/java/me/vaperion/blade/command/Command.java b/core/src/main/java/me/vaperion/blade/command/Command.java index 498e32b..bd4eaa7 100644 --- a/core/src/main/java/me/vaperion/blade/command/Command.java +++ b/core/src/main/java/me/vaperion/blade/command/Command.java @@ -1,6 +1,7 @@ package me.vaperion.blade.command; import lombok.Getter; +import lombok.ToString; import me.vaperion.blade.Blade; import me.vaperion.blade.annotation.argument.*; import me.vaperion.blade.annotation.command.*; @@ -24,6 +25,7 @@ import static me.vaperion.blade.util.Preconditions.runOrDefault; @Getter +@ToString public final class Command { private final Blade blade; diff --git a/core/src/main/java/me/vaperion/blade/platform/BladePlatform.java b/core/src/main/java/me/vaperion/blade/platform/BladePlatform.java index a537416..5b97aa5 100644 --- a/core/src/main/java/me/vaperion/blade/platform/BladePlatform.java +++ b/core/src/main/java/me/vaperion/blade/platform/BladePlatform.java @@ -1,13 +1,20 @@ package me.vaperion.blade.platform; +import me.vaperion.blade.Blade; import me.vaperion.blade.Blade.Builder; import me.vaperion.blade.container.ContainerCreator; import org.jetbrains.annotations.NotNull; public interface BladePlatform { - @NotNull Object getPluginInstance(); + @NotNull + Object getPluginInstance(); - @NotNull ContainerCreator getContainerCreator(); + @NotNull + ContainerCreator getContainerCreator(); void configureBlade(@NotNull Builder builder, @NotNull BladeConfiguration configuration); + + default void ingestBlade(@NotNull Blade blade) {} + + default void postCommandMapUpdate() {} } diff --git a/core/src/main/java/me/vaperion/blade/service/CommandRegistrar.java b/core/src/main/java/me/vaperion/blade/service/CommandRegistrar.java index f323bd8..b112025 100644 --- a/core/src/main/java/me/vaperion/blade/service/CommandRegistrar.java +++ b/core/src/main/java/me/vaperion/blade/service/CommandRegistrar.java @@ -28,6 +28,8 @@ public void registerClass(@Nullable Object instance, @NotNull Class clazz) { registerMethod(instance, method); } + + blade.getPlatform().postCommandMapUpdate(); } catch (Exception ex) { System.err.println("An exception was thrown while registering commands in class " + clazz.getCanonicalName() + " (instance: " + instance + ")"); ex.printStackTrace(); @@ -42,6 +44,8 @@ public void unregisterClass(@Nullable Object instance, @NotNull Class clazz) unregisterMethod(instance, method); } + + blade.getPlatform().postCommandMapUpdate(); } catch (Exception ex) { System.err.println("An exception was thrown while registering commands in class " + clazz.getCanonicalName() + " (instance: " + instance + ")"); ex.printStackTrace(); diff --git a/settings.gradle b/settings.gradle index bce78eb..1c2d545 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,5 +3,6 @@ rootProject.name = 'blade' include( 'core', 'bukkit', - 'velocity' + 'velocity', + 'bukkit-brigadier' ) \ No newline at end of file