Skip to content

Commit

Permalink
Add adapter for converting relocated adventure objects to native
Browse files Browse the repository at this point in the history
  • Loading branch information
Brikster committed Jun 30, 2024
1 parent 3f12e62 commit f2417c1
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 41 deletions.
20 changes: 10 additions & 10 deletions spigot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ tasks {
shadowJar {
archiveFileName = "Chatty-${rootProject.version}.jar"
destinationDirectory = rootProject.layout.buildDirectory.dir('libs')
def relocations = ['org.bstats',
'com.google.gson',
'com.google.guava',
'com.google.common',
'org.yaml.snakeyaml',
'cloud.commandframework',
// 'eu.okaeri',
'com.zaxxer',
// 'net.kyori'
def relocations = [
'org.bstats',
'com.google.gson',
'com.google.guava',
'com.google.common',
'org.yaml.snakeyaml',
'cloud.commandframework',
// 'eu.okaeri',
'com.zaxxer',
'net.kyori'
]
relocations.forEach {
relocate it, "ru.brikster.chatty.shaded.${it}"
Expand Down Expand Up @@ -49,7 +50,6 @@ repositories {
maven { url = "https://storehouse.okaeri.eu/repository/maven-public/" }
maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' }
maven { url = 'https://repo.codemc.org/repository/maven-public' }

}

def okaeriVersion = '5a33076d8c'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package ru.brikster.chatty.adventure;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.SneakyThrows;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.Title.Times;
import org.jetbrains.annotations.NotNull;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

public final class NativeAudienceAdapter implements Audience {

private static final Cache<Object, Object> CONVERTED_OBJECTS_CACHE =
CacheBuilder.newBuilder()
.weakKeys()
.expireAfterAccess(Duration.ofMinutes(5))
.build();

private static final String NET_KYORI_ADVENTURE = "net.".concat("kyori.adventure.");
private static final String AUDIENCE_CLASS_NAME = NET_KYORI_ADVENTURE.concat("audience.Audience");
private static final String COMPONENT_CLASS_NAME = NET_KYORI_ADVENTURE.concat("text.Component");
private static final String IDENTITY_CLASS_NAME = NET_KYORI_ADVENTURE.concat("identity.Identity");
private static final String KEY_CLASS_NAME = NET_KYORI_ADVENTURE.concat("key.Key");
private static final String SOUND_CLASS_NAME = NET_KYORI_ADVENTURE.concat("sound.Sound");
private static final String TITLE_CLASS_NAME = NET_KYORI_ADVENTURE.concat("title.Title");
private static final String TITLE_TIMES_CLASS_NAME = NET_KYORI_ADVENTURE.concat("title.Title$Times");
private static final String SOUND_SOURCE_CLASS_NAME = NET_KYORI_ADVENTURE.concat("sound.Sound$Source");
private static final String GSON_SERIALIZER_CLASS_NAME = NET_KYORI_ADVENTURE
.concat("text.serializer.gson.GsonComponentSerializer");
private static final String COMPONENT_SERIALIZER_CLASS_NAME = NET_KYORI_ADVENTURE
.concat("text.serializer.ComponentSerializer");

private static final Class<?> SOUND_SOURCE_CLASS;

private static final Lookup LOOKUP;

private static final MethodHandle SEND_MESSAGE_METHOD;
private static final MethodHandle SEND_MESSAGE_WITH_IDENTITY_METHOD;
private static final MethodHandle SEND_ACTION_BAR_METHOD;
private static final MethodHandle PLAY_SOUND_METHOD;
private static final MethodHandle SHOW_TITLE_METHOD;

private static final Object GSON_COMPONENT_SERIALIZER;
private static final MethodHandle DESERIALIZE_METHOD;
private static final MethodHandle IDENTITY_FACTORY_METHOD;
private static final MethodHandle KEY_FACTORY_METHOD;
private static final MethodHandle SOUND_FACTORY_METHOD;
private static final MethodHandle TITLE_FACTORY_METHOD;
private static MethodHandle TITLE_TIMES_FACTORY_METHOD;

static {
try {
LOOKUP = MethodHandles.lookup();
Class<?> audienceClass = Class.forName(AUDIENCE_CLASS_NAME);
Class<?> componentClass = Class.forName(COMPONENT_CLASS_NAME);
Class<?> identityClass = Class.forName(IDENTITY_CLASS_NAME);
Class<?> keyClass = Class.forName(KEY_CLASS_NAME);
Class<?> soundClass = Class.forName(SOUND_CLASS_NAME);
Class<?> titleClass = Class.forName(TITLE_CLASS_NAME);
Class<?> titleTimesClass = Class.forName(TITLE_TIMES_CLASS_NAME);
SOUND_SOURCE_CLASS = Class.forName(SOUND_SOURCE_CLASS_NAME);

SEND_MESSAGE_METHOD = LOOKUP.findVirtual(audienceClass, "sendMessage",
MethodType.methodType(void.class, componentClass));
SEND_MESSAGE_WITH_IDENTITY_METHOD = LOOKUP.findVirtual(audienceClass, "sendMessage",
MethodType.methodType(void.class, identityClass, componentClass));
SEND_ACTION_BAR_METHOD = LOOKUP.findVirtual(audienceClass, "sendActionBar",
MethodType.methodType(void.class, componentClass));
PLAY_SOUND_METHOD = LOOKUP.findVirtual(audienceClass, "playSound",
MethodType.methodType(void.class, soundClass));
SHOW_TITLE_METHOD = LOOKUP.findVirtual(audienceClass, "showTitle",
MethodType.methodType(void.class, titleClass));

Class<?> gsonComponentSerializerClass = Class.forName(GSON_SERIALIZER_CLASS_NAME);
GSON_COMPONENT_SERIALIZER = gsonComponentSerializerClass.getMethod("gson").invoke(null);
Class<?> componentSerializerClass = Class.forName(COMPONENT_SERIALIZER_CLASS_NAME);
DESERIALIZE_METHOD = LOOKUP.findVirtual(componentSerializerClass, "deserialize",
MethodType.methodType(componentClass, Object.class));
IDENTITY_FACTORY_METHOD = LOOKUP.findStatic(identityClass, "identity",
MethodType.methodType(identityClass, UUID.class));

KEY_FACTORY_METHOD = LOOKUP.findStatic(keyClass, "key",
MethodType.methodType(keyClass, String.class, String.class));
SOUND_FACTORY_METHOD = LOOKUP.findStatic(soundClass, "sound",
MethodType.methodType(soundClass, keyClass, SOUND_SOURCE_CLASS, float.class, float.class));
TITLE_FACTORY_METHOD = LOOKUP.findStatic(titleClass, "title",
MethodType.methodType(titleClass, componentClass, componentClass, titleTimesClass));
try {
TITLE_TIMES_FACTORY_METHOD = LOOKUP.findStatic(titleTimesClass, "times",
MethodType.methodType(titleTimesClass, Duration.class, Duration.class, Duration.class));
} catch (NoSuchMethodException m) {
TITLE_TIMES_FACTORY_METHOD = LOOKUP.findStatic(titleTimesClass, "of",
MethodType.methodType(titleTimesClass, Duration.class, Duration.class, Duration.class));
}
} catch (Throwable t) {
throw new RuntimeException(t);
}
}

private final Object target;

public NativeAudienceAdapter(Object target) {
this.target = target;
}

@SneakyThrows
@Override
public void sendMessage(@NotNull Component message) {
Object convertedComponent = getCachedOrConvert(message, () -> convertComponent(message));
SEND_MESSAGE_METHOD.invoke(target, convertedComponent);
}

@SneakyThrows
@SuppressWarnings("deprecation")
@Override
public void sendMessage(@NotNull Identity source, @NotNull Component message) {
Object identity = getCachedOrConvert(source, () -> IDENTITY_FACTORY_METHOD.invoke(source.uuid()));
Object convertedComponent = getCachedOrConvert(message, () -> convertComponent(message));
SEND_MESSAGE_WITH_IDENTITY_METHOD.invoke(target, identity, convertedComponent);
}

@SneakyThrows
@Override
public void playSound(@NotNull Sound sound) {
Object convertedSound = getCachedOrConvert(sound, () -> convertSound(sound));
PLAY_SOUND_METHOD.invoke(target, convertedSound);
}

@SneakyThrows
@Override
public void sendActionBar(@NotNull Component message) {
Object convertedComponent = getCachedOrConvert(message, () -> convertComponent(message));
SEND_ACTION_BAR_METHOD.invoke(target, convertedComponent);
}

@SneakyThrows
@Override
public void showTitle(@NotNull Title title) {
Object convertedTitle = getCachedOrConvert(title, () -> {
Times times = title.times();
if (times == null) times = Title.DEFAULT_TIMES;
Object convertedTimes = TITLE_TIMES_FACTORY_METHOD.invoke(
times.fadeIn(),
times.stay(),
times.fadeOut());
return TITLE_FACTORY_METHOD.invoke(
convertComponent(title.title()),
convertComponent(title.subtitle()),
convertedTimes);
});
SHOW_TITLE_METHOD.invoke(target, convertedTitle);
}

private static Object convertComponent(@NotNull Component message) throws Throwable {
String serializedComponent = GsonComponentSerializer.gson().serialize(message);
return DESERIALIZE_METHOD.invoke(GSON_COMPONENT_SERIALIZER, serializedComponent);
}

private static Object convertSound(@NotNull Sound sound) throws Throwable {
Object name = KEY_FACTORY_METHOD.invoke(sound.name().namespace(), sound.name().value());
@SuppressWarnings({"rawtypes", "unchecked"})
Object source = Enum.valueOf((Class<? extends Enum>) SOUND_SOURCE_CLASS, sound.source().name());
return SOUND_FACTORY_METHOD.invoke(name, source, sound.volume(), sound.pitch());
}

private static Object getCachedOrConvert(Object object, UncheckedCallable<Object> callable)
throws ExecutionException {
return CONVERTED_OBJECTS_CACHE.get(object, callable);
}

interface UncheckedCallable<T> extends Callable<T> {

@Override
default T call() {
try {
return run();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

T run() throws Throwable;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,65 @@
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Predicate;

@SuppressWarnings("rawtypes")
public final class NativeBukkitAudienceProvider implements BukkitAudiences {

@Override
public @NotNull Audience all() {
//noinspection unchecked
List<Audience> audienceList = new ArrayList<>(
(Collection) Bukkit.getOnlinePlayers());
audienceList.add((Audience) Bukkit.getConsoleSender());
List<Audience> audienceList = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
audienceList.add(new NativeAudienceAdapter(player));
}
audienceList.add(new NativeAudienceAdapter(Bukkit.getConsoleSender()));
return Audience.audience(audienceList);
}

@Override
public @NotNull Audience console() {
return (Audience) Bukkit.getConsoleSender();
return new NativeAudienceAdapter(Bukkit.getConsoleSender());
}

@Override
public @NotNull Audience players() {
//noinspection unchecked
return Audience.audience(
(Collection) Bukkit.getOnlinePlayers());
List<Audience> audienceList = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
audienceList.add(new NativeAudienceAdapter(player));
}
return Audience.audience(audienceList);
}

@Override
public @NotNull Audience player(@NotNull UUID playerId) {
return Audience.audience((Audience) Objects.requireNonNull(Bukkit.getPlayer(playerId),
return new NativeAudienceAdapter(Objects.requireNonNull(Bukkit.getPlayer(playerId),
"Cannot find player"));
}

@Override
public @NotNull Audience permission(@NotNull String permission) {
List<CommandSender> audienceList = new ArrayList<>(
Bukkit.getOnlinePlayers());
audienceList.add(Bukkit.getConsoleSender());
audienceList.removeIf(sender -> !sender.hasPermission(permission));
//noinspection unchecked
return Audience.audience((Collection) audienceList);
List<Audience> audienceList = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
audienceList.add(new NativeAudienceAdapter(player));
}
}
audienceList.add(new NativeAudienceAdapter(Bukkit.getConsoleSender()));
return Audience.audience(audienceList);
}

@Override
public @NotNull Audience world(@NotNull Key world) {
List<Player> audienceList = new ArrayList<>(
Bukkit.getOnlinePlayers());
audienceList.removeIf(player -> !player.getWorld().getName().equals(world.asString()));
//noinspection unchecked
return Audience.audience((Collection) audienceList);
List<Audience> audienceList = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getWorld().getName().equals(world.asString())) {
audienceList.add(new NativeAudienceAdapter(player));
}
}
return Audience.audience(audienceList);
}

@Override
Expand All @@ -78,7 +87,7 @@ public void close() {

@Override
public @NotNull Audience sender(@NotNull CommandSender sender) {
return Audience.audience((Audience) sender);
return Audience.audience(new NativeAudienceAdapter(sender));
}

@Override
Expand All @@ -88,12 +97,16 @@ public void close() {

@Override
public @NotNull Audience filter(@NotNull Predicate<CommandSender> filter) {
List<CommandSender> audienceList = new ArrayList<>(
Bukkit.getOnlinePlayers());
audienceList.add(Bukkit.getConsoleSender());
audienceList.removeIf(sender -> !filter.test(sender));
//noinspection unchecked
return Audience.audience((Collection) audienceList);
List<Audience> audienceList = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
if (filter.test(player)) {
audienceList.add(new NativeAudienceAdapter(player));
}
}
if (filter.test(Bukkit.getConsoleSender())) {
audienceList.add(new NativeAudienceAdapter(Bukkit.getConsoleSender()));
}
return Audience.audience(audienceList);
}

}
4 changes: 2 additions & 2 deletions spigot/src/main/java/ru/brikster/chatty/util/PaperUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public boolean isPaper() {

public boolean isSupportAdventure() {
try {
//noinspection JavaReflectionMemberAccess
Player.class.getMethod("sendMessage", Class.forName("net.kyori.adventure.text.Component"));
// Concatenation to prevent shadow's relocation
Player.class.getMethod("sendMessage", Class.forName("net".concat(".kyori.adventure.text.Component")));
return true;
} catch (NoSuchMethodException | ClassNotFoundException e) {
return false;
Expand Down

0 comments on commit f2417c1

Please sign in to comment.