Skip to content

Commit

Permalink
Add proper brigadier support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vaperion committed Feb 24, 2024
1 parent 7b45b01 commit aef966a
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 6 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions bukkit-brigadier/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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<CommandSender, WrappedSender<?>> wrappedSenderFunction;

public BladeBrigadierSupport(@NotNull Blade blade,
@NotNull Function<CommandSender, WrappedSender<?>> 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<BukkitBrigadierCommandSource> event) {
SimpleBladeNode node = nodeDiscovery.discoverCommand(event.getCommandLabel());
if (node == null) return;

event.setLiteral(buildLiteral(
node,
event.getCommandLabel(),
event.getBrigadierCommand(),
event.getBrigadierCommand()
));
}

@NotNull
private LiteralCommandNode<BukkitBrigadierCommandSource> buildLiteral(
@NotNull SimpleBladeNode node,
@NotNull String label,
@NotNull SuggestionProvider<BukkitBrigadierCommandSource> suggestionProvider,
@NotNull Command<BukkitBrigadierCommandSource> brigadierCommand) {
LiteralArgumentBuilder<BukkitBrigadierCommandSource> builder = LiteralArgumentBuilder.<BukkitBrigadierCommandSource>literal(label)
.requires(createPermissionPredicate(node))
.executes(brigadierCommand);

LiteralCommandNode<BukkitBrigadierCommandSource> 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<BukkitBrigadierCommandSource> suggestionProvider,
@NotNull Command<BukkitBrigadierCommandSource> brigadierCommand,
@NotNull LiteralCommandNode<BukkitBrigadierCommandSource> root) {
if (subCommand.isStub()) return;

if (label.contains(" ")) {
String[] parts = label.split(" ");

String stubName = parts[0];
String rest = label.substring(stubName.length() + 1);

CommandNode<BukkitBrigadierCommandSource> subCommandNode = root.getChild(stubName);

if (subCommandNode == null) {
subCommandNode = LiteralArgumentBuilder.<BukkitBrigadierCommandSource>literal(stubName)
.requires(createPermissionPredicate(subCommand))
.executes(brigadierCommand)
.build();
}

root.addChild(subCommandNode);
registerSubCommand(subCommand, rest, suggestionProvider, brigadierCommand, (LiteralCommandNode<BukkitBrigadierCommandSource>) subCommandNode);
} else {
CommandNode<BukkitBrigadierCommandSource> subCommandNode = root.getChild(label);

if (subCommandNode == null) {
subCommandNode = LiteralArgumentBuilder.<BukkitBrigadierCommandSource>literal(label)
.requires(createPermissionPredicate(subCommand))
.executes(brigadierCommand)
.build();
}

root.addChild(subCommandNode);
registerParams(subCommand, subCommandNode, suggestionProvider, brigadierCommand);
}
}

private void registerParams(@NotNull SimpleBladeNode node,
@NotNull CommandNode<BukkitBrigadierCommandSource> commandNode,
@NotNull SuggestionProvider<BukkitBrigadierCommandSource> suggestionProvider,
@NotNull Command<BukkitBrigadierCommandSource> brigadierCommand) {
if (node.isStub()) return;

for (Parameter.CommandParameter parameter : node.getCommand().getCommandParameters()) {
RequiredArgumentBuilder<BukkitBrigadierCommandSource, Object> builder = RequiredArgumentBuilder
.<BukkitBrigadierCommandSource, Object>argument(parameter.getName(), mapBrigadierType(parameter.getType()))
.suggests(suggestionProvider)
.requires(createPermissionPredicate(node))
.executes(brigadierCommand);

CommandNode<BukkitBrigadierCommandSource> 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<BukkitBrigadierCommandSource> 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<Object> 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 <T> ArgumentType<Object> objectifyArgument(@NotNull ArgumentType<T> type) {
return (ArgumentType<Object>) type;
}
}
Original file line number Diff line number Diff line change
@@ -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<Command, String> 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<Command> commands = blade.getAliasToCommands().get(label);
if (commands == null || commands.isEmpty()) return null;

List<SimpleBladeNode> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<SimpleBladeNode> subCommands;

public SimpleBladeNode(boolean isStub, @Nullable Command command, @NotNull List<SimpleBladeNode> subCommands) {
this.isStub = isStub;
this.command = command;
this.subCommands = subCommands;
}
}
3 changes: 2 additions & 1 deletion bukkit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/me/vaperion/blade/Blade.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private Blade(Builder builder) {
}

configuration.getTabCompleter().init(this);
platform.ingestBlade(this);
}

@NotNull
Expand Down
Loading

0 comments on commit aef966a

Please sign in to comment.