All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cryptomorin.xseries.reflection.XReflection Maven / Gradle / Ivy

There is a newer version: 12.1.0
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2024 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 com.cryptomorin.xseries.reflection;

import com.cryptomorin.xseries.reflection.jvm.classes.DynamicClassHandle;
import com.cryptomorin.xseries.reflection.jvm.classes.StaticClassHandle;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftClassHandle;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftMapping;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftPackage;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.ApiStatus;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * General Java reflection handler, but specialized for Minecraft NMS/CraftBukkit reflection as well.
 * 

*

Starting Points

* Basic reflection starting points are through the {@link com.cryptomorin.xseries.reflection.jvm.classes.ClassHandle} * methods: *
    *
  • {@link #of(Class)}: For static classes with known type at compile time.
  • *
  • {@link #classHandle()}: For general classes that have unknown type at compile time.
  • *
  • {@link #ofMinecraft()}: Specialized for Minecraft-related classes.
  • *
  • {@link #namespaced()}: String-based API for getting classes with Java code inside strings that is more readable.
  • *
*

Fallback

* Some methods exist to choose between different values depending on the situation: *
    *
  • {@link #v(int, Object)}: Basic Minecraft version-based value handler.
  • *
  • {@link #any(ReflectiveHandle[])} and {@link #anyOf(Callable[])}: Advanced fallback-based support for all the reflection operations.
  • *
*

Others

* Also, there are a few other non-reflection APIs in this class that are a bit "hacky" which is why they're here. *
    *
  • {@link #getVersionInformation()}: Useful string to include in your reflection related errors.
  • *
  • {@link #throwCheckedException(Throwable)}: Force throw checked exceptions as unchecked.
  • *
  • {@link #stacktrace(CompletableFuture)}: Add stacktrace information to {@link CompletableFuture}s.
  • *
  • {@link #relativizeSuppressedExceptions(Throwable)}: Relativize the stacktrace of exceptions that are thrown from the same location.
  • *
* @author Crypto Morin * @version 11.2.1 * @see com.cryptomorin.xseries.reflection.minecraft.MinecraftConnection * @see com.cryptomorin.xseries.reflection.minecraft.NMSExtras */ public final class XReflection { /** * 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 *

* This will no longer work because of * Paper no-relocation * strategy. */ @Nullable @ApiStatus.Internal public static final String NMS_VERSION = findNMSVersionString(); /** * The current version of XSeries. Mostly used for the {@link com.cryptomorin.xseries.profiles.builder.XSkull} API. */ @ApiStatus.Internal public static final String XSERIES_VERSION = "11.2.0"; @Nullable @ApiStatus.Internal public static String findNMSVersionString() { // 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 software like catmc. try { Class.forName("org.bukkit.craftbukkit." + found + ".entity.CraftPlayer"); break; } catch (ClassNotFoundException e) { found = null; } } } return found; } public static final int MAJOR_NUMBER; /** * 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. *

    *
  • {@code v1.20.4} to {@code 4}
  • *
  • {@code v1.18.2} to {@code 2}
  • *
  • {@code v1.19.1} to {@code 1}
  • *
*

* 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 { /* Old way of doing this. 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); } */ // NMS_VERSION = v1_20_R3 // Bukkit.getBukkitVersion() = 1.20.4-R0.1-SNAPSHOT // Bukkit.getVersion() = git-Paper-364 (MC: 1.20.4) Matcher bukkitVer = Pattern // is optional for first releases like "1.8-R0.1-SNAPSHOT" .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. String patch = bukkitVer.group("patch"); MAJOR_NUMBER = Integer.parseInt(bukkitVer.group("major")); MINOR_NUMBER = Integer.parseInt(bukkitVer.group("minor")); PATCH_NUMBER = Integer.parseInt((patch == null || patch.isEmpty()) ? "0" : patch); } catch (Throwable ex) { throw new RuntimeException("Failed to parse minor number: " + bukkitVer + ' ' + getVersionInformation(), ex); } } else { throw new IllegalStateException("Cannot parse server version: \"" + Bukkit.getBukkitVersion() + '"'); } } /** * Gets the full version information of the server. Useful for including in errors. * "NMS" might return "Unknown NMS", which means that they're running a Paper * server that removed the CraftBukkit NMS version guard. * * @since 7.0.0 */ public static String getVersionInformation() { // Bukkit.getServer().getMinecraftVersion() is for Paper return "(NMS: " + (NMS_VERSION == null ? "Unknown NMS" : NMS_VERSION) + " | " + "Parsed: " + MAJOR_NUMBER + '.' + MINOR_NUMBER + '.' + PATCH_NUMBER + " | " + "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 */ @Nullable 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 */ 6, /* 21 */ 0, }; if (minorVersion > patches.length) return null; return patches[minorVersion - 1]; } /** * Mojang remapped their NMS in 1.17: Spigot Thread */ @ApiStatus.Internal public static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackage().getName(), NMS_PACKAGE = v(17, "net.minecraft").orElse("net.minecraft.server." + NMS_VERSION); @ApiStatus.Internal @ApiStatus.Experimental public static final Set SUPPORTED_MAPPINGS; static { if (ofMinecraft() .inPackage(MinecraftPackage.NMS, "server.level") .map(MinecraftMapping.MOJANG, "ServerPlayer") .exists()) { SUPPORTED_MAPPINGS = EnumSet.of(MinecraftMapping.MOJANG); } else if (ofMinecraft() .inPackage(MinecraftPackage.NMS, "server.level") .map(MinecraftMapping.MOJANG, "EntityPlayer") .exists()) { SUPPORTED_MAPPINGS = EnumSet.of(MinecraftMapping.SPIGOT, MinecraftMapping.OBFUSCATED); } else { MinecraftClassHandle entityPlayer = ofMinecraft() .inPackage(MinecraftPackage.NMS, "server.level") .map(MinecraftMapping.MOJANG, "ServerPlayer") .map(MinecraftMapping.SPIGOT, "EntityPlayer"); throw new RuntimeException("Unknown Minecraft mapping " + getVersionInformation(), entityPlayer.catchError()); } } private XReflection() {} /** * 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 VersionHandle#orElse(Object)}. * * @see #v(int, int, Object) * @see VersionHandle#orElse(Object) * @since 5.0.0 */ public static VersionHandle v(int version, T handle) { return new VersionHandle<>(version, handle); } /** * @since 9.5.0 */ public static VersionHandle v(int version, int patch, T handle) { return new VersionHandle<>(version, patch, handle); } public static VersionHandle v(int version, Callable handle) { return new VersionHandle<>(version, handle); } public static VersionHandle v(int version, int patch, Callable handle) { return new VersionHandle<>(version, patch, 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; } /** * A more friendly version of {@link #supports(int, int)} for people with OCD. */ public static boolean supports(int majorNumber, int minorNumber, int patchNumber) { if (majorNumber != 1) throw new IllegalArgumentException("Invalid major number: " + majorNumber); return supports(minorNumber, patchNumber); } /** * 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 ? PATCH_NUMBER >= patchNumber : supports(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 * @deprecated use {@link #supports(int, int)} */ @Deprecated 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. * @throws RuntimeException if the class could not be found. * @deprecated use {@link #ofMinecraft()} instead. * @see #getNMSClass(String) * @since 4.0.0 */ @Nonnull @Deprecated 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) { throw new RuntimeException(ex); } } /** * Get a NMS {@link #NMS_PACKAGE} class. * * @param name the name of the class. * @return the NMS class or null if not found. * @throws RuntimeException if the class could not be found. * @see #getNMSClass(String, String) * @since 1.0.0 * @deprecated use {@link #ofMinecraft()} */ @Nonnull @Deprecated public static Class getNMSClass(@Nonnull String name) { return getNMSClass(null, name); } /** * 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. * @throws RuntimeException if the class could not be found. * @since 1.0.0 * @deprecated use {@link #ofMinecraft()} instead. */ @Nonnull @Deprecated public static Class getCraftClass(@Nonnull String name) { try { return Class.forName(CRAFTBUKKIT_PACKAGE + '.' + name); } catch (ClassNotFoundException ex) { throw new RuntimeException(ex); } } /** * 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);
     * }
*

* Note that this doesn't work on primitive classes. * * @param clazz the class to get the array version of. You could use for multi-dimensions arrays too. * @throws RuntimeException if the class could not be found. */ @Nonnull public static Class toArrayClass(Class clazz) { try { return Class.forName("[L" + clazz.getName() + ';'); } catch (ClassNotFoundException ex) { throw new RuntimeException("Cannot find array class for class: " + clazz, ex); } } /** * @since v9.0.0 */ public static MinecraftClassHandle ofMinecraft() { return new MinecraftClassHandle(new ReflectiveNamespace()); } /** * @since v9.0.0 */ public static DynamicClassHandle classHandle() { return new DynamicClassHandle(new ReflectiveNamespace()); } /** * @since v11.0.0 */ public static StaticClassHandle of(Class clazz) { return new StaticClassHandle(new ReflectiveNamespace(), clazz); } /** * Read {@link ReflectiveNamespace} for more info. * @since v11.0.0 */ public static ReflectiveNamespace namespaced() { return new ReflectiveNamespace(); } /** * @since v9.0.0 */ @SafeVarargs public static > AggregateReflectiveHandle any(H... handles) { return new AggregateReflectiveHandle<>(Arrays.stream(handles).map(x -> (Callable) () -> x).collect(Collectors.toList())); } /** * @since v9.0.0 */ @SafeVarargs public static > AggregateReflectiveHandle anyOf(Callable... handles) { return new AggregateReflectiveHandle<>(Arrays.asList(handles)); } /** * Relativize the stacktrace of exceptions that are thrown from the same location. * The suppressed exception's ({@link Throwable#getSuppressed()}) stacktrace are relativized against * the given exceptions stacktrace. *

* This is mostly useful when you have a trial-and-error mechanism that accumulates all the errors * to throw them in case all the attempts have failed. This removes unnecessary line information to * help the developer focus on important, non-repeated lines. * * @param ex the exception to have it's suppressed exceptions relativized. * @return the same exception. * @param the type of the exception. */ @ApiStatus.Experimental public static T relativizeSuppressedExceptions(T ex) { Objects.requireNonNull(ex, "Cannot relativize null exception"); final StackTraceElement[] EMPTY_STACK_TRACE_ARRAY = new StackTraceElement[0]; StackTraceElement[] mainStackTrace = ex.getStackTrace(); for (Throwable suppressed : ex.getSuppressed()) { StackTraceElement[] suppressedStackTrace = suppressed.getStackTrace(); List relativized = new ArrayList<>(10); for (int i = 0; i < suppressedStackTrace.length; i++) { if (mainStackTrace.length <= i) { relativized = null; break; } StackTraceElement mainTrace = mainStackTrace[i]; StackTraceElement suppTrace = suppressedStackTrace[i]; if (mainTrace.equals(suppTrace)) { break; } else { relativized.add(suppTrace); } } if (relativized != null) { // We might not know the line so let's not add this: // if (!relativized.isEmpty()) relativized.remove(relativized.size() - 1); suppressed.setStackTrace(relativized.toArray(EMPTY_STACK_TRACE_ARRAY)); } } return ex; } @SuppressWarnings("unchecked") private static void throwException(Throwable exception) throws T { throw (T) exception; } /** * Throws a checked exception (see {@link Exception}) silently without forcing the programmer to handle it. This is usually considered * a very bad practice, as those errors are meant to be handled, so please use sparingly. You should just * create a {@link RuntimeException} instead and putting the checked exception as a cause if necessary. *

Usage

*
{@code
     *     void doStuff() throws IOException {}
     *
     *     void rethrowAsRuntime() {
     *         try {
     *             doStuff();
     *         } catch (IOException ex) {
     *             throw new RuntimeException(ex);
     *         }
     *     }
     *
     *     void ignoreTheLawsOfJavaQuantumMechanics() {
     *         try {
     *             doStuff();
     *         } catch (IOException ex) {
     *             throw XReflection.throwCheckedException(ex);
     *         }
     *     }
     * }
* @return {@code null}, but it's intended to be thrown, this is a hacky trick to stop the IDE * from complaining about non-terminating statements. */ public static RuntimeException throwCheckedException(Throwable exception) { Objects.requireNonNull(exception, "Cannot throw null exception"); // The following commented statement is not needed because the exception was created somewhere else and the stacktrace reflects that. // exception.setStackTrace(Arrays.stream(exception.getStackTrace()).skip(1).toArray(StackTraceElement[]::new)); throwException(exception); return null; // Trick the compiler to stfu for "throw" terminating statements. } /** * Adds the stacktrace of the current thread in case an error occurs in the given Future. */ @ApiStatus.Experimental public static CompletableFuture stacktrace(@Nonnull CompletableFuture completableFuture) { StackTraceElement[] currentStacktrace = Thread.currentThread().getStackTrace(); return completableFuture.whenComplete((value, ex) -> { // Gets called even when it's completed. if (ex == null) { // This happens if for example someone does: // completableFuture.exceptionally(e -> { e.printStackTrace(); return null; }) completableFuture.complete(value); return; } try { // Remove these: // at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315) // at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320) // at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1770) // at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) // at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) // at java.base/java.lang.Thread.run(Thread.java:1583) StackTraceElement[] exStacktrace = ex.getStackTrace(); if (exStacktrace.length >= 3) { List clearStacktrace = new ArrayList<>(Arrays.asList(exStacktrace)); Collections.reverse(clearStacktrace); Iterator iter = clearStacktrace.iterator(); List watchClassNames = Arrays.asList("java.util.concurrent.CompletableFuture", "java.util.concurrent.ThreadPoolExecutor", "java.util.concurrent.ForkJoinTask", "java.util.concurrent.ForkJoinWorkerThread", "java.util.concurrent.ForkJoinPool"); List watchMethodNames = Arrays.asList("postComplete", "encodeThrowable", "completeThrowable", "tryFire", "run", "runWorker", "scan", "exec", "doExec", "topLevelExec", "uniWhenComplete"); while (iter.hasNext()) { StackTraceElement stackTraceElement = iter.next(); String className = stackTraceElement.getClassName(); String methodName = stackTraceElement.getMethodName(); // Let's keep this just as an indicator. if (className.equals(Thread.class.getName())) continue; if (watchClassNames.stream().anyMatch(className::startsWith) && watchMethodNames.stream().anyMatch(methodName::equals)) { iter.remove(); } else { break; } } Collections.reverse(clearStacktrace); exStacktrace = clearStacktrace.toArray(new StackTraceElement[0]); } // Skip 2 -> the getStackTrace() method + this method StackTraceElement[] finalCurrentStackTrace = Arrays.stream(currentStacktrace).skip(2).toArray(StackTraceElement[]::new); ex.setStackTrace(concatenate(exStacktrace, finalCurrentStackTrace)); } catch (Throwable ex2) { ex.addSuppressed(ex2); } finally { completableFuture.completeExceptionally(ex); } }); } @ApiStatus.Internal public static T[] concatenate(T[] a, T[] b) { int aLen = a.length; int bLen = b.length; @SuppressWarnings("unchecked") T[] c = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), aLen + bLen); System.arraycopy(a, 0, c, 0, aLen); System.arraycopy(b, 0, c, aLen, bLen); return c; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy