diff --git a/build.gradle.kts b/build.gradle.kts index 0e9089b..4b00934 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,10 +2,11 @@ plugins { id("java") id("java-library") id("maven-publish") + kotlin("jvm") version "1.7.0" } group = "ru.d3st0ny" -version = "0.1" +version = "0.3" repositories { mavenCentral() @@ -17,17 +18,16 @@ dependencies { api("com.mojang:brigadier:1.0.18") { exclude("com.google.guava", "guava") } - compileOnly("io.papermc.paper:paper-api:1.19-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:1.19.2-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-mojangapi:1.19.2-R0.1-SNAPSHOT") } publishing { - publications { - create("maven"){ - artifactId = project.name.toLowerCase() - groupId = "${project.group}" - version = "${project.version}" - from(components["java"]) - } + publications.create("maven") { + artifactId = project.name.toLowerCase() + groupId = "${project.group}" + version = "${project.version}" + from(components["java"]) } repositories { maven { diff --git a/src/main/java/ru/d3st0ny/adajency/AbstractAdajency.java b/src/main/java/ru/d3st0ny/adajency/AbstractAdajency.java new file mode 100644 index 0000000..6bb6ede --- /dev/null +++ b/src/main/java/ru/d3st0ny/adajency/AbstractAdajency.java @@ -0,0 +1,139 @@ +package ru.d3st0ny.adajency; + +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; + +import org.bukkit.command.Command; +import org.bukkit.command.PluginCommand; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +abstract class AbstractAdajency implements Adajency { + + // ArgumentCommandNode#customSuggestions field + protected static final Field CUSTOM_SUGGESTIONS_FIELD; + + // CommandNode#command + protected static final Field COMMAND_EXECUTE_FUNCTION_FIELD; + + // CommandNode#children, CommandNode#literals, CommandNode#arguments fields + protected static final Field CHILDREN_FIELD; + protected static final Field LITERALS_FIELD; + protected static final Field ARGUMENTS_FIELD; + + // An array of the CommandNode fields above: [#children, #literals, #arguments] + protected static final Field[] CHILDREN_FIELDS; + + // Dummy instance of Command used to ensure the executable bit gets set on + // mock commands when they're encoded into data sent to the client + protected static final com.mojang.brigadier.Command DUMMY_COMMAND; + protected static final SuggestionProvider DUMMY_SUGGESTION_PROVIDER; + + static { + try { + CUSTOM_SUGGESTIONS_FIELD = ArgumentCommandNode.class.getDeclaredField("customSuggestions"); + CUSTOM_SUGGESTIONS_FIELD.setAccessible(true); + + COMMAND_EXECUTE_FUNCTION_FIELD = CommandNode.class.getDeclaredField("command"); + COMMAND_EXECUTE_FUNCTION_FIELD.setAccessible(true); + + CHILDREN_FIELD = CommandNode.class.getDeclaredField("children"); + LITERALS_FIELD = CommandNode.class.getDeclaredField("literals"); + ARGUMENTS_FIELD = CommandNode.class.getDeclaredField("arguments"); + CHILDREN_FIELDS = new Field[]{CHILDREN_FIELD, LITERALS_FIELD, ARGUMENTS_FIELD}; + for (Field field : CHILDREN_FIELDS) { + field.setAccessible(true); + } + + // should never be called + // if ReflectionCommodore: bukkit handling should override + // if PaperCommodore: this is only sent to the client, not used for actual command handling + DUMMY_COMMAND = (ctx) -> { throw new UnsupportedOperationException(); }; + // should never be called - only used in clientbound root node, and the server impl will pass anything through + // SuggestionProviders#safelySwap (swap it for the ASK_SERVER provider) before sending + DUMMY_SUGGESTION_PROVIDER = (context, builder) -> { throw new UnsupportedOperationException(); }; + + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected static void removeChild(RootCommandNode root, String name) { + try { + for (Field field : CHILDREN_FIELDS) { + Map children = (Map) field.get(root); + children.remove(name); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + protected static void setRequiredHackyFieldsRecursively(CommandNode node, SuggestionProvider suggestionProvider) { + // set command execution function so the server sets the executable flag on the command + try { + COMMAND_EXECUTE_FUNCTION_FIELD.set(node, DUMMY_COMMAND); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + if (suggestionProvider != null && node instanceof ArgumentCommandNode argumentNode) { + // set the custom suggestion provider field so tab completions work + try { + CUSTOM_SUGGESTIONS_FIELD.set(argumentNode, suggestionProvider); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + for (CommandNode child : node.getChildren()) { + setRequiredHackyFieldsRecursively(child, suggestionProvider); + } + } + + protected static LiteralCommandNode renameLiteralNode(LiteralCommandNode node, String newLiteral) { + LiteralCommandNode clone = new LiteralCommandNode<>(newLiteral, node.getCommand(), node.getRequirement(), node.getRedirect(), node.getRedirectModifier(), node.isFork()); + for (CommandNode child : node.getChildren()) { + clone.addChild(child); + } + return clone; + } + + /** + * Gets the aliases known for the given command. + * + *

This will include the main label, as well as defined aliases, and + * aliases including the fallback prefix added by Bukkit.

+ * + * @param command the command + * @return the aliases + */ + protected static Collection getAliases(Command command) { + Objects.requireNonNull(command, "command"); + + Stream aliasesStream = Stream.concat( + Stream.of(command.getLabel()), + command.getAliases().stream() + ); + + if (command instanceof PluginCommand) { + String fallbackPrefix = ((PluginCommand) command).getPlugin().getName().toLowerCase().trim(); + aliasesStream = aliasesStream.flatMap(alias -> Stream.of( + alias, + fallbackPrefix + ":" + alias + )); + } + + return aliasesStream.distinct().collect(Collectors.toList()); + } + +} diff --git a/src/main/java/ru/d3st0ny/adajency/Adajency.java b/src/main/java/ru/d3st0ny/adajency/Adajency.java index 1b74f21..8f80d8d 100644 --- a/src/main/java/ru/d3st0ny/adajency/Adajency.java +++ b/src/main/java/ru/d3st0ny/adajency/Adajency.java @@ -66,7 +66,11 @@ public interface Adajency { * @param command the command to read aliases from * @param node the argument data */ - void register(Command command, LiteralCommandNode node); + default void register(Command command, LiteralCommandNode node) { + Objects.requireNonNull(command, "command"); + Objects.requireNonNull(node, "node"); + register(command, node, command::testPermissionSilent); + } /** * Registers the provided argument data to the dispatcher, against all diff --git a/src/main/java/ru/d3st0ny/adajency/AdajencyProvider.java b/src/main/java/ru/d3st0ny/adajency/AdajencyProvider.java index 5906367..1122016 100644 --- a/src/main/java/ru/d3st0ny/adajency/AdajencyProvider.java +++ b/src/main/java/ru/d3st0ny/adajency/AdajencyProvider.java @@ -3,6 +3,7 @@ package ru.d3st0ny.adajency; import org.bukkit.plugin.Plugin; import java.util.Objects; +import java.util.function.Function; /** * Factory for obtaining instances of {@link Adajency}. @@ -12,20 +13,39 @@ public final class AdajencyProvider { throw new AssertionError(); } - private static final Throwable SETUP_EXCEPTION = checkSupported(); + private static final Function PROVIDER = checkSupported(); - private static Throwable checkSupported() { + private static Function checkSupported() { try { Class.forName("com.mojang.brigadier.CommandDispatcher"); - AdajencyImpl.ensureSetup(); - MinecraftArgumentType.ensureSetup(); - return null; } catch (Throwable e) { - if (System.getProperty("adajency.debug") != null) { - System.err.println("Exception while initialising adajency:"); - e.printStackTrace(System.err); - } - return e; + printDebugInfo(e); + return null; + } + + // try the paper impl + try { + PaperAdajency.ensureSetup(); + return PaperAdajency::new; + } catch (Throwable e) { + printDebugInfo(e); + } + + // try reflection impl + try { + ReflectionAdajency.ensureSetup(); + return ReflectionAdajency::new; + } catch (Throwable e) { + printDebugInfo(e); + } + + return null; + } + + private static void printDebugInfo(Throwable e) { + if (System.getProperty("adajency.debug") != null) { + System.err.println("Exception while initialising adajency:"); + e.printStackTrace(System.err); } } @@ -35,7 +55,7 @@ public final class AdajencyProvider { * @return true if adajency is supported. */ public static boolean isSupported() { - return SETUP_EXCEPTION == null; + return PROVIDER != null; } /** @@ -48,9 +68,12 @@ public final class AdajencyProvider { */ public static Adajency getAdajency(Plugin plugin) throws BrigadierUnsupportedException { Objects.requireNonNull(plugin, "plugin"); - if (SETUP_EXCEPTION != null) { - throw new BrigadierUnsupportedException("Brigadier is not supported by the server.", SETUP_EXCEPTION); + if (PROVIDER == null) { + throw new BrigadierUnsupportedException( + "Brigadier is not supported by the server. " + + "Set -Dadajency.debug=true for debug info." + ); } - return new AdajencyImpl(plugin); + return PROVIDER.apply(plugin); } } diff --git a/src/main/java/ru/d3st0ny/adajency/BrigadierUnsupportedException.java b/src/main/java/ru/d3st0ny/adajency/BrigadierUnsupportedException.java index ad4f909..55daee0 100644 --- a/src/main/java/ru/d3st0ny/adajency/BrigadierUnsupportedException.java +++ b/src/main/java/ru/d3st0ny/adajency/BrigadierUnsupportedException.java @@ -5,7 +5,7 @@ package ru.d3st0ny.adajency; */ public final class BrigadierUnsupportedException extends UnsupportedOperationException { - public BrigadierUnsupportedException(String message, Throwable cause) { - super(message, cause); + BrigadierUnsupportedException(String message) { + super(message); } } diff --git a/src/main/java/ru/d3st0ny/adajency/PaperAdajency.java b/src/main/java/ru/d3st0ny/adajency/PaperAdajency.java new file mode 100644 index 0000000..52bda8f --- /dev/null +++ b/src/main/java/ru/d3st0ny/adajency/PaperAdajency.java @@ -0,0 +1,96 @@ +package ru.d3st0ny.adajency; + +import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; + +import org.bukkit.command.Command; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +final class PaperAdajency extends AbstractAdajency implements Adajency, Listener { + + static { + try { + Class.forName("com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent"); + } catch (ClassNotFoundException e) { + throw new UnsupportedOperationException("Not running on modern Paper!", e); + } + } + + private final List commands = new ArrayList<>(); + + PaperAdajency(Plugin plugin) { + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + @Override + public void register(LiteralCommandNode node) { + Objects.requireNonNull(node, "node"); + this.commands.add(new AdajencyCommand(node, null)); + } + + @Override + public void register(Command command, LiteralCommandNode node, Predicate permissionTest) { + Objects.requireNonNull(command, "command"); + Objects.requireNonNull(node, "node"); + Objects.requireNonNull(permissionTest, "permissionTest"); + + try { + setRequiredHackyFieldsRecursively(node, DUMMY_SUGGESTION_PROVIDER); + } catch (Throwable e) { + e.printStackTrace(); + } + + Collection aliases = getAliases(command); + if (!aliases.contains(node.getLiteral())) { + node = renameLiteralNode(node, command.getName()); + } + + for (String alias : aliases) { + if (node.getLiteral().equals(alias)) { + this.commands.add(new AdajencyCommand(node, permissionTest)); + } else { + LiteralCommandNode redirectNode = LiteralArgumentBuilder.literal(alias) + .redirect((LiteralCommandNode) node) + .build(); + this.commands.add(new AdajencyCommand(redirectNode, permissionTest)); + } + } + } + + @EventHandler + @SuppressWarnings("deprecation") // draft API, ok... + public void onPlayerSendCommandsEvent(AsyncPlayerSendCommandsEvent event) { + if (event.isAsynchronous() || !event.hasFiredAsync()) { + for (AdajencyCommand command : this.commands) { + command.apply(event.getPlayer(), event.getCommandNode()); + } + } + } + + private record AdajencyCommand(LiteralCommandNode node, Predicate permissionTest) { + @SuppressWarnings({"unchecked", "rawtypes"}) + public void apply(Player player, RootCommandNode root) { + if (this.permissionTest != null && !this.permissionTest.test(player)) { + return; + } + removeChild(root, this.node.getName()); + root.addChild((CommandNode) this.node); + } + } + + static void ensureSetup() { + // do nothing - this is only called to trigger the static initializer + } +} diff --git a/src/main/java/ru/d3st0ny/adajency/AdajencyImpl.java b/src/main/java/ru/d3st0ny/adajency/ReflectionAdajency.java similarity index 59% rename from src/main/java/ru/d3st0ny/adajency/AdajencyImpl.java rename to src/main/java/ru/d3st0ny/adajency/ReflectionAdajency.java index 8f12044..e7cf798 100644 --- a/src/main/java/ru/d3st0ny/adajency/AdajencyImpl.java +++ b/src/main/java/ru/d3st0ny/adajency/ReflectionAdajency.java @@ -3,14 +3,11 @@ package ru.d3st0ny.adajency; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.suggestion.SuggestionProvider; -import com.mojang.brigadier.tree.ArgumentCommandNode; -import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; import org.bukkit.Bukkit; import org.bukkit.command.Command; -import org.bukkit.command.PluginCommand; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -26,14 +23,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; -final class AdajencyImpl implements Adajency { +final class ReflectionAdajency extends AbstractAdajency implements Adajency { // obc.CraftServer#console field private static final Field CONSOLE_FIELD; @@ -47,26 +42,12 @@ final class AdajencyImpl implements Adajency { // obc.command.BukkitCommandWrapper constructor private static final Constructor COMMAND_WRAPPER_CONSTRUCTOR; - // ArgumentCommandNode#customSuggestions field - private static final Field CUSTOM_SUGGESTIONS_FIELD; - - // CommandNode#command - private static final Field COMMAND_EXECUTE_FUNCTION_FIELD; - - // CommandNode#children, CommandNode#literals, CommandNode#arguments fields - private static final Field CHILDREN_FIELD; - private static final Field LITERALS_FIELD; - private static final Field ARGUMENTS_FIELD; - - // An array of the CommandNode fields above: [#children, #literals, #arguments] - private static final Field[] CHILDREN_FIELDS; - - // Dummy instance of Command used to ensure the executable bit gets set on - // mock commands when they're encoded into data sent to the client - private static final com.mojang.brigadier.Command DUMMY_COMMAND; - static { try { + if (ReflectionUtil.minecraftVersion() >= 19) { + throw new UnsupportedOperationException("ReflectionAdajency is not supported on MC 1.19 or above. Switch to Paper :)"); + } + final Class minecraftServer; final Class commandDispatcher; @@ -97,22 +78,6 @@ final class AdajencyImpl implements Adajency { Class commandWrapperClass = ReflectionUtil.obcClass("command.BukkitCommandWrapper"); COMMAND_WRAPPER_CONSTRUCTOR = commandWrapperClass.getConstructor(craftServer, Command.class); - CUSTOM_SUGGESTIONS_FIELD = ArgumentCommandNode.class.getDeclaredField("customSuggestions"); - CUSTOM_SUGGESTIONS_FIELD.setAccessible(true); - - COMMAND_EXECUTE_FUNCTION_FIELD = CommandNode.class.getDeclaredField("command"); - COMMAND_EXECUTE_FUNCTION_FIELD.setAccessible(true); - - CHILDREN_FIELD = CommandNode.class.getDeclaredField("children"); - LITERALS_FIELD = CommandNode.class.getDeclaredField("literals"); - ARGUMENTS_FIELD = CommandNode.class.getDeclaredField("arguments"); - CHILDREN_FIELDS = new Field[]{CHILDREN_FIELD, LITERALS_FIELD, ARGUMENTS_FIELD}; - for (Field field : CHILDREN_FIELDS) { - field.setAccessible(true); - } - - DUMMY_COMMAND = (ctx) -> { throw new UnsupportedOperationException(); }; - } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } @@ -121,7 +86,7 @@ final class AdajencyImpl implements Adajency { private final Plugin plugin; private final List> registeredNodes = new ArrayList<>(); - AdajencyImpl(Plugin plugin) { + ReflectionAdajency(Plugin plugin) { this.plugin = plugin; this.plugin.getServer().getPluginManager().registerEvents(new ServerReloadListener(this), this.plugin); } @@ -172,70 +137,19 @@ final class AdajencyImpl implements Adajency { if (node.getLiteral().equals(alias)) { register(node); } else { - register(LiteralArgumentBuilder.literal(alias).redirect((LiteralCommandNode)node).build()); + register(LiteralArgumentBuilder.literal(alias).redirect((LiteralCommandNode) node).build()); } } this.plugin.getServer().getPluginManager().registerEvents(new CommandDataSendListener(command, permissionTest), this.plugin); } - @Override - public void register(Command command, LiteralCommandNode node) { - Objects.requireNonNull(command, "command"); - Objects.requireNonNull(node, "node"); - - register(command, node, command::testPermissionSilent); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private static void removeChild(RootCommandNode root, String name) { - try { - for (Field field : CHILDREN_FIELDS) { - Map children = (Map) field.get(root); - children.remove(name); - } - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - private static void setRequiredHackyFieldsRecursively(CommandNode node, SuggestionProvider suggestionProvider) { - // set command execution function so the server sets the executable flag on the command - try { - COMMAND_EXECUTE_FUNCTION_FIELD.set(node, DUMMY_COMMAND); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - if (node instanceof ArgumentCommandNode argumentNode) { - - // set the custom suggestion provider field so tab completions work - try { - CUSTOM_SUGGESTIONS_FIELD.set(argumentNode, suggestionProvider); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - - for (CommandNode child : node.getChildren()) { - setRequiredHackyFieldsRecursively(child, suggestionProvider); - } - } - - private static LiteralCommandNode renameLiteralNode(LiteralCommandNode node, String newLiteral) { - LiteralCommandNode clone = new LiteralCommandNode<>(newLiteral, node.getCommand(), node.getRequirement(), node.getRedirect(), node.getRedirectModifier(), node.isFork()); - for (CommandNode child : node.getChildren()) { - clone.addChild(child); - } - return clone; - } - /** * Listens for server (re)loads, and re-adds all registered nodes to the dispatcher. */ - private record ServerReloadListener(AdajencyImpl adajency) implements Listener { + private record ServerReloadListener(ReflectionAdajency adajency) implements Listener { - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({"rawtypes", "unchecked"}) @EventHandler public void onLoad(ServerLoadEvent e) { CommandDispatcher dispatcher = this.adajency.getDispatcher(); @@ -276,36 +190,7 @@ final class AdajencyImpl implements Adajency { } } - /** - * Gets the aliases known for the given command. - * - *

This will include the main label, as well as defined aliases, and - * aliases including the fallback prefix added by Bukkit.

- * - * @param command the command - * @return the aliases - */ - private static Collection getAliases(Command command) { - Objects.requireNonNull(command, "command"); - - Stream aliasesStream = Stream.concat( - Stream.of(command.getLabel()), - command.getAliases().stream() - ); - - if (command instanceof PluginCommand) { - String fallbackPrefix = ((PluginCommand) command).getPlugin().getName().toLowerCase().trim(); - aliasesStream = aliasesStream.flatMap(alias -> Stream.of( - alias, - fallbackPrefix + ":" + alias - )); - } - - return aliasesStream.distinct().collect(Collectors.toList()); - } - static void ensureSetup() { // do nothing - this is only called to trigger the static initializer } - } diff --git a/src/main/java/ru/d3st0ny/adajency/ReflectionUtil.java b/src/main/java/ru/d3st0ny/adajency/ReflectionUtil.java index 002ef4b..e238fcf 100644 --- a/src/main/java/ru/d3st0ny/adajency/ReflectionUtil.java +++ b/src/main/java/ru/d3st0ny/adajency/ReflectionUtil.java @@ -4,7 +4,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bukkit.Bukkit; -final class ReflectionUtil { +public final class ReflectionUtil { private static final String SERVER_VERSION = getServerVersion(); private static String getServerVersion() {