From 308527e8227641f75e089eac1f6d793a7049a646 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Sun, 24 Aug 2025 02:06:51 +0200 Subject: [PATCH 01/15] chore: 1.17 is not supported anymore --- .../plotsquared/bukkit/generator/BukkitPlotGenerator.java | 7 +------ .../bukkit/generator/LegacyBlockStatePopulator.java | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/generator/BukkitPlotGenerator.java b/Bukkit/src/main/java/com/plotsquared/bukkit/generator/BukkitPlotGenerator.java index f713ed3506..7d92eee9e2 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/generator/BukkitPlotGenerator.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/generator/BukkitPlotGenerator.java @@ -90,12 +90,7 @@ public BukkitPlotGenerator( this.plotGenerator = generator; this.platformGenerator = this; this.populators = new ArrayList<>(); - int minecraftMinorVersion = PlotSquared.platform().serverVersion()[1]; - if (minecraftMinorVersion >= 17) { - this.populators.add(new BlockStatePopulator(this.plotGenerator)); - } else { - this.populators.add(new LegacyBlockStatePopulator(this.plotGenerator)); - } + this.populators.add(new BlockStatePopulator(this.plotGenerator)); this.full = true; this.useNewGenerationMethods = PlotSquared.platform().serverVersion()[1] >= 19; this.biomeProvider = new BukkitPlotBiomeProvider(); diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/generator/LegacyBlockStatePopulator.java b/Bukkit/src/main/java/com/plotsquared/bukkit/generator/LegacyBlockStatePopulator.java index 8b72781735..bb814f61d1 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/generator/LegacyBlockStatePopulator.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/generator/LegacyBlockStatePopulator.java @@ -36,6 +36,7 @@ import java.util.Random; +@Deprecated(since = "TODO") final class LegacyBlockStatePopulator extends BlockPopulator { private final IndependentPlotGenerator plotGenerator; From 590f442a0f634b9cdd66eac1dd8dcd592a517c94 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Sun, 24 Aug 2025 02:07:16 +0200 Subject: [PATCH 02/15] fix: block nbt population --- .../bukkit/schematic/StateWrapper.java | 275 +++++++++++++----- 1 file changed, 207 insertions(+), 68 deletions(-) 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 4ba2c1dbd1..25ee5ca093 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -21,6 +21,7 @@ import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import com.plotsquared.bukkit.util.BukkitUtil; +import com.plotsquared.core.PlotSquared; import com.sk89q.jnbt.ByteTag; import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.ListTag; @@ -36,6 +37,8 @@ import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.DyeColor; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.World; import org.bukkit.block.Banner; import org.bukkit.block.Block; @@ -44,25 +47,35 @@ import org.bukkit.block.Skull; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.ApiStatus; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.UUID; +import java.util.function.BiConsumer; +// TODO: somehow unbreak this class so it doesn't fuck up the whole schematic population system due to MC updates +@ApiStatus.Internal public class StateWrapper { - public CompoundTag tag; - - private boolean paperErrorTextureSent = false; private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); + private static final boolean MODERN_SIGNS = PlotSquared.platform().serverVersion()[1] > 19; + private final Registry PATTERN_TYPE_REGISTRY = Objects.requireNonNull(Bukkit.getRegistry(PatternType.class)); + + private static boolean paperErrorTextureSent = false; + + public CompoundTag tag; public StateWrapper(CompoundTag tag) { this.tag = tag; @@ -212,9 +225,9 @@ public boolean restoreTag(@NonNull Block block) { if (type == null) { continue; } - int count = itemComp.getByte("Count"); + int count = itemComp.containsKey("count") ? itemComp.getInt("count") : itemComp.getByte("Count"); int slot = itemComp.getByte("Slot"); - CompoundTag tag = (CompoundTag) itemComp.getValue().get("tag"); + CompoundTag tag = (CompoundTag) itemComp.getValue().get(itemComp.containsKey("tag") ? "tag" : "components"); BaseItemStack baseItemStack = new BaseItemStack(type, tag, count); ItemStack itemStack = BukkitAdapter.adapt(baseItemStack); inv.setItem(slot, itemStack); @@ -223,82 +236,62 @@ public boolean restoreTag(@NonNull Block block) { return true; } case "sign" -> { - if (state instanceof Sign sign) { - sign.setLine(0, jsonToColourCode(tag.getString("Text1"))); - sign.setLine(1, jsonToColourCode(tag.getString("Text2"))); - sign.setLine(2, jsonToColourCode(tag.getString("Text3"))); - sign.setLine(3, jsonToColourCode(tag.getString("Text4"))); - state.update(true); + if (state instanceof Sign sign && this.restoreSign(sign)) { + state.update(true, false); return true; } return false; } case "skull" -> { - if (state instanceof Skull skull) { - CompoundTag skullOwner = ((CompoundTag) this.tag.getValue().get("SkullOwner")); - if (skullOwner == null) { - return true; - } - String player = skullOwner.getString("Name"); - - if (player != null && !player.isEmpty()) { - try { - skull.setOwningPlayer(Bukkit.getOfflinePlayer(player)); - skull.update(true); - } catch (Exception e) { - e.printStackTrace(); - } - return true; - } - - final CompoundTag properties = (CompoundTag) skullOwner.getValue().get("Properties"); - if (properties == null) { - return false; - } - final ListTag textures = properties.getListTag("textures"); - if (textures.getValue().isEmpty()) { - return false; - } - final CompoundTag textureCompound = (CompoundTag) textures.getValue().get(0); - if (textureCompound == null) { - return false; - } - String textureValue = textureCompound.getString("Value"); - if (textureValue == null) { - return false; - } - if (!PaperLib.isPaper()) { - if (!paperErrorTextureSent) { - paperErrorTextureSent = true; - LOGGER.error("Failed to populate skull data in your road schematic - This is a Spigot limitation."); - } - return false; - } - final PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); - profile.setProperty(new ProfileProperty("textures", textureValue)); - skull.setPlayerProfile(profile); - skull.update(true); + if (state instanceof Skull skull && this.restoreSkull(skull)) { + skull.update(true, false); return true; - } return false; } case "banner" -> { if (state instanceof Banner banner) { - List patterns = this.tag.getListTag("Patterns").getValue(); - if (patterns == null || patterns.isEmpty()) { - return false; + List patterns; + // "old" format + if ((patterns = this.tag.getList("Patterns", CompoundTag.class)) != null && !patterns.isEmpty()) { + banner.setPatterns(patterns.stream().map(compoundTag -> { + DyeColor color = DyeColor.getByWoolData((byte) compoundTag.getInt("Color")); + final PatternType patternType = PATTERN_TYPE_REGISTRY.get(Objects.requireNonNull( + NamespacedKey.fromString(compoundTag.getString("Pattern")) + )); + if (color == null || patternType == null) { + return null; + } + return new Pattern(color, patternType); + }).filter(Objects::nonNull).toList()); + banner.update(true, false); + return true; } - banner.setPatterns(patterns.stream().map(t -> (CompoundTag) t).map(compoundTag -> { - DyeColor color = DyeColor.getByWoolData((byte) compoundTag.getInt("Color")); - PatternType patternType = PatternType.getByIdentifier(compoundTag.getString("Pattern")); - if (color == null || patternType == null) { - return null; + + // "new" format - since 1.21.3-ish + if ((patterns = this.tag.getList("patterns", CompoundTag.class)) != null && !patterns.isEmpty()) { + for (final CompoundTag patternTag : patterns) { + final String color = patternTag.getString("color"); + if (color.isEmpty()) { + continue; + } + final Tag pattern = patternTag.getValue().get("pattern"); + if (pattern instanceof StringTag patternString && !patternString.getValue().isEmpty()) { + final PatternType patternType = PATTERN_TYPE_REGISTRY.get(Objects.requireNonNull( + NamespacedKey.fromString(patternString.getValue()) + )); + if (patternType == null) { + continue; + } + banner.addPattern(new Pattern( + DyeColor.legacyValueOf(color.toUpperCase(Locale.ROOT)), + patternType + )); + } + // not supporting banner pattern definitions (no API available) } - return new Pattern(color, patternType); - }).filter(Objects::nonNull).toList()); - banner.update(true); - return true; + banner.update(true, false); + } } return false; } @@ -346,4 +339,150 @@ public Map serializeItem(ItemStack item) { return data; } + private boolean restoreSkull(Skull skull) { + boolean updated = false; + // can't support custom_name - Spigot does not provide any API for that + if (this.tag.containsKey("note_block_sound")) { + skull.setNoteBlockSound(NamespacedKey.fromString(this.tag.getString("note_block_sound"))); + updated = true; + } + // modern format - MC 1.21.3-ish + if (this.tag.containsKey("profile")) { + final Tag profile = this.tag.getValue().get("profile"); + if (profile instanceof StringTag stringTag) { + final String name = stringTag.getValue(); + if (name != null && !name.isEmpty()) { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(name)); + return true; + } + return updated; + } + if (profile instanceof CompoundTag compoundTag) { + final List properties = compoundTag.getList("properties"); + if (properties != null && !properties.isEmpty()) { + if (!PaperLib.isPaper()) { + if (!paperErrorTextureSent) { + paperErrorTextureSent = true; + LOGGER.error("Failed to populate schematic skull data - this is a Spigot limitation."); + } + return updated; + } + for (final Tag propertyTag : properties) { + if (!(propertyTag instanceof CompoundTag property)) { + continue; + } + if (!property.getString("name").equals("textures")) { + continue; + } + final String value = property.getString("value"); + final String signature = property.containsKey("signature") ? property.getString("signature") : null; + final PlayerProfile playerProfile = Bukkit.createProfile(UUID.randomUUID()); + playerProfile.setProperty(new ProfileProperty("textures", value, signature)); + skull.setPlayerProfile(playerProfile); + return true; + } + return updated; + } + final int[] id = compoundTag.getIntArray("id"); + if (id != null && id.length == 4) { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(new UUID( + (long) id[0] << 32 | (id[1] & 0xFFFFFFFFL), + (long) id[2] << 32 | (id[3] & 0xFFFFFFFFL) + ))); + return true; + } + final String name = compoundTag.getString("name"); + if (name != null && !name.isEmpty()) { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(name)); + return true; + } + } + } + + // "Old" MC format (idk when it got updated) + if (this.tag.getValue().get("SkullOwner") instanceof CompoundTag skullOwner) { + if (skullOwner.getValue().get("Name") instanceof StringTag ownerName && !ownerName.getValue().isEmpty()) { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(ownerName.getValue())); + skull.update(true); + return true; + } + if (skullOwner.getValue().get("Properties") instanceof CompoundTag properties) { + if (!paperErrorTextureSent) { + paperErrorTextureSent = true; + LOGGER.error("Failed to populate schematic skull data - this is a Spigot limitation."); + return updated; + } + final List textures = properties.getList("textures", CompoundTag.class); + if (textures.isEmpty()) { + return updated; + } + final String value = textures.get(0).getString("Value"); + if (!value.isEmpty()) { + final PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); + profile.setProperty(new ProfileProperty("textures", value)); + skull.setPlayerProfile(profile); + return true; + } + } + } + return updated; + } + + private boolean restoreSign(Sign sign) { + // "old" format (pre 1.20) + if (this.tag.containsKey("Text1") || this.tag.containsKey("Text2") + || this.tag.containsKey("Text3") || this.tag.containsKey("Text4")) { + if (!MODERN_SIGNS) { + sign.setLine(0, jsonToColourCode(tag.getString("Text1"))); + sign.setLine(1, jsonToColourCode(tag.getString("Text2"))); + sign.setLine(2, jsonToColourCode(tag.getString("Text3"))); + sign.setLine(3, jsonToColourCode(tag.getString("Text4"))); + sign.setGlowingText(tag.getByte("GlowingText") == 1); + if (tag.getValue().get("Color") instanceof StringTag colorTag && !colorTag.getValue().isEmpty()) { + sign.setColor(DyeColor.legacyValueOf(colorTag.getValue())); + } + return true; + } + SignSide front = sign.getSide(Side.FRONT); + front.setLine(0, jsonToColourCode(tag.getString("Text1"))); + front.setLine(1, jsonToColourCode(tag.getString("Text2"))); + front.setLine(2, jsonToColourCode(tag.getString("Text3"))); + front.setLine(3, jsonToColourCode(tag.getString("Text4"))); + front.setGlowingText(tag.getByte("GlowingText") == 1); + if (tag.getValue().get("Color") instanceof StringTag colorTag && !colorTag.getValue().isEmpty()) { + front.setColor(DyeColor.legacyValueOf(colorTag.getValue())); + } + return true; + } + + // "modern" format + if (this.tag.containsKey("front_text") || this.tag.containsKey("back_text") || this.tag.containsKey("is_waxed")) { + // the new format on older servers shouldn't be possible, I hope? + sign.setWaxed(this.tag.getByte("is_waxed") == 1); + BiConsumer sideSetter = (signSide, compoundTag) -> { + signSide.setGlowingText(compoundTag.getByte("has_glowing_text") == 1); + if (tag.getValue().get("color") instanceof StringTag colorTag && !colorTag.getValue().isEmpty()) { + signSide.setColor(DyeColor.legacyValueOf(colorTag.getValue())); + } + final List lines = compoundTag.getList("messages"); + for (int i = 0; i < Math.min(lines.size(), 4); i++) { + final Tag line = lines.get(i); + if (line instanceof StringTag stringLine) { + signSide.setLine(i, jsonToColourCode(stringLine.getValue())); + continue; + } + // TODO: how tf support list of components + components - utilize paper + adventure? + } + }; + if (this.tag.getValue().get("front_text") instanceof CompoundTag frontText) { + sideSetter.accept(sign.getSide(Side.FRONT), frontText); + } + if (this.tag.getValue().get("back_text") instanceof CompoundTag backText) { + sideSetter.accept(sign.getSide(Side.BACK), backText); + } + return true; + } + return false; + } + } From 1ad0e700a5ee49b4dc179a6260b51a3b2a3245ce Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Sun, 31 Aug 2025 01:27:28 +0200 Subject: [PATCH 03/15] feat: use NMS to populate tile entities --- Bukkit/build.gradle.kts | 2 +- .../bukkit/schematic/StateWrapper.java | 499 ++++-------------- 2 files changed, 109 insertions(+), 392 deletions(-) diff --git a/Bukkit/build.gradle.kts b/Bukkit/build.gradle.kts index 2f0f9d20ac..c66c3f775c 100644 --- a/Bukkit/build.gradle.kts +++ b/Bukkit/build.gradle.kts @@ -80,7 +80,7 @@ tasks.named("shadowJar") { relocate("net.kyori.examination", "com.plotsquared.core.configuration.examination") relocate("io.papermc.lib", "com.plotsquared.bukkit.paperlib") relocate("org.bstats", "com.plotsquared.metrics") - relocate("org.enginehub", "com.plotsquared.squirrelid") + relocate("org.enginehub.squirrelid", "com.plotsquared.squirrelid") relocate("org.khelekore.prtree", "com.plotsquared.prtree") relocate("com.google.inject", "com.plotsquared.google") relocate("org.aopalliance", "com.plotsquared.core.aopalliance") 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 25ee5ca093..d1e53c0e7f 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -18,62 +18,39 @@ */ package com.plotsquared.bukkit.schematic; -import com.destroystokyo.paper.profile.PlayerProfile; -import com.destroystokyo.paper.profile.ProfileProperty; import com.plotsquared.bukkit.util.BukkitUtil; -import com.plotsquared.core.PlotSquared; -import com.sk89q.jnbt.ByteTag; +import com.plotsquared.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.ListTag; -import com.sk89q.jnbt.ShortTag; -import com.sk89q.jnbt.StringTag; -import com.sk89q.jnbt.Tag; -import com.sk89q.worldedit.blocks.BaseItemStack; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldedit.world.item.ItemType; -import io.papermc.lib.PaperLib; +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.ChatColor; -import org.bukkit.DyeColor; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; import org.bukkit.World; -import org.bukkit.block.Banner; import org.bukkit.block.Block; -import org.bukkit.block.Container; -import org.bukkit.block.Sign; -import org.bukkit.block.Skull; -import org.bukkit.block.banner.Pattern; -import org.bukkit.block.banner.PatternType; -import org.bukkit.block.sign.Side; -import org.bukkit.block.sign.SignSide; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; +import org.bukkit.block.BlockState; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.UUID; -import java.util.function.BiConsumer; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; -// TODO: somehow unbreak this class so it doesn't fuck up the whole schematic population system due to MC updates @ApiStatus.Internal public class StateWrapper { private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); - private static final boolean MODERN_SIGNS = PlotSquared.platform().serverVersion()[1] > 19; - private final Registry PATTERN_TYPE_REGISTRY = Objects.requireNonNull(Bukkit.getRegistry(PatternType.class)); - private static boolean paperErrorTextureSent = 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; public CompoundTag tag; @@ -81,108 +58,6 @@ public StateWrapper(CompoundTag tag) { this.tag = tag; } - public static String jsonToColourCode(String str) { - str = str.replace("{\"extra\":", "").replace("],\"text\":\"\"}", "]") - .replace("[{\"color\":\"black\",\"text\":\"", "&0") - .replace("[{\"color\":\"dark_blue\",\"text\":\"", "&1") - .replace("[{\"color\":\"dark_green\",\"text\":\"", "&2") - .replace("[{\"color\":\"dark_aqua\",\"text\":\"", "&3") - .replace("[{\"color\":\"dark_red\",\"text\":\"", "&4") - .replace("[{\"color\":\"dark_purple\",\"text\":\"", "&5") - .replace("[{\"color\":\"gold\",\"text\":\"", "&6") - .replace("[{\"color\":\"gray\",\"text\":\"", "&7") - .replace("[{\"color\":\"dark_gray\",\"text\":\"", "&8") - .replace("[{\"color\":\"blue\",\"text\":\"", "&9") - .replace("[{\"color\":\"green\",\"text\":\"", "&a") - .replace("[{\"color\":\"aqua\",\"text\":\"", "&b") - .replace("[{\"color\":\"red\",\"text\":\"", "&c") - .replace("[{\"color\":\"light_purple\",\"text\":\"", "&d") - .replace("[{\"color\":\"yellow\",\"text\":\"", "&e") - .replace("[{\"color\":\"white\",\"text\":\"", "&f") - .replace("[{\"obfuscated\":true,\"text\":\"", "&k") - .replace("[{\"bold\":true,\"text\":\"", "&l") - .replace("[{\"strikethrough\":true,\"text\":\"", "&m") - .replace("[{\"underlined\":true,\"text\":\"", "&n") - .replace("[{\"italic\":true,\"text\":\"", "&o").replace("[{\"color\":\"black\",", "&0") - .replace("[{\"color\":\"dark_blue\",", "&1") - .replace("[{\"color\":\"dark_green\",", "&2") - .replace("[{\"color\":\"dark_aqua\",", "&3").replace("[{\"color\":\"dark_red\",", "&4") - .replace("[{\"color\":\"dark_purple\",", "&5").replace("[{\"color\":\"gold\",", "&6") - .replace("[{\"color\":\"gray\",", "&7").replace("[{\"color\":\"dark_gray\",", "&8") - .replace("[{\"color\":\"blue\",", "&9").replace("[{\"color\":\"green\",", "&a") - .replace("[{\"color\":\"aqua\",", "&b").replace("[{\"color\":\"red\",", "&c") - .replace("[{\"color\":\"light_purple\",", "&d").replace("[{\"color\":\"yellow\",", "&e") - .replace("[{\"color\":\"white\",", "&f").replace("[{\"obfuscated\":true,", "&k") - .replace("[{\"bold\":true,", "&l").replace("[{\"strikethrough\":true,", "&m") - .replace("[{\"underlined\":true,", "&n").replace("[{\"italic\":true,", "&o") - .replace("{\"color\":\"black\",\"text\":\"", "&0") - .replace("{\"color\":\"dark_blue\",\"text\":\"", "&1") - .replace("{\"color\":\"dark_green\",\"text\":\"", "&2") - .replace("{\"color\":\"dark_aqua\",\"text\":\"", "&3") - .replace("{\"color\":\"dark_red\",\"text\":\"", "&4") - .replace("{\"color\":\"dark_purple\",\"text\":\"", "&5") - .replace("{\"color\":\"gold\",\"text\":\"", "&6") - .replace("{\"color\":\"gray\",\"text\":\"", "&7") - .replace("{\"color\":\"dark_gray\",\"text\":\"", "&8") - .replace("{\"color\":\"blue\",\"text\":\"", "&9") - .replace("{\"color\":\"green\",\"text\":\"", "&a") - .replace("{\"color\":\"aqua\",\"text\":\"", "&b") - .replace("{\"color\":\"red\",\"text\":\"", "&c") - .replace("{\"color\":\"light_purple\",\"text\":\"", "&d") - .replace("{\"color\":\"yellow\",\"text\":\"", "&e") - .replace("{\"color\":\"white\",\"text\":\"", "&f") - .replace("{\"obfuscated\":true,\"text\":\"", "&k") - .replace("{\"bold\":true,\"text\":\"", "&l") - .replace("{\"strikethrough\":true,\"text\":\"", "&m") - .replace("{\"underlined\":true,\"text\":\"", "&n") - .replace("{\"italic\":true,\"text\":\"", "&o").replace("{\"color\":\"black\",", "&0") - .replace("{\"color\":\"dark_blue\",", "&1").replace("{\"color\":\"dark_green\",", "&2") - .replace("{\"color\":\"dark_aqua\",", "&3").replace("{\"color\":\"dark_red\",", "&4") - .replace("{\"color\":\"dark_purple\",", "&5").replace("{\"color\":\"gold\",", "&6") - .replace("{\"color\":\"gray\",", "&7").replace("{\"color\":\"dark_gray\",", "&8") - .replace("{\"color\":\"blue\",", "&9").replace("{\"color\":\"green\",", "&a") - .replace("{\"color\":\"aqua\",", "&b").replace("{\"color\":\"red\",", "&c") - .replace("{\"color\":\"light_purple\",", "&d").replace("{\"color\":\"yellow\",", "&e") - .replace("{\"color\":\"white\",", "&f").replace("{\"obfuscated\":true,", "&k") - .replace("{\"bold\":true,", "&l").replace("{\"strikethrough\":true,", "&m") - .replace("{\"underlined\":true,", "&n").replace("{\"italic\":true,", "&o") - .replace("\"color\":\"black\",\"text\":\"", "&0") - .replace("\"color\":\"dark_blue\",\"text\":\"", "&1") - .replace("\"color\":\"dark_green\",\"text\":\"", "&2") - .replace("\"color\":\"dark_aqua\",\"text\":\"", "&3") - .replace("\"color\":\"dark_red\",\"text\":\"", "&4") - .replace("\"color\":\"dark_purple\",\"text\":\"", "&5") - .replace("\"color\":\"gold\",\"text\":\"", "&6") - .replace("\"color\":\"gray\",\"text\":\"", "&7") - .replace("\"color\":\"dark_gray\",\"text\":\"", "&8") - .replace("\"color\":\"blue\",\"text\":\"", "&9") - .replace("\"color\":\"green\",\"text\":\"", "&a") - .replace("\"color\":\"aqua\",\"text\":\"", "&b") - .replace("\"color\":\"red\",\"text\":\"", "&c") - .replace("\"color\":\"light_purple\",\"text\":\"", "&d") - .replace("\"color\":\"yellow\",\"text\":\"", "&e") - .replace("\"color\":\"white\",\"text\":\"", "&f") - .replace("\"obfuscated\":true,\"text\":\"", "&k") - .replace("\"bold\":true,\"text\":\"", "&l") - .replace("\"strikethrough\":true,\"text\":\"", "&m") - .replace("\"underlined\":true,\"text\":\"", "&n") - .replace("\"italic\":true,\"text\":\"", "&o").replace("\"color\":\"black\",", "&0") - .replace("\"color\":\"dark_blue\",", "&1").replace("\"color\":\"dark_green\",", "&2") - .replace("\"color\":\"dark_aqua\",", "&3").replace("\"color\":\"dark_red\",", "&4") - .replace("\"color\":\"dark_purple\",", "&5").replace("\"color\":\"gold\",", "&6") - .replace("\"color\":\"gray\",", "&7").replace("\"color\":\"dark_gray\",", "&8") - .replace("\"color\":\"blue\",", "&9").replace("\"color\":\"green\",", "&a") - .replace("\"color\":\"aqua\",", "&b").replace("\"color\":\"red\",", "&c") - .replace("\"color\":\"light_purple\",", "&d").replace("\"color\":\"yellow\",", "&e") - .replace("\"color\":\"white\",", "&f").replace("\"obfuscated\":true,", "&k") - .replace("\"bold\":true,", "&l").replace("\"strikethrough\":true,", "&m") - .replace("\"underlined\":true,", "&n").replace("\"italic\":true,", "&o") - .replace("[{\"text\":\"", "&0").replace("{\"text\":\"", "&0").replace("\"},", "") - .replace("\"}]", "").replace("\"}", ""); - str = ChatColor.translateAlternateColorCodes('&', str); - return str; - } - /** * Restore the TileEntity data to the given world at the given coordinates. * @@ -206,283 +81,125 @@ public boolean restoreTag(String worldName, int x, int y, int z) { * @param block Block to restore to * @return true if successful */ - @SuppressWarnings("deprecation") // #setLine is needed for Spigot compatibility public boolean restoreTag(@NonNull Block block) { - if (this.tag == null) { + if (this.tag == null || FAILED_INITIALIZATION) { return false; } - org.bukkit.block.BlockState state = block.getState(); - switch (getId()) { - case "chest", "beacon", "brewingstand", "dispenser", "dropper", "furnace", "hopper", "shulkerbox" -> { - if (!(state instanceof Container container)) { - return false; - } - List itemsTag = this.tag.getListTag("Items").getValue(); - Inventory inv = container.getSnapshotInventory(); - for (Tag itemTag : itemsTag) { - CompoundTag itemComp = (CompoundTag) itemTag; - ItemType type = ItemType.REGISTRY.get(itemComp.getString("id").toLowerCase()); - if (type == null) { - continue; - } - int count = itemComp.containsKey("count") ? itemComp.getInt("count") : itemComp.getByte("Count"); - int slot = itemComp.getByte("Slot"); - CompoundTag tag = (CompoundTag) itemComp.getValue().get(itemComp.containsKey("tag") ? "tag" : "components"); - BaseItemStack baseItemStack = new BaseItemStack(type, tag, count); - ItemStack itemStack = BukkitAdapter.adapt(baseItemStack); - inv.setItem(slot, itemStack); - } - container.update(true, false); - return true; - } - case "sign" -> { - if (state instanceof Sign sign && this.restoreSign(sign)) { - state.update(true, false); - return true; - } + if (ADAPTER == null) { + try { + initializeNbtCompoundClassType(); + 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()); + TO_LIN_TAG = findToLinTagMethodHandle(LIN_TAG_CLASS); + } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) { + LOGGER.error( + "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; return false; } - case "skull" -> { - if (state instanceof Skull skull && this.restoreSkull(skull)) { - skull.update(true, false); - return true; - } + try { + CRAFT_BLOCK_ENTITY_STATE_CLASS = findCraftBlockEntityStateClass(); + 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( + "Failed to initialize required method accessors for block state population.", + e + ); + FAILED_INITIALIZATION = true; return false; } - case "banner" -> { - if (state instanceof Banner banner) { - List patterns; - // "old" format - if ((patterns = this.tag.getList("Patterns", CompoundTag.class)) != null && !patterns.isEmpty()) { - banner.setPatterns(patterns.stream().map(compoundTag -> { - DyeColor color = DyeColor.getByWoolData((byte) compoundTag.getInt("Color")); - final PatternType patternType = PATTERN_TYPE_REGISTRY.get(Objects.requireNonNull( - NamespacedKey.fromString(compoundTag.getString("Pattern")) - )); - if (color == null || patternType == null) { - return null; - } - return new Pattern(color, patternType); - }).filter(Objects::nonNull).toList()); - banner.update(true, false); - return true; - } - - // "new" format - since 1.21.3-ish - if ((patterns = this.tag.getList("patterns", CompoundTag.class)) != null && !patterns.isEmpty()) { - for (final CompoundTag patternTag : patterns) { - final String color = patternTag.getString("color"); - if (color.isEmpty()) { - continue; - } - final Tag pattern = patternTag.getValue().get("pattern"); - if (pattern instanceof StringTag patternString && !patternString.getValue().isEmpty()) { - final PatternType patternType = PATTERN_TYPE_REGISTRY.get(Objects.requireNonNull( - NamespacedKey.fromString(patternString.getValue()) - )); - if (patternType == null) { - continue; - } - banner.addPattern(new Pattern( - DyeColor.legacyValueOf(color.toUpperCase(Locale.ROOT)), - patternType - )); - } - // not supporting banner pattern definitions (no API available) - } - banner.update(true, 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); + CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, true, false); + } catch (Throwable e) { + LOGGER.error("Failed to update tile entity", e); } return false; } - public String getId() { - String tileid = this.tag.getString("id").toLowerCase(); - if (tileid.startsWith("minecraft:")) { - tileid = tileid.replace("minecraft:", ""); + private static void initializeNbtCompoundClassType() throws ClassNotFoundException { + try { + LIN_TAG_CLASS = Class.forName("org.enginehub.linbus.tree.LinTag"); + } catch (ClassNotFoundException e) { + JNBT_TAG_CLASS = Class.forName("com.sk89q.jnbt.Tag"); } - return tileid; } - public List serializeInventory(ItemStack[] items) { - List tags = new ArrayList<>(); - for (int i = 0; i < items.length; ++i) { - if (items[i] != null) { - Map tagData = serializeItem(items[i]); - tagData.put("Slot", new ByteTag((byte) i)); - tags.add(new CompoundTag(tagData)); - } - } - return tags; - } - - public Map serializeItem(ItemStack item) { - Map data = new HashMap<>(); - data.put("id", new StringTag(item.getType().name())); - data.put("Damage", new ShortTag(item.getDurability())); - data.put("Count", new ByteTag((byte) item.getAmount())); - if (!item.getEnchantments().isEmpty()) { - List enchantmentList = new ArrayList<>(); - for (Entry entry : item.getEnchantments().entrySet()) { - Map enchantment = new HashMap<>(); - enchantment.put("id", new StringTag(entry.getKey().toString())); - enchantment.put("lvl", new ShortTag(entry.getValue().shortValue())); - enchantmentList.add(new CompoundTag(enchantment)); - } - Map auxData = new HashMap<>(); - auxData.put("ench", new ListTag(CompoundTag.class, enchantmentList)); - data.put("tag", new CompoundTag(auxData)); + private static MethodHandle findToLinTagMethodHandle(Class LIN_TAG_CLASS) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + if (LIN_TAG_CLASS == null) { + return null; } - return data; + return MethodHandles.lookup().findVirtual( + Class.forName("org.enginehub.linbus.tree.ToLinTag"), + "toLinTag", + MethodType.methodType(LIN_TAG_CLASS) + ); } - private boolean restoreSkull(Skull skull) { - boolean updated = false; - // can't support custom_name - Spigot does not provide any API for that - if (this.tag.containsKey("note_block_sound")) { - skull.setNoteBlockSound(NamespacedKey.fromString(this.tag.getString("note_block_sound"))); - updated = true; + private static MethodHandle findPaperweightAdapterFromNativeMethodHandle(Class adapterClass) + throws IllegalAccessException, NoSuchMethodException { + final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(adapterClass, MethodHandles.lookup()); + if (JNBT_TAG_CLASS != null) { + // usage of JNBT = identical method signatures for WE and FAWE + return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, JNBT_TAG_CLASS)); } - // modern format - MC 1.21.3-ish - if (this.tag.containsKey("profile")) { - final Tag profile = this.tag.getValue().get("profile"); - if (profile instanceof StringTag stringTag) { - final String name = stringTag.getValue(); - if (name != null && !name.isEmpty()) { - skull.setOwningPlayer(Bukkit.getOfflinePlayer(name)); - return true; - } - return updated; - } - if (profile instanceof CompoundTag compoundTag) { - final List properties = compoundTag.getList("properties"); - if (properties != null && !properties.isEmpty()) { - if (!PaperLib.isPaper()) { - if (!paperErrorTextureSent) { - paperErrorTextureSent = true; - LOGGER.error("Failed to populate schematic skull data - this is a Spigot limitation."); - } - return updated; - } - for (final Tag propertyTag : properties) { - if (!(propertyTag instanceof CompoundTag property)) { - continue; - } - if (!property.getString("name").equals("textures")) { - continue; - } - final String value = property.getString("value"); - final String signature = property.containsKey("signature") ? property.getString("signature") : null; - final PlayerProfile playerProfile = Bukkit.createProfile(UUID.randomUUID()); - playerProfile.setProperty(new ProfileProperty("textures", value, signature)); - skull.setPlayerProfile(playerProfile); - return true; - } - return updated; - } - final int[] id = compoundTag.getIntArray("id"); - if (id != null && id.length == 4) { - skull.setOwningPlayer(Bukkit.getOfflinePlayer(new UUID( - (long) id[0] << 32 | (id[1] & 0xFFFFFFFFL), - (long) id[2] << 32 | (id[3] & 0xFFFFFFFFL) - ))); - return true; - } - final String name = compoundTag.getString("name"); - if (name != null && !name.isEmpty()) { - skull.setOwningPlayer(Bukkit.getOfflinePlayer(name)); - return true; - } - } + try { + // FAWE + return lookup.findVirtual(adapterClass, "fromNativeLin", MethodType.methodType(Object.class, LIN_TAG_CLASS)); + } catch (NoSuchMethodException e) { + // WE + return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, LIN_TAG_CLASS)); } + } - // "Old" MC format (idk when it got updated) - if (this.tag.getValue().get("SkullOwner") instanceof CompoundTag skullOwner) { - if (skullOwner.getValue().get("Name") instanceof StringTag ownerName && !ownerName.getValue().isEmpty()) { - skull.setOwningPlayer(Bukkit.getOfflinePlayer(ownerName.getValue())); - skull.update(true); - return true; - } - if (skullOwner.getValue().get("Properties") instanceof CompoundTag properties) { - if (!paperErrorTextureSent) { - paperErrorTextureSent = true; - LOGGER.error("Failed to populate schematic skull data - this is a Spigot limitation."); - return updated; - } - final List textures = properties.getList("textures", CompoundTag.class); - if (textures.isEmpty()) { - return updated; - } - final String value = textures.get(0).getString("Value"); - if (!value.isEmpty()) { - final PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); - profile.setProperty(new ProfileProperty("textures", value)); - skull.setPlayerProfile(profile); - return true; - } + private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class craftBlockEntityStateClass) + throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + final Class compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); + for (final Method method : craftBlockEntityStateClass.getMethods()) { + if (method.getReturnType().equals(Void.TYPE) && method.getParameterCount() == 1 + && method.getParameterTypes()[0] == compoundTagClass) { + return MethodHandles.lookup().unreflect(method); } } - return updated; + throw new NoSuchMethodException("Couldn't find method for #loadData(CompoundTag) in " + compoundTagClass.getName()); } - private boolean restoreSign(Sign sign) { - // "old" format (pre 1.20) - if (this.tag.containsKey("Text1") || this.tag.containsKey("Text2") - || this.tag.containsKey("Text3") || this.tag.containsKey("Text4")) { - if (!MODERN_SIGNS) { - sign.setLine(0, jsonToColourCode(tag.getString("Text1"))); - sign.setLine(1, jsonToColourCode(tag.getString("Text2"))); - sign.setLine(2, jsonToColourCode(tag.getString("Text3"))); - sign.setLine(3, jsonToColourCode(tag.getString("Text4"))); - sign.setGlowingText(tag.getByte("GlowingText") == 1); - if (tag.getValue().get("Color") instanceof StringTag colorTag && !colorTag.getValue().isEmpty()) { - sign.setColor(DyeColor.legacyValueOf(colorTag.getValue())); - } - return true; - } - SignSide front = sign.getSide(Side.FRONT); - front.setLine(0, jsonToColourCode(tag.getString("Text1"))); - front.setLine(1, jsonToColourCode(tag.getString("Text2"))); - front.setLine(2, jsonToColourCode(tag.getString("Text3"))); - front.setLine(3, jsonToColourCode(tag.getString("Text4"))); - front.setGlowingText(tag.getByte("GlowingText") == 1); - if (tag.getValue().get("Color") instanceof StringTag colorTag && !colorTag.getValue().isEmpty()) { - front.setColor(DyeColor.legacyValueOf(colorTag.getValue())); + private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class craftBlockEntityStateClass) + throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + final Class compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); + 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 MethodHandles.lookup().unreflect(method); } - return true; } + throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + compoundTagClass.getName()); + } - // "modern" format - if (this.tag.containsKey("front_text") || this.tag.containsKey("back_text") || this.tag.containsKey("is_waxed")) { - // the new format on older servers shouldn't be possible, I hope? - sign.setWaxed(this.tag.getByte("is_waxed") == 1); - BiConsumer sideSetter = (signSide, compoundTag) -> { - signSide.setGlowingText(compoundTag.getByte("has_glowing_text") == 1); - if (tag.getValue().get("color") instanceof StringTag colorTag && !colorTag.getValue().isEmpty()) { - signSide.setColor(DyeColor.legacyValueOf(colorTag.getValue())); - } - final List lines = compoundTag.getList("messages"); - for (int i = 0; i < Math.min(lines.size(), 4); i++) { - final Tag line = lines.get(i); - if (line instanceof StringTag stringLine) { - signSide.setLine(i, jsonToColourCode(stringLine.getValue())); - continue; - } - // TODO: how tf support list of components + components - utilize paper + adventure? - } - }; - if (this.tag.getValue().get("front_text") instanceof CompoundTag frontText) { - sideSetter.accept(sign.getSide(Side.FRONT), frontText); - } - if (this.tag.getValue().get("back_text") instanceof CompoundTag backText) { - sideSetter.accept(sign.getSide(Side.BACK), backText); - } - return true; - } - return false; + private static Class findCraftBlockEntityStateClass() throws ClassNotFoundException { + return Class.forName("org.bukkit.craftbukkit.block.CraftBlockEntityState"); } } From 313ea8b266d5ef484018f128f6373f83f56f1e45 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Wed, 3 Sep 2025 00:34:29 +0200 Subject: [PATCH 04/15] feat: sign contents --- .../bukkit/schematic/StateWrapper.java | 246 ++++++++++++++---- 1 file changed, 201 insertions(+), 45 deletions(-) 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 d1e53c0e7f..97c15e77bc 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -18,17 +18,27 @@ */ package com.plotsquared.bukkit.schematic; +import com.google.gson.Gson; import com.plotsquared.bukkit.util.BukkitUtil; import com.plotsquared.core.util.ReflectionUtils; 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.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; @@ -36,12 +46,37 @@ 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 { 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 BukkitImplAdapter ADAPTER = 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 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) { @@ -87,7 +129,7 @@ public boolean restoreTag(@NonNull Block block) { } if (ADAPTER == null) { try { - initializeNbtCompoundClassType(); + findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz); ReflectionUtils.RefClass worldEditPluginRefClass = ReflectionUtils.getRefClass(WorldEditPlugin.class); WorldEditPlugin worldEditPlugin = (WorldEditPlugin) worldEditPluginRefClass .getMethod("getInstance") @@ -97,25 +139,21 @@ public boolean restoreTag(@NonNull Block block) { .getMethod("getBukkitImplAdapter") .of(worldEditPlugin) .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); } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) { - LOGGER.error( - "Failed to access required WorldEdit methods, which are required to populate block data from " + - "schematics. Pasted blocks will not have their respective data", e - ); + LOGGER.error(INITIALIZATION_ERROR_TEMPLATE.formatted("Failed to access required WorldEdit methods"), e); FAILED_INITIALIZATION = true; return false; } 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_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { - LOGGER.error( - "Failed to initialize required method accessors for block state population.", - e - ); + LOGGER.error(INITIALIZATION_ERROR_TEMPLATE.formatted("Failed to initialize required native method accessors"), e); FAILED_INITIALIZATION = true; return false; } @@ -131,75 +169,193 @@ public boolean restoreTag(@NonNull Block block) { 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_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) { LOGGER.error("Failed to update tile entity", e); } return false; } - private static void initializeNbtCompoundClassType() throws ClassNotFoundException { + /** + * 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") == 0x1b); + } + List 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 boolean initializeSignHack() { + if (FAILED_SIGN_INITIALIZATION) { + 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 { - LIN_TAG_CLASS = Class.forName("org.enginehub.linbus.tree.LinTag"); + 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> linClass, Consumer> jnbtClass) throws + ClassNotFoundException { + try { + linClass.accept(Class.forName("org.enginehub.linbus.tree.LinTag")); } catch (ClassNotFoundException e) { - JNBT_TAG_CLASS = Class.forName("com.sk89q.jnbt.Tag"); + jnbtClass.accept(Class.forName("com.sk89q.jnbt.Tag")); } } - private static MethodHandle findToLinTagMethodHandle(Class LIN_TAG_CLASS) - throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { - if (LIN_TAG_CLASS == null) { + /** + * 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 MethodHandles.lookup().findVirtual( + return LOOKUP.findVirtual( Class.forName("org.enginehub.linbus.tree.ToLinTag"), "toLinTag", - MethodType.methodType(LIN_TAG_CLASS) + MethodType.methodType(linTagClass) ); } - private static MethodHandle findPaperweightAdapterFromNativeMethodHandle(Class adapterClass) - throws IllegalAccessException, NoSuchMethodException { - final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(adapterClass, MethodHandles.lookup()); - if (JNBT_TAG_CLASS != null) { + /** + * 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, JNBT_TAG_CLASS)); + return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, jnbtTagClass)); } try { // 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) { // 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) - throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { - final Class compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); + private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class craftBlockEntityStateClass) throws + NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + final Class compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); // TODO: obfuscation... for (final Method method : craftBlockEntityStateClass.getMethods()) { - if (method.getReturnType().equals(Void.TYPE) && method.getParameterCount() == 1 - && method.getParameterTypes()[0] == compoundTagClass) { - return MethodHandles.lookup().unreflect(method); + if (method + .getReturnType() + .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) - throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { - final Class compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); + 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 MethodHandles.lookup().unreflect(method); + 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 " + compoundTagClass.getName()); - } - - private static Class findCraftBlockEntityStateClass() throws ClassNotFoundException { - return Class.forName("org.bukkit.craftbukkit.block.CraftBlockEntityState"); + throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName()); } } From dc30408ed05a11c4df1354bf91f13aa9d5609c62 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Wed, 3 Sep 2025 23:00:55 +0200 Subject: [PATCH 05/15] fix: signs with component lines --- .../bukkit/schematic/NbtGsonSerializer.java | 62 ++++++++++++++++ .../bukkit/schematic/StateWrapper.java | 74 +++++++++++++------ 2 files changed, 114 insertions(+), 22 deletions(-) create mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java new file mode 100644 index 0000000000..734b50f994 --- /dev/null +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java @@ -0,0 +1,62 @@ +package com.plotsquared.bukkit.schematic; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.sk89q.jnbt.ByteArrayTag; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.IntArrayTag; +import com.sk89q.jnbt.ListTag; +import com.sk89q.jnbt.LongArrayTag; +import com.sk89q.jnbt.Tag; + +import java.lang.reflect.Type; + +final class NbtGsonSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(final Tag src, final Type typeOfSrc, final JsonSerializationContext context) { + if (src instanceof CompoundTag compoundTag) { + JsonObject object = new JsonObject(); + compoundTag.getValue().forEach((s, tag) -> object.add(s, context.serialize(tag))); + return object; + } + if (src instanceof ListTag listTag) { + JsonArray array = new JsonArray(); + listTag.getValue().forEach(tag -> array.add(context.serialize(tag))); + return array; + } + if (src instanceof ByteArrayTag byteArrayTag) { + JsonArray array = new JsonArray(); + for (final byte b : byteArrayTag.getValue()) { + array.add(b); + } + return array; + } + if (src instanceof IntArrayTag intArrayTag) { + JsonArray array = new JsonArray(); + for (final int i : intArrayTag.getValue()) { + array.add(i); + } + return array; + } + if (src instanceof LongArrayTag longArrayTag) { + JsonArray array = new JsonArray(); + for (final long l : longArrayTag.getValue()) { + array.add(l); + } + return array; + } + if (src.getValue() instanceof Number number) { + return new JsonPrimitive(number); + } + if (src.getValue() instanceof String string) { + return new JsonPrimitive(string); + } + throw new IllegalArgumentException("Don't know how to serialize " + src); + } + +} 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 97c15e77bc..ca150861be 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -19,7 +19,9 @@ 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.ListTag; @@ -66,17 +68,20 @@ public class StateWrapper { 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 Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); 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 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: %s + 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; @@ -127,6 +132,13 @@ 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); @@ -144,7 +156,7 @@ public boolean restoreTag(@NonNull Block block) { ); TO_LIN_TAG = findToLinTagMethodHandle(LIN_TAG_CLASS); } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) { - LOGGER.error(INITIALIZATION_ERROR_TEMPLATE.formatted("Failed to access required WorldEdit methods"), e); + LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, "Failed to access required WorldEdit methods", e); FAILED_INITIALIZATION = true; return false; } @@ -153,7 +165,7 @@ public boolean restoreTag(@NonNull Block block) { 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.formatted("Failed to initialize required native method accessors"), e); + LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, "Failed to initialize required native method accessors", e); FAILED_INITIALIZATION = true; return false; } @@ -204,7 +216,7 @@ private static void setSignTextHack(Sign sign, CompoundTag text, boolean front) side.setColor(DyeColor.legacyValueOf(text.getString("color").toUpperCase(Locale.ROOT))); } if (text.containsKey("has_glowing_text")) { - side.setGlowingText(text.getByte("has_glowing_text") == 0x1b); + side.setGlowingText(text.getByte("has_glowing_text") == 1); } List lines = text.getList("messages"); if (lines != null) { @@ -219,11 +231,22 @@ private static void setSignTextHack(Sign sign, CompoundTag text, boolean front) if (!initializeSignHack()) { continue; } - final Object component = GSON_SERIALIZER_DESERIALIZE_TREE.invoke( - KYORI_GSON_SERIALIZER, - GSON.toJsonTree(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). Adventure can't parse those, so we handle these lines as + // plaintext lines (can't contain any other extra data either way). + if (line instanceof CompoundTag compoundTag && compoundTag.getValue().containsKey("")) { + //noinspection deprecation - Paper deprecatiom + side.setLine(i, compoundTag.getString("")); + } + // serializes the line content from JNBT to Gson JSON objects, passes that to adventure and deserializes + // into an adventure component. + BUKKIT_SIGN_SIDE_LINE_SET.invoke( + side, i, GSON_SERIALIZER_DESERIALIZE_TREE.invoke( + KYORI_GSON_SERIALIZER, + GSON.toJsonTree(line.getValue()) + ) ); - BUKKIT_SIGN_SIDE_LINE_SET.invoke(side, i, component); } } } @@ -233,6 +256,9 @@ private static boolean initializeSignHack() { if (FAILED_SIGN_INITIALIZATION) { return false; } + if (KYORI_GSON_SERIALIZER != null) { + return true; // already initialized + } if (!PaperLib.isPaper()) { if (!PAPER_SIGN_NOTIFIED) { PAPER_SIGN_NOTIFIED = true; @@ -241,19 +267,26 @@ private static boolean initializeSignHack() { 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); + 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)); + LOGGER.info(gsonComponentSerializerClass); + 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()); + 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; @@ -336,15 +369,12 @@ private static MethodHandle findPaperweightAdapterFromNativeMethodHandle( private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class craftBlockEntityStateClass) throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException { - final Class compoundTagClass = Class.forName("net.minecraft.nbt.CompoundTag"); // TODO: obfuscation... for (final Method method : craftBlockEntityStateClass.getMethods()) { - if (method - .getReturnType() - .equals(Void.TYPE) && method.getParameterCount() == 1 && method.getParameterTypes()[0] == compoundTagClass) { + if (method.getName().equals("loadData") && method.getParameterCount() == 1) { return LOOKUP.unreflect(method); } } - throw new NoSuchMethodException("Couldn't find method for component loading in " + compoundTagClass.getName()); + throw new NoSuchMethodException("Couldn't find #loadData(CompoundTag) in " + craftBlockEntityStateClass.getName()); } private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class craftBlockEntityStateClass) throws From 2f1fe633af0b99036c233e2f57b442ca9bb6aaba Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Wed, 3 Sep 2025 23:10:06 +0200 Subject: [PATCH 06/15] fix: missing continue and license header --- .../bukkit/schematic/NbtGsonSerializer.java | 18 ++++++++++++++++++ .../bukkit/schematic/StateWrapper.java | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java index 734b50f994..8e29f3e6a1 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java @@ -1,3 +1,21 @@ +/* + * 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.JsonArray; 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 ca150861be..baf06631bd 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -238,6 +238,7 @@ private static void setSignTextHack(Sign sign, CompoundTag text, boolean front) if (line instanceof CompoundTag compoundTag && compoundTag.getValue().containsKey("")) { //noinspection deprecation - Paper deprecatiom side.setLine(i, compoundTag.getString("")); + continue; } // serializes the line content from JNBT to Gson JSON objects, passes that to adventure and deserializes // into an adventure component. @@ -273,7 +274,6 @@ private static boolean initializeSignHack() { '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)); - LOGGER.info(gsonComponentSerializerClass); KYORI_GSON_SERIALIZER = Arrays.stream(gsonComponentSerializerClass.getMethods()) .filter(method -> method.getName().equals("gson")) .findFirst() From 52e8ba6ddd5e0e050814ae64fc2db54511b52e1a Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Wed, 17 Sep 2025 22:03:49 +0200 Subject: [PATCH 07/15] chore: simplify sign logic --- .../bukkit/schematic/StateWrapper.java | 65 +++++++++---------- 1 file changed, 29 insertions(+), 36 deletions(-) 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 baf06631bd..e525254233 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -24,8 +24,6 @@ import com.plotsquared.core.PlotSquared; import com.plotsquared.core.util.ReflectionUtils; 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.adapter.BukkitImplAdapter; @@ -182,6 +180,13 @@ public boolean restoreTag(@NonNull Block block) { ); 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); @@ -218,37 +223,32 @@ private static void setSignTextHack(Sign sign, CompoundTag text, boolean front) 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); - 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; - } - // 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). Adventure can't parse those, so we handle these lines as - // plaintext lines (can't contain any other extra data either way). - if (line instanceof CompoundTag compoundTag && compoundTag.getValue().containsKey("")) { - //noinspection deprecation - Paper deprecatiom - side.setLine(i, compoundTag.getString("")); - continue; - } - // serializes the line content from JNBT to Gson JSON objects, passes that to adventure and deserializes - // into an adventure component. - BUKKIT_SIGN_SIDE_LINE_SET.invoke( - side, i, GSON_SERIALIZER_DESERIALIZE_TREE.invoke( - KYORI_GSON_SERIALIZER, - GSON.toJsonTree(line.getValue()) - ) - ); + 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) + ) + ); } } } @@ -260,13 +260,6 @@ private static boolean initializeSignHack() { if (KYORI_GSON_SERIALIZER != null) { return true; // already initialized } - 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 { char[] dontObfuscate = new char[]{ 'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e', '.', @@ -290,7 +283,7 @@ private static boolean initializeSignHack() { 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); + LOGGER.error("Failed to initialize sign-hack. Signs populated by schematics won't have their line contents.", e); return false; } } From 8002d170f5e1500b9acf7f1c46e969fa073c560a Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Tue, 21 Oct 2025 00:03:53 +0200 Subject: [PATCH 08/15] chore: split logic per platform --- .../bukkit/queue/BukkitQueueCoordinator.java | 14 +- .../queue/LimitedRegionWrapperQueue.java | 17 +- .../schematic/BukkitSchematicHandler.java | 6 +- .../bukkit/schematic/StateWrapper.java | 366 ++---------------- .../schematic/StateWrapperPaper1_21_5.java | 155 ++++++++ .../bukkit/schematic/StateWrapperSpigot.java | 210 ++++++++++ 6 files changed, 417 insertions(+), 351 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 8c2bfa50f4..e833ef44ae 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 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 boolean enqueue() { 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 @@ private void setWorldBlock(int x, int y, int z, @NonNull BaseBlock block, @NonNu 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 46161d4c46..ef746520a0 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.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 boolean setBlock(final int x, final int y, final int z, @NonNull final Ba 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 boolean setEntity(@NonNull final Entity entity) { @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 64d6d902f6..e6a5a51bf3 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.sk89q.jnbt.CompoundTag; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Objects; + /** * Schematic Handler. */ @@ -39,8 +41,8 @@ public BukkitSchematicHandler(final @NonNull WorldUtil worldUtil, @NonNull Progr } @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 e525254233..29d54c085a 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 { - - 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 GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); - 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 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 sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Factory.NoopStateWrapper { - public CompoundTag tag; + StateWrapper INSTANCE = Factory.createStateWrapper(); - public StateWrapper(CompoundTag tag) { - this.tag = tag; - } + boolean restore(final @NonNull Block block, final @NonNull CompoundTag data); - /** - * 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; - } - 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 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; - } + @ApiStatus.Internal + final class Factory { - /** - * 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 final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); - 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; - } - } + 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. + """; - /** - * 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")); + private static StateWrapper createStateWrapper() { + int[] serverVersion = PlotSquared.platform().serverVersion(); + if (PaperLib.isPaper() && (serverVersion[1] == 21 && serverVersion[2] >= 5) || serverVersion[1] > 21) { + try { + return new StateWrapperPaper1_21_5(); + } catch (Exception e) { + LOGGER.error("Failed to initialize Paper-specific state wrapper, falling back to Spigot", e); + } + } + try { + return new StateWrapperSpigot(); + } catch (Exception e) { + LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, StateWrapperSpigot.class.getSimpleName(), e); + } + return new NoopStateWrapper(); } - } - /** - * 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)); - } - } + @ApiStatus.Internal + static final class NoopStateWrapper implements StateWrapper { - 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); + @Override + public boolean restore(final @NonNull Block block, final @NonNull CompoundTag data) { + return false; } - } - 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 0000000000..ab93a83cdf --- /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 0000000000..264deabb80 --- /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()); + } + +} From e2f6b0a6fe106c7b16be1e7b8781db520e7ffd11 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 19:56:38 +0200 Subject: [PATCH 09/15] fix/chore: support signs in spigot no need to use different implementations for spigot vs paper anymore --- .../plotsquared/bukkit/BukkitPlatform.java | 13 ++ .../bukkit/schematic/StateWrapper.java | 40 +---- .../schematic/StateWrapperPaper1_21_5.java | 155 ---------------- .../bukkit/schematic/StateWrapperSpigot.java | 169 +++++++++++++++++- 4 files changed, 175 insertions(+), 202 deletions(-) delete mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java index d60bbe3879..ae6c9067cb 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java @@ -46,6 +46,7 @@ import com.plotsquared.bukkit.placeholder.PAPIPlaceholders; import com.plotsquared.bukkit.placeholder.PlaceholderFormatter; import com.plotsquared.bukkit.player.BukkitPlayerManager; +import com.plotsquared.bukkit.schematic.StateWrapper; import com.plotsquared.bukkit.util.BukkitUtil; import com.plotsquared.bukkit.util.BukkitWorld; import com.plotsquared.bukkit.util.SetGenCB; @@ -289,6 +290,18 @@ public void onEnable() { } } + // Validate compatibility of StateWrapper with the current running server version + // Do this always, even if it's not required, to prevent running servers which fail to restore plot backups or + // inserting broken plot / road templates. + try { + var instance = StateWrapper.INSTANCE; + } catch (Exception e) { + LOGGER.error("Failed to initialize required classes for restoring tile entities. " + + "PlotSquared will disable itself to prevent possible damages.", e); + getServer().getPluginManager().disablePlugin(this); + return; + } + // We create the injector after PlotSquared has been initialized, so that we have access // to generated instances and settings this.injector = Guice diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java index 29d54c085a..ffc7a31c5e 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -19,18 +19,14 @@ package com.plotsquared.bukkit.schematic; import com.plotsquared.bukkit.util.BukkitUtil; -import com.plotsquared.core.PlotSquared; import com.sk89q.jnbt.CompoundTag; -import io.papermc.lib.PaperLib; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.bukkit.World; import org.bukkit.block.Block; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus; @ApiStatus.Internal -public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Factory.NoopStateWrapper { +public sealed interface StateWrapper permits StateWrapperSpigot { StateWrapper INSTANCE = Factory.createStateWrapper(); @@ -47,40 +43,8 @@ default boolean restore(final String worldName, final int x, final int y, final @ApiStatus.Internal final class Factory { - private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); - - private static final String INITIALIZATION_ERROR_TEMPLATE = """ - Failed to initialize StateWrapper: {} - Block-/Tile-Entities, pasted by schematics for example, won't be updated with their respective block data. This affects things like sign text, banner patterns, skulls, etc. - Try updating your Server Software, PlotSquared and WorldEdit / FastAsyncWorldEdit first. If the issue persists, report it on the issue tracker. - """; - private static StateWrapper createStateWrapper() { - int[] serverVersion = PlotSquared.platform().serverVersion(); - if (PaperLib.isPaper() && (serverVersion[1] == 21 && serverVersion[2] >= 5) || serverVersion[1] > 21) { - try { - return new StateWrapperPaper1_21_5(); - } catch (Exception e) { - LOGGER.error("Failed to initialize Paper-specific state wrapper, falling back to Spigot", e); - } - } - try { - return new StateWrapperSpigot(); - } catch (Exception e) { - LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, StateWrapperSpigot.class.getSimpleName(), e); - } - return new NoopStateWrapper(); - } - - - @ApiStatus.Internal - static final class NoopStateWrapper implements StateWrapper { - - @Override - public boolean restore(final @NonNull Block block, final @NonNull CompoundTag data) { - return false; - } - + return new StateWrapperSpigot(); } } diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java deleted file mode 100644 index ab93a83cdf..0000000000 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperPaper1_21_5.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * PlotSquared, a land and world management plugin for Minecraft. - * Copyright (C) IntellectualSites - * Copyright (C) IntellectualSites team and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.plotsquared.bukkit.schematic; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.Tag; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.bukkit.DyeColor; -import org.bukkit.block.BlockState; -import org.bukkit.block.Sign; -import org.bukkit.block.sign.Side; -import org.bukkit.block.sign.SignSide; -import org.checkerframework.checker.nullness.qual.NonNull; - -import java.lang.invoke.MethodHandle; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -final class StateWrapperPaper1_21_5 extends StateWrapperSpigot { - - private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperPaper1_21_5.class.getSimpleName()); - - private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); - private static Object KYORI_GSON_SERIALIZER = null; - private static MethodHandle GSON_SERIALIZER_DESERIALIZE_TREE = null; - private static MethodHandle BUKKIT_SIGN_SIDE_LINE_SET = null; - - public StateWrapperPaper1_21_5() { - super(); - try { - initializeSignHack(); - LOGGER.info("Using {} for block data population", StateWrapperPaper1_21_5.class.getSimpleName()); - } catch (Throwable e) { - throw new RuntimeException("Failed to initialize sign hack", e); - } - } - - @Override - public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable { - // signs need special handling during generation - if (blockState instanceof Sign sign) { - if (data.getValue().get("front_text") instanceof CompoundTag textTag) { - setSignTextHack(sign.getSide(Side.FRONT), textTag); - } - if (data.getValue().get("back_text") instanceof CompoundTag textTag) { - setSignTextHack(sign.getSide(Side.BACK), textTag); - } - } - } - - @Override - public Logger logger() { - return StateWrapperPaper1_21_5.LOGGER; - } - - /** - * Set sign content on the bukkit tile entity. The server does not load sign content applied via the main logic - * (CraftBlockEntity#load), as the SignEntity needs to have a valid ServerLevel assigned to it. - * That's not possible on worldgen; therefore, this hack has to be used additionally. - *
- * Modern sign content (non-plain-text sign lines) require Paper. - * - * @param side The sign side to apply data onto. - * @param text The compound tag containing the data for the sign side ({@code front_text} / {@code back_text}) - * @throws Throwable if something went wrong when reflectively updating the sign. - */ - private static void setSignTextHack(SignSide side, CompoundTag text) throws Throwable { - if (text.containsKey("color")) { - //noinspection UnstableApiUsage - side.setColor(DyeColor.legacyValueOf(text.getString("color").toUpperCase(Locale.ROOT))); - } - if (text.containsKey("has_glowing_text")) { - side.setGlowingText(text.getByte("has_glowing_text") == 1); - } - List lines = text.getList("messages"); - if (lines != null) { - for (int i = 0; i < Math.min(lines.size(), 3); i++) { - Tag line = lines.get(i); - Object content = line.getValue(); - // Minecraft uses mixed lists / arrays in their sign texts. One line can be a complex component, whereas - // the following line could simply be a string. Those simpler lines are represented as `{"": ""}` (only in - // SNBT those will be shown as a standard string). - if (line instanceof CompoundTag compoundTag && compoundTag.getValue().containsKey("")) { - content = compoundTag.getValue().get(""); - } - // absolute garbage way to try to handle stringified components (pre 1.21.5) - else if (content instanceof String contentAsString && (contentAsString.startsWith("{") || contentAsString.startsWith("["))) { - try { - content = JsonParser.parseString(contentAsString); - } catch (JsonSyntaxException e) { - // well, it wasn't JSON after all - } - } - - // serializes the line content from JNBT to Gson JSON objects, passes that to adventure and deserializes - // into an adventure component. - // pass all possible types of content into the deserializer (Strings, Compounds, Arrays), even though Strings - // could be set directly via Sign#setLine(int, String). The overhead is minimal, the serializer can handle - // strings - and we don't have to use the deprecated method. - BUKKIT_SIGN_SIDE_LINE_SET.invoke( - side, i, GSON_SERIALIZER_DESERIALIZE_TREE.invoke( - KYORI_GSON_SERIALIZER, - content instanceof JsonElement ? content : GSON.toJsonTree(content) - ) - ); - } - } - } - - private static void initializeSignHack() throws Throwable { - char[] dontObfuscate = new char[]{ - 'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e', '.', - 't', 'e', 'x', 't', '.', 's', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r', '.', 'g', 's', 'o', 'n', '.', - 'G', 's', 'o', 'n', 'C', 'o', 'm', 'p', 'o', 'n', 'e', 'n', 't', 'S', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r' - }; - Class gsonComponentSerializerClass = Class.forName(new String(dontObfuscate)); - KYORI_GSON_SERIALIZER = Arrays.stream(gsonComponentSerializerClass.getMethods()) - .filter(method -> method.getName().equals("gson")) - .findFirst() - .orElseThrow().invoke(null); - GSON_SERIALIZER_DESERIALIZE_TREE = LOOKUP.unreflect(Arrays - .stream(gsonComponentSerializerClass.getMethods()) - .filter(method -> method.getName().equals("deserializeFromTree") && method.getParameterCount() == 1) - .findFirst() - .orElseThrow()); - BUKKIT_SIGN_SIDE_LINE_SET = LOOKUP.unreflect(Arrays.stream(SignSide.class.getMethods()) - .filter(method -> method.getName().equals("line") && method.getParameterCount() == 2) - .findFirst() - .orElseThrow()); - } - -} diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 264deabb80..7530f63d80 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -18,42 +18,66 @@ */ package com.plotsquared.bukkit.schematic; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.plotsquared.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.adapter.Refraction; import com.sk89q.worldedit.extension.platform.NoCapablePlatformException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; +import org.bukkit.DyeColor; import org.bukkit.block.Block; import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; import org.checkerframework.checker.nullness.qual.NonNull; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.function.Consumer; -sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPaper1_21_5 { +final class StateWrapperSpigot implements StateWrapper { private static final boolean FORCE_UPDATE_STATE = true; private static final boolean UPDATE_TRIGGER_PHYSICS = false; private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName(); private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName()); - static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static BukkitImplAdapter ADAPTER = null; private static Class LIN_TAG_CLASS = null; private static Class JNBT_TAG_CLASS = null; private static Class CRAFT_BLOCK_ENTITY_STATE_CLASS = null; + private static Class MINECRAFT_CHAT_COMPONENT_CLASS = null; + private static Field CRAFT_SIGN_SIDE_SIGN_TEXT = null; private static MethodHandle PAPERWEIGHT_ADAPTER_FROM_NATIVE = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null; + private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = null; + private static MethodHandle SIGN_BLOCK_ENTITY_SET_TEXT = null; + private static MethodHandle MINECRAFT_DYE_COLOR_BY_NAME = null; + private static MethodHandle CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = null; + private static MethodHandle SIGN_TEXT_CONSTRUCTOR = null; private static MethodHandle TO_LIN_TAG = null; + private static Object DYE_COLOR_BLACK = null; + public StateWrapperSpigot() { try { findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz); @@ -74,10 +98,39 @@ public StateWrapperSpigot() { throw new RuntimeException("Failed to access required WorldEdit methods", e); } try { + final Class SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.SignText"); + MINECRAFT_CHAT_COMPONENT_CLASS = Class.forName(Refraction.pickName( + "net.minecraft.network.chat.Component", + "net.minecraft.network.chat.IChatBaseComponent" + )); + final Class MINECRAFT_DYE_COLOR_CLASS = Class.forName(Refraction.pickName( + "net.minecraft.world.item.DyeColor", + "net.minecraft.world.item.EnumColor" + )); + CRAFT_SIGN_SIDE_SIGN_TEXT = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.sign.CraftSignSide") + .getDeclaredField("signText"); + CRAFT_SIGN_SIDE_SIGN_TEXT.setAccessible(true); CRAFT_BLOCK_ENTITY_STATE_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.CraftBlockEntityState"); CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); CRAFT_BLOCK_ENTITY_STATE_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = findCraftBlockEntityStateSnapshotMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); + SIGN_BLOCK_ENTITY_SET_TEXT = findSignBlockEntitySetTextMethodHandle( + Class.forName(Refraction.pickName( + "net.minecraft.world.level.block.entity.SignBlockEntity", + "net.minecraft.world.level.block.entity.TileEntitySign" + )), + SIGN_TEXT_CLASS + ); + CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = findCraftChatMessageFromJsonOrStringMethodHandle( + MINECRAFT_CHAT_COMPONENT_CLASS); + SIGN_TEXT_CONSTRUCTOR = findSignTextConstructor( + SIGN_TEXT_CLASS, MINECRAFT_CHAT_COMPONENT_CLASS, MINECRAFT_DYE_COLOR_CLASS + ); + MINECRAFT_DYE_COLOR_BY_NAME = findDyeColorByNameMethodHandle(MINECRAFT_DYE_COLOR_CLASS); + DYE_COLOR_BLACK = Objects.requireNonNull( + MINECRAFT_DYE_COLOR_BY_NAME.invoke("black", null), "couldn't find black dye color" + ); + } catch (Throwable e) { throw new RuntimeException("Failed to initialize required native method accessors", e); } } @@ -101,17 +154,67 @@ public boolean restore(final @NonNull Block block, final @NonNull CompoundTag da CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS); } catch (Throwable e) { - logger().error("Failed to update tile entity", e); + LOGGER.error("Failed to update tile entity", e); } return false; } public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable { - + if (blockState instanceof Sign sign) { + if (data.getValue().get("front_text") instanceof CompoundTag textTag) { + setSignContents(true, sign.getSide(Side.FRONT), blockState, textTag); + } + if (data.getValue().get("back_text") instanceof CompoundTag textTag) { + setSignContents(false, sign.getSide(Side.BACK), blockState, textTag); + } + } } - public Logger logger() { - return StateWrapperSpigot.LOGGER; + private static void setSignContents(boolean front, SignSide side, BlockState blockState, CompoundTag data) throws Throwable { + final List messages = data.getList("messages"); + if (messages.size() != 4) { + if (data.containsKey("color")) { + //noinspection UnstableApiUsage + side.setColor(DyeColor.legacyValueOf(data.getString("color").toUpperCase(Locale.ROOT))); + } + side.setGlowingText(data.getByte("has_glowing_text") == 1); + return; + } + + final String color = data.getString("color"); + final boolean glowing = data.getByte("has_glowing_text") == 1; + final Object dyeColor = color.isEmpty() ? DYE_COLOR_BLACK : MINECRAFT_DYE_COLOR_BY_NAME.invoke( + color.equalsIgnoreCase("silver") ? "light_gray" : color, + DYE_COLOR_BLACK // fallback + ); + + Object[] components = new Object[messages.size()]; + for (int i = 0; i < components.length; i++) { + final Tag message = messages.get(i); + Object content; + // unwrap possible nested entry for mixed array types in later versions + if (message instanceof CompoundTag tag && tag.containsKey("")) { + content = tag.getString(""); + } else { + content = message.getValue(); + } + // if the value is not a string, make it one so it can be converted to a chat component + if (!(content instanceof String)) { + content = GSON.toJson(content); + LOGGER.info("GSON serialized to {}", content); + LOGGER.info("factory to {}", CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content)); + } + // chat.Component + components[i] = CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content); + } + final Object typedComponents = Array.newInstance(MINECRAFT_CHAT_COMPONENT_CLASS, components.length); + System.arraycopy(components, 0, typedComponents, 0, components.length); + final Object signText = SIGN_TEXT_CONSTRUCTOR.invoke(typedComponents, typedComponents, dyeColor, glowing); + + // blockState == org.bukkit.craftbukkit.block.CraftBlockEntityState + // --> net.minecraft.world.level.block.entity.SignBlockEntity + SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front); + CRAFT_SIGN_SIDE_SIGN_TEXT.set(side, signText); } /** @@ -187,7 +290,7 @@ private static MethodHandle findPaperweightAdapterFromNativeMethodHandle( } private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class craftBlockEntityStateClass) throws - NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + NoSuchMethodException, IllegalAccessException { for (final Method method : craftBlockEntityStateClass.getMethods()) { if (method.getName().equals("loadData") && method.getParameterCount() == 1) { return LOOKUP.unreflect(method); @@ -197,7 +300,7 @@ private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class< } private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class craftBlockEntityStateClass) throws - NoSuchMethodException, IllegalAccessException, ClassNotFoundException { + NoSuchMethodException, IllegalAccessException { for (final Method method : craftBlockEntityStateClass.getMethods()) { if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 && method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) { @@ -207,4 +310,52 @@ private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName()); } + private static MethodHandle findCraftBlockEntityStateSnapshotMethodHandle(Class craftBlockEntityStateClass) throws + IllegalAccessException, NoSuchMethodException { + // doesn't seem to be obfuscated, but protected + final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(craftBlockEntityStateClass, LOOKUP); + return lookup.unreflect(craftBlockEntityStateClass.getDeclaredMethod("getSnapshot")); + } + + private static MethodHandle findSignBlockEntitySetTextMethodHandle(Class signBlockEntity, Class signText) throws + NoSuchMethodException, IllegalAccessException { + for (final Method method : signBlockEntity.getMethods()) { + if (method.getReturnType() == Boolean.TYPE && method.getParameterCount() == 2 + && method.getParameterTypes()[0] == signText && method.getParameterTypes()[1] == Boolean.TYPE) { + return LOOKUP.unreflect(method); + } + } + throw new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"); + } + + private static MethodHandle findCraftChatMessageFromJsonOrStringMethodHandle(Class minecraftChatComponent) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + // public static IChatBaseComponent fromJSONOrString(String message) + return LOOKUP.findStatic( + Class.forName(CRAFTBUKKIT_PACKAGE + ".util.CraftChatMessage"), + "fromJSONOrString", + MethodType.methodType(minecraftChatComponent, String.class) + ); + } + + private static MethodHandle findSignTextConstructor(Class signText, Class chatComponent, Class dyeColorEnum) throws + NoSuchMethodException, IllegalAccessException { + return LOOKUP.findConstructor( + signText, MethodType.methodType( + void.class, + chatComponent.arrayType(), chatComponent.arrayType(), dyeColorEnum, Boolean.TYPE + ) + ); + } + + private static MethodHandle findDyeColorByNameMethodHandle(Class dyeColorClass) throws + NoSuchMethodException, IllegalAccessException { + for (final Method method : dyeColorClass.getMethods()) { + if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 2 && method.getParameterTypes()[0] == String.class) { + return LOOKUP.unreflect(method); + } + } + throw new NoSuchMethodException("Couldn't lookup static DyeColor.byName(String, DyeColor)"); + } + } From e60a016164dfc87c65427f054b7170e44d8cac78 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 20:05:54 +0200 Subject: [PATCH 10/15] chore: only support Lin latest 1.19.4 FAWE has support for it --- .../bukkit/schematic/StateWrapperSpigot.java | 56 +++++-------------- 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 7530f63d80..02ef2576c6 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -48,7 +48,6 @@ import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.function.Consumer; final class StateWrapperSpigot implements StateWrapper { @@ -57,12 +56,12 @@ final class StateWrapperSpigot implements StateWrapper { private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName(); private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName()); - private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping() + .registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static BukkitImplAdapter ADAPTER = null; private static Class LIN_TAG_CLASS = null; - private static Class JNBT_TAG_CLASS = null; private static Class CRAFT_BLOCK_ENTITY_STATE_CLASS = null; private static Class MINECRAFT_CHAT_COMPONENT_CLASS = null; private static Field CRAFT_SIGN_SIDE_SIGN_TEXT = null; @@ -80,7 +79,6 @@ final class StateWrapperSpigot implements StateWrapper { 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") @@ -90,12 +88,11 @@ public StateWrapperSpigot() { .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); + LIN_TAG_CLASS = Class.forName("org.enginehub.linbus.tree.LinTag"); // provided WE / FAWE version is too old + PAPERWEIGHT_ADAPTER_FROM_NATIVE = findPaperweightAdapterFromNativeMethodHandle(ADAPTER.getClass()); + TO_LIN_TAG = findToLinTagMethodHandle(); } catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException | NoCapablePlatformException e) { - throw new RuntimeException("Failed to access required WorldEdit methods", e); + throw new RuntimeException("Failed to access required WorldEdit classes or methods", e); } try { final Class SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.SignText"); @@ -145,7 +142,7 @@ public boolean restore(final @NonNull Block block, final @NonNull CompoundTag da // get native tag Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke( ADAPTER, - LIN_TAG_CLASS == null ? data : TO_LIN_TAG.invoke(data) + TO_LIN_TAG.invoke(data) ); // load block entity data CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag); @@ -212,46 +209,28 @@ private static void setSignContents(boolean front, SignSide side, BlockState blo final Object signText = SIGN_TEXT_CONSTRUCTOR.invoke(typedComponents, typedComponents, dyeColor, glowing); // blockState == org.bukkit.craftbukkit.block.CraftBlockEntityState - // --> net.minecraft.world.level.block.entity.SignBlockEntity + // --> getSnapshot() == net.minecraft.world.level.block.entity.SignBlockEntity SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front); CRAFT_SIGN_SIDE_SIGN_TEXT.set(side, signText); } - /** - * 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, + private static MethodHandle findToLinTagMethodHandle() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { - if (linTagClass == null) { - return null; - } return LOOKUP.findVirtual( Class.forName("org.enginehub.linbus.tree.ToLinTag"), "toLinTag", - MethodType.methodType(linTagClass) + MethodType.methodType(LIN_TAG_CLASS) ); } @@ -266,26 +245,19 @@ private static MethodHandle findToLinTagMethodHandle(Class linTagClass) throw * * * @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 { + private static MethodHandle findPaperweightAdapterFromNativeMethodHandle(Class adapterClass) 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)); + return lookup.findVirtual(adapterClass, "fromNativeLin", MethodType.methodType(Object.class, LIN_TAG_CLASS)); } catch (NoSuchMethodException e) { // WE - return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, linTagClass)); + return lookup.findVirtual(adapterClass, "fromNative", MethodType.methodType(Object.class, LIN_TAG_CLASS)); } } From 2cf57dda476890a0310502651fa661dfd277ec4c Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 20:27:36 +0200 Subject: [PATCH 11/15] chore/fix: use minecraft codec to create SignTexts --- .../bukkit/schematic/StateWrapperSpigot.java | 160 +++++------------- 1 file changed, 41 insertions(+), 119 deletions(-) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 02ef2576c6..b61b9630b3 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -18,11 +18,8 @@ */ package com.plotsquared.bukkit.schematic; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.plotsquared.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; import com.sk89q.worldedit.bukkit.adapter.Refraction; @@ -30,7 +27,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; -import org.bukkit.DyeColor; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.Sign; @@ -41,13 +37,10 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Locale; -import java.util.Objects; +import java.util.Arrays; final class StateWrapperSpigot implements StateWrapper { @@ -56,26 +49,23 @@ final class StateWrapperSpigot implements StateWrapper { private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName(); private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName()); - private static final Gson GSON = new GsonBuilder().disableHtmlEscaping() - .registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create(); private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static BukkitImplAdapter ADAPTER = null; private static Class LIN_TAG_CLASS = null; private static Class CRAFT_BLOCK_ENTITY_STATE_CLASS = null; - private static Class MINECRAFT_CHAT_COMPONENT_CLASS = null; private static Field CRAFT_SIGN_SIDE_SIGN_TEXT = null; private static MethodHandle PAPERWEIGHT_ADAPTER_FROM_NATIVE = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null; private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = null; private static MethodHandle SIGN_BLOCK_ENTITY_SET_TEXT = null; - private static MethodHandle MINECRAFT_DYE_COLOR_BY_NAME = null; - private static MethodHandle CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = null; - private static MethodHandle SIGN_TEXT_CONSTRUCTOR = null; + private static MethodHandle DECODER_PARSE = null; + private static MethodHandle DATA_RESULT_GET_OR_THROW = null; private static MethodHandle TO_LIN_TAG = null; - private static Object DYE_COLOR_BLACK = null; + private static Object SIGN_TEXT_DIRECT_CODEC = null; + private static Object NBT_OPS_INSTANCE = null; public StateWrapperSpigot() { try { @@ -96,14 +86,6 @@ public StateWrapperSpigot() { } try { final Class SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.SignText"); - MINECRAFT_CHAT_COMPONENT_CLASS = Class.forName(Refraction.pickName( - "net.minecraft.network.chat.Component", - "net.minecraft.network.chat.IChatBaseComponent" - )); - final Class MINECRAFT_DYE_COLOR_CLASS = Class.forName(Refraction.pickName( - "net.minecraft.world.item.DyeColor", - "net.minecraft.world.item.EnumColor" - )); CRAFT_SIGN_SIDE_SIGN_TEXT = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.sign.CraftSignSide") .getDeclaredField("signText"); CRAFT_SIGN_SIDE_SIGN_TEXT.setAccessible(true); @@ -118,14 +100,30 @@ public StateWrapperSpigot() { )), SIGN_TEXT_CLASS ); - CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = findCraftChatMessageFromJsonOrStringMethodHandle( - MINECRAFT_CHAT_COMPONENT_CLASS); - SIGN_TEXT_CONSTRUCTOR = findSignTextConstructor( - SIGN_TEXT_CLASS, MINECRAFT_CHAT_COMPONENT_CLASS, MINECRAFT_DYE_COLOR_CLASS + final Class CODEC_CLASS = Class.forName("com.mojang.serialization.Codec"); + final Class DECODER_CLASS = Class.forName("com.mojang.serialization.Decoder"); + final Class DATA_RESULT_CLASS = Class.forName("com.mojang.serialization.DataResult"); + final Class DYNAMIC_OPS_CLASS = Class.forName("com.mojang.serialization.DynamicOps"); + final Class NBT_OPS_CLASS = Class.forName(Refraction.pickName( + "net.minecraft.nbt.NbtOps", + "net.minecraft.nbt.DynamicOpsNBT" + )); + SIGN_TEXT_DIRECT_CODEC = Arrays.stream(SIGN_TEXT_CLASS.getFields()) + .filter(field -> Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers())) + .filter(field -> field.getType() == CODEC_CLASS) + .findFirst().orElseThrow().get(null); + DECODER_PARSE = LOOKUP.findVirtual( + DECODER_CLASS, "parse", MethodType.methodType( + DATA_RESULT_CLASS, DYNAMIC_OPS_CLASS, Object.class + ) ); - MINECRAFT_DYE_COLOR_BY_NAME = findDyeColorByNameMethodHandle(MINECRAFT_DYE_COLOR_CLASS); - DYE_COLOR_BLACK = Objects.requireNonNull( - MINECRAFT_DYE_COLOR_BY_NAME.invoke("black", null), "couldn't find black dye color" + NBT_OPS_INSTANCE = Arrays.stream(NBT_OPS_CLASS.getFields()) + .filter(field -> Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers())) + .filter(field -> field.getType() == NBT_OPS_CLASS) + .findFirst().orElseThrow().get(null); + DATA_RESULT_GET_OR_THROW = LOOKUP.findVirtual( + DATA_RESULT_CLASS, "getOrThrow", + MethodType.genericMethodType(0) ); } catch (Throwable e) { throw new RuntimeException("Failed to initialize required native method accessors", e); @@ -140,14 +138,19 @@ public boolean restore(final @NonNull Block block, final @NonNull CompoundTag da return false; } // get native tag - Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke( - ADAPTER, - TO_LIN_TAG.invoke(data) - ); + Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke(ADAPTER, TO_LIN_TAG.invoke(data)); // load block entity data CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag); - postEntityStateLoad(blockState, data); + // signs need to be handled explicitly (at least during worldgen) + if (blockState instanceof Sign sign) { + if (data.getValue().get("front_text") instanceof CompoundTag textTag) { + setSignContents(true, sign.getSide(Side.FRONT), blockState, textTag); + } + if (data.getValue().get("back_text") instanceof CompoundTag textTag) { + setSignContents(false, sign.getSide(Side.BACK), blockState, textTag); + } + } CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS); } catch (Throwable e) { @@ -156,60 +159,10 @@ public boolean restore(final @NonNull Block block, final @NonNull CompoundTag da return false; } - public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable { - if (blockState instanceof Sign sign) { - if (data.getValue().get("front_text") instanceof CompoundTag textTag) { - setSignContents(true, sign.getSide(Side.FRONT), blockState, textTag); - } - if (data.getValue().get("back_text") instanceof CompoundTag textTag) { - setSignContents(false, sign.getSide(Side.BACK), blockState, textTag); - } - } - } - private static void setSignContents(boolean front, SignSide side, BlockState blockState, CompoundTag data) throws Throwable { - final List messages = data.getList("messages"); - if (messages.size() != 4) { - if (data.containsKey("color")) { - //noinspection UnstableApiUsage - side.setColor(DyeColor.legacyValueOf(data.getString("color").toUpperCase(Locale.ROOT))); - } - side.setGlowingText(data.getByte("has_glowing_text") == 1); - return; - } - - final String color = data.getString("color"); - final boolean glowing = data.getByte("has_glowing_text") == 1; - final Object dyeColor = color.isEmpty() ? DYE_COLOR_BLACK : MINECRAFT_DYE_COLOR_BY_NAME.invoke( - color.equalsIgnoreCase("silver") ? "light_gray" : color, - DYE_COLOR_BLACK // fallback - ); - - Object[] components = new Object[messages.size()]; - for (int i = 0; i < components.length; i++) { - final Tag message = messages.get(i); - Object content; - // unwrap possible nested entry for mixed array types in later versions - if (message instanceof CompoundTag tag && tag.containsKey("")) { - content = tag.getString(""); - } else { - content = message.getValue(); - } - // if the value is not a string, make it one so it can be converted to a chat component - if (!(content instanceof String)) { - content = GSON.toJson(content); - LOGGER.info("GSON serialized to {}", content); - LOGGER.info("factory to {}", CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content)); - } - // chat.Component - components[i] = CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content); - } - final Object typedComponents = Array.newInstance(MINECRAFT_CHAT_COMPONENT_CLASS, components.length); - System.arraycopy(components, 0, typedComponents, 0, components.length); - final Object signText = SIGN_TEXT_CONSTRUCTOR.invoke(typedComponents, typedComponents, dyeColor, glowing); - - // blockState == org.bukkit.craftbukkit.block.CraftBlockEntityState - // --> getSnapshot() == net.minecraft.world.level.block.entity.SignBlockEntity + Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke(ADAPTER, TO_LIN_TAG.invoke(data)); + Object dataResult = DECODER_PARSE.invoke(SIGN_TEXT_DIRECT_CODEC, NBT_OPS_INSTANCE, nativeTag); + Object signText = DATA_RESULT_GET_OR_THROW.invoke(dataResult); SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front); CRAFT_SIGN_SIDE_SIGN_TEXT.set(side, signText); } @@ -239,7 +192,6 @@ private static MethodHandle findToLinTagMethodHandle() throws ClassNotFoundExcep *
* 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)}
  • *
@@ -300,34 +252,4 @@ private static MethodHandle findSignBlockEntitySetTextMethodHandle(Class sign throw new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"); } - private static MethodHandle findCraftChatMessageFromJsonOrStringMethodHandle(Class minecraftChatComponent) - throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { - // public static IChatBaseComponent fromJSONOrString(String message) - return LOOKUP.findStatic( - Class.forName(CRAFTBUKKIT_PACKAGE + ".util.CraftChatMessage"), - "fromJSONOrString", - MethodType.methodType(minecraftChatComponent, String.class) - ); - } - - private static MethodHandle findSignTextConstructor(Class signText, Class chatComponent, Class dyeColorEnum) throws - NoSuchMethodException, IllegalAccessException { - return LOOKUP.findConstructor( - signText, MethodType.methodType( - void.class, - chatComponent.arrayType(), chatComponent.arrayType(), dyeColorEnum, Boolean.TYPE - ) - ); - } - - private static MethodHandle findDyeColorByNameMethodHandle(Class dyeColorClass) throws - NoSuchMethodException, IllegalAccessException { - for (final Method method : dyeColorClass.getMethods()) { - if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 2 && method.getParameterTypes()[0] == String.class) { - return LOOKUP.unreflect(method); - } - } - throw new NoSuchMethodException("Couldn't lookup static DyeColor.byName(String, DyeColor)"); - } - } From a191000142e1327883ee2b557627587955f60b24 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 20:32:09 +0200 Subject: [PATCH 12/15] chore: drop unneeded type adapter --- .../bukkit/schematic/NbtGsonSerializer.java | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java deleted file mode 100644 index 8e29f3e6a1..0000000000 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/NbtGsonSerializer.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * PlotSquared, a land and world management plugin for Minecraft. - * Copyright (C) IntellectualSites - * Copyright (C) IntellectualSites team and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.plotsquared.bukkit.schematic; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.sk89q.jnbt.ByteArrayTag; -import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.IntArrayTag; -import com.sk89q.jnbt.ListTag; -import com.sk89q.jnbt.LongArrayTag; -import com.sk89q.jnbt.Tag; - -import java.lang.reflect.Type; - -final class NbtGsonSerializer implements JsonSerializer { - - @Override - public JsonElement serialize(final Tag src, final Type typeOfSrc, final JsonSerializationContext context) { - if (src instanceof CompoundTag compoundTag) { - JsonObject object = new JsonObject(); - compoundTag.getValue().forEach((s, tag) -> object.add(s, context.serialize(tag))); - return object; - } - if (src instanceof ListTag listTag) { - JsonArray array = new JsonArray(); - listTag.getValue().forEach(tag -> array.add(context.serialize(tag))); - return array; - } - if (src instanceof ByteArrayTag byteArrayTag) { - JsonArray array = new JsonArray(); - for (final byte b : byteArrayTag.getValue()) { - array.add(b); - } - return array; - } - if (src instanceof IntArrayTag intArrayTag) { - JsonArray array = new JsonArray(); - for (final int i : intArrayTag.getValue()) { - array.add(i); - } - return array; - } - if (src instanceof LongArrayTag longArrayTag) { - JsonArray array = new JsonArray(); - for (final long l : longArrayTag.getValue()) { - array.add(l); - } - return array; - } - if (src.getValue() instanceof Number number) { - return new JsonPrimitive(number); - } - if (src.getValue() instanceof String string) { - return new JsonPrimitive(string); - } - throw new IllegalArgumentException("Don't know how to serialize " + src); - } - -} From 6991bc79d6835926eb32330183fb1274dbe8a0f0 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 21:24:13 +0200 Subject: [PATCH 13/15] chore: invalidate sign cache --- .../bukkit/schematic/StateWrapperSpigot.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index b61b9630b3..7729aadb6f 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -55,6 +55,7 @@ final class StateWrapperSpigot implements StateWrapper { private static Class LIN_TAG_CLASS = null; private static Class CRAFT_BLOCK_ENTITY_STATE_CLASS = null; private static Field CRAFT_SIGN_SIDE_SIGN_TEXT = null; + private static Field CRAFT_SIGN_SIDE_LINES = 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; @@ -86,9 +87,11 @@ public StateWrapperSpigot() { } try { final Class SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.SignText"); - CRAFT_SIGN_SIDE_SIGN_TEXT = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.sign.CraftSignSide") - .getDeclaredField("signText"); + final Class CRAFT_SIGN_SIDE_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.sign.CraftSignSide"); + CRAFT_SIGN_SIDE_SIGN_TEXT = CRAFT_SIGN_SIDE_CLASS.getDeclaredField("signText"); CRAFT_SIGN_SIDE_SIGN_TEXT.setAccessible(true); + CRAFT_SIGN_SIDE_LINES = CRAFT_SIGN_SIDE_CLASS.getDeclaredField("lines"); + CRAFT_SIGN_SIDE_LINES.setAccessible(true); CRAFT_BLOCK_ENTITY_STATE_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.CraftBlockEntityState"); CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); CRAFT_BLOCK_ENTITY_STATE_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS); @@ -163,8 +166,14 @@ private static void setSignContents(boolean front, SignSide side, BlockState blo Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke(ADAPTER, TO_LIN_TAG.invoke(data)); Object dataResult = DECODER_PARSE.invoke(SIGN_TEXT_DIRECT_CODEC, NBT_OPS_INSTANCE, nativeTag); Object signText = DATA_RESULT_GET_OR_THROW.invoke(dataResult); + + // set the SignText on the underlying tile entity snapshot (SignBlockEntity) SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front); + // and update the SignText field on the CraftSignSide - changes are otherwise not reflected CRAFT_SIGN_SIDE_SIGN_TEXT.set(side, signText); + + // reset cached lines to null, so it can be re-retrieved from SignText (for API access etc.) + CRAFT_SIGN_SIDE_LINES.set(side, null); } /** From a388629d62e7d5c9b5d1151f7e325acb842c0bef Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Thu, 23 Oct 2025 21:48:54 +0200 Subject: [PATCH 14/15] fix: backwards compatibility --- .../bukkit/schematic/StateWrapperSpigot.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 7729aadb6f..4e6f390d6f 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -41,6 +41,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.Optional; final class StateWrapperSpigot implements StateWrapper { @@ -62,7 +63,7 @@ final class StateWrapperSpigot implements StateWrapper { private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = null; private static MethodHandle SIGN_BLOCK_ENTITY_SET_TEXT = null; private static MethodHandle DECODER_PARSE = null; - private static MethodHandle DATA_RESULT_GET_OR_THROW = null; + private static MethodHandle DATA_RESULT_RESULT = null; private static MethodHandle TO_LIN_TAG = null; private static Object SIGN_TEXT_DIRECT_CODEC = null; @@ -124,9 +125,9 @@ public StateWrapperSpigot() { .filter(field -> Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers())) .filter(field -> field.getType() == NBT_OPS_CLASS) .findFirst().orElseThrow().get(null); - DATA_RESULT_GET_OR_THROW = LOOKUP.findVirtual( - DATA_RESULT_CLASS, "getOrThrow", - MethodType.genericMethodType(0) + DATA_RESULT_RESULT = LOOKUP.findVirtual( + DATA_RESULT_CLASS, "result", + MethodType.methodType(Optional.class) ); } catch (Throwable e) { throw new RuntimeException("Failed to initialize required native method accessors", e); @@ -165,7 +166,8 @@ public boolean restore(final @NonNull Block block, final @NonNull CompoundTag da private static void setSignContents(boolean front, SignSide side, BlockState blockState, CompoundTag data) throws Throwable { Object nativeTag = PAPERWEIGHT_ADAPTER_FROM_NATIVE.invoke(ADAPTER, TO_LIN_TAG.invoke(data)); Object dataResult = DECODER_PARSE.invoke(SIGN_TEXT_DIRECT_CODEC, NBT_OPS_INSTANCE, nativeTag); - Object signText = DATA_RESULT_GET_OR_THROW.invoke(dataResult); + //noinspection rawtypes + Object signText = ((Optional) DATA_RESULT_RESULT.invoke(dataResult)).orElseThrow(); // set the SignText on the underlying tile entity snapshot (SignBlockEntity) SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front); From d1a6c021fcbef245e34d9ae5fd98c724fb2dda99 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Fri, 24 Oct 2025 22:35:18 +0200 Subject: [PATCH 15/15] chore: improve readability of method retrieval --- .../bukkit/schematic/StateWrapperSpigot.java | 25 +++--- .../core/util/ReflectionHelper.java | 76 ++++++++++++++++++ .../core/util/ReflectionHelperTest.java | 79 +++++++++++++++++++ 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java create mode 100644 Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java index 4e6f390d6f..000d707d55 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -18,6 +18,7 @@ */ package com.plotsquared.bukkit.schematic; +import com.plotsquared.core.util.ReflectionHelper; import com.plotsquared.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.bukkit.WorldEditPlugin; @@ -236,13 +237,11 @@ private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class< private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class craftBlockEntityStateClass) throws NoSuchMethodException, IllegalAccessException { - for (final Method method : craftBlockEntityStateClass.getMethods()) { - if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 && - method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) { - return LOOKUP.unreflect(method); - } - } - throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName()); + return LOOKUP.unreflect(ReflectionHelper.findMethod( + craftBlockEntityStateClass, + MethodType.methodType(Boolean.TYPE, Boolean.TYPE, Boolean.TYPE), + Modifier.PUBLIC + ).orElseThrow(() -> new NoSuchMethodException("Couldn't lookup CraftBlockEntityState#update(boolean, boolean) boolean"))); } private static MethodHandle findCraftBlockEntityStateSnapshotMethodHandle(Class craftBlockEntityStateClass) throws @@ -254,13 +253,11 @@ private static MethodHandle findCraftBlockEntityStateSnapshotMethodHandle(Class< private static MethodHandle findSignBlockEntitySetTextMethodHandle(Class signBlockEntity, Class signText) throws NoSuchMethodException, IllegalAccessException { - for (final Method method : signBlockEntity.getMethods()) { - if (method.getReturnType() == Boolean.TYPE && method.getParameterCount() == 2 - && method.getParameterTypes()[0] == signText && method.getParameterTypes()[1] == Boolean.TYPE) { - return LOOKUP.unreflect(method); - } - } - throw new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"); + return LOOKUP.unreflect(ReflectionHelper.findMethod( + signBlockEntity, + MethodType.methodType(Boolean.TYPE, signText, Boolean.TYPE), + Modifier.PUBLIC + ).orElseThrow(() -> new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean"))); } } diff --git a/Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java b/Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java new file mode 100644 index 0000000000..c24ad06ea7 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/util/ReflectionHelper.java @@ -0,0 +1,76 @@ +/* + * 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.core.util; + +import org.jetbrains.annotations.ApiStatus; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.util.Optional; + +@ApiStatus.Internal +public final class ReflectionHelper { + + /** + * Find a (declared) method with an unknown or potentially obfuscated name by its signature and optional modifiers. + *
+ * The method - if private - is not made accessible. Either call {@link Method#setAccessible(boolean)} or + * use a {@link java.lang.invoke.MethodHandles.Lookup#privateLookupIn(Class, MethodHandles.Lookup) private lookup}. + * + * @param holder The class providing the method. + * @param signature The signature of the method, identified by parameter types and the return type. + * @param modifiers All possible modifiers of the method that should be validated. + * @return The method, if one has been found. Otherwise, an empty Optional. + * @throws RuntimeException if multiple matching methods have been found. + * @see java.lang.reflect.Modifier + */ + public static Optional findMethod(Class holder, MethodType signature, int... modifiers) { + Method found = null; + outer: + for (final Method method : holder.getDeclaredMethods()) { + if (method.getParameterCount() != signature.parameterCount()) { + continue; + } + if (!signature.returnType().isAssignableFrom(method.getReturnType())) { + continue; + } + + for (final int modifier : modifiers) { + if ((method.getModifiers() & modifier) == 0) { + continue outer; + } + } + + Class[] parameterTypes = signature.parameterArray(); + for (int i = 0; i < parameterTypes.length; i++) { + // validate expected parameter is either the same type or subtype of actual parameter + if (!parameterTypes[i].isAssignableFrom(method.getParameterTypes()[i])) { + continue outer; + } + } + if (found != null) { + throw new RuntimeException("Found ambiguous method by selector: " + method + " vs " + found); + } + found = method; + } + return Optional.ofNullable(found); + } + +} diff --git a/Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java b/Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java new file mode 100644 index 0000000000..4be21e4906 --- /dev/null +++ b/Core/src/test/java/com/plotsquared/core/util/ReflectionHelperTest.java @@ -0,0 +1,79 @@ +/* + * 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.core.util; + +import org.junit.jupiter.api.Test; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Modifier; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ReflectionHelperTest { + + @Test + void findMethod() throws NoSuchMethodException { + assertThrows( + RuntimeException.class, () -> + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType(String.class)) + ); + assertEquals( + MethodTesterClass.class.getMethod("methodThree"), + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType(String.class), Modifier.PUBLIC) + .orElse(null) + ); + assertEquals( + MethodTesterClass.class.getDeclaredMethod("methodFour", String.class, Collection.class), + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType( + String.class, String.class, Collection.class + )).orElse(null) + ); + // check that helper allows super classes of parameters when searching + assertEquals( + MethodTesterClass.class.getDeclaredMethod("methodFour", String.class, Collection.class), + ReflectionHelper.findMethod(MethodTesterClass.class, MethodType.methodType( + String.class, String.class, Object.class + )).orElse(null) + ); + } + + @SuppressWarnings("unused") + private static class MethodTesterClass { + + private static String methodOne() { + return ""; + } + + private static String methodTwo() { + return ""; + } + + public static String methodThree() { + return ""; + } + + protected static String methodFour(String param, Collection paramList) { + return ""; + } + + } + +}