Skip to content

Commit

Permalink
lunar classloader fix
Browse files Browse the repository at this point in the history
  • Loading branch information
Zxnii committed Feb 13, 2024
1 parent 99a03f1 commit 29e6faa
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ bin/
.ichor
logs
*_*_*1_*.json
jcef-bundle
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
43 changes: 34 additions & 9 deletions src/main/kotlin/wtf/zani/launchwrapper/LunarLaunchWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>) {
llwDir.createDirectories()

offlineDir.createDirectories()
textureDir.createDirectories()

Expand All @@ -43,6 +50,9 @@ suspend fun main(args: Array<String>) {
.withRequiredArg()
.ofType(String::class.java)
.withValuesSeparatedBy(",")
val disableUpdatesSpec =
optionParser
.accepts("disable-updates")

optionParser.allowsUnrecognizedOptions()

Expand All @@ -51,7 +61,10 @@ suspend fun main(args: Array<String>) {
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) }
Expand All @@ -73,12 +86,18 @@ suspend fun main(args: Array<String>) {

var hashes: List<String>? = 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
Expand Down Expand Up @@ -107,11 +126,12 @@ suspend fun main(args: Array<String>) {
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()
Expand All @@ -120,9 +140,13 @@ suspend fun main(args: Array<String>) {

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")

Expand All @@ -134,6 +158,7 @@ suspend fun main(args: Array<String>) {
Thread.currentThread().contextClassLoader = loader

try {
bootstrapProxy.getMethod("setUrls", Array<URL>::class.java).invoke(null, urls)
genesis.getMethod("main", Array<String>::class.java).invoke(null, minecraftArgs.toTypedArray())
} catch (error: Throwable) {
error.printStackTrace()
Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/wtf/zani/launchwrapper/loader/BootstrapProxy.kt
Original file line number Diff line number Diff line change
@@ -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<URL>

@JvmStatic
fun addUrls(classLoader: URLClassLoader) {
val addUrl = classLoader::class.java.getMethod("addURL", URL::class.java)
addUrl.isAccessible = true

urls.forEach { addUrl.invoke(classLoader, it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package wtf.zani.launchwrapper.loader

interface ClassLoaderExtensions {
fun findClassWithSuper(name: String): Class<*>?
}
11 changes: 11 additions & 0 deletions src/main/kotlin/wtf/zani/launchwrapper/loader/DefinitionProxy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,20 +18,34 @@ 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

val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)

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<Transformer> =
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)
}
}
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 == "<init>" }!!
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")
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package wtf.zani.launchwrapper.patches.ipc

// todo
class IpcPatch

0 comments on commit 29e6faa

Please sign in to comment.