From 29e6faaf3d90f826e477a0f723a01de8aef606af Mon Sep 17 00:00:00 2001 From: zani Date: Tue, 13 Feb 2024 11:06:00 -0700 Subject: [PATCH] lunar classloader fix --- .gitignore | 1 + build.gradle.kts | 6 ++ .../zani/launchwrapper/LunarLaunchWrapper.kt | 43 ++++++-- .../launchwrapper/loader/BootstrapProxy.kt | 18 ++++ .../loader/ClassLoaderExtensions.kt | 5 + .../launchwrapper/loader/DefinitionProxy.kt | 11 +++ .../loader/TransformationHandler.kt | 23 ++++- .../loader/transformers/GenesisTransformer.kt | 99 ++++++++++++++++--- .../launchwrapper/patches/AntiAntiAgent.kt | 1 - .../launchwrapper/patches/ipc/IpcPatch.kt | 4 + 10 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/wtf/zani/launchwrapper/loader/BootstrapProxy.kt create mode 100644 src/main/kotlin/wtf/zani/launchwrapper/loader/ClassLoaderExtensions.kt create mode 100644 src/main/kotlin/wtf/zani/launchwrapper/patches/ipc/IpcPatch.kt diff --git a/.gitignore b/.gitignore index df5a3bf..f563825 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ bin/ .ichor logs *_*_*1_*.json +jcef-bundle diff --git a/build.gradle.kts b/build.gradle.kts index 49c2112..b073d34 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,12 +10,18 @@ version = "1.0.0" repositories { mavenCentral() + maven("https://jogamp.org/deployment/maven") } dependencies { val asmVersion = "9.4" val ktorVersion = "2.3.3" + compileOnly("com.google.guava:guava:33.0.0-jre") + compileOnly("org.json:json:20231013") + + implementation("dev.datlag:kcef:2024.01.07.1") + implementation("org.ow2.asm:asm-tree:$asmVersion") implementation("org.ow2.asm:asm-util:$asmVersion") implementation("org.ow2.asm:asm-commons:$asmVersion") diff --git a/src/main/kotlin/wtf/zani/launchwrapper/LunarLaunchWrapper.kt b/src/main/kotlin/wtf/zani/launchwrapper/LunarLaunchWrapper.kt index 3aea32a..b36d520 100644 --- a/src/main/kotlin/wtf/zani/launchwrapper/LunarLaunchWrapper.kt +++ b/src/main/kotlin/wtf/zani/launchwrapper/LunarLaunchWrapper.kt @@ -8,8 +8,11 @@ import wtf.zani.launchwrapper.loader.LibraryLoader import wtf.zani.launchwrapper.loader.LunarLoader import wtf.zani.launchwrapper.loader.PrebakeHelper import wtf.zani.launchwrapper.patches.AntiAntiAgent +import wtf.zani.launchwrapper.patches.ipc.IpcPatch import wtf.zani.launchwrapper.util.toHexString import wtf.zani.launchwrapper.version.VersionManifest +import java.io.File +import java.net.URL import java.security.MessageDigest import kotlin.io.path.Path import kotlin.io.path.createDirectories @@ -19,7 +22,11 @@ private const val nativeDirKey = "wtf.zani.launchwrapper.nativedir" private val offlineDir = Path(System.getProperty("user.home"), ".lunarclient", "offline", "multiver") private val textureDir = Path(System.getProperty("user.home"), ".lunarclient", "textures") +val llwDir = Path(System.getProperty("user.home"), ".llw") + suspend fun main(args: Array) { + llwDir.createDirectories() + offlineDir.createDirectories() textureDir.createDirectories() @@ -43,6 +50,9 @@ suspend fun main(args: Array) { .withRequiredArg() .ofType(String::class.java) .withValuesSeparatedBy(",") + val disableUpdatesSpec = + optionParser + .accepts("disable-updates") optionParser.allowsUnrecognizedOptions() @@ -51,7 +61,10 @@ suspend fun main(args: Array) { val gameVersion = options.valueOf(versionSpec) val lunarModule = options.valueOf(moduleSpec) - val availablePatches = arrayOf(AntiAntiAgent::class.java) + val availablePatches = arrayOf( + AntiAntiAgent::class.java, + IpcPatch::class.java + ) val disabledPatches = options.valuesOf(disabledPatchesSpec).filter { patch -> availablePatches.find { it.name == patch } != null } val enabledPatches = availablePatches.filter { !disabledPatches.contains(it.name) } @@ -73,12 +86,18 @@ suspend fun main(args: Array) { var hashes: List? = null - withContext(Dispatchers.IO) { - launch { hashes = version.download(offlineDir) } - launch { textures.download(textureDir) } - } + if (!options.has(disableUpdatesSpec)) { + withContext(Dispatchers.IO) { + launch { hashes = version.download(offlineDir) } + launch { textures.download(textureDir) } + } + + cache?.write() + } else { + println("Updates are disabled") - cache?.write() + hashes = cache?.version?.artifacts?.map { it.sha1 } + } val natives = version @@ -107,11 +126,12 @@ suspend fun main(args: Array) { System.setProperty("org.lwjgl.librarypath", System.getProperty(nativeDirKey)) val minecraftArgs = mutableListOf( - "--launcherVersion", "3.1.0", + "--launcherVersion", "3.2.3", "--classpathDir", offlineDir.toString(), "--workingDirectory", offlineDir.toString(), "--ichorClassPath", classpath.map { it.fileName }.joinToString(","), - "--ichorExternalFiles", externalFiles.joinToString(",") + "--ichorExternalFiles", externalFiles.joinToString(","), + "--webosrPath", natives.first().toString() ) minecraftArgs += args.toList() @@ -120,9 +140,13 @@ suspend fun main(args: Array) { System.setProperty("llw.lunar.module", lunarModule) System.setProperty("llw.minecraft.version", gameVersion) + System.setProperty("llw.java.classpath", classpath.map { it.fileName }.joinToString(File.separator)) + + val urls = classpath.map { it.toUri().toURL() }.toTypedArray() + val loader = LunarLoader(urls) - val loader = LunarLoader(classpath.map { it.toUri().toURL() }.toTypedArray()) val genesis = loader.loadClass("com.moonsworth.lunar.genesis.Genesis") + val bootstrapProxy = loader.loadClass("wtf.zani.launchwrapper.loader.BootstrapProxy") val digest = MessageDigest.getInstance("SHA-256") @@ -134,6 +158,7 @@ suspend fun main(args: Array) { Thread.currentThread().contextClassLoader = loader try { + bootstrapProxy.getMethod("setUrls", Array::class.java).invoke(null, urls) genesis.getMethod("main", Array::class.java).invoke(null, minecraftArgs.toTypedArray()) } catch (error: Throwable) { error.printStackTrace() diff --git a/src/main/kotlin/wtf/zani/launchwrapper/loader/BootstrapProxy.kt b/src/main/kotlin/wtf/zani/launchwrapper/loader/BootstrapProxy.kt new file mode 100644 index 0000000..2a5670e --- /dev/null +++ b/src/main/kotlin/wtf/zani/launchwrapper/loader/BootstrapProxy.kt @@ -0,0 +1,18 @@ +package wtf.zani.launchwrapper.loader + +import java.net.URL +import java.net.URLClassLoader + +@Suppress("unused") +object BootstrapProxy { + @JvmStatic + lateinit var urls: Array + + @JvmStatic + fun addUrls(classLoader: URLClassLoader) { + val addUrl = classLoader::class.java.getMethod("addURL", URL::class.java) + addUrl.isAccessible = true + + urls.forEach { addUrl.invoke(classLoader, it) } + } +} diff --git a/src/main/kotlin/wtf/zani/launchwrapper/loader/ClassLoaderExtensions.kt b/src/main/kotlin/wtf/zani/launchwrapper/loader/ClassLoaderExtensions.kt new file mode 100644 index 0000000..ddbb7fa --- /dev/null +++ b/src/main/kotlin/wtf/zani/launchwrapper/loader/ClassLoaderExtensions.kt @@ -0,0 +1,5 @@ +package wtf.zani.launchwrapper.loader + +interface ClassLoaderExtensions { + fun findClassWithSuper(name: String): Class<*>? +} diff --git a/src/main/kotlin/wtf/zani/launchwrapper/loader/DefinitionProxy.kt b/src/main/kotlin/wtf/zani/launchwrapper/loader/DefinitionProxy.kt index 5244fa4..59d05d7 100644 --- a/src/main/kotlin/wtf/zani/launchwrapper/loader/DefinitionProxy.kt +++ b/src/main/kotlin/wtf/zani/launchwrapper/loader/DefinitionProxy.kt @@ -30,4 +30,15 @@ object DefinitionProxy { 0, transformed.second.size) as Class<*> } + + @JvmStatic + fun findClass(instance: ClassLoader, name: String): Class<*>? { + val data = instance.getResourceAsStream("${name.replace(".", "/")}.class")?.readAllBytes() ?: return null + + if (TransformationHandler.getTransformers(name.replace(".", "/")).isEmpty()) { + return (instance as ClassLoaderExtensions).findClassWithSuper(name) + } + + return defineClass(instance, name, data, 0, data.size) + } } diff --git a/src/main/kotlin/wtf/zani/launchwrapper/loader/TransformationHandler.kt b/src/main/kotlin/wtf/zani/launchwrapper/loader/TransformationHandler.kt index dcfc8c0..9957176 100644 --- a/src/main/kotlin/wtf/zani/launchwrapper/loader/TransformationHandler.kt +++ b/src/main/kotlin/wtf/zani/launchwrapper/loader/TransformationHandler.kt @@ -3,6 +3,7 @@ package wtf.zani.launchwrapper.loader import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import org.objectweb.asm.tree.ClassNode +import wtf.zani.launchwrapper.llwDir import wtf.zani.launchwrapper.loader.transformers.GenesisTransformer import wtf.zani.launchwrapper.loader.transformers.LibraryTransformer import wtf.zani.launchwrapper.loader.transformers.Transformer @@ -17,9 +18,7 @@ object TransformationHandler { ClassReader(data).accept(node, 0) - val transformersRan = transformers.filter { - (it.classNames.find { name -> node.name.startsWith(name) } != null && !it.exact) || it.classNames.find { name -> name == node.name } != null - }.map { it.transform(node) }.contains(true) + val transformersRan = getTransformers(node.name).map { it.transform(node) }.contains(true) if (!transformersRan) return null @@ -27,10 +26,26 @@ object TransformationHandler { node.accept(writer) - return Pair(node.name, writer.toByteArray()) + val bytecode = writer.toByteArray() + + if (System.getProperty("llw.dumpBytecode") == "true") dumpClass(node.name, bytecode) + + return Pair(node.name, bytecode) } + fun getTransformers(target: String): List = + transformers.filter { + (it.classNames.find { name -> target.startsWith(name) } != null && !it.exact) || it.classNames.find { name -> name == target } != null + } + fun addTransformer(transformer: Transformer) { transformers.add(transformer) } + + private fun dumpClass(name: String, bytecode: ByteArray) { + val dumpPath = llwDir.resolve("class_dump/$name.class") + + dumpPath.parent.toFile().mkdirs() + dumpPath.toFile().writeBytes(bytecode) + } } diff --git a/src/main/kotlin/wtf/zani/launchwrapper/loader/transformers/GenesisTransformer.kt b/src/main/kotlin/wtf/zani/launchwrapper/loader/transformers/GenesisTransformer.kt index 5248fd1..f41221c 100644 --- a/src/main/kotlin/wtf/zani/launchwrapper/loader/transformers/GenesisTransformer.kt +++ b/src/main/kotlin/wtf/zani/launchwrapper/loader/transformers/GenesisTransformer.kt @@ -1,23 +1,55 @@ package wtf.zani.launchwrapper.loader.transformers import net.weavemc.loader.api.util.asm -import org.objectweb.asm.Opcodes.INVOKESTATIC +import org.objectweb.asm.Opcodes.* import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.InvokeDynamicInsnNode import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.VarInsnNode class GenesisTransformer : Transformer("com/moonsworth/lunar/") { override fun transform(node: ClassNode): Boolean { - if (node.superName == "java/net/URLClassLoader") { transformClassLoader(node); return true } - // if lunar ever changes the main class name llw is DONE FOR - if (node.name == "com/moonsworth/lunar/genesis/Genesis") { transformMain(node); return true } + return when { + node.superName == "java/net/URLClassLoader" -> { + node.interfaces.add("wtf/zani/launchwrapper/loader/ClassLoaderExtensions") - return false + val findClassWithSuper = MethodNode(ACC_PUBLIC, "findClassWithSuper", "(Ljava/lang/String;)Ljava/lang/Class;", null, null) + + findClassWithSuper.instructions = asm { + aload(0) + aload(1) + + invokespecial("java/net/URLClassLoader", "findClass", "(Ljava/lang/String;)Ljava/lang/Class;") + + areturn + } + + node.methods.add(findClassWithSuper) + + transformClassDefintions(node) + transformBootstrapClassLoader(node) + + true + } + node.name == "com/moonsworth/lunar/genesis/Genesis" -> { + transformLegacyPrebake(node) + + true + } + node.name == "com/moonsworth/lunar/genesis/ClientGameBootstrap" -> { + transformModernPrebake(node) + + true + } + else -> false + } } - private fun transformMain(node: ClassNode) { + private fun transformLegacyPrebake(node: ClassNode) { val main = node.methods.find { it.name == "main" }!! - val prebakeString = main.instructions.find { it is LdcInsnNode && it.cst is String && it.cst as String == "prebake.cache" }!! + val prebakeString = main.instructions.find { it is LdcInsnNode && it.cst is String && it.cst as String == "prebake.cache" } ?: return main.instructions.remove(prebakeString.previous) main.instructions.insertBefore( @@ -27,17 +59,60 @@ class GenesisTransformer : Transformer("com/moonsworth/lunar/") { }) } - private fun transformClassLoader(node: ClassNode) { + private fun transformModernPrebake(node: ClassNode) { + val apply = node.methods.find { it.name == "apply" }!! + + val bakeString = apply.instructions.find { it is LdcInsnNode && it.cst is String && it.cst as String == "bake.cache" } ?: return + val variable = if (bakeString.previous is VarInsnNode) { (bakeString.previous as VarInsnNode).`var` } else return + + val store = apply.instructions.find { + it is VarInsnNode + && it.`var` == variable + && it.opcode == ASTORE + && it.previous is MethodInsnNode + && (it.previous as MethodInsnNode).owner == "java/nio/file/Path" + && (it.previous as MethodInsnNode).name == "resolve" } + + apply.instructions.insertBefore( + store, + asm { + pop + getstatic("wtf/zani/launchwrapper/loader/PrebakeHelper", "location", "Ljava/nio/file/Path;") + }) + } + + private fun transformClassDefintions(node: ClassNode) { node.methods.forEach { method -> method .instructions .forEach { insn -> - if (insn is MethodInsnNode && insn.owner == node.name && insn.name == "defineClass") { - insn.owner = "wtf/zani/launchwrapper/loader/DefinitionProxy" - insn.desc = "(Ljava/lang/ClassLoader;Ljava/lang/String;[BII)Ljava/lang/Class;" - insn.opcode = INVOKESTATIC + if (insn is MethodInsnNode) { + when { + insn.owner == node.name && insn.name == "defineClass" -> { + insn.owner = "wtf/zani/launchwrapper/loader/DefinitionProxy" + insn.desc = "(Ljava/lang/ClassLoader;Ljava/lang/String;[BII)Ljava/lang/Class;" + insn.opcode = INVOKESTATIC + } + insn.owner == node.superName && insn.name == "findClass" && method.name == "findClass" -> { + insn.owner = "wtf/zani/launchwrapper/loader/DefinitionProxy" + insn.desc = "(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;" + insn.opcode = INVOKESTATIC + } + } } } } } + + private fun transformBootstrapClassLoader(node: ClassNode) { + val ctor = node.methods.find { it.name == "" }!! + val concatInsn = ctor.instructions.find { it is InvokeDynamicInsnNode && it.bsmArgs[0] is String && it.bsmArgs[0] == "Bootstrap#\u0001" } ?: return + + ctor.instructions.insertBefore( + concatInsn, + asm { + aload(0) + invokestatic("wtf/zani/launchwrapper/loader/BootstrapProxy", "addUrls", "(Ljava/net/URLClassLoader;)V") + }) + } } diff --git a/src/main/kotlin/wtf/zani/launchwrapper/patches/AntiAntiAgent.kt b/src/main/kotlin/wtf/zani/launchwrapper/patches/AntiAntiAgent.kt index c567dcb..a3a1f36 100644 --- a/src/main/kotlin/wtf/zani/launchwrapper/patches/AntiAntiAgent.kt +++ b/src/main/kotlin/wtf/zani/launchwrapper/patches/AntiAntiAgent.kt @@ -6,7 +6,6 @@ import wtf.zani.launchwrapper.loader.TransformationHandler import wtf.zani.launchwrapper.loader.transformers.Transformer import wtf.zani.launchwrapper.util.getStrings -@Suppress("unused") class AntiAntiAgent { init { TransformationHandler.addTransformer(AntiAntiAgentTransformer()) diff --git a/src/main/kotlin/wtf/zani/launchwrapper/patches/ipc/IpcPatch.kt b/src/main/kotlin/wtf/zani/launchwrapper/patches/ipc/IpcPatch.kt new file mode 100644 index 0000000..64c2cf8 --- /dev/null +++ b/src/main/kotlin/wtf/zani/launchwrapper/patches/ipc/IpcPatch.kt @@ -0,0 +1,4 @@ +package wtf.zani.launchwrapper.patches.ipc + +// todo +class IpcPatch