From 77a837fc050fe46983369ffebee873396fa65694 Mon Sep 17 00:00:00 2001 From: Eric Date: Sun, 28 Apr 2019 20:56:49 +0200 Subject: [PATCH] Rework NMS Fixes hologram duplicates Fixes support for 1.14 Breaks hologram interaction (Events cannot be sent to the server) Armor stand and item entities are now totally client side,they are not even created, but instead are just put in a packet to send to the client Performance has not been tested! --- .../shopchest/nms/ArmorStandWrapper.java | 144 +++---------- .../de/epiceric/shopchest/nms/Hologram.java | 4 +- .../de/epiceric/shopchest/shop/ShopItem.java | 92 +++------ .../de/epiceric/shopchest/utils/Utils.java | 189 ++++++++++++++++++ 4 files changed, 248 insertions(+), 181 deletions(-) diff --git a/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java b/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java index 02c67c4..78395a7 100644 --- a/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java +++ b/src/main/java/de/epiceric/shopchest/nms/ArmorStandWrapper.java @@ -2,99 +2,44 @@ package de.epiceric.shopchest.nms; import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.utils.Utils; -import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import java.lang.reflect.Method; +import java.lang.reflect.Field; import java.util.UUID; public class ArmorStandWrapper { - - private Class worldClass = Utils.getNMSClass("World"); - private Class worldServerClass = Utils.getNMSClass("WorldServer"); - private Class dataWatcherClass = Utils.getNMSClass("DataWatcher"); - private Class iChatBaseComponentClass = Utils.getNMSClass("IChatBaseComponent"); - private Class chatMessageClass = Utils.getNMSClass("ChatMessage"); - private Class entityClass = Utils.getNMSClass("Entity"); - private Class entityArmorStandClass = Utils.getNMSClass("EntityArmorStand"); - private Class entityLivingClass = Utils.getNMSClass("EntityLiving"); - private Class packetPlayOutSpawnEntityLivingClass = Utils.getNMSClass("PacketPlayOutSpawnEntityLiving"); - private Class packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy"); - private Class packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata"); - private Class packetPlayOutEntityTeleportClass = Utils.getNMSClass("PacketPlayOutEntityTeleport"); + + private final Class packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy"); + private final Class packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata"); + private final Class packetPlayOutEntityTeleportClass = Utils.getNMSClass("PacketPlayOutEntityTeleport"); + private final Class dataWatcherClass = Utils.getNMSClass("DataWatcher"); private ShopChest plugin; - - private Object nmsWorld; - private Object entity; private Location location; private String customName; private UUID uuid; - private int entityId; + private int entityId = -1; public ArmorStandWrapper(ShopChest plugin, Location location, String customName, boolean interactable) { this.plugin = plugin; this.location = location; this.customName = customName; - - try { - Object craftWorld = location.getWorld().getClass().cast(location.getWorld()); - nmsWorld = craftWorld.getClass().getMethod("getHandle").invoke(craftWorld); - - entity = entityArmorStandClass.getConstructor(worldClass, double.class, double.class,double.class) - .newInstance(nmsWorld, location.getX(), location.getY(), location.getZ()); - - if (customName != null && !customName.trim().isEmpty()) { - if (Utils.getMajorVersion() < 13) { - entityArmorStandClass.getMethod("setCustomName", String.class).invoke(entity, customName); - } else { - Object chatMessage = chatMessageClass.getConstructor(String.class, Object[].class) - .newInstance(customName, new Object[0]); - - entityArmorStandClass.getMethod("setCustomName", iChatBaseComponentClass).invoke(entity, chatMessage); - } - entityArmorStandClass.getMethod("setCustomNameVisible", boolean.class).invoke(entity, true); - } - - if (Utils.getMajorVersion() < 10) { - entityArmorStandClass.getMethod("setGravity", boolean.class).invoke(entity, false); - } else { - entityArmorStandClass.getMethod("setNoGravity", boolean.class).invoke(entity, true); - } - - entityArmorStandClass.getMethod("setInvisible", boolean.class).invoke(entity, true); - - // Adds the entity to some lists so it can call interact events - // It will also automatically load/unload it when far away - if (interactable) { - Method addEntityMethod = worldServerClass.getDeclaredMethod(Utils.getMajorVersion() == 8 ? "a" : "b", entityClass); - addEntityMethod.setAccessible(true); - addEntityMethod.invoke(worldServerClass.cast(nmsWorld), entity); - } - - uuid = (UUID) entityClass.getMethod("getUniqueID").invoke(entity); - entityId = (int) entityArmorStandClass.getMethod("getId").invoke(entity); - } catch (ReflectiveOperationException e) { - plugin.getLogger().severe("Failed to create line for hologram"); - plugin.debug("Failed to create armor stand"); - plugin.debug(e); - } } public void setVisible(Player player, boolean visible) { try { - Object entityLiving = entityLivingClass.cast(entity); - Object packet; - if (visible) { - packet = packetPlayOutSpawnEntityLivingClass.getConstructor(entityLivingClass).newInstance(entityLiving); - } else { - packet = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[]{entityId}); + Object dataWatcher = Utils.createDataWatcher(customName, null); + entityId = Utils.getFreeEntityId(); + Utils.sendPacket(plugin, Utils.createPacketSpawnEntity(plugin, entityId, UUID.randomUUID(), location, EntityType.ARMOR_STAND), player); + Utils.sendPacket(plugin, packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class) + .newInstance(entityId, dataWatcher, true), player); + } else if (entityId != -1) { + Utils.sendPacket(plugin, packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[]{entityId}), player); } - - Utils.sendPacket(plugin, packet, player); } catch (ReflectiveOperationException e) { plugin.getLogger().severe("Could not change hologram visibility"); plugin.debug("Could not change armor stand visibility"); @@ -104,12 +49,21 @@ public class ArmorStandWrapper { public void setLocation(Location location) { this.location = location; - try { - entityClass.getMethod("setPosition", double.class, double.class, double.class).invoke( - entity, location.getX(), location.getY(), location.getZ()); + Object packet = packetPlayOutEntityTeleportClass.getConstructor().newInstance(); + Field[] fields = packetPlayOutEntityTeleportClass.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + } - Object packet = packetPlayOutEntityTeleportClass.getConstructor(entityClass).newInstance(entity); + boolean isPre9 = Utils.getMajorVersion() < 9; + fields[0].set(packet, entityId); + fields[1].set(packet, isPre9 ? location.getBlockX() : location.getX()); + fields[2].set(packet, isPre9 ? location.getBlockY() : location.getY()); + fields[3].set(packet, isPre9 ? location.getBlockZ() : location.getZ()); + fields[4].set(packet, (byte) 0); + fields[5].set(packet, (byte) 0); + fields[6].set(packet, true); for (Player player : location.getWorld().getPlayers()) { Utils.sendPacket(plugin, packet, player); @@ -123,39 +77,14 @@ public class ArmorStandWrapper { public void setCustomName(String customName) { this.customName = customName; - + Object dataWatcher = Utils.createDataWatcher(customName, null); try { - if (customName != null && !customName.isEmpty()) { - if (Utils.getMajorVersion() < 13) { - entityClass.getMethod("setCustomName", String.class).invoke(entity, customName); - } else { - Object chatMessage = chatMessageClass.getConstructor(String.class, Object[].class) - .newInstance(customName, new Object[0]); - - entityClass.getMethod("setCustomName", iChatBaseComponentClass).invoke(entity, chatMessage); - } - entityClass.getMethod("setCustomNameVisible", boolean.class).invoke(entity, true); - } else { - if (Utils.getMajorVersion() < 13) { - entityClass.getMethod("setCustomName", String.class).invoke(entity, ""); - } else { - Object chatMessage = chatMessageClass.getConstructor(String.class, Object[].class) - .newInstance("", new Object[0]); - - entityClass.getMethod("setCustomName", iChatBaseComponentClass).invoke(entity, chatMessage); - } - entityClass.getMethod("setCustomNameVisible", boolean.class).invoke(entity, false); - } - - Object dataWatcher = entityClass.getMethod("getDataWatcher").invoke(entity); - Object packet = packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class) .newInstance(entityId, dataWatcher, true); for (Player player : location.getWorld().getPlayers()) { Utils.sendPacket(plugin, packet, player); } - } catch (ReflectiveOperationException e) { plugin.getLogger().severe("Could not set hologram text"); plugin.debug("Could not set armor stand custom name"); @@ -164,22 +93,9 @@ public class ArmorStandWrapper { } public void remove() { - for (Player player : Bukkit.getOnlinePlayers()) { + for (Player player : location.getWorld().getPlayers()) { setVisible(player, false); } - - try { - // Removes the entity from the lists it was added to for interaction - Method addEntityMethod = worldServerClass.getDeclaredMethod(Utils.getMajorVersion() == 8 ? "b" : "c", entityClass); - addEntityMethod.setAccessible(true); - addEntityMethod.invoke(worldServerClass.cast(nmsWorld), entity); - - entityClass.getMethod("die").invoke(entity); - } catch (ReflectiveOperationException e) { - plugin.getLogger().severe("Could not remove hologram"); - plugin.debug("Could not remove armor stand from entity lists"); - plugin.debug(e); - } } public int getEntityId() { diff --git a/src/main/java/de/epiceric/shopchest/nms/Hologram.java b/src/main/java/de/epiceric/shopchest/nms/Hologram.java index aac1c09..f3343d2 100644 --- a/src/main/java/de/epiceric/shopchest/nms/Hologram.java +++ b/src/main/java/de/epiceric/shopchest/nms/Hologram.java @@ -86,11 +86,11 @@ public class Hologram { */ public boolean contains(ArmorStand armorStand) { for (ArmorStandWrapper wrapper : wrappers) { - if (wrapper.getUuid().equals(armorStand.getUniqueId())) { + if (armorStand.getUniqueId().equals(wrapper.getUuid())) { return true; } } - return interactArmorStandWrapper != null && interactArmorStandWrapper.getUuid().equals(armorStand.getUniqueId()); + return interactArmorStandWrapper != null && armorStand.getUniqueId().equals(interactArmorStandWrapper.getUuid()); } /** diff --git a/src/main/java/de/epiceric/shopchest/shop/ShopItem.java b/src/main/java/de/epiceric/shopchest/shop/ShopItem.java index d1ffb8a..1358cf4 100644 --- a/src/main/java/de/epiceric/shopchest/shop/ShopItem.java +++ b/src/main/java/de/epiceric/shopchest/shop/ShopItem.java @@ -4,11 +4,10 @@ import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.utils.Utils; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -23,94 +22,43 @@ public class ShopItem { private final ItemStack itemStack; private final Location location; + private int entityId = -1; + private final Class packetPlayOutEntityDestroyClass = Utils.getNMSClass("PacketPlayOutEntityDestroy"); - private final Object[] creationPackets = new Object[3]; - private final Object entityItem; - private final int entityId; + private final Class packetPlayOutEntityVelocityClass = Utils.getNMSClass("PacketPlayOutEntityVelocity"); + private final Class packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata"); + private final Class dataWatcherClass = Utils.getNMSClass("DataWatcher"); + private final Class vec3dClass = Utils.getNMSClass("Vec3D"); + private final Class craftItemStackClass = Utils.getCraftClass("inventory.CraftItemStack"); + private final Class nmsItemStackClass = Utils.getNMSClass("ItemStack"); public ShopItem(ShopChest plugin, ItemStack itemStack, Location location) { this.plugin = plugin; this.itemStack = itemStack; this.location = location; - Class packetPlayOutEntityVelocityClass = Utils.getNMSClass("PacketPlayOutEntityVelocity"); - Class dataWatcherClass = Utils.getNMSClass("DataWatcher"); - Class packetPlayOutEntityMetadataClass = Utils.getNMSClass("PacketPlayOutEntityMetadata"); - Class packetPlayOutSpawnEntityClass = Utils.getNMSClass("PacketPlayOutSpawnEntity"); Class entityClass = Utils.getNMSClass("Entity"); - Class entityItemClass = Utils.getNMSClass("EntityItem"); - Class craftItemStackClass = Utils.getCraftClass("inventory.CraftItemStack"); - Class nmsItemStackClass = Utils.getNMSClass("ItemStack"); - Class craftWorldClass = Utils.getCraftClass("CraftWorld"); - Class nmsWorldClass = Utils.getNMSClass("World"); Class[] requiredClasses = new Class[] { - nmsWorldClass, craftWorldClass, nmsItemStackClass, craftItemStackClass, entityItemClass, - packetPlayOutSpawnEntityClass, packetPlayOutEntityMetadataClass, dataWatcherClass, + nmsItemStackClass, craftItemStackClass, packetPlayOutEntityMetadataClass, dataWatcherClass, packetPlayOutEntityDestroyClass, entityClass, packetPlayOutEntityVelocityClass, }; for (Class c : requiredClasses) { if (c == null) { plugin.debug("Failed to create shop item: Could not find all required classes"); - entityItem = null; - entityId = -1; return; } } - - Object tmpEntityItem = null; - int tmpEntityId = -1; - - try { - Object craftWorld = craftWorldClass.cast(location.getWorld()); - Object nmsWorld = craftWorldClass.getMethod("getHandle").invoke(craftWorld); - - Object nmsItemStack = craftItemStackClass.getMethod("asNMSCopy", ItemStack.class).invoke(null, itemStack); - - Constructor entityItemConstructor = entityItemClass.getConstructor(nmsWorldClass); - tmpEntityItem = entityItemConstructor.newInstance(nmsWorld); - - entityItemClass.getMethod("setPosition", double.class, double.class, double.class).invoke(tmpEntityItem, location.getX(), location.getY(), location.getZ()); - entityItemClass.getMethod("setItemStack", nmsItemStackClass).invoke(tmpEntityItem, nmsItemStack); - if (Utils.getMajorVersion() >= 10) entityItemClass.getMethod("setNoGravity", boolean.class).invoke(tmpEntityItem, true); - - Field ageField = entityItemClass.getDeclaredField("age"); - ageField.setAccessible(true); - ageField.setInt(tmpEntityItem, -32768); - - tmpEntityId = (int) entityItemClass.getMethod("getId").invoke(tmpEntityItem); - Object dataWatcher = entityItemClass.getMethod("getDataWatcher").invoke(tmpEntityItem); - - creationPackets[0] = packetPlayOutSpawnEntityClass.getConstructor(entityClass, int.class).newInstance(tmpEntityItem, 2); - creationPackets[1] = packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class).newInstance(tmpEntityId, dataWatcher, true); - creationPackets[2] = packetPlayOutEntityVelocityClass.getConstructor(int.class, double.class, double.class, double.class).newInstance(tmpEntityId, 0D, 0D, 0D); - } catch (NoSuchMethodException | NoSuchFieldException | InstantiationException | IllegalAccessException | InvocationTargetException e) { - plugin.getLogger().severe("Failed to create shop item"); - plugin.debug("Failed to create shop item with reflection"); - plugin.debug(e); - } - - entityItem = tmpEntityItem; - entityId = tmpEntityId; } /** * @return Clone of the location, where the shop item should be (it could have been moved by something, even though it shouldn't) - * To get the exact location, use reflection and extract the location of the {@code EntityItem} - * which you can get in {@link #getEntityItem()}. */ public Location getLocation() { return location.clone(); } - /** - * @return {@code net.minecraft.server.[VERSION].EntityItem} - */ - public Object getEntityItem() { - return entityItem; - } - /** * @return A clone of this Item's {@link ItemStack} */ @@ -139,9 +87,23 @@ public class ShopItem { */ public void showPlayer(Player p, boolean force) { if (viewers.add(p.getUniqueId()) || force) { - for (Object packet : creationPackets) { - Utils.sendPacket(plugin, packet, p); - } + try { + Object nmsItemStack = craftItemStackClass.getMethod("asNMSCopy", ItemStack.class).invoke(null, itemStack); + Object dataWatcher = Utils.createDataWatcher(null, nmsItemStack); + entityId = Utils.getFreeEntityId(); + Utils.sendPacket(plugin, Utils.createPacketSpawnEntity(plugin, entityId, UUID.randomUUID(), location, EntityType.DROPPED_ITEM), p); + Utils.sendPacket(plugin, packetPlayOutEntityMetadataClass.getConstructor(int.class, dataWatcherClass, boolean.class).newInstance(entityId, dataWatcher, true), p); + if (Utils.getMajorVersion() < 14) { + Utils.sendPacket(plugin, packetPlayOutEntityVelocityClass.getConstructor(int.class, double.class, double.class, double.class).newInstance(entityId, 0D, 0D, 0D), p); + } else { + Object vec3d = vec3dClass.getConstructor(double.class, double.class, double.class).newInstance(0D, 0D, 0D); + Utils.sendPacket(plugin, packetPlayOutEntityVelocityClass.getConstructor(int.class, vec3dClass).newInstance(entityId, vec3d), p); + } + } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException | InstantiationException e) { + plugin.getLogger().severe("Failed to create item!"); + plugin.debug("Failed to create item!"); + plugin.debug(e); + } } } diff --git a/src/main/java/de/epiceric/shopchest/utils/Utils.java b/src/main/java/de/epiceric/shopchest/utils/Utils.java index 7ae73a5..553a437 100644 --- a/src/main/java/de/epiceric/shopchest/utils/Utils.java +++ b/src/main/java/de/epiceric/shopchest/utils/Utils.java @@ -9,16 +9,21 @@ import de.epiceric.shopchest.nms.CustomBookMeta; import de.epiceric.shopchest.nms.JsonBuilder; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.*; +import org.bukkit.util.Vector; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -26,6 +31,9 @@ import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; public class Utils { @@ -326,6 +334,187 @@ public class Utils { } } + /** + * Create a NMS data watcher object to send via a {@code PacketPlayOutEntityMetadata} packet. + * Gravity will be disabled and the custom name will be displayed if available. + * @param customName Custom Name of the entity or {@code null} + * @param nmsItemStack NMS ItemStack or {@code null} if armor stand + */ + public static Object createDataWatcher(String customName, Object nmsItemStack) { + String version = getServerVersion(); + int majorVersion = getMajorVersion(); + + try { + Class entityClass = getNMSClass("Entity"); + Class entityArmorStandClass = getNMSClass("EntityArmorStand"); + Class entityItemClass = getNMSClass("EntityItem"); + Class dataWatcherClass = getNMSClass("DataWatcher"); + Class dataWatcherObjectClass = getNMSClass("DataWatcherObject"); + + byte flags = nmsItemStack == null ? (byte) (1 << 5) : 0; // invisible if armor stand + + Object dataWatcher = dataWatcherClass.getConstructor(entityClass).newInstance((Object) null); + if (majorVersion < 9) { + Method a = dataWatcherClass.getMethod("a", int.class, Object.class); + a.invoke(dataWatcher, 0, flags); // flags + a.invoke(dataWatcher, 1, (short) 300); // air ticks (?) + a.invoke(dataWatcher, 2, (byte) (customName != null ? 1 : 0)); // custom name visible + a.invoke(dataWatcher, 3, customName); // custom name + a.invoke(dataWatcher, 4, (byte) 1); // silent + a.invoke(dataWatcher, 10, nmsItemStack == null ? (byte) 1 : nmsItemStack); // item / no gravity + } else { + Method register = dataWatcherClass.getMethod("register", dataWatcherObjectClass, Object.class); + String[] dataWatcherObjectFieldNames; + + if ("v1_9_R1".equals(version)) { + dataWatcherObjectFieldNames = new String[] {"ax", "ay", "aA", "az", "aB", "a", "c"}; + } else if ("v1_9_R2".equals(version)){ + dataWatcherObjectFieldNames = new String[] {"ay", "az", "aB", "aA", "aC", "a", "c"}; + } else if ("v1_10_R1".equals(version)) { + dataWatcherObjectFieldNames = new String[] {"aa", "az", "aB", "aA", "aC", "aD", "c"}; + } else if ("v1_11_R1".equals(version)) { + dataWatcherObjectFieldNames = new String[] {"Z", "az", "aB", "aA", "aC", "aD", "c"}; + } else if ("v1_12_R1".equals(version) || "v1_12_R2".equals(version)) { + dataWatcherObjectFieldNames = new String[] {"Z", "aA", "aC", "aB", "aD", "aE", "c"}; + } else if ("v1_13_R1".equals(version) || "v1_13_R2".equals(version)) { + dataWatcherObjectFieldNames = new String[] {"ac", "aD", "aF", "aE", "aG", "aH", "b"}; + } else if ("v1_14_R1".equals(version)) { + dataWatcherObjectFieldNames = new String[] {"W", "AIR_TICKS", "aA", "az", "aB", "aC", "ITEM"}; + } else { + return null; + } + + Class f6Class = majorVersion < 9 ? entityArmorStandClass : entityClass; + + Field f1 = entityClass.getDeclaredField(dataWatcherObjectFieldNames[0]); + Field f2 = entityClass.getDeclaredField(dataWatcherObjectFieldNames[1]); + Field f3 = entityClass.getDeclaredField(dataWatcherObjectFieldNames[2]); + Field f4 = entityClass.getDeclaredField(dataWatcherObjectFieldNames[3]); + Field f5 = entityClass.getDeclaredField(dataWatcherObjectFieldNames[4]); + Field f6 = f6Class.getDeclaredField(dataWatcherObjectFieldNames[5]); + Field f7 = entityItemClass.getDeclaredField(dataWatcherObjectFieldNames[6]); + + f1.setAccessible(true); + f2.setAccessible(true); + f3.setAccessible(true); + f4.setAccessible(true); + f5.setAccessible(true); + f6.setAccessible(true); + f7.setAccessible(true); + + register.invoke(dataWatcher, f1.get(null), flags); // flags + register.invoke(dataWatcher, f2.get(null), 300); // air ticks (?) + register.invoke(dataWatcher, f3.get(null), customName != null); // custom name visible + if (majorVersion < 13) register.invoke(dataWatcher, f4.get(null), customName); // custom name + register.invoke(dataWatcher, f5.get(null), true); // silent + + if (nmsItemStack != null) { + register.invoke(dataWatcher, f7.get(null), majorVersion < 11 ? Optional.of(nmsItemStack) : nmsItemStack); // item + } + + if (majorVersion >= 10 || nmsItemStack == null) { + register.invoke(dataWatcher, f6.get(null), true); // no gravity + if (majorVersion >= 13) { + if (customName != null) { + Class chatSerializerClass = Utils.getNMSClass("IChatBaseComponent$ChatSerializer"); + Object iChatBaseComponent = chatSerializerClass.getMethod("a", String.class).invoke(null, JsonBuilder.parse(customName).toString()); + register.invoke(dataWatcher, f4.get(null), Optional.of(iChatBaseComponent)); // custom name + } else { + register.invoke(dataWatcher, f4.get(null), Optional.empty()); // custom name + } + } + } + } + return dataWatcher; + } catch (InstantiationException | InvocationTargetException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) { + ShopChest.getInstance().getLogger().severe("Failed to create data watcher!"); + ShopChest.getInstance().debug("Failed to create data watcher"); + ShopChest.getInstance().debug(e); + } + return null; + } + + /** + * Get a free entity ID for use in {@link #createPacketSpawnEntity(ShopChest, int, UUID, Location, Vector, EntityType)} + * + * @return The id or {@code -1} if a free entity ID could not be retrieved. + */ + public static int getFreeEntityId() { + try { + Class entityClass = getNMSClass("Entity"); + Field entityCountField = entityClass.getDeclaredField("entityCount"); + entityCountField.setAccessible(true); + if (entityCountField.getType() == int.class) { + int id = entityCountField.getInt(null); + entityCountField.setInt(null, id+1); + return id; + } else if (entityCountField.getType() == AtomicInteger.class) { + return ((AtomicInteger) entityCountField.get(null)).incrementAndGet(); + } + + return -1; + } catch (Exception e) { + return -1; + } + } + + /** + * Create a {@code PacketPlayOutSpawnEntity} object. + * Only {@link EntityType#ARMOR_STAND} and {@link EntityType#DROPPED_ITEM} are supported! + */ + public static Object createPacketSpawnEntity(ShopChest plugin, int id, UUID uuid, Location loc, EntityType type) { + try { + Class packetClass = getNMSClass("PacketPlayOutSpawnEntity"); + Object packet = packetClass.getConstructor().newInstance(); + boolean isPre9 = getMajorVersion() < 9; + boolean isPre14 = getMajorVersion() < 14; + + Field[] fields = new Field[12]; + fields[0] = packetClass.getDeclaredField("a"); // ID + fields[1] = packetClass.getDeclaredField("b"); // UUID (Only 1.9+) + fields[2] = packetClass.getDeclaredField(isPre9 ? "b" : "c"); // Loc X + fields[3] = packetClass.getDeclaredField(isPre9 ? "c" : "d"); // Loc Y + fields[4] = packetClass.getDeclaredField(isPre9 ? "d" : "e"); // Loc Z + fields[5] = packetClass.getDeclaredField(isPre9 ? "e" : "f"); // Mot X + fields[6] = packetClass.getDeclaredField(isPre9 ? "f" : "g"); // Mot Y + fields[7] = packetClass.getDeclaredField(isPre9 ? "g" : "h"); // Mot Z + fields[8] = packetClass.getDeclaredField(isPre9 ? "h" : "i"); // Pitch + fields[9] = packetClass.getDeclaredField(isPre9 ? "i" : "j"); // Yaw + fields[10] = packetClass.getDeclaredField(isPre9 ? "j" : "k"); // Type + fields[11] = packetClass.getDeclaredField(isPre9 ? "k" : "l"); // Data + + for (Field field : fields) { + field.setAccessible(true); + } + + Object entityType = null; + if (!isPre14) { + Class entityTypesClass = getNMSClass("EntityTypes"); + entityType = entityTypesClass.getField(type == EntityType.ARMOR_STAND ? "ARMOR_STAND" : "ITEM").get(null); + } + + fields[0].set(packet, id); + if (!isPre9) fields[1].set(packet, uuid); + fields[2].set(packet, isPre9 ? loc.getBlockX() : loc.getX()); + fields[3].set(packet, isPre9 ? loc.getBlockY() : loc.getY()); + fields[4].set(packet, isPre9 ? loc.getBlockZ() : loc.getZ()); + fields[5].set(packet, 0); + fields[6].set(packet, 0); + fields[7].set(packet, 0); + fields[8].set(packet, 0); + fields[9].set(packet, 0); + fields[10].set(packet, isPre14 ? (type == EntityType.ARMOR_STAND ? 78 : 2) : entityType); + fields[11].set(packet, 0); + + return packet; + } catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + plugin.getLogger().severe("Failed to create packet to spawn entity!"); + plugin.debug("Failed to create packet to spawn entity!"); + plugin.debug(e); + return null; + } + } + /** * Send a packet to a player * @param plugin An instance of the {@link ShopChest} plugin