Last active
April 18, 2022 10:13
-
-
Save PlanetTeamSpeakk/def82764fd0d730e375394233175f016 to your computer and use it in GitHub Desktop.
Small tool I made used to automatically use the ASMDump#map(String, String) method to map dumps made by ASMifier. Must be run in a Fabric development environment and assumes there's a mappings.tiny file taken from a yarn v2 jar on the classpath.
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
package com.ptsmods.morecommands; | |
import com.google.common.collect.ImmutableList; | |
import org.apache.commons.lang3.tuple.Pair; | |
import org.objectweb.asm.util.ASMifier; | |
import java.io.*; | |
import java.util.*; | |
import java.util.function.BiFunction; | |
import java.util.function.Function; | |
import java.util.regex.MatchResult; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
public class ASMRemapper { | |
public static void main(String[] args) throws IOException { | |
Map<String, ClassMapping> classes = new HashMap<>(); | |
Map<String, ClassMapping> iClasses = new HashMap<>(); | |
Map<Pair<String, String>, MethodMapping> methods = new HashMap<>(); | |
Map<Pair<String, String>, FieldMapping> fields = new HashMap<>(); | |
List<Pair<Integer, String[]>> rawMappings = new BufferedReader(new InputStreamReader(Objects.requireNonNull(ASMRemapper.class.getResourceAsStream("/mappings.tiny")))).lines() | |
.skip(1) | |
.map(s -> Pair.of(s.length() - s.stripLeading().length(), s.trim().split("\t"))) | |
.toList(); | |
for (Pair<Integer, String[]> rawMapping : rawMappings) { | |
String[] mapping = rawMapping.getRight(); | |
MappingType type = MappingType.fromChar(mapping[0].charAt(0)); | |
if (rawMapping.getLeft() == 0 && type == MappingType.CLASS) { | |
ClassMapping classMapping = new ClassMapping(mapping[1], mapping.length > 2 ? mapping[2] : mapping[1]); // No mapping yet | |
classes.put(mapping.length > 2 ? mapping[2] : mapping[1], classMapping); | |
iClasses.put(mapping[1], classMapping); | |
} | |
} | |
ClassMapping lastClass = null; | |
Pattern intermediateClassPattern = Pattern.compile("L(net/minecraft/.*?);"); | |
for (Pair<Integer, String[]> rawMapping : rawMappings) { | |
String[] mapping = rawMapping.getRight(); | |
MappingType type = MappingType.fromChar(mapping[0].charAt(0)); | |
if (type == MappingType.PARAM) continue; | |
if (type == MappingType.CLASS) { | |
if (rawMapping.getLeft() == 0) lastClass = classes.get(mapping.length > 2 ? mapping[2] : mapping[1]); | |
continue; | |
} | |
Objects.requireNonNull(lastClass); | |
if (type == MappingType.METHOD) methods.put(Pair.of(lastClass.named(), mapping[3] + intermediateClassPattern.matcher(mapping[1]) | |
.replaceAll(res -> Matcher.quoteReplacement("L" + (iClasses.containsKey(res.group(1)) ? iClasses.get(res.group(1)).named() : res.group(1)) + ";"))), | |
new MethodMapping(lastClass.intermediary(), mapping[1], mapping[2], mapping[3])); | |
else if (type == MappingType.FIELD) fields.put(Pair.of(lastClass.named(), mapping[3]), | |
new FieldMapping(lastClass.intermediary(), mapping[1], mapping[2], mapping[3])); | |
} | |
PrintStream ogOut = System.out; | |
// Capture System output when running ASMifier. | |
OutputStream dataStream = new ByteArrayOutputStream(); | |
System.setOut(new PrintStream(dataStream)); | |
ASMifier.main(args); | |
System.setOut(ogOut); | |
String ogData = dataStream.toString(); | |
String data = ogData.replace("implements Opcodes", "extends ASMDump implements Opcodes"); | |
BiFunction<String, Boolean, Function<MatchResult, String>> methodMatcher = (prefix, appendSC) -> res -> { | |
String intermediary; | |
if ("<init>".equals(res.group(3)) || "<clinit>".equals(res.group(3))) intermediary = null; // (Static) constructors do not get remapped, obviously. | |
else if (methods.containsKey(Pair.of(res.group(2), res.group(3) + res.group(4)))) | |
intermediary = methods.get(Pair.of(res.group(2), res.group(3) + res.group(4))).intermediary(); | |
else { | |
Class<?> owner; | |
try { | |
owner = Class.forName(res.group(2).replace('/', '.'), false, ASMRemapper.class.getClassLoader()); | |
} catch (ClassNotFoundException e) { | |
throw new RuntimeException(e); | |
} | |
Descriptor descriptor = parseDescriptor(res.group(4)); | |
if (owner.isEnum() && ("values".equals(res.group(3)) && descriptor.returnType() == owner.arrayType() && descriptor.parameterTypes().isEmpty() || | |
"valueOf".equals(res.group(3)) && descriptor.returnType() == owner && descriptor.parameterTypes().size() == 1 && descriptor.parameterTypes().get(0) == String.class)) | |
intermediary = null; // Default methods 'values' and 'valueOf' of Enums do not get remapped, obviously. | |
else intermediary = methods.get(Pair.of(getDeclaringClass(owner, res.group(3), parseDescriptor(res.group(4)).parameterTypes()).getName().replace('.', '/'), | |
res.group(3) + res.group(4))).intermediary(); | |
} | |
return Matcher.quoteReplacement(String.format(prefix + "(%s, \"%s\", %s, \"%s\", %s)" + (appendSC ? ";" : ""), res.group(1), res.group(2), | |
intermediary == null ? '"' + res.group(3) + '"' : formatMapCall(intermediary, res.group(3)), | |
res.group(4), res.group(5))); | |
}; | |
// Map method instructions | |
data = Pattern.compile("methodVisitor\\.visitMethodInsn\\(([A-Z]*), \"(net/minecraft/[A-Za-z\\d/$]*)\", \"(.*)\", \"(.*)\", (.*)\\)") | |
.matcher(data) | |
.replaceAll(methodMatcher.apply("methodVisitor.visitMethodInsn", true)); | |
// Map handles | |
data = Pattern.compile("new Handle\\(([\\w.]*), \"(net/minecraft/[A-Za-z\\d/$]*)\", \"(.*?)\", \"(.*?)\", (.*?)\\)") | |
.matcher(data) | |
.replaceAll(methodMatcher.apply("new Handle", false)); | |
// Map field instructions | |
data = Pattern.compile("methodVisitor\\.visitFieldInsn\\(([A-Z]*), \"(net/minecraft/[A-Za-z\\d/$]*)\", \"(.*)\", \"(.*)\"\\);") | |
.matcher(data) | |
.replaceAll(res -> Matcher.quoteReplacement(String.format("methodVisitor.visitFieldInsn(%s, \"%s\", %s, \"%s\");", res.group(1), res.group(2), | |
formatMapCall(fields.get(Pair.of(res.group(2), res.group(3))).intermediary(), res.group(3)), res.group(4)))); | |
// Map inner classes | |
data = Pattern.compile("classWriter\\.visitInnerClass\\(\"(net/minecraft/[A-Za-z\\d/]*/[A-Za-z\\d$]*)\", \"(net/minecraft/[A-Za-z\\d/]*/[A-Za-z\\d$]*)\", \"(.*?)\", (.*?)\\);") | |
.matcher(data) | |
.replaceAll(res -> Matcher.quoteReplacement(String.format("classWriter.visitInnerClass(\"%s\", \"%s\", %s, %s);", res.group(1), res.group(2), | |
formatMapCall(classes.get(res.group(1)).intermediary().substring(classes.get(res.group(1)).intermediary().lastIndexOf('$') + 1), | |
res.group(3)), res.group(4)))); | |
// Map Minecraft classes | |
data = Pattern.compile("(L?)(net/minecraft/[A-Za-z\\d/]*/[A-Za-z\\d$]*)(;?)") | |
.matcher(data) | |
.replaceAll(res -> "\" + " + Matcher.quoteReplacement(formatMapCall(res.group(1) + classes.get(res.group(2)).intermediary() + res.group(3), res.group(1) + res.group(2) + res.group(3))) + " + \""); | |
System.out.println(data.replace(" + \"\"", "").replace("\"\" + ", "")); // Remove empty string concatenation resulting from earlier replacements. | |
} | |
private static String formatMapCall(String arg1, String arg2) { | |
return String.format("map(\"%s\", \"%s\")", arg1.replace("\"", "\\\""), arg2.replace("\"", "\\\"")); | |
} | |
private static Descriptor parseDescriptor(String descriptor) { | |
List<Class<?>> classes = new ArrayList<>(); | |
boolean readingRetType = false; | |
int arrayDepth = 0; | |
for (int i = 0; i < descriptor.length(); i++) { | |
if (i == 0 && descriptor.charAt(i) == '(') continue; | |
if (descriptor.charAt(i) == ')') { | |
readingRetType = true; | |
continue; | |
} | |
if (descriptor.charAt(i) == '[') { | |
arrayDepth++; | |
continue; | |
} | |
Class<?> c = switch (descriptor.charAt(i)) { | |
case 'Z' -> boolean.class; | |
case 'B' -> byte.class; | |
case 'C' -> char.class; | |
case 'D' -> double.class; | |
case 'F' -> float.class; | |
case 'I' -> int.class; | |
case 'J' -> long.class; | |
case 'S' -> short.class; | |
case 'L' -> { | |
int sci = descriptor.substring(i + 1).indexOf(';'); | |
try { | |
String name = descriptor.substring(i + 1, i + 1 + sci).replace('/', '.'); | |
i += sci + 1; | |
yield Class.forName(name, false, ASMRemapper.class.getClassLoader()); | |
} catch (ClassNotFoundException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
case 'V' -> void.class; // For return types | |
default -> throw new IllegalStateException("Unexpected value: " + descriptor.charAt(i)); | |
}; | |
while (arrayDepth > 0) { | |
c = c.arrayType(); | |
arrayDepth--; | |
} | |
if (!readingRetType) classes.add(c); | |
else return new Descriptor(c, ImmutableList.copyOf(classes)); | |
} | |
throw new IllegalArgumentException("Invalid descriptor"); | |
} | |
private static Class<?> getDeclaringClass(Class<?> owner, String methodName, List<Class<?>> classes) { | |
Class<?> c = owner; | |
Class<?>[] classesArray = classes.toArray(new Class[0]); | |
do { | |
owner = c; | |
c = getDeclaringClass0(true, owner, methodName, classesArray); | |
// E.g. PlayerEntity#getUuid() is declared in the Entity class, but that class gets it from the EntityLike interface. | |
} while (c != null); | |
return owner; | |
} | |
private static Class<?> getDeclaringClass0(boolean skipOwner, Class<?> owner, String methodName, Class<?>... classes) { | |
if (!skipOwner) | |
try { | |
owner.getDeclaredMethod(methodName, classes); | |
return owner; | |
} catch (NoSuchMethodException ignored) {} | |
for (Class<?> iface : owner.getInterfaces()) { | |
Class<?> c = getDeclaringClass0(false, iface, methodName, classes); | |
if (c != null) return c; | |
} | |
Class<?> sup = owner.getSuperclass(); | |
if (sup != null) | |
return getDeclaringClass0(false, sup, methodName, classes); | |
return null; | |
} | |
public interface Mapping { | |
MappingType type(); | |
String intermediary(); | |
String named(); | |
} | |
public record ClassMapping(String intermediary, String named) implements Mapping { | |
@Override | |
public MappingType type() { | |
return MappingType.CLASS; | |
} | |
} | |
public record MethodMapping(String owner, String signature, String intermediary, String named) implements Mapping { | |
@Override | |
public MappingType type() { | |
return MappingType.METHOD; | |
} | |
} | |
public record FieldMapping(String owner, String descriptor, String intermediary, String named) implements Mapping { | |
@Override | |
public MappingType type() { | |
return MappingType.FIELD; | |
} | |
} | |
public record Descriptor(Class<?> returnType, List<Class<?>> parameterTypes) {} | |
public enum MappingType { | |
CLASS, METHOD, FIELD, PARAM; | |
public static MappingType fromChar(char ch) { | |
return switch (ch) { | |
case 'c' -> CLASS; | |
case 'm' -> METHOD; | |
case 'f' -> FIELD; | |
case 'p' -> PARAM; | |
default -> throw new IllegalStateException("Unexpected value: " + ch); | |
}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment