diff --git a/decompiler/pom.xml b/decompiler/pom.xml
new file mode 100644
index 0000000000..2f67e781a0
--- /dev/null
+++ b/decompiler/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+
+ dev.openrs2
+ openrs2
+ 1.0.0-SNAPSHOT
+
+
+ openrs2-decompiler
+ jar
+
+ OpenRS2 Decompiler
+
+
+
+ dev.openrs2
+ openrs2-fernflower
+ ${project.version}
+
+
+ dev.openrs2
+ openrs2-util
+ ${project.version}
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.java b/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.java
new file mode 100644
index 0000000000..c04a5d89fb
--- /dev/null
+++ b/decompiler/src/main/java/dev/openrs2/decompiler/Decompiler.java
@@ -0,0 +1,73 @@
+package dev.openrs2.decompiler;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.jetbrains.java.decompiler.main.Fernflower;
+import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;
+
+public final class Decompiler implements Closeable {
+ private static final Map OPTIONS = Map.of(
+ IFernflowerPreferences.ASCII_STRING_CHARACTERS, "1",
+ IFernflowerPreferences.INDENT_STRING, "\t",
+ IFernflowerPreferences.SYNTHETIC_NOT_SET, "1"
+ );
+
+ private static Path getDestination(String archive) {
+ archive = archive.replaceAll("_gl[.]jar$", "");
+ if (archive.equals("runescape")) {
+ archive = "client";
+ }
+ return Paths.get("nonfree").resolve(archive).resolve("src/main/java");
+ }
+
+ public static void main(String[] args) throws IOException {
+ var libraries = List.of(Paths.get("nonfree/code/jogl.jar"));
+
+ var deobOutput = Paths.get("nonfree/code/deob");
+ var sources = List.of(
+ deobOutput.resolve("runescape_gl.jar"),
+ deobOutput.resolve("loader_gl.jar"),
+ deobOutput.resolve("signlink_gl.jar"),
+ deobOutput.resolve("unpack_gl.jar"),
+ deobOutput.resolve("unpacker_gl.jar")
+ );
+
+ try (var decompiler = new Decompiler(libraries, sources, Decompiler::getDestination)) {
+ decompiler.run();
+ }
+ }
+
+ private final DecompilerIo io;
+ private final Fernflower fernflower;
+ private final List libraries, sources;
+
+ public Decompiler(List libraries, List sources, Function destination) {
+ this.io = new DecompilerIo(destination);
+ this.fernflower = new Fernflower(io, io, OPTIONS, Slf4jFernflowerLogger.INSTANCE);
+ this.libraries = libraries;
+ this.sources = sources;
+ }
+
+ public void run() {
+ for (var library : libraries) {
+ fernflower.addLibrary(library.toFile());
+ }
+
+ for (var source : sources) {
+ fernflower.addSource(source.toFile());
+ }
+
+ fernflower.decompileContext();
+ }
+
+ @Override
+ public void close() throws IOException {
+ io.close();
+ }
+}
diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.java b/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.java
new file mode 100644
index 0000000000..69f95b67e8
--- /dev/null
+++ b/decompiler/src/main/java/dev/openrs2/decompiler/DecompilerIo.java
@@ -0,0 +1,97 @@
+package dev.openrs2.decompiler;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import com.google.common.io.ByteStreams;
+import org.jetbrains.java.decompiler.main.extern.IBytecodeProvider;
+import org.jetbrains.java.decompiler.main.extern.IResultSaver;
+
+public final class DecompilerIo implements IBytecodeProvider, IResultSaver, Closeable {
+ private final Map inputJars = new HashMap<>();
+ private final Function destination;
+
+ public DecompilerIo(Function destination) {
+ this.destination = destination;
+ }
+
+ @Override
+ public byte[] getBytecode(String externalPath, String internalPath) throws IOException {
+ if (internalPath == null) {
+ throw new UnsupportedOperationException();
+ }
+
+ var jar = inputJars.get(externalPath);
+ if (jar == null) {
+ jar = new JarFile(externalPath);
+ inputJars.put(externalPath, jar);
+ }
+
+ try (var in = jar.getInputStream(jar.getJarEntry(internalPath))) {
+ return ByteStreams.toByteArray(in);
+ }
+ }
+
+ @Override
+ public void saveFolder(String path) {
+ /* ignore */
+ }
+
+ @Override
+ public void copyFile(String source, String path, String entryName) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void createArchive(String path, String archiveName, Manifest manifest) {
+ /* ignore */
+ }
+
+ @Override
+ public void saveDirEntry(String path, String archiveName, String entryName) {
+ /* ignore */
+ }
+
+ @Override
+ public void copyEntry(String source, String path, String archiveName, String entry) {
+ /* ignore */
+ }
+
+ @Override
+ public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) {
+ var p = destination.apply(archiveName).resolve(entryName);
+ try {
+ Files.createDirectories(p.getParent());
+ try (var writer = Files.newBufferedWriter(p)) {
+ writer.write(content);
+ }
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ @Override
+ public void closeArchive(String path, String archiveName) {
+ /* ignore */
+ }
+
+ @Override
+ public void close() throws IOException {
+ for (var jar : inputJars.values()) {
+ jar.close();
+ }
+ }
+}
diff --git a/decompiler/src/main/java/dev/openrs2/decompiler/Slf4jFernflowerLogger.java b/decompiler/src/main/java/dev/openrs2/decompiler/Slf4jFernflowerLogger.java
new file mode 100644
index 0000000000..f82bb15943
--- /dev/null
+++ b/decompiler/src/main/java/dev/openrs2/decompiler/Slf4jFernflowerLogger.java
@@ -0,0 +1,52 @@
+package dev.openrs2.decompiler;
+
+import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class Slf4jFernflowerLogger extends IFernflowerLogger {
+ private static final Logger logger = LoggerFactory.getLogger(Slf4jFernflowerLogger.class);
+
+ public static final IFernflowerLogger INSTANCE = new Slf4jFernflowerLogger();
+
+ private Slf4jFernflowerLogger() {
+ /* empty */
+ }
+
+ @Override
+ public void startClass(String className) {
+ logger.info("Decompiling {}", className);
+ }
+
+ @Override
+ public void writeMessage(String message, Severity severity) {
+ switch (severity) {
+ case TRACE:
+ logger.trace(message);
+ case INFO:
+ logger.info(message);
+ case WARN:
+ logger.warn(message);
+ case ERROR:
+ logger.error(message);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public void writeMessage(String message, Severity severity, Throwable t) {
+ switch (severity) {
+ case TRACE:
+ logger.trace(message, t);
+ case INFO:
+ logger.info(message, t);
+ case WARN:
+ logger.warn(message, t);
+ case ERROR:
+ logger.error(message, t);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index f134c2a46b..5122367be2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,7 @@
asm
+ decompiler
deob
deob-annotations
fernflower