diff --git a/asm/src/main/java/dev/openrs2/asm/FieldRef.java b/asm/src/main/java/dev/openrs2/asm/FieldRef.java
new file mode 100644
index 00000000..e2a488c1
--- /dev/null
+++ b/asm/src/main/java/dev/openrs2/asm/FieldRef.java
@@ -0,0 +1,49 @@
+package dev.openrs2.asm;
+
+import java.util.Objects;
+
+public final class FieldRef {
+ private final String owner, name, desc;
+
+ public FieldRef(String owner, String name, String desc) {
+ this.owner = owner;
+ this.name = name;
+ this.desc = desc;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ 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;
+ }
+ FieldRef fieldRef = (FieldRef) o;
+ return owner.equals(fieldRef.owner) &&
+ name.equals(fieldRef.name) &&
+ desc.equals(fieldRef.desc);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(owner, name, desc);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %s.%s", desc, owner, name);
+ }
+}
diff --git a/deob/pom.xml b/deob/pom.xml
index 56d02627..97f2c811 100644
--- a/deob/pom.xml
+++ b/deob/pom.xml
@@ -12,4 +12,16 @@
jar
OpenRS2 Deobfuscator
+
+
+
+ dev.openrs2
+ openrs2-asm
+ ${project.version}
+
+
+ ch.qos.logback
+ logback-classic
+
+
diff --git a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java
new file mode 100644
index 00000000..6ed671c3
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java
@@ -0,0 +1,57 @@
+package dev.openrs2.deob;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+import dev.openrs2.asm.Library;
+import dev.openrs2.asm.Transformer;
+import dev.openrs2.deob.transform.OpaquePredicateTransformer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class Deobfuscator {
+ private static final Logger logger = LoggerFactory.getLogger(Deobfuscator.class);
+
+ private static final List TRANSFORMERS = List.of(
+ new OpaquePredicateTransformer()
+ );
+
+ public static void main(String[] args) throws IOException {
+ var deobfuscator = new Deobfuscator(Paths.get("nonfree/code"));
+ deobfuscator.run();
+ }
+
+ private final Path input;
+
+ public Deobfuscator(Path input) {
+ this.input = input;
+ }
+
+ public void run() throws IOException {
+ var unpacker = Library.readJar(input.resolve("game_unpacker.dat"));
+ 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 libraries = Map.of(
+ "unpacker", unpacker,
+ "loader", loader,
+ "loader_gl", glLoader,
+ "runescape", client,
+ "runescape_gl", glClient
+ );
+
+ for (var entry : libraries.entrySet()) {
+ logger.info("Transforming library {}", entry.getKey());
+
+ for (var transformer : TRANSFORMERS) {
+ logger.info("Running transformer {}", transformer.getClass().getSimpleName());
+ transformer.transform(entry.getValue());
+ }
+ }
+ }
+}
diff --git a/deob/src/main/java/dev/openrs2/deob/transform/OpaquePredicateTransformer.java b/deob/src/main/java/dev/openrs2/deob/transform/OpaquePredicateTransformer.java
new file mode 100644
index 00000000..1aa46755
--- /dev/null
+++ b/deob/src/main/java/dev/openrs2/deob/transform/OpaquePredicateTransformer.java
@@ -0,0 +1,119 @@
+package dev.openrs2.deob.transform;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import dev.openrs2.asm.FieldRef;
+import dev.openrs2.asm.InsnMatcher;
+import dev.openrs2.asm.Library;
+import dev.openrs2.asm.Transformer;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.FieldInsnNode;
+import org.objectweb.asm.tree.JumpInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.VarInsnNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class OpaquePredicateTransformer extends Transformer {
+ private static final Logger logger = LoggerFactory.getLogger(OpaquePredicateTransformer.class);
+
+ private static final InsnMatcher FLOW_OBSTRUCTOR_INITIALIZER_MATCHER = InsnMatcher.compile("(GETSTATIC | ILOAD) IFEQ (((GETSTATIC ISTORE)? IINC ILOAD) | ((GETSTATIC | ILOAD) IFEQ ICONST GOTO ICONST)) PUTSTATIC");
+ private static final InsnMatcher OPAQUE_PREDICATE_MATCHER = InsnMatcher.compile("(GETSTATIC | ILOAD) (IFEQ | IFNE)");
+ private static final InsnMatcher STORE_MATCHER = InsnMatcher.compile("GETSTATIC ISTORE");
+
+ private final Set flowObstructors = new HashSet<>();
+ private int opaquePredicates, stores;
+
+ @Override
+ public void preTransform(Library library) {
+ flowObstructors.clear();
+ opaquePredicates = 0;
+ stores = 0;
+
+ for (var clazz : library) {
+ for (var method : clazz.methods) {
+ if ((method.access & (Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT)) == 0) {
+ findFlowObstructors(library, method);
+ }
+ }
+ }
+
+ logger.info("Identified flow obstructors {}", flowObstructors);
+ }
+
+ private void findFlowObstructors(Library library, MethodNode method) {
+ FLOW_OBSTRUCTOR_INITIALIZER_MATCHER.match(method).forEach(match -> {
+ /* add flow obstructor to set */
+ var putstatic = (FieldInsnNode) match.get(match.size() - 1);
+ flowObstructors.add(new FieldRef(putstatic.owner, putstatic.name, putstatic.desc));
+
+ /* remove initializer */
+ match.forEach(method.instructions::remove);
+
+ /* remove field */
+ var owner = library.get(putstatic.owner);
+ owner.fields.removeIf(field -> field.name.equals(putstatic.name) && field.desc.equals(putstatic.desc));
+ });
+ }
+
+ private boolean isFlowObstructor(FieldInsnNode insn) {
+ return flowObstructors.contains(new FieldRef(insn.owner, insn.name, insn.desc));
+ }
+
+ private boolean isOpaquePredicate(MethodNode method, List match) {
+ var load = match.get(0);
+
+ /* flow obstructor loaded directly? */
+ if (load.getOpcode() == Opcodes.GETSTATIC) {
+ var getstatic = (FieldInsnNode) load;
+ return isFlowObstructor(getstatic);
+ }
+
+ /* flow obstructor loaded via local variable? */
+ var iload = (VarInsnNode) load;
+ return STORE_MATCHER.match(method).anyMatch(storeMatch -> {
+ var getstatic = (FieldInsnNode) storeMatch.get(0);
+ var istore = (VarInsnNode) storeMatch.get(1);
+ return isFlowObstructor(getstatic) && iload.var == istore.var;
+ });
+ }
+
+ private boolean isRedundantStore(List match) {
+ var getstatic = (FieldInsnNode) match.get(0);
+ return isFlowObstructor(getstatic);
+ }
+
+ @Override
+ public void transformMethod(ClassNode clazz, MethodNode method) {
+ /* find and fix opaque predicates */
+ OPAQUE_PREDICATE_MATCHER.match(method).filter(match -> isOpaquePredicate(method, match)).forEach(match -> {
+ var branch = (JumpInsnNode) match.get(1);
+
+ if (branch.getOpcode() == Opcodes.IFEQ) {
+ /* branch is always taken */
+ method.instructions.remove(match.get(0));
+ branch.setOpcode(Opcodes.GOTO);
+ } else { /* IFNE */
+ /* branch is never taken */
+ match.forEach(method.instructions::remove);
+ }
+
+ opaquePredicates++;
+ });
+
+ /* remove redundant stores */
+ STORE_MATCHER.match(method).filter(this::isRedundantStore).forEach(match -> {
+ match.forEach(method.instructions::remove);
+ stores++;
+ });
+ }
+
+ @Override
+ public void postTransform(Library library) {
+ logger.info("Removed {} opaque predicates and {} redundant stores", opaquePredicates, stores);
+ }
+}