feat: sign contents

This commit is contained in:
Pierre Maurice Schwang
2025-09-03 00:34:29 +02:00
parent 44e024df5e
commit 2dad3c2fe1

View File

@@ -18,17 +18,27 @@
*/ */
package com.plotsquared.bukkit.schematic; package com.plotsquared.bukkit.schematic;
import com.google.gson.Gson;
import com.plotsquared.bukkit.util.BukkitUtil; import com.plotsquared.bukkit.util.BukkitUtil;
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.ListTag;
import com.sk89q.jnbt.StringTag;
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.extension.platform.NoCapablePlatformException; import com.sk89q.worldedit.extension.platform.NoCapablePlatformException;
import io.papermc.lib.PaperLib;
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.DyeColor;
import org.bukkit.World; import org.bukkit.World;
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 org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@@ -36,12 +46,37 @@ 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.Method; import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
/**
* This class (attempts to) restore block tile entity data, after the underlying block state has been placed.
* This is used on chunk population (world generation) and in the platforms queue handler (as a fallback for WorldEdit placement).
* <br />
* This class relies heavily on reflective access, native minecraft methods and non-standardized WorldEdit / FAWE methods. It's
* extremely prone to breakage between versions (Minecraft and/or (FA)WE), but supports most if not all possible tile entities.
* Given the previous logic of this class was also non-reliable between version updates, and did only support a small subset of
* tile entities, it's a fair trade-off.
*/
@ApiStatus.Internal @ApiStatus.Internal
public class StateWrapper { public class StateWrapper {
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName());
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final Gson GSON = new Gson();
private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName();
private static final boolean FORCE_UPDATE_STATE = true;
private static final boolean UPDATE_TRIGGER_PHYSICS = false;
private static final String INITIALIZATION_ERROR_TEMPLATE = """
Failed to initialize StateWrapper: %s
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 boolean FAILED_INITIALIZATION = false; private static boolean FAILED_INITIALIZATION = false;
private static BukkitImplAdapter ADAPTER = null; private static BukkitImplAdapter ADAPTER = null;
private static Class<?> LIN_TAG_CLASS = null; private static Class<?> LIN_TAG_CLASS = null;
@@ -52,6 +87,13 @@ public class StateWrapper {
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null;
private static MethodHandle TO_LIN_TAG = null; private static MethodHandle TO_LIN_TAG = null;
// SIGN HACK
private static boolean PAPER_SIGN_NOTIFIED = false;
private static boolean FAILED_SIGN_INITIALIZATION = false;
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 CompoundTag tag; public CompoundTag tag;
public StateWrapper(CompoundTag tag) { public StateWrapper(CompoundTag tag) {
@@ -87,7 +129,7 @@ public class StateWrapper {
} }
if (ADAPTER == null) { if (ADAPTER == null) {
try { try {
initializeNbtCompoundClassType(); findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz);
ReflectionUtils.RefClass worldEditPluginRefClass = ReflectionUtils.getRefClass(WorldEditPlugin.class); ReflectionUtils.RefClass worldEditPluginRefClass = ReflectionUtils.getRefClass(WorldEditPlugin.class);
WorldEditPlugin worldEditPlugin = (WorldEditPlugin) worldEditPluginRefClass WorldEditPlugin worldEditPlugin = (WorldEditPlugin) worldEditPluginRefClass
.getMethod("getInstance") .getMethod("getInstance")
@@ -97,25 +139,21 @@ public class StateWrapper {
.getMethod("getBukkitImplAdapter") .getMethod("getBukkitImplAdapter")
.of(worldEditPlugin) .of(worldEditPlugin)
.call(); .call();
PAPERWEIGHT_ADAPTER_FROM_NATIVE = findPaperweightAdapterFromNativeMethodHandle(ADAPTER.getClass()); PAPERWEIGHT_ADAPTER_FROM_NATIVE = findPaperweightAdapterFromNativeMethodHandle(
ADAPTER.getClass(), LIN_TAG_CLASS, JNBT_TAG_CLASS
);
TO_LIN_TAG = findToLinTagMethodHandle(LIN_TAG_CLASS); TO_LIN_TAG = findToLinTagMethodHandle(LIN_TAG_CLASS);
} catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) { } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) {
LOGGER.error( LOGGER.error(INITIALIZATION_ERROR_TEMPLATE.formatted("Failed to access required WorldEdit methods"), e);
"Failed to access required WorldEdit methods, which are required to populate block data from " +
"schematics. Pasted blocks will not have their respective data", e
);
FAILED_INITIALIZATION = true; FAILED_INITIALIZATION = true;
return false; return false;
} }
try { try {
CRAFT_BLOCK_ENTITY_STATE_CLASS = findCraftBlockEntityStateClass(); 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) { } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
LOGGER.error( LOGGER.error(INITIALIZATION_ERROR_TEMPLATE.formatted("Failed to initialize required native method accessors"), e);
"Failed to initialize required method accessors for block state population.",
e
);
FAILED_INITIALIZATION = true; FAILED_INITIALIZATION = true;
return false; return false;
} }
@@ -131,75 +169,193 @@ public class StateWrapper {
LIN_TAG_CLASS == null ? this.tag : TO_LIN_TAG.invoke(this.tag) LIN_TAG_CLASS == null ? this.tag : TO_LIN_TAG.invoke(this.tag)
); );
CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag); CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag);
CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, true, false); if (blockState instanceof Sign sign) {
Object text;
if ((text = tag.getValue().get("front_text")) != null && text instanceof CompoundTag textTag) {
setSignTextHack(sign, textTag, true);
}
if ((text = tag.getValue().get("back_text")) != null && text instanceof CompoundTag textTag) {
setSignTextHack(sign, textTag, false);
}
}
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;
} }
private static void initializeNbtCompoundClassType() throws ClassNotFoundException { /**
try { * Set sign content on the bukkit tile entity. The server does not load sign content applied via the main logic
LIN_TAG_CLASS = Class.forName("org.enginehub.linbus.tree.LinTag"); * (CraftBlockEntity#load), as the SignEntity needs to have a valid ServerLevel assigned to it.
} catch (ClassNotFoundException e) { * That's not possible on worldgen; therefore, this hack has to be used additionally.
JNBT_TAG_CLASS = Class.forName("com.sk89q.jnbt.Tag"); * <br />
* Modern sign content (non-plain-text sign lines) require Paper.
*
* @param sign The sign to apply data onto.
* @param text The compound tag containing the data for the sign side ({@code front_text} / {@code back_text})
* @param front If the compound tag contains the data for the front side.
* @throws Throwable if something went wrong when reflectively updating the sign.
*/
private static void setSignTextHack(Sign sign, CompoundTag text, boolean front) throws Throwable {
final SignSide side = sign.getSide(front ? Side.FRONT : Side.BACK);
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") == 0x1b);
}
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);
if (line instanceof StringTag stringTag) {
//noinspection deprecation - Paper deprecatiom
side.setLine(i, stringTag.getValue());
continue;
}
if (line instanceof ListTag || line instanceof CompoundTag) {
if (!initializeSignHack()) {
continue;
}
final Object component = GSON_SERIALIZER_DESERIALIZE_TREE.invoke(
KYORI_GSON_SERIALIZER,
GSON.toJsonTree(line.getValue())
);
BUKKIT_SIGN_SIDE_LINE_SET.invoke(side, i, component);
}
}
} }
} }
private static MethodHandle findToLinTagMethodHandle(Class<?> LIN_TAG_CLASS) private static boolean initializeSignHack() {
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { if (FAILED_SIGN_INITIALIZATION) {
if (LIN_TAG_CLASS == null) { return false;
}
if (!PaperLib.isPaper()) {
if (!PAPER_SIGN_NOTIFIED) {
PAPER_SIGN_NOTIFIED = true;
LOGGER.error("Can't populate non-plain sign line. To load modern sign content, use Paper.");
}
return false;
}
try {
final String[] dontRelocate = new String[]{"net.kyo" + "ri.adventure.text.serializer.gson.GsonComponentSerializer"};
Class<?> gsonComponentSerializerClass = Class.forName(String.join("", dontRelocate));
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());
return true;
} catch (Throwable e) {
FAILED_SIGN_INITIALIZATION = true;
LOGGER.error("Failed to initialize sign-hack. Signs populated by schematics might not have their line contents.", e);
return false;
}
}
/**
* Initialize the used NBT tag class. For modern FAWE and WE that'll be Lin - for older ones JNBT.
*
* @throws ClassNotFoundException if neither can be found.
*/
private static void findNbtCompoundClassType(Consumer<Class<?>> linClass, Consumer<Class<?>> jnbtClass) throws
ClassNotFoundException {
try {
linClass.accept(Class.forName("org.enginehub.linbus.tree.LinTag"));
} catch (ClassNotFoundException e) {
jnbtClass.accept(Class.forName("com.sk89q.jnbt.Tag"));
}
}
/**
* Finds the {@code toLinTag} method on the {@code ToLinTag} interface, if lin-bus is available in the classpath.
* <br />
* Required to access the underlying lin tag of the used JNBT tag by PlotSquared, so it can be converted into the platforms
* native tag later.
*
* @param linTagClass {@code Tag} class of lin-bus, or {@code null} if not available.
* @return the MethodHandle for {@code toLinTag}, or {@code null} if lin-bus is not available in the classpath.
* @throws ClassNotFoundException if the {@code ToLinTag} class could not be found.
* @throws NoSuchMethodException if no {@code toLinTag} method exists.
* @throws IllegalAccessException shouldn't happen.
*/
private static MethodHandle findToLinTagMethodHandle(Class<?> linTagClass) throws ClassNotFoundException,
NoSuchMethodException, IllegalAccessException {
if (linTagClass == null) {
return null; return null;
} }
return MethodHandles.lookup().findVirtual( return LOOKUP.findVirtual(
Class.forName("org.enginehub.linbus.tree.ToLinTag"), Class.forName("org.enginehub.linbus.tree.ToLinTag"),
"toLinTag", "toLinTag",
MethodType.methodType(LIN_TAG_CLASS) MethodType.methodType(linTagClass)
); );
} }
private static MethodHandle findPaperweightAdapterFromNativeMethodHandle(Class<?> adapterClass) /**
throws IllegalAccessException, NoSuchMethodException { * Find the method (handle) to convert from native (= WE/FAWE) NBT tags to minecraft NBT tags.
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(adapterClass, MethodHandles.lookup()); * <br />
if (JNBT_TAG_CLASS != null) { * Depending on the used version of WE/FAWE, this differs:
* <ul>
* <li>On WE/FAWE version pre LinBus introduction: {@code fromNative(org.sk89q.jnbt.Tag)}</li>
* <li>On WE versions post LinBus introduction: {@code fromNative(org.enginehub.linbus.tree.LinTag)}</li>
* <li>On FAWE versions post LinBus introduction: {@code fromNativeLin(org.enginehub.linbus.tree.LinTag)}</li>
* </ul>
*
* @param adapterClass The bukkit adapter implementation class
* @param linTagClass The lin-bus {@code Tag} class, if existing - otherwise {@code null}
* @param jnbtTagClass The jnbt {@code Tag} class, if lin-bus was not found in classpath - otherwise {@code null}
* @return the method.
* @throws IllegalAccessException shouldn't happen as private lookup is used.
* @throws NoSuchMethodException if the method couldn't be found.
*/
private static MethodHandle findPaperweightAdapterFromNativeMethodHandle(
Class<?> adapterClass, Class<?> linTagClass, Class<?> jnbtTagClass
) throws IllegalAccessException, NoSuchMethodException {
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(adapterClass, LOOKUP);
if (jnbtTagClass != null) {
// usage of JNBT = identical method signatures for WE and FAWE // usage of JNBT = identical method signatures for WE and FAWE
return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, JNBT_TAG_CLASS)); return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, jnbtTagClass));
} }
try { try {
// FAWE // FAWE
return lookup.findVirtual(adapterClass, "fromNativeLin", MethodType.methodType(Object.class, LIN_TAG_CLASS)); return lookup.findVirtual(adapterClass, "fromNativeLin", MethodType.methodType(Object.class, linTagClass));
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
// WE // WE
return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, LIN_TAG_CLASS)); return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, linTagClass));
} }
} }
private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class<?> craftBlockEntityStateClass) private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class<?> craftBlockEntityStateClass) throws
throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
final Class<?> compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); final Class<?> compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); // TODO: obfuscation...
for (final Method method : craftBlockEntityStateClass.getMethods()) { for (final Method method : craftBlockEntityStateClass.getMethods()) {
if (method.getReturnType().equals(Void.TYPE) && method.getParameterCount() == 1 if (method
&& method.getParameterTypes()[0] == compoundTagClass) { .getReturnType()
return MethodHandles.lookup().unreflect(method); .equals(Void.TYPE) && method.getParameterCount() == 1 && method.getParameterTypes()[0] == compoundTagClass) {
return LOOKUP.unreflect(method);
} }
} }
throw new NoSuchMethodException("Couldn't find method for #loadData(CompoundTag) in " + compoundTagClass.getName()); throw new NoSuchMethodException("Couldn't find method for component loading in " + compoundTagClass.getName());
} }
private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class<?> craftBlockEntityStateClass) private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class<?> craftBlockEntityStateClass) throws
throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
final Class<?> compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag");
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) {
return MethodHandles.lookup().unreflect(method); return LOOKUP.unreflect(method);
} }
} }
throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + compoundTagClass.getName()); throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName());
}
private static Class<?> findCraftBlockEntityStateClass() throws ClassNotFoundException {
return Class.forName("org.bukkit.craftbukkit.block.CraftBlockEntityState");
} }
} }