From e2f6b0a6fe106c7b16be1e7b8781db520e7ffd11 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 19:56:38 +0200 Subject: [PATCH] fix/chore: support signs in spigot no need to use different implementations for spigot vs paper anymore --- .../plotsquared/bukkit/BukkitPlatform.java | 13 ++ .../bukkit/schematic/StateWrapper.java | 40 +---- .../schematic/StateWrapperPaper1_21_5.java | 155 ---------------- .../bukkit/schematic/StateWrapperSpigot.java | 169 +++++++++++++++++- 4 files changed, 175 insertions(+), 202 deletions(-) delete mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java index d60bbe387..ae6c9067c 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java @@ -46,6 +46,7 @@ import com.plotsquared.bukkit.listener.WorldEvents; import com.plotsquared.bukkit.placeholder.PAPIPlaceholders; import com.plotsquared.bukkit.placeholder.PlaceholderFormatter; import com.plotsquared.bukkit.player.BukkitPlayerManager; +import com.plotsquared.bukkit.schematic.StateWrapper; import com.plotsquared.bukkit.util.BukkitUtil; import com.plotsquared.bukkit.util.BukkitWorld; import com.plotsquared.bukkit.util.SetGenCB; @@ -289,6 +290,18 @@ public final class BukkitPlatform extends JavaPlugin implements Listener, PlotPl } } + // Validate compatibility of StateWrapper with the current running server version + // Do this always, even if it's not required, to prevent running servers which fail to restore plot backups or + // inserting broken plot / road templates. + try { + var instance = StateWrapper.INSTANCE; + } catch (Exception e) { + LOGGER.error("Failed to initialize required classes for restoring tile entities. " + + "PlotSquared will disable itself to prevent possible damages.", e); + getServer().getPluginManager().disablePlugin(this); + return; + } + // We create the injector after PlotSquared has been initialized, so that we have access // to generated instances and settings this.injector = Guice diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java index 29d54c085..ffc7a31c5 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -19,18 +19,14 @@ package com.plotsquared.bukkit.schematic; import com.plotsquared.bukkit.util.BukkitUtil; -import com.plotsquared.core.PlotSquared; import com.sk89q.jnbt.CompoundTag; -import io.papermc.lib.PaperLib; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.bukkit.World; import org.bukkit.block.Block; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus; @ApiStatus.Internal -public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Factory.NoopStateWrapper { +public sealed interface StateWrapper permits StateWrapperSpigot { StateWrapper INSTANCE = Factory.createStateWrapper(); @@ -47,40 +43,8 @@ public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Fa @ApiStatus.Internal final class Factory { - private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); - - private static final String INITIALIZATION_ERROR_TEMPLATE = """ - Failed to initialize StateWrapper: {} - Block-/Tile-Entities, pasted by schematics for example, won't be updated with their respective block data. This affects things like sign text, banner patterns, skulls, etc. - Try updating your Server Software, PlotSquared and WorldEdit / FastAsyncWorldEdit first. If the issue persists, report it on the issue tracker. - """; - private static StateWrapper createStateWrapper() { - int[] serverVersion = PlotSquared.platform().serverVersion(); - if (PaperLib.isPaper() && (serverVersion[1] == 21 && serverVersion[2] >= 5) || serverVersion[1] > 21) { - try { - return new StateWrapperPaper1_21_5(); - } catch (Exception e) { - LOGGER.error("Failed to initialize Paper-specific state wrapper, falling back to Spigot", e); - } - } - try { - return new StateWrapperSpigot(); - } catch (Exception e) { - LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, StateWrapperSpigot.class.getSimpleName(), e); - } - return new NoopStateWrapper(); - } - - - @ApiStatus.Internal - static final class NoopStateWrapper implements StateWrapper { - - @Override - public boolean restore(final @NonNull Block block, final @NonNull CompoundTag data) { - return false; - } - + return new StateWrapperSpigot(); } } diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java deleted file mode 100644 index ab93a83cd..000000000 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * PlotSquared, a land and world management plugin for Minecraft. - * Copyright (C) IntellectualSites - * Copyright (C) IntellectualSites team and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.plotsquared.bukkit.schematic; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.Tag; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.bukkit.DyeColor; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.block.sign.Side; -import org.bukkit.block.sign.SignSide; -import org.checkerframework.checker.nullness.qual.NonNull; - -import java.lang.invoke.MethodHandle; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -final class StateWrapperPaper1_21_5 extends StateWrapperSpigot { - - private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperPaper1_21_5.class.getSimpleName()); - - private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); - private static Object KYORI_GSON_SERIALIZER = null; - private static MethodHandle GSON_SERIALIZER_DESERIALIZE_TREE = null; - private static MethodHandle BUKKIT_SIGN_SIDE_LINE_SET = null; - - public StateWrapperPaper1_21_5() { - super(); - try { - initializeSignHack(); - LOGGER.info("Using {} for block data population", StateWrapperPaper1_21_5.class.getSimpleName()); - } catch (Throwable e) { - throw new RuntimeException("Failed to initialize sign hack", e); - } - } - - @Override - public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable { - // signs need special handling during generation - if (blockState instanceof Sign sign) { - if (data.getValue().get("front_text") instanceof CompoundTag textTag) { - setSignTextHack(sign.getSide(Side.FRONT), textTag); - } - if (data.getValue().get("back_text") instanceof CompoundTag textTag) { - setSignTextHack(sign.getSide(Side.BACK), textTag); - } - } - } - - @Override - public Logger logger() { - return StateWrapperPaper1_21_5.LOGGER; - } - - /** - * Set sign content on the bukkit tile entity. The server does not load sign content applied via the main logic - * (CraftBlockEntity#load), as the SignEntity needs to have a valid ServerLevel assigned to it. - * That's not possible on worldgen; therefore, this hack has to be used additionally. - *
- * Modern sign content (non-plain-text sign lines) require Paper. - * - * @param side The sign side to apply data onto. - * @param text The compound tag containing the data for the sign side ({@code front_text} / {@code back_text}) - * @throws Throwable if something went wrong when reflectively updating the sign. - */ - private static void setSignTextHack(SignSide side, CompoundTag text) throws Throwable { - if (text.containsKey("color")) { - //noinspection UnstableApiUsage - side.setColor(DyeColor.legacyValueOf(text.getString("color").toUpperCase(Locale.ROOT))); - } - if (text.containsKey("has_glowing_text")) { - side.setGlowingText(text.getByte("has_glowing_text") == 1); - } - List lines = text.getList("messages"); - if (lines != null) { - for (int i = 0; i < Math.min(lines.size(), 3); i++) { - Tag line = lines.get(i); - Object content = line.getValue(); - // Minecraft uses mixed lists / arrays in their sign texts. One line can be a complex component, whereas - // the following line could simply be a string. Those simpler lines are represented as `{"": ""}` (only in - // SNBT those will be shown as a standard string). - if (line instanceof CompoundTag compoundTag && compoundTag.getValue().containsKey("")) { - content = compoundTag.getValue().get(""); - } - // absolute garbage way to try to handle stringified components (pre 1.21.5) - else if (content instanceof String contentAsString && (contentAsString.startsWith("{") || contentAsString.startsWith("["))) { - try { - content = JsonParser.parseString(contentAsString); - } catch (JsonSyntaxException e) { - // well, it wasn't JSON after all - } - } - - // serializes the line content from JNBT to Gson JSON objects, passes that to adventure and deserializes - // into an adventure component. - // pass all possible types of content into the deserializer (Strings, Compounds, Arrays), even though Strings - // could be set directly via Sign#setLine(int, String). The overhead is minimal, the serializer can handle - // strings - and we don't have to use the deprecated method. - BUKKIT_SIGN_SIDE_LINE_SET.invoke( - side, i, GSON_SERIALIZER_DESERIALIZE_TREE.invoke( - KYORI_GSON_SERIALIZER, - content instanceof JsonElement ? content : GSON.toJsonTree(content) - ) - ); - } - } - } - - private static void initializeSignHack() throws Throwable { - char[] dontObfuscate = new char[]{ - 'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e', '.', - 't', 'e', 'x', 't', '.', 's', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r', '.', 'g', 's', 'o', 'n', '.', - 'G', 's', 'o', 'n', 'C', 'o', 'm', 'p', 'o', 'n', 'e', 'n', 't', 'S', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r' - }; - Class gsonComponentSerializerClass = Class.forName(new String(dontObfuscate)); - KYORI_GSON_SERIALIZER = Arrays.stream(gsonComponentSerializerClass.getMethods()) - .filter(method -> method.getName().equals("gson")) - .findFirst() - .orElseThrow().invoke(null); - GSON_SERIALIZER_DESERIALIZE_TREE = LOOKUP.unreflect(Arrays - .stream(gsonComponentSerializerClass.getMethods()) - .filter(method -> method.getName().equals("deserializeFromTree") && method.getParameterCount() == 1) - .findFirst() - .orElseThrow()); - BUKKIT_SIGN_SIDE_LINE_SET = LOOKUP.unreflect(Arrays.stream(SignSide.class.getMethods()) - .filter(method -> method.getName().equals("line") && method.getParameterCount() == 2) - .findFirst() - .orElseThrow()); - } - -} diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 264deabb8..7530f63d8 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -18,42 +18,66 @@ */ package com.plotsquared.bukkit.schematic; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.plotsquared.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.adapter.Refraction; import com.sk89q.worldedit.extension.platform.NoCapablePlatformException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; +import org.bukkit.DyeColor; import org.bukkit.block.Block; import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; import org.checkerframework.checker.nullness.qual.NonNull; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.function.Consumer; -sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPaper1_21_5 { +final class StateWrapperSpigot implements StateWrapper { private static final boolean FORCE_UPDATE_STATE = true; private static final boolean UPDATE_TRIGGER_PHYSICS = false; private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName(); private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName()); - static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static BukkitImplAdapter ADAPTER = null; private static Class LIN_TAG_CLASS = null; private static Class JNBT_TAG_CLASS = null; private static Class CRAFT_BLOCK_ENTITY_STATE_CLASS = null; + private static Class MINECRAFT_CHAT_COMPONENT_CLASS = null; + private static Field CRAFT_SIGN_SIDE_SIGN_TEXT = null; private static MethodHandle PAPERWEIGHT_ADAPTER_FROM_NATIVE = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null; + private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = null; + private static MethodHandle SIGN_BLOCK_ENTITY_SET_TEXT = null; + private static MethodHandle MINECRAFT_DYE_COLOR_BY_NAME = null; + private static MethodHandle CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = null; + private static MethodHandle SIGN_TEXT_CONSTRUCTOR = null; private static MethodHandle TO_LIN_TAG = null; + private static Object DYE_COLOR_BLACK = null; + public StateWrapperSpigot() { try { findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz); @@ -74,10 +98,39 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape throw new RuntimeException("Failed to access required WorldEdit methods", e); } try { + final Class SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.SignText"); + MINECRAFT_CHAT_COMPONENT_CLASS = Class.forName(Refraction.pickName( + "net.minecraft.network.chat.Component", + "net.minecraft.network.chat.IChatBaseComponent" + )); + final Class MINECRAFT_DYE_COLOR_CLASS = Class.forName(Refraction.pickName( + "net.minecraft.world.item.DyeColor", + "net.minecraft.world.item.EnumColor" + )); + CRAFT_SIGN_SIDE_SIGN_TEXT = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.sign.CraftSignSide") + .getDeclaredField("signText"); + CRAFT_SIGN_SIDE_SIGN_TEXT.setAccessible(true); CRAFT_BLOCK_ENTITY_STATE_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.CraftBlockEntityState"); CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); CRAFT_BLOCK_ENTITY_STATE_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = findCraftBlockEntityStateSnapshotMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); + SIGN_BLOCK_ENTITY_SET_TEXT = findSignBlockEntitySetTextMethodHandle( + Class.forName(Refraction.pickName( + "net.minecraft.world.level.block.entity.SignBlockEntity", + "net.minecraft.world.level.block.entity.TileEntitySign" + )), + SIGN_TEXT_CLASS + ); + CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = findCraftChatMessageFromJsonOrStringMethodHandle( + MINECRAFT_CHAT_COMPONENT_CLASS); + SIGN_TEXT_CONSTRUCTOR = findSignTextConstructor( + SIGN_TEXT_CLASS, MINECRAFT_CHAT_COMPONENT_CLASS, MINECRAFT_DYE_COLOR_CLASS + ); + MINECRAFT_DYE_COLOR_BY_NAME = findDyeColorByNameMethodHandle(MINECRAFT_DYE_COLOR_CLASS); + DYE_COLOR_BLACK = Objects.requireNonNull( + MINECRAFT_DYE_COLOR_BY_NAME.invoke("black", null), "couldn't find black dye color" + ); + } catch (Throwable e) { throw new RuntimeException("Failed to initialize required native method accessors", e); } } @@ -101,17 +154,67 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS); } catch (Throwable e) { - logger().error("Failed to update tile entity", e); + LOGGER.error("Failed to update tile entity", e); } return false; } public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable { - + if (blockState instanceof Sign sign) { + if (data.getValue().get("front_text") instanceof CompoundTag textTag) { + setSignContents(true, sign.getSide(Side.FRONT), blockState, textTag); + } + if (data.getValue().get("back_text") instanceof CompoundTag textTag) { + setSignContents(false, sign.getSide(Side.BACK), blockState, textTag); + } + } } - public Logger logger() { - return StateWrapperSpigot.LOGGER; + private static void setSignContents(boolean front, SignSide side, BlockState blockState, CompoundTag data) throws Throwable { + final List messages = data.getList("messages"); + if (messages.size() != 4) { + if (data.containsKey("color")) { + //noinspection UnstableApiUsage + side.setColor(DyeColor.legacyValueOf(data.getString("color").toUpperCase(Locale.ROOT))); + } + side.setGlowingText(data.getByte("has_glowing_text") == 1); + return; + } + + final String color = data.getString("color"); + final boolean glowing = data.getByte("has_glowing_text") == 1; + final Object dyeColor = color.isEmpty() ? DYE_COLOR_BLACK : MINECRAFT_DYE_COLOR_BY_NAME.invoke( + color.equalsIgnoreCase("silver") ? "light_gray" : color, + DYE_COLOR_BLACK // fallback + ); + + Object[] components = new Object[messages.size()]; + for (int i = 0; i < components.length; i++) { + final Tag message = messages.get(i); + Object content; + // unwrap possible nested entry for mixed array types in later versions + if (message instanceof CompoundTag tag && tag.containsKey("")) { + content = tag.getString(""); + } else { + content = message.getValue(); + } + // if the value is not a string, make it one so it can be converted to a chat component + if (!(content instanceof String)) { + content = GSON.toJson(content); + LOGGER.info("GSON serialized to {}", content); + LOGGER.info("factory to {}", CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content)); + } + // chat.Component + components[i] = CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content); + } + final Object typedComponents = Array.newInstance(MINECRAFT_CHAT_COMPONENT_CLASS, components.length); + System.arraycopy(components, 0, typedComponents, 0, components.length); + final Object signText = SIGN_TEXT_CONSTRUCTOR.invoke(typedComponents, typedComponents, dyeColor, glowing); + + // blockState == org.bukkit.craftbukkit.block.CraftBlockEntityState + // --> net.minecraft.world.level.block.entity.SignBlockEntity + SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front); + CRAFT_SIGN_SIDE_SIGN_TEXT.set(side, signText); } /** @@ -187,7 +290,7 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape } private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class craftBlockEntityStateClass) throws - NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + NoSuchMethodException, IllegalAccessException { for (final Method method : craftBlockEntityStateClass.getMethods()) { if (method.getName().equals("loadData") && method.getParameterCount() == 1) { return LOOKUP.unreflect(method); @@ -197,7 +300,7 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape } private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class craftBlockEntityStateClass) throws - NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + NoSuchMethodException, IllegalAccessException { for (final Method method : craftBlockEntityStateClass.getMethods()) { if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 && method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) { @@ -207,4 +310,52 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName()); } + private static MethodHandle findCraftBlockEntityStateSnapshotMethodHandle(Class craftBlockEntityStateClass) throws + IllegalAccessException, NoSuchMethodException { + // doesn't seem to be obfuscated, but protected + final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(craftBlockEntityStateClass, LOOKUP); + return lookup.unreflect(craftBlockEntityStateClass.getDeclaredMethod("getSnapshot")); + } + + private static MethodHandle findSignBlockEntitySetTextMethodHandle(Class signBlockEntity, Class signText) throws + NoSuchMethodException, IllegalAccessException { + for (final Method method : signBlockEntity.getMethods()) { + if (method.getReturnType() == Boolean.TYPE && method.getParameterCount() == 2 + && method.getParameterTypes()[0] == signText && method.getParameterTypes()[1] == Boolean.TYPE) { + return LOOKUP.unreflect(method); + } + } + throw new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"); + } + + private static MethodHandle findCraftChatMessageFromJsonOrStringMethodHandle(Class minecraftChatComponent) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + // public static IChatBaseComponent fromJSONOrString(String message) + return LOOKUP.findStatic( + Class.forName(CRAFTBUKKIT_PACKAGE + ".util.CraftChatMessage"), + "fromJSONOrString", + MethodType.methodType(minecraftChatComponent, String.class) + ); + } + + private static MethodHandle findSignTextConstructor(Class signText, Class chatComponent, Class dyeColorEnum) throws + NoSuchMethodException, IllegalAccessException { + return LOOKUP.findConstructor( + signText, MethodType.methodType( + void.class, + chatComponent.arrayType(), chatComponent.arrayType(), dyeColorEnum, Boolean.TYPE + ) + ); + } + + private static MethodHandle findDyeColorByNameMethodHandle(Class dyeColorClass) throws + NoSuchMethodException, IllegalAccessException { + for (final Method method : dyeColorClass.getMethods()) { + if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 2 && method.getParameterTypes()[0] == String.class) { + return LOOKUP.unreflect(method); + } + } + throw new NoSuchMethodException("Couldn't lookup static DyeColor.byName(String, DyeColor)"); + } + }