diff --git a/modules/common/src/main/kotlin/org/polyfrost/spice/util/ClassUtil.kt b/modules/common/src/main/kotlin/org/polyfrost/spice/util/ClassUtil.kt index 9b31c51..5f05d6f 100644 --- a/modules/common/src/main/kotlin/org/polyfrost/spice/util/ClassUtil.kt +++ b/modules/common/src/main/kotlin/org/polyfrost/spice/util/ClassUtil.kt @@ -1,10 +1,25 @@ package org.polyfrost.spice.util +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassReader.SKIP_CODE import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.InvokeDynamicInsnNode import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.MethodNode +object ClassUtil + +private val chainCache = mutableMapOf>() + +fun readClass(name: String): ClassNode? = + ClassNode().also { node -> + ClassReader( + ClassUtil::class.java + .getResourceAsStream("${name.replace(".", "/")}.class") + ?.use { it.readBytes() } ?: return null + ).accept(node, SKIP_CODE) + } + fun getStrings(node: ClassNode): Set = node.methods.map { getStrings(it as MethodNode) @@ -22,3 +37,25 @@ fun getStrings(node: MethodNode): Set = } .flatten().toSet() +@Suppress("NAME_SHADOWING") +fun classChain(clazz: Class<*>): List> { + val chain = mutableListOf>() + var clazz = clazz + + while (clazz.superclass != null) { + chain.add(clazz) + clazz = clazz.superclass + } + + return chain +} + +fun classChain(node: ClassNode): List { + return chainCache.computeIfAbsent(node.name) { + val chain = mutableListOf() + val newNode = readClass(node.superName) ?: return@computeIfAbsent chain + + chain.addAll(classChain(newNode)) + chain + } +} diff --git a/modules/common/src/main/kotlin/org/polyfrost/spice/util/SpiceClassWriter.kt b/modules/common/src/main/kotlin/org/polyfrost/spice/util/SpiceClassWriter.kt new file mode 100644 index 0000000..bc66ab3 --- /dev/null +++ b/modules/common/src/main/kotlin/org/polyfrost/spice/util/SpiceClassWriter.kt @@ -0,0 +1,24 @@ +package org.polyfrost.spice.util + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter + +class SpiceClassWriter : ClassWriter { + @Suppress("unused") + constructor(flags: Int) : super(flags) + + @Suppress("unused") + constructor(reader: ClassReader, flags: Int) + : super(reader, flags) + + override fun getCommonSuperClass(a: String, b: String): String? { + val chainA = classChain(readClass(a) ?: return "java/lang/Object") + val chainB = classChain(readClass(b) ?: return "java/lang/Object") + + val commonSuperClasses = mutableSetOf() + + chainA.forEach { if (chainB.contains(it)) commonSuperClasses += it } + + return commonSuperClasses.firstOrNull() + } +} diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/Cache.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/Cache.kt index 7bfa116..a2fb91b 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/Cache.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/Cache.kt @@ -1,16 +1,16 @@ package org.polyfrost.spice.patcher -import kotlinx.coroutines.coroutineScope import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode import org.polyfrost.spice.patcher.lwjgl.LibraryTransformer import org.polyfrost.spice.patcher.lwjgl.LwjglTransformer import org.polyfrost.spice.spiceDirectory -import org.spongepowered.asm.transformers.MixinClassWriter +import org.polyfrost.spice.util.SpiceClassWriter import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.zip.ZipEntry @@ -51,72 +51,76 @@ fun loadCacheBuffers(hash: String): Map { } } -suspend fun buildCache(hash: String, `in`: List): Map { +fun buildCache(hash: String, `in`: List): Pair, Map> { // todo: abuse coroutines. - return coroutineScope { - val transformers = arrayOf( - LwjglTransformer, - LibraryTransformer - ) + val transformers = arrayOf( + LwjglTransformer, + LibraryTransformer + ) - val transformable = mutableSetOf() - val provider = LwjglTransformer.provider + val transformable = mutableSetOf() + val provider = LwjglTransformer.provider - val transformed = mutableMapOf() - val buffers = mutableMapOf() + val transformed = mutableMapOf() + val buffers = mutableMapOf() - `in`.forEach { node -> - transformable.add(node.name) + `in`.forEach { node -> + transformable.add(node.name) + transformers.forEach transform@{ transformer -> + val targets = transformer.targets - transformers.forEach transform@{ transformer -> - val targets = transformer.getClassNames() + if (targets != null + && !targets.contains(node.name.replace("/", ".")) + ) return@transform - if (targets != null - && !targets.contains(node.name.replace("/", ".")) - ) return@transform + transformer.transform(node) + } - transformer.transform(node) - } + node.methods.forEach { method -> + method as MethodNode - transformed[node.name] = node - buffers["${node.name}.class"] = - MixinClassWriter(COMPUTE_FRAMES) - .also { node.accept(it) } - .toByteArray() + // this fixes stuff because sometimes exceptions are null (????) + if (method.exceptions == null) method.exceptions = mutableListOf() } - provider.allEntries.forEach { entry -> - if (!entry.endsWith(".class") || !entry.startsWith("org/lwjgl/")) return@forEach + transformed[node.name] = node + buffers["${node.name}.class"] = + SpiceClassWriter(COMPUTE_FRAMES) + .also { node.accept(it) } + .toByteArray() + } + + provider.allEntries.forEach { entry -> + if (!entry.endsWith(".class") || !entry.startsWith("org/lwjgl/")) return@forEach - if (!buffers.contains(entry)) { - buffers[entry] = - provider.readFile(entry) ?: return@forEach - } + if (!buffers.contains(entry)) { + buffers[entry] = + provider.readFile(entry) ?: return@forEach } + } - JarOutputStream(cachePath(hash).outputStream()) - .use { out -> - out.putNextEntry(ZipEntry("cache-manifest.json")) - out.write( - Json.encodeToString( - CacheManifest( - transformable.toList() - ) - ).toByteArray(Charsets.UTF_8) - ) + JarOutputStream(cachePath(hash).outputStream()) + .use { out -> + out.putNextEntry(ZipEntry("cache-manifest.json")) + out.write( + Json.encodeToString( + CacheManifest( + transformable.toList() + ) + ).toByteArray(Charsets.UTF_8) + ) + out.closeEntry() + + buffers.forEach { (name, buffer) -> + out.putNextEntry(ZipEntry(name)) + out.write(buffer) out.closeEntry() - - buffers.forEach { (name, buffer) -> - out.putNextEntry(ZipEntry(name)) - out.write(buffer) - out.closeEntry() - } - - out.finish() } - transformed - } + out.finish() + } + + return Pair(transformed, buffers) } private fun cachePath(hash: String) = diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/LunarTransformer.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/LunarTransformer.kt index 6ee4cd1..173fa51 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/LunarTransformer.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/LunarTransformer.kt @@ -7,9 +7,7 @@ import org.polyfrost.spice.platform.api.IClassTransformer import org.polyfrost.spice.util.getStrings object LunarTransformer : IClassTransformer { - override fun getClassNames(): Array? { - return null - } + override val targets = null override fun transform(node: ClassNode) { if (!node.name.startsWith("com/moonsworth/lunar/")) return diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/OptifineTransformer.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/OptifineTransformer.kt index 8484706..749fd88 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/OptifineTransformer.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/OptifineTransformer.kt @@ -7,9 +7,7 @@ import org.objectweb.asm.tree.MethodNode import org.polyfrost.spice.platform.api.IClassTransformer object OptifineTransformer : IClassTransformer { - override fun getClassNames(): Array { - return arrayOf("net.optifine.shaders.Shaders") - } + override val targets = arrayOf("net.optifine.shaders.Shaders") override fun transform(node: ClassNode) { node.methods.forEach { method -> diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LibraryTransformer.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LibraryTransformer.kt index cd53682..95b7258 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LibraryTransformer.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LibraryTransformer.kt @@ -6,9 +6,7 @@ import org.objectweb.asm.tree.MethodNode import org.polyfrost.spice.platform.api.IClassTransformer object LibraryTransformer : IClassTransformer { - override fun getClassNames(): Array { - return arrayOf("org.lwjgl.system.Library") - } + override val targets = arrayOf("org.lwjgl.system.Library") override fun transform(node: ClassNode) { node.methods.forEach { method -> @@ -19,4 +17,4 @@ object LibraryTransformer : IClassTransformer { .forEach { (it as LdcInsnNode).cst = "spice.library.path" } } } -} \ No newline at end of file +} diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglProvider.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglProvider.kt index 1390e0b..ac0e2c0 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglProvider.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglProvider.kt @@ -1,8 +1,5 @@ package org.polyfrost.spice.patcher.lwjgl -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import org.objectweb.asm.ClassReader import org.objectweb.asm.tree.ClassNode import org.polyfrost.spice.util.UrlByteArrayConnection @@ -14,7 +11,6 @@ import java.security.MessageDigest import java.util.jar.JarInputStream class LwjglProvider { - private val cacheLock = Mutex() private val fileCache = mutableMapOf() private val jar by lazy { JarInputStream(openStream() ?: return@lazy null) } @@ -73,44 +69,36 @@ class LwjglProvider { ?.openStream() private fun readEntryUntil(path: String?): ByteArray? { - return runBlocking { - if (closed || jar == null) return@runBlocking null - - while (true) { - val entry = jar!!.nextEntry ?: run { - jar!!.close() - closed = true - - return@runBlocking null - } - - if (entry.isDirectory) continue + if (closed || jar == null) return null - val length = entry.size.toInt() - val entryBuffer = ByteArray(length) + while (true) { + val entry = jar!!.nextEntry ?: run { + jar!!.close() + closed = true - var offset = 0 + return null + } - while (true) { - val read = jar!!.read(entryBuffer, offset, length - offset) + val length = entry.size.toInt() - offset += read + if (entry.isDirectory) continue + if (length == -1) continue - if (offset == length) break - } + val entryBuffer = ByteArray(length) + var offset = 0 - jar!!.closeEntry() + while (true) { + val read = jar!!.read(entryBuffer, offset, length - offset) - cacheLock.withLock { - fileCache[entry.name] = entryBuffer - } + offset += read - if (path != null && entry.name == path) return@runBlocking entryBuffer + if (offset == length) break } - // compiler isn't smart enough - @Suppress("UNREACHABLE_CODE") - null + jar!!.closeEntry() + fileCache[entry.name] = entryBuffer + + if (path != null && entry.name == path) return entryBuffer } } } diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglTransformer.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglTransformer.kt index 2f09299..2ed1134 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglTransformer.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/patcher/lwjgl/LwjglTransformer.kt @@ -10,13 +10,10 @@ import org.polyfrost.spice.platform.api.IClassTransformer object LwjglTransformer : IClassTransformer { private val logger = LogManager.getLogger("Spice/Transformer")!! + override val targets = null val provider = LwjglProvider() - override fun getClassNames(): Array? { - return null - } - override fun transform(node: ClassNode) { if (!node.name.startsWith("org/lwjgl")) return if (node.name == "org/lwjgl/opengl/PixelFormat") return diff --git a/modules/core/src/main/kotlin/org/polyfrost/spice/platform/api/IClassTransformer.kt b/modules/core/src/main/kotlin/org/polyfrost/spice/platform/api/IClassTransformer.kt index 68b25fb..1798db4 100644 --- a/modules/core/src/main/kotlin/org/polyfrost/spice/platform/api/IClassTransformer.kt +++ b/modules/core/src/main/kotlin/org/polyfrost/spice/platform/api/IClassTransformer.kt @@ -8,6 +8,7 @@ interface IClassTransformer { * If null, the transformer will transform all classes * Format like net.minecraft.client.Minecraft, not like net/minecraft/client/Minecraft */ - fun getClassNames(): Array? + val targets: Array? + fun transform(node: ClassNode) } diff --git a/versions/1.8.9-fabric/src/main/kotlin/org/polyfrost/spice/platform/impl/fabric/asm/TransformerPlugin.kt b/versions/1.8.9-fabric/src/main/kotlin/org/polyfrost/spice/platform/impl/fabric/asm/TransformerPlugin.kt index 8851d57..c1e7b92 100644 --- a/versions/1.8.9-fabric/src/main/kotlin/org/polyfrost/spice/platform/impl/fabric/asm/TransformerPlugin.kt +++ b/versions/1.8.9-fabric/src/main/kotlin/org/polyfrost/spice/platform/impl/fabric/asm/TransformerPlugin.kt @@ -1,7 +1,6 @@ package org.polyfrost.spice.platform.impl.fabric.asm import com.google.common.base.Stopwatch -import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager import org.objectweb.asm.ClassReader import org.objectweb.asm.tree.ClassNode @@ -39,6 +38,8 @@ class TransformerPlugin : IMixinConfigPlugin, Transformer { @Suppress("UnstableApiUsage") override fun onLoad(mixinPackage: String) { + logger.info("Initializing Fabric transformer") + loader = javaClass.classLoader addUrl = loader .javaClass @@ -92,37 +93,45 @@ class TransformerPlugin : IMixinConfigPlugin, Transformer { mixinClassName: String, mixinInfo: IMixinInfo ) { + val cached = transformerCache[targetClass.name] + if (transformedClasses.contains(targetClassName)) return + if (cached != null) { + targetClass.version = cached.version + targetClass.access = cached.access + targetClass.name = cached.name + targetClass.signature = cached.signature + targetClass.superName = cached.superName + targetClass.interfaces = cached.interfaces + targetClass.sourceFile = cached.sourceFile + targetClass.sourceDebug = cached.sourceDebug + targetClass.module = cached.module + targetClass.outerClass = cached.outerClass + targetClass.outerMethod = cached.outerMethod + targetClass.outerMethodDesc = cached.outerMethodDesc + targetClass.visibleAnnotations = cached.visibleAnnotations + targetClass.invisibleAnnotations = cached.invisibleAnnotations + targetClass.visibleTypeAnnotations = cached.visibleTypeAnnotations + targetClass.invisibleTypeAnnotations = cached.invisibleTypeAnnotations + targetClass.attrs = cached.attrs + targetClass.innerClasses = cached.innerClasses + targetClass.nestHostClass = cached.nestHostClass + targetClass.nestMembers = cached.nestMembers + targetClass.permittedSubclasses = cached.permittedSubclasses + targetClass.recordComponents = cached.recordComponents + targetClass.fields = cached.fields + targetClass.methods = cached.methods + } - transformers.forEach { it.transform(targetClass) } - transformedClasses += targetClassName + transformers + .filter { + val targets = it.targets - val cached = transformerCache[targetClass.name] ?: return - - targetClass.version = cached.version - targetClass.access = cached.access - targetClass.name = cached.name - targetClass.signature = cached.signature - targetClass.superName = cached.superName - targetClass.interfaces = cached.interfaces - targetClass.sourceFile = cached.sourceFile - targetClass.sourceDebug = cached.sourceDebug - targetClass.module = cached.module - targetClass.outerClass = cached.outerClass - targetClass.outerMethod = cached.outerMethod - targetClass.outerMethodDesc = cached.outerMethodDesc - targetClass.visibleAnnotations = cached.visibleAnnotations - targetClass.invisibleAnnotations = cached.invisibleAnnotations - targetClass.visibleTypeAnnotations = cached.visibleTypeAnnotations - targetClass.invisibleTypeAnnotations = cached.invisibleTypeAnnotations - targetClass.attrs = cached.attrs - targetClass.innerClasses = cached.innerClasses - targetClass.nestHostClass = cached.nestHostClass - targetClass.nestMembers = cached.nestMembers - targetClass.permittedSubclasses = cached.permittedSubclasses - targetClass.recordComponents = cached.recordComponents - targetClass.fields = cached.fields - targetClass.methods = cached.methods + targets == null + || targets.contains(targetClassName.replace("/", ".")) + } + .forEach { it.transform(targetClass) } + transformedClasses += targetClassName } override fun postApply( @@ -153,6 +162,7 @@ class TransformerPlugin : IMixinConfigPlugin, Transformer { val stopwatch = Stopwatch.createUnstarted() return if (!isCached(hash)) { + logger.info("Cache does not contain an entry for $hash, beginning build") logger.info("Searching for LWJGL classes") stopwatch.start() @@ -166,7 +176,6 @@ class TransformerPlugin : IMixinConfigPlugin, Transformer { stopwatch.stop() logger.info("Found ${resources.size} LWJGL classes in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms") - logger.info("Building cache: $hash") stopwatch.reset() stopwatch.start() @@ -181,15 +190,16 @@ class TransformerPlugin : IMixinConfigPlugin, Transformer { logger.info("Transforming ${classes.size} classes") - val transformed = runBlocking { buildCache(hash, classes) } + val transformed = buildCache(hash, classes) stopwatch.stop() + + logger.info("Transformed ${transformed.first.size}/${classes.size} classes") logger.info("Built cache in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms") - transformed + transformed.first } else { - logger.info("Loading cache: $hash") - + logger.info("Loading classes from cache $hash") stopwatch.start() val cache = loadCache(hash) diff --git a/versions/build.gradle.kts b/versions/build.gradle.kts index d2d1860..6a8d1e1 100644 --- a/versions/build.gradle.kts +++ b/versions/build.gradle.kts @@ -1,7 +1,6 @@ @file:Suppress("UnstableApiUsage") import org.polyfrost.gradle.util.noServerRunConfigs -import org.polyfrost.gradle.util.prebundle plugins { kotlin("jvm") version libs.versions.kotlin.get() @@ -12,7 +11,9 @@ plugins { id("com.github.johnrengelman.shadow") `java-library` } + val tweakClass = "org.spongepowered.asm.launch.MixinTweaker" +val transformerPlugin = "org.polyfrost.spice.platform.impl.forge.TransformerPlugin" base.archivesName = "Spice-${platform}" @@ -25,7 +26,7 @@ loom { runConfigs { "client" { if (project.platform.isLegacyForge) { - property("fml.coreMods.load", "org.polyfrost.spice.platform.impl.forge.asm.TransformerPlugin") + property("fml.coreMods.load", transformerPlugin) programArgs("--tweakClass", tweakClass) } property("mixin.debug.export", "true") @@ -66,9 +67,6 @@ dependencies { shadowImpl(rootProject.libs.kotlinx.coroutines) if (platform.isLegacyForge) { - val configuration = configurations.create("tempLwjglConfiguration") - compileOnly(configuration(project(":modules:lwjgl"))!!) - shadowImpl(prebundle(configuration, "lwjgl.jar")) shadowImpl(rootProject.libs.kotlinx.serialization.json) shadowImpl(project(":modules:common")) { isTransitive = false @@ -107,7 +105,7 @@ tasks { manifest { attributes += mapOf( "FMLCorePluginContainsFMLMod" to "Yes, yes it does", - "FMLCorePlugin" to "org.polyfrost.spice.platform.impl.forge.asm.TransformerPlugin", + "FMLCorePlugin" to transformerPlugin, "ModSide" to "CLIENT", "ForceLoadAsMod" to true, "TweakOrder" to "0", @@ -146,6 +144,14 @@ tasks { } processResources { + from( + project(":modules:lwjgl") + .tasks + .shadowJar + .get() + .archiveFile + ) + inputs.property("id", rootProject.properties["mod_id"].toString()) inputs.property("name", rootProject.name) inputs.property("java", 8) diff --git a/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/ClassTransformer.kt b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/ClassTransformer.kt index 95019fd..0cee88e 100644 --- a/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/ClassTransformer.kt +++ b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/ClassTransformer.kt @@ -1,93 +1,152 @@ package org.polyfrost.spice.platform.impl.forge.asm //#if FORGE -import com.google.common.collect.ArrayListMultimap -import com.google.common.collect.Multimap +import com.google.common.base.Stopwatch +import net.minecraft.launchwrapper.LaunchClassLoader +import net.minecraft.launchwrapper.LogWrapper +import org.apache.logging.log4j.LogManager import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassWriter +import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES import org.objectweb.asm.tree.ClassNode +import org.polyfrost.spice.patcher.buildCache +import org.polyfrost.spice.patcher.currentHash +import org.polyfrost.spice.patcher.isCached +import org.polyfrost.spice.patcher.loadCacheBuffers import org.polyfrost.spice.platform.api.IClassTransformer import org.polyfrost.spice.platform.api.Transformer import org.polyfrost.spice.platform.bootstrapTransformer -import java.io.File -import java.io.FileOutputStream -import java.io.IOException +import org.polyfrost.spice.platform.impl.forge.util.LaunchWrapperLogger +import org.polyfrost.spice.platform.impl.forge.util.collectResources +import org.polyfrost.spice.util.SpiceClassWriter import java.net.URL +import java.util.concurrent.TimeUnit +import net.minecraft.launchwrapper.IClassTransformer as LaunchTransformer -class ClassTransformer : net.minecraft.launchwrapper.IClassTransformer, Transformer { - - private val transformerMap: Multimap = - ArrayListMultimap.create() - private val transformers: MutableList = ArrayList() +@Suppress("UnstableApiUsage") +class ClassTransformer : LaunchTransformer, Transformer { private val outputBytecode = System.getProperty("debugBytecode", "false").toBoolean() + private val transformerCache: Map + + private val loader = + ClassTransformer::class.java + .classLoader + .let { + assert(it is LaunchClassLoader) { "Class loader isn't LaunchClassLoader..?" } + + it as LaunchClassLoader + } + + private val logger = LogManager.getLogger("Spice/Forge/Transformer") + private val transformers = mutableListOf() + init { + logger.info("Initializing Forge transformer") + logger.info("Removing LWJGL exclusion") + + @Suppress("UNCHECKED_CAST") + val exclusions = loader::class.java + .getDeclaredField("classLoaderExceptions") + .also { it.isAccessible = true } + .get(loader) as MutableSet + + exclusions.remove("org.lwjgl.") + + LogWrapper.retarget(LaunchWrapperLogger) + + val stopwatch = Stopwatch.createStarted() + + transformerCache = loadCache() bootstrapTransformer(this) - } - private fun registerTransformer(transformer: IClassTransformer) { - val classes = transformer.getClassNames() - if (classes == null) { - transformers.add(transformer) - } else { - for (cls in classes) { - transformerMap.put(cls, transformer) - } - } + logger.info("Ready in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms") } override fun transform(name: String, transformedName: String, bytes: ByteArray?): ByteArray? { if (bytes == null) return null - val transformers = transformerMap[transformedName] - transformers.addAll(this.transformers) - if (transformers.isEmpty()) return bytes - val classReader = ClassReader(bytes) - val classNode = ClassNode() - classReader.accept(classNode, ClassReader.EXPAND_FRAMES) - for (transformer in transformers) { - transformer.transform(classNode) - } - val classWriter = ClassWriter(ClassWriter.COMPUTE_FRAMES) - try { - classNode.accept(classWriter) - } catch (e: Throwable) { - e.printStackTrace() - } - if (outputBytecode) { - val bytecodeDirectory = File("bytecode") - - // anonymous classes - val transformedClassName = if (transformedName.contains("$")) { - transformedName.replace('$', '.') + ".class" - } else { - "$transformedName.class" - } - val bytecodeOutput = File(bytecodeDirectory, transformedClassName) - try { - if (!bytecodeDirectory.exists()) { - bytecodeDirectory.mkdirs() - } - if (!bytecodeOutput.exists()) { - bytecodeOutput.createNewFile() - } - } catch (e: Exception) { - e.printStackTrace() - } - try { - FileOutputStream(bytecodeOutput).use { os -> os.write(classWriter.toByteArray()) } - } catch (e: IOException) { - e.printStackTrace() - } + + @Suppress("NAME_SHADOWING") + val bytes = transformerCache[name.replace(".", "/")] ?: bytes + val validTransformers = transformers.filter { + val targets = it.targets + + targets == null + || targets.contains(name) } - return classWriter.toByteArray() + + return if (validTransformers.isNotEmpty()) { + val node = ClassNode().also { ClassReader(bytes).accept(it, 0) } + + validTransformers.forEach { it.transform(node) } + + SpiceClassWriter(COMPUTE_FRAMES) + .also { node.accept(it) } + .toByteArray() + } else bytes } override fun addTransformer(transformer: IClassTransformer) { - registerTransformer(transformer) + transformers += transformer } override fun appendToClassPath(url: URL) { - //TODO: Implement + logger.info("Appending $url to the classpath") + + loader.addURL(url) + } + + private fun loadCache(): Map { + val hash = currentHash() + val stopwatch = Stopwatch.createUnstarted() + + return if (!isCached(hash)) { + logger.info("Cache does not contain an entry for $hash, beginning build") + logger.info("Searching for LWJGL classes") + stopwatch.start() + + val resources = + collectResources(loader.urLs) + .filter { + it.endsWith(".class") + && it.startsWith("org/lwjgl/") + } + + stopwatch.stop() + + logger.info("Found ${resources.size} LWJGL classes in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms") + + stopwatch.reset() + stopwatch.start() + + val classes = resources.mapNotNull { resource -> + ClassNode().also { node -> + ClassReader( + loader.getResourceAsStream(resource) + ?.use { it.readBytes() } ?: return@mapNotNull null).accept(node, 0) + } + } + + logger.info("Transforming ${classes.size} classes") + + val transformed = buildCache(hash, classes) + + stopwatch.stop() + + logger.info("Transformed ${transformed.first.size}/${classes.size} classes") + logger.info("Built cache in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms") + + transformed.second + } else { + logger.info("Loading classes from cache $hash") + stopwatch.start() + + val cache = loadCacheBuffers(hash) + + stopwatch.stop() + logger.info("Loaded ${cache.size} cached classes in ${stopwatch.elapsed(TimeUnit.MILLISECONDS)}ms") + + cache + } } } -//#endif \ No newline at end of file +//#endif diff --git a/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/TransformerPlugin.kt b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/TransformerPlugin.kt index 21980a7..05c8ddb 100644 --- a/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/TransformerPlugin.kt +++ b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/asm/TransformerPlugin.kt @@ -22,4 +22,4 @@ class TransformerPlugin : IFMLLoadingPlugin { } } -//#endif \ No newline at end of file +//#endif diff --git a/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/util/Classpath.kt b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/util/Classpath.kt new file mode 100644 index 0000000..f085501 --- /dev/null +++ b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/util/Classpath.kt @@ -0,0 +1,26 @@ +package org.polyfrost.spice.platform.impl.forge.util + +import java.io.File +import java.net.URL +import java.util.jar.JarInputStream + +fun collectResources(urls: Array): List = + urls + .filter { it.protocol != "spice" && it.protocol != "asmgen" } + .map { + runCatching { + val file = File(it.toURI()) + + if (file.isDirectory) return@map emptyList() + } + + JarInputStream(it.openStream()) + .use { stream -> + val entries = mutableListOf() + + while (true) entries += stream.nextEntry?.name ?: break + + entries + } + } + .flatten() diff --git a/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/util/LaunchWrapperLogger.kt b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/util/LaunchWrapperLogger.kt new file mode 100644 index 0000000..6fb5377 --- /dev/null +++ b/versions/src/main/kotlin/org/polyfrost/spice/platform/impl/forge/util/LaunchWrapperLogger.kt @@ -0,0 +1,19 @@ +package org.polyfrost.spice.platform.impl.forge.util + +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger + +private val base = LogManager.getLogger("LaunchWrapper") + +object LaunchWrapperLogger : Logger by base { + override fun log(level: Level, format: String, vararg params: Any?) { + when (format) { + "The jar file %s is trying to seal already secured path %s" -> return + "The jar file %s has a security seal for path %s, but that path is defined and not secure" -> return + "The URL %s is defining elements for sealed path %s" -> return + } + + base.log(level, format, params) + } +}