/* * The MIT License (MIT) * * Copyright (c) 2023 Crypto Morin * * 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.Bukkit; 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.Arrays; import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 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 7.1.0.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. *

* Versions Legacy */ public static final String NMS_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'"); NMS_VERSION = found; } /** * The raw minor version number. * E.g. {@code v1_17_R1} to {@code 17} * * @see #supports(int) * @since 4.0.0 */ public static final int MINOR_NUMBER; /** * The raw patch version number. Refers to the major.minor.patch version scheme. * E.g. *

*

* I'd not recommend developers to support individual patches at all. You should always support the latest patch. * For example, between v1.14.0, v1.14.1, v1.14.2, v1.14.3 and v1.14.4 you should only support v1.14.4 *

* This can be used to warn server owners when your plugin will break on older patches. * * @see #supportsPatch(int) * @since 7.0.0 */ public static final int PATCH_NUMBER; static { String[] split = NMS_VERSION.substring(1).split("_"); if (split.length < 1) { throw new IllegalStateException("Version number division error: " + Arrays.toString(split) + ' ' + getVersionInformation()); } String minorVer = split[1]; try { MINOR_NUMBER = Integer.parseInt(minorVer); if (MINOR_NUMBER < 0) throw new IllegalStateException("Negative minor number? " + minorVer + ' ' + getVersionInformation()); } catch (Throwable ex) { throw new RuntimeException("Failed to parse minor number: " + minorVer + ' ' + getVersionInformation(), ex); } // Bukkit.getBukkitVersion() = "1.12.2-R0.1-SNAPSHOT" Matcher bukkitVer = Pattern.compile("^\\d+\\.\\d+\\.(\\d+)").matcher(Bukkit.getBukkitVersion()); if (bukkitVer.find()) { // matches() won't work, we just want to match the start using "^" try { // group(0) gives the whole matched string, we just want the captured group. PATCH_NUMBER = Integer.parseInt(bukkitVer.group(1)); } catch (Throwable ex) { throw new RuntimeException("Failed to parse minor number: " + bukkitVer + ' ' + getVersionInformation(), ex); } } else { // 1.8-R0.1-SNAPSHOT PATCH_NUMBER = 0; } } /** * Gets the full version information of the server. Useful for including in errors. * * @since 7.0.0 */ public static String getVersionInformation() { return "(NMS: " + NMS_VERSION + " | " + "Minecraft: " + Bukkit.getVersion() + " | " + "Bukkit: " + Bukkit.getBukkitVersion() + ')'; } /** * Gets the latest known patch number of the given minor version. * For example: 1.14 -> 4, 1.17 -> 10 * The latest version is expected to get newer patches, so make sure to account for unexpected results. * * @param minorVersion the minor version to get the patch number of. * @return the patch number of the given minor version if recognized, otherwise null. * @since 7.0.0 */ public static Integer getLatestPatchNumberOf(int minorVersion) { if (minorVersion <= 0) throw new IllegalArgumentException("Minor version must be positive: " + minorVersion); // https://minecraft.wiki/w/Java_Edition_version_history // There are many ways to do this, but this is more visually appealing. int[] patches = { /* 1 */ 1, /* 2 */ 5, /* 3 */ 2, /* 4 */ 7, /* 5 */ 2, /* 6 */ 4, /* 7 */ 10, /* 8 */ 8, // I don't think they released a server version for 1.8.9 /* 9 */ 4, /* 10 */ 2,// ,_ _ _, /* 11 */ 2,// \o-o/ /* 12 */ 2,// ,(.-.), /* 13 */ 2,// _/ |) (| \_ /* 14 */ 4,// /\=-=/\ /* 15 */ 2,// ,| \=/ |, /* 16 */ 5,// _/ \ | / \_ /* 17 */ 1,// \_!_/ /* 18 */ 2, /* 19 */ 4, /* 20 */ 4, }; if (minorVersion > patches.length) return null; return patches[minorVersion - 1]; } /** * Mojang remapped their NMS in 1.17: Spigot Thread */ public static final String CRAFTBUKKIT_PACKAGE = "org.bukkit.craftbukkit." + NMS_VERSION + '.', NMS_PACKAGE = v(17, "net.minecraft.").orElse("net.minecraft.server." + NMS_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; /** * Responsible for getting the NMS handler {@code WorldServer} object for the world. * {@code CraftWorld} is simply a wrapper for {@code WorldServer}. */ 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"); Class playerCommonConnection; if (supports(20) && supportsPatch(2)) { // The packet send method has been abstracted from ServerGamePacketListenerImpl to ServerCommonPacketListenerImpl in 1.20.2 playerCommonConnection = getNMSClass("server.network", "ServerCommonPacketListenerImpl"); } else { playerCommonConnection = playerConnection; } MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle sendPacket = null, getHandle = null, getHandleWorld = null, connection = null; try { connection = lookup.findGetter(entityPlayer, v(20, "c").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(playerCommonConnection, v(20, 2, "b").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() { } /** * Gives the {@code handle} object if the server version is equal or greater than the given version. * This method is purely for readability and should be always used with {@link VersionHandler#orElse(Object)}. * * @see #v(int, int, Object) * @see VersionHandler#orElse(Object) * @since 5.0.0 */ public static VersionHandler v(int version, T handle) { return new VersionHandler<>(version, handle); } /** * Overload for {@link #v(int, T)} that supports patch versions * * @since 9.5.0 */ public static VersionHandler v(int version, int patch, T handle) { return new VersionHandler<>(version, patch, 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 minorNumber the version to compare the server version with. * @return true if the version is equal or newer, otherwise false. * @see #MINOR_NUMBER * @since 4.0.0 */ public static boolean supports(int minorNumber) { return MINOR_NUMBER >= minorNumber; } /** * Checks whether the server version is equal or greater than the given version. * * @param minorNumber the minor version to compare the server version with. * @param patchNumber the patch number to compare the server version with. * @return true if the version is equal or newer, otherwise false. * @see #MINOR_NUMBER * @see #PATCH_NUMBER * @since 7.1.0 */ public static boolean supports(int minorNumber, int patchNumber) { return (MINOR_NUMBER == minorNumber && supportsPatch(patchNumber)) || MINOR_NUMBER > minorNumber; } /** * Checks whether the server version is equal or greater than the given version. * * @param patchNumber the version to compare the server version with. * @return true if the version is equal or newer, otherwise false. * @see #PATCH_NUMBER * @since 7.0.0 */ public static boolean supportsPatch(int patchNumber) { return PATCH_NUMBER >= patchNumber; } /** * Get a NMS (net.minecraft.server) class which accepts a package for 1.17 compatibility. * * @param packageName the 1.17+ package name of this class. * @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(@Nullable String packageName, @Nonnull String name) { if (packageName != null && supports(17)) name = packageName + '.' + name; try { return Class.forName(NMS_PACKAGE + name); } catch (ClassNotFoundException ex) { ex.printStackTrace(); return null; } } /** * Get a NMS {@link #NMS_PACKAGE} 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) { return getNMSClass(null, name); } /** * 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_PACKAGE + name); } catch (ClassNotFoundException ex) { ex.printStackTrace(); return null; } } /** * @deprecated Use {@link #toArrayClass(Class)} instead. */ @Deprecated public static Class getArrayClass(String clazz, boolean nms) { clazz = "[L" + (nms ? NMS_PACKAGE : CRAFTBUKKIT_PACKAGE) + clazz + ';'; try { return Class.forName(clazz); } catch (ClassNotFoundException ex) { ex.printStackTrace(); return null; } } /** * Gives an array version of a class. For example if you wanted {@code EntityPlayer[]} you'd use: *

{@code
     *     Class EntityPlayer = ReflectionUtils.getNMSClass("...", "EntityPlayer");
     *     Class EntityPlayerArray = ReflectionUtils.toArrayClass(EntityPlayer);
     * }
* * @param clazz the class to get the array version of. You could use for multi-dimensions arrays too. */ 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, patch; private T handle; private VersionHandler(int version, T handle) { this(version, 0, handle); } private VersionHandler(int version, int patch, T handle) { if (supports(version) && supportsPatch(patch)) { this.version = version; this.patch = patch; this.handle = handle; } } public VersionHandler v(int version, T handle) { return v(version, 0, handle); } public VersionHandler v(int version, int patch, T handle) { if (version == this.version && patch == this.patch) throw new IllegalArgumentException("Cannot have duplicate version handles for version: " + version + '.' + patch); if (version > this.version && supports(version) && patch >= this.patch && supportsPatch(patch)) { this.version = version; this.patch = patch; this.handle = handle; } return this; } /** * If none of the previous version checks matched, it'll return this object. */ 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; } } } }