diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java index 9dc7241a6..d0ef73839 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java @@ -1,29 +1,19 @@ package cuchaz.enigma.gui; import cuchaz.enigma.gui.config.keybind.KeyBinds; +import cuchaz.enigma.gui.elements.ClassTreeCellRenderer; import cuchaz.enigma.gui.node.ClassSelectorClassNode; import cuchaz.enigma.gui.node.SortedMutableTreeNode; -import cuchaz.enigma.stats.StatType; -import cuchaz.enigma.gui.util.StatsManager; -import cuchaz.enigma.stats.StatsResult; import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.translation.representation.entry.ClassEntry; -import cuchaz.enigma.utils.I18n; import javax.annotation.Nullable; -import javax.swing.BoxLayout; -import javax.swing.JLabel; -import javax.swing.JPanel; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; -import java.awt.Component; -import java.awt.event.InputEvent; -import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -34,7 +24,6 @@ public class ClassSelector extends JTree { private final Comparator comparator; private final GuiController controller; - private final StatsManager statsManager; private NestedPackages packageManager; private ClassSelectionListener selectionListener; @@ -42,7 +31,6 @@ public class ClassSelector extends JTree { public ClassSelector(Gui gui, Comparator comparator) { this.comparator = comparator; this.controller = gui.getController(); - this.statsManager = gui.getStatsManager(); // configure the tree control this.setEditable(false); @@ -83,64 +71,7 @@ public ClassSelector(Gui gui, Comparator comparator) { } })); - this.setCellRenderer(new DefaultTreeCellRenderer() { - { - this.setLeafIcon(null); - } - - @Override - public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - - if (gui.getController().getProject() != null && leaf && value instanceof ClassSelectorClassNode node) { - class TooltipPanel extends JPanel { - @Override - public String getToolTipText(MouseEvent event) { - StringBuilder text = new StringBuilder(I18n.translateFormatted("class_selector.tooltip.stats_for", node.getDeobfEntry().getSimpleName())); - text.append(System.lineSeparator()); - StatsResult stats = ClassSelector.this.statsManager.getStats(node); - - if (stats == null) { - text.append(I18n.translate("class_selector.tooltip.stats_not_generated")); - } else { - if ((event.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0) { - for (int i = 0; i < StatType.values().length; i++) { - StatType type = StatType.values()[i]; - text.append(type.getName()).append(": ").append(stats.toString(type)).append(i == StatType.values().length - 1 ? "" : "\n"); - } - } else { - text.append(stats); - } - } - - return text.toString(); - } - } - - JPanel panel = new TooltipPanel(); - panel.setOpaque(false); - panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); - JLabel nodeLabel = new JLabel(GuiUtil.getClassIcon(gui, node.getObfEntry())); - panel.add(nodeLabel); - - StatsResult stats = ClassSelector.this.statsManager.getStats(node); - if (stats == null) { - // calculate stats on a separate thread for performance reasons - this.setIcon(GuiUtil.PENDING_STATUS_ICON); - node.reloadStats(gui, ClassSelector.this, false); - } else { - this.setIcon(GuiUtil.getDeobfuscationIcon(stats)); - } - - panel.add(this); - - return panel; - } - - return this; - } - }); - + this.setCellRenderer(new ClassTreeCellRenderer(gui, this)); ToolTipManager.sharedInstance().registerComponent(this); // init defaults @@ -182,7 +113,6 @@ public void setClasses(@Nullable Collection classEntries) { // update the tree control this.packageManager = new NestedPackages(classEntries, this.comparator, this.controller.getProject().getMapper()); this.setModel(new DefaultTreeModel(this.packageManager.getRoot())); - this.invalidateStats(); this.restoreExpansionState(state); } @@ -360,14 +290,6 @@ public void reload() { this.reload(this.packageManager.getRoot(), true); } - /** - * Invalidates the stats for all classes in the tree, forcing them to be reloaded. - * Stats will be calculated asynchronously for each entry the next time that entry is visible. - */ - public void invalidateStats() { - this.statsManager.invalidateStats(); - } - /** * Requests an asynchronous reload of the stats for the given class. * On completion, the class's stats icon will be updated. diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java index afe204607..60ae657b1 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java @@ -30,10 +30,8 @@ import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.gui.util.LanguageUtil; import cuchaz.enigma.gui.util.ScaleUtil; -import cuchaz.enigma.gui.util.StatsManager; import cuchaz.enigma.network.ServerMessage; import cuchaz.enigma.source.Token; -import cuchaz.enigma.stats.StatsGenerator; import cuchaz.enigma.translation.mapping.EntryChange; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; @@ -93,7 +91,6 @@ public class Gui { private final JLabel connectionStatusLabel; private final NotificationManager notificationManager; - private final StatsManager statsManager; public final JFileChooser jarFileChooser; public final JFileChooser tinyMappingsFileChooser; @@ -126,7 +123,6 @@ public Gui(EnigmaProfile profile, Set editableTypes, boolean visib this.connectionStatusLabel = new JLabel(); this.notificationManager = new NotificationManager(this); this.searchDialog = new SearchDialog(this); - this.statsManager = new StatsManager(); this.showsProgressBars = true; @@ -272,10 +268,6 @@ public GuiController getController() { return this.controller; } - public StatsManager getStatsManager() { - return this.statsManager; - } - public List getCrashHistory() { return this.crashHistory; } @@ -298,8 +290,6 @@ public void onFinishOpenJar(String jarName) { this.mainWindow.setTitle(Enigma.NAME + " - " + jarName); this.editorTabbedPane.closeAllEditorTabs(); - this.statsManager.setStatsGenerator(new StatsGenerator(this.controller.getProject())); - // update menu this.isJarOpen = true; diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java index 3fc9188b7..45a035989 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java @@ -1,5 +1,7 @@ package cuchaz.enigma.gui; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import cuchaz.enigma.Enigma; import cuchaz.enigma.EnigmaProfile; import cuchaz.enigma.EnigmaProject; @@ -38,6 +40,8 @@ import cuchaz.enigma.source.SourceIndex; import cuchaz.enigma.source.Token; import cuchaz.enigma.stats.StatsGenerator; +import cuchaz.enigma.stats.StatsResult; +import cuchaz.enigma.stats.StatsTree; import cuchaz.enigma.translation.TranslateResult; import cuchaz.enigma.translation.Translator; import cuchaz.enigma.translation.mapping.EntryChange; @@ -81,11 +85,14 @@ import java.util.stream.Stream; public class GuiController implements ClientPacketHandler { + public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private final Gui gui; private final Enigma enigma; private EnigmaProject project; private IndexTreeBuilder indexTreeBuilder; + private StatsGenerator statsGenerator; private Path loadedMappingPath; private MappingFormat loadedMappingFormat; @@ -117,6 +124,8 @@ public CompletableFuture openJar(final Path jarPath) { this.project = this.enigma.openJar(jarPath, new ClasspathClassProvider(), progress); this.indexTreeBuilder = new IndexTreeBuilder(this.project.getJarIndex()); this.chp = new ClassHandleProvider(this.project, UiConfig.getDecompiler().service); + this.statsGenerator = new StatsGenerator(this.project); + SwingUtilities.invokeLater(() -> { this.gui.onFinishOpenJar(jarPath.getFileName().toString()); this.refreshClasses(); @@ -128,6 +137,7 @@ public void closeJar() { this.chp.destroy(); this.chp = null; this.project = null; + this.statsGenerator = null; this.gui.onCloseJar(); } @@ -154,7 +164,7 @@ public CompletableFuture openMappings(MappingFormat format, Path path) { this.refreshClasses(); this.chp.invalidateJavadoc(); - this.gui.getStatsManager().setStatsGenerator(new StatsGenerator(this.project)); + this.statsGenerator = new StatsGenerator(this.project); } catch (MappingParseException e) { JOptionPane.showMessageDialog(this.gui.getFrame(), e.getMessage()); } @@ -539,9 +549,11 @@ private void applyChange0(ValidationContext vc, EntryChange change, boolean u } } - public void openStats(Set includedTypes, String topLevelPackage, boolean includeSynthetic) { + public void openStatsTree(Set includedTypes) { ProgressDialog.runOffThread(this.gui, progress -> { - String data = this.gui.getStatsManager().getGenerator().generate(progress, includedTypes, topLevelPackage, includeSynthetic).getTreeJson(); + StatsResult overall = this.getStatsGenerator().getResultNullable().getOverall(); + StatsTree tree = overall.buildTree(UiConfig.getLastTopLevelPackage(), includedTypes); + String treeJson = GSON.toJson(tree.root); try { File statsFile = File.createTempFile("stats", ".html"); @@ -549,7 +561,7 @@ public void openStats(Set includedTypes, String topLevelPackage, boole try (FileWriter w = new FileWriter(statsFile)) { w.write( Utils.readResourceToString("/stats.html") - .replace("/*data*/", data) + .replace("/*data*/", treeJson) ); } @@ -586,6 +598,10 @@ public Enigma getEnigma() { return this.enigma; } + public StatsGenerator getStatsGenerator() { + return this.statsGenerator; + } + public void createClient(String username, String ip, int port, char[] password) throws IOException { this.client = new EnigmaClient(this, ip, port); this.client.connect(); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java index 1fe61b000..2f6065a9d 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java @@ -2,8 +2,8 @@ import cuchaz.enigma.gui.Gui; import cuchaz.enigma.gui.config.UiConfig; +import cuchaz.enigma.stats.ProjectStatsResult; import cuchaz.enigma.stats.StatType; -import cuchaz.enigma.stats.StatsResult; import cuchaz.enigma.gui.util.GridBagConstraintsBuilder; import cuchaz.enigma.gui.util.ScaleUtil; import cuchaz.enigma.utils.I18n; @@ -27,13 +27,12 @@ public class StatsDialog { public static void show(Gui gui) { ProgressDialog.runOffThread(gui, listener -> { - StatsResult result = gui.getStatsManager().getGenerator().generate(listener, Set.of(StatType.values()), "", false); - + ProjectStatsResult result = gui.getController().getStatsGenerator().getResult(false); SwingUtilities.invokeLater(() -> show(gui, result, "")); }); } - public static void show(Gui gui, StatsResult result, String packageName) { + public static void show(Gui gui, ProjectStatsResult result, String packageName) { // init frame JDialog dialog = new JDialog(gui.getFrame(), packageName.isEmpty() ? I18n.translate("menu.file.stats.title") : I18n.translateFormatted("menu.file.stats.title_filtered", packageName), true); Container contentPane = dialog.getContentPane(); @@ -44,7 +43,7 @@ public static void show(Gui gui, StatsResult result, String packageName) { Map checkboxes = new EnumMap<>(StatType.class); final int[] i = {0}; - result.getTypes().stream().sorted(Comparator.comparing(StatType::getName)).forEach(type -> { + result.getOverall().getTypes().stream().sorted(Comparator.comparing(StatType::getName)).forEach(type -> { JCheckBox checkBox = new JCheckBox(type.getName()); checkboxes.put(type, checkBox); contentPane.add(checkBox, cb.pos(0, i[0]).weightX(1.0).anchor(GridBagConstraints.WEST).build()); @@ -63,13 +62,18 @@ public static void show(Gui gui, StatsResult result, String packageName) { // show top-level package option JLabel topLevelPackageOption = new JLabel(I18n.translate("menu.file.stats.top_level_package")); - contentPane.add(topLevelPackageOption, cb1.pos(0, result.getTypes().size() + 1).build()); + contentPane.add(topLevelPackageOption, cb1.pos(0, result.getOverall().getTypes().size() + 1).build()); JTextField topLevelPackage = new JTextField(); topLevelPackage.setText(UiConfig.getLastTopLevelPackage()); - contentPane.add(topLevelPackage, cb1.pos(0, result.getTypes().size() + 2).fill(GridBagConstraints.HORIZONTAL).build()); + contentPane.add(topLevelPackage, cb1.pos(0, result.getOverall().getTypes().size() + 2).fill(GridBagConstraints.HORIZONTAL).build()); + + // show synthetic members option + JCheckBox syntheticParametersOption = new JCheckBox(I18n.translate("menu.file.stats.synthetic_parameters")); + syntheticParametersOption.setSelected(UiConfig.shouldIncludeSyntheticParameters()); + contentPane.add(syntheticParametersOption, cb1.pos(0, result.getOverall().getTypes().size() + 4).build()); - // Show filter button + // show filter button JButton filterButton = new JButton(I18n.translate("menu.file.stats.filter")); filterButton.addActionListener(action -> { dialog.dispose(); @@ -77,17 +81,11 @@ public static void show(Gui gui, StatsResult result, String packageName) { UiConfig.setLastTopLevelPackage(topLevelPackage.getText()); UiConfig.save(); - StatsResult statResult = gui.getStatsManager().getGenerator().generate(listener, Set.of(StatType.values()), UiConfig.getLastTopLevelPackage(), false); - - SwingUtilities.invokeLater(() -> show(gui, statResult, UiConfig.getLastTopLevelPackage())); + ProjectStatsResult projectResult = gui.getController().getStatsGenerator().getResult(syntheticParametersOption.isSelected()).filter(UiConfig.getLastTopLevelPackage()); + SwingUtilities.invokeLater(() -> show(gui, projectResult, UiConfig.getLastTopLevelPackage())); }); }); - contentPane.add(filterButton, cb1.pos(0, result.getTypes().size() + 3).anchor(GridBagConstraints.EAST).build()); - - // show synthetic members option - JCheckBox syntheticParametersOption = new JCheckBox(I18n.translate("menu.file.stats.synthetic_parameters")); - syntheticParametersOption.setSelected(UiConfig.shouldIncludeSyntheticParameters()); - contentPane.add(syntheticParametersOption, cb1.pos(0, result.getTypes().size() + 4).build()); + contentPane.add(filterButton, cb1.pos(0, result.getOverall().getTypes().size() + 3).anchor(GridBagConstraints.EAST).build()); // show generate button JButton button = new JButton(I18n.translate("menu.file.stats.generate")); @@ -99,10 +97,10 @@ public static void show(Gui gui, StatsResult result, String packageName) { UiConfig.setIncludeSyntheticParameters(syntheticParametersOption.isSelected()); UiConfig.save(); - generateStats(gui, checkboxes, topLevelPackage.getText(), syntheticParametersOption.isSelected()); + generateStats(gui, checkboxes); }); - contentPane.add(button, cb1.pos(0, result.getTypes().size() + 5).weightY(1.0).anchor(GridBagConstraints.SOUTHWEST).build()); + contentPane.add(button, cb1.pos(0, result.getOverall().getTypes().size() + 5).weightY(1.0).anchor(GridBagConstraints.SOUTHWEST).build()); // add action listener to each checkbox checkboxes.forEach((key, value) -> value.addActionListener(action -> { @@ -123,7 +121,7 @@ public static void show(Gui gui, StatsResult result, String packageName) { dialog.setVisible(true); } - private static void generateStats(Gui gui, Map checkboxes, String topLevelPackage, boolean includeSynthetic) { + private static void generateStats(Gui gui, Map checkboxes) { // get members from selected checkboxes Set includedMembers = checkboxes .entrySet() @@ -134,7 +132,7 @@ private static void generateStats(Gui gui, Map checkboxes, // checks if a project is open if (gui.getController().getProject() != null) { - gui.getController().openStats(includedMembers, topLevelPackage, includeSynthetic); + gui.getController().openStatsTree(includedMembers); } } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ClassTreeCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ClassTreeCellRenderer.java new file mode 100644 index 000000000..090601c76 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ClassTreeCellRenderer.java @@ -0,0 +1,114 @@ +package cuchaz.enigma.gui.elements; + +import cuchaz.enigma.gui.ClassSelector; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.gui.node.ClassSelectorClassNode; +import cuchaz.enigma.gui.node.ClassSelectorPackageNode; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.stats.ProjectStatsResult; +import cuchaz.enigma.stats.StatsResult; +import cuchaz.enigma.stats.StatsGenerator; + +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; +import java.awt.Component; +import java.util.function.Function; + +public class ClassTreeCellRenderer extends DefaultTreeCellRenderer { + private final GuiController controller; + private final ClassSelector selector; + + public ClassTreeCellRenderer(Gui gui, ClassSelector selector) { + this.controller = gui.getController(); + this.selector = selector; + + this.setLeafIcon(null); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + + if ((this.controller.getProject() != null && leaf && value instanceof ClassSelectorClassNode) + || (this.controller.getProject() != null && value instanceof ClassSelectorPackageNode)) { + TooltipPanel panel; + Icon icon; + Function deobfuscationIconGetter; + Runnable reloader; + + if (value instanceof ClassSelectorPackageNode node) { + class PackageTooltipPanel extends TooltipPanel { + PackageTooltipPanel(GuiController controller) { + super(controller); + } + + @Override + StatsResult getStats(StatsGenerator generator) { + return generator.getResultNullable().getPackageStats(this.getDisplayName()); + } + + @Override + String getDisplayName() { + return node.getPackageName(); + } + } + + panel = new PackageTooltipPanel(this.controller); + icon = GuiUtil.getFolderIcon(this, tree, node); + deobfuscationIconGetter = projectStatsResult -> GuiUtil.getDeobfuscationIcon(projectStatsResult, node.getPackageName()); + reloader = () -> {}; + } else { + ClassSelectorClassNode node = (ClassSelectorClassNode) value; + + class ClassTooltipPanel extends TooltipPanel { + ClassTooltipPanel(GuiController controller) { + super(controller); + } + + @Override + StatsResult getStats(StatsGenerator generator) { + return generator.getStats(node.getObfEntry()); + } + + @Override + String getDisplayName() { + return node.getDeobfEntry().getSimpleName(); + } + } + + panel = new ClassTooltipPanel(this.controller); + icon = GuiUtil.getClassIcon(this.controller.getGui(), node.getObfEntry()); + deobfuscationIconGetter = projectStatsResult -> GuiUtil.getDeobfuscationIcon(projectStatsResult, node.getObfEntry()); + reloader = () -> node.reloadStats(this.controller.getGui(), this.selector, false); + } + + panel.setOpaque(false); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + JLabel nodeLabel = new JLabel(icon); + panel.add(nodeLabel); + + if (this.controller.getStatsGenerator() != null) { + ProjectStatsResult stats = this.controller.getStatsGenerator().getResultNullable(); + if (stats == null) { + // calculate stats on a separate thread for performance reasons + this.setIcon(GuiUtil.PENDING_STATUS_ICON); + reloader.run(); + } else { + this.setIcon(deobfuscationIconGetter.apply(stats)); + } + } else { + this.setIcon(GuiUtil.PENDING_STATUS_ICON); + } + + panel.add(this); + + return panel; + } + + return this; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/TooltipPanel.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/TooltipPanel.java new file mode 100644 index 000000000..e74092183 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/TooltipPanel.java @@ -0,0 +1,48 @@ +package cuchaz.enigma.gui.elements; + +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.stats.StatType; +import cuchaz.enigma.stats.StatsGenerator; +import cuchaz.enigma.stats.StatsResult; +import cuchaz.enigma.utils.I18n; + +import javax.swing.JPanel; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; + +public abstract class TooltipPanel extends JPanel { + private final GuiController controller; + + public TooltipPanel(GuiController controller) { + this.controller = controller; + } + + @Override + public String getToolTipText(MouseEvent event) { + StringBuilder text = new StringBuilder(I18n.translateFormatted("class_selector.tooltip.stats_for", this.getDisplayName())); + text.append(System.lineSeparator()); + + StatsGenerator generator = this.controller.getStatsGenerator(); + + if (generator == null || generator.getResultNullable() == null) { + text.append(I18n.translate("class_selector.tooltip.stats_not_generated")); + } else { + StatsResult stats = this.getStats(generator); + + if ((event.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0) { + for (int i = 0; i < StatType.values().length; i++) { + StatType type = StatType.values()[i]; + text.append(type.getName()).append(": ").append(stats.toString(type)).append(i == StatType.values().length - 1 ? "" : "\n"); + } + } else { + text.append(stats); + } + } + + return text.toString(); + } + + abstract StatsResult getStats(StatsGenerator generator); + + abstract String getDisplayName(); +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java index f4b628235..3da6ffef0 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java @@ -1,9 +1,10 @@ package cuchaz.enigma.gui.node; +import cuchaz.enigma.ProgressListener; import cuchaz.enigma.gui.ClassSelector; import cuchaz.enigma.gui.Gui; -import cuchaz.enigma.gui.util.StatsManager; import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.stats.StatsGenerator; import cuchaz.enigma.translation.representation.entry.ClassEntry; import javax.swing.SwingUtilities; @@ -39,13 +40,13 @@ public ClassEntry getDeobfEntry() { * @param updateIfPresent whether to update the stats if they have already been generated for this node */ public void reloadStats(Gui gui, ClassSelector selector, boolean updateIfPresent) { - StatsManager manager = gui.getStatsManager(); + StatsGenerator generator = gui.getController().getStatsGenerator(); SwingWorker iconUpdateWorker = new SwingWorker<>() { @Override protected ClassSelectorClassNode doInBackground() { - if (manager.getStats(ClassSelectorClassNode.this) == null || updateIfPresent) { - manager.generateFor(ClassSelectorClassNode.this); + if (generator.getResultNullable() == null || updateIfPresent) { + generator.generateForClassTree(ProgressListener.none(), ClassSelectorClassNode.this.getObfEntry(), false); } return ClassSelectorClassNode.this; @@ -53,7 +54,7 @@ protected ClassSelectorClassNode doInBackground() { @Override public void done() { - ((DefaultTreeCellRenderer) selector.getCellRenderer()).setIcon(GuiUtil.getDeobfuscationIcon(manager.getStats(ClassSelectorClassNode.this))); + ((DefaultTreeCellRenderer) selector.getCellRenderer()).setIcon(GuiUtil.getDeobfuscationIcon(generator.getResultNullable(), ClassSelectorClassNode.this.getObfEntry())); SwingUtilities.invokeLater(() -> selector.reload(ClassSelectorClassNode.this, false)); } }; diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java index 57d0c61ab..da61d6b1a 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java @@ -3,7 +3,7 @@ import com.formdev.flatlaf.extras.FlatSVGIcon; import cuchaz.enigma.analysis.index.EntryIndex; import cuchaz.enigma.gui.Gui; -import cuchaz.enigma.stats.StatsResult; +import cuchaz.enigma.stats.ProjectStatsResult; import cuchaz.enigma.translation.representation.AccessFlags; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.MethodEntry; @@ -13,9 +13,12 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JToolTip; +import javax.swing.JTree; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.Timer; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.awt.Color; @@ -156,9 +159,28 @@ public static Icon getClassIcon(Gui gui, ClassEntry entry) { return CLASS_ICON; } - public static Icon getDeobfuscationIcon(StatsResult stats) { - if (stats != null) { - double percentage = stats.getPercentage(); + public static Icon getFolderIcon(DefaultTreeCellRenderer renderer, JTree tree, DefaultMutableTreeNode node) { + boolean expanded = tree.isExpanded(new TreePath(node.getPath())); + return expanded ? renderer.getOpenIcon() : renderer.getClosedIcon(); + } + + public static Icon getDeobfuscationIcon(ProjectStatsResult stats, String packageName) { + if (stats != null && stats.getPackageStats(packageName) != null) { + double percentage = stats.getPackageStats(packageName).getPercentage(); + + if (percentage == 100d) { + return DEOBFUSCATED_ICON; + } else if (percentage > 0) { + return PARTIALLY_DEOBFUSCATED_ICON; + } + } + + return OBFUSCATED_ICON; + } + + public static Icon getDeobfuscationIcon(ProjectStatsResult stats, ClassEntry obfEntry) { + if (stats != null && stats.getStats().get(obfEntry) != null) { + double percentage = stats.getStats().get(obfEntry).getPercentage(); if (percentage == 100d) { return DEOBFUSCATED_ICON; diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/StatsManager.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/StatsManager.java deleted file mode 100644 index 893246686..000000000 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/StatsManager.java +++ /dev/null @@ -1,101 +0,0 @@ -package cuchaz.enigma.gui.util; - -import cuchaz.enigma.ProgressListener; -import cuchaz.enigma.gui.node.ClassSelectorClassNode; -import cuchaz.enigma.stats.StatsGenerator; -import cuchaz.enigma.stats.StatsResult; -import cuchaz.enigma.translation.representation.entry.ClassEntry; -import org.tinylog.Logger; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CountDownLatch; - -/** - * A class to manage generated stats for class tree classes. This is used to avoid generating stats for a class multiple times. - */ -public class StatsManager { - private final HashMap results = new HashMap<>(); - private final Map latches = new HashMap<>(); - private StatsGenerator generator; - - public StatsManager() { - this.generator = null; - } - - /** - * Gets the current {@link StatsGenerator}. - * - * @return the current generator - */ - public StatsGenerator getGenerator() { - return this.generator; - } - - /** - * Sets the stats generator to use for generating stats. This should be called whenever the underlying {@link cuchaz.enigma.EnigmaProject} is updated. - * - * @param generator the generator to use - */ - public void setStatsGenerator(StatsGenerator generator) { - this.generator = generator; - } - - /** - * Generates stats for the given class node. If stats are already being generated for that node's class entry, this method will block until the stats are generated to preserve processing power. - * - *

When complete, the stats will be stored in this manager and are ready for use. - * - * @param node the node to generate stats for - */ - public void generateFor(ClassSelectorClassNode node) { - ClassEntry entry = node.getObfEntry(); - - if (!this.latches.containsKey(entry)) { - this.latches.put(entry, new CountDownLatch(1)); - - StatsResult stats = this.generator.generateForClassTree(ProgressListener.none(), entry, false); - this.setStats(node, stats); - } else { - try { - this.latches.get(entry).await(); - } catch (InterruptedException e) { - Logger.error(e, "Failed to await stats generation for class \"{}\"!", entry); - } - } - } - - /** - * Sets the stats for the given class node. - * - * @param node the node to set stats for - * @param stats the stats to associate - */ - public void setStats(ClassSelectorClassNode node, StatsResult stats) { - ClassEntry entry = node.getObfEntry(); - - this.results.put(entry, stats); - if (this.latches.containsKey(entry)) { - this.latches.get(entry).countDown(); - this.latches.remove(entry); - } - } - - /** - * Invalidates all stats stored in this manager by clearing them from storage. - */ - public void invalidateStats() { - this.results.clear(); - } - - /** - * Gets the stats for the given class node. - * - * @param node the node to get stats for - * @return the stats for the given class node, or {@code null} if not yet generated - */ - public StatsResult getStats(ClassSelectorClassNode node) { - ClassEntry entry = node.getObfEntry(); - return this.results.get(entry); - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/stats/ProjectStatsResult.java b/enigma/src/main/java/cuchaz/enigma/stats/ProjectStatsResult.java new file mode 100644 index 000000000..a2b6cf319 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/stats/ProjectStatsResult.java @@ -0,0 +1,174 @@ +package cuchaz.enigma.stats; + +import com.strobel.core.Triple; +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ProjectStatsResult implements StatsProvider { + private final EnigmaProject project; + + private final Map> packageToClasses = new HashMap<>(); + private final Map stats = new HashMap<>(); + private final Map packageStats = new HashMap<>(); + + private StatsResult overall; + + /** + * Creates and indexes an overall result. + * @param project the project the stats are for + * @param stats a map of classes to results + */ + public ProjectStatsResult(EnigmaProject project, Map stats) { + this.project = project; + + for (var entry : stats.entrySet()) { + ClassEntry classEntry = entry.getKey(); + StatsResult statEntry = entry.getValue(); + + this.stats.put(classEntry, statEntry); + this.updatePackage(classEntry, statEntry); + } + + this.rebuildOverall(); + } + + private void updatePackage(ClassEntry obfEntry, StatsResult newStats) { + ClassEntry deobfuscated = this.project.getMapper().deobfuscate(obfEntry); + ClassEntry classEntry = deobfuscated == null ? obfEntry : deobfuscated; + + String packageName = classEntry.getPackageName() == null ? "" : classEntry.getPackageName(); + List packages = getPackages(packageName); + + this.addClass(packages, newStats); + this.rebuildPackageFor(packages); + } + + private void addClass(List packages, StatsResult newStats) { + for (String name : packages) { + this.packageToClasses.putIfAbsent(name, new ArrayList<>()); + List newResults = this.packageToClasses.get(name); + newResults.add(newStats); + } + } + + private static List getPackages(String packageName) { + List packages = new ArrayList<>(); + packages.add(packageName); + for (int i = packageName.lastIndexOf('/'); i > 0; i--) { + if (packageName.charAt(i) == '/') { + packages.add(packageName.substring(0, i)); + } + } + + return packages; + } + + private void rebuildOverall() { + var maps = this.buildStats(this.stats.values()); + this.overall = new StatsResult(maps.getFirst(), maps.getSecond(), maps.getThird(), false); + } + + private Triple, Map, Map>> buildStats(Collection stats) { + Map totalMappable = new HashMap<>(); + Map totalUnmapped = new HashMap<>(); + Map> unmappedTreeData = new HashMap<>(); + + for (StatsResult result : stats) { + for (var unmappedEntry : result.totalUnmapped().entrySet()) { + totalUnmapped.put(unmappedEntry.getKey(), totalUnmapped.getOrDefault(unmappedEntry.getKey(), 0) + unmappedEntry.getValue()); + } + + for (var mappableEntry : result.totalMappable().entrySet()) { + totalMappable.put(mappableEntry.getKey(), totalMappable.getOrDefault(mappableEntry.getKey(), 0) + mappableEntry.getValue()); + } + + if (!result.isPackage()) { + for (var dataEntry : result.unmappedTreeData().entrySet()) { + Map classData = unmappedTreeData.getOrDefault(dataEntry.getKey(), new HashMap<>()); + + for (var data : dataEntry.getValue().entrySet()) { + classData.put(data.getKey(), classData.getOrDefault(data.getKey(), 0) + data.getValue()); + } + + unmappedTreeData.put(dataEntry.getKey(), classData); + } + } + } + + return new Triple<>(totalMappable, totalUnmapped, unmappedTreeData); + } + + private void rebuildPackageFor(List packageNames) { + for (String name : packageNames) { + var newStats = this.buildStats(this.packageToClasses.get(name)); + this.packageStats.put(name, new StatsResult(newStats.getFirst(), newStats.getSecond(), newStats.getThird(), true)); + } + } + + /** + * Filters results by the provided top-level package. + * @param topLevelPackage the package to get stats for, separated with slashes + * @return a new result, containing only classes which match the filter + */ + public ProjectStatsResult filter(String topLevelPackage) { + Map newStats = new HashMap<>(); + + for (var entry : this.stats.entrySet()) { + ClassEntry deobfuscated = this.project.getMapper().deobfuscate(entry.getKey()); + ClassEntry classEntry = deobfuscated == null ? entry.getKey() : deobfuscated; + + String packageName = classEntry.getPackageName() == null ? "" : classEntry.getPackageName(); + if (packageName.startsWith(topLevelPackage)) { + newStats.put(entry.getKey(), entry.getValue()); + } + } + + return new ProjectStatsResult(this.project, newStats); + } + + /** + * Gets the overall result for the provided package + * @param name the package name, separated by slashes + * @return the overall result + */ + public StatsResult getPackageStats(String name) { + return this.packageStats.get(name); + } + + /** + * Gets all per-class stats. + * @return the stats, as a class-to-result map + */ + public Map getStats() { + return this.stats; + } + + /** + * Gets the overall stats for the full project. + * @return the overall result + */ + public StatsResult getOverall() { + return this.overall; + } + + @Override + public int getMappable(StatType... types) { + return this.overall.getMappable(types); + } + + @Override + public int getUnmapped(StatType... types) { + return this.overall.getUnmapped(types); + } + + @Override + public int getMapped(StatType... types) { + return this.overall.getMapped(types); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/stats/StatsGenerator.java b/enigma/src/main/java/cuchaz/enigma/stats/StatsGenerator.java index ed2b9baca..c868b6aa4 100644 --- a/enigma/src/main/java/cuchaz/enigma/stats/StatsGenerator.java +++ b/enigma/src/main/java/cuchaz/enigma/stats/StatsGenerator.java @@ -1,9 +1,9 @@ package cuchaz.enigma.stats; +import com.google.common.base.Preconditions; import cuchaz.enigma.EnigmaProject; import cuchaz.enigma.ProgressListener; import cuchaz.enigma.analysis.index.EntryIndex; -import cuchaz.enigma.translation.mapping.EntryRemapper; import cuchaz.enigma.translation.mapping.EntryResolver; import cuchaz.enigma.translation.mapping.ResolutionStrategy; import cuchaz.enigma.translation.representation.ArgumentDescriptor; @@ -15,29 +15,54 @@ import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; import cuchaz.enigma.translation.representation.entry.MethodDefEntry; import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.translation.representation.entry.ParentedEntry; import cuchaz.enigma.utils.I18n; +import org.tinylog.Logger; import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CountDownLatch; public class StatsGenerator { private final EnigmaProject project; private final EntryIndex entryIndex; - private final EntryRemapper mapper; private final EntryResolver entryResolver; + private ProjectStatsResult result = null; + + private CountDownLatch generationLatch = null; public StatsGenerator(EnigmaProject project) { this.project = project; this.entryIndex = project.getJarIndex().getEntryIndex(); - this.mapper = project.getMapper(); this.entryResolver = project.getJarIndex().getEntryResolver(); } + /** + * Gets the latest generated stats. + * @return the stats, or {@code null} if not yet generated + */ + public ProjectStatsResult getResultNullable() { + return this.result; + } + + /** + * Gets the latest generated stats, or generates them if not yet present. + * @return the stats + */ + public ProjectStatsResult getResult(boolean includeSynthetic) { + if (this.result == null) { + return this.generateForClassTree(ProgressListener.none(), null, includeSynthetic); + } + + return this.result; + } + /** * Generates stats for the given class. Includes all {@link StatType}s in the calculation. * @param progress a listener to update with current progress @@ -45,67 +70,116 @@ public StatsGenerator(EnigmaProject project) { * @param includeSynthetic whether to include synthetic methods * @return the generated {@link StatsResult} for the provided class */ - public StatsResult generateForClassTree(ProgressListener progress, ClassEntry entry, boolean includeSynthetic) { - return this.generate(progress, EnumSet.allOf(StatType.class), entry.getFullName(), entry, includeSynthetic); + public ProjectStatsResult generateForClassTree(ProgressListener progress, ClassEntry entry, boolean includeSynthetic) { + return this.generate(progress, EnumSet.allOf(StatType.class), entry, includeSynthetic); } /** - * Generates stats for the given package. + * Generates stats for the current project. * @param progress a listener to update with current progress * @param includedTypes the types of entry to include in the stats - * @param topLevelPackage the package to generate stats for. Can be separated by slashes or dots. If this is empty, stats will be generated for the entire project * @param includeSynthetic whether to include synthetic methods - * @return the generated {@link StatsResult} for the provided package + * @return the generated {@link ProjectStatsResult} */ - public StatsResult generate(ProgressListener progress, Set includedTypes, String topLevelPackage, boolean includeSynthetic) { - return this.generate(progress, includedTypes, topLevelPackage, null, includeSynthetic); + public ProjectStatsResult generate(ProgressListener progress, Set includedTypes, boolean includeSynthetic) { + return this.generate(progress, includedTypes, null, includeSynthetic); } /** - * Generates stats for the given package or class. + * Generates stats for the current project or updates existing stats with the provided class. + * Somewhat thread-safe: will only generate stats on one thread at a time, awaiting generation on all other threads if called in parallel. * @param progress a listener to update with current progress * @param includedTypes the types of entry to include in the stats - * @param topLevelPackage the package or class to generate stats for. Can be separated by slashes or dots. If this is empty, stats will be generated for the entire project * @param classEntry if stats are being generated for a single class, provide the class here * @param includeSynthetic whether to include synthetic methods - * @return the generated {@link StatsResult} for the provided class or package. + * @return the generated {@link ProjectStatsResult} for the provided class or package */ - public StatsResult generate(ProgressListener progress, Set includedTypes, String topLevelPackage, @Nullable ClassEntry classEntry, boolean includeSynthetic) { + public ProjectStatsResult generate(ProgressListener progress, Set includedTypes, @Nullable ClassEntry classEntry, boolean includeSynthetic) { includedTypes = EnumSet.copyOf(includedTypes); - int totalWork = 0; - Map mappableCounts = new EnumMap<>(StatType.class); - Map> unmappedCounts = new EnumMap<>(StatType.class); + Map stats = this.result == null ? new HashMap<>() : this.result.getStats(); - if (includedTypes.contains(StatType.METHODS) || includedTypes.contains(StatType.PARAMETERS)) { - totalWork += this.entryIndex.getMethods().size(); - } + if (this.result == null || classEntry == null) { + if (this.generationLatch == null) { + this.generationLatch = new CountDownLatch(1); + + List classes = this.entryIndex.getClasses() + .stream().filter(entry -> !entry.isInnerClass()).toList(); - if (includedTypes.contains(StatType.FIELDS)) { - totalWork += this.entryIndex.getFields().size(); + int done = 0; + progress.init(classes.size(), I18n.translate("progress.stats")); + + for (ClassEntry entry : classes) { + progress.step(done++, I18n.translateFormatted("progress.stats.for", entry.getName())); + StatsResult result = this.generate(includedTypes, entry, includeSynthetic); + stats.put(entry, result); + } + + this.result = new ProjectStatsResult(this.project, stats); + this.generationLatch.countDown(); + } else { + try { + progress.init(1, "progress.stats.awaiting"); + this.generationLatch.await(); + } catch (InterruptedException e) { + Logger.error(e, "Failed to await stats generation for project!"); + } + } + } else { + Preconditions.checkNotNull(classEntry, "Entry cannot be null after initial stat generation!"); + stats.put(classEntry, this.generate(includedTypes, classEntry, includeSynthetic)); + this.result = new ProjectStatsResult(this.project, stats); } - if (includedTypes.contains(StatType.CLASSES)) { - totalWork += this.entryIndex.getClasses().size(); + return this.result; + } + + private void addChildrenRecursively(List> entries, Entry toCheck) { + if (toCheck instanceof ClassEntry innerClassEntry) { + List> classChildren = this.project.getJarIndex().getChildrenByClass().get(innerClassEntry); + if (!classChildren.isEmpty()) { + entries.addAll(classChildren); + for (Entry entry : classChildren) { + if (entry instanceof ClassEntry innerInnerClassEntry) { + this.addChildrenRecursively(entries, innerInnerClassEntry); + } + } + } } + } - progress.init(totalWork, I18n.translate("progress.stats")); + /** + * Generates stats for the provided class. + * @param includedTypes the types of entry to include in the stats + * @param classEntry the class to generate stats for + * @param includeSynthetic whether to include synthetic parameters + * @return the generated {@link StatsResult} + */ + public StatsResult generate(Set includedTypes, ClassEntry classEntry, boolean includeSynthetic) { + Map mappableCounts = new EnumMap<>(StatType.class); + Map> unmappedCounts = new EnumMap<>(StatType.class); - String topLevelPackageSlash = topLevelPackage.replace(".", "/"); + List> children = this.project.getJarIndex().getChildrenByClass().get(classEntry); + List> entries = new ArrayList<>(children); - int numDone = 0; - if (includedTypes.contains(StatType.METHODS) || includedTypes.contains(StatType.PARAMETERS)) { - for (MethodEntry method : this.entryIndex.getMethods()) { - progress.step(numDone++, I18n.translate("type.methods")); + for (Entry entry : children) { + this.addChildrenRecursively(entries, entry); + } + entries.add(classEntry); + + for (Entry entry : entries) { + if (entry instanceof FieldEntry field) { + if (!((FieldDefEntry) field).getAccess().isSynthetic()) { + this.update(StatType.FIELDS, mappableCounts, unmappedCounts, field); + } + } else if (entry instanceof MethodEntry method) { MethodEntry root = this.entryResolver .resolveEntry(method, ResolutionStrategy.RESOLVE_ROOT) .stream() .findFirst() .orElseThrow(AssertionError::new); - ClassEntry clazz = root.getParent(); - - if (root == method && this.checkPackage(clazz, topLevelPackageSlash, classEntry)) { + if (root == method) { if (includedTypes.contains(StatType.METHODS) && !((MethodDefEntry) method).getAccess().isSynthetic()) { this.update(StatType.METHODS, mappableCounts, unmappedCounts, method); } @@ -127,73 +201,25 @@ public StatsResult generate(ProgressListener progress, Set includedTyp } } } + } else if (entry instanceof ClassEntry clazz) { + this.update(StatType.CLASSES, mappableCounts, unmappedCounts, clazz); } } - if (includedTypes.contains(StatType.FIELDS)) { - for (FieldEntry field : this.entryIndex.getFields()) { - progress.step(numDone++, I18n.translate("type.fields")); - ClassEntry clazz = field.getParent(); - - if (!((FieldDefEntry) field).getAccess().isSynthetic() && this.checkPackage(clazz, topLevelPackageSlash, classEntry)) { - this.update(StatType.FIELDS, mappableCounts, unmappedCounts, field); - } - } - } - - if (includedTypes.contains(StatType.CLASSES)) { - for (ClassEntry clazz : this.entryIndex.getClasses()) { - progress.step(numDone++, I18n.translate("type.classes")); - - if (this.checkPackage(clazz, topLevelPackageSlash, classEntry)) { - this.update(StatType.CLASSES, mappableCounts, unmappedCounts, clazz); - } - } - } - - progress.step(-1, I18n.translate("progress.stats.data")); - - // generate html display - String topLevelPackageDot = topLevelPackageSlash.replace("/", "."); - StatsResult.Tree tree = getStatTree(unmappedCounts, topLevelPackageDot); - - tree.collapse(tree.root); - - Map rawUnmappedCounts = new EnumMap<>(StatType.class); - for (var entry : unmappedCounts.entrySet()) { - for (int value : entry.getValue().values()) { - rawUnmappedCounts.put(entry.getKey(), rawUnmappedCounts.getOrDefault(entry.getKey(), 0) + value); - } - } - - return new StatsResult(mappableCounts, rawUnmappedCounts, tree); + return StatsResult.create(mappableCounts, unmappedCounts, false); } - private static StatsResult.Tree getStatTree(Map> unmappedCounts, String topLevelPackageDot) { - StatsResult.Tree tree = new StatsResult.Tree<>(); - - for (Map.Entry> typedEntry : unmappedCounts.entrySet()) { - for (Map.Entry entry : typedEntry.getValue().entrySet()) { - if (entry.getKey().startsWith(topLevelPackageDot)) { - StatsResult.Tree.Node node = tree.getNode(entry.getKey()); - int value = node.getValue() == null ? 0 : node.getValue(); - - node.setValue(value + entry.getValue()); - } - } + /** + * Gets the stats for the provided class. + * @param entry the class to get stats for + * @return the stats, or {@code null} if not generated + */ + public StatsResult getStats(ClassEntry entry) { + if (this.result == null) { + return null; } - return tree; - } - - private boolean checkPackage(ClassEntry clazz, String topLevelPackage, @Nullable ClassEntry entry) { - String packageName = this.mapper.deobfuscate(clazz).getPackageName(); - - if (entry == null) { - return topLevelPackage.isBlank() || (packageName != null && packageName.startsWith(topLevelPackage)); - } else { - return clazz.getTopLevelClass().equals(entry.getTopLevelClass()); - } + return this.result.getStats().get(entry); } private void update(StatType type, Map mappable, Map> unmapped, Entry entry) { @@ -203,7 +229,7 @@ private void update(StatType type, Map mappable, Map new HashMap<>()); unmapped.get(type).put(parent, unmapped.get(type).getOrDefault(parent, 0) + 1); diff --git a/enigma/src/main/java/cuchaz/enigma/stats/StatsProvider.java b/enigma/src/main/java/cuchaz/enigma/stats/StatsProvider.java new file mode 100644 index 000000000..cc49e95e4 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/stats/StatsProvider.java @@ -0,0 +1,52 @@ +package cuchaz.enigma.stats; + +public interface StatsProvider { + /** + * Gets the total number of entries that can be mapped, taking into consideration only the provided {@link StatType}s. + * Defaults to all types if none are provided. + * + * @param types the types of entry to include in the result + * @return the number of mappable entries for the given types + */ + int getMappable(StatType... types); + + /** + * Gets the total number of entries that are mappable and remain obfuscated, taking into consideration only the provided {@link StatType}s. + * Defaults to all types if none are provided. + * + * @param types the types of entry to include in the result + * @return the number of unmapped entries for the given types + */ + int getUnmapped(StatType... types); + + /** + * Gets the total number of entries that have been mapped, taking into consideration only the provided {@link StatType}s. + * Defaults to all types if none are provided. + * + * @param types the types of entry to include in the result + * @return the number of mapped entries for the given types + */ + int getMapped(StatType... types); + + /** + * Gets the percentage of entries that have been mapped, taking into consideration only the provided {@link StatType}s. + * Defaults to all types if none are provided. + * + * @param types the types of entry to include in the result + * @return the percentage of entries mapped for the given types + */ + default double getPercentage(StatType... types) { + if (types.length == 0) { + types = StatType.values(); + } + + // avoid showing "Nan%" when there are no entries to map + // if there are none, you've mapped them all! + int mappable = this.getMappable(types); + if (mappable == 0) { + return 100.0f; + } + + return (this.getMapped(types) * 100.0f) / mappable; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/stats/StatsResult.java b/enigma/src/main/java/cuchaz/enigma/stats/StatsResult.java index 28deb52d8..81fa25b2e 100644 --- a/enigma/src/main/java/cuchaz/enigma/stats/StatsResult.java +++ b/enigma/src/main/java/cuchaz/enigma/stats/StatsResult.java @@ -1,31 +1,29 @@ package cuchaz.enigma.stats; -import com.google.gson.GsonBuilder; - -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; -public final class StatsResult { - private final Map totalMappable; - private final Map totalUnmapped; - private final Tree tree; +public record StatsResult(Map totalMappable, Map totalUnmapped, Map> unmappedTreeData, boolean isPackage) implements StatsProvider { + /** + * Creates a new stats result, generating the total unmapped entries from the provided {@code unmappedTreeData}. + * @param totalMappable the total mappable entries + * @param unmappedTreeData the data to use when generating the {@link StatsTree} + * @param isPackage whether the stats are for a package + * @return the result + */ + public static StatsResult create(Map totalMappable, Map> unmappedTreeData, boolean isPackage) { + Map totalUnmapped = new HashMap<>(); + for (var entry : unmappedTreeData.entrySet()) { + for (int value : entry.getValue().values()) { + totalUnmapped.put(entry.getKey(), totalUnmapped.getOrDefault(entry.getKey(), 0) + value); + } + } - public StatsResult(Map totalMappable, Map totalUnmapped, Tree tree) { - this.totalMappable = totalMappable; - this.totalUnmapped = totalUnmapped; - this.tree = tree; + return new StatsResult(totalMappable, totalUnmapped, unmappedTreeData, isPackage); } - /** - * Gets the total number of entries that can be mapped, taking into consideration only the provided {@link StatType}s. - * Defaults to all types if none are provided. - * - * @param types the types of entry to include in the result - * @return the number of mappable entries for the given types - */ + @Override public int getMappable(StatType... types) { if (types.length == 0) { types = StatType.values(); @@ -34,13 +32,7 @@ public int getMappable(StatType... types) { return this.getSum(this.totalMappable, types); } - /** - * Gets the total number of entries that are mappable and remain obfuscated, taking into consideration only the provided {@link StatType}s. - * Defaults to all types if none are provided. - * - * @param types the types of entry to include in the result - * @return the number of unmapped entries for the given types - */ + @Override public int getUnmapped(StatType... types) { if (types.length == 0) { types = StatType.values(); @@ -60,13 +52,7 @@ private int getSum(Map map, StatType... types) { return sum; } - /** - * Gets the total number of entries that have been mapped, taking into consideration only the provided {@link StatType}s. - * Defaults to all types if none are provided. - * - * @param types the types of entry to include in the result - * @return the number of mapped entries for the given types - */ + @Override public int getMapped(StatType... types) { if (types.length == 0) { types = StatType.values(); @@ -75,28 +61,6 @@ public int getMapped(StatType... types) { return this.getMappable(types) - this.getUnmapped(types); } - /** - * Gets the percentage of entries that have been mapped, taking into consideration only the provided {@link StatType}s. - * Defaults to all types if none are provided. - * - * @param types the types of entry to include in the result - * @return the percentage of entries mapped for the given types - */ - public double getPercentage(StatType... types) { - if (types.length == 0) { - types = StatType.values(); - } - - // avoid showing "Nan%" when there are no entries to map - // if there are none, you've mapped them all! - int mappable = this.getMappable(types); - if (mappable == 0) { - return 100.0f; - } - - return (this.getMapped(types) * 100.0f) / mappable; - } - /** * Gets the set of {@link StatType}s that were considered when producing this result. * @@ -107,12 +71,31 @@ public Set getTypes() { } /** - * Gets a tree representation of unmapped entries, formatted to JSON. This is used to show a graph of entries that need mapping. - * - * @return the tree of unmapped entries as JSON + * Builds a tree representation of this stats result. + * @param topLevelPackageDot the top level package, separated by dots + * @param includedTypes the types to include in the tree + * @return the tree */ - public String getTreeJson() { - return new GsonBuilder().setPrettyPrinting().create().toJson(this.tree.root); + public StatsTree buildTree(String topLevelPackageDot, Set includedTypes) { + StatsTree tree = new StatsTree<>(); + + for (Map.Entry> typedEntry : this.unmappedTreeData.entrySet()) { + if (!includedTypes.contains(typedEntry.getKey())) { + continue; + } + + for (Map.Entry entry : typedEntry.getValue().entrySet()) { + if (entry.getKey().startsWith(topLevelPackageDot)) { + StatsTree.Node node = tree.getNode(entry.getKey()); + int value = node.getValue() == null ? 0 : node.getValue(); + + node.setValue(value + entry.getValue()); + } + } + } + + tree.collapse(tree.root); + return tree; } @Override @@ -135,70 +118,4 @@ public String toString(StatType... types) { return String.format("%s/%s %.1f%%", this.getMapped(types), this.getMappable(types), this.getPercentage(types)); } - - public static class Tree { - public final Node root; - private final Map> nodes = new HashMap<>(); - - public static class Node { - private String name; - private T value; - private List> children = new ArrayList<>(); - private final Map> namedChildren = new HashMap<>(); - - public Node(String name, T value) { - this.name = name; - this.value = value; - } - - public T getValue() { - return this.value; - } - - public void setValue(T value) { - this.value = value; - } - } - - public Tree() { - this.root = new Node<>("", null); - } - - public Node getNode(String name) { - Node node = this.nodes.get(name); - - if (node == null) { - node = this.root; - - for (String part : name.split("\\.")) { - Node child = node.namedChildren.get(part); - - if (child == null) { - child = new Node<>(part, null); - node.namedChildren.put(part, child); - node.children.add(child); - } - - node = child; - } - - this.nodes.put(name, node); - } - - return node; - } - - public void collapse(Node node) { - while (node.children.size() == 1) { - Node child = node.children.get(0); - node.name = node.name.isEmpty() ? child.name : node.name + "." + child.name; - node.children = child.children; - node.value = child.value; - } - - for (Node child : node.children) { - this.collapse(child); - } - } - } } diff --git a/enigma/src/main/java/cuchaz/enigma/stats/StatsTree.java b/enigma/src/main/java/cuchaz/enigma/stats/StatsTree.java new file mode 100644 index 000000000..63d5c61a8 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/stats/StatsTree.java @@ -0,0 +1,72 @@ +package cuchaz.enigma.stats; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StatsTree { + public final Node root; + private final Map> nodes = new HashMap<>(); + + public static class Node { + private String name; + private T value; + private List> children = new ArrayList<>(); + private final Map> namedChildren = new HashMap<>(); + + public Node(String name, T value) { + this.name = name; + this.value = value; + } + + public T getValue() { + return this.value; + } + + public void setValue(T value) { + this.value = value; + } + } + + public StatsTree() { + this.root = new Node<>("", null); + } + + public Node getNode(String name) { + Node node = this.nodes.get(name); + + if (node == null) { + node = this.root; + + for (String part : name.split("\\.")) { + Node child = node.namedChildren.get(part); + + if (child == null) { + child = new Node<>(part, null); + node.namedChildren.put(part, child); + node.children.add(child); + } + + node = child; + } + + this.nodes.put(name, node); + } + + return node; + } + + public void collapse(Node node) { + while (node.children.size() == 1) { + Node child = node.children.get(0); + node.name = node.name.isEmpty() ? child.name : node.name + "." + child.name; + node.children = child.children; + node.value = child.value; + } + + for (Node child : node.children) { + this.collapse(child); + } + } +} diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index c446823c7..441557b34 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -190,7 +190,8 @@ "progress.mappings.srg_file.generating": "Generating mappings", "progress.mappings.srg_file.writing": "Writing mappings", "progress.stats": "Generating stats", - "progress.stats.data": "Generating data", + "progress.stats.for": "Generating stats for: %s", + "progress.stats.awaiting": "Awaiting off-thread generation", "javadocs.edit": "Edit Javadocs", "javadocs.instruction": "Edit javadocs here.", diff --git a/enigma/src/main/resources/lang/fr_fr.json b/enigma/src/main/resources/lang/fr_fr.json index 066574151..133f468e0 100644 --- a/enigma/src/main/resources/lang/fr_fr.json +++ b/enigma/src/main/resources/lang/fr_fr.json @@ -186,7 +186,7 @@ "progress.mappings.srg_file.generating": "Génération des mappings", "progress.mappings.srg_file.writing": "Écriture des mappings", "progress.stats": "Génération des statistiques", - "progress.stats.data": "Génération des données", + "progress.stats.for": "Génération des statistiques pour: %s", "javadocs.edit": "Éditer les Javadocs", "javadocs.instruction": "Éditer les Javadocs ici.", diff --git a/enigma/src/main/resources/lang/ja_jp.json b/enigma/src/main/resources/lang/ja_jp.json index c690764ce..cb95b9601 100644 --- a/enigma/src/main/resources/lang/ja_jp.json +++ b/enigma/src/main/resources/lang/ja_jp.json @@ -141,7 +141,6 @@ "progress.mappings.srg_file.generating": "マッピングの作成中", "progress.mappings.srg_file.writing": "マッピングの書き出し中", "progress.stats": "統計情報を作成中", - "progress.stats.data": "データの作成中", "javadocs.edit": "Javadocを編集", "javadocs.instruction": "このJavadocを編集", diff --git a/enigma/src/main/resources/lang/zh_cn.json b/enigma/src/main/resources/lang/zh_cn.json index 4c09e81ed..b3581a922 100644 --- a/enigma/src/main/resources/lang/zh_cn.json +++ b/enigma/src/main/resources/lang/zh_cn.json @@ -89,7 +89,6 @@ "progress.mappings.srg_file.generating": "生成映射", "progress.mappings.srg_file.writing": "写出映射", "progress.stats": "生成统计范围", - "progress.stats.data": "生成数据", "javadocs.edit": "编辑注释", "javadocs.instruction": "在此处编辑编辑注释。", diff --git a/enigma/src/test/java/cuchaz/enigma/TestInnerClassParameterStats.java b/enigma/src/test/java/cuchaz/enigma/TestInnerClassParameterStats.java index 12c15d036..183149dbe 100644 --- a/enigma/src/test/java/cuchaz/enigma/TestInnerClassParameterStats.java +++ b/enigma/src/test/java/cuchaz/enigma/TestInnerClassParameterStats.java @@ -1,9 +1,9 @@ package cuchaz.enigma; import cuchaz.enigma.classprovider.JarClassProvider; +import cuchaz.enigma.stats.ProjectStatsResult; import cuchaz.enigma.stats.StatType; import cuchaz.enigma.stats.StatsGenerator; -import cuchaz.enigma.stats.StatsResult; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -19,7 +19,7 @@ public class TestInnerClassParameterStats { @Test public void testInnerClassParameterStats() { EnigmaProject project = openProject(); - StatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), EnumSet.of(StatType.PARAMETERS), "", false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), EnumSet.of(StatType.PARAMETERS), null, false); // 5/8 total parameters in our six classes are non-mappable, meaning that we should get 0/3 parameters mapped // these non-mappable parameters come from non-static inner classes taking their enclosing class as a parameter // they are currently manually excluded by a check in the stats generator diff --git a/enigma/src/test/java/cuchaz/enigma/TestJarIndexEnums.java b/enigma/src/test/java/cuchaz/enigma/TestJarIndexEnums.java index 432ca6335..dabd3ab0f 100644 --- a/enigma/src/test/java/cuchaz/enigma/TestJarIndexEnums.java +++ b/enigma/src/test/java/cuchaz/enigma/TestJarIndexEnums.java @@ -1,10 +1,10 @@ package cuchaz.enigma; +import cuchaz.enigma.stats.ProjectStatsResult; import org.junit.jupiter.api.Test; import cuchaz.enigma.classprovider.ClasspathClassProvider; import cuchaz.enigma.stats.StatType; import cuchaz.enigma.stats.StatsGenerator; -import cuchaz.enigma.stats.StatsResult; import java.io.IOException; import java.nio.file.Path; @@ -19,7 +19,7 @@ public class TestJarIndexEnums { @Test void checkEnumStats() { EnigmaProject project = openProject(); - StatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), EnumSet.allOf(StatType.class), "", false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), EnumSet.allOf(StatType.class), null, false); assertThat(stats.getMapped(StatType.CLASSES), equalTo(0)); assertThat(stats.getMapped(StatType.FIELDS), equalTo(0)); diff --git a/enigma/src/test/java/cuchaz/enigma/TestStatsGeneration.java b/enigma/src/test/java/cuchaz/enigma/TestStatsGeneration.java index 45759e0ae..5cf95b834 100644 --- a/enigma/src/test/java/cuchaz/enigma/TestStatsGeneration.java +++ b/enigma/src/test/java/cuchaz/enigma/TestStatsGeneration.java @@ -1,9 +1,9 @@ package cuchaz.enigma; import cuchaz.enigma.classprovider.JarClassProvider; +import cuchaz.enigma.stats.ProjectStatsResult; import cuchaz.enigma.stats.StatType; import cuchaz.enigma.stats.StatsGenerator; -import cuchaz.enigma.stats.StatsResult; import cuchaz.enigma.translation.mapping.EntryChange; import cuchaz.enigma.translation.mapping.EntryMapping; import cuchaz.enigma.translation.mapping.EntryUtil; @@ -25,7 +25,7 @@ public class TestStatsGeneration { @Test void checkNoMappedEntriesByDefault() { EnigmaProject project = openProject(); - StatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), Set.of(StatType.values()), "", false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), Set.of(StatType.values()), null, false); assertThat(stats.getMapped(), equalTo(0)); assertThat(stats.getPercentage(), equalTo(0d)); } @@ -85,7 +85,7 @@ private static EnigmaProject openProject() { } private static void checkFullyMapped(EnigmaProject project, StatType... types) { - StatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), Set.of(types), "", false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.none(), Set.of(types), null, false); assertThat(stats.getMapped(types), equalTo(stats.getMappable(types))); assertThat(stats.getPercentage(types), equalTo(100d)); }