Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
89 changes: 89 additions & 0 deletions common/src/main/java/taboolib/common/NMSProxyLoader.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
}
3 changes: 2 additions & 1 deletion common/src/main/java/taboolib/common/PrimitiveLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -238,7 +239,7 @@ static void loadFile(File file, boolean isIsolated, boolean isExternal, List<Str
rel.add(new Relocation(ktc + ".", PrimitiveSettings.getRelocatedKotlinCoroutinesVersion() + "."));
}
// 是否重定向
String hash = PrimitiveIO.getHash(file.getName() + deepHashCode(relocate) + KOTLIN_VERSION + KOTLIN_COROUTINES_VERSION);
String hash = PrimitiveIO.getHash(file.getName() + deepHashCode(relocate) + KOTLIN_VERSION + KOTLIN_COROUTINES_VERSION + SKIP_KOTLIN_RELOCATE);
String name = file.getName().substring(0, file.getName().lastIndexOf('.'));
jar = new File(getCacheFile(), name + "-" + hash.substring(0, 8) + ".jar");
// 文件为空 || 开发模式 || 强制重定向
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package taboolib.common.classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.JarURLConnection;
import java.net.URLConnection;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.jar.Manifest;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
Expand Down Expand Up @@ -124,6 +132,66 @@ public Class<?> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<String>>("lines")!!.toTypedArray()
MinecraftVersion.isHigherOrEqual(MinecraftVersion.V1_17) -> event.packet.read<Array<String>>("lines")!!
MinecraftVersion.isHigherOrEqual(MinecraftVersion.V1_9) -> event.packet.read<Array<String>>("b")!!
else -> event.packet.read<Array<Any>>("b")!!.map { NMSSign.instance.deserialize(it) }.toTypedArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
// 保存字节码用于调试
Expand All @@ -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),将强制重新转译
Expand Down
22 changes: 22 additions & 0 deletions module/bukkit-nms/src/main/kotlin/taboolib/module/nms/NMSProxy.kt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading