From 13573d4add31cf379a25d1c17afc58db3fd6a86a Mon Sep 17 00:00:00 2001 From: SBDeveloper Date: Fri, 1 Jul 2022 13:27:07 +0200 Subject: [PATCH] :recycle: Merged reflection utility classes --- .../mapreflectionapi/MapReflectionAPI.java | 4 +- .../mapreflectionapi/api/MapSender.java | 11 +- .../mapreflectionapi/api/MapWrapper.java | 53 ++- .../utils/ReflectionUtil.java | 267 ++++++++++++ .../utils/ReflectionUtils.java | 382 ------------------ 5 files changed, 300 insertions(+), 417 deletions(-) delete mode 100644 src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtils.java diff --git a/src/main/java/tech/sbdevelopment/mapreflectionapi/MapReflectionAPI.java b/src/main/java/tech/sbdevelopment/mapreflectionapi/MapReflectionAPI.java index aa32b80..85fd0bf 100644 --- a/src/main/java/tech/sbdevelopment/mapreflectionapi/MapReflectionAPI.java +++ b/src/main/java/tech/sbdevelopment/mapreflectionapi/MapReflectionAPI.java @@ -30,7 +30,7 @@ import org.bukkit.plugin.java.JavaPlugin; import tech.sbdevelopment.mapreflectionapi.api.MapManager; import tech.sbdevelopment.mapreflectionapi.listeners.MapListener; import tech.sbdevelopment.mapreflectionapi.listeners.PacketListener; -import tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtils; +import tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtil; import java.util.logging.Level; @@ -66,7 +66,7 @@ public class MapReflectionAPI extends JavaPlugin { getLogger().info("MapReflectionAPI v" + getDescription().getVersion() + ""); getLogger().info("Made by © Copyright SBDevelopment 2022"); - if (!ReflectionUtils.supports(12)) { + if (!ReflectionUtil.supports(12)) { getLogger().severe("MapReflectionAPI only supports Minecraft 1.12 - 1.19!"); Bukkit.getPluginManager().disablePlugin(this); return; diff --git a/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapSender.java b/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapSender.java index ffd694c..388b3d8 100644 --- a/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapSender.java +++ b/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapSender.java @@ -27,7 +27,6 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import tech.sbdevelopment.mapreflectionapi.MapReflectionAPI; import tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtil; -import tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtils; import java.util.ArrayList; import java.util.List; @@ -73,8 +72,8 @@ public class MapSender { }, 0, 2); } - private static final Class packetPlayOutMapClass = ReflectionUtils.getNMSClass("network.protocol.game", "PacketPlayOutMap"); - private static final Class worldMapData = ReflectionUtils.supports(17) ? ReflectionUtils.getNMSClass("world.level.saveddata.maps", "WorldMap") : null; + private static final Class packetPlayOutMapClass = ReflectionUtil.getNMSClass("network.protocol.game", "PacketPlayOutMap"); + private static final Class worldMapData = ReflectionUtil.supports(17) ? ReflectionUtil.getNMSClass("world.level.saveddata.maps", "WorldMap") : null; /** * Send a map to a player @@ -101,7 +100,7 @@ public class MapSender { final int id = -id0; Object packet; - if (ReflectionUtils.supports(17)) { //1.17+ + if (ReflectionUtil.supports(17)) { //1.17+ Object updateData = ReflectionUtil.callConstructor(worldMapData, content.minX, //X pos content.minY, //Y pos @@ -117,7 +116,7 @@ public class MapSender { new ArrayList<>(), //Icons updateData ); - } else if (ReflectionUtils.supports(14)) { //1.16-1.14 + } else if (ReflectionUtil.supports(14)) { //1.16-1.14 packet = ReflectionUtil.callConstructor(packetPlayOutMapClass, id, //ID (byte) 0, //Scale @@ -144,7 +143,7 @@ public class MapSender { ); } - ReflectionUtils.sendPacket(player, packet); + ReflectionUtil.sendPacket(player, packet); } static final class QueuedMap { private final int id; diff --git a/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapWrapper.java b/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapWrapper.java index e10ff64..2e30754 100644 --- a/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapWrapper.java +++ b/src/main/java/tech/sbdevelopment/mapreflectionapi/api/MapWrapper.java @@ -32,7 +32,6 @@ import org.bukkit.metadata.FixedMetadataValue; import tech.sbdevelopment.mapreflectionapi.MapReflectionAPI; import tech.sbdevelopment.mapreflectionapi.api.exceptions.MapLimitExceededException; import tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtil; -import tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtils; import java.util.*; @@ -48,14 +47,14 @@ public class MapWrapper { this.content = image; } - private static final Class craftStackClass = ReflectionUtils.getCraftClass("CraftItemStack"); - private static final Class setSlotPacketClass = ReflectionUtils.getNMSClass("network.protocol.game", "PacketPlayOutSetSlot"); - private static final Class tagCompoundClass = ReflectionUtils.getCraftClass("NBTTagCompound"); - private static final Class entityClass = ReflectionUtils.getNMSClass("world.entity", "Entity"); - private static final Class dataWatcherClass = ReflectionUtils.getNMSClass("network.syncher", "DataWatcher"); - private static final Class entityMetadataPacketClass = ReflectionUtils.getNMSClass("network.protocol.game", "PacketPlayOutEntityMetadata"); - private static final Class entityItemFrameClass = ReflectionUtils.getNMSClass("world.entity.decoration", "EntityItemFrame"); - private static final Class dataWatcherItemClass = ReflectionUtils.getNMSClass("network.syncher", "DataWatcher$Item"); + private static final Class craftStackClass = ReflectionUtil.getCraftClass("CraftItemStack"); + private static final Class setSlotPacketClass = ReflectionUtil.getNMSClass("network.protocol.game", "PacketPlayOutSetSlot"); + private static final Class tagCompoundClass = ReflectionUtil.getCraftClass("NBTTagCompound"); + private static final Class entityClass = ReflectionUtil.getNMSClass("world.entity", "Entity"); + private static final Class dataWatcherClass = ReflectionUtil.getNMSClass("network.syncher", "DataWatcher"); + private static final Class entityMetadataPacketClass = ReflectionUtil.getNMSClass("network.protocol.game", "PacketPlayOutEntityMetadata"); + private static final Class entityItemFrameClass = ReflectionUtil.getNMSClass("world.entity.decoration", "EntityItemFrame"); + private static final Class dataWatcherItemClass = ReflectionUtil.getNMSClass("network.syncher", "DataWatcher$Item"); protected MapController controller = new MapController() { private final Map viewers = new HashMap<>(); @@ -142,17 +141,17 @@ public class MapWrapper { slot = 8 - (slot - 36); } - Object playerHandle = ReflectionUtils.getHandle(player); - Object inventoryMenu = ReflectionUtil.getField(playerHandle, ReflectionUtils.supports(19) ? "bT" : ReflectionUtils.supports(17) ? "bU" : "defaultContainer"); - int windowId = (int) ReflectionUtil.getField(inventoryMenu, ReflectionUtils.supports(17) ? "j" : "windowId"); + Object playerHandle = ReflectionUtil.getHandle(player); + Object inventoryMenu = ReflectionUtil.getField(playerHandle, ReflectionUtil.supports(19) ? "bT" : ReflectionUtil.supports(17) ? "bU" : "defaultContainer"); + int windowId = (int) ReflectionUtil.getField(inventoryMenu, ReflectionUtil.supports(17) ? "j" : "windowId"); - ItemStack stack = new ItemStack(ReflectionUtils.supports(13) ? Material.FILLED_MAP : Material.MAP, 1); + ItemStack stack = new ItemStack(ReflectionUtil.supports(13) ? Material.FILLED_MAP : Material.MAP, 1); Object nmsStack = ReflectionUtil.callMethod(craftStackClass, "asNMSCopy", stack); Object packet; - if (ReflectionUtils.supports(17)) { //1.17+ - int stateId = (int) ReflectionUtil.callMethod(inventoryMenu, ReflectionUtils.supports(18) ? "j" : "getStateId"); + if (ReflectionUtil.supports(17)) { //1.17+ + int stateId = (int) ReflectionUtil.callMethod(inventoryMenu, ReflectionUtil.supports(18) ? "j" : "getStateId"); packet = ReflectionUtil.callConstructor(setSlotPacketClass, windowId, @@ -168,7 +167,7 @@ public class MapWrapper { ); } - ReflectionUtils.sendPacket(player, packet); + ReflectionUtil.sendPacket(player, packet); } @Override @@ -178,7 +177,7 @@ public class MapWrapper { @Override public void showInHand(Player player, boolean force) { - if (player.getInventory().getItemInMainHand().getType() != (ReflectionUtils.supports(13) ? Material.FILLED_MAP : Material.MAP) && !force) + if (player.getInventory().getItemInMainHand().getType() != (ReflectionUtil.supports(13) ? Material.FILLED_MAP : Material.MAP) && !force) return; showInInventory(player, player.getInventory().getHeldItemSlot(), force); } @@ -195,7 +194,7 @@ public class MapWrapper { @Override public void showInFrame(Player player, ItemFrame frame, boolean force) { - if (frame.getItem().getType() != (ReflectionUtils.supports(13) ? Material.FILLED_MAP : Material.MAP) && !force) + if (frame.getItem().getType() != (ReflectionUtil.supports(13) ? Material.FILLED_MAP : Material.MAP) && !force) return; showInFrame(player, frame.getEntityId()); } @@ -209,7 +208,7 @@ public class MapWrapper { public void showInFrame(Player player, int entityId, String debugInfo) { if (!isViewing(player)) return; - ItemStack stack = new ItemStack(ReflectionUtils.supports(13) ? Material.FILLED_MAP : Material.MAP, 1); + ItemStack stack = new ItemStack(ReflectionUtil.supports(13) ? Material.FILLED_MAP : Material.MAP, 1); if (debugInfo != null) { ItemMeta itemMeta = stack.getItemMeta(); itemMeta.setDisplayName(debugInfo); @@ -239,11 +238,11 @@ public class MapWrapper { @Override public ItemFrame getItemFrameById(World world, int entityId) { - Object worldHandle = ReflectionUtils.getHandle(world); - Object nmsEntity = ReflectionUtil.callMethod(worldHandle, ReflectionUtils.supports(18) ? "a" : "getEntity"); + Object worldHandle = ReflectionUtil.getHandle(world); + Object nmsEntity = ReflectionUtil.callMethod(worldHandle, ReflectionUtil.supports(18) ? "a" : "getEntity"); if (nmsEntity == null) return null; - if (!ReflectionUtils.supports(17)) { + if (!ReflectionUtil.supports(17)) { nmsEntity = ReflectionUtil.callMethod(nmsEntity, "getBukkitEntity"); } @@ -253,14 +252,14 @@ public class MapWrapper { private void sendItemFramePacket(Player player, int entityId, ItemStack stack, int mapId) { Object nmsStack = ReflectionUtil.callMethod(craftStackClass, "asNMSCopy", stack); - Object nbtObject = ReflectionUtil.callMethod(nmsStack, ReflectionUtils.supports(19) ? "v" : ReflectionUtils.supports(18) ? "u" : ReflectionUtils.supports(13) ? "getOrCreateTag" : "getTag"); + Object nbtObject = ReflectionUtil.callMethod(nmsStack, ReflectionUtil.supports(19) ? "v" : ReflectionUtil.supports(18) ? "u" : ReflectionUtil.supports(13) ? "getOrCreateTag" : "getTag"); - if (!ReflectionUtils.supports(13) && nbtObject == null) { //1.12 has no getOrCreate, call create if null! + if (!ReflectionUtil.supports(13) && nbtObject == null) { //1.12 has no getOrCreate, call create if null! Object tagCompound = ReflectionUtil.callConstructor(tagCompoundClass); ReflectionUtil.callMethod(nbtObject, "setTag", tagCompound); } - ReflectionUtil.callMethod(nbtObject, ReflectionUtils.supports(18) ? "a" : "setInt", "map", mapId); + ReflectionUtil.callMethod(nbtObject, ReflectionUtil.supports(18) ? "a" : "setInt", "map", mapId); Object dataWatcher = ReflectionUtil.callConstructor(dataWatcherClass, entityClass.cast(null)); Object packet = ReflectionUtil.callConstructor(entityMetadataPacketClass, @@ -270,12 +269,12 @@ public class MapWrapper { ); List list = new ArrayList<>(); - Object dataWatcherObject = ReflectionUtil.getDeclaredField(entityItemFrameClass, ReflectionUtils.supports(17) ? "ao" : ReflectionUtils.supports(14) ? "ITEM" : ReflectionUtils.supports(13) ? "e" : "c"); + Object dataWatcherObject = ReflectionUtil.getDeclaredField(entityItemFrameClass, ReflectionUtil.supports(17) ? "ao" : ReflectionUtil.supports(14) ? "ITEM" : ReflectionUtil.supports(13) ? "e" : "c"); Object dataWatcherItem = ReflectionUtil.callConstructor(dataWatcherItemClass, dataWatcherObject, nmsStack); list.add(dataWatcherItem); ReflectionUtil.setDeclaredField(packet, "b", list); - ReflectionUtils.sendPacket(player, packet); + ReflectionUtil.sendPacket(player, packet); } }; diff --git a/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtil.java b/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtil.java index ef97a64..87f9b91 100644 --- a/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtil.java +++ b/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtil.java @@ -23,14 +23,22 @@ package tech.sbdevelopment.mapreflectionapi.utils; +import org.bukkit.World; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import javax.annotation.Nonnull; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; /** * ReflectionUtil - Reflection handler for NMS and CraftBukkit.
@@ -47,9 +55,131 @@ import java.util.Arrays; * @version 2.1 */ public class ReflectionUtil { + /** + * We use reflection mainly to avoid writing a new class for version barrier. + * The version barrier is for NMS that uses the Minecraft version as the main package name. + *

+ * E.g. EntityPlayer in 1.15 is in the class {@code net.minecraft.server.v1_15_R1} + * but in 1.14 it's in {@code net.minecraft.server.v1_14_R1} + * In order to maintain cross-version compatibility we cannot import these classes. + *

+ * Performance is not a concern for these specific statically initialized values. + */ + public static final String VERSION; + /** + * The raw minor version number. + * E.g. {@code v1_17_R1} to {@code 17} + * + * @since 4.0.0 + */ + public static final int VER = Integer.parseInt(VERSION.substring(1).split("_")[1]); + /** + * Mojang remapped their NMS in 1.17 https://www.spigotmc.org/threads/spigot-bungeecord-1-17.510208/#post-4184317 + */ + public static final String + CRAFTBUKKIT = "org.bukkit.craftbukkit." + VERSION + '.', + NMS = v(17, "net.minecraft.").orElse("net.minecraft.server." + VERSION + '.'); + /** + * A nullable public accessible field only available in {@code EntityPlayer}. + * This can be null if the player is offline. + */ + private static final MethodHandle PLAYER_CONNECTION; + /** + * Responsible for getting the NMS handler {@code EntityPlayer} object for the player. + * {@code CraftPlayer} is simply a wrapper for {@code EntityPlayer}. + * Used mainly for handling packet related operations. + *

+ * This is also where the famous player {@code ping} field comes from! + */ + private static final MethodHandle GET_HANDLE; + private static final MethodHandle GET_HANDLE_WORLD; + /** + * Sends a packet to the player's client through a {@code NetworkManager} which + * is where {@code ProtocolLib} controls packets by injecting channels! + */ + private static final MethodHandle SEND_PACKET; + + static { // This needs to be right below VERSION because of initialization order. + // This package loop is used to avoid implementation-dependant strings like Bukkit.getVersion() or Bukkit.getBukkitVersion() + // which allows easier testing as well. + String found = null; + for (Package pack : Package.getPackages()) { + String name = pack.getName(); + + // .v because there are other packages. + if (name.startsWith("org.bukkit.craftbukkit.v")) { + found = pack.getName().split("\\.")[3]; + + // Just a final guard to make sure it finds this important class. + // As a protection for forge+bukkit implementation that tend to mix versions. + // The real CraftPlayer should exist in the package. + // Note: Doesn't seem to function properly. Will need to separate the version + // handler for NMS and CraftBukkit for softwares like catmc. + try { + Class.forName("org.bukkit.craftbukkit." + found + ".entity.CraftPlayer"); + break; + } catch (ClassNotFoundException e) { + found = null; + } + } + } + if (found == null) + throw new IllegalArgumentException("Failed to parse server version. Could not find any package starting with name: 'org.bukkit.craftbukkit.v'"); + VERSION = found; + } + + static { + Class entityPlayer = getNMSClass("server.level", "EntityPlayer"); + Class worldServer = getNMSClass("server.level", "WorldServer"); + Class craftPlayer = getCraftClass("entity.CraftPlayer"); + Class craftWorld = getCraftClass("CraftWorld"); + Class playerConnection = getNMSClass("server.network", "PlayerConnection"); + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle sendPacket = null, getHandle = null, getHandleWorld = null, connection = null; + + try { + connection = lookup.findGetter(entityPlayer, + v(17, "b").orElse("playerConnection"), playerConnection); + getHandle = lookup.findVirtual(craftPlayer, "getHandle", MethodType.methodType(entityPlayer)); + getHandleWorld = lookup.findVirtual(craftWorld, "getHandle", MethodType.methodType(worldServer)); + sendPacket = lookup.findVirtual(playerConnection, + v(18, "a").orElse("sendPacket"), + MethodType.methodType(void.class, getNMSClass("network.protocol", "Packet"))); + } catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException ex) { + ex.printStackTrace(); + } + + PLAYER_CONNECTION = connection; + SEND_PACKET = sendPacket; + GET_HANDLE = getHandle; + GET_HANDLE_WORLD = getHandleWorld; + } + private ReflectionUtil() { } + /** + * This method is purely for readability. + * No performance is gained. + * + * @since 5.0.0 + */ + public static VersionHandler v(int version, T handle) { + return new VersionHandler<>(version, handle); + } + + /** + * Checks whether the server version is equal or greater than the given version. + * + * @param version the version to compare the server version with. + * @return true if the version is equal or newer, otherwise false. + * @since 4.0.0 + */ + public static boolean supports(int version) { + return VER >= version; + } + private static Class wrapperToPrimitive(Class clazz) { if (clazz == Boolean.class) return boolean.class; if (clazz == Integer.class) return int.class; @@ -162,4 +292,141 @@ public class ReflectionUtil { ex.printStackTrace(); } } + + /** + * Get a NMS (net.minecraft.server) class which accepts a package for 1.17 compatibility. + * + * @param newPackage the 1.17 package name. + * @param name the name of the class. + * @return the NMS class or null if not found. + * @since 4.0.0 + */ + @javax.annotation.Nullable + public static Class getNMSClass(@Nonnull String newPackage, @Nonnull String name) { + if (supports(17)) name = newPackage + '.' + name; + return getNMSClass(name); + } + + /** + * Get a NMS (net.minecraft.server) class. + * + * @param name the name of the class. + * @return the NMS class or null if not found. + * @since 1.0.0 + */ + @javax.annotation.Nullable + public static Class getNMSClass(@Nonnull String name) { + try { + return Class.forName(NMS + name); + } catch (ClassNotFoundException ex) { + ex.printStackTrace(); + return null; + } + } + + /** + * Sends a packet to the player asynchronously if they're online. + * Packets are thread-safe. + * + * @param player the player to send the packet to. + * @param packets the packets to send. + * @return the async thread handling the packet. + * @see #sendPacketSync(Player, Object...) + * @since 1.0.0 + */ + @Nonnull + public static CompletableFuture sendPacket(@Nonnull Player player, @Nonnull Object... packets) { + return CompletableFuture.runAsync(() -> sendPacketSync(player, packets)) + .exceptionally(ex -> { + ex.printStackTrace(); + return null; + }); + } + + /** + * Sends a packet to the player synchronously if they're online. + * + * @param player the player to send the packet to. + * @param packets the packets to send. + * @see #sendPacket(Player, Object...) + * @since 2.0.0 + */ + public static void sendPacketSync(@Nonnull Player player, @Nonnull Object... packets) { + try { + Object handle = GET_HANDLE.invoke(player); + Object connection = PLAYER_CONNECTION.invoke(handle); + + // Checking if the connection is not null is enough. There is no need to check if the player is online. + if (connection != null) { + for (Object packet : packets) SEND_PACKET.invoke(connection, packet); + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + + @javax.annotation.Nullable + public static Object getHandle(@Nonnull Player player) { + Objects.requireNonNull(player, "Cannot get handle of null player"); + try { + return GET_HANDLE.invoke(player); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return null; + } + } + + @javax.annotation.Nullable + public static Object getHandle(@Nonnull World world) { + Objects.requireNonNull(world, "Cannot get handle of null world"); + try { + return GET_HANDLE_WORLD.invoke(world); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return null; + } + } + + /** + * Get a CraftBukkit (org.bukkit.craftbukkit) class. + * + * @param name the name of the class to load. + * @return the CraftBukkit class or null if not found. + * @since 1.0.0 + */ + @javax.annotation.Nullable + public static Class getCraftClass(@Nonnull String name) { + try { + return Class.forName(CRAFTBUKKIT + name); + } catch (ClassNotFoundException ex) { + ex.printStackTrace(); + return null; + } + } + + public static final class VersionHandler { + private int version; + private T handle; + + private VersionHandler(int version, T handle) { + if (supports(version)) { + this.version = version; + this.handle = handle; + } + } + + public VersionHandler v(int version, T handle) { + if (version == this.version) + throw new IllegalArgumentException("Cannot have duplicate version handles for version: " + version); + if (version > this.version && supports(version)) { + this.version = version; + this.handle = handle; + } + return this; + } + + public T orElse(T handle) { + return this.version == 0 ? handle : this.handle; + } + } } \ No newline at end of file diff --git a/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtils.java b/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtils.java deleted file mode 100644 index 795a3d9..0000000 --- a/src/main/java/tech/sbdevelopment/mapreflectionapi/utils/ReflectionUtils.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * This file is part of MapReflectionAPI. - * Copyright (c) 2022 inventivetalent / SBDevelopment - All Rights Reserved - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package tech.sbdevelopment.mapreflectionapi.utils; - -import org.bukkit.World; -import org.bukkit.entity.Player; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; - -/** - * ReflectionUtils - Reflection handler for NMS and CraftBukkit.
- * Caches the packet related methods and is asynchronous. - *

- * This class does not handle null checks as most of the requests are from the - * other utility classes that already handle null checks. - *

- * Clientbound Packets are considered fake - * updates to the client without changing the actual data. Since all the data is handled - * by the server. - *

- * A useful resource used to compare mappings is Mini's Mapping Viewer - * - * @author Crypto Morin - * @version 6.0.1 - */ -public final class ReflectionUtils { - /** - * We use reflection mainly to avoid writing a new class for version barrier. - * The version barrier is for NMS that uses the Minecraft version as the main package name. - *

- * E.g. EntityPlayer in 1.15 is in the class {@code net.minecraft.server.v1_15_R1} - * but in 1.14 it's in {@code net.minecraft.server.v1_14_R1} - * In order to maintain cross-version compatibility we cannot import these classes. - *

- * Performance is not a concern for these specific statically initialized values. - */ - public static final String VERSION; - - static { // This needs to be right below VERSION because of initialization order. - // This package loop is used to avoid implementation-dependant strings like Bukkit.getVersion() or Bukkit.getBukkitVersion() - // which allows easier testing as well. - String found = null; - for (Package pack : Package.getPackages()) { - String name = pack.getName(); - - // .v because there are other packages. - if (name.startsWith("org.bukkit.craftbukkit.v")) { - found = pack.getName().split("\\.")[3]; - - // Just a final guard to make sure it finds this important class. - // As a protection for forge+bukkit implementation that tend to mix versions. - // The real CraftPlayer should exist in the package. - // Note: Doesn't seem to function properly. Will need to separate the version - // handler for NMS and CraftBukkit for softwares like catmc. - try { - Class.forName("org.bukkit.craftbukkit." + found + ".entity.CraftPlayer"); - break; - } catch (ClassNotFoundException e) { - found = null; - } - } - } - if (found == null) - throw new IllegalArgumentException("Failed to parse server version. Could not find any package starting with name: 'org.bukkit.craftbukkit.v'"); - VERSION = found; - } - - /** - * The raw minor version number. - * E.g. {@code v1_17_R1} to {@code 17} - * - * @since 4.0.0 - */ - public static final int VER = Integer.parseInt(VERSION.substring(1).split("_")[1]); - /** - * Mojang remapped their NMS in 1.17 https://www.spigotmc.org/threads/spigot-bungeecord-1-17.510208/#post-4184317 - */ - public static final String - CRAFTBUKKIT = "org.bukkit.craftbukkit." + VERSION + '.', - NMS = v(17, "net.minecraft.").orElse("net.minecraft.server." + VERSION + '.'); - /** - * A nullable public accessible field only available in {@code EntityPlayer}. - * This can be null if the player is offline. - */ - private static final MethodHandle PLAYER_CONNECTION; - /** - * Responsible for getting the NMS handler {@code EntityPlayer} object for the player. - * {@code CraftPlayer} is simply a wrapper for {@code EntityPlayer}. - * Used mainly for handling packet related operations. - *

- * This is also where the famous player {@code ping} field comes from! - */ - private static final MethodHandle GET_HANDLE; - private static final MethodHandle GET_HANDLE_WORLD; - /** - * Sends a packet to the player's client through a {@code NetworkManager} which - * is where {@code ProtocolLib} controls packets by injecting channels! - */ - private static final MethodHandle SEND_PACKET; - - static { - Class entityPlayer = getNMSClass("server.level", "EntityPlayer"); - Class worldServer = getNMSClass("server.level", "WorldServer"); - Class craftPlayer = getCraftClass("entity.CraftPlayer"); - Class craftWorld = getCraftClass("CraftWorld"); - Class playerConnection = getNMSClass("server.network", "PlayerConnection"); - - MethodHandles.Lookup lookup = MethodHandles.lookup(); - MethodHandle sendPacket = null, getHandle = null, getHandleWorld = null, connection = null; - - try { - connection = lookup.findGetter(entityPlayer, - v(17, "b").orElse("playerConnection"), playerConnection); - getHandle = lookup.findVirtual(craftPlayer, "getHandle", MethodType.methodType(entityPlayer)); - getHandleWorld = lookup.findVirtual(craftWorld, "getHandle", MethodType.methodType(worldServer)); - sendPacket = lookup.findVirtual(playerConnection, - v(18, "a").orElse("sendPacket"), - MethodType.methodType(void.class, getNMSClass("network.protocol", "Packet"))); - } catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException ex) { - ex.printStackTrace(); - } - - PLAYER_CONNECTION = connection; - SEND_PACKET = sendPacket; - GET_HANDLE = getHandle; - GET_HANDLE_WORLD = getHandleWorld; - } - - private ReflectionUtils() { - } - - /** - * This method is purely for readability. - * No performance is gained. - * - * @since 5.0.0 - */ - public static VersionHandler v(int version, T handle) { - return new VersionHandler<>(version, handle); - } - - public static CallableVersionHandler v(int version, Callable handle) { - return new CallableVersionHandler<>(version, handle); - } - - /** - * Checks whether the server version is equal or greater than the given version. - * - * @param version the version to compare the server version with. - * @return true if the version is equal or newer, otherwise false. - * @since 4.0.0 - */ - public static boolean supports(int version) { - return VER >= version; - } - - /** - * Get a NMS (net.minecraft.server) class which accepts a package for 1.17 compatibility. - * - * @param newPackage the 1.17 package name. - * @param name the name of the class. - * @return the NMS class or null if not found. - * @since 4.0.0 - */ - @Nullable - public static Class getNMSClass(@Nonnull String newPackage, @Nonnull String name) { - if (supports(17)) name = newPackage + '.' + name; - return getNMSClass(name); - } - - /** - * Get a NMS (net.minecraft.server) class. - * - * @param name the name of the class. - * @return the NMS class or null if not found. - * @since 1.0.0 - */ - @Nullable - public static Class getNMSClass(@Nonnull String name) { - try { - return Class.forName(NMS + name); - } catch (ClassNotFoundException ex) { - ex.printStackTrace(); - return null; - } - } - - /** - * Sends a packet to the player asynchronously if they're online. - * Packets are thread-safe. - * - * @param player the player to send the packet to. - * @param packets the packets to send. - * @return the async thread handling the packet. - * @see #sendPacketSync(Player, Object...) - * @since 1.0.0 - */ - @Nonnull - public static CompletableFuture sendPacket(@Nonnull Player player, @Nonnull Object... packets) { - return CompletableFuture.runAsync(() -> sendPacketSync(player, packets)) - .exceptionally(ex -> { - ex.printStackTrace(); - return null; - }); - } - - /** - * Sends a packet to the player synchronously if they're online. - * - * @param player the player to send the packet to. - * @param packets the packets to send. - * @see #sendPacket(Player, Object...) - * @since 2.0.0 - */ - public static void sendPacketSync(@Nonnull Player player, @Nonnull Object... packets) { - try { - Object handle = GET_HANDLE.invoke(player); - Object connection = PLAYER_CONNECTION.invoke(handle); - - // Checking if the connection is not null is enough. There is no need to check if the player is online. - if (connection != null) { - for (Object packet : packets) SEND_PACKET.invoke(connection, packet); - } - } catch (Throwable throwable) { - throwable.printStackTrace(); - } - } - - @Nullable - public static Object getHandle(@Nonnull Player player) { - Objects.requireNonNull(player, "Cannot get handle of null player"); - try { - return GET_HANDLE.invoke(player); - } catch (Throwable throwable) { - throwable.printStackTrace(); - return null; - } - } - - @Nullable - public static Object getHandle(@Nonnull World world) { - Objects.requireNonNull(world, "Cannot get handle of null world"); - try { - return GET_HANDLE_WORLD.invoke(world); - } catch (Throwable throwable) { - throwable.printStackTrace(); - return null; - } - } - - @Nullable - public static Object getConnection(@Nonnull Player player) { - Objects.requireNonNull(player, "Cannot get connection of null player"); - try { - Object handle = GET_HANDLE.invoke(player); - return PLAYER_CONNECTION.invoke(handle); - } catch (Throwable throwable) { - throwable.printStackTrace(); - return null; - } - } - - /** - * Get a CraftBukkit (org.bukkit.craftbukkit) class. - * - * @param name the name of the class to load. - * @return the CraftBukkit class or null if not found. - * @since 1.0.0 - */ - @Nullable - public static Class getCraftClass(@Nonnull String name) { - try { - return Class.forName(CRAFTBUKKIT + name); - } catch (ClassNotFoundException ex) { - ex.printStackTrace(); - return null; - } - } - - public static Class getArrayClass(String clazz, boolean nms) { - clazz = "[L" + (nms ? NMS : CRAFTBUKKIT) + clazz + ';'; - try { - return Class.forName(clazz); - } catch (ClassNotFoundException ex) { - ex.printStackTrace(); - return null; - } - } - - public static Class toArrayClass(Class clazz) { - try { - return Class.forName("[L" + clazz.getName() + ';'); - } catch (ClassNotFoundException ex) { - ex.printStackTrace(); - return null; - } - } - - public static final class VersionHandler { - private int version; - private T handle; - - private VersionHandler(int version, T handle) { - if (supports(version)) { - this.version = version; - this.handle = handle; - } - } - - public VersionHandler v(int version, T handle) { - if (version == this.version) - throw new IllegalArgumentException("Cannot have duplicate version handles for version: " + version); - if (version > this.version && supports(version)) { - this.version = version; - this.handle = handle; - } - return this; - } - - public T orElse(T handle) { - return this.version == 0 ? handle : this.handle; - } - } - - public static final class CallableVersionHandler { - private int version; - private Callable handle; - - private CallableVersionHandler(int version, Callable handle) { - if (supports(version)) { - this.version = version; - this.handle = handle; - } - } - - public CallableVersionHandler v(int version, Callable handle) { - if (version == this.version) - throw new IllegalArgumentException("Cannot have duplicate version handles for version: " + version); - if (version > this.version && supports(version)) { - this.version = version; - this.handle = handle; - } - return this; - } - - public T orElse(Callable handle) { - try { - return (this.version == 0 ? handle : this.handle).call(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - } -} \ No newline at end of file