Skip to content

Instantly share code, notes, and snippets.

@PlanetTeamSpeakk
Last active April 18, 2022 10:13
Show Gist options
  • Save PlanetTeamSpeakk/def82764fd0d730e375394233175f016 to your computer and use it in GitHub Desktop.
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.
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