diff --git a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java index 4257ec0..dbd4ba2 100644 --- a/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java +++ b/src/main/java/net/fabricmc/stitch/commands/tinyv2/CommandMergeTinyV2.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.regex.Pattern; @@ -39,11 +40,11 @@ import net.fabricmc.stitch.util.Pair; /** - * Merges a tiny file with 2 columns (namespaces) of mappings, with another tiny file that has - * the same namespace as the first column and a different namespace as the second column. - * The first column of the output will contain the shared namespace, - * the second column of the output would be the second namespace of input a, - * and the third column of the output would be the second namespace of input b + * Merges a tiny file with at least 2 columns (namespaces) of mappings, with another tiny + * file that has the same namespace as the first column and different namespaces as the + * other columns. The first column of the output will contain the shared namespace, the + * next columns of the output would be the namespaces of all inputs, excluding the first + * column namespace *

* Descriptors will remain as-is (using the namespace of the first column) *

@@ -65,12 +66,6 @@ * intermediary named official * c net/minecraft/class_123 net/minecraft/somePackage/someClass a * m (Lnet/minecraft/class_124;)V method_1234 someMethod a - *

- *

- * After intermediary-named mappings are obtained, - * and official-intermediary mappings are obtained and swapped using CommandReorderTinyV2, Loom merges them using this command, - * and then reorders it to official-intermediary-named using CommandReorderTinyV2 again. - * This is a convenient way of storing all the mappings in Loom. */ public class CommandMergeTinyV2 extends Command { public CommandMergeTinyV2() { @@ -82,62 +77,67 @@ public CommandMergeTinyV2() { */ @Override public String getHelpString() { - return " "; + return " [...] "; } @Override public boolean isArgumentCountValid(int count) { - return count == 3; + return count >= 3; } @Override public void run(String[] args) throws IOException { - Path inputA = Paths.get(args[0]); - Path inputB = Paths.get(args[1]); - System.out.println("Reading " + inputA); - TinyFile tinyFileA = TinyV2Reader.read(inputA); - System.out.println("Reading " + inputB); - TinyFile tinyFileB = TinyV2Reader.read(inputB); - TinyHeader headerA = tinyFileA.getHeader(); - TinyHeader headerB = tinyFileB.getHeader(); - if (headerA.getNamespaces().size() != 2) { - throw new IllegalArgumentException(inputA + " must have exactly 2 namespaces."); + Path[] inputs = new Path[args.length - 1]; + for (int i = 0; i < args.length - 1; ++i) { + inputs[i] = Paths.get(args[i]); } - if (headerB.getNamespaces().size() != 2) { - throw new IllegalArgumentException(inputB + " must have exactly 2 namespaces."); + Path output = Paths.get(args[args.length - 1]); + + List tinyFiles = new ArrayList<>(); + TinyFile tinyFileA = TinyV2Reader.read(inputs[0]); + tinyFiles.add(tinyFileA); + + TinyHeader headerA = tinyFileA.getHeader(); + if (headerA.getNamespaces().size() < 2) { + throw new IllegalArgumentException(inputs[0] + " must have at least 2 namespaces."); } - if (!headerA.getNamespaces().get(0).equals(headerB.getNamespaces().get(0))) { - throw new IllegalArgumentException( - String.format("The input tiny files must have the same namespaces as the first column. " + - "(%s has %s while %s has %s)", - inputA, headerA.getNamespaces().get(0), inputB, headerB.getNamespaces().get(0)) - ); + String baseNamespace = headerA.getNamespaces().get(0); + for (int i = 1; i < inputs.length; ++i) { + Path input = inputs[i]; + TinyFile tinyFile = TinyV2Reader.read(input); + tinyFiles.add(tinyFile); + TinyHeader header = tinyFile.getHeader(); + List namespaces = header.getNamespaces(); + + if (header.getNamespaces().size() < 2) { + throw new IllegalArgumentException(inputs[i] + " must have at least 2 namespaces."); + } + + if (!namespaces.get(0).equals(baseNamespace)) { + throw new IllegalArgumentException(String.format("The input tiny files must have the same namespaces as the first column. " + + "(%s has %s instead of %s)", input, namespaces.get(0), baseNamespace)); + } } - System.out.println("Merging " + inputA + " with " + inputB); - TinyFile mergedFile = merge(tinyFileA, tinyFileB); - TinyV2Writer.write(mergedFile, Paths.get(args[2])); - System.out.println("Merged mappings written to " + Paths.get(args[2])); - } + System.out.println("Merging " + inputs[0] + " with " + Arrays.stream(inputs).skip(1).map(Path::toString).collect(Collectors.joining(", "))); + TinyFile mergedFile = merge(tinyFiles); + TinyV2Writer.write(mergedFile, output); + System.out.println("Merged mappings written to " + output); + } - private TinyFile merge(TinyFile inputA, TinyFile inputB) { + private TinyFile merge(List inputs) { //TODO: how to merge properties? - TinyHeader mergedHeader = mergeHeaders(inputA.getHeader(), inputB.getHeader()); + TinyHeader mergedHeader = mergeHeaders(inputs.stream().map(TinyFile::getHeader).collect(Collectors.toList())); - List keyUnion = keyUnion(inputA.getClassEntries(), inputB.getClassEntries()); + List keyUnion = keyUnion(inputs.stream().map(TinyFile::getClassEntries).collect(Collectors.toList())); - Map inputAClasses = inputA.mapClassesByFirstNamespace(); - Map inputBClasses = inputB.mapClassesByFirstNamespace(); + List> inputsClasses = inputs.stream().map(TinyFile::mapClassesByFirstNamespace).collect(Collectors.toList()); List mergedClasses = map(keyUnion, key -> { - TinyClass classA = inputAClasses.get(key); - TinyClass classB = inputBClasses.get(key); - - classA = matchEnclosingClassIfNeeded(key, classA, inputAClasses); - classB = matchEnclosingClassIfNeeded(key, classB, inputBClasses); - return mergeClasses(key, classA, classB); + List classes = inputsClasses.stream().map(inputClasses -> matchEnclosingClassIfNeeded(key, inputClasses.get(key), inputClasses)).collect(Collectors.toList()); + return mergeClasses(key, classes); }); return new TinyFile(mergedHeader, mergedClasses); @@ -175,106 +175,109 @@ private String matchEnclosingClass(String sharedName, Map inp return sharedName; } + private TinyClass mergeClasses(String sharedClassName, List classes) { + List mergedNames = mergeNames(sharedClassName, classes); + List mergedComments = mergeComments(classes.stream().map(TinyClass::getComments).collect(Collectors.toList())); - private TinyClass mergeClasses(String sharedClassName, @Nonnull TinyClass classA, @Nonnull TinyClass classB) { - List mergedNames = mergeNames(sharedClassName, classA, classB); - List mergedComments = mergeComments(classA.getComments(), classB.getComments()); - - List> methodKeyUnion = union(mapToFirstNamespaceAndDescriptor(classA), mapToFirstNamespaceAndDescriptor(classB)); - Map, TinyMethod> methodsA = classA.mapMethodsByFirstNamespaceAndDescriptor(); - Map, TinyMethod> methodsB = classB.mapMethodsByFirstNamespaceAndDescriptor(); - List mergedMethods = map(methodKeyUnion, - (Pair k) -> mergeMethods(k.getLeft(), methodsA.get(k), methodsB.get(k))); + List> methodKeyUnion = union(classes.stream().map(clazz -> mapToFirstNamespaceAndDescriptor(clazz).collect(Collectors.toList())).collect(Collectors.toList())); + List, TinyMethod>> methods = classes.stream().map(TinyClass::mapMethodsByFirstNamespaceAndDescriptor).collect(Collectors.toList()); + List mergedMethods = map(methodKeyUnion, (Pair k) -> + mergeMethods(k.getLeft(), methods.stream().map(method -> method.get(k)).collect(Collectors.toList()))); - List fieldKeyUnion = keyUnion(classA.getFields(), classB.getFields()); - Map fieldsA = classA.mapFieldsByFirstNamespace(); - Map fieldsB = classB.mapFieldsByFirstNamespace(); - List mergedFields = map(fieldKeyUnion, k -> mergeFields(k, fieldsA.get(k), fieldsB.get(k))); + List fieldKeyUnion = keyUnion(classes.stream().map(TinyClass::getFields).collect(Collectors.toList())); + List> fields = classes.stream().map(TinyClass::mapFieldsByFirstNamespace).collect(Collectors.toList()); + List mergedFields = map(fieldKeyUnion, k -> mergeFields(k, fields.stream().map(map -> map.get(k)).collect(Collectors.toList()))); return new TinyClass(mergedNames, mergedMethods, mergedFields, mergedComments); } private static final TinyMethod EMPTY_METHOD = new TinyMethod(null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + private TinyMethod mergeMethods(String sharedMethodName, List methods) { + List mergedNames = mergeNames(sharedMethodName, methods); + methods.replaceAll(method -> method == null ? EMPTY_METHOD : method); + List mergedComments = mergeComments(methods.stream().map(TinyMethod::getComments).collect(Collectors.toList())); - private TinyMethod mergeMethods(String sharedMethodName, @Nullable TinyMethod methodA, @Nullable TinyMethod methodB) { - List mergedNames = mergeNames(sharedMethodName, methodA, methodB); - if (methodA == null) methodA = EMPTY_METHOD; - if (methodB == null) methodB = EMPTY_METHOD; - List mergedComments = mergeComments(methodA.getComments(), methodB.getComments()); - - String descriptor = methodA.getMethodDescriptorInFirstNamespace() != null ? methodA.getMethodDescriptorInFirstNamespace() - : methodB.getMethodDescriptorInFirstNamespace(); + String descriptor = methods.get(0).getMethodDescriptorInFirstNamespace() != null ? methods.get(0).getMethodDescriptorInFirstNamespace() + : methods.get(1).getMethodDescriptorInFirstNamespace(); if (descriptor == null) throw new RuntimeException("no descriptor for key " + sharedMethodName); - - //TODO: this won't work too well when the first namespace is named or there is more than one named namespace (hack) + // TODO: Fix parameters List mergedParameters = new ArrayList<>(); - addParameters(methodA, mergedParameters, 2); - addParameters(methodB, mergedParameters, 1); + addParameters(methods, mergedParameters); List mergedLocalVariables = new ArrayList<>(); - addLocalVariables(methodA, mergedLocalVariables, 2); - addLocalVariables(methodB, mergedLocalVariables, 1); + addLocalVariables(methods, mergedLocalVariables); return new TinyMethod(descriptor, mergedNames, mergedParameters, mergedLocalVariables, mergedComments); } - private void addParameters(TinyMethod method, List addTo, int emptySpacePos) { - for (TinyMethodParameter localVariable : method.getParameters()) { - List names = new ArrayList<>(localVariable.getParameterNames()); - names.add(emptySpacePos, ""); - addTo.add(new TinyMethodParameter(localVariable.getLvIndex(), names, localVariable.getComments())); + private void addParameters(List methods, List addTo) { + for (TinyMethod method : methods) { + for (TinyMethodParameter localVariable : method.getParameters()) { + List names = new ArrayList<>(localVariable.getParameterNames()); + addTo.add(new TinyMethodParameter(localVariable.getLvIndex(), names, localVariable.getComments())); + } } } - private void addLocalVariables(TinyMethod method, List addTo, int emptySpacePos) { - for (TinyLocalVariable localVariable : method.getLocalVariables()) { - List names = new ArrayList<>(localVariable.getLocalVariableNames()); - names.add(emptySpacePos, ""); - addTo.add(new TinyLocalVariable(localVariable.getLvIndex(), localVariable.getLvStartOffset(), - localVariable.getLvTableIndex(), names, localVariable.getComments())); + private void addLocalVariables(List methods, List addTo) { + for (TinyMethod method : methods) { + for (TinyLocalVariable localVariable : method.getLocalVariables()) { + List names = new ArrayList<>(localVariable.getLocalVariableNames()); + addTo.add(new TinyLocalVariable(localVariable.getLvIndex(), localVariable.getLvStartOffset(), + localVariable.getLvTableIndex(), names, localVariable.getComments())); + } } } + private TinyField mergeFields(String sharedFieldName, List fields) { + List mergedNames = mergeNames(sharedFieldName, fields); + List mergedComments = mergeComments(fields.stream().map(field -> field != null ? field.getComments() : Collections.emptyList()).collect(Collectors.toList())); - private TinyField mergeFields(String sharedFieldName, @Nullable TinyField fieldA, @Nullable TinyField fieldB) { - List mergedNames = mergeNames(sharedFieldName, fieldA, fieldB); - List mergedComments = mergeComments(fieldA != null ? fieldA.getComments() : Collections.emptyList(), - fieldB != null ? fieldB.getComments() : Collections.emptyList()); - - String descriptor = fieldA != null ? fieldA.getFieldDescriptorInFirstNamespace() - : fieldB != null ? fieldB.getFieldDescriptorInFirstNamespace() : null; + String descriptor = fields.stream().filter(Objects::nonNull).findFirst().map(TinyField::getFieldDescriptorInFirstNamespace).orElse(null); if (descriptor == null) throw new RuntimeException("no descriptor for key " + sharedFieldName); return new TinyField(descriptor, mergedNames, mergedComments); } - private TinyHeader mergeHeaders(TinyHeader headerA, TinyHeader headerB) { + private TinyHeader mergeHeaders(List headers) { + TinyHeader headerA = headers.get(0); List namespaces = new ArrayList<>(headerA.getNamespaces()); - namespaces.add(headerB.getNamespaces().get(1)); + for (int i = 1; i < headers.size(); ++i) { + for (String namespace : headers.get(i).getNamespaces()) { + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } + } // TODO: how should versions and properties be merged? return new TinyHeader(namespaces, headerA.getMajorVersion(), headerA.getMinorVersion(), headerA.getProperties()); } - private List mergeComments(Collection commentsA, Collection commentsB) { - return union(commentsA, commentsB); + private List mergeComments(List> comments) { + return union(comments); } - private List keyUnion(Collection mappingsA, Collection mappingB) { - return union(mappingsA.stream().map(m -> m.getMapping().get(0)), mappingB.stream().map(m -> m.getMapping().get(0))); + private List keyUnion(List> mappings) { + return union(mappings.stream().map(c -> c.stream().map(m -> m.getMapping().get(0)).collect(Collectors.toList())).collect(Collectors.toList())); } private Stream> mapToFirstNamespaceAndDescriptor(TinyClass tinyClass) { return tinyClass.getMethods().stream().map(m -> Pair.of(m.getMapping().get(0), m.getMethodDescriptorInFirstNamespace())); } - - private List mergeNames(String key, @Nullable Mapping mappingA, @Nullable Mapping mappingB) { + private List mergeNames(String key, List mappings) { List merged = new ArrayList<>(); merged.add(key); - merged.add(mappingExists(mappingA) ? mappingA.getMapping().get(1) : key); - merged.add(mappingExists(mappingB) ? mappingB.getMapping().get(1) : key); + mappings.forEach(mapping -> { + if (mapping != null) { + for (int i = 1; i < mapping.getMapping().size(); ++i) { + String m = mapping.getMapping().get(i); + merged.add(!m.isEmpty() ? m : key); + } + } + }); return merged; } @@ -283,8 +286,12 @@ private boolean mappingExists(@Nullable Mapping mapping) { return mapping != null && !mapping.getMapping().get(1).isEmpty(); } - private List union(Stream list1, Stream list2) { - return union(list1.collect(Collectors.toList()), list2.collect(Collectors.toList())); + private List union(List> lists) { + Set set = new HashSet<>(); + + lists.forEach(set::addAll); + + return new ArrayList<>(set); } private List union(Collection list1, Collection list2) { @@ -303,5 +310,4 @@ private static String escape(String str) { private List map(List from, Function mapper) { return from.stream().map(mapper).collect(Collectors.toList()); } - } diff --git a/src/test/java/net/fabricmc/stitch/tinyv2/MergeTest.java b/src/test/java/net/fabricmc/stitch/tinyv2/MergeTest.java new file mode 100644 index 0000000..da7a1a5 --- /dev/null +++ b/src/test/java/net/fabricmc/stitch/tinyv2/MergeTest.java @@ -0,0 +1,29 @@ +package net.fabricmc.stitch.tinyv2; + +import net.fabricmc.stitch.commands.tinyv2.CommandMergeTinyV2; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +public class MergeTest { + private static final Path DIR = new File(MergeTest.class.getClassLoader().getResource("merge").getPath()).toPath().toAbsolutePath(); + + @Test + public void test() throws Exception { + Path expectedOutput = DIR.resolve("expected.tiny"); + Path output = Files.createTempFile("stitch-merge-result-", ".tiny"); + Path inputA = DIR.resolve("input-a.tiny"); + Path inputB = DIR.resolve("input-b.tiny"); + Path inputC = DIR.resolve("input-c.tiny"); + new CommandMergeTinyV2().run(new String[]{inputA.toString(), inputB.toString(), inputC.toString(), output.toString()}); + + String expectedOutputContent = new String(Files.readAllBytes(expectedOutput), StandardCharsets.UTF_8).replace("\r\n", "\n"); + String outputContent = new String(Files.readAllBytes(output), StandardCharsets.UTF_8).replace("\r\n", "\n"); + assertEquals(expectedOutputContent, outputContent); + } +} diff --git a/src/test/resources/merge/expected.tiny b/src/test/resources/merge/expected.tiny new file mode 100644 index 0000000..9fc307c --- /dev/null +++ b/src/test/resources/merge/expected.tiny @@ -0,0 +1,16 @@ +tiny 2 0 official hashed mojmap intermediary named +c a net/minecraft/unmapped/C_lctoxfsg com/mojang/math/Constants net/minecraft/class_5973 net/minecraft/util/math/MathConstants + f F a f_ygxirswm PI field_29658 PI + f F b f_ncoyhkkx RAD_TO_DEG field_29659 DEGREES_PER_RADIAN + f F c f_vaqzaaae DEG_TO_RAD field_29660 RADIANS_PER_DEGREE + f F d f_jhuobxwz EPSILON field_29661 EPSILON +c c net/minecraft/unmapped/C_ffukturc com/mojang/math/Matrix3f net/minecraft/class_4581 net/minecraft/util/math/Matrix3f + m (Lc;)V + p 1 source + m (Ld;)V + p 1 matrix + m (Lg;)V + p 1 quaternion + m ()V a m_jdnetyjq transpose method_22847 transpose + m (F)V a m_asnpwjdg mul method_23274 multiply + p 1 scalar diff --git a/src/test/resources/merge/input-a.tiny b/src/test/resources/merge/input-a.tiny new file mode 100644 index 0000000..33609a1 --- /dev/null +++ b/src/test/resources/merge/input-a.tiny @@ -0,0 +1,12 @@ +tiny 2 0 official hashed mojmap +c a net/minecraft/unmapped/C_lctoxfsg com/mojang/math/Constants + f F a f_ygxirswm PI + f F b f_ncoyhkkx RAD_TO_DEG + f F c f_vaqzaaae DEG_TO_RAD + f F d f_jhuobxwz EPSILON +c c net/minecraft/unmapped/C_ffukturc com/mojang/math/Matrix3f + m (Lc;)V + m (Ld;)V + m (Lg;)V + m ()V a m_jdnetyjq transpose + m (F)V a m_asnpwjdg mul diff --git a/src/test/resources/merge/input-b.tiny b/src/test/resources/merge/input-b.tiny new file mode 100644 index 0000000..8443aaa --- /dev/null +++ b/src/test/resources/merge/input-b.tiny @@ -0,0 +1,12 @@ +tiny 2 0 official intermediary +c a net/minecraft/class_5973 + f F a field_29658 + f F b field_29659 + f F c field_29660 + f F d field_29661 +c c net/minecraft/class_4581 + m (Lc;)V + m (Ld;)V + m (Lg;)V + m ()V a method_22847 + m (F)V a method_23274 diff --git a/src/test/resources/merge/input-c.tiny b/src/test/resources/merge/input-c.tiny new file mode 100644 index 0000000..6da8528 --- /dev/null +++ b/src/test/resources/merge/input-c.tiny @@ -0,0 +1,16 @@ +tiny 2 0 official named +c a net/minecraft/util/math/MathConstants + f F a PI + f F b DEGREES_PER_RADIAN + f F c RADIANS_PER_DEGREE + f F d EPSILON +c c net/minecraft/util/math/Matrix3f + m (Lc;)V + p 1 source + m (Ld;)V + p 1 matrix + m (Lg;)V + p 1 quaternion + m ()V a transpose + m (F)V a multiply + p 1 scalar \ No newline at end of file