From 8002d170f5e1500b9acf7f1c46e969fa073c560a Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Tue, 21 Oct 2025 00:03:53 +0200 Subject: [PATCH] chore: split logic per platform --- .../bukkit/queue/BukkitQueueCoordinator.java | 14 +- .../queue/LimitedRegionWrapperQueue.java | 17 +- .../schematic/BukkitSchematicHandler.java | 6 +- .../bukkit/schematic/StateWrapper.java | 372 ++---------------- .../schematic/StateWrapperPaper1_21_5.java | 155 ++++++++ .../bukkit/schematic/StateWrapperSpigot.java | 210 ++++++++++ 6 files changed, 420 insertions(+), 354 deletions(-) create mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java create mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java 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()); + } + +}