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/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/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; 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 4ba2c1dbd1..ffc7a31c5e 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapper.java @@ -18,332 +18,35 @@ */ 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.sk89q.jnbt.ByteTag; 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 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.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.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.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.UUID; +@ApiStatus.Internal +public sealed interface StateWrapper permits StateWrapperSpigot { -public class StateWrapper { + StateWrapper INSTANCE = Factory.createStateWrapper(); - public CompoundTag tag; + boolean restore(final @NonNull Block block, final @NonNull CompoundTag data); - private boolean paperErrorTextureSent = false; - private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName()); - - 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. - * - * @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 - */ - @SuppressWarnings("deprecation") // #setLine is needed for Spigot compatibility - public boolean restoreTag(@NonNull Block block) { - if (this.tag == null) { - 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.getByte("Count"); - int slot = itemComp.getByte("Slot"); - CompoundTag tag = (CompoundTag) itemComp.getValue().get("tag"); - 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) { - 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); - 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); - return true; + @ApiStatus.Internal + final class Factory { - } - return false; - } - case "banner" -> { - if (state instanceof Banner banner) { - List patterns = this.tag.getListTag("Patterns").getValue(); - if (patterns == null || patterns.isEmpty()) { - return false; - } - 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; - } - return new Pattern(color, patternType); - }).filter(Objects::nonNull).toList()); - banner.update(true); - return true; - } - return false; - } + private static StateWrapper createStateWrapper() { + return new StateWrapperSpigot(); } - return false; - } - - public String getId() { - String tileid = this.tag.getString("id").toLowerCase(); - if (tileid.startsWith("minecraft:")) { - tileid = tileid.replace("minecraft:", ""); - } - 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)); - } - return data; } } 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..000d707d55 --- /dev/null +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/schematic/StateWrapperSpigot.java @@ -0,0 +1,263 @@ +/* + * 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.ReflectionHelper; +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.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.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.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Optional; + +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()); + 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 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; + 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_RESULT = null; + private static MethodHandle TO_LIN_TAG = null; + + private static Object SIGN_TEXT_DIRECT_CODEC = null; + private static Object NBT_OPS_INSTANCE = null; + + public StateWrapperSpigot() { + try { + ReflectionUtils.RefClass worldEditPluginRefClass = ReflectionUtils.getRefClass(WorldEditPlugin.class); + WorldEditPlugin worldEditPlugin = (WorldEditPlugin) worldEditPluginRefClass + .getMethod("getInstance") + .of(null) + .call(); + ADAPTER = (BukkitImplAdapter) worldEditPluginRefClass + .getMethod("getBukkitImplAdapter") + .of(worldEditPlugin) + .call(); + 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 classes or methods", e); + } + try { + final Class SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.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); + 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 + ); + 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 + ) + ); + 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_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); + } + } + + @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, TO_LIN_TAG.invoke(data)); + // load block entity data + CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA.invoke(blockState, nativeTag); + + // 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) { + LOGGER.error("Failed to update tile entity", e); + } + return false; + } + + 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); + //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); + // 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); + } + + /** + * 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. + * + * @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() throws ClassNotFoundException, + NoSuchMethodException, IllegalAccessException { + return LOOKUP.findVirtual( + Class.forName("org.enginehub.linbus.tree.ToLinTag"), + "toLinTag", + MethodType.methodType(LIN_TAG_CLASS) + ); + } + + /** + * 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 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 + * @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) throws + IllegalAccessException, NoSuchMethodException { + final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(adapterClass, LOOKUP); + 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)); + } + } + + private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class craftBlockEntityStateClass) throws + NoSuchMethodException, IllegalAccessException { + 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 { + 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 + 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 { + 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 ""; + } + + } + +}