diff --git a/runelite-api/src/main/java/net/runelite/api/Opcodes.java b/runelite-api/src/main/java/net/runelite/api/Opcodes.java index 1ee96df082..b5fe92196a 100644 --- a/runelite-api/src/main/java/net/runelite/api/Opcodes.java +++ b/runelite-api/src/main/java/net/runelite/api/Opcodes.java @@ -29,6 +29,16 @@ package net.runelite.api; */ public class Opcodes { + /** + * opcode used to return from scripts. + */ + public static final int RETURN = 21; + + /** + * opcode used to invoke scripts. + */ + public static final int INVOKE = 40; + /** * RuneLite execution opcode used to inject scripts. */ diff --git a/runelite-api/src/main/java/net/runelite/api/events/ScriptPostFired.java b/runelite-api/src/main/java/net/runelite/api/events/ScriptPostFired.java new file mode 100644 index 0000000000..1337dcead1 --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/events/ScriptPostFired.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020, Trevor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.api.events; + +import lombok.Value; + +/** + * An event that is fired after the designated script is ran + */ +@Value +public class ScriptPostFired +{ + /** + * The script id of the invoked script + */ + private final int scriptId; +} diff --git a/runelite-api/src/main/java/net/runelite/api/events/ScriptPreFired.java b/runelite-api/src/main/java/net/runelite/api/events/ScriptPreFired.java new file mode 100644 index 0000000000..c509303784 --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/events/ScriptPreFired.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, Trevor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.api.events; + +import lombok.Data; +import net.runelite.api.ScriptEvent; + +/** + * An event that is fired before the designated script is ran + */ +@Data +public class ScriptPreFired +{ + /** + * The script id of the invoked script + */ + private final int scriptId; + + /** + * The input of the script invoke, this will be null unless it is the root script + */ + private ScriptEvent scriptEvent; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java index a888df3334..2c276aee0a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java @@ -43,15 +43,23 @@ class DevToolsPanel extends PluginPanel private final WidgetInspector widgetInspector; private final VarInspector varInspector; + private final ScriptInspector scriptInspector; @Inject - private DevToolsPanel(Client client, DevToolsPlugin plugin, WidgetInspector widgetInspector, VarInspector varInspector, Notifier notifier) + private DevToolsPanel( + Client client, + DevToolsPlugin plugin, + WidgetInspector widgetInspector, + VarInspector varInspector, + ScriptInspector scriptInspector, + Notifier notifier) { super(); this.client = client; this.plugin = plugin; this.widgetInspector = widgetInspector; this.varInspector = varInspector; + this.scriptInspector = scriptInspector; this.notifier = notifier; setBackground(ColorScheme.DARK_GRAY_COLOR); @@ -133,6 +141,19 @@ class DevToolsPanel extends PluginPanel }); container.add(notificationBtn); + container.add(plugin.getScriptInspector()); + plugin.getScriptInspector().addActionListener((ev) -> + { + if (plugin.getScriptInspector().isActive()) + { + scriptInspector.close(); + } + else + { + scriptInspector.open(); + } + }); + return container; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java index 0f6a4e0648..7f2f6ff885 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java @@ -129,6 +129,7 @@ public class DevToolsPlugin extends Plugin private DevToolsButton widgetInspector; private DevToolsButton varInspector; private DevToolsButton soundEffects; + private DevToolsButton scriptInspector; private NavigationButton navButton; @Provides @@ -170,6 +171,7 @@ public class DevToolsPlugin extends Plugin widgetInspector = new DevToolsButton("Widget Inspector"); varInspector = new DevToolsButton("Var Inspector"); soundEffects = new DevToolsButton("Sound Effects"); + scriptInspector = new DevToolsButton("Script Inspector"); overlayManager.add(overlay); overlayManager.add(locationOverlay); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java new file mode 100644 index 0000000000..1cc8582c9a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ScriptInspector.java @@ -0,0 +1,494 @@ +/* + * Copyright (c) 2020, Trevor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.devtools; + +import com.google.common.collect.Lists; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import javax.inject.Inject; +import javax.swing.BorderFactory; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JFormattedTextField; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTree; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.border.CompoundBorder; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.events.ScriptPostFired; +import net.runelite.api.events.ScriptPreFired; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import static net.runelite.api.widgets.WidgetInfo.TO_CHILD; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.util.Text; + +@Slf4j +public class ScriptInspector extends JFrame +{ + // These scripts are the only ones that fire every client tick regardless of location. + private final static String DEFAULT_BLACKLIST = "3174,1004"; + private final static int MAX_LOG_ENTRIES = 10000; + + private final Client client; + private final EventBus eventBus; + private final ConfigManager configManager; + + private final JPanel tracker = new JPanel(); + private ScriptTreeNode currentNode; + private int lastTick; + private Set blacklist; + private Set highlights; + private JList jList; + private DefaultListModel listModel; + private ListState state = ListState.BLACKLIST; + + private enum ListState + { + BLACKLIST, + HIGHLIGHT + } + + @Data + private class ScriptTreeNode extends DefaultMutableTreeNode + { + private final int scriptId; + private Widget source; + private int duplicateNumber = 1; + + @Override + public String toString() + { + String output = Integer.toString(scriptId); + + if (duplicateNumber != 1) + { + output += " (" + duplicateNumber + ")"; + } + + if (source != null) + { + int id = source.getId(); + output += " - " + TO_GROUP(id) + "." + TO_CHILD(id); + + if (source.getIndex() != -1) + { + output += "[" + source.getIndex() + "]"; + } + + WidgetInfo info = WidgetInspector.getWidgetInfo(id); + if (info != null) + { + output += " " + info.name(); + } + } + + return output; + } + } + + @Inject + ScriptInspector(Client client, EventBus eventBus, DevToolsPlugin plugin, ConfigManager configManager) + { + this.eventBus = eventBus; + this.client = client; + this.configManager = configManager; + + setTitle("RuneLite Script Inspector"); + setIconImage(ClientUI.ICON); + + setLayout(new BorderLayout()); + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() + { + @Override + public void windowClosing(WindowEvent e) + { + close(); + plugin.getScriptInspector().setActive(false); + } + }); + + tracker.setLayout(new DynamicGridLayout(0, 1, 0, 3)); + + final JPanel leftSide = new JPanel(); + leftSide.setLayout(new BorderLayout()); + + final JPanel trackerWrapper = new JPanel(); + trackerWrapper.setLayout(new BorderLayout()); + trackerWrapper.add(tracker, BorderLayout.NORTH); + + final JScrollPane trackerScroller = new JScrollPane(trackerWrapper); + trackerScroller.setPreferredSize(new Dimension(400, 400)); + + final JScrollBar vertical = trackerScroller.getVerticalScrollBar(); + vertical.addAdjustmentListener(new AdjustmentListener() + { + int lastMaximum = actualMax(); + + private int actualMax() + { + return vertical.getMaximum() - vertical.getModel().getExtent(); + } + + @Override + public void adjustmentValueChanged(AdjustmentEvent e) + { + if (vertical.getValue() >= lastMaximum) + { + vertical.setValue(actualMax()); + } + lastMaximum = actualMax(); + } + }); + + leftSide.add(trackerScroller, BorderLayout.CENTER); + + final JPanel bottomLeftRow = new JPanel(); + final JButton clearBtn = new JButton("Clear"); + clearBtn.addActionListener(e -> + { + tracker.removeAll(); + tracker.revalidate(); + }); + + bottomLeftRow.add(clearBtn); + leftSide.add(bottomLeftRow, BorderLayout.SOUTH); + add(leftSide, BorderLayout.CENTER); + + String blacklistConfig = configManager.getConfiguration("devtools", "blacklist"); + + if (blacklistConfig == null) + { + blacklistConfig = DEFAULT_BLACKLIST; + } + + try + { + blacklist = new HashSet<>(Lists.transform(Text.fromCSV(blacklistConfig), Integer::parseInt)); + } + catch (NumberFormatException e) + { + blacklist = new HashSet<>(Lists.transform(Text.fromCSV(DEFAULT_BLACKLIST), Integer::parseInt)); + } + + String highlightsConfig = configManager.getConfiguration("devtools", "highlights"); + + if (highlightsConfig == null) + { + highlightsConfig = ""; + } + + try + { + highlights = new HashSet<>(Lists.transform(Text.fromCSV(highlightsConfig), Integer::parseInt)); + } + catch (NumberFormatException e) + { + blacklist = new HashSet<>(); + } + + final JPanel rightSide = new JPanel(); + rightSide.setLayout(new BorderLayout()); + + listModel = new DefaultListModel(); + changeState(ListState.BLACKLIST); + jList = new JList(listModel); + jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + JScrollPane listScrollPane = new JScrollPane(jList); + + final JButton blacklistButton = new JButton("Blacklist"); + blacklistButton.addActionListener(e -> changeState(ListState.BLACKLIST)); + + final JButton highlightsButton = new JButton("Highlights"); + highlightsButton.addActionListener(e -> changeState(ListState.HIGHLIGHT)); + + final JPanel topLeftRow = new JPanel(); + topLeftRow.setLayout(new FlowLayout()); + topLeftRow.add(blacklistButton); + topLeftRow.add(highlightsButton); + + rightSide.add(topLeftRow, BorderLayout.NORTH); + rightSide.add(listScrollPane, BorderLayout.CENTER); + + final JSpinner jSpinner = new JSpinner(); + Component mySpinnerEditor = jSpinner.getEditor(); + JFormattedTextField textField = ((JSpinner.DefaultEditor) mySpinnerEditor).getTextField(); + textField.setColumns(5); + + final JButton addButton = new JButton("Add"); + addButton.addActionListener(e -> addToSet(jSpinner)); + + final JButton removeButton = new JButton("Remove"); + removeButton.addActionListener(e -> removeSelectedFromSet()); + + final JPanel bottomButtonRow = new JPanel(); + bottomButtonRow.setLayout(new FlowLayout()); + bottomButtonRow.add(addButton); + bottomButtonRow.add(jSpinner); + bottomButtonRow.add(removeButton); + + rightSide.add(bottomButtonRow, BorderLayout.SOUTH); + + add(rightSide, BorderLayout.EAST); + + pack(); + } + + @Subscribe + public void onScriptPreFired(ScriptPreFired event) + { + ScriptTreeNode newNode = new ScriptTreeNode(event.getScriptId()); + if (event.getScriptEvent() != null) + { + newNode.setSource(event.getScriptEvent().getSource()); + } + + if (currentNode == null) + { + currentNode = newNode; + } + else + { + int count = 0; + Enumeration children = currentNode.children(); + if (children != null) + { + while (children.hasMoreElements()) + { + ScriptTreeNode child = (ScriptTreeNode) children.nextElement(); + + if (child.getScriptId() == event.getScriptId()) + { + count++; + } + } + + newNode.setDuplicateNumber(count + 1); + } + + currentNode.add(newNode); + currentNode = newNode; + } + } + + @Subscribe + public void onScriptPostFired(ScriptPostFired event) + { + if (currentNode == null || currentNode.getScriptId() != event.getScriptId()) + { + log.warn("a script was post-fired that was never pre-fired. Script id: " + event.getScriptId()); + return; + } + + if (currentNode.getParent() != null) + { + currentNode = (ScriptTreeNode) currentNode.getParent(); + } + else + { + addScriptLog(currentNode); + currentNode = null; + } + } + + public void open() + { + eventBus.register(this); + setVisible(true); + toFront(); + repaint(); + } + + public void close() + { + configManager.setConfiguration("devtools", "highlights", + Text.toCSV(Lists.transform(new ArrayList<>(highlights), String::valueOf))); + configManager.setConfiguration("devtools", "blacklist", + Text.toCSV(Lists.transform(new ArrayList<>(blacklist), String::valueOf))); + currentNode = null; + eventBus.unregister(this); + setVisible(false); + } + + private void addScriptLog(ScriptTreeNode treeNode) + { + if (blacklist.contains(treeNode.getScriptId())) + { + return; + } + + int tick = client.getTickCount(); + SwingUtilities.invokeLater(() -> + { + if (tick != lastTick) + { + lastTick = tick; + JLabel header = new JLabel("Tick " + tick); + header.setFont(FontManager.getRunescapeSmallFont()); + header.setBorder(new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ColorScheme.LIGHT_GRAY_COLOR), + BorderFactory.createEmptyBorder(3, 6, 0, 0) + )); + tracker.add(header); + } + DefaultTreeModel treeModel = new DefaultTreeModel(treeNode); + JTree tree = new JTree(treeModel); + tree.setRootVisible(true); + tree.setShowsRootHandles(true); + tree.collapsePath(new TreePath(treeNode)); + + ScriptTreeNode highlightNode = findHighlightPathNode(treeNode); + + if (highlightNode != null) + { + tree.setExpandsSelectedPaths(true); + tree.setSelectionPath(new TreePath(treeModel.getPathToRoot(highlightNode))); + } + + tracker.add(tree); + + // Cull very old stuff + for (; tracker.getComponentCount() > MAX_LOG_ENTRIES; ) + { + tracker.remove(0); + } + + tracker.revalidate(); + }); + } + + private void changeState(ListState state) + { + this.state = state; + refreshList(); + } + + private void addToSet(JSpinner spinner) + { + int script = (Integer) spinner.getValue(); + Set set = getSet(); + set.add(script); + refreshList(); + spinner.setValue(0); + } + + private void removeSelectedFromSet() + { + int index = jList.getSelectedIndex(); + + if (index == -1) + { + return; + } + + int script = (Integer) listModel.get(index); + getSet().remove(script); + refreshList(); + } + + private void refreshList() + { + listModel.clear(); + Set set = getSet(); + + for (Integer i : set) + { + listModel.addElement(i); + } + } + + private Set getSet() + { + Set set; + + if (state == ListState.BLACKLIST) + { + set = blacklist; + } + else + { + set = highlights; + } + + return set; + } + + private ScriptTreeNode findHighlightPathNode(ScriptTreeNode node) + { + if (highlights.contains(node.getScriptId())) + { + return node; + } + + Enumeration children = node.children(); + if (children != null) + { + while (children.hasMoreElements()) + { + ScriptTreeNode child = (ScriptTreeNode) children.nextElement(); + + ScriptTreeNode find = findHighlightPathNode(child); + + if (find != null) + { + return find; + } + } + } + + return null; + } +}