Skip to content

Commit

Permalink
add client patch peek/poke
Browse files Browse the repository at this point in the history
With asm and kotlin-reflect added, we can analyze and even modify the client patch at runtime. A simple example has been added that will print out the instructions for client.changeWorld on startup.

This is how we will get our mappings etc so best to get this launcher / client update out of the way now.
  • Loading branch information
zeruth committed Jan 27, 2024
1 parent 867a1f6 commit afe60a2
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 4 deletions.
20 changes: 20 additions & 0 deletions runelite-client/src/main/java/ext/java/JarFileExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ext.java

import java.io.ByteArrayOutputStream
import java.util.jar.JarEntry
import java.util.jar.JarFile

object JarFileExt {
fun JarFile.getBytes(entry: JarEntry): ByteArray {
getInputStream(entry).use { inputStream ->
ByteArrayOutputStream().use { byteArrayOutputStream ->
val buffer = ByteArray(4096)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead)
}
return byteArrayOutputStream.toByteArray()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package hotlite.patch

import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor

//TODO: methodDesc is inconsistent atm
class ExamplePrintInstructionClassVisitor(api: Int, private val methodName: String, private val methodDesc: String, classWriter: ClassWriter) : ClassVisitor(api, classWriter) {
override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?): MethodVisitor {
if (methodName == name) {
val originalMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
return PrintingMethodVisitor(api, originalMethodVisitor)
}
return super.visitMethod(access, name, descriptor, signature, exceptions)
}
}
82 changes: 82 additions & 0 deletions runelite-client/src/main/java/hotlite/patch/PatchManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package hotlite.patch

import ext.java.JarFileExt.getBytes
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import java.io.IOException
import java.util.jar.JarFile
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.jvm.javaMethod


object PatchManager {
lateinit var PATCHED_JAR: String
lateinit var CLASSLOADER: ClassLoader
val classNames = HashSet<String>()
val classes = HashMap<KClass<*>, ByteArray>()

fun init(pathToJar: String, classLoader: ClassLoader) {
PATCHED_JAR = pathToJar
CLASSLOADER = classLoader

loadPatch(PATCHED_JAR, CLASSLOADER)

println("Loaded ${classes.size} patch classes")

printChangeWorldInstructions()
}

fun loadPatch(pathToPatch: String, classLoader: ClassLoader) {
try {
val jarFile = JarFile(pathToPatch)
val entries = jarFile.entries()

while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (entry.name.endsWith(".class")) {
val className = entry.name.replace('/', '.').substring(0, entry.name.length - 6)
classNames.add(className)
classes[classLoader.loadClass(className).kotlin] =
jarFile.getBytes(entry)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}

fun printChangeWorldInstructions() {
for (c in classes.keys) {
for (m in c.memberFunctions) {
if (m.name == "changeWorld") {
m.javaMethod?.let {
try {
val classBytes = classes[c]
val reader = ClassReader(classBytes)
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
val visitor = ExamplePrintInstructionClassVisitor(Opcodes.ASM9, m.name, getMethodDescriptor(m), classWriter)
reader.accept(visitor, 0)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
}
}

//FIXME
fun getMethodDescriptor(kFunction: KFunction<*>): String {
val javaMethod = kFunction.javaMethod ?: error("Java method not found for the provided Kotlin function")
val methodDescriptor = javaMethod.toGenericString()

// If you specifically want just the method descriptor, you can extract it from the generic string
val descriptorStart = methodDescriptor.indexOf('(')
val descriptorEnd = methodDescriptor.indexOf(')') + 1
return methodDescriptor.substring(descriptorStart, descriptorEnd)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package hotlite.patch

import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.util.Printer

class PrintingMethodVisitor(api: Int, mv: MethodVisitor?) : MethodVisitor(api, mv) {
override fun visitCode() {
println("Method Instructions:")
super.visitCode()
}

override fun visitInsn(opcode: Int) {
println(getOpcodeName(opcode))
super.visitInsn(opcode)
}

override fun visitIntInsn(opcode: Int, operand: Int) {
println(getOpcodeName(opcode) + " " + operand)
super.visitIntInsn(opcode, operand)
}

override fun visitVarInsn(opcode: Int, `var`: Int) {
println(getOpcodeName(opcode) + " " + `var`)
super.visitVarInsn(opcode, `var`)
}

override fun visitTypeInsn(opcode: Int, type: String) {
println(getOpcodeName(opcode) + " " + type)
super.visitTypeInsn(opcode, type)
}

override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) {
println(getOpcodeName(opcode) + " " + owner + " " + name + " " + descriptor)
super.visitFieldInsn(opcode, owner, name, descriptor)
}

override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) {
println(getOpcodeName(opcode) + " " + owner + " " + name + " " + descriptor + " " + isInterface)
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}

override fun visitJumpInsn(opcode: Int, label: Label) {
println(getOpcodeName(opcode) + " " + label)
super.visitJumpInsn(opcode, label)
}

override fun visitLabel(label: Label) {
println("Label: $label")
super.visitLabel(label)
}

override fun visitLdcInsn(value: Any) {
println("LDC: $value")
super.visitLdcInsn(value)
}

override fun visitIincInsn(`var`: Int, increment: Int) {
println("IINC: $`var` $increment")
super.visitIincInsn(`var`, increment)
}

override fun visitTableSwitchInsn(min: Int, max: Int, dflt: Label, vararg labels: Label) {
println("TABLESWITCH: " + min + " " + max + " " + dflt + " " + labels.contentToString())
super.visitTableSwitchInsn(min, max, dflt, *labels)
}

override fun visitLookupSwitchInsn(dflt: Label, keys: IntArray, labels: Array<Label>) {
println("LOOKUPSWITCH: " + dflt + " " + keys.contentToString() + " " + labels.contentToString())
super.visitLookupSwitchInsn(dflt, keys, labels)
}

override fun visitMultiANewArrayInsn(descriptor: String, dims: Int) {
println("MULTIANEWARRAY: $descriptor $dims")
super.visitMultiANewArrayInsn(descriptor, dims)
}

override fun visitMaxs(maxStack: Int, maxLocals: Int) {
println("Max Stack: $maxStack, Max Locals: $maxLocals")
super.visitMaxs(maxStack, maxLocals)
}

override fun visitEnd() {
println("End of Method")
super.visitEnd()
}

private fun getOpcodeName(opcode: Int): String {
return Printer.OPCODES[opcode]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,15 @@
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.*;
import java.util.function.Supplier;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import javax.annotation.Nonnull;
import javax.swing.SwingUtilities;

import hotlite.patch.PatchManager;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.client.RuneLite;
Expand Down Expand Up @@ -169,6 +168,10 @@ private Object doLoad()
// create the classloader for the jar while we hold the lock, and eagerly load and link all classes
// in the jar. Otherwise the jar can change on disk and can break future classloads.
classLoader = createJarClassLoader(jarFile);

if (PATCHED_CACHE.exists()) {
PatchManager.INSTANCE.init(PATCHED_CACHE.getAbsolutePath(), classLoader);
}
}

SplashScreen.stage(.465, "Starting", "Starting Old School RuneScape");
Expand Down

0 comments on commit afe60a2

Please sign in to comment.