fix/chore: support signs in spigot

no need to use different implementations for spigot vs paper anymore
This commit is contained in:
Pierre Maurice Schwang
2025-10-23 19:56:38 +02:00
parent 8002d170f5
commit e2f6b0a6fe
4 changed files with 175 additions and 202 deletions

View File

@@ -46,6 +46,7 @@ import com.plotsquared.bukkit.listener.WorldEvents;
import com.plotsquared.bukkit.placeholder.PAPIPlaceholders; import com.plotsquared.bukkit.placeholder.PAPIPlaceholders;
import com.plotsquared.bukkit.placeholder.PlaceholderFormatter; import com.plotsquared.bukkit.placeholder.PlaceholderFormatter;
import com.plotsquared.bukkit.player.BukkitPlayerManager; import com.plotsquared.bukkit.player.BukkitPlayerManager;
import com.plotsquared.bukkit.schematic.StateWrapper;
import com.plotsquared.bukkit.util.BukkitUtil; import com.plotsquared.bukkit.util.BukkitUtil;
import com.plotsquared.bukkit.util.BukkitWorld; import com.plotsquared.bukkit.util.BukkitWorld;
import com.plotsquared.bukkit.util.SetGenCB; 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 // We create the injector after PlotSquared has been initialized, so that we have access
// to generated instances and settings // to generated instances and settings
this.injector = Guice this.injector = Guice

View File

@@ -19,18 +19,14 @@
package com.plotsquared.bukkit.schematic; package com.plotsquared.bukkit.schematic;
import com.plotsquared.bukkit.util.BukkitUtil; import com.plotsquared.bukkit.util.BukkitUtil;
import com.plotsquared.core.PlotSquared;
import com.sk89q.jnbt.CompoundTag; 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.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal @ApiStatus.Internal
public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Factory.NoopStateWrapper { public sealed interface StateWrapper permits StateWrapperSpigot {
StateWrapper INSTANCE = Factory.createStateWrapper(); StateWrapper INSTANCE = Factory.createStateWrapper();
@@ -47,40 +43,8 @@ public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Fa
@ApiStatus.Internal @ApiStatus.Internal
final class Factory { 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() { private static StateWrapper createStateWrapper() {
int[] serverVersion = PlotSquared.platform().serverVersion(); return new StateWrapperSpigot();
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;
}
} }
} }

View File

@@ -1,155 +0,0 @@
/*
* PlotSquared, a land and world management plugin for Minecraft.
* Copyright (C) IntellectualSites <https://intellectualsites.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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.
* <br />
* 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<Tag> 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());
}
}

View File

@@ -18,42 +18,66 @@
*/ */
package com.plotsquared.bukkit.schematic; package com.plotsquared.bukkit.schematic;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.plotsquared.core.util.ReflectionUtils; import com.plotsquared.core.util.ReflectionUtils;
import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.Tag;
import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.WorldEditPlugin;
import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter;
import com.sk89q.worldedit.bukkit.adapter.Refraction;
import com.sk89q.worldedit.extension.platform.NoCapablePlatformException; import com.sk89q.worldedit.extension.platform.NoCapablePlatformException;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockState; 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 org.checkerframework.checker.nullness.qual.NonNull;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType; import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method; 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; 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 FORCE_UPDATE_STATE = true;
private static final boolean UPDATE_TRIGGER_PHYSICS = false; private static final boolean UPDATE_TRIGGER_PHYSICS = false;
private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName(); private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName();
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName()); 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 BukkitImplAdapter ADAPTER = null;
private static Class<?> LIN_TAG_CLASS = null; private static Class<?> LIN_TAG_CLASS = null;
private static Class<?> JNBT_TAG_CLASS = null; private static Class<?> JNBT_TAG_CLASS = null;
private static Class<?> CRAFT_BLOCK_ENTITY_STATE_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 PAPERWEIGHT_ADAPTER_FROM_NATIVE = null;
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = 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_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 MethodHandle TO_LIN_TAG = null;
private static Object DYE_COLOR_BLACK = null;
public StateWrapperSpigot() { public StateWrapperSpigot() {
try { try {
findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz); 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); throw new RuntimeException("Failed to access required WorldEdit methods", e);
} }
try { 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_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.CraftBlockEntityState");
CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS);
CRAFT_BLOCK_ENTITY_STATE_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(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); 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); CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS);
} catch (Throwable e) { } catch (Throwable e) {
logger().error("Failed to update tile entity", e); LOGGER.error("Failed to update tile entity", e);
} }
return false; return false;
} }
public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable { 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() { private static void setSignContents(boolean front, SignSide side, BlockState blockState, CompoundTag data) throws Throwable {
return StateWrapperSpigot.LOGGER; final List<Tag> 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 private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class<?> craftBlockEntityStateClass) throws
NoSuchMethodException, IllegalAccessException, ClassNotFoundException { NoSuchMethodException, IllegalAccessException {
for (final Method method : craftBlockEntityStateClass.getMethods()) { for (final Method method : craftBlockEntityStateClass.getMethods()) {
if (method.getName().equals("loadData") && method.getParameterCount() == 1) { if (method.getName().equals("loadData") && method.getParameterCount() == 1) {
return LOOKUP.unreflect(method); return LOOKUP.unreflect(method);
@@ -197,7 +300,7 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape
} }
private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class<?> craftBlockEntityStateClass) throws private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class<?> craftBlockEntityStateClass) throws
NoSuchMethodException, IllegalAccessException, ClassNotFoundException { NoSuchMethodException, IllegalAccessException {
for (final Method method : craftBlockEntityStateClass.getMethods()) { for (final Method method : craftBlockEntityStateClass.getMethods()) {
if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 && if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 &&
method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) { 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()); 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)");
}
} }