-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
944f013
commit 61a0273
Showing
7 changed files
with
358 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,108 @@ | ||
# simpledeobf | ||
simple deobfuscator for Minecraft mods | ||
### What is this? | ||
|
||
**simpledeobf** is a very simple and primitive command-line deobfuscator for | ||
Minecraft mods. You can use it to create dev versions of mod releases | ||
for debugging. Or anything else, really - it all depends on what inputs and | ||
mapping files you use. Feel free to get creative with it. | ||
|
||
### How to use it? | ||
|
||
simpledeobf will need files from your Gradle cache which are created by | ||
ForgeGradle, so it's best to start with a ForgeGradle workspace already | ||
set up for your target Minecraft version. | ||
|
||
Command-line options: | ||
|
||
* `--output` The path to the resulting jar. You can only have one. | ||
* `--input` The input jar you wish to deobfuscate. Can be repeated, but the | ||
files from all inputs will just end up in a single output jar. | ||
* `--mapFile` The path to the MCP mapping file, found in your Gradle cache | ||
somewhere under `minecraft/de/oceanlabs/mcp`. You need to use the one that | ||
corresponds to the namespaces you are converting from and to, and also | ||
matches the ones you use in your project. Can be repeated, later files will | ||
overwrite mappings from previous ones. | ||
* `--map` Defines a single explicit mapping. This option is treated as if | ||
it was a line in the MCP mapping file. Takes precedence over mappings | ||
read from MCP files. Can be repeated. | ||
* `--ref` The path to a reference jar. Can be repeated. The classes inside | ||
are read only to determine the class hierarchy, which may be needed to properly | ||
deobfuscate certain jar files. The reference jar must be in the same namespace | ||
as the input. | ||
|
||
More precisely: you will need to provide a proper class hierarchy | ||
to deobfuscate overridden methods whose declaring classes or interfaces are | ||
2 or more levels above the overriding class. | ||
|
||
When in doubt, try using a Minecraft jar of the proper namespace. | ||
* `--defaultPkg` move all classes from the default package into this one. Having | ||
classes in the default package will mess with source attachment in the IDE. | ||
* `--forcePublic` make all fields, methods and inner classes public. The | ||
poor man's access transformer. | ||
* `--help` or `-?` displays a quick overview of these options. | ||
|
||
Have your favourite brand of decompiler ready, and be prepared to dig through | ||
the result, and make several iterations before you end up with something | ||
usable. | ||
|
||
Also there's no guarantee that it's even possible to create a workable dev jar | ||
for any given mod. If it uses reflection and/or class transformation, chances | ||
are good that it will just crash anyway, unless it's specifically made to be | ||
environment agnostic. | ||
|
||
### Have OptiFine, will debug | ||
|
||
As an example, here is a guide on getting OptiFine into your dev | ||
environment with simpledeobf. This is *the* prime use-case of simpledeobf, | ||
and the reason for its existence. | ||
|
||
The following is the actual command I use for the deobfuscation itself, | ||
so you'll need to change the directory names. It's a single command, but I | ||
broke it up into lines for readability. | ||
``` | ||
java -jar simpledeobf-0.5.jar | ||
--input h:\Minecraft\mods\obf\OptiFine_1.8.8_HD_U_H2.jar | ||
--output h:\Minecraft\mods\mcp\OptiFine_1.8.8_HD_U_H2-dev.jar | ||
--mapFile h:\Minecraft\.gradle\caches\minecraft\de\oceanlabs\mcp\mcp_stable\20\srgs\notch-mcp.srg | ||
--ref h:\Minecraft\.gradle\caches\minecraft\net\minecraft\minecraft_merged\1.8.8\minecraft_merged-1.8.8.jar | ||
--map="CL: bet$1 net/minecraft/client/entity/AbstractClientPlayer$1" | ||
--map="CL: b$8 net/minecraft/crash/CrashReport$8" | ||
--defaultPkg optifineroot | ||
--forcePublic | ||
``` | ||
This will give you a jar with the `stable_20` mappings. The 2 `map` options are | ||
needed because OptiFine declares some extra inner classes that are not present | ||
in vanilla and have no mappings. If you want to do this for a different Minecraft | ||
version, you'll have to change these. Just start without manual mappings, and | ||
check if there are still obfuscated classes in the result. Check in the MCP | ||
files what the outer class is, and add a mapping. Rinse and repeat. | ||
|
||
Open the resulting jar file, and delete the `net/minecraftforge` directory. | ||
There are dummy classes inside that are normally not loaded, but will cause | ||
problems in a dev environment. They need to go. | ||
|
||
Now you have to create a tweaker and class transformer, because the default | ||
OptiFine ones will not work properly in a dev environment. | ||
[Here is mine](https://github.com/octarine-noise/BetterFoliage/blob/c0be72bb37311508c68db5bd3b09d2f99a76614c/src/main/kotlin/optifine/OptifineTweakerDevWrapper.kt) | ||
that I use in Better Foliage, you need something similar. The point is to | ||
change dots to slashes in the class names, so the OptiFine transformer can find | ||
the deobfuscated class files. | ||
|
||
Edit the `META-INF/MANIFEST.MF` file. Change the `TweakClass` option to the | ||
tweaker you just made. | ||
|
||
The jar is now ready. If you also want source attachment, which is highly | ||
recommended, just decompile it with your favourite tool (I prefer JD-GUI), and | ||
save a source jar. Make sure that *"Realign line numbers"* or the equivalent | ||
option is turned on. | ||
|
||
Drop the jar in the mods folder of your workspace, manually add it to the project | ||
dependencies after all the Gradle stuff, set its source attachment, and you're | ||
ready. You can start your project with OptiFine thrown in the mix, debug, | ||
set breakpoints, and everything. | ||
|
||
**Note:** Debugging *into* one of the vanilla classes that OptiFine overwrites | ||
may or may not give you some headache under Eclipse. I can only confirm it works | ||
fine under IDEA, which allows you to switch between sources on the fly if multiple | ||
jar files declare a class. A popup comes up saying *"Alternative source available | ||
for the class blah blah blah"*, and you can switch to the OptiFine jar, which | ||
contains the class actually executing. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
apply plugin: 'kotlin' | ||
|
||
group = 'com.octarine' | ||
version = '0.5' | ||
|
||
repositories { mavenCentral() } | ||
|
||
buildscript { | ||
ext.kotlin_version = '1.0.0' | ||
repositories { | ||
mavenCentral() | ||
} | ||
dependencies { | ||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||
} | ||
} | ||
|
||
dependencies { | ||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||
compile 'org.ow2.asm:asm-debug-all:5.0.4' | ||
compile 'net.sf.jopt-simple:jopt-simple:4.9' | ||
} | ||
|
||
jar { | ||
manifest { | ||
attributes "Main-Class": "com.octarine.simpledeobf.Main" | ||
} | ||
|
||
configurations.compile.each { dep -> | ||
from(project.zipTree(dep)){ | ||
exclude 'META-INF', 'META-INF/**' | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
rootProject.name = 'simpledeobf' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
@file:JvmName("Main") | ||
package com.octarine.simpledeobf | ||
|
||
import joptsimple.OptionException | ||
import joptsimple.OptionParser | ||
import org.objectweb.asm.ClassReader | ||
import org.objectweb.asm.ClassWriter | ||
import org.objectweb.asm.tree.ClassNode | ||
import java.io.File | ||
import java.io.FileNotFoundException | ||
import java.io.FileOutputStream | ||
import java.util.zip.ZipEntry | ||
import java.util.zip.ZipFile | ||
import java.util.zip.ZipOutputStream | ||
|
||
fun main(args : Array<String>) { | ||
|
||
try { | ||
val parser = OptionParser() | ||
val inputFile = parser.accepts("input", "Path to input JAR file").withRequiredArg().ofType(File::class.java).required() | ||
val outputFile = parser.accepts("output", "Path to output JAR file").withRequiredArg().ofType(File::class.java).required() | ||
val referenceFile = parser.accepts("ref", "Path to reference JAR file").withRequiredArg().ofType(File::class.java) | ||
val mappingFile = parser.accepts("mapFile", "Path to mapping file").withRequiredArg().ofType(File::class.java) | ||
val mapping = parser.accepts("map", "Manual mapping entry").withRequiredArg().ofType(String::class.java) | ||
val defaultPkg = parser.accepts("defaultPkg", "Map default package").withRequiredArg().ofType(String::class.java) | ||
val forcePublic = parser.accepts("forcePublic", "Force everything to be public") | ||
val help = parser.acceptsAll(listOf("?", "help")).forHelp() | ||
|
||
val options = parser.parse(*args) | ||
|
||
if (options.has(help)) { | ||
parser.printHelpOn(System.out) | ||
System.exit(0) | ||
} | ||
|
||
if (options.valuesOf(outputFile).size != 1) { | ||
println("Maximum of 1 output file is allowed") | ||
System.exit(1) | ||
} | ||
|
||
if (options.valuesOf(defaultPkg).size > 1) { | ||
println("Maximum of 1 default package is allowed") | ||
System.exit(1) | ||
} | ||
|
||
val mapper = SimpleRemapper(options.valueOf(defaultPkg)) | ||
options.valuesOf(mappingFile).forEach { | ||
println("Reading mappings from: ${it.absolutePath}") | ||
mapper.readMappingFile(it) | ||
} | ||
options.valuesOf(mapping).forEach { mapper.readMappingLine(it) } | ||
options.valuesOf(referenceFile).forEach { | ||
println("Reading hiererchy from: ${it.absolutePath}") | ||
mapper.hierarchyReader.visitAllFromFile(it) | ||
} | ||
options.valuesOf(inputFile).forEach { | ||
println("Reading hiererchy from: ${it.absolutePath}") | ||
mapper.hierarchyReader.visitAllFromFile(it) | ||
} | ||
|
||
val destJar = options.valuesOf(outputFile)[0].let { | ||
if (it.exists()) it.delete() | ||
ZipOutputStream(FileOutputStream(it)) | ||
} | ||
|
||
for (srcFile in options.valuesOf(inputFile)) { | ||
println("Processing input file: ${srcFile.absolutePath}") | ||
val srcJar = ZipFile(srcFile) | ||
for (srcEntry in srcJar.entries()) { | ||
if (srcEntry.name.endsWith(".class")) { | ||
print(" processing: ${srcEntry.name} ") | ||
val srcBytes = srcJar.getInputStream(srcEntry).readBytes() | ||
val srcClass = ClassNode().apply { ClassReader(srcBytes).accept(this, ClassReader.EXPAND_FRAMES) } | ||
val destClass = mapper.remapClass(srcClass, options.has(forcePublic)) | ||
val destBytes = ClassWriter(0).apply { destClass.accept(this) }.toByteArray() | ||
|
||
println(if (!srcEntry.name.startsWith(destClass.name)) "-> ${destClass.name}.class" else "") | ||
destJar.putNextEntry(ZipEntry("${destClass.name}.class")) | ||
destJar.write(destBytes) | ||
destJar.closeEntry() | ||
} else { | ||
println(" copying: ${srcEntry.name}") | ||
destJar.putNextEntry(ZipEntry(srcEntry.name)) | ||
srcJar.getInputStream(srcEntry).copyTo(destJar) | ||
destJar.closeEntry() | ||
} | ||
} | ||
} | ||
|
||
println("Conversion finished") | ||
destJar.close() | ||
} catch(e: OptionException) { | ||
println(e.message) | ||
System.exit(1) | ||
} catch(e: FileNotFoundException) { | ||
println(e.message) | ||
System.exit(1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package com.octarine.simpledeobf | ||
|
||
import org.objectweb.asm.* | ||
import org.objectweb.asm.commons.Remapper | ||
import org.objectweb.asm.commons.RemappingClassAdapter | ||
import org.objectweb.asm.tree.ClassNode | ||
import java.io.File | ||
import java.util.* | ||
import java.util.zip.ZipFile | ||
|
||
class SimpleRemapper(val defaultPkg: String?) : Remapper() { | ||
|
||
val String.partOwner: String get() = this.substring(0, this.lastIndexOf("/")) | ||
val String.partName: String get() = this.substring(this.lastIndexOf("/") + 1) | ||
|
||
val mappings = HashMap<String, ClassMapping>() | ||
val hierarchy = HashMap<String, ClassHierarchy>() | ||
val hierarchyReader = SimpleHierarchyReader() | ||
|
||
inner class SimpleHierarchyReader : ClassVisitor(Opcodes.ASM5) { | ||
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>?) { | ||
hierarchy.put(name, ClassHierarchy(superName, interfaces)) | ||
} | ||
|
||
fun visitAllFromFile(file: File) { | ||
val jarFile = ZipFile(file) | ||
for (srcEntry in jarFile.entries()) { | ||
if (srcEntry.name.endsWith(".class")) { | ||
val srcBytes = jarFile.getInputStream(srcEntry).readBytes() | ||
ClassReader(srcBytes).accept(this, ClassReader.EXPAND_FRAMES) | ||
} | ||
} | ||
} | ||
} | ||
|
||
fun remapClass(srcClass: ClassNode, forcePublic: Boolean) = ClassNode().apply { | ||
srcClass.accept(PublicAccessRemappingClassAdapter(this, this@SimpleRemapper, forcePublic)) | ||
} | ||
|
||
fun readMappingFile(file: File) { | ||
if (!file.exists()) throw Exception("Mappings file doesn't exist: ${file.absolutePath}") | ||
file.readLines().forEach { readMappingLine(it) } | ||
} | ||
|
||
fun readMappingLine(line: String) { | ||
val tokens = line.split(" ") | ||
if (tokens[0] == "CL:") addClassMapping(tokens[1], tokens[2]) | ||
if (tokens[0] == "FD:") addFieldMapping(tokens[1].partOwner, tokens[1].partName, tokens[2].partOwner, tokens[2].partName) | ||
if (tokens[0] == "MD:") addMethodMapping(tokens[1].partOwner, tokens[1].partName, tokens[2], tokens[3].partOwner, tokens[3].partName, tokens[4]) | ||
} | ||
|
||
fun addClassMapping(fromName: String, toName: String) { | ||
mappings.put(fromName, ClassMapping(toName)) | ||
} | ||
|
||
fun addFieldMapping(fromOwner: String, fromName: String, toOwner: String, toName: String) { | ||
mappings[fromOwner]?.fields?.put(fromName, toName) | ||
} | ||
|
||
fun addMethodMapping(fromOwner: String, fromName: String, fromDesc: String, toOwner: String, toName: String, toDesc: String) { | ||
mappings[fromOwner]?.methods?.put(fromName to fromDesc, toName) | ||
} | ||
|
||
override fun map(typeName: String): String? = (mappings[typeName]?.mappedName ?: typeName).let { | ||
if (it.contains("/") || defaultPkg == null) it else "$defaultPkg/$it" | ||
} | ||
|
||
override fun mapFieldName(owner: String, name: String, desc: String?): String? { | ||
mappings[owner]?.let { it.fields[name] }?.let { return it } | ||
hierarchy[owner]?.superName?.let { return mapFieldName(it, name, desc) } | ||
return name | ||
} | ||
|
||
override fun mapMethodName(owner: String, name: String, desc: String) = | ||
mapMethodNameInternal(owner, name, desc) ?: name | ||
|
||
fun mapMethodNameInternal(owner: String, name: String, desc: String): String? { | ||
mappings[owner]?.let { it.methods[name to desc] }?.let { return it } | ||
val h = hierarchy[owner] ?: return null | ||
if (h.superName != null) mapMethodNameInternal(h.superName, name, desc)?.let { return it } | ||
if (h.interfaces != null) for (interfaceName in h.interfaces) { | ||
mapMethodNameInternal(interfaceName, name, desc)?.let { return it } | ||
} | ||
return null | ||
} | ||
} | ||
|
||
class PublicAccessRemappingClassAdapter(cv: ClassVisitor, remapper: Remapper, val force: Boolean) : RemappingClassAdapter(cv, remapper) { | ||
|
||
val Int.toPublic: Int get() = (this and 0xFFF8) or 0x1 | ||
|
||
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? { | ||
return super.visitMethod(if (force) access.toPublic else access, name, desc, signature, exceptions) | ||
} | ||
|
||
override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor? { | ||
return super.visitField(if (force) access.toPublic else access, name, desc, signature, value) | ||
} | ||
|
||
override fun visitInnerClass(name: String?, outerName: String?, innerName: String?, access: Int) { | ||
super.visitInnerClass(name, outerName, innerName, if (force) access.toPublic else access) | ||
} | ||
} | ||
|
||
class ClassMapping(val mappedName: String) { | ||
val fields = HashMap<String, String>() | ||
val methods = HashMap<Pair<String, String>, String>() | ||
} | ||
|
||
class ClassHierarchy(val superName: String?, val interfaces: Array<out String>?) {} |