diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/queue/BukkitQueueCoordinator.java b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/BukkitQueueCoordinator.java
index 8c2bfa50f..e833ef44a 100644
--- a/Bukkit/src/main/java/com/plotsquared/bukkit/queue/BukkitQueueCoordinator.java
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/BukkitQueueCoordinator.java
@@ -52,6 +52,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Objects;
import java.util.function.Consumer;
public class BukkitQueueCoordinator extends BasicQueueCoordinator {
@@ -210,8 +211,13 @@ public class BukkitQueueCoordinator extends BasicQueueCoordinator {
BaseBlock block = getWorld().getBlock(blockVector3).toBaseBlock(tag);
getWorld().setBlock(blockVector3, block, getSideEffectSet(SideEffectState.NONE));
} catch (WorldEditException ignored) {
- StateWrapper sw = new StateWrapper(tag);
- sw.restoreTag(getWorld().getName(), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ());
+ StateWrapper.INSTANCE.restore(
+ getWorld().getName(),
+ blockVector3.getX(),
+ blockVector3.getY(),
+ blockVector3.getZ(),
+ tag
+ );
}
});
}
@@ -295,9 +301,7 @@ public class BukkitQueueCoordinator extends BasicQueueCoordinator {
existing.setBlockData(blockData, false);
if (block.hasNbtData()) {
CompoundTag tag = block.getNbtData();
- StateWrapper sw = new StateWrapper(tag);
-
- sw.restoreTag(existing);
+ StateWrapper.INSTANCE.restore(existing, Objects.requireNonNull(tag));
}
}
}
diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/queue/LimitedRegionWrapperQueue.java b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/LimitedRegionWrapperQueue.java
index 46161d4c4..ef746520a 100644
--- a/Bukkit/src/main/java/com/plotsquared/bukkit/queue/LimitedRegionWrapperQueue.java
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/LimitedRegionWrapperQueue.java
@@ -34,6 +34,8 @@ import org.bukkit.entity.EntityType;
import org.bukkit.generator.LimitedRegion;
import org.checkerframework.checker.nullness.qual.NonNull;
+import java.util.Objects;
+
/**
* Wraps a {@link LimitedRegion} inside a {@link com.plotsquared.core.queue.QueueCoordinator} so it can be written to.
*
@@ -44,7 +46,6 @@ public class LimitedRegionWrapperQueue extends DelegateQueueCoordinator {
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + LimitedRegionWrapperQueue.class.getSimpleName());
private final LimitedRegion limitedRegion;
- private boolean useOtherRestoreTagMethod = false;
/**
* @since 6.9.0
@@ -64,20 +65,11 @@ public class LimitedRegionWrapperQueue extends DelegateQueueCoordinator {
boolean result = setBlock(x, y, z, id.toImmutableState());
if (result && id.hasNbtData()) {
CompoundTag tag = id.getNbtData();
- StateWrapper sw = new StateWrapper(tag);
try {
- if (useOtherRestoreTagMethod && getWorld() != null) {
- sw.restoreTag(getWorld().getName(), x, y, z);
- } else {
- sw.restoreTag(limitedRegion.getBlockState(x, y, z).getBlock());
- }
+ StateWrapper.INSTANCE.restore(limitedRegion.getBlockState(x, y, z).getBlock(), Objects.requireNonNull(tag));
} catch (IllegalArgumentException e) {
LOGGER.error("Error attempting to populate tile entity into the world at location {},{},{}", x, y, z, e);
return false;
- } catch (IllegalStateException e) {
- useOtherRestoreTagMethod = true;
- LOGGER.warn("IllegalStateException attempting to populate tile entity into the world at location {},{},{}. " +
- "Possibly on <=1.17.1, switching to secondary method.", x, y, z, e);
}
}
return result;
@@ -113,9 +105,8 @@ public class LimitedRegionWrapperQueue extends DelegateQueueCoordinator {
@Override
public boolean setTile(final int x, final int y, final int z, @NonNull final CompoundTag tag) {
- StateWrapper sw = new StateWrapper(tag);
try {
- return sw.restoreTag(limitedRegion.getBlockState(x, y, z).getBlock());
+ return StateWrapper.INSTANCE.restore(limitedRegion.getBlockState(x, y, z).getBlock(), tag);
} catch (IllegalArgumentException e) {
LOGGER.error("Error attempting to populate tile entity into the world at location {},{},{}", x, y, z, e);
return false;
diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/BukkitSchematicHandler.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/BukkitSchematicHandler.java
index 64d6d902f..e6a5a51bf 100644
--- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/BukkitSchematicHandler.java
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/BukkitSchematicHandler.java
@@ -27,6 +27,8 @@ import com.plotsquared.core.util.WorldUtil;
import com.sk89q.jnbt.CompoundTag;
import org.checkerframework.checker.nullness.qual.NonNull;
+import java.util.Objects;
+
/**
* Schematic Handler.
*/
@@ -39,8 +41,8 @@ public class BukkitSchematicHandler extends SchematicHandler {
}
@Override
- public boolean restoreTile(QueueCoordinator queue, CompoundTag ct, int x, int y, int z) {
- return new StateWrapper(ct).restoreTag(queue.getWorld().getName(), x, y, z);
+ public boolean restoreTile(QueueCoordinator queue, CompoundTag tag, int x, int y, int z) {
+ return StateWrapper.INSTANCE.restore(Objects.requireNonNull(queue.getWorld()).getName(), x, y, z, tag);
}
}
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 e52525423..29d54c085 100644
--- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java
@@ -18,367 +18,71 @@
*/
package com.plotsquared.bukkit.schematic;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
import com.plotsquared.bukkit.util.BukkitUtil;
import com.plotsquared.core.PlotSquared;
-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.extension.platform.NoCapablePlatformException;
import io.papermc.lib.PaperLib;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.bukkit.Bukkit;
-import org.bukkit.DyeColor;
import org.bukkit.World;
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 org.jetbrains.annotations.ApiStatus;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-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).
- *
- * 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
-public class StateWrapper {
+public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Factory.NoopStateWrapper {
- private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName());
+ StateWrapper INSTANCE = Factory.createStateWrapper();
- private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
- private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create();
- private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName();
+ boolean restore(final @NonNull Block block, final @NonNull CompoundTag data);
- private static final boolean FORCE_UPDATE_STATE = true;
- private static final boolean UPDATE_TRIGGER_PHYSICS = false;
- private static final boolean SUPPORTED = PlotSquared.platform().serverVersion()[1] > 20 ||
- (PlotSquared.platform().serverVersion()[1] == 20 && PlotSquared.platform().serverVersion()[2] >= 4);
- 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 boolean NOT_SUPPORTED_NOTIFIED = false;
- private static boolean FAILED_INITIALIZATION = false;
- 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 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 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 StateWrapper(CompoundTag tag) {
- this.tag = tag;
- }
-
- /**
- * Restore the TileEntity data to the given world at the given coordinates.
- *
- * @param worldName World name
- * @param x x position
- * @param y y position
- * @param z z position
- * @return true if successful
- */
- public boolean restoreTag(String worldName, int x, int y, int z) {
- World world = BukkitUtil.getWorld(worldName);
+ default boolean restore(final String worldName, final int x, final int y, final int z, final CompoundTag data) {
+ final World world = BukkitUtil.getWorld(worldName);
if (world == null) {
return false;
}
- return restoreTag(world.getBlockAt(x, y, z));
+ return this.restore(world.getBlockAt(x, y, z), data);
}
- /**
- * Restore the TileEntity data to the given block
- *
- * @param block Block to restore to
- * @return true if successful
- */
- public boolean restoreTag(@NonNull Block block) {
- if (this.tag == null || FAILED_INITIALIZATION) {
- return false;
- }
- if (!SUPPORTED) {
- if (!NOT_SUPPORTED_NOTIFIED) {
- NOT_SUPPORTED_NOTIFIED = true;
- LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, "Your server version is not supported. 1.20.4 or later is required");
- }
- return false;
- }
- if (ADAPTER == null) {
- try {
- findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz);
- ReflectionUtils.RefClass worldEditPluginRefClass = ReflectionUtils.getRefClass(WorldEditPlugin.class);
- WorldEditPlugin worldEditPlugin = (WorldEditPlugin) worldEditPluginRefClass
- .getMethod("getInstance")
- .of(null)
- .call();
- ADAPTER = (BukkitImplAdapter) worldEditPluginRefClass
- .getMethod("getBukkitImplAdapter")
- .of(worldEditPlugin)
- .call();
- PAPERWEIGHT_ADAPTER_FROM_NATIVE = findPaperweightAdapterFromNativeMethodHandle(
- ADAPTER.getClass(), LIN_TAG_CLASS, JNBT_TAG_CLASS
- );
- TO_LIN_TAG = findToLinTagMethodHandle(LIN_TAG_CLASS);
- } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) {
- LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, "Failed to access required WorldEdit methods", e);
- FAILED_INITIALIZATION = true;
- return false;
+ @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 {
- 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) {
- LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, "Failed to initialize required native method accessors", e);
- FAILED_INITIALIZATION = true;
+ 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;
}
- }
- try {
- final BlockState blockState = block.getState();
- if (!CRAFT_BLOCK_ENTITY_STATE_CLASS.isAssignableFrom(blockState.getClass())) {
- return false;
- }
- // get native tag
- Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke(
- ADAPTER,
- LIN_TAG_CLASS == null ? this.tag : TO_LIN_TAG.invoke(this.tag)
- );
- CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag);
- if (blockState instanceof Sign sign) {
- if (!PaperLib.isPaper()) {
- if (!PAPER_SIGN_NOTIFIED) {
- PAPER_SIGN_NOTIFIED = true;
- LOGGER.error("PlotSquared can't populate sign tile entities. To load sign content, use Paper.");
- }
- return false;
- }
- 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) {
- LOGGER.error("Failed to update tile entity", e);
- }
- return false;
- }
- /**
- * 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 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") == 1);
- }
- if (!initializeSignHack()) {
- return;
- }
- // TODO: Pre 1.21.5 sign texts are JSON in string tags... somehow support and fix that
- 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("");
- }
- // 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,
- GSON.toJsonTree(content)
- )
- );
- }
- }
- }
- private static boolean initializeSignHack() {
- if (FAILED_SIGN_INITIALIZATION) {
- return false;
- }
- if (KYORI_GSON_SERIALIZER != null) {
- return true; // already initialized
- }
- try {
- 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());
- return true;
- } catch (Throwable e) {
- FAILED_SIGN_INITIALIZATION = true;
- LOGGER.error("Failed to initialize sign-hack. Signs populated by schematics won't 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> linClass, Consumer> 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.
- *
- * 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 LOOKUP.findVirtual(
- Class.forName("org.enginehub.linbus.tree.ToLinTag"),
- "toLinTag",
- MethodType.methodType(linTagClass)
- );
- }
-
- /**
- * Find the method (handle) to convert from native (= WE/FAWE) NBT tags to minecraft NBT tags.
- *
- * Depending on the used version of WE/FAWE, this differs:
- *
- * - On WE/FAWE version pre LinBus introduction: {@code fromNative(org.sk89q.jnbt.Tag)}
- * - On WE versions post LinBus introduction: {@code fromNative(org.enginehub.linbus.tree.LinTag)}
- * - On FAWE versions post LinBus introduction: {@code fromNativeLin(org.enginehub.linbus.tree.LinTag)}
- *
- *
- * @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
- return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, jnbtTagClass));
- }
- try {
- // FAWE
- return lookup.findVirtual(adapterClass, "fromNativeLin", MethodType.methodType(Object.class, linTagClass));
- } catch (NoSuchMethodException e) {
- // WE
- return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, linTagClass));
- }
- }
-
- private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class> craftBlockEntityStateClass) throws
- NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
- for (final Method method : craftBlockEntityStateClass.getMethods()) {
- if (method.getName().equals("loadData") && method.getParameterCount() == 1) {
- return LOOKUP.unreflect(method);
- }
- }
- throw new NoSuchMethodException("Couldn't find #loadData(CompoundTag) in " + craftBlockEntityStateClass.getName());
- }
-
- private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class> craftBlockEntityStateClass) throws
- NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
- 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) {
- return LOOKUP.unreflect(method);
- }
- }
- throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName());
}
}
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
new file mode 100644
index 000000000..ab93a83cd
--- /dev/null
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java
@@ -0,0 +1,155 @@
+/*
+ * 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
new file mode 100644
index 000000000..264deabb8
--- /dev/null
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java
@@ -0,0 +1,210 @@
+/*
+ * 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.plotsquared.core.util.ReflectionUtils;
+import com.sk89q.jnbt.CompoundTag;
+import com.sk89q.worldedit.bukkit.WorldEditPlugin;
+import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter;
+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.block.Block;
+import org.bukkit.block.BlockState;
+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.Method;
+import java.util.function.Consumer;
+
+sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPaper1_21_5 {
+
+ 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 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 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 TO_LIN_TAG = null;
+
+ public StateWrapperSpigot() {
+ try {
+ findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz);
+ ReflectionUtils.RefClass worldEditPluginRefClass = ReflectionUtils.getRefClass(WorldEditPlugin.class);
+ WorldEditPlugin worldEditPlugin = (WorldEditPlugin) worldEditPluginRefClass
+ .getMethod("getInstance")
+ .of(null)
+ .call();
+ ADAPTER = (BukkitImplAdapter) worldEditPluginRefClass
+ .getMethod("getBukkitImplAdapter")
+ .of(worldEditPlugin)
+ .call();
+ PAPERWEIGHT_ADAPTER_FROM_NATIVE = findPaperweightAdapterFromNativeMethodHandle(
+ ADAPTER.getClass(), LIN_TAG_CLASS, JNBT_TAG_CLASS
+ );
+ TO_LIN_TAG = findToLinTagMethodHandle(LIN_TAG_CLASS);
+ } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) {
+ throw new RuntimeException("Failed to access required WorldEdit methods", e);
+ }
+ try {
+ 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) {
+ throw new RuntimeException("Failed to initialize required native method accessors", e);
+ }
+ }
+
+ @Override
+ public boolean restore(final @NonNull Block block, final @NonNull CompoundTag data) {
+ try {
+ final BlockState blockState = block.getState();
+ if (!CRAFT_BLOCK_ENTITY_STATE_CLASS.isAssignableFrom(blockState.getClass())) {
+ return false;
+ }
+ // get native tag
+ Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke(
+ ADAPTER,
+ LIN_TAG_CLASS == null ? data : TO_LIN_TAG.invoke(data)
+ );
+ // load block entity data
+ CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag);
+
+ postEntityStateLoad(blockState, data);
+
+ CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS);
+ } catch (Throwable e) {
+ logger().error("Failed to update tile entity", e);
+ }
+ return false;
+ }
+
+ public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable {
+
+ }
+
+ public Logger logger() {
+ return StateWrapperSpigot.LOGGER;
+ }
+
+ /**
+ * 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> linClass, Consumer> 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.
+ *
+ * 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 LOOKUP.findVirtual(
+ Class.forName("org.enginehub.linbus.tree.ToLinTag"),
+ "toLinTag",
+ MethodType.methodType(linTagClass)
+ );
+ }
+
+ /**
+ * Find the method (handle) to convert from native (= WE/FAWE) NBT tags to minecraft NBT tags.
+ *
+ * Depending on the used version of WE/FAWE, this differs:
+ *
+ * - On WE/FAWE version pre LinBus introduction: {@code fromNative(org.sk89q.jnbt.Tag)}
+ * - On WE versions post LinBus introduction: {@code fromNative(org.enginehub.linbus.tree.LinTag)}
+ * - On FAWE versions post LinBus introduction: {@code fromNativeLin(org.enginehub.linbus.tree.LinTag)}
+ *
+ *
+ * @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
+ return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, jnbtTagClass));
+ }
+ try {
+ // FAWE
+ return lookup.findVirtual(adapterClass, "fromNativeLin", MethodType.methodType(Object.class, linTagClass));
+ } catch (NoSuchMethodException e) {
+ // WE
+ return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, linTagClass));
+ }
+ }
+
+ private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class> craftBlockEntityStateClass) throws
+ NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
+ for (final Method method : craftBlockEntityStateClass.getMethods()) {
+ if (method.getName().equals("loadData") && method.getParameterCount() == 1) {
+ return LOOKUP.unreflect(method);
+ }
+ }
+ throw new NoSuchMethodException("Couldn't find #loadData(CompoundTag) in " + craftBlockEntityStateClass.getName());
+ }
+
+ private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class> craftBlockEntityStateClass) throws
+ NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
+ 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) {
+ return LOOKUP.unreflect(method);
+ }
+ }
+ throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName());
+ }
+
+}