diff --git a/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt b/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt index d66670fc2..2858b806e 100644 --- a/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt +++ b/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt @@ -169,8 +169,20 @@ fun URL.getClasses(classLoader: ClassLoader = ClassAppender.getClassLoader()): M .forEach { val className = it.name.replace('/', '.').substringBeforeLast('.') val lc = LazyClass.of(className) { Class.forName(className, false, classLoader) } - classes[className] = ReflexClass.of(lc, jar.getInputStream(it)) + jar.getInputStream(it).use { input -> classes[className] = ReflexClass.of(lc, input) } } + // Gradle 将 NMSProxy 类封装为资源,仍需加入元数据扫描以支持 Awake、事件等注解。 + val proxyIndex = jar.getJarEntry("META-INF/taboolib/nms-proxy/index") + if (proxyIndex != null) { + val names = jar.getInputStream(proxyIndex).bufferedReader(Charsets.UTF_8).use { it.readLines() } + names.filter { it.isNotBlank() }.forEach { className -> + check(!classes.containsKey(className)) { "Duplicate NMSProxy class: $className" } + val entry = jar.getJarEntry("META-INF/taboolib/nms-proxy/${className.replace('.', '/')}.bin") + ?: error("Missing packaged NMSProxy class: $className") + val lc = LazyClass.of(className) { Class.forName(className, false, classLoader) } + jar.getInputStream(entry).use { input -> classes[className] = ReflexClass.of(lc, input) } + } + } } // 保存 BinaryCache.save(srcFile.nameWithoutExtension, srcVersion) { ReflexClassMap.serializeToBytes(classes) } diff --git a/common/src/main/java/taboolib/common/NMSProxyLoader.java b/common/src/main/java/taboolib/common/NMSProxyLoader.java new file mode 100644 index 000000000..c9efbd6d2 --- /dev/null +++ b/common/src/main/java/taboolib/common/NMSProxyLoader.java @@ -0,0 +1,89 @@ +package taboolib.common; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +/** 非隔离模式在业务类加载前转译资源,并交给插件原加载器定义。 */ +public class NMSProxyLoader { + + private static final String PREFIX = "META-INF/taboolib/nms-proxy/"; + private static boolean initialized; + + public static synchronized void init() throws Throwable { + if (initialized || PrimitiveSettings.IS_ISOLATED_MODE) { + return; + } + File source = new File(TabooLib.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + if (!source.isFile()) { + return; + } + // 只读取当前插件的索引,避免读到其他插件的同名资源。 + try (JarFile jar = new JarFile(source)) { + JarEntry index = jar.getJarEntry(PREFIX + "index"); + if (index == null) { + return; + } + ClassLoader loader = ClassAppender.getClassLoader(); + Class runtime = Class.forName("taboolib.module.nms.NMSProxyRuntime", true, loader); + Method transform = runtime.getMethod("transform", String.class, byte[].class); + File translated = File.createTempFile("taboolib-nms-proxy-", ".jar"); + boolean attached = false; + try { + Manifest manifest = jar.getManifest(); + try (JarOutputStream output = manifest == null + ? new JarOutputStream(Files.newOutputStream(translated.toPath())) + : new JarOutputStream(Files.newOutputStream(translated.toPath()), manifest); + BufferedReader names = new BufferedReader(new InputStreamReader(jar.getInputStream(index), StandardCharsets.UTF_8))) { + String name; + while ((name = names.readLine()) != null) { + if (name.trim().isEmpty()) { + continue; + } + String path = name.replace('.', '/'); + JarEntry entry = jar.getJarEntry(PREFIX + path + ".bin"); + if (entry == null) { + throw new IllegalStateException("Missing packaged NMSProxy class: " + name); + } + byte[] bytes; + try (InputStream input = jar.getInputStream(entry); ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { + byte[] chunk = new byte[8192]; + int count; + while ((count = input.read(chunk)) != -1) { + buffer.write(chunk, 0, count); + } + bytes = buffer.toByteArray(); + } + try { + bytes = (byte[]) transform.invoke(null, name, bytes); + } catch (InvocationTargetException ex) { + throw new IllegalStateException("NMSProxy translation failed: " + name, ex.getCause()); + } + output.putNextEntry(new JarEntry(path + ".class")); + output.write(bytes); + output.closeEntry(); + } + } + // 全部转译成功后再注入,父类、内部类和普通 new 均由原加载器解析。 + ClassAppender.addPath(translated.toPath(), false, true); + attached = true; + translated.deleteOnExit(); + initialized = true; + } finally { + if (!attached) { + Files.deleteIfExists(translated.toPath()); + } + } + } + } +} diff --git a/common/src/main/java/taboolib/common/PrimitiveLoader.java b/common/src/main/java/taboolib/common/PrimitiveLoader.java index 0c85b27ee..6e3cc00cb 100644 --- a/common/src/main/java/taboolib/common/PrimitiveLoader.java +++ b/common/src/main/java/taboolib/common/PrimitiveLoader.java @@ -210,6 +210,7 @@ static void loadAll() throws Throwable { load(REPO_TABOOLIB, TABOOLIB_GROUP, i, TABOOLIB_VERSION, IS_ISOLATED_MODE, false, rule); } }); + NMSProxyLoader.init(); PrimitiveIO.debug("所有依赖加载完成,用时 {0} 毫秒。", time); } @@ -238,7 +239,7 @@ static void loadFile(File file, boolean isIsolated, boolean isExternal, List loadClass(String name, boolean resolve, boolean checkParents) th } } + /** Gradle 封装的 NMS 类必须在当前加载器中转译、定义,普通构造调用才会使用同一类型。 */ + @Override + protected Class findClass(String name) throws ClassNotFoundException { + String resourceName = "META-INF/taboolib/nms-proxy/" + name.replace('.', '/') + ".bin"; + URL resource = findResource(resourceName); + if (resource == null) { + return super.findClass(name); + } + try { + URLConnection connection = resource.openConnection(); + byte[] bytes; + try (InputStream input = connection.getInputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int count; + while ((count = input.read(buffer)) != -1) { + output.write(buffer, 0, count); + } + bytes = output.toByteArray(); + } + Class translator = loadClass("taboolib.module.nms.NMSProxyRuntime"); + try { + bytes = (byte[]) translator.getMethod("transform", String.class, byte[].class).invoke(null, name, bytes); + } catch (InvocationTargetException ex) { + throw new IllegalStateException("NMSProxy translation failed: " + name, ex.getCause()); + } + URL location; + Manifest manifest = null; + Certificate[] certificates = null; + if (connection instanceof JarURLConnection) { + JarURLConnection jar = (JarURLConnection) connection; + location = jar.getJarFileURL(); + manifest = jar.getManifest(); + certificates = jar.getJarEntry().getCertificates(); + } else { + String external = resource.toExternalForm(); + location = new URL(external.substring(0, external.length() - resourceName.length())); + } + int separator = name.lastIndexOf('.'); + if (separator != -1) { + String packageName = name.substring(0, separator); + Package existing = getPackage(packageName); + if (existing == null) { + if (manifest == null) { + definePackage(packageName, null, null, null, null, null, null, null); + } else { + definePackage(packageName, manifest, location); + } + } else if (existing.isSealed() && !existing.isSealed(location)) { + throw new SecurityException("Sealing violation: " + packageName); + } + } + return defineClass(name, bytes, 0, bytes.length, new CodeSource(location, certificates)); + } catch (RuntimeException | LinkageError ex) { + throw ex; + } catch (Exception ex) { + // 目标存在但处理失败时禁止回退到父加载器,避免悄悄加载错误的版本。 + throw new IllegalStateException("Cannot load packaged NMSProxy class: " + name, ex); + } + } + private Class findClassOrNull(String name) { try { return findClass(name); diff --git a/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSignListener.kt b/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSignListener.kt index dd3bcbcd4..7e29e88ab 100644 --- a/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSignListener.kt +++ b/module/bukkit-nms/bukkit-nms-stable/src/main/kotlin/taboolib/module/nms/NMSSignListener.kt @@ -46,6 +46,7 @@ internal object NMSSignListener { if ((event.packet.name == "PacketPlayInUpdateSign" || event.packet.name == "ServerboundSignUpdatePacket") && callback.containsKey(event.player.name)) { val function = callback.remove(event.player.name) ?: return val lines = when { + MinecraftVersion.isHigherOrEqual(MinecraftVersion.V26_3) -> event.packet.read>("lines")!!.toTypedArray() MinecraftVersion.isHigherOrEqual(MinecraftVersion.V1_17) -> event.packet.read>("lines")!! MinecraftVersion.isHigherOrEqual(MinecraftVersion.V1_9) -> event.packet.read>("b")!! else -> event.packet.read>("b")!!.map { NMSSign.instance.deserialize(it) }.toTypedArray() diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt index ac2b4dc1b..d81e3c831 100644 --- a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt @@ -65,31 +65,7 @@ class AsmClassTranslation(val source: String) { } // 转译 val (newClass, cost2) = execution { - val classReader = ClassReader(bytes) - val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS) - // 若当前运行环境为非混淆服务端,则不应该进行除 dynamic、requires 外的任何转译操作 - val remapper = if (MinecraftVersion.isUnobfuscated) { - RemapTranslationUnobfuscated() - } else if (MinecraftVersion.isUniversalCraftBukkit) { - // 若当前运行环境为 Paper 时使用新版转换器 - // 若转译对象为 TabooLib 类,需要特殊处理 - if (source.startsWith(taboolibPath)) RemapTranslationTabooLib() else RemapTranslation() - } - // 使用旧版本转译器 - else { - RemapTranslationLegacy( - sourceName = classReader.className.replace('/', '.'), - sourceParents = listOf(classReader.superName, *classReader.interfaces), - ) - } - classReader.accept(ClassRemapper(classWriter, remapper), 0) - var newBytes = classWriter.toByteArray() - // 应用 require 转换(检测并替换 require 调用) - newBytes = remapper.applyRequireTransform(newBytes) - // 应用 dynamic 转换(检测并替换 dynamic() 调用为直接 JVM 指令) - newBytes = remapper.applyDynamicTransform(newBytes) - // 应用额外 transformer(incision 等模块在此织入) - newBytes = remapper.applyExtraTransforms(source, newBytes) + val newBytes = translate(bytes) // 缓存 BinaryCache.save("remap/$source", combinedVersion) { newBytes } // 保存字节码用于调试 @@ -100,6 +76,36 @@ class AsmClassTranslation(val source: String) { return newClass } + /** 转译字节码;original 表示字节码尚未经过 Paper 的插件重映射。 */ + fun translate(bytes: ByteArray, original: Boolean = false): ByteArray { + val classReader = ClassReader(bytes) + val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS) + // 若当前运行环境为非混淆服务端,则不应该进行除 dynamic、requires 外的任何转译操作 + val remapper = if (MinecraftVersion.isUnobfuscated) { + RemapTranslationUnobfuscated() + } else if (MinecraftVersion.isUniversalCraftBukkit) { + // 若当前运行环境为 Paper 时使用新版转换器 + // 若转译对象为 TabooLib 类,需要特殊处理 + if (original || source.startsWith(taboolibPath)) RemapTranslationTabooLib() else RemapTranslation() + } + // 使用旧版本转译器 + else { + RemapTranslationLegacy( + sourceName = classReader.className.replace('/', '.'), + sourceParents = listOf(classReader.superName, *classReader.interfaces), + ) + } + classReader.accept(ClassRemapper(classWriter, remapper), 0) + var newBytes = classWriter.toByteArray() + // 应用 require 转换(检测并替换 require 调用) + newBytes = remapper.applyRequireTransform(newBytes) + // 应用 dynamic 转换(检测并替换 dynamic() 调用为直接 JVM 指令) + newBytes = remapper.applyDynamicTransform(newBytes) + // 应用额外 transformer(incision 等模块在此织入) + newBytes = remapper.applyExtraTransforms(source, newBytes) + return newBytes + } + /** * 获取目标运行环境信息,用于区分不同的服务端环境 * 当环境发生变化时(如从 Spigot 切换到 Arclight),将强制重新转译 diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxy.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxy.kt new file mode 100644 index 000000000..56b6b0af3 --- /dev/null +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxy.kt @@ -0,0 +1,22 @@ +package taboolib.module.nms + +/** + * 由 TabooLib Gradle 插件在打包时封装当前类及其内部类,由运行库按当前服务端版本转译。 + * + * 使用此注解需要配套的 BukkitNMS 模块、Gradle 插件及运行库,无需开启 enableIsolatedClassloader。 + * 保留原类名,使用当前插件对应的类加载器,可以直接调用构造函数,无需抽象代理类。 + * 不缓存实例或读写转译字节码缓存,Kotlin object 仍遵循自身的单例语义。 + * 类的注解元数据仍参与框架扫描,支持 PlatformSide、Requires、Awake 和事件监听。 + * 不会修复目标版本不存在或签名发生变化的 NMS API。业务代码应从 TabooLib 插件入口访问, + * 不应从服务端生成的引导主类或其他插件的类加载器直接引用这些类。 + * + * ```kotlin + * @NMSProxy + * class EntityAccess(val entity: org.bukkit.entity.Entity) { + * fun handle(): Any = (entity as org.bukkit.craftbukkit.v1_16_R3.entity.CraftEntity).handle + * } + * ``` + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class NMSProxy diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxyRuntime.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxyRuntime.kt new file mode 100644 index 000000000..181138484 --- /dev/null +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxyRuntime.kt @@ -0,0 +1,10 @@ +package taboolib.module.nms + +/** 由 NMSProxy 加载入口调用;资源中的原始字节码尚未经过 Paper 的插件重映射。 */ +object NMSProxyRuntime { + + @JvmStatic + fun transform(name: String, bytes: ByteArray): ByteArray { + return AsmClassTranslation(name).translate(bytes, true) + } +}