/*
* 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 {@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