package de.geolykt.starplane.remapping; import java.io.BufferedReader; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.stianloader.remapper.MemberRef; import org.stianloader.remapper.Remapper; import org.stianloader.remapper.SimpleMappingLookup; import de.geolykt.starplane.Utils; public class StarplaneMappingLookup extends SimpleMappingLookup { @NotNull private final Path map; private final boolean reverse; private final boolean ignoreNonExistentFiles; public StarplaneMappingLookup(@NotNull Path map, boolean reversed) { this(map, reversed, false); } public StarplaneMappingLookup(@NotNull Path map, boolean reversed, boolean ignoreNonExistentFiles) { this.map = map; this.reverse = reversed; this.ignoreNonExistentFiles = ignoreNonExistentFiles; } @NotNull @Contract(mutates = "this", pure = false, value = "-> this") public StarplaneMappingLookup load() throws IOException { if (this.ignoreNonExistentFiles && Files.notExists(this.map)) { return this; } // For reversed mappings to take effect correctly, we need to "delay" the application of member mappings // until all classes were mapped - as this could have an effect on owner name and the member descriptor // of the target namespace (which acts as the source namespace in reversed mappings - it's confusing, I know). // The contents of the string array are the same as the tiny columns. That is index 1 for owner (in the source namespace), // index 2 for descriptor (in the source namespace), index 3 the name of the member in the source namespace // and index 4 the name of the member in the destination namespace. // Of course there is still index 0 but that one can be disregarded as it will be FIELD or METHOD. // The SimpleMappingLookup is required to lookup the non-reversed class names in order to be able // to swap the namespaces of the owner and descriptors. Note that this lookup instance // will not be filled in if there is no reversal to be done (this.reverse == false). SimpleMappingLookup classLookup = new SimpleMappingLookup(); List<@NotNull String[]> delayedMemberMappings = new ArrayList<>(); int lineNr = 0; try (BufferedReader br = Files.newBufferedReader(this.map, StandardCharsets.UTF_8)) { // the first line must specify the version of tiny and the namespace. // we are going to ignore the namespace as they just produce too much headache // - instead, we assume that the user (usually a developer) knows what they are // doing and give them this free ticket to exploit the system to the degree tolerable. // This is done as in galimulator modding (especially under gsl-starplane) there is // no real concept of namespaces, that is each mapping contributes to a broader // project, with the user being able to specify infinitely more as they please // - as long as they are compatible with each other of course. // So while "vanilla" obfuscated mappings could be attributed to official, // slIntermediary to intermediary and spStarmap to named, this distinction makes // little sense as the intermediary and named channels are technically speaking // incomplete. String header = br.readLine(); lineNr++; if (header == null || Utils.isBlank(header)) { br.close(); throw new IOException("No tiny header present (empty file?)."); } String[] headerTokens = header.split("\\s+"); if (headerTokens.length != 3) { br.close(); throw new IOException("The tiny header had " + headerTokens.length + " tokens, however it is expected to be exactly 3."); } if (!headerTokens[0].equals("v1")) { br.close(); throw new IOException("This method can only read tiny v1 maps."); } for (String line = br.readLine(); line != null; line = br.readLine()) { lineNr++; if (line.charAt(0) == '#') { // fast short-circuiting continue; } line = line.split("#", 2)[0]; if (line == null || Utils.isBlank(line)) { continue; } @NotNull String[] colums = line.split("\\s+"); String type = colums[0].toUpperCase(Locale.ROOT); if (type.equals("CLASS")) { // Format: CLASS originalName newName if (colums.length != 3) { throw new IOException("Line " + lineNr + " is of type CLASS, but only " + colums.length + " colums are present, even though it expects 3."); } if (this.reverse) { super.remapClass(colums[2], colums[1]); classLookup.remapClass(colums[1], colums[2]); } else { super.remapClass(colums[1], colums[2]); } } else if (type.equals("METHOD")) { if (colums.length != 5) { throw new IOException("Line " + lineNr + " is of type CLASS, but only " + colums.length + " colums are present, even though it expects 5."); } // The official tinyV1 mappings format that actually makes sense // Format: METHOD owner descriptor originalName newName // 11111 2222222222 333333333333 4444444 delayedMemberMappings.add(colums); } else if (type.equals("FIELD")) { // Format: FIELD owner descriptor originalName newName // 11111 2222222222 333333333333 4444444 if (colums.length != 5) { throw new IOException("Line " + lineNr + " is of type CLASS, but only " + colums.length + " colums are present, even though it expects 5."); } delayedMemberMappings.add(colums); } } } StringBuilder descBuilder = new StringBuilder(); for (@NotNull String[] columns : delayedMemberMappings) { String srcOwner = columns[1]; String srcDesc = columns[2]; String srcName = columns[3]; String dstName = columns[4]; if (this.reverse) { boolean field = srcDesc.codePointAt(0) != '('; // Swap source and destination namespace descriptors and classes srcOwner = classLookup.getRemappedClassName(srcOwner); if (field) { srcDesc = Remapper.getRemappedFieldDescriptor(classLookup, srcDesc, descBuilder); } else { srcDesc = Remapper.getRemappedMethodDescriptor(classLookup, srcDesc, descBuilder); } // Swap source and destination namespace names String swapStore = srcName; srcName = dstName; dstName = swapStore; } super.remapMember(new MemberRef(srcOwner, srcName, srcDesc), dstName); } return this; } @Override public String toString() { return "SP Mapping Lookup [map=" + this.map.getFileName() + ",reversed=" + this.reverse + "]"; } }