This commit is contained in:
destony 2022-09-25 16:30:11 +03:00
parent f8e79ea869
commit 4f8429e0b1
8 changed files with 298 additions and 151 deletions

View file

@ -2,10 +2,11 @@ plugins {
id("java") id("java")
id("java-library") id("java-library")
id("maven-publish") id("maven-publish")
kotlin("jvm") version "1.7.0"
} }
group = "ru.d3st0ny" group = "ru.d3st0ny"
version = "0.1" version = "0.3"
repositories { repositories {
mavenCentral() mavenCentral()
@ -17,17 +18,16 @@ dependencies {
api("com.mojang:brigadier:1.0.18") { api("com.mojang:brigadier:1.0.18") {
exclude("com.google.guava", "guava") 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 { publishing {
publications { publications.create<MavenPublication>("maven") {
create<MavenPublication>("maven"){ artifactId = project.name.toLowerCase()
artifactId = project.name.toLowerCase() groupId = "${project.group}"
groupId = "${project.group}" version = "${project.version}"
version = "${project.version}" from(components["java"])
from(components["java"])
}
} }
repositories { repositories {
maven { maven {

View file

@ -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<String, ?> children = (Map<String, ?>) 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 <S> LiteralCommandNode<S> renameLiteralNode(LiteralCommandNode<S> node, String newLiteral) {
LiteralCommandNode<S> clone = new LiteralCommandNode<>(newLiteral, node.getCommand(), node.getRequirement(), node.getRedirect(), node.getRedirectModifier(), node.isFork());
for (CommandNode<S> child : node.getChildren()) {
clone.addChild(child);
}
return clone;
}
/**
* Gets the aliases known for the given command.
*
* <p>This will include the main label, as well as defined aliases, and
* aliases including the fallback prefix added by Bukkit.</p>
*
* @param command the command
* @return the aliases
*/
protected static Collection<String> getAliases(Command command) {
Objects.requireNonNull(command, "command");
Stream<String> 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());
}
}

View file

@ -66,7 +66,11 @@ public interface Adajency {
* @param command the command to read aliases from * @param command the command to read aliases from
* @param node the argument data * @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 * Registers the provided argument data to the dispatcher, against all

View file

@ -3,6 +3,7 @@ package ru.d3st0ny.adajency;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
/** /**
* Factory for obtaining instances of {@link Adajency}. * Factory for obtaining instances of {@link Adajency}.
@ -12,20 +13,39 @@ public final class AdajencyProvider {
throw new AssertionError(); throw new AssertionError();
} }
private static final Throwable SETUP_EXCEPTION = checkSupported(); private static final Function<Plugin, Adajency> PROVIDER = checkSupported();
private static Throwable checkSupported() { private static Function<Plugin, Adajency> checkSupported() {
try { try {
Class.forName("com.mojang.brigadier.CommandDispatcher"); Class.forName("com.mojang.brigadier.CommandDispatcher");
AdajencyImpl.ensureSetup();
MinecraftArgumentType.ensureSetup();
return null;
} catch (Throwable e) { } catch (Throwable e) {
if (System.getProperty("adajency.debug") != null) { printDebugInfo(e);
System.err.println("Exception while initialising adajency:"); return null;
e.printStackTrace(System.err); }
}
return e; // 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. * @return true if adajency is supported.
*/ */
public static boolean isSupported() { 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 { public static Adajency getAdajency(Plugin plugin) throws BrigadierUnsupportedException {
Objects.requireNonNull(plugin, "plugin"); Objects.requireNonNull(plugin, "plugin");
if (SETUP_EXCEPTION != null) { if (PROVIDER == null) {
throw new BrigadierUnsupportedException("Brigadier is not supported by the server.", SETUP_EXCEPTION); 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);
} }
} }

View file

@ -5,7 +5,7 @@ package ru.d3st0ny.adajency;
*/ */
public final class BrigadierUnsupportedException extends UnsupportedOperationException { public final class BrigadierUnsupportedException extends UnsupportedOperationException {
public BrigadierUnsupportedException(String message, Throwable cause) { BrigadierUnsupportedException(String message) {
super(message, cause); super(message);
} }
} }

View file

@ -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<AdajencyCommand> 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<? super Player> 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<String> 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<Object> redirectNode = LiteralArgumentBuilder.literal(alias)
.redirect((LiteralCommandNode<Object>) 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<? super Player> 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
}
}

View file

@ -3,14 +3,11 @@ package ru.d3st0ny.adajency;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider; 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.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode; import com.mojang.brigadier.tree.RootCommandNode;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@ -26,14 +23,12 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; 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 // obc.CraftServer#console field
private static final Field CONSOLE_FIELD; private static final Field CONSOLE_FIELD;
@ -47,26 +42,12 @@ final class AdajencyImpl implements Adajency {
// obc.command.BukkitCommandWrapper constructor // obc.command.BukkitCommandWrapper constructor
private static final Constructor<?> COMMAND_WRAPPER_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 { static {
try { 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<?> minecraftServer;
final Class<?> commandDispatcher; final Class<?> commandDispatcher;
@ -97,22 +78,6 @@ final class AdajencyImpl implements Adajency {
Class<?> commandWrapperClass = ReflectionUtil.obcClass("command.BukkitCommandWrapper"); Class<?> commandWrapperClass = ReflectionUtil.obcClass("command.BukkitCommandWrapper");
COMMAND_WRAPPER_CONSTRUCTOR = commandWrapperClass.getConstructor(craftServer, Command.class); 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) { } catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e); throw new ExceptionInInitializerError(e);
} }
@ -121,7 +86,7 @@ final class AdajencyImpl implements Adajency {
private final Plugin plugin; private final Plugin plugin;
private final List<LiteralCommandNode<?>> registeredNodes = new ArrayList<>(); private final List<LiteralCommandNode<?>> registeredNodes = new ArrayList<>();
AdajencyImpl(Plugin plugin) { ReflectionAdajency(Plugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.plugin.getServer().getPluginManager().registerEvents(new ServerReloadListener(this), this.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)) { if (node.getLiteral().equals(alias)) {
register(node); register(node);
} else { } else {
register(LiteralArgumentBuilder.literal(alias).redirect((LiteralCommandNode<Object>)node).build()); register(LiteralArgumentBuilder.literal(alias).redirect((LiteralCommandNode<Object>) node).build());
} }
} }
this.plugin.getServer().getPluginManager().registerEvents(new CommandDataSendListener(command, permissionTest), this.plugin); 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<String, ?> children = (Map<String, ?>) 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 <S> LiteralCommandNode<S> renameLiteralNode(LiteralCommandNode<S> node, String newLiteral) {
LiteralCommandNode<S> clone = new LiteralCommandNode<>(newLiteral, node.getCommand(), node.getRequirement(), node.getRedirect(), node.getRedirectModifier(), node.isFork());
for (CommandNode<S> child : node.getChildren()) {
clone.addChild(child);
}
return clone;
}
/** /**
* Listens for server (re)loads, and re-adds all registered nodes to the dispatcher. * 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 @EventHandler
public void onLoad(ServerLoadEvent e) { public void onLoad(ServerLoadEvent e) {
CommandDispatcher dispatcher = this.adajency.getDispatcher(); CommandDispatcher dispatcher = this.adajency.getDispatcher();
@ -276,36 +190,7 @@ final class AdajencyImpl implements Adajency {
} }
} }
/**
* Gets the aliases known for the given command.
*
* <p>This will include the main label, as well as defined aliases, and
* aliases including the fallback prefix added by Bukkit.</p>
*
* @param command the command
* @return the aliases
*/
private static Collection<String> getAliases(Command command) {
Objects.requireNonNull(command, "command");
Stream<String> 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() { static void ensureSetup() {
// do nothing - this is only called to trigger the static initializer // do nothing - this is only called to trigger the static initializer
} }
} }

View file

@ -4,7 +4,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
final class ReflectionUtil { public final class ReflectionUtil {
private static final String SERVER_VERSION = getServerVersion(); private static final String SERVER_VERSION = getServerVersion();
private static String getServerVersion() { private static String getServerVersion() {