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-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<MavenPublication>("maven"){
artifactId = project.name.toLowerCase()
groupId = "${project.group}"
version = "${project.version}"
from(components["java"])
}
publications.create<MavenPublication>("maven") {
artifactId = project.name.toLowerCase()
groupId = "${project.group}"
version = "${project.version}"
from(components["java"])
}
repositories {
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 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

View file

@ -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<Plugin, Adajency> PROVIDER = checkSupported();
private static Throwable checkSupported() {
private static Function<Plugin, Adajency> 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);
}
}

View file

@ -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);
}
}

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.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<LiteralCommandNode<?>> 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<Object>)node).build());
register(LiteralArgumentBuilder.literal(alias).redirect((LiteralCommandNode<Object>) 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<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.
*/
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.
*
* <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() {
// 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 org.bukkit.Bukkit;
final class ReflectionUtil {
public final class ReflectionUtil {
private static final String SERVER_VERSION = getServerVersion();
private static String getServerVersion() {