diff --git a/enigma-swing/build.gradle b/enigma-swing/build.gradle index 62f4a2441..db4b11a65 100644 --- a/enigma-swing/build.gradle +++ b/enigma-swing/build.gradle @@ -48,7 +48,7 @@ def registerTestTask(String name) { def jar = project(":enigma").file("build/test-obf/${name}.jar") def mappings = file("mappings/${name}") - args('-jar', jar, '-mappings', mappings) + args('-jar', jar, '-mappings', mappings, '--development') doFirst { mappings.mkdirs() } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java index 3bd974911..ba883a64b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java @@ -3,6 +3,7 @@ import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.EnigmaProfile; import org.quiltmc.enigma.api.analysis.EntryReference; +import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.gui.config.Config; @@ -457,20 +458,24 @@ public void toggleMapping(EditorPanel editor) { public void toggleMappingFromEntry(Entry obfEntry) { EntryMapping mapping = this.controller.getProject().getRemapper().getMapping(obfEntry); + EntryChange change = EntryChange.modify(obfEntry); if (mapping.targetName() != null) { - EntryRemapper remapper = this.controller.getProject().getRemapper(); - EntryChange> change; - EntryMapping proposedMapping = remapper.getProposedMappings().get(obfEntry); - if (proposedMapping != null) { - change = EntryChange.modify(obfEntry).withDeobfName(proposedMapping.targetName()).withTokenType(proposedMapping.tokenType()).withSourcePluginId(proposedMapping.sourcePluginId()); + if (mapping.tokenType().isProposed()) { + change = change.withTokenType(TokenType.DEOBFUSCATED).clearSourcePluginId(); } else { - change = EntryChange.modify(obfEntry).clearDeobfName(); + EntryRemapper remapper = this.controller.getProject().getRemapper(); + EntryMapping proposedMapping = remapper.getProposedMappings().get(obfEntry); + if (proposedMapping != null) { + change = change.withDeobfName(proposedMapping.targetName()).withTokenType(proposedMapping.tokenType()).withSourcePluginId(proposedMapping.sourcePluginId()); + } else { + change = change.clearDeobfName(); + } } - - this.controller.applyChange(new ValidationContext(this.getNotificationManager()), change); } else { - this.controller.applyChange(new ValidationContext(this.getNotificationManager()), EntryChange.modify(obfEntry).withDeobfName(obfEntry.getName())); + change = change.withDeobfName(obfEntry.getName()); } + + this.controller.applyChange(new ValidationContext(this.getNotificationManager()), change); } public void showDiscardDiag(IntFunction callback, String... options) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java index 87b73d66e..f5505cb4e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java @@ -535,7 +535,7 @@ private void applyChange0(ValidationContext vc, EntryChange change, boolean u this.gui.getActiveEditor().onRename(target); } - if (!Objects.equals(prev.targetName(), mapping.targetName())) { + if (!Objects.equals(prev.targetName(), mapping.targetName()) || !Objects.equals(prev.tokenType(), mapping.tokenType())) { this.chp.invalidateMapped(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Main.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Main.java index bba10288c..24cdb5b01 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Main.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Main.java @@ -53,7 +53,7 @@ public static void main(String[] args) throws IOException { parser.acceptsAll(List.of("edit-javadocs", "d"), "Enable editing Javadocs"); parser.acceptsAll(List.of("no-edit-javadocs", "D"), "Disable editing Javadocs"); - parser.accepts("single-class-tree", "Unify the deobfuscated and obfuscated class panels"); + parser.accepts("development", "Enable extra options and information for development"); parser.accepts("help", "Displays help information"); @@ -65,6 +65,10 @@ public static void main(String[] args) throws IOException { return; } + if (options.has("development")) { + System.setProperty("enigma.development", "true"); + } + Set editables = EnumSet.allOf(EditableType.class); for (OptionSpec spec : options.specs()) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java index 355bb6b23..b4fed2697 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.element; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.NotificationManager; @@ -20,7 +21,7 @@ import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.LanguageUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import org.quiltmc.enigma.util.EntryTreePrinter; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.Pair; import org.quiltmc.enigma.util.validation.Message; @@ -28,14 +29,23 @@ import javax.annotation.Nullable; import javax.swing.ButtonGroup; +import javax.swing.JButton; import javax.swing.JFileChooser; +import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JRadioButtonMenuItem; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.WindowConstants; +import java.awt.BorderLayout; +import java.awt.Font; import java.io.File; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -91,6 +101,10 @@ public class MenuBar { private final JMenuItem aboutItem = new JMenuItem(); private final JMenuItem githubItem = new JMenuItem(); + // Enabled with system property "enigma.development" or "--development" flag + private final JMenu devMenu = new JMenu(); + private final JMenuItem printMappingTreeItem = new JMenuItem(); + private final Gui gui; public MenuBar(Gui gui) { @@ -160,6 +174,11 @@ public MenuBar(Gui gui) { this.helpMenu.add(this.githubItem); ui.add(this.helpMenu); + this.devMenu.add(this.printMappingTreeItem); + if (System.getProperty("enigma.development", "false").equalsIgnoreCase("true")) { + ui.add(this.devMenu); + } + this.setKeyBinds(); this.jarOpenItem.addActionListener(e -> this.onOpenJarClicked()); @@ -188,6 +207,7 @@ public MenuBar(Gui gui) { this.startServerItem.addActionListener(e -> this.onStartServerClicked()); this.aboutItem.addActionListener(e -> AboutDialog.show(this.gui.getFrame())); this.githubItem.addActionListener(e -> this.onGithubClicked()); + this.printMappingTreeItem.addActionListener(e -> this.onPrintMappingTreeClicked()); } public void setKeyBinds() { @@ -222,6 +242,7 @@ public void updateUiState() { this.exportSourceItem.setEnabled(jarOpen); this.exportJarItem.setEnabled(jarOpen); this.statsItem.setEnabled(jarOpen); + this.printMappingTreeItem.setEnabled(jarOpen); } public void retranslateUi() { @@ -269,6 +290,9 @@ public void retranslateUi() { this.helpMenu.setText(I18n.translate("menu.help")); this.aboutItem.setText(I18n.translate("menu.help.about")); this.githubItem.setText(I18n.translate("menu.help.github")); + + this.devMenu.setText("Dev"); + this.printMappingTreeItem.setText("Print mapping tree"); } private void onOpenJarClicked() { @@ -455,6 +479,29 @@ private void onGithubClicked() { GuiUtil.openUrl("https://github.com/QuiltMC/Enigma"); } + private void onPrintMappingTreeClicked() { + var mappings = this.gui.getController().getProject().getRemapper().getMappings(); + + StringWriter text = new StringWriter(); + EntryTreePrinter.print(new PrintWriter(text), mappings); + + var frame = new JFrame("Mapping Tree"); + var pane = frame.getContentPane(); + pane.setLayout(new BorderLayout()); + + var textArea = new JTextArea(text.toString()); + textArea.setFont(ScaleUtil.getFont(Font.MONOSPACED, Font.PLAIN, 12)); + pane.add(new JScrollPane(textArea), BorderLayout.CENTER); + var button = new JButton("Close"); + button.addActionListener(e -> frame.dispose()); + pane.add(button, BorderLayout.SOUTH); + + frame.setSize(ScaleUtil.getDimension(1200, 400)); + frame.setLocationRelativeTo(this.gui.getFrame()); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.setVisible(true); + } + private void onOpenMappingsClicked() { this.gui.enigmaMappingsFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); if (this.gui.enigmaMappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java index 98310825b..898f68994 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java @@ -164,7 +164,7 @@ public EntryRemapper getRemapper() { } public void dropMappings(ProgressListener progress) { - DeltaTrackingTree mappings = this.remapper.getDeobfMappings(); + DeltaTrackingTree mappings = this.remapper.getMappings(); Collection> dropped = this.dropMappings(mappings, progress); for (Entry entry : dropped) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java index fd11760d6..108541098 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java @@ -77,7 +77,13 @@ public boolean hasEntry(Entry entry) { } else if (entry instanceof FieldEntry fieldEntry) { return this.hasField(fieldEntry); } else if (entry instanceof LocalVariableEntry localVariableEntry) { - return this.hasMethod(localVariableEntry.getParent()); + MethodEntry parent = localVariableEntry.getParent(); + if (this.hasMethod(parent)) { + // TODO: Check using max_locals from the Code attribute (JVMS§4.7.3) + AccessFlags parentAccess = this.getMethodAccess(parent); + int startIndex = parentAccess != null && parentAccess.isStatic() ? 0 : 1; + return localVariableEntry.getIndex() >= startIndex; + } } return false; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java index d921eca22..a400d68a9 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java @@ -10,8 +10,8 @@ import org.quiltmc.enigma.api.translation.Translator; import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; -import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeUtil; import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; +import org.quiltmc.enigma.api.translation.mapping.tree.MergedEntryMappingTree; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; @@ -27,9 +27,9 @@ import javax.annotation.Nullable; public class EntryRemapper { - private final DeltaTrackingTree deobfNames; - private final DeltaTrackingTree proposedNames; - private final DeltaTrackingTree mergedNames; + private final EntryTree deobfMappings; + private final EntryTree proposedMappings; + private final DeltaTrackingTree mappings; private final EntryResolver obfResolver; private final Translator deobfuscator; @@ -40,13 +40,13 @@ public class EntryRemapper { private final List proposalServices; private EntryRemapper(JarIndex jarIndex, MappingsIndex mappingsIndex, EntryTree proposedMappings, EntryTree deobfMappings, List proposalServices) { - this.deobfNames = new DeltaTrackingTree<>(deobfMappings); - this.proposedNames = new DeltaTrackingTree<>(proposedMappings); - this.mergedNames = new DeltaTrackingTree<>(EntryTreeUtil.merge(proposedMappings, deobfMappings)); + this.deobfMappings = deobfMappings; + this.proposedMappings = proposedMappings; + this.mappings = new DeltaTrackingTree<>(new MergedEntryMappingTree(deobfMappings, proposedMappings)); this.obfResolver = jarIndex.getEntryResolver(); - this.deobfuscator = new MappingTranslator(this.mergedNames, this.obfResolver); + this.deobfuscator = new MappingTranslator(this.mappings, this.obfResolver); this.jarIndex = jarIndex; this.mappingsIndex = mappingsIndex; @@ -93,9 +93,9 @@ private void doPutMapping(ValidationContext vc, Entry obfuscatedEntry, @Nonnu for (Entry resolvedEntry : resolvedEntries) { if (deobfMapping.equals(EntryMapping.DEFAULT)) { - this.insertName(resolvedEntry, null); + this.mappings.insert(resolvedEntry, null); } else { - this.insertName(resolvedEntry, deobfMapping); + this.mappings.insert(resolvedEntry, deobfMapping); } } @@ -137,17 +137,6 @@ private void mapRecordComponentGetter(ValidationContext vc, ClassEntry classEntr this.doPutMapping(vc, methodEntry, new EntryMapping(fieldMapping.targetName()), false); } - private void insertName(Entry entry, @Nullable EntryMapping mapping) { - this.mergedNames.insert(entry, mapping); - if (mapping != null) { - if (mapping.tokenType().isProposed()) { - this.proposedNames.insert(entry, mapping); - } else { - this.deobfNames.insert(entry, mapping); - } - } - } - /** * Runs {@link NameProposalService#getDynamicProposedNames(EntryRemapper, Entry, EntryMapping, EntryMapping)} over the names stored in this remapper, * inserting all mappings generated. @@ -156,14 +145,14 @@ public void insertDynamicallyProposedMappings(@Nullable Entry obfEntry, @Null for (var service : this.proposalServices) { var proposedNames = service.getDynamicProposedNames(this, obfEntry, oldMapping, newMapping); if (proposedNames != null) { - proposedNames.forEach(this::insertName); + proposedNames.forEach(this.proposedMappings::insert); } } } @Nonnull public EntryMapping getMapping(Entry entry) { - EntryMapping entryMapping = this.mergedNames.get(entry); + EntryMapping entryMapping = this.mappings.get(entry); return entryMapping == null ? EntryMapping.DEFAULT : entryMapping; } @@ -180,11 +169,11 @@ public Translator getDeobfuscator() { } public Stream> getObfEntries() { - return this.mergedNames.getAllEntries(); + return this.mappings.getAllEntries(); } public Collection> getObfChildren(Entry obfuscatedEntry) { - return this.mergedNames.getChildren(obfuscatedEntry); + return this.mappings.getChildren(obfuscatedEntry); } /** @@ -192,31 +181,31 @@ public Collection> getObfChildren(Entry obfuscatedEntry) { * @return the merged mapping tree */ public DeltaTrackingTree getMappings() { - return this.mergedNames; + return this.mappings; } /** * Gets all manually inserted mappings. * @return the deobfuscated mapping tree */ - public DeltaTrackingTree getDeobfMappings() { - return this.deobfNames; + public EntryTree getDeobfMappings() { + return this.deobfMappings; } /** * Gets all proposed mappings. * @return the proposed mapping tree */ - public DeltaTrackingTree getProposedMappings() { - return this.proposedNames; + public EntryTree getProposedMappings() { + return this.proposedMappings; } public MappingDelta takeMappingDelta() { - return this.mergedNames.takeDelta(); + return this.mappings.takeDelta(); } public boolean isDirty() { - return this.mergedNames.isDirty(); + return this.mappings.isDirty(); } public EntryResolver getObfResolver() { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/IndexEntryResolver.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/IndexEntryResolver.java index 3c5f2cea8..36ac4ba43 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/IndexEntryResolver.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/IndexEntryResolver.java @@ -64,6 +64,12 @@ public > Collection resolveEntry(E entry, ResolutionStrate return Collections.singleton(entry); } + /** + * Get a direct child of any class that is an ancestor of the given entry. + * + * @param entry the descendant of a class + * @return the direct child of a class, which is an ancestor of the given entry or the entry itself + */ @Nullable private Entry getClassChild(Entry entry) { if (entry instanceof ClassEntry) { @@ -87,6 +93,7 @@ private Entry getClassChild(Entry entry) { private Set> resolveChildEntry(Entry entry, ResolutionStrategy strategy) { ClassEntry ownerClass = entry.getParent(); + // Resolve specialized methods using their bridges if (entry instanceof MethodEntry methodEntry) { MethodEntry bridgeMethod = this.bridgeMethodIndex.getBridgeFromSpecialized(methodEntry); if (bridgeMethod != null && ownerClass.equals(bridgeMethod.getParent())) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/DeltaTrackingTree.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/DeltaTrackingTree.java index c2f969de4..69eb2bf12 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/DeltaTrackingTree.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/DeltaTrackingTree.java @@ -107,4 +107,9 @@ private void resetDelta() { public boolean isDirty() { return !this.changes.isEmpty(); } + + @Override + public String toString() { + return "DeltaTrackingTree[" + this.delegate + "]"; + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java new file mode 100644 index 000000000..78e1d66b7 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java @@ -0,0 +1,135 @@ +package org.quiltmc.enigma.api.translation.mapping.tree; + +import org.quiltmc.enigma.api.translation.Translator; +import org.quiltmc.enigma.api.translation.mapping.EntryMap; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryResolver; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * An {@link EntryMapping entry mapping} {@link EntryTree tree} that represents both a main and a secondary tree. + * The secondary tree is used to provide entries that aren't present in the main one. Both trees are used by reference, + * which means changes made directly to either of them, will also be reflected in the merged tree. + * + *

+ * Writing to the merged tree will only apply the changes to the main tree, while reading from it will include values + * from both trees. + */ +public record MergedEntryMappingTree(EntryTree mainTree, EntryTree secondaryTree) implements EntryTree { + @Override + public void insert(Entry entry, EntryMapping value) { + this.mainTree.insert(entry, value); + } + + @Nullable + @Override + public EntryMapping remove(Entry entry) { + return this.mainTree.remove(entry); + } + + @Nullable + @Override + public EntryMapping get(Entry entry) { + EntryMapping main = this.mainTree.get(entry); + if (main == null) { + return this.secondaryTree.get(entry); + } + + return main; + } + + @Override + public boolean contains(Entry entry) { + return this.mainTree.contains(entry) || this.secondaryTree.contains(entry); + } + + @Override + public Collection> getChildren(Entry entry) { + var leaf = this.findNode(entry); + if (leaf == null) { + return Collections.emptyList(); + } + + return leaf.getChildren(); + } + + @Override + public Collection> getSiblings(Entry entry) { + Entry parent = entry.getParent(); + if (parent == null) { + return getSiblings(entry, this.getRootNodes().map(EntryTreeNode::getEntry).collect(Collectors.toSet())); + } + + return getSiblings(entry, this.getChildren(parent)); + } + + private static Collection> getSiblings(Entry entry, Collection> generation) { + var siblings = new HashSet<>(generation); + siblings.remove(entry); + return siblings; + } + + @Override + public EntryTreeNode findNode(Entry entry) { + var main = this.mainTree.findNode(entry); + var secondary = this.secondaryTree.findNode(entry); + + if (main != null && secondary != null) { + return new MergedMappingTreeNode(main, secondary); + } else if (main == null) { + return secondary; + } + + return main; + } + + static Stream> mergeNodeStreams(Stream> mainNodes, Stream> secondaryNodes) { + Map, EntryTreeNode> nodes = new HashMap<>(); + + mainNodes.forEach(mainNode -> nodes.put(mainNode.getEntry(), mainNode)); + secondaryNodes.forEach(secondaryNode -> nodes.merge(secondaryNode.getEntry(), secondaryNode, MergedMappingTreeNode::new)); + + return nodes.values().stream(); + } + + @Override + public Stream> getRootNodes() { + return mergeNodeStreams(this.mainTree.getRootNodes(), this.secondaryTree.getRootNodes()); + } + + @Nonnull + @Override + public Iterator> iterator() { + return this.getRootNodes().flatMap(n -> n.getNodesRecursively().stream()) + .map(n -> (EntryTreeNode) n) // ? extends EntryTreeNode -> EntryTreeNode + .iterator(); + } + + @Override + public Stream> getAllEntries() { + return this.getRootNodes().flatMap(n -> n.getChildrenRecursively().stream()); + } + + @Override + public boolean isEmpty() { + return this.mainTree.isEmpty() && this.secondaryTree.isEmpty(); + } + + @Override + public MergedEntryMappingTree translate(Translator translator, EntryResolver resolver, EntryMap mappings) { + var main = this.mainTree.translate(translator, resolver, mappings); + var secondary = this.secondaryTree.translate(translator, resolver, mappings); + return new MergedEntryMappingTree(main, secondary); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedMappingTreeNode.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedMappingTreeNode.java new file mode 100644 index 000000000..0d8a9531a --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedMappingTreeNode.java @@ -0,0 +1,68 @@ +package org.quiltmc.enigma.api.translation.mapping.tree; + +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +/** + * A node of a {@link MergedEntryMappingTree}. Check the documentation of said class for details. + */ +public record MergedMappingTreeNode(EntryTreeNode mainNode, EntryTreeNode secondaryNode) implements EntryTreeNode { + public MergedMappingTreeNode { + if (!isMatchingNode(mainNode, secondaryNode)) { + throw new IllegalArgumentException("The main and secondary nodes don't represent the same entry"); + } + } + + public static boolean isMatchingNode(EntryTreeNode mainNode, EntryTreeNode secondaryNode) { + return mainNode.getEntry().equals(secondaryNode.getEntry()); + } + + @Nullable + @Override + public EntryMapping getValue() { + var main = this.mainNode.getValue(); + if (main == null) { + return this.secondaryNode.getValue(); + } + + return main; + } + + @Override + public Entry getEntry() { + return this.mainNode.getEntry(); + } + + @Override + public boolean isEmpty() { + return this.mainNode.isEmpty() && this.secondaryNode.isEmpty(); + } + + @Override + public Collection> getChildren() { + var children = new HashSet<>(this.mainNode.getChildren()); + children.addAll(this.secondaryNode.getChildren()); + return children; + } + + @Override + public Collection> getChildNodes() { + Map, EntryTreeNode> nodes = new HashMap<>(); + + for (EntryTreeNode mainNode : this.mainNode.getChildNodes()) { + nodes.put(mainNode.getEntry(), mainNode); + } + + for (EntryTreeNode secondaryNode : this.secondaryNode.getChildNodes()) { + nodes.merge(secondaryNode.getEntry(), secondaryNode, MergedMappingTreeNode::new); + } + + return nodes.values(); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java index 923caa6a4..171707e0d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java @@ -109,23 +109,23 @@ default String getSourceRemapName() { *

 	 * {@code
 	 * public class D {
-	 * 	 public int name;
-	 * 	 public String a;
+	 * 	public int name;
+	 * 	public String a;
 	 *
-	 *  	public void b() {
-	 *  	}
+	 * 	public void b() {
+	 * 	}
 	 *
-	 * 	 public class D.E {
+	 * 	public class D.E {
 	 * 		public int name;
 	 *
 	 * 		public void b() {
-	 *         }
-	 *  	}
-	 *  }
-	 *  }
-	 *  
- * In this example, {@code D.E.name} shadows {@code D.name} and {@code D.E.b} shadows {@code D.b}. - * This means that calling either one from {@code D.E} will use the shadowed entry instead of the original. + * } + * } + * } + * } + * + * In this example, {@code D.E.name} shadows {@code D.name} and {@code D.E.b} shadows {@code D.b}. + * This means that calling either one from {@code D.E} will use the shadowed entry instead of the original. */ boolean canShadow(Entry entry); @@ -158,6 +158,15 @@ default ClassEntry getTopLevelClass() { return Objects.requireNonNull(last, () -> String.format("%s has no top level class?", this)); } + /** + * Get the complete ancestry list of this entry, including itself. + * Searches recursively: an entry is considered an ancestor if it's the parent of the current entry or any of its parents. + * The ancestry of any ancestor is guaranteed to be a subset of this entry's one. + * + * @return the ancestry list, from outermost to innermost, with this entry as the last one + * @see #findAncestor(Class) + * @see #replaceAncestor(Entry, Entry) + */ default List> getAncestry() { P parent = this.getParent(); List> entries = new ArrayList<>(); @@ -169,6 +178,15 @@ default List> getAncestry() { return entries; } + /** + * Find the closest ancestor of the given entry type. + * + * @param the entry type to search for + * @param type the class of the entry type to search for + * @return the closest ancestor entry of the given type + * @see #getAncestry() + * @see #replaceAncestor(Entry, Entry) + */ @Nullable @SuppressWarnings("unchecked") default > E findAncestor(Class type) { @@ -183,6 +201,18 @@ default > E findAncestor(Class type) { return null; } + /** + * Replaces an entry in the ancestry of this entry. + * If the target entry is this one, returns the replacement instead. + * If the target and replacement entries are the same, returns this entry without changes. + * + * @param the type of the entry to replace + * @param target the entry to replace + * @param replacement the replacement entry + * @return an entry with the same ancestry as this one, with the replacement applied + * @see #getAncestry() + * @see #findAncestor(Class) + */ @SuppressWarnings("unchecked") default > Entry

replaceAncestor(E target, E replacement) { if (replacement.equals(target)) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java index 015ff00b4..bc9adb7fc 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java @@ -16,6 +16,7 @@ import javax.annotation.Nonnull; public class MethodEntry extends ParentedEntry implements Comparable { + @Nonnull protected final MethodDescriptor descriptor; public MethodEntry(ClassEntry parent, String name, MethodDescriptor descriptor) { @@ -49,29 +50,45 @@ public boolean isConstructor() { } /** - * Creates an iterator of all parameters in this method. Unmapped args will have no name, and javadoc is ignored. + * Get a list of all parameters in this method. All parameters will have an empty name. + * * @param index the entry index - * @param deobfuscator a translator - * @return an iterator of this method's parameters + * @return a list of this method's parameters */ - public Iterator getParameterIterator(EntryIndex index, Translator deobfuscator) { + public List getParameters(EntryIndex index) { List parameters = new ArrayList<>(); - - MethodDescriptor desc = this.getDesc(); AccessFlags flags = index.getMethodAccess(this); - if (desc != null && flags != null) { - int argIndex = flags.isStatic() ? 0 : 1; + if (flags != null) { + int i = flags.isStatic() ? 0 : 1; - for (ArgumentDescriptor arg : desc.getArgumentDescs()) { - LocalVariableEntry argEntry = new LocalVariableEntry(this, argIndex, "", true, null); - LocalVariableEntry translatedArgEntry = deobfuscator.translate(argEntry); + for (ArgumentDescriptor arg : this.descriptor.getArgumentDescs()) { + LocalVariableEntry argEntry = new LocalVariableEntry(this, i, "", true, null); + parameters.add(argEntry); - parameters.add(translatedArgEntry == null ? argEntry : translatedArgEntry); - argIndex += arg.getSize(); + i += arg.getSize(); } } + return parameters; + } + + /** + * Creates an iterator of all parameters in this method, also doing translation. Unmapped args will have an empty name, and javadoc is ignored. + * + * @param index the entry index + * @param deobfuscator a translator + * @return an iterator of this method's translated parameters + */ + public Iterator getParameterIterator(EntryIndex index, Translator deobfuscator) { + List parameters = new ArrayList<>(); + + for (LocalVariableEntry arg : this.getParameters(index)) { + LocalVariableEntry translatedArg = deobfuscator.translate(arg); + + parameters.add(translatedArg == null ? arg : translatedArg); + } + return parameters.iterator(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/EntryTreePrinter.java b/enigma/src/main/java/org/quiltmc/enigma/util/EntryTreePrinter.java new file mode 100644 index 000000000..de6477264 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/EntryTreePrinter.java @@ -0,0 +1,54 @@ +package org.quiltmc.enigma.util; + +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeNode; + +import java.io.PrintWriter; + +public final class EntryTreePrinter { + private final PrintWriter output; + + public EntryTreePrinter(PrintWriter output) { + this.output = output; + } + + public static void print(PrintWriter output, EntryTree tree) { + var printer = new EntryTreePrinter(output); + printer.print(tree); + } + + public void print(EntryTree tree) { + this.output.println(tree); + this.output.flush(); + + var iterator = tree.getRootNodes().iterator(); + while (iterator.hasNext()) { + var node = iterator.next(); + this.printNode(node, iterator.hasNext(), ""); + } + } + + private void printNode(EntryTreeNode node, boolean hasNext, String indent) { + this.output.print(indent); + + this.output.print(hasNext ? "├── " : "└── "); + this.output.print(node.getEntry()); + + if (node.hasValue()) { + this.output.print(" -> "); + this.output.println(node.getValue()); + } else { + this.output.println(); + } + + this.output.flush(); + + var iterator = node.getChildNodes().iterator(); + while (iterator.hasNext()) { + var child = iterator.next(); + var childIndent = indent + (hasNext ? "│\u00A0\u00A0 " : " "); // \u00A0 is non-breaking space + + this.printNode(child, iterator.hasNext(), childIndent); + } + } +} diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index af40b4c7e..47274d3bb 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -16,9 +16,9 @@ "menu.file": "File", "menu.file.jar.open": "Open Jar...", "menu.file.jar.close": "Close Jar", - "menu.file.open_recent_project": "Open Recent Project", - "menu.file.max_recent_projects": "Max Recent Projects", - "menu.file.dialog.max_recent_projects.set": "Set Max Recent Projects", + "menu.file.open_recent_project": "Open Recent Project", + "menu.file.max_recent_projects": "Max Recent Projects", + "menu.file.dialog.max_recent_projects.set": "Set Max Recent Projects", "menu.file.mappings.open": "Open Mappings...", "menu.file.mappings.save": "Save Mappings", "menu.file.mappings.save_as": "Save Mappings As...", @@ -60,7 +60,7 @@ "menu.decompiler.settings": "Decompiler Settings", "menu.decompiler.settings.vineflower": "Vineflower Settings", "menu.view": "View", - "menu.view.notifications": "Server Notifications", + "menu.view.notifications": "Server Notifications", "menu.view.themes": "Themes", "menu.view.themes.default": "Default", "menu.view.themes.darcula": "Darcula", @@ -75,7 +75,7 @@ "menu.view.change.title": "Changes", "menu.view.change.summary": "Changes will be applied after the next restart.", "menu.search": "Search", - "menu.search.all": "Search All", + "menu.search.all": "Search All", "menu.search.class": "Search Classes", "menu.search.method": "Search Methods", "menu.search.field": "Search Fields", @@ -120,10 +120,10 @@ "popup_menu.class_selector.rename_class": "Rename Class", "popup_menu.class_selector.expand_all": "Expand All", "popup_menu.class_selector.collapse_all": "Collapse All", - "popup_menu.class_selector.package_rename.title": "Renaming package...", - "popup_menu.class_selector.package_rename.discovering": "Discovering classes to rename...", - "popup_menu.class_selector.package_rename.renaming_classes": "Renaming classes...", - "popup_menu.class_selector.package_rename.renaming_class": "Renaming class: %s", + "popup_menu.class_selector.package_rename.title": "Renaming package...", + "popup_menu.class_selector.package_rename.discovering": "Discovering classes to rename...", + "popup_menu.class_selector.package_rename.renaming_classes": "Renaming classes...", + "popup_menu.class_selector.package_rename.renaming_class": "Renaming class: %s", "editor.decompiling": "Decompiling...", "editor.decompile_error": "An error was encountered while decompiling.", @@ -146,8 +146,8 @@ "info_panel.editor.class.decompiling": "(decompiling...)", "info_panel.editor.class.not_found": "Unable to find class:", - "class_selector.tooltip.stats_not_generated": "Stats are being generated...", - "class_selector.tooltip.stats_for": "Stats for %s:", + "class_selector.tooltip.stats_not_generated": "Stats are being generated...", + "class_selector.tooltip.stats_for": "Stats for %s:", "popup.copied": "Copied!", @@ -220,9 +220,9 @@ "prompt.error": "Error", "prompt.invalid_input": "Invalid input", - "prompt.search.classes": "Classes", - "prompt.search.methods": "Methods", - "prompt.search.fields": "Fields", + "prompt.search.classes": "Classes", + "prompt.search.methods": "Methods", + "prompt.search.fields": "Fields", "prompt.close.title": "Save your changes?", "prompt.close.summary": "Your mappings have not been saved yet. Do you want to save?", @@ -270,14 +270,14 @@ "validation.message.illegal_doc_comment_end": "Javadoc comment cannot contain the character sequence '*/'.", "validation.message.reserved_identifier": "'%s' is a reserved identifier.", "validation.message.unknown_record_getter": "Could not find a matching record component getter for %s", - "validation.message.server_started": "Server started on port %s!", - "validation.message.connected_to_server": "Connected to server with address %s!", - "validation.message.left_server": "Left server.", - "validation.message.user_connected": "User joined server: %s", - "validation.message.user_left_server": "User left server: %s", - "validation.message.opened_project": "Opened project: (%s -> %s)", - "validation.message.opened_jar": "Opened jar: %s", - "validation.message.invalid_package_name": "Invalid package name!", + "validation.message.server_started": "Server started on port %s!", + "validation.message.connected_to_server": "Connected to server with address %s!", + "validation.message.left_server": "Left server.", + "validation.message.user_connected": "User joined server: %s", + "validation.message.user_left_server": "User left server: %s", + "validation.message.opened_project": "Opened project: (%s -> %s)", + "validation.message.opened_jar": "Opened jar: %s", + "validation.message.invalid_package_name": "Invalid package name!", "validation.message.invalid_package_name.long": "Package must not start or end with a slash, and only contain lowercase letters, digits, underscores, and slashes.", "validation.message.shadowed_name_class": "Name '%s' shadows another in '%s'.", @@ -336,9 +336,9 @@ "notification.misc.continue": "Continue?", "notification.type.error": "Error", - "notification.type.warning": "Warning", - "notification.type.info": "Info", - "notification.level.none": "No server notifications", - "notification.level.no_chat": "No chat messages", - "notification.level.full": "All server notifications" + "notification.type.warning": "Warning", + "notification.type.info": "Info", + "notification.level.none": "No server notifications", + "notification.level.no_chat": "No chat messages", + "notification.level.full": "All server notifications" } diff --git a/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposal.java b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposal.java index 68b9ba60a..a9ba57ca5 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposal.java @@ -19,6 +19,7 @@ import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.util.validation.ValidationContext; import org.tinylog.Logger; @@ -28,6 +29,7 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -55,7 +57,7 @@ public static void setupEnigma() { "id": "test:name_all_fields_d" }, { - "id": "test:q_to_w" + "id": "test:owner_name" } ] } @@ -99,8 +101,17 @@ public void testDynamicNameProposal() { throw new RuntimeException("didn't find any fields"); } - project.getRemapper().putMapping(new ValidationContext(null), entry.get(), new EntryMapping("q", null, TokenType.DEOBFUSCATED, null)); - Assertions.assertEquals(new EntryMapping("w", null, TokenType.DYNAMIC_PROPOSED, "test:q_to_w"), project.getRemapper().getMapping(entry.get())); + project.getRemapper().putMapping(new ValidationContext(null), entry.get(), new EntryMapping("query", null, TokenType.DEOBFUSCATED, null)); + Assertions.assertEquals(new EntryMapping("QueryOwner", null, TokenType.DYNAMIC_PROPOSED, "test:owner_name"), project.getRemapper().getMapping(entry.get().getParent())); + + Optional entry2 = project.getJarIndex().getIndex(EntryIndex.class).getMethods().stream().findFirst(); + + if (entry2.isEmpty()) { + throw new RuntimeException("didn't find any methods"); + } + + project.getRemapper().putMapping(new ValidationContext(null), entry2.get(), new EntryMapping("testFoo", null, TokenType.DEOBFUSCATED, null)); + Assertions.assertEquals(new EntryMapping("TestFooOwner", null, TokenType.DYNAMIC_PROPOSED, "test:owner_name"), project.getRemapper().getMapping(entry.get().getParent())); } private static class TestPlugin implements EnigmaPlugin { @@ -111,7 +122,7 @@ public void init(EnigmaPluginContext ctx) { nameAllFields(ctx, "c"); nameAllFields(ctx, "b"); - ctx.registerService(NameProposalService.TYPE, ctx1 -> new TestDynamicNameProposer("test:q_to_w")); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new TestDynamicNameProposer("test:owner_name")); } private static void nameAllFields(EnigmaPluginContext ctx, String prefix) { @@ -150,10 +161,10 @@ public Map, EntryMapping> getProposedNames(JarIndex index) { @Override public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { if (obfEntry != null && oldMapping != null && newMapping != null) { - if (newMapping.targetName() != null && newMapping.targetName().equals("q")) { - Map, EntryMapping> map = new HashMap<>(); - map.put(obfEntry, new EntryMapping("w", null, TokenType.DYNAMIC_PROPOSED, this.id)); - return map; + var name = newMapping.targetName(); + if (name != null && !name.isEmpty() && obfEntry.getParent() != null) { + name = name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1) + "Owner"; + return Map.of(obfEntry.getParent(), new EntryMapping(name, null, TokenType.DYNAMIC_PROPOSED, this.id)); } }