diff --git a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/BukkitAudience.java b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/BukkitAudience.java index 733a8140..986e6f8c 100644 --- a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/BukkitAudience.java +++ b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/BukkitAudience.java @@ -45,42 +45,44 @@ final class BukkitAudience extends FacetAudience { static final ThreadLocal PLUGIN = new ThreadLocal<>(); private static final Function VIA = new BukkitFacet.ViaHook(); - private static final Collection> CHAT = Facet.of( + private static final Collection> CHAT = Facet.of( () -> new ViaFacet.Chat<>(Player.class, VIA), - // () -> new SpigotFacet.ChatWithType(), - // () -> new SpigotFacet.Chat(), + () -> new PaperFacet.Chat(), () -> new CraftBukkitFacet.Chat(), () -> new BukkitFacet.Chat()); - private static final Collection> ACTION_BAR = Facet.of( + private static final Collection> ACTION_BAR = Facet.of( () -> new ViaFacet.ActionBarTitle<>(Player.class, VIA), () -> new ViaFacet.ActionBar<>(Player.class, VIA), - // () -> new SpigotFacet.ActionBar(), + () -> new PaperFacet.ActionBar(), () -> new CraftBukkitFacet.ActionBar_1_17(), () -> new CraftBukkitFacet.ActionBar(), () -> new CraftBukkitFacet.ActionBarLegacy()); - private static final Collection> TITLE = Facet.of( + private static final Collection> TITLE = Facet.of( () -> new ViaFacet.Title<>(Player.class, VIA), - // () -> new PaperFacet.Title(), + () -> new PaperFacet.Title(), () -> new CraftBukkitFacet.Title_1_17(), () -> new CraftBukkitFacet.Title()); - private static final Collection> SOUND = Facet.of( + private static final Collection> SOUND = Facet.of( + () -> new PaperFacet.Sound(), () -> new BukkitFacet.SoundWithCategory(), () -> new BukkitFacet.Sound()); - private static final Collection> ENTITY_SOUND = Facet.of( + private static final Collection> ENTITY_SOUND = Facet.of( + () -> new PaperFacet.EntitySound(), () -> new CraftBukkitFacet.EntitySound() ); - private static final Collection> BOOK = Facet.of( - // () -> new SpigotFacet.Book(), + private static final Collection> BOOK = Facet.of( + () -> new PaperFacet.Book(), () -> new CraftBukkitFacet.BookPost1_13(), () -> new CraftBukkitFacet.Book1_13(), () -> new CraftBukkitFacet.BookPre1_13()); - private static final Collection> BOSS_BAR = Facet.of( + private static final Collection> BOSS_BAR = Facet.of( () -> new ViaFacet.BossBar.Builder<>(Player.class, VIA), () -> new ViaFacet.BossBar.Builder1_9_To_1_15<>(Player.class, VIA), + () -> new PaperFacet.BossBarBuilder(), () -> new CraftBukkitFacet.BossBar.Builder(), () -> new BukkitFacet.BossBarBuilder(), () -> new CraftBukkitFacet.BossBarWither.Builder()); - private static final Collection> TAB_LIST = Facet.of( + private static final Collection> TAB_LIST = Facet.of( () -> new ViaFacet.TabList<>(Player.class, VIA), () -> new PaperFacet.TabList(), () -> new CraftBukkitFacet.TabList(), diff --git a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MappedBuilder.java b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MappedBuilder.java new file mode 100644 index 00000000..ebb7c50a --- /dev/null +++ b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MappedBuilder.java @@ -0,0 +1,113 @@ +package net.kyori.adventure.platform.bukkit; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import net.kyori.adventure.platform.facet.Knob; +import org.jetbrains.annotations.Nullable; + +import static net.kyori.adventure.platform.facet.Knob.logError; + +final class MappedBuilder { + private final Class builtClass; + private final Class builderClass; + private final MethodHandle builderFactory; + private final MethodHandle buildMethod; + private final Map builderMethods; + + static @Nullable MappedBuilder mappedBuilder(final @Nullable Class built, final @Nullable Class builder, final String builderMethodName) { + if (built == null || builder == null) { + return null; + } + + final MethodHandle builderMethod = MinecraftReflection.findStaticMethod(built, builderMethodName, builder); + final MethodHandle buildMethod = MinecraftReflection.findMethod(builder, "build", built); + + if (builderMethod == null || buildMethod == null) return null; + + return new MappedBuilder(built, builder, builderMethod, buildMethod, discoverBuilderMethods(builder)); + } + + private static Map discoverBuilderMethods(final Class builderClass) { + final Map discoveredMethods = new HashMap<>(); + + for (final Method method : builderClass.getMethods()) { + final String name = method.getName(); + final int mod = method.getModifiers(); + + if (!Modifier.isPublic(mod) || Modifier.isStatic(mod)) continue; + if ( + !builderClass.isAssignableFrom(method.getReturnType()) + && !method.getReturnType().isAssignableFrom(builderClass) + ) continue; + if (name.equals("build")) continue; + + method.setAccessible(true); + + try { + discoveredMethods.put(name, MinecraftReflection.lookup().unreflect(method)); + } catch (final IllegalAccessException ex) { + logError(ex, "Could not unreflect builder method {}", method); + } + } + + return Collections.unmodifiableMap(discoveredMethods); + } + + public MappedBuilder(final Class built, final Class builder, final MethodHandle builderFactory, final MethodHandle build, final Map builderMethods) { + this.builtClass = built; + this.builderClass = builder; + this.builderFactory = builderFactory; + this.buildMethod = build; + this.builderMethods = builderMethods; + } + + public Instance begin() { + try { + final Object builder = this.builderFactory.invoke(); + return new Instance(builder); + } catch (final Throwable ex) { + logError(ex, "Failed to create builder instance for {}", this.builderClass); + return new Instance(null); + } + } + + final class Instance { + private final Object builder; + + Instance(final Object builder) { + this.builder = builder; + } + + Instance set(final String key, final Object value) { + if (this.builder != null) { + final MethodHandle builderMethod = MappedBuilder.this.builderMethods.get(key); + if (builderMethod == null) { + Knob.logMessage("No builder method found in {} for {}", MappedBuilder.this.builderClass, key); + } else { + try { + builderMethod.invoke(this.builder, value); + } catch (final Throwable ex) { + logError(ex, "Failed to invoke builder method {}", builderMethod); + } + } + } + + return this; + } + + @Nullable Object build() { + if (this.builder == null) return null; + + try { + return MappedBuilder.this.buildMethod.invoke(this.builder); + } catch (final Throwable ex) { + logError(ex, "Failed to create an instance of {}", MappedBuilder.this.builtClass); + return null; + } + } + } +} diff --git a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MinecraftReflection.java b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MinecraftReflection.java index c582d24a..69429fdd 100644 --- a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MinecraftReflection.java +++ b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/MinecraftReflection.java @@ -28,7 +28,9 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Arrays; +import net.kyori.adventure.platform.facet.Knob; import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -364,6 +366,40 @@ public static boolean hasMethod(final @Nullable Class holderClass, final Stri return null; } + /** + * Gets a class field if possible and makes it accessible. + * + * @param holderClass a class + * @param fieldName a field name + * @return a field value + */ + public static @Nullable Object findConstantFieldValue(final @Nullable Class holderClass, final @NotNull String... fieldName) { + return findField(holderClass, null, fieldName); + } + + /** + * Gets the value of a constant field in a class + * + * @param holderClass a class + * @param expectedType the expected type of the field + * @param fieldNames a field name + * @return a field value + */ + @SuppressWarnings("unchecked") + public static @Nullable T findConnstantFieldValue(final @Nullable Class holderClass, final @Nullable Class expectedType, final @NotNull String... fieldNames) { + final Field f = findField(holderClass, expectedType, fieldNames); + if (f == null) return null; + final int mod = f.getModifiers(); + if (!Modifier.isStatic(mod) || !Modifier.isFinal(mod)) return null; + + try { + return (T) f.get(null); + } catch (IllegalArgumentException | IllegalAccessException ex) { + Knob.logError(ex, "While getting value of field {}.{}", f.getDeclaringClass().getName(), f.getName()); + return null; + } + } + /** * Return a method handle that can set the value of the provided field. * diff --git a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/PaperFacet.java b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/PaperFacet.java index 170ba17e..43ad2cb3 100644 --- a/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/PaperFacet.java +++ b/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/PaperFacet.java @@ -25,43 +25,87 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import net.kyori.adventure.audience.MessageType; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.key.Key; import net.kyori.adventure.platform.facet.Facet; import net.kyori.adventure.platform.facet.FacetBase; +import net.kyori.adventure.sound.Sound.Emitter; +import net.kyori.adventure.sound.SoundStop; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.md_5.bungee.api.chat.BaseComponent; -import org.bukkit.command.CommandSender; +import net.kyori.adventure.util.Index; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; +import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static net.kyori.adventure.platform.bukkit.MinecraftReflection.findClass; +import static net.kyori.adventure.platform.bukkit.MinecraftReflection.findMethod; import static net.kyori.adventure.platform.bukkit.MinecraftReflection.findStaticMethod; -import static net.kyori.adventure.platform.bukkit.MinecraftReflection.hasClass; import static net.kyori.adventure.platform.bukkit.MinecraftReflection.hasField; import static net.kyori.adventure.platform.bukkit.MinecraftReflection.lookup; import static net.kyori.adventure.platform.facet.Knob.isEnabled; import static net.kyori.adventure.platform.facet.Knob.logError; -class PaperFacet extends FacetBase { - private static final boolean SUPPORTED = isEnabled("paper", true); - static final Class NATIVE_COMPONENT_CLASS = findClass(String.join(".", "net", "kyori", "adventure", "text", "Component")); - private static final Class NATIVE_GSON_COMPONENT_SERIALIZER_CLASS = findClass(String.join(".", "net", "kyori", "adventure", "text", "serializer", "gson", "GsonComponentSerializer")); - private static final Class NATIVE_GSON_COMPONENT_SERIALIZER_IMPL_CLASS = findClass(String.join(".", "net", "kyori", "adventure", "text", "serializer", "gson", "GsonComponentSerializerImpl")); +class PaperFacet extends FacetBase { + static final Class NATIVE_COMPONENT_CLASS = findClass(nativeClazz("text.Component")); + private static final Class NATIVE_AUDIENCE_CLASS = findClass(nativeClazz("audience.Audience")); + private static final boolean SUPPORTED = isEnabled("paper", true) && !Component.class.equals(NATIVE_COMPONENT_CLASS); + private static final Class NATIVE_GSON_COMPONENT_SERIALIZER_CLASS = findClass(nativeClazz("text.serializer.gson.GsonComponentSerializer")); + private static final Class NATIVE_KEY_CLASS = findClass(nativeClazz("key.Key")); private static final MethodHandle NATIVE_GSON_COMPONENT_SERIALIZER_GSON_GETTER = findStaticMethod(NATIVE_GSON_COMPONENT_SERIALIZER_CLASS, "gson", NATIVE_GSON_COMPONENT_SERIALIZER_CLASS); private static final MethodHandle NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD = findNativeDeserializeMethod(); + private static final MethodHandle NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND = createBoundNativeDeserializeMethodHandle(); + private static final MethodHandle NATIVE_KEY_FACTORY = findStaticMethod(NATIVE_KEY_CLASS, "key", NATIVE_KEY_CLASS, String.class, String.class); private static @Nullable MethodHandle findNativeDeserializeMethod() { + if (NATIVE_GSON_COMPONENT_SERIALIZER_CLASS == null) return null; + try { - final Method method = NATIVE_GSON_COMPONENT_SERIALIZER_IMPL_CLASS.getDeclaredMethod("deserialize", String.class); + final Method method = NATIVE_GSON_COMPONENT_SERIALIZER_CLASS.getDeclaredMethod("deserialize", String.class); method.setAccessible(true); return lookup().unreflect(method); - } catch (final NoSuchMethodException | IllegalAccessException | NullPointerException e) { + } catch (final NoSuchMethodException | IllegalAccessException ex) { return null; } } - protected PaperFacet(final @Nullable Class viewerClass) { + private static @Nullable MethodHandle createBoundNativeDeserializeMethodHandle() { + if (NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD != null) { + try { + return NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD.bindTo(NATIVE_GSON_COMPONENT_SERIALIZER_GSON_GETTER.invoke()); + } catch (final Throwable throwable) { + logError(throwable, "Failed to access native GsonComponentSerializer"); + return null; + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static > Index createNativeEnumMapping(final Class ourClass, final Class nativeClass) { + if (nativeClass == null) { + return null; + } + return Index.create(nativeClass.asSubclass(Enum.class), k -> Enum.valueOf(ourClass, k.name())); + } + + private static String nativeClazz(final String relativeName) { + return String.join(".", "net", "kyori", "adventure", relativeName); + } + + protected PaperFacet() { + this(NATIVE_AUDIENCE_CLASS); + } + + protected PaperFacet(final @Nullable Class viewerClass) { super(viewerClass); } @@ -70,60 +114,321 @@ public boolean isSupported() { return super.isSupported() && SUPPORTED; } - // don't use without resolving parts handling - static class Title extends SpigotFacet.Message implements Facet.Title { - private static final boolean SUPPORTED = hasClass("com.destroystokyo.paper.Title"); + public final @Nullable Object createMessage(final @NotNull Object viewer, final @NotNull Component message) { + return componentToNative(message); + } + + // Convert + + static Object componentToNative(final Component component) { + if (NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND != null) { + try { + return NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND.invoke(GsonComponentSerializer.gson().serialize(component)); + } catch (final Throwable throwable) { + logError(throwable, "Failed to create native Component message"); + } + } + return null; + } + + static Object keyToNative(final Key key) { + try { + return NATIVE_KEY_FACTORY.invoke(key.namespace(), key.value()); + } catch (final Throwable ex) { + logError(ex, "Failed to create native Key message"); + return null; + } + } + + // Facet implementations + + static final class Chat extends PaperFacet implements Facet.Chat { + private static final Class NATIVE_MESSAGE_TYPE = findClass(nativeClazz("audience.MessageType")); + private static final Index MESSAGE_TYPE_TO_NATIVE = createNativeEnumMapping(MessageType.class, NATIVE_MESSAGE_TYPE); + private static final Class NATIVE_IDENTITY_TYPE = findClass(nativeClazz("identity.Identity")); + private static final MethodHandle AUDIENCE_SEND_MESSAGE = findMethod(NATIVE_AUDIENCE_CLASS, "sendMessage", void.class, NATIVE_IDENTITY_TYPE, NATIVE_COMPONENT_CLASS, NATIVE_MESSAGE_TYPE); + + @Override + public boolean isSupported() { + return super.isSupported() && AUDIENCE_SEND_MESSAGE != null && MESSAGE_TYPE_TO_NATIVE != null; + } + + @Override + public void sendMessage(final @NotNull Object viewer, final @NotNull Identity source, final @NotNull Object message, final @NotNull MessageType type) { + try { + AUDIENCE_SEND_MESSAGE.invoke(viewer, source, message, MESSAGE_TYPE_TO_NATIVE.value(type)); + } catch (final Throwable ex) { + logError(ex, "While sending message to {}", viewer); + } + } + } + + static final class ActionBar extends PaperFacet implements Facet.ActionBar { + private static final MethodHandle AUDIENCE_SEND_ACTION_BAR = findMethod(NATIVE_AUDIENCE_CLASS, "sendActionBar", void.class, NATIVE_COMPONENT_CLASS); - protected Title() { - super(Player.class); + ActionBar() { } @Override public boolean isSupported() { - return super.isSupported() && SUPPORTED; + return super.isSupported() && AUDIENCE_SEND_ACTION_BAR != null; } @Override - public com.destroystokyo.paper.Title.@NotNull Builder createTitleCollection() { - return com.destroystokyo.paper.Title.builder(); + public void sendMessage(final @NotNull Object viewer, final @NotNull Object message) { + try { + AUDIENCE_SEND_ACTION_BAR.invoke(viewer, message); + } catch (final Throwable ex) { + logError(ex, "Failed to send actuon bar to {}", viewer); + } } + } + + static final class Title extends PaperFacet implements Facet.Title>, List>> { + private static final Class NATIVE_TITLE_PART_CLASS = findClass(nativeClazz("title.TitlePart")); + private static final Class NATIVE_TITLE_TIMES_CLASS = findClass(nativeClazz("title.Title$Times")); + private static final MethodHandle AUDIENCE_SEND_TITLE_PART = findMethod(NATIVE_AUDIENCE_CLASS, "sendTitlePart", void.class, NATIVE_TITLE_PART_CLASS, Object.class); + private static final MethodHandle AUDIENCE_CLEAR_TITLE = findMethod(NATIVE_AUDIENCE_CLASS, "clearTitle", void.class); + private static final MethodHandle AUDIENCE_RESET_TITLE = findMethod(NATIVE_AUDIENCE_CLASS, "resetTitle", void.class); + private static final MethodHandle TITLE_TIMES_TIMES = findStaticMethod(NATIVE_TITLE_TIMES_CLASS, "times", NATIVE_TITLE_TIMES_CLASS, Duration.class, Duration.class, Duration.class); + private static final Object TITLE_PART_TITLE = MinecraftReflection.findConstantFieldValue(NATIVE_TITLE_PART_CLASS, "TITLE"); + private static final Object TITLE_PART_SUBTITLE = MinecraftReflection.findConstantFieldValue(NATIVE_TITLE_PART_CLASS, "SUBTITLE"); + private static final Object TITLE_PART_TIMES = MinecraftReflection.findConstantFieldValue(NATIVE_TITLE_PART_CLASS, "TIMES"); @Override - public void contributeTitle(final com.destroystokyo.paper.Title.@NotNull Builder coll, final BaseComponent @NotNull [] title) { - coll.title(title); + public boolean isSupported() { + return super.isSupported() && TITLE_PART_TITLE != null && AUDIENCE_SEND_TITLE_PART != null && AUDIENCE_CLEAR_TITLE != null && AUDIENCE_RESET_TITLE != null && TITLE_TIMES_TIMES != null; } @Override - public void contributeSubtitle(final com.destroystokyo.paper.Title.@NotNull Builder coll, final BaseComponent @NotNull [] subtitle) { - coll.subtitle(subtitle); + public @NotNull List> createTitleCollection() { + return new ArrayList<>(3); + } + + private void addSendTitlePart(final List> items, final Object titlePart, final Object value) { + items.add(viewer -> { + try { + AUDIENCE_SEND_TITLE_PART.invoke(viewer, titlePart, value); + } catch (final Throwable ex) { + logError(ex, "Failed to send title part {} to {}", titlePart, viewer); + } + }); } @Override - public void contributeTimes(final com.destroystokyo.paper.Title.@NotNull Builder coll, final int inTicks, final int stayTicks, final int outTicks) { - if (inTicks > -1) coll.fadeIn(inTicks); - if (stayTicks > -1) coll.stay(stayTicks); - if (outTicks > -1) coll.fadeOut(outTicks); + public void contributeTitle(final @NotNull List> coll, final @NotNull Object title) { + this.addSendTitlePart(coll, TITLE_PART_TITLE, title); } - @Nullable @Override - public com.destroystokyo.paper.Title completeTitle(final com.destroystokyo.paper.Title.@NotNull Builder coll) { - return coll.build(); // todo: can't really do parts + public void contributeSubtitle(final @NotNull List> coll, final @NotNull Object subtitle) { + this.addSendTitlePart(coll, TITLE_PART_SUBTITLE, subtitle); } @Override - public void showTitle(final @NotNull Player viewer, final com.destroystokyo.paper.@NotNull Title title) { - viewer.sendTitle(title); + public void contributeTimes(final @NotNull List> coll, final int inTicks, final int stayTicks, final int outTicks) { + Object nativeTimes; + try { + nativeTimes = TITLE_TIMES_TIMES.invoke( + Duration.ofSeconds(inTicks / 20), + Duration.ofSeconds(stayTicks / 20), + Duration.ofSeconds(outTicks / 20) + ); + } catch (final Throwable ex) { + logError(ex, "Failed to create a title times instance", ex); + return; + } + + this.addSendTitlePart(coll, TITLE_PART_TIMES, nativeTimes); } @Override - public void clearTitle(final @NotNull Player viewer) { - viewer.hideTitle(); + public @Nullable List> completeTitle(final @NotNull List> coll) { + return coll; } @Override - public void resetTitle(final @NotNull Player viewer) { - viewer.resetTitle(); + public void showTitle(final @NotNull Object viewer, final @NotNull List> title) { + for (final Consumer run : title) { + run.accept(viewer); + } + } + + @Override + public void clearTitle(final @NotNull Object viewer) { + try { + AUDIENCE_CLEAR_TITLE.invoke(viewer); + } catch (final Throwable ex) { + logError(ex, "Failed to clear title"); + } + } + + @Override + public void resetTitle(final @NotNull Object viewer) { + try { + AUDIENCE_RESET_TITLE.invoke(viewer); + } catch (final Throwable ex) { + logError(ex, "Failed to reset title"); + } + } + } + + static final class Sound extends PaperFacet implements Facet.Sound { + private static final Class NATIVE_SOUND = findClass(nativeClazz("sound.Sound")); + private static final Class NATIVE_SOUND_STOP = findClass(nativeClazz("sound.SoundStop")); + private static final Class NATIVE_SOUND_SOURCE_CLASS = findClass(nativeClazz("sound.Sound$Source")); + private static final Index SOURCE_TO_NATIVE_SOURCE = createNativeEnumMapping(net.kyori.adventure.sound.Sound.Source.class, NATIVE_SOUND_SOURCE_CLASS); + + private static final MethodHandle AUDIENCE_PLAY_SOUND = findMethod(NATIVE_AUDIENCE_CLASS, "playSound", void.class, NATIVE_SOUND, double.class, double.class, double.class); + private static final MethodHandle AUDIENCE_STOP_SOUND = findMethod(NATIVE_AUDIENCE_CLASS, "stopSound", void.class, NATIVE_SOUND_STOP); + private static final MethodHandle SOUND_SOUND = findStaticMethod(NATIVE_SOUND, "sound", NATIVE_SOUND, NATIVE_KEY_CLASS, NATIVE_SOUND_SOURCE_CLASS, float.class, float.class); + private static final MethodHandle SOUND_STOP_NAMED = findStaticMethod(NATIVE_SOUND_STOP, "named", NATIVE_SOUND_STOP, NATIVE_KEY_CLASS); + private static final MethodHandle SOUND_STOP_SOURCE = findStaticMethod(NATIVE_SOUND_STOP, "source", NATIVE_SOUND_STOP, NATIVE_SOUND_SOURCE_CLASS); + private static final MethodHandle SOUND_STOP_NAMED_ON_SOURCE = findStaticMethod(NATIVE_SOUND_SOURCE_CLASS, "namedOnSource", NATIVE_SOUND_STOP, NATIVE_KEY_CLASS, NATIVE_SOUND_SOURCE_CLASS); + + private static final Object SOUND_STOP_ALL; + + static { + Object soundStopAll = null; + try { + soundStopAll = findStaticMethod(NATIVE_SOUND_STOP, "all", NATIVE_SOUND_STOP).invoke(); + } catch (final Throwable ex) { + // no-op + } + + SOUND_STOP_ALL = soundStopAll; + } + + @Override + public boolean isSupported() { + return super.isSupported() && SOUND_SOUND != null && AUDIENCE_PLAY_SOUND != null && AUDIENCE_STOP_SOUND != null; + } + + @Override + public @Nullable Vector createPosition(final @NotNull Object viewer) { + if (viewer instanceof Entity) { + return ((Entity) viewer).getLocation().toVector(); + } + return null; + } + + @Override + public @NotNull Vector createPosition(final double x, final double y, final double z) { + return new Vector(x, y, z); + } + + @Override + public void playSound(final @NotNull Object viewer, final net.kyori.adventure.sound.@NotNull Sound sound, final @NotNull Vector position) { + try { + final Object nativeSound = SOUND_SOUND.invoke( + keyToNative(sound.name()), + SOURCE_TO_NATIVE_SOURCE.value(sound.source()), + sound.volume(), + sound.pitch() + ); + + AUDIENCE_PLAY_SOUND.invoke(viewer, nativeSound, position.getX(), position.getY(), position.getZ()); + } catch (final Throwable ex) { + logError(ex, "Failed to play sound {}", sound); + } + } + + @Override + public void stopSound(final @NotNull Object viewer, final @NotNull SoundStop sound) { + final boolean hasSound = sound.sound() != null; + final boolean hasSource = sound.source() != null; + + final Object stop; + try { + if (hasSound && hasSource) { + stop = SOUND_STOP_NAMED_ON_SOURCE.invoke(keyToNative(sound.sound()), SOURCE_TO_NATIVE_SOURCE.value(sound.source())); + } else if (hasSound) { + stop = SOUND_STOP_NAMED.invoke(keyToNative(sound.sound())); + } else if (hasSource) { + stop = SOUND_STOP_SOURCE.invoke(SOURCE_TO_NATIVE_SOURCE.value(sound.source())); + } else { + stop = SOUND_STOP_ALL; + } + + AUDIENCE_STOP_SOUND.invoke(viewer, stop); + } catch (final Throwable ex) { + logError(ex, "Failed to stop sound {}", sound); + } + } + } + + static final class EntitySound extends PaperFacet implements Facet.EntitySound { + @Override + public MethodHandle createForSelf(final Object viewer, final net.kyori.adventure.sound.@NotNull Sound sound) { + // TODO Auto-generated method stub + return null; + } + + @Override + public MethodHandle createForEmitter(final net.kyori.adventure.sound.@NotNull Sound sound, final @NotNull Emitter emitter) { + if (emitter instanceof BukkitEmitter) { + final Entity entity = ((BukkitEmitter) emitter).entity; + // AUDIENCE_PLAY_SOUND(viewer, entity, sound) + } + // TODO Auto-generated method stub + return null; + } + + @Override + public void playSound(final @NotNull Object viewer, final MethodHandle message) { + // TODO Auto-generated method stub + } + } + + static final class Book extends PaperFacet implements Facet.Book { + @Override + public @Nullable Object createBook(final @NotNull String title, final @NotNull String author, final @NotNull Iterable pages) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void openBook(final @NotNull Object viewer, final @NotNull Object book) { + // TODO Auto-generated method stub + + } + + } + + static final class BossBarBuilder extends PaperFacet implements Facet.BossBar.Builder { + @Override + public @NotNull PaperFacet.BossBar createBossBar(final @NotNull Collection viewer) { + return new PaperFacet.BossBar(viewer); + } + } + + static final class BossBar extends PaperFacet implements Facet.BossBar { + BossBar(final Collection viewer) { + + } + + @Override + public void addViewer(final @NotNull Object viewer) { + // TODO Auto-generated method stub + + } + + @Override + public void removeViewer(final @NotNull Object viewer) { + // TODO Auto-generated method stub + + } + + @Override + public boolean isEmpty() { + // TODO Auto-generated method stub + return false; + } + + @Override + public void close() { + // TODO Auto-generated method stub + } }