diff --git a/nms/reflection/pom.xml b/nms/reflection/pom.xml
index 635de26..253819d 100644
--- a/nms/reflection/pom.xml
+++ b/nms/reflection/pom.xml
@@ -23,6 +23,11 @@
shopchest-nms-interface
provided
+
+ org.inventivetalent
+ reflectionhelper
+ provided
+
\ No newline at end of file
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeArmorStandImpl.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeArmorStandImpl.java
new file mode 100644
index 0000000..6f3d86e
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeArmorStandImpl.java
@@ -0,0 +1,22 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import de.epiceric.shopchest.nms.FakeArmorStand;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+public class FakeArmorStandImpl extends FakeEntityImpl implements FakeArmorStand {
+
+ public FakeArmorStandImpl(ShopChestDebug debug) {
+ super(debug);
+ }
+
+ @Override
+ public void sendData(String name, Iterable receivers) {
+
+ }
+
+ @Override
+ public void setLocation(Location location, Iterable receivers) {
+
+ }
+}
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeEntityImpl.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeEntityImpl.java
new file mode 100644
index 0000000..8d59d69
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeEntityImpl.java
@@ -0,0 +1,33 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import de.epiceric.shopchest.nms.FakeEntity;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+import java.util.UUID;
+
+public class FakeEntityImpl implements FakeEntity {
+
+ protected final int entityId;
+ protected final ShopChestDebug debug;
+
+ public FakeEntityImpl(ShopChestDebug debug) {
+ this.entityId = ReflectionUtils.getFreeEntityId();
+ this.debug = debug;
+ }
+
+ @Override
+ public int getEntityId() {
+ return entityId;
+ }
+
+ @Override
+ public void spawn(UUID uuid, Location location, Iterable receivers) {
+
+ }
+
+ @Override
+ public void remove(Iterable receivers) {
+
+ }
+}
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeItemImpl.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeItemImpl.java
new file mode 100644
index 0000000..2250ad3
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/FakeItemImpl.java
@@ -0,0 +1,22 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import de.epiceric.shopchest.nms.FakeItem;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class FakeItemImpl extends FakeEntityImpl implements FakeItem {
+
+ public FakeItemImpl(ShopChestDebug debug) {
+ super(debug);
+ }
+
+ @Override
+ public void sendData(ItemStack item, Iterable receivers) {
+
+ }
+
+ @Override
+ public void resetVelocity(Iterable receivers) {
+
+ }
+}
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/JsonBuilder.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/JsonBuilder.java
new file mode 100644
index 0000000..3db5cf3
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/JsonBuilder.java
@@ -0,0 +1,243 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+import org.inventivetalent.reflection.resolver.FieldResolver;
+import org.inventivetalent.reflection.resolver.minecraft.NMSClassResolver;
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class JsonBuilder {
+
+ public static class Part {
+ private String value;
+
+ public Part() {
+ this("", true);
+ }
+
+ public Part(Object value) {
+ this(value, value instanceof CharSequence);
+ }
+
+ public Part(Object value, boolean appendQuotes) {
+ if (appendQuotes) {
+ this.value = "\"" + value + "\"";
+ } else {
+ this.value = String.valueOf(value);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ public PartArray toArray() {
+ return new PartArray(this);
+ }
+
+ public PartMap toMap() {
+ PartMap map = new PartMap();
+ map.setValue("text", new Part());
+ map.setValue("extra", toArray());
+ return map;
+ }
+ }
+
+ public static class PartMap extends Part {
+ private Map values = new HashMap<>();
+
+ public PartMap() {
+ }
+
+ public PartMap(Map values) {
+ this.values.putAll(values);
+ }
+
+ public void setValue(String key, Part value) {
+ values.put(key, value);
+ }
+
+ public void removeValue(String key) {
+ values.remove(key);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner joiner = new StringJoiner(",", "{", "}");
+ values.forEach((key, value) -> joiner.add("\"" + key + "\":" + value.toString()));
+ return joiner.toString();
+ }
+
+ @Override
+ public PartMap toMap() {
+ return this;
+ }
+ }
+
+ public static class PartArray extends Part {
+ private List parts = new ArrayList<>();
+
+ public PartArray(Part... parts) {
+ this.parts.addAll(Arrays.asList(parts));
+ }
+
+ public void addPart(Part part) {
+ parts.add(part);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner joiner = new StringJoiner(",", "[", "]");
+ parts.forEach(part -> joiner.add(part.toString()));
+ return joiner.toString();
+ }
+
+ @Override
+ public PartArray toArray() {
+ return this;
+ }
+ }
+
+ private static final Pattern PART_PATTERN = Pattern.compile("((§(?:[a-fA-Fk-oK-OrR0-9]|#[a-fA-F0-9]{6}))+)([^§]*)");
+ private static final Pattern HEX_PATTERN = Pattern.compile("(§[a-fA-F0-9]){6}");
+
+ private Part rootPart;
+ private ShopChestDebug debug;
+
+ private final NMSClassResolver nmsClassResolver = new NMSClassResolver();
+ private Class> iChatBaseComponentClass = nmsClassResolver.resolveSilent("network.chat.IChatBaseComponent");
+ private Class> packetPlayOutChatClass = nmsClassResolver.resolveSilent("network.protocol.game.PacketPlayOutChat");
+ private Class> chatSerializerClass = nmsClassResolver.resolveSilent("ChatSerializer", "network.chat.IChatBaseComponent$ChatSerializer");
+ private Class> chatMessageTypeClass;
+
+ public JsonBuilder(ShopChestDebug debug) {
+ this.debug = debug;
+
+ if (ReflectionUtils.getMajorVersion() >= 16) {
+ chatMessageTypeClass = nmsClassResolver.resolveSilent("network.chat.ChatMessageType");
+ }
+
+ Class>[] requiredClasses = new Class>[] {
+ iChatBaseComponentClass, packetPlayOutChatClass, chatSerializerClass
+ };
+
+ for (Class> c : requiredClasses) {
+ if (c == null) {
+ debug.debug("Failed to instantiate JsonBuilder: Could not find all required classes");
+ return;
+ }
+ }
+ }
+
+ public static Part parse(String text) {
+ Matcher hexMatcher = HEX_PATTERN.matcher(text);
+ while (hexMatcher.find()) {
+ String hexCode = hexMatcher.group(0).replace("§", "");
+ text = text.replace(hexMatcher.group(0), "§#" + hexCode);
+ }
+
+ Matcher matcher = PART_PATTERN.matcher(text);
+
+ if (!matcher.find()) {
+ return new Part(text);
+ }
+
+ matcher.reset();
+
+ PartArray array = new PartArray(new Part());
+ int lastEndIndex = 0;
+
+ while (matcher.find()) {
+ int startIndex = matcher.start();
+ int endIndex = matcher.end();
+
+ if (lastEndIndex != startIndex) {
+ String betweenMatches = text.substring(lastEndIndex, startIndex);
+ array.addPart(new Part(betweenMatches));
+ }
+
+ String format = matcher.group(1);
+ String value = matcher.group(3);
+
+ PartMap part = new PartMap();
+ part.setValue("text", new Part(value));
+
+ String[] formats = format.split("§");
+ for (String f : formats) {
+ switch (f.toLowerCase()) {
+ case "":
+ break;
+ case "k":
+ part.setValue("obuscated", new Part(true));
+ break;
+ case "l":
+ part.setValue("bold", new Part(true));
+ break;
+ case "m":
+ part.setValue("strikethrough", new Part(true));
+ break;
+ case "n":
+ part.setValue("underlined", new Part(true));
+ break;
+ case "o":
+ part.setValue("italic", new Part(true));
+ break;
+ case "r":
+ part.removeValue("obfuscated");
+ part.removeValue("bold");
+ part.removeValue("strikethrough");
+ part.removeValue("underlined");
+ part.removeValue("italic");
+ part.removeValue("color");
+ break;
+ default:
+ if (f.startsWith("#")) {
+ part.setValue("color", new Part(f));
+ } else {
+ part.setValue("color", new Part(ChatColor.getByChar(f).name().toLowerCase()));
+ }
+ }
+ }
+
+ array.addPart(part);
+ lastEndIndex = endIndex;
+ }
+
+ return array;
+ }
+
+ @Override
+ public String toString() {
+ return rootPart.toString();
+ }
+
+ public Part getRootPart() {
+ return rootPart;
+ }
+
+ public void setRootPart(Part rootPart) {
+ this.rootPart = rootPart;
+ }
+
+ public void sendJson(Player p) {
+ try {
+ Object iChatBaseComponent = chatSerializerClass.getMethod("a", String.class).invoke(null, toString());
+ Object packetPlayOutChat = ReflectionUtils.getMajorVersion() < 16
+ ? packetPlayOutChatClass.getConstructor(iChatBaseComponentClass).newInstance(iChatBaseComponent)
+ : packetPlayOutChatClass.getConstructor(iChatBaseComponentClass, chatMessageTypeClass, UUID.class)
+ .newInstance(iChatBaseComponent, (new FieldResolver(chatMessageTypeClass)).resolve("CHAT", "a").get(null), UUID.randomUUID());
+
+ ReflectionUtils.sendPacket(debug, packetPlayOutChat, p);
+ debug.debug("Sent JSON: " + toString());
+ } catch (ReflectiveOperationException e) {
+ debug.getLogger().severe("Failed to send JSON with reflection");
+ debug.debug("Failed to send JSON with reflection: " + toString());
+ debug.debug(e);
+ }
+ }
+
+}
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/PlatformImpl.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/PlatformImpl.java
new file mode 100644
index 0000000..c12948a
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/PlatformImpl.java
@@ -0,0 +1,25 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import de.epiceric.shopchest.nms.FakeArmorStand;
+import de.epiceric.shopchest.nms.FakeItem;
+import de.epiceric.shopchest.nms.Platform;
+
+public class PlatformImpl implements Platform {
+
+ private final ShopChestDebug debug;
+
+ public PlatformImpl(ShopChestDebug debug) {
+ this.debug = debug;
+ }
+
+
+ @Override
+ public FakeArmorStand createFakeArmorStand() {
+ return new FakeArmorStandImpl(debug);
+ }
+
+ @Override
+ public FakeItem createFakeItem() {
+ return new FakeItemImpl(debug);
+ }
+}
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/ReflectionUtils.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/ReflectionUtils.java
new file mode 100644
index 0000000..f32f12f
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/ReflectionUtils.java
@@ -0,0 +1,285 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.inventivetalent.reflection.resolver.FieldResolver;
+import org.inventivetalent.reflection.resolver.minecraft.NMSClassResolver;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ReflectionUtils {
+ static NMSClassResolver nmsClassResolver = new NMSClassResolver();
+ static Class> entityClass = nmsClassResolver.resolveSilent("world.entity.Entity");
+ static Class> entityArmorStandClass = nmsClassResolver.resolveSilent("world.entity.decoration.EntityArmorStand");
+ static Class> entityItemClass = nmsClassResolver.resolveSilent("world.entity.item.EntityItem");
+ static Class> dataWatcherClass = nmsClassResolver.resolveSilent("network.syncher.DataWatcher");
+ static Class> dataWatcherObjectClass = nmsClassResolver.resolveSilent("network.syncher.DataWatcherObject");
+ static Class> chatSerializerClass = nmsClassResolver.resolveSilent("ChatSerializer", "network.chat.IChatBaseComponent$ChatSerializer");
+
+ private ReflectionUtils() {}
+
+ /**
+ * 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(ShopChestDebug debug, String customName, Object nmsItemStack) {
+ String version = getServerVersion();
+ int majorVersion = getMajorVersion();
+
+ try {
+ byte entityFlags = nmsItemStack == null ? (byte) 0b100000 : 0; // invisible if armor stand
+ byte armorStandFlags = nmsItemStack == null ? (byte) 0b10000 : 0; // marker (since 1.8_R2)
+
+ Object dataWatcher = dataWatcherClass.getConstructor(entityClass).newInstance((Object) null);
+ if (majorVersion < 9) {
+ if (getRevision() == 1) armorStandFlags = 0; // Marker not supported on 1.8_R1
+
+ Method a = dataWatcherClass.getMethod("a", int.class, Object.class);
+ a.invoke(dataWatcher, 0, entityFlags); // flags
+ a.invoke(dataWatcher, 1, (short) 300); // air ticks (?)
+ a.invoke(dataWatcher, 3, (byte) (customName != null ? 1 : 0)); // custom name visible
+ a.invoke(dataWatcher, 2, customName != null ? customName : ""); // custom name
+ a.invoke(dataWatcher, 4, (byte) 1); // silent
+ a.invoke(dataWatcher, 10, nmsItemStack == null ? armorStandFlags : nmsItemStack); // item / armor stand flags
+ } 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", null, "c", "a"};
+ } else if ("v1_9_R2".equals(version)){
+ dataWatcherObjectFieldNames = new String[] {"ay", "az", "aB", "aA", "aC", null, "c", "a"};
+ } else if ("v1_10_R1".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"aa", "az", "aB", "aA", "aC", "aD", "c", "a"};
+ } else if ("v1_11_R1".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"Z", "az", "aB", "aA", "aC", "aD", "c", "a"};
+ } else if ("v1_12_R1".equals(version) || "v1_12_R2".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"Z", "aA", "aC", "aB", "aD", "aE", "c", "a"};
+ } else if ("v1_13_R1".equals(version) || "v1_13_R2".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"ac", "aD", "aF", "aE", "aG", "aH", "b", "a"};
+ } else if ("v1_14_R1".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"W", "AIR_TICKS", "aA", "az", "aB", "aC", "ITEM", "b"};
+ } else if ("v1_15_R1".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"T", "AIR_TICKS", "aA", "az", "aB", "aC", "ITEM", "b"};
+ } else if ("v1_16_R1".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"T", "AIR_TICKS", "ay", "ax", "az", "aA", "ITEM", "b"};
+ } else if ("v1_16_R2".equals(version) || "v1_16_R3".equals(version)) {
+ dataWatcherObjectFieldNames = new String[] {"S", "AIR_TICKS", "ar", "aq", "as", "at", "ITEM", "b"};
+ } else {
+ return null;
+ }
+
+ Field fEntityFlags = entityClass.getDeclaredField(dataWatcherObjectFieldNames[0]);
+ Field fAirTicks = entityClass.getDeclaredField(dataWatcherObjectFieldNames[1]);
+ Field fNameVisible = entityClass.getDeclaredField(dataWatcherObjectFieldNames[2]);
+ Field fCustomName = entityClass.getDeclaredField(dataWatcherObjectFieldNames[3]);
+ Field fSilent = entityClass.getDeclaredField(dataWatcherObjectFieldNames[4]);
+ Field fNoGravity = majorVersion >= 10 ? entityClass.getDeclaredField(dataWatcherObjectFieldNames[5]) : null;
+ Field fItem = entityItemClass.getDeclaredField(dataWatcherObjectFieldNames[6]);
+ Field fArmorStandFlags = entityArmorStandClass.getDeclaredField(dataWatcherObjectFieldNames[7]);
+
+ fEntityFlags.setAccessible(true);
+ fAirTicks.setAccessible(true);
+ fNameVisible.setAccessible(true);
+ fCustomName.setAccessible(true);
+ fSilent.setAccessible(true);
+ if (majorVersion >= 10) fNoGravity.setAccessible(true);
+ fItem.setAccessible(true);
+ fArmorStandFlags.setAccessible(true);
+
+ register.invoke(dataWatcher, fEntityFlags.get(null), entityFlags);
+ register.invoke(dataWatcher, fAirTicks.get(null), 300);
+ register.invoke(dataWatcher, fNameVisible.get(null), customName != null);
+ register.invoke(dataWatcher, fSilent.get(null), true);
+ if (majorVersion < 13) register.invoke(dataWatcher, fCustomName.get(null), customName != null ? customName : "");
+
+ if (nmsItemStack != null) {
+ register.invoke(dataWatcher, fItem.get(null), majorVersion < 11 ? com.google.common.base.Optional.of(nmsItemStack) : nmsItemStack);
+ } else {
+ register.invoke(dataWatcher, fArmorStandFlags.get(null), armorStandFlags);
+ }
+
+ if (majorVersion >= 10) {
+ register.invoke(dataWatcher, fNoGravity.get(null), true);
+ if (majorVersion >= 13) {
+ if (customName != null) {
+ Object iChatBaseComponent = chatSerializerClass.getMethod("a", String.class).invoke(null, JsonBuilder.parse(customName).toString());
+ register.invoke(dataWatcher, fCustomName.get(null), Optional.of(iChatBaseComponent));
+ } else {
+ register.invoke(dataWatcher, fCustomName.get(null), Optional.empty());
+ }
+ }
+ }
+ }
+ return dataWatcher;
+ } catch (InstantiationException | InvocationTargetException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) {
+ debug.getLogger().severe("Failed to create data watcher!");
+ debug.debug("Failed to create data watcher");
+ debug.debug(e);
+ }
+ return null;
+ }
+
+ /**
+ * Get a free entity ID for use in {@link #createPacketSpawnEntity(ShopChestDebug, int, UUID, Location, EntityType)}
+ *
+ * @return The id or {@code -1} if a free entity ID could not be retrieved.
+ */
+ public static int getFreeEntityId() {
+ try {
+ Field entityCountField = new FieldResolver(entityClass).resolve("entityCount", "b");
+ 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(ShopChestDebug debug, int id, UUID uuid, Location loc, EntityType type) {
+ try {
+ Class> packetPlayOutSpawnEntityClass = nmsClassResolver.resolveSilent("network.protocol.game.PacketPlayOutSpawnEntity");
+ Class> entityTypesClass = nmsClassResolver.resolveSilent("world.entity.EntityTypes");
+
+ boolean isPre9 = getMajorVersion() < 9;
+ boolean isPre14 = getMajorVersion() < 14;
+
+ double y = loc.getY();
+ if (type == EntityType.ARMOR_STAND && !getServerVersion().equals("v1_8_R1")) {
+ // Marker armor stand => lift by normal armor stand height
+ y += 1.975;
+ }
+
+ Object packet = packetPlayOutSpawnEntityClass.getConstructor().newInstance();
+
+ Field[] fields = new Field[12];
+ fields[0] = packetPlayOutSpawnEntityClass.getDeclaredField("a"); // ID
+ fields[1] = packetPlayOutSpawnEntityClass.getDeclaredField("b"); // UUID (Only 1.9+)
+ fields[2] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "b" : "c"); // Loc X
+ fields[3] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "c" : "d"); // Loc Y
+ fields[4] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "d" : "e"); // Loc Z
+ fields[5] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "e" : "f"); // Mot X
+ fields[6] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "f" : "g"); // Mot Y
+ fields[7] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "g" : "h"); // Mot Z
+ fields[8] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "h" : "i"); // Pitch
+ fields[9] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "i" : "j"); // Yaw
+ fields[10] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "j" : "k"); // Type
+ fields[11] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "k" : "l"); // Data
+
+ for (Field field : fields) {
+ field.setAccessible(true);
+ }
+
+ Object entityType = null;
+ if (!isPre14) {
+ entityType = entityTypesClass.getField(type == EntityType.ARMOR_STAND ? "ARMOR_STAND" : "ITEM").get(null);
+ }
+
+ fields[0].set(packet, id);
+ if (!isPre9) fields[1].set(packet, uuid);
+ if (isPre9) {
+ fields[2].set(packet, (int)(loc.getX() * 32));
+ fields[3].set(packet, (int)(y * 32));
+ fields[4].set(packet, (int)(loc.getZ() * 32));
+ } else {
+ fields[2].set(packet, loc.getX());
+ fields[3].set(packet, y);
+ fields[4].set(packet, 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);
+ if (isPre14) fields[10].set(packet, type == EntityType.ARMOR_STAND ? 78 : 2);
+ else fields[10].set(packet, entityType);
+ fields[11].set(packet, 0);
+
+ return packet;
+ } catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
+ debug.getLogger().severe("Failed to create packet to spawn entity!");
+ debug.debug("Failed to create packet to spawn entity!");
+ debug.debug(e);
+ return null;
+ }
+ }
+
+ /**
+ * Send a packet to a player
+ * @param debug An instance of the {@link ShopChestDebug} debug instance
+ * @param packet Packet to send
+ * @param player Player to which the packet should be sent
+ * @return {@code true} if the packet was sent, or {@code false} if an exception was thrown
+ */
+ public static boolean sendPacket(ShopChestDebug debug, Object packet, Player player) {
+ try {
+ if (packet == null) {
+ debug.debug("Failed to send packet: Packet is null");
+ return false;
+ }
+
+ Class> packetClass = nmsClassResolver.resolveSilent("network.protocol.Packet");
+ if (packetClass == null) {
+ debug.debug("Failed to send packet: Could not find Packet class");
+ return false;
+ }
+
+ Object nmsPlayer = player.getClass().getMethod("getHandle").invoke(player);
+ Field fConnection = (new FieldResolver(nmsPlayer.getClass())).resolve("playerConnection", "b");
+ Object playerConnection = fConnection.get(nmsPlayer);
+
+ playerConnection.getClass().getMethod("sendPacket", packetClass).invoke(playerConnection, packet);
+
+ return true;
+ } catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
+ debug.getLogger().severe("Failed to send packet " + packet.getClass().getName());
+ debug.debug("Failed to send packet " + packet.getClass().getName());
+ debug.debug(e);
+ return false;
+ }
+ }
+
+ /**
+ * @return The current server version with revision number (e.g. v1_9_R2, v1_10_R1)
+ */
+ public static String getServerVersion() {
+ String packageName = Bukkit.getServer().getClass().getPackage().getName();
+
+ return packageName.substring(packageName.lastIndexOf('.') + 1);
+ }
+
+ /**
+ * @return The revision of the current server version (e.g. 2 for v1_9_R2, 1 for v1_10_R1)
+ */
+ public static int getRevision() {
+ return Integer.parseInt(getServerVersion().substring(getServerVersion().length() - 1));
+ }
+
+ /**
+ * @return The major version of the server (e.g. 9 for 1.9.2, 10 for 1.10)
+ */
+ public static int getMajorVersion() {
+ return Integer.parseInt(getServerVersion().split("_")[1]);
+ }
+
+}
diff --git a/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/ShopChestDebug.java b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/ShopChestDebug.java
new file mode 100644
index 0000000..f94ec0a
--- /dev/null
+++ b/nms/reflection/src/main/java/de/epiceric/shopchest/nms/reflection/ShopChestDebug.java
@@ -0,0 +1,30 @@
+package de.epiceric.shopchest.nms.reflection;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+public class ShopChestDebug {
+
+ private final Logger logger;
+ private final Consumer debugConsumer;
+ private final Consumer throwableConsumer;
+
+ public ShopChestDebug(Logger logger, Consumer debugConsumer, Consumer throwableConsumer) {
+ this.logger = logger;
+ this.debugConsumer = debugConsumer;
+ this.throwableConsumer = throwableConsumer;
+ }
+
+ public Logger getLogger() {
+ return logger;
+ }
+
+ public void debug(String message){
+ debugConsumer.accept(message);
+ }
+
+ public void debug(Throwable e){
+ throwableConsumer.accept(e);
+ }
+
+}