From ec50b28f4bef1d21966027284ac11e733a727077 Mon Sep 17 00:00:00 2001 From: Josh Roy <10731363+JRoy@users.noreply.github.com> Date: Fri, 28 May 2021 07:23:44 -0400 Subject: [PATCH] Add ItemStack 1.8.8+ cross-version PersistentDataContainer (#4143) This PR itself does nothing on its own but creates the underlying backbone I need to make a less hacky solution in #3963 lmfao. This PR creates a provider which uses NBT on 1.8.8-1.13 to mimic the exact structure of a PersistentDataContainer on 1.14+ which will allow us make any possible upgrades (which don't die from the lack of DFU on >1.13) work as expected. Additionally, this does not use reflection on modern Minecraft versions and thus will not need to be maintained/updated on MC version updates. In the future, we will need to find a way to store data on tile entities (signs namely) so that we are able to store UUIDs on signs for future plans, but for now ItemStacks work to fix our spawner issues. --- .../com/earth2me/essentials/Essentials.java | 15 ++ .../com/earth2me/essentials/IEssentials.java | 5 +- .../ess3/provider/PersistentDataProvider.java | 11 ++ .../ModernPersistentDataProvider.java | 51 +++++++ .../providers/ReflPersistentDataProvider.java | 131 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 providers/BaseProviders/src/main/java/net/ess3/provider/PersistentDataProvider.java create mode 100644 providers/BaseProviders/src/main/java/net/ess3/provider/providers/ModernPersistentDataProvider.java create mode 100644 providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflPersistentDataProvider.java diff --git a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java index 97c160578..c1127351a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java @@ -47,6 +47,7 @@ import net.ess3.api.IJails; import net.ess3.api.ISettings; import net.ess3.nms.refl.providers.ReflFormattedCommandAliasProvider; import net.ess3.nms.refl.providers.ReflKnownCommandsProvider; +import net.ess3.nms.refl.providers.ReflPersistentDataProvider; import net.ess3.nms.refl.providers.ReflServerStateProvider; import net.ess3.nms.refl.providers.ReflSpawnEggProvider; import net.ess3.nms.refl.providers.ReflSpawnerBlockProvider; @@ -55,6 +56,7 @@ import net.ess3.provider.ContainerProvider; import net.ess3.provider.FormattedCommandAliasProvider; import net.ess3.provider.KnownCommandsProvider; import net.ess3.provider.MaterialTagProvider; +import net.ess3.provider.PersistentDataProvider; import net.ess3.provider.PotionMetaProvider; import net.ess3.provider.ProviderListener; import net.ess3.provider.ServerStateProvider; @@ -69,6 +71,7 @@ import net.ess3.provider.providers.BukkitSpawnerBlockProvider; import net.ess3.provider.providers.FlatSpawnEggProvider; import net.ess3.provider.providers.LegacyPotionMetaProvider; import net.ess3.provider.providers.LegacySpawnEggProvider; +import net.ess3.provider.providers.ModernPersistentDataProvider; import net.ess3.provider.providers.PaperContainerProvider; import net.ess3.provider.providers.PaperKnownCommandsProvider; import net.ess3.provider.providers.PaperMaterialTagProvider; @@ -154,6 +157,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { private transient ProviderListener recipeBookEventProvider; private transient MaterialTagProvider materialTagProvider; private transient SyncCommandsProvider syncCommandsProvider; + private transient PersistentDataProvider persistentDataProvider; private transient Kits kits; private transient RandomTeleport randomTeleport; private transient UpdateChecker updateChecker; @@ -383,6 +387,12 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { // Sync Commands Provider syncCommandsProvider = new ReflSyncCommandsProvider(); + if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_14_4_R01)) { + persistentDataProvider = new ModernPersistentDataProvider(this); + } else { + persistentDataProvider = new ReflPersistentDataProvider(this); + } + execTimer.mark("Init(Providers)"); reload(); @@ -1143,6 +1153,11 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { return syncCommandsProvider; } + @Override + public PersistentDataProvider getPersistentDataProvider() { + return persistentDataProvider; + } + @Override public PluginCommand getPluginCommand(final String cmd) { return this.getCommand(cmd); diff --git a/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java b/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java index 52d322e29..c8cd2c43d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java @@ -6,10 +6,11 @@ import com.earth2me.essentials.api.IWarps; import com.earth2me.essentials.commands.IEssentialsCommand; import com.earth2me.essentials.perm.PermissionsHandler; import com.earth2me.essentials.updatecheck.UpdateChecker; -import net.ess3.provider.MaterialTagProvider; import net.ess3.provider.ContainerProvider; import net.ess3.provider.FormattedCommandAliasProvider; import net.ess3.provider.KnownCommandsProvider; +import net.ess3.provider.MaterialTagProvider; +import net.ess3.provider.PersistentDataProvider; import net.ess3.provider.ServerStateProvider; import net.ess3.provider.SpawnerBlockProvider; import net.ess3.provider.SpawnerItemProvider; @@ -139,5 +140,7 @@ public interface IEssentials extends Plugin { SyncCommandsProvider getSyncCommandsProvider(); + PersistentDataProvider getPersistentDataProvider(); + PluginCommand getPluginCommand(String cmd); } diff --git a/providers/BaseProviders/src/main/java/net/ess3/provider/PersistentDataProvider.java b/providers/BaseProviders/src/main/java/net/ess3/provider/PersistentDataProvider.java new file mode 100644 index 000000000..55d11651a --- /dev/null +++ b/providers/BaseProviders/src/main/java/net/ess3/provider/PersistentDataProvider.java @@ -0,0 +1,11 @@ +package net.ess3.provider; + +import org.bukkit.inventory.ItemStack; + +public interface PersistentDataProvider extends Provider { + void set(ItemStack itemStack, String key, String value); + + String getString(ItemStack itemStack, String key); + + void remove(ItemStack itemStack, String key); +} diff --git a/providers/BaseProviders/src/main/java/net/ess3/provider/providers/ModernPersistentDataProvider.java b/providers/BaseProviders/src/main/java/net/ess3/provider/providers/ModernPersistentDataProvider.java new file mode 100644 index 000000000..7c3ebbede --- /dev/null +++ b/providers/BaseProviders/src/main/java/net/ess3/provider/providers/ModernPersistentDataProvider.java @@ -0,0 +1,51 @@ +package net.ess3.provider.providers; + +import net.ess3.provider.PersistentDataProvider; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; + +@SuppressWarnings("ConstantConditions") +public class ModernPersistentDataProvider implements PersistentDataProvider { + private final Plugin plugin; + + public ModernPersistentDataProvider(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public void set(ItemStack itemStack, String key, String value) { + if (itemStack == null || itemStack.getItemMeta() == null || key == null || value == null) { + return; + } + final ItemMeta im = itemStack.getItemMeta(); + + im.getPersistentDataContainer().set(new NamespacedKey(plugin, key), PersistentDataType.STRING, value); + itemStack.setItemMeta(im); + } + + @Override + public String getString(ItemStack itemStack, String key) { + if (itemStack == null || itemStack.getItemMeta() == null || key == null) { + return null; + } + + try { + return itemStack.getItemMeta().getPersistentDataContainer().get(new NamespacedKey(plugin, key), PersistentDataType.STRING); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + @Override + public void remove(ItemStack itemStack, String key) { + itemStack.getItemMeta().getPersistentDataContainer().remove(new NamespacedKey(plugin, key)); + } + + @Override + public String getDescription() { + return "1.14+ Persistent Data Container Provider"; + } +} diff --git a/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflPersistentDataProvider.java b/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflPersistentDataProvider.java new file mode 100644 index 000000000..bc23bbdb6 --- /dev/null +++ b/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflPersistentDataProvider.java @@ -0,0 +1,131 @@ +package net.ess3.nms.refl.providers; + +import net.ess3.nms.refl.ReflUtil; +import net.ess3.provider.PersistentDataProvider; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.util.Locale; + +/** + * Stores persistent data on 1.18-1.13 in a manner that's consistent with PDC on 1.14+ to enable + * seamless upgrades. + */ +public class ReflPersistentDataProvider implements PersistentDataProvider { + private static final String PDC_ROOT_TAG = "PublicBukkitValues"; + private static final String ROOT_TAG = "tag"; + private final String namespace; + private final MethodHandle itemHandleGetterHandle; + private final MethodHandle getTagHandle; + private final MethodHandle tagSetterHandle; + private final MethodHandle newCompoundHandle; + private final MethodHandle getCompoundHandle; + private final MethodHandle setCompoundHandle; + private final MethodHandle setStringHandle; + private final MethodHandle removeHandle; + private final MethodHandle getStringHandle; + + public ReflPersistentDataProvider(Plugin plugin) { + this.namespace = plugin.getName().toLowerCase(Locale.ROOT); + + MethodHandle itemHandleGetterHandle = null; + MethodHandle getTagHandle = null; + MethodHandle tagSetterHandle = null; + MethodHandle newCompoundHandle = null; + MethodHandle getCompoundHandle = null; + MethodHandle setCompoundHandle = null; + MethodHandle setStringHandle = null; + MethodHandle removeHandle = null; + MethodHandle getStringHandle = null; + try { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + final Field handleGetter = ReflUtil.getOBCClass("inventory.CraftItemStack").getDeclaredField("handle"); + handleGetter.setAccessible(true); + itemHandleGetterHandle = lookup.unreflectGetter(handleGetter); + final Field tagSetter = ReflUtil.getNMSClass("ItemStack").getDeclaredField("tag"); + tagSetter.setAccessible(true); + tagSetterHandle = lookup.unreflectSetter(tagSetter); + getTagHandle = lookup.findVirtual(ReflUtil.getNMSClass("ItemStack"), "getTag", MethodType.methodType(ReflUtil.getNMSClass("NBTTagCompound"))); + newCompoundHandle = lookup.findConstructor(ReflUtil.getNMSClass("NBTTagCompound"), MethodType.methodType(void.class)); + getCompoundHandle = lookup.findVirtual(ReflUtil.getNMSClass("NBTTagCompound"), "getCompound", MethodType.methodType(ReflUtil.getNMSClass("NBTTagCompound"), String.class)); + setCompoundHandle = lookup.findVirtual(ReflUtil.getNMSClass("NBTTagCompound"), "set", MethodType.methodType(void.class, String.class, ReflUtil.getNMSClass("NBTBase"))); + setStringHandle = lookup.findVirtual(ReflUtil.getNMSClass("NBTTagCompound"), "setString", MethodType.methodType(void.class, String.class, String.class)); + removeHandle = lookup.findVirtual(ReflUtil.getNMSClass("NBTTagCompound"), "remove", MethodType.methodType(void.class, String.class)); + getStringHandle = lookup.findVirtual(ReflUtil.getNMSClass("NBTTagCompound"), "getString", MethodType.methodType(String.class, String.class)); + } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) { + e.printStackTrace(); + } + this.itemHandleGetterHandle = itemHandleGetterHandle; + this.getTagHandle = getTagHandle; + this.tagSetterHandle = tagSetterHandle; + this.newCompoundHandle = newCompoundHandle; + this.getCompoundHandle = getCompoundHandle; + this.setCompoundHandle = setCompoundHandle; + this.setStringHandle = setStringHandle; + this.removeHandle = removeHandle; + this.getStringHandle = getStringHandle; + } + + private String getPersistentString(ItemStack itemStack, String key) throws Throwable { + final Object nmsItem = itemHandleGetterHandle.invoke(itemStack); + final Object itemRootTag = getTagHandle.invoke(nmsItem); + if (itemRootTag == null) { + return null; + } + final Object tagCompound = getCompoundHandle.invoke(itemRootTag, ROOT_TAG); + final Object publicBukkitValuesCompound = getCompoundHandle.invoke(tagCompound, PDC_ROOT_TAG); + return (String) getStringHandle.invoke(publicBukkitValuesCompound, key); + } + + private void setPersistentString(ItemStack itemStack, String key, String value) throws Throwable { + final Object nmsItem = itemHandleGetterHandle.invoke(itemStack); + Object itemRootTag = getTagHandle.invoke(nmsItem); + if (itemRootTag == null) { + itemRootTag = newCompoundHandle.invoke(); + tagSetterHandle.invoke(nmsItem, itemRootTag); + } + final Object tagCompound = getCompoundHandle.invoke(itemRootTag, ROOT_TAG); + final Object publicBukkitValuesCompound = getCompoundHandle.invoke(tagCompound, PDC_ROOT_TAG); + if (value == null) { + removeHandle.invoke(publicBukkitValuesCompound, key); + } else { + setStringHandle.invoke(publicBukkitValuesCompound, key, value); + } + setCompoundHandle.invoke(tagCompound, PDC_ROOT_TAG, publicBukkitValuesCompound); + setCompoundHandle.invoke(itemRootTag, ROOT_TAG, tagCompound); + } + + @Override + public void set(ItemStack itemStack, String key, String value) { + try { + setPersistentString(itemStack, namespace + ":" + key, value); + } catch (Throwable ignored) { + } + } + + @Override + public String getString(ItemStack itemStack, String key) { + try { + return getPersistentString(itemStack, namespace + ":" + key); + } catch (Throwable ignored) { + return null; + } + } + + @Override + public void remove(ItemStack itemStack, String key) { + try { + setPersistentString(itemStack, namespace + ":" + key, null); + } catch (Throwable ignored) { + } + } + + @Override + public String getDescription() { + return "1.13 >= Persistent Data Container Provider"; + } +}