diff --git a/asm/pom.xml b/asm/pom.xml
index d019a3ab83..91d95e20a8 100644
--- a/asm/pom.xml
+++ b/asm/pom.xml
@@ -23,6 +23,10 @@
org.ow2.asm
asm
+
+ org.ow2.asm
+ asm-commons
+
org.ow2.asm
asm-tree
diff --git a/asm/src/main/java/dev/openrs2/asm/Library.java b/asm/src/main/java/dev/openrs2/asm/Library.java
index 497da16438..8a670ebb4b 100644
--- a/asm/src/main/java/dev/openrs2/asm/Library.java
+++ b/asm/src/main/java/dev/openrs2/asm/Library.java
@@ -18,7 +18,10 @@ import java.util.zip.GZIPOutputStream;
import dev.openrs2.util.io.DeterministicJarOutputStream;
import dev.openrs2.util.io.SkipOutputStream;
import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.util.CheckClassAdapter;
import org.slf4j.Logger;
@@ -107,26 +110,33 @@ public final class Library implements Iterable {
return classes.values().iterator();
}
- public void writeJar(Path path) throws IOException {
+ public void writeJar(Path path, Remapper remapper) throws IOException {
logger.info("Writing jar {}", path);
try (var out = new DeterministicJarOutputStream(Files.newOutputStream(path))) {
for (var clazz : classes.values()) {
+ var name = clazz.name;
var writer = new ClassWriter(0);
- clazz.accept(new CheckClassAdapter(writer, true));
- out.putNextEntry(new JarEntry(clazz.name + CLASS_SUFFIX));
+ ClassVisitor visitor = new CheckClassAdapter(writer, true);
+ if (remapper != null) {
+ visitor = new ClassRemapper(visitor, remapper);
+ name = remapper.map(name);
+ }
+ clazz.accept(visitor);
+
+ out.putNextEntry(new JarEntry(name + CLASS_SUFFIX));
out.write(writer.toByteArray());
}
}
}
- public void writePack(Path path) throws IOException {
+ public void writePack(Path path, Remapper remapper) throws IOException {
logger.info("Writing pack {}", path);
var temp = Files.createTempFile(TEMP_PREFIX, JAR_SUFFIX);
try {
- writeJar(temp);
+ writeJar(temp, remapper);
try (var in = new JarInputStream(Files.newInputStream(temp));
var out = new GZIPOutputStream(new SkipOutputStream(Files.newOutputStream(path), 2))) {
diff --git a/asm/src/main/java/dev/openrs2/asm/MemberDesc.java b/asm/src/main/java/dev/openrs2/asm/MemberDesc.java
new file mode 100644
index 0000000000..0ac941aa22
--- /dev/null
+++ b/asm/src/main/java/dev/openrs2/asm/MemberDesc.java
@@ -0,0 +1,43 @@
+package dev.openrs2.asm;
+
+import java.util.Objects;
+
+public final class MemberDesc {
+ private final String name, desc;
+
+ public MemberDesc(String name, String desc) {
+ this.name = name;
+ this.desc = desc;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MemberDesc fieldRef = (MemberDesc) o;
+ return name.equals(fieldRef.name) &&
+ desc.equals(fieldRef.desc);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, desc);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %s", desc, name);
+ }
+}
diff --git a/asm/src/main/java/dev/openrs2/asm/MemberRef.java b/asm/src/main/java/dev/openrs2/asm/MemberRef.java
index f4a7e5491a..3bff555972 100644
--- a/asm/src/main/java/dev/openrs2/asm/MemberRef.java
+++ b/asm/src/main/java/dev/openrs2/asm/MemberRef.java
@@ -5,6 +5,10 @@ import java.util.Objects;
public final class MemberRef {
private final String owner, name, desc;
+ public MemberRef(String owner, MemberDesc desc) {
+ this(owner, desc.getName(), desc.getDesc());
+ }
+
public MemberRef(String owner, String name, String desc) {
this.owner = owner;
this.name = name;
diff --git a/deob/src/main/java/dev/openrs2/deob/ClassNamePrefixer.java b/deob/src/main/java/dev/openrs2/deob/ClassNamePrefixer.java
new file mode 100644
index 0000000000..f1d424a941
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/ClassNamePrefixer.java
@@ -0,0 +1,36 @@
+package dev.openrs2.deob;
+
+import java.util.HashMap;
+
+import dev.openrs2.asm.Library;
+import dev.openrs2.deob.path.TypedRemapper;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.SimpleRemapper;
+import org.objectweb.asm.tree.ClassNode;
+
+public final class ClassNamePrefixer {
+ public static void addPrefix(Library library, String prefix) {
+ var mapping = new HashMap();
+ for (var clazz : library) {
+ if (TypedRemapper.EXCLUDED_CLASSES.contains(clazz.name)) {
+ mapping.put(clazz.name, clazz.name);
+ } else {
+ mapping.put(clazz.name, prefix + clazz.name);
+ }
+ }
+ var remapper = new SimpleRemapper(mapping);
+
+ for (var name : mapping.keySet()) {
+ var in = library.remove(name);
+
+ var out = new ClassNode();
+ in.accept(new ClassRemapper(out, remapper));
+
+ library.add(out);
+ }
+ }
+
+ private ClassNamePrefixer() {
+ /* empty */
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java
index 6ed671c32a..4606bb0411 100644
--- a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java
+++ b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java
@@ -1,6 +1,9 @@
package dev.openrs2.deob;
import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@@ -8,6 +11,9 @@ import java.util.Map;
import dev.openrs2.asm.Library;
import dev.openrs2.asm.Transformer;
+import dev.openrs2.deob.path.ClassPath;
+import dev.openrs2.deob.path.TypedRemapper;
+import dev.openrs2.deob.transform.ClassForNameTransformer;
import dev.openrs2.deob.transform.OpaquePredicateTransformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,32 +26,53 @@ public final class Deobfuscator {
);
public static void main(String[] args) throws IOException {
- var deobfuscator = new Deobfuscator(Paths.get("nonfree/code"));
+ var deobfuscator = new Deobfuscator(Paths.get("nonfree/code"), Paths.get("nonfree/code/deob"));
deobfuscator.run();
}
- private final Path input;
+ private final Path input, output;
- public Deobfuscator(Path input) {
+ public Deobfuscator(Path input, Path output) {
this.input = input;
+ this.output = output;
}
public void run() throws IOException {
+ /* read input jars/packs */
+ logger.info("Reading input jars");
var unpacker = Library.readJar(input.resolve("game_unpacker.dat"));
+ var glUnpacker = new Library(unpacker);
var loader = Library.readJar(input.resolve("loader.jar"));
var glLoader = Library.readJar(input.resolve("loader_gl.jar"));
var client = Library.readJar(input.resolve("runescape.jar"));
var glClient = Library.readPack(input.resolve("runescape_gl.pack200"));
+ var unsignedClient = new Library(client);
- var libraries = Map.of(
+ /* read dependencies */
+ var runtime = ClassLoader.getPlatformClassLoader();
+ var jogl = new URLClassLoader(new URL[] {
+ input.resolve("jogl.jar").toUri().toURL()
+ }, runtime);
+
+ /* overwrite client's classes with signed classes from the loader */
+ logger.info("Moving signed classes from loader to runescape");
+ var signedClasses = SignedClassSet.create(loader, client);
+
+ logger.info("Moving signed classes from loader_gl to runescape_gl");
+ var glSignedClasses = SignedClassSet.create(glLoader, glClient);
+
+ /* deobfuscate */
+ var allLibraries = Map.of(
"unpacker", unpacker,
+ "unpacker_gl", glUnpacker,
"loader", loader,
"loader_gl", glLoader,
"runescape", client,
- "runescape_gl", glClient
+ "runescape_gl", glClient,
+ "runescape_unsigned", unsignedClient
);
- for (var entry : libraries.entrySet()) {
+ for (var entry : allLibraries.entrySet()) {
logger.info("Transforming library {}", entry.getKey());
for (var transformer : TRANSFORMERS) {
@@ -53,5 +80,74 @@ public final class Deobfuscator {
transformer.transform(entry.getValue());
}
}
+
+ /* move unpack class out of the loader (so the unpacker and loader can both depend on it) */
+ logger.info("Moving unpack from loader to unpack");
+ var unpack = new Library();
+ unpack.add(loader.remove("unpack"));
+
+ logger.info("Moving unpack from loader_gl to unpack_gl");
+ var glUnpack = new Library();
+ glUnpack.add(glLoader.remove("unpack"));
+
+ /* move signed classes out of the client (so the client and loader can both depend on them) */
+ logger.info("Moving signed classes from runescape to signlink");
+ var signLink = new Library();
+ signedClasses.move(client, signLink);
+
+ logger.info("Moving signed classes from runescape_gl to signlink_gl");
+ var glSignLink = new Library();
+ glSignedClasses.move(glClient, glSignLink);
+
+ /* prefix remaining loader/unpacker classes (to avoid conflicts when we rename in the same classpath as the client) */
+ logger.info("Prefixing loader and unpacker class names");
+ ClassNamePrefixer.addPrefix(loader, "loader_");
+ ClassNamePrefixer.addPrefix(glLoader, "loader_");
+ ClassNamePrefixer.addPrefix(unpacker, "unpacker_");
+ ClassNamePrefixer.addPrefix(glUnpacker, "unpacker_");
+
+ /* remap all class, method and field names */
+ logger.info("Creating remappers");
+ var libraries = new Library[] { client, loader, signLink, unpack, unpacker };
+ var remapper = TypedRemapper.create(new ClassPath(runtime, libraries));
+
+ var glLibraries = new Library[] { glClient, glLoader, glSignLink, glUnpack, glUnpacker };
+ var glRemapper = TypedRemapper.create(new ClassPath(jogl, glLibraries));
+
+ var unsignedRemapper = TypedRemapper.create(new ClassPath(runtime, unsignedClient));
+
+ /* transform Class.forName() calls */
+ logger.info("Transforming Class.forName() calls");
+ var transformer = new ClassForNameTransformer(remapper);
+ for (var library : libraries) {
+ transformer.transform(library);
+ }
+
+ var glTransformer = new ClassForNameTransformer(glRemapper);
+ for (var library : glLibraries) {
+ glTransformer.transform(library);
+ }
+
+ var unsignedTransformer = new ClassForNameTransformer(unsignedRemapper);
+ unsignedTransformer.transform(unsignedClient);
+
+ /* write output jars */
+ logger.info("Writing output jars");
+
+ Files.createDirectories(output);
+
+ client.writeJar(output.resolve("runescape.jar"), remapper);
+ loader.writeJar(output.resolve("loader.jar"), remapper);
+ signLink.writeJar(output.resolve("signlink.jar"), remapper);
+ unpack.writeJar(output.resolve("unpack.jar"), remapper);
+ unpacker.writeJar(output.resolve("unpacker.jar"), remapper);
+
+ glClient.writeJar(output.resolve("runescape_gl.jar"), glRemapper);
+ glLoader.writeJar(output.resolve("loader_gl.jar"), glRemapper);
+ glSignLink.writeJar(output.resolve("signlink_gl.jar"), glRemapper);
+ glUnpack.writeJar(output.resolve("unpack_gl.jar"), glRemapper);
+ glUnpacker.writeJar(output.resolve("unpacker_gl.jar"), glRemapper);
+
+ unsignedClient.writeJar(output.resolve("runescape_unsigned.jar"), unsignedRemapper);
}
}
diff --git a/deob/src/main/java/dev/openrs2/deob/SignedClassSet.java b/deob/src/main/java/dev/openrs2/deob/SignedClassSet.java
new file mode 100644
index 0000000000..0e2ef63534
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/SignedClassSet.java
@@ -0,0 +1,114 @@
+package dev.openrs2.deob;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.google.common.collect.Sets;
+import dev.openrs2.asm.InsnMatcher;
+import dev.openrs2.asm.Library;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.SimpleRemapper;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class SignedClassSet {
+ private static final Logger logger = LoggerFactory.getLogger(SignedClassSet.class);
+
+ private static final InsnMatcher LOAD_SIGNED_CLASS_MATCHER = InsnMatcher.compile("LDC INVOKESTATIC ASTORE ALOAD GETFIELD ALOAD INVOKEVIRTUAL ALOAD INVOKEVIRTUAL POP");
+
+ public static SignedClassSet create(Library loader, Library client) {
+ /* find signed classes */
+ var signedClasses = findSignedClasses(loader);
+ logger.info("Identified signed classes {}", signedClasses);
+
+ var dependencies = findDependencies(loader, signedClasses);
+ logger.info("Identified signed class dependencies {}", dependencies);
+
+ /* rename dependencies of signed classes so they don't clash with client classes */
+ var mapping = new HashMap();
+ for (var dependency : dependencies) {
+ mapping.put(dependency, "loader_" + dependency);
+ }
+ var remapper = new SimpleRemapper(mapping);
+
+ /* move signed classes to the client */
+ var remappedSignedClasses = new HashSet();
+ for (var name : Sets.union(signedClasses, dependencies)) {
+ var in = loader.remove(name);
+
+ var out = new ClassNode();
+ in.accept(new ClassRemapper(out, remapper));
+
+ remappedSignedClasses.add(out.name);
+ client.add(out);
+ }
+ return new SignedClassSet(remappedSignedClasses);
+ }
+
+ private static Set findSignedClasses(Library loader) {
+ var clazz = loader.get("loader");
+ if (clazz == null) {
+ throw new IllegalArgumentException("Failed to find loader class");
+ }
+
+ for (var method : clazz.methods) {
+ if (method.name.equals("run") && method.desc.equals("()V")) {
+ return findSignedClasses(method);
+ }
+ }
+
+ throw new IllegalArgumentException("Failed to find loader.run() method");
+ }
+
+ private static Set findSignedClasses(MethodNode method) {
+ var classes = new HashSet();
+
+ LOAD_SIGNED_CLASS_MATCHER.match(method).forEach(match -> {
+ var ldc = (LdcInsnNode) match.get(0);
+ if (ldc.cst instanceof String && !ldc.cst.equals("unpack")) {
+ classes.add((String) ldc.cst);
+ }
+ });
+
+ return classes;
+ }
+
+ private static Set findDependencies(Library loader, Set signedClasses) {
+ var dependencies = new HashSet();
+
+ for (var signedClass : signedClasses) {
+ var clazz = loader.get(signedClass);
+
+ for (var field : clazz.fields) {
+ var type = Type.getType(field.desc);
+ if (type.getSort() != Type.OBJECT) {
+ continue;
+ }
+
+ var name = type.getClassName();
+ if (loader.contains(name) && !signedClasses.contains(name)) {
+ dependencies.add(name);
+ }
+ }
+ }
+
+ return dependencies;
+ }
+
+ private final Set signedClasses;
+
+ private SignedClassSet(Set signedClasses) {
+ this.signedClasses = signedClasses;
+ }
+
+ public void move(Library client, Library signLink) {
+ for (var name : signedClasses) {
+ signLink.add(client.remove(name));
+ }
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/path/AsmClassMetadata.java b/deob/src/main/java/dev/openrs2/deob/path/AsmClassMetadata.java
new file mode 100644
index 0000000000..f0dbd77e72
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/path/AsmClassMetadata.java
@@ -0,0 +1,72 @@
+package dev.openrs2.deob.path;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import dev.openrs2.asm.MemberDesc;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.ClassNode;
+
+public final class AsmClassMetadata extends ClassMetadata {
+ private final ClassPath classPath;
+ private final ClassNode clazz;
+
+ public AsmClassMetadata(ClassPath classPath, ClassNode clazz) {
+ this.classPath = classPath;
+ this.clazz = clazz;
+ }
+
+ @Override
+ public String getName() {
+ return clazz.name;
+ }
+
+ @Override
+ public boolean isMutable() {
+ return true;
+ }
+
+ @Override
+ public boolean isInterface() {
+ return (clazz.access & Opcodes.ACC_INTERFACE) != 0;
+ }
+
+ @Override
+ public ClassMetadata getSuperClass() {
+ if (clazz.superName != null) {
+ return classPath.get(clazz.superName);
+ }
+ return null;
+ }
+
+ @Override
+ public List getSuperInterfaces() {
+ return clazz.interfaces.stream()
+ .map(classPath::get)
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ @Override
+ public List getFields() {
+ return clazz.fields.stream()
+ .map(f -> new MemberDesc(f.name, f.desc))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ @Override
+ public List getMethods() {
+ return clazz.methods.stream()
+ .map(m -> new MemberDesc(m.name, m.desc))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ @Override
+ public boolean isNative(MemberDesc method) {
+ for (var m : clazz.methods) {
+ if (m.name.equals(method.getName()) && m.desc.equals(method.getDesc())) {
+ return (m.access & Opcodes.ACC_NATIVE) != 0;
+ }
+ }
+ return false;
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/path/ClassMetadata.java b/deob/src/main/java/dev/openrs2/deob/path/ClassMetadata.java
new file mode 100644
index 0000000000..9117160dff
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/path/ClassMetadata.java
@@ -0,0 +1,34 @@
+package dev.openrs2.deob.path;
+
+import java.util.List;
+import java.util.Objects;
+
+import dev.openrs2.asm.MemberDesc;
+
+public abstract class ClassMetadata {
+ public abstract String getName();
+ public abstract boolean isMutable();
+ public abstract boolean isInterface();
+ public abstract ClassMetadata getSuperClass();
+ public abstract List getSuperInterfaces();
+ public abstract List getFields();
+ public abstract List getMethods();
+ public abstract boolean isNative(MemberDesc method);
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ var that = (ClassMetadata) o;
+ return getName().equals(that.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getName());
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/path/ClassPath.java b/deob/src/main/java/dev/openrs2/deob/path/ClassPath.java
new file mode 100644
index 0000000000..9892759358
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/path/ClassPath.java
@@ -0,0 +1,61 @@
+package dev.openrs2.deob.path;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import dev.openrs2.asm.Library;
+
+public final class ClassPath {
+ private final ClassLoader dependencyLoader;
+ private final List libraries;
+ private final Map cache = new HashMap<>();
+
+ public ClassPath(ClassLoader dependencyLoader, Library... libraries) {
+ this.dependencyLoader = dependencyLoader;
+ this.libraries = List.of(libraries);
+ }
+
+ public List getLibraryClasses() {
+ var classes = new ArrayList();
+
+ for (var library : libraries) {
+ for (var clazz : library) {
+ classes.add(get(clazz.name));
+ }
+ }
+
+ return Collections.unmodifiableList(classes);
+ }
+
+ public ClassMetadata get(String name) {
+ var metadata = cache.get(name);
+ if (metadata != null) {
+ return metadata;
+ }
+
+ for (var library : libraries) {
+ var clazz = library.get(name);
+ if (clazz != null) {
+ metadata = new AsmClassMetadata(this, clazz);
+ cache.put(name, metadata);
+ return metadata;
+ }
+ }
+
+ var reflectionName = name.replace('/', '.');
+
+ Class> clazz;
+ try {
+ clazz = dependencyLoader.loadClass(reflectionName);
+ } catch (ClassNotFoundException ex) {
+ throw new IllegalArgumentException("Unknown class " + name);
+ }
+
+ metadata = new ReflectionClassMetadata(this, clazz);
+ cache.put(name, metadata);
+ return metadata;
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/path/ReflectionClassMetadata.java b/deob/src/main/java/dev/openrs2/deob/path/ReflectionClassMetadata.java
new file mode 100644
index 0000000000..c6a9d6aa42
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/path/ReflectionClassMetadata.java
@@ -0,0 +1,74 @@
+package dev.openrs2.deob.path;
+
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import dev.openrs2.asm.MemberDesc;
+import org.objectweb.asm.Type;
+
+public final class ReflectionClassMetadata extends ClassMetadata {
+ private final ClassPath classPath;
+ private final Class> clazz;
+
+ public ReflectionClassMetadata(ClassPath classPath, Class> clazz) {
+ this.classPath = classPath;
+ this.clazz = clazz;
+ }
+
+ @Override
+ public String getName() {
+ return clazz.getName().replace('.', '/');
+ }
+
+ @Override
+ public boolean isMutable() {
+ return false;
+ }
+
+ @Override
+ public boolean isInterface() {
+ return clazz.isInterface();
+ }
+
+ @Override
+ public ClassMetadata getSuperClass() {
+ var superClass = clazz.getSuperclass();
+ if (superClass != null) {
+ return classPath.get(superClass.getName().replace('.', '/'));
+ }
+ return null;
+ }
+
+ @Override
+ public List getSuperInterfaces() {
+ return Arrays.stream(clazz.getInterfaces())
+ .map(i -> classPath.get(i.getName().replace('.', '/')))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ @Override
+ public List getFields() {
+ return Arrays.stream(clazz.getDeclaredFields())
+ .map(f -> new MemberDesc(f.getName(), Type.getDescriptor(f.getType())))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ @Override
+ public List getMethods() {
+ return Arrays.stream(clazz.getDeclaredMethods())
+ .map(m -> new MemberDesc(m.getName(), Type.getMethodDescriptor(m)))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ @Override
+ public boolean isNative(MemberDesc method) {
+ for (var m : clazz.getDeclaredMethods()) {
+ if (m.getName().equals(method.getName()) && Type.getMethodDescriptor(m).equals(method.getDesc())) {
+ return Modifier.isNative(m.getModifiers());
+ }
+ }
+ return false;
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/path/TypedRemapper.java b/deob/src/main/java/dev/openrs2/deob/path/TypedRemapper.java
new file mode 100644
index 0000000000..0181ac48a9
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/path/TypedRemapper.java
@@ -0,0 +1,320 @@
+package dev.openrs2.deob.path;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Strings;
+import dev.openrs2.asm.MemberDesc;
+import dev.openrs2.asm.MemberRef;
+import dev.openrs2.util.StringUtils;
+import dev.openrs2.util.collect.DisjointSet;
+import dev.openrs2.util.collect.ForestDisjointSet;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.commons.Remapper;
+
+public final class TypedRemapper extends Remapper {
+ public static final Set EXCLUDED_CLASSES = Set.of(
+ "client",
+ "jagex3/jagmisc/jagmisc",
+ "loader",
+ "unpack",
+ "unpackclass"
+ );
+ public static final Set EXCLUDED_METHODS = Set.of(
+ "",
+ "",
+ "main",
+ "providesignlink",
+ "quit"
+ );
+ public static final Set EXCLUDED_FIELDS = Set.of(
+ "cache"
+ );
+
+ public static TypedRemapper create(ClassPath classPath) {
+ var libraryClasses = classPath.getLibraryClasses();
+
+ var inheritedFieldSets = createInheritedFieldSets(libraryClasses);
+ var inheritedMethodSets = createInheritedMethodSets(libraryClasses);
+
+ var classes = createClassMapping(libraryClasses);
+ var fields = createFieldMapping(classPath, inheritedFieldSets, classes);
+ var methods = createMethodMapping(classPath, inheritedMethodSets);
+
+ return new TypedRemapper(classes, fields, methods);
+ }
+
+ private static DisjointSet createInheritedFieldSets(List classes) {
+ var disjointSet = new ForestDisjointSet();
+ var ancestorCache = new HashMap>();
+
+ for (var clazz : classes) {
+ populateInheritedFieldSets(ancestorCache, disjointSet, clazz);
+ }
+
+ return disjointSet;
+ }
+
+ private static Set populateInheritedFieldSets(Map> ancestorCache, DisjointSet disjointSet, ClassMetadata clazz) {
+ var ancestors = ancestorCache.get(clazz);
+ if (ancestors != null) {
+ return ancestors;
+ }
+ ancestors = new HashSet<>();
+
+ var superClass = clazz.getSuperClass();
+ if (superClass != null) {
+ var fields = populateInheritedFieldSets(ancestorCache, disjointSet, superClass);
+
+ for (var field : fields) {
+ var partition1 = disjointSet.add(new MemberRef(clazz.getName(), field));
+ var partition2 = disjointSet.add(new MemberRef(superClass.getName(), field));
+ disjointSet.union(partition1, partition2);
+ }
+
+ ancestors.addAll(fields);
+ }
+
+ for (var superInterface : clazz.getSuperInterfaces()) {
+ var fields = populateInheritedFieldSets(ancestorCache, disjointSet, superInterface);
+
+ for (var field : fields) {
+ var partition1 = disjointSet.add(new MemberRef(clazz.getName(), field));
+ var partition2 = disjointSet.add(new MemberRef(superInterface.getName(), field));
+ disjointSet.union(partition1, partition2);
+ }
+
+ ancestors.addAll(fields);
+ }
+
+ for (var field : clazz.getFields()) {
+ if (EXCLUDED_FIELDS.contains(field.getName())) {
+ continue;
+ }
+
+ disjointSet.add(new MemberRef(clazz.getName(), field));
+ ancestors.add(field);
+ }
+
+ ancestors = Collections.unmodifiableSet(ancestors);
+ ancestorCache.put(clazz, ancestors);
+ return ancestors;
+ }
+
+ private static DisjointSet createInheritedMethodSets(List classes) {
+ var disjointSet = new ForestDisjointSet();
+ var ancestorCache = new HashMap>();
+
+ for (var clazz : classes) {
+ populateInheritedMethodSets(ancestorCache, disjointSet, clazz);
+ }
+
+ return disjointSet;
+ }
+
+ private static Set populateInheritedMethodSets(Map> ancestorCache, DisjointSet disjointSet, ClassMetadata clazz) {
+ var ancestors = ancestorCache.get(clazz);
+ if (ancestors != null) {
+ return ancestors;
+ }
+ ancestors = new HashSet<>();
+
+ var superClass = clazz.getSuperClass();
+ if (superClass != null) {
+ var methods = populateInheritedMethodSets(ancestorCache, disjointSet, superClass);
+
+ for (var method : methods) {
+ var partition1 = disjointSet.add(new MemberRef(clazz.getName(), method));
+ var partition2 = disjointSet.add(new MemberRef(superClass.getName(), method));
+ disjointSet.union(partition1, partition2);
+ }
+
+ ancestors.addAll(methods);
+ }
+
+ for (var superInterface : clazz.getSuperInterfaces()) {
+ var methods = populateInheritedMethodSets(ancestorCache, disjointSet, superInterface);
+
+ for (var method : methods) {
+ var partition1 = disjointSet.add(new MemberRef(clazz.getName(), method));
+ var partition2 = disjointSet.add(new MemberRef(superInterface.getName(), method));
+ disjointSet.union(partition1, partition2);
+ }
+
+ ancestors.addAll(methods);
+ }
+
+ for (var method : clazz.getMethods()) {
+ if (EXCLUDED_METHODS.contains(method.getName())) {
+ continue;
+ }
+
+ disjointSet.add(new MemberRef(clazz.getName(), method));
+ ancestors.add(method);
+ }
+
+ ancestors = Collections.unmodifiableSet(ancestors);
+ ancestorCache.put(clazz, ancestors);
+ return ancestors;
+ }
+
+ private static String generateName(Map prefixes, String prefix) {
+ return prefix + prefixes.merge(prefix, 1, Integer::sum);
+ }
+
+ private static Map createClassMapping(List classes) {
+ var mapping = new HashMap();
+ var prefixes = new HashMap();
+
+ for (var clazz : classes) {
+ populateClassMapping(mapping, prefixes, clazz);
+ }
+
+ return mapping;
+ }
+
+ private static String populateClassMapping(Map mapping, Map prefixes, ClassMetadata clazz) {
+ var name = clazz.getName();
+ if (mapping.containsKey(name) || EXCLUDED_CLASSES.contains(name) || !clazz.isMutable()) {
+ return mapping.getOrDefault(name, name);
+ }
+
+ var mappedName = name.substring(0, name.lastIndexOf('/') + 1);
+
+ var superClass = clazz.getSuperClass();
+ if (superClass != null && !superClass.getName().equals("java/lang/Object")) {
+ var superName = populateClassMapping(mapping, prefixes, superClass);
+ superName = superName.substring(superName.lastIndexOf('/') + 1);
+
+ mappedName += generateName(prefixes, superName + "_Sub");
+ } else if (clazz.isInterface()) {
+ mappedName += generateName(prefixes, "Interface");
+ } else {
+ mappedName += generateName(prefixes, "Class");
+ }
+
+ mapping.put(name, mappedName);
+ return mappedName;
+ }
+
+ private static Map createFieldMapping(ClassPath classPath, DisjointSet disjointSet, Map classMapping) {
+ var mapping = new HashMap();
+ var prefixes = new HashMap();
+
+ for (var partition : disjointSet) {
+ boolean skip = false;
+
+ for (var field : partition) {
+ var clazz = classPath.get(field.getOwner());
+
+ if (!clazz.isMutable()) {
+ skip = true;
+ break;
+ }
+ }
+
+ if (skip) {
+ continue;
+ }
+
+ var prefix = "";
+
+ var type = Type.getType(partition.iterator().next().getDesc());
+ if (type.getSort() == Type.ARRAY) {
+ prefix = Strings.repeat("Array", type.getDimensions());
+ type = type.getElementType();
+ }
+
+ switch (type.getSort()) {
+ case Type.BOOLEAN:
+ case Type.BYTE:
+ case Type.CHAR:
+ case Type.SHORT:
+ case Type.INT:
+ case Type.LONG:
+ case Type.FLOAT:
+ case Type.DOUBLE:
+ prefix = type.getClassName() + prefix;
+ break;
+ case Type.OBJECT:
+ var className = classMapping.getOrDefault(type.getInternalName(), type.getInternalName());
+ className = className.substring(className.lastIndexOf('/') + 1);
+ prefix = className + prefix;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown field type " + type);
+ }
+
+ prefix = StringUtils.indefiniteArticle(prefix) + StringUtils.capitalize(prefix);
+
+ var mappedName = generateName(prefixes, prefix);
+ for (var field : partition) {
+ mapping.put(field, mappedName);
+ }
+ }
+
+ return mapping;
+ }
+
+ private static Map createMethodMapping(ClassPath classPath, DisjointSet disjointSet) {
+ var mapping = new HashMap();
+ var id = 0;
+
+ for (var partition : disjointSet) {
+ boolean skip = false;
+
+ for (var method : partition) {
+ var clazz = classPath.get(method.getOwner());
+
+ if (!clazz.isMutable()) {
+ skip = true;
+ break;
+ }
+
+ if (clazz.isNative(new MemberDesc(method.getName(), method.getDesc()))) {
+ skip = true;
+ break;
+ }
+ }
+
+ if (skip) {
+ continue;
+ }
+
+ var mappedName = "method" + (++id);
+ for (var method : partition) {
+ mapping.put(method, mappedName);
+ }
+ }
+
+ return mapping;
+ }
+
+ private final Map classes;
+ private final Map fields, methods;
+
+ private TypedRemapper(Map classes, Map fields, Map methods) {
+ this.classes = classes;
+ this.fields = fields;
+ this.methods = methods;
+ }
+
+ @Override
+ public String map(String internalName) {
+ return classes.getOrDefault(internalName, internalName);
+ }
+
+ @Override
+ public String mapFieldName(String owner, String name, String descriptor) {
+ return fields.getOrDefault(new MemberRef(owner, name, descriptor), name);
+ }
+
+ @Override
+ public String mapMethodName(String owner, String name, String descriptor) {
+ return methods.getOrDefault(new MemberRef(owner, name, descriptor), name);
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/transform/ClassForNameTransformer.java b/deob/src/main/java/dev/openrs2/deob/transform/ClassForNameTransformer.java
new file mode 100644
index 0000000000..b0ede053f1
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/transform/ClassForNameTransformer.java
@@ -0,0 +1,42 @@
+package dev.openrs2.deob.transform;
+
+import java.util.List;
+
+import dev.openrs2.asm.InsnMatcher;
+import dev.openrs2.asm.Transformer;
+import org.objectweb.asm.commons.Remapper;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+public final class ClassForNameTransformer extends Transformer {
+ private static final InsnMatcher INVOKE_MATCHER = InsnMatcher.compile("LDC INVOKESTATIC");
+
+ private static boolean isClassForName(List match) {
+ var ldc = (LdcInsnNode) match.get(0);
+ if (!(ldc.cst instanceof String)) {
+ return false;
+ }
+
+ var invokestatic = (MethodInsnNode) match.get(1);
+ return invokestatic.owner.equals("java/lang/Class") &&
+ invokestatic.name.equals("forName") &&
+ invokestatic.desc.equals("(Ljava/lang/String;)Ljava/lang/Class;");
+ }
+
+ private final Remapper remapper;
+
+ public ClassForNameTransformer(Remapper remapper) {
+ this.remapper = remapper;
+ }
+
+ @Override
+ public void transformMethod(ClassNode clazz, MethodNode method) {
+ INVOKE_MATCHER.match(method).filter(ClassForNameTransformer::isClassForName).forEach(match -> {
+ var ldc = (LdcInsnNode) match.get(0);
+ ldc.cst = remapper.map((String) ldc.cst);
+ });
+ }
+}
diff --git a/util/src/main/java/dev/openrs2/util/StringUtils.java b/util/src/main/java/dev/openrs2/util/StringUtils.java
new file mode 100644
index 0000000000..a3913c1c1f
--- /dev/null
+++ b/util/src/main/java/dev/openrs2/util/StringUtils.java
@@ -0,0 +1,29 @@
+package dev.openrs2.util;
+
+import com.google.common.base.Preconditions;
+
+public final class StringUtils {
+ public static String indefiniteArticle(String str) {
+ Preconditions.checkArgument(!str.isEmpty());
+
+ var first = Character.toLowerCase(str.charAt(0));
+ if (first == 'a' || first == 'e' || first == 'i' || first == 'o' || first == 'u') {
+ return "an";
+ } else {
+ return "a";
+ }
+ }
+
+ public static String capitalize(String str) {
+ if (str.isEmpty()) {
+ return str;
+ }
+
+ var first = Character.toUpperCase(str.charAt(0));
+ return first + str.substring(1);
+ }
+
+ private StringUtils() {
+ /* empty */
+ }
+}