diff --git a/runelite-api/src/main/java/net/runelite/api/ParamID.java b/runelite-api/src/main/java/net/runelite/api/ParamID.java
index d726067c5a..d769a715ed 100644
--- a/runelite-api/src/main/java/net/runelite/api/ParamID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ParamID.java
@@ -33,6 +33,7 @@ public final class ParamID
* Long name for NPCs used in the HP hud
*/
public static final int NPC_HP_NAME = 510;
+ public static final int QUEST_NAME = 610;
/**
* @see SettingID
*/
diff --git a/runelite-api/src/main/java/net/runelite/api/ScriptID.java b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
index 5af00b3b87..26573b729e 100644
--- a/runelite-api/src/main/java/net/runelite/api/ScriptID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
@@ -217,6 +217,12 @@ public final class ScriptID
@ScriptArguments(integer = 3)
public static final int GE_ITEM_SEARCH = 752;
+ /**
+ * On load listener for building the quest list interface
+ */
+ @ScriptArguments(integer = 8)
+ public static final int QUESTLIST_INIT = 1350;
+
/**
* Called when the friends list is updated
*
@@ -384,4 +390,16 @@ public final class ScriptID
*/
@ScriptArguments(integer = 1)
public static final int NOTIFICATION_DELAY = 3347;
+
+ /**
+ * Check if a quest should be filtered from the quest list
+ *
+ * - int (StructID) Quest struct
+ * - int State filter
+ * - int Requirement filter
+ * - int Stats filter
+ *
+ */
+ @ScriptArguments(integer = 4)
+ public static final int QUEST_FILTER = 3238;
}
\ No newline at end of file
diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java
index 3b796c354d..f2ca9ffd6a 100644
--- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java
+++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java
@@ -132,6 +132,7 @@ public final class WidgetID
public static final int SKOTIZO_GROUP_ID = 308;
public static final int ENTERING_HOUSE_GROUP_ID = 71;
public static final int FULLSCREEN_CONTAINER_TLI = 165;
+ public static final int QUESTLIST_GROUP_ID = 399;
public static final int SKILLS_GROUP_ID = 320;
public static final int MUSIC_GROUP_ID = 239;
public static final int BARROWS_PUZZLE_GROUP_ID = 25;
@@ -806,6 +807,12 @@ public final class WidgetID
static final int CONTAINER = 2;
}
+ static class QuestList
+ {
+ static final int BOX = 0;
+ static final int CONTAINER = 2;
+ }
+
static class Music
{
static final int CONTAINER = 0;
diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java
index 847359cc70..f22eaa0cbd 100644
--- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java
+++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java
@@ -519,6 +519,9 @@ public enum WidgetInfo
SKOTIZO_CONTAINER(WidgetID.SKOTIZO_GROUP_ID, WidgetID.Skotizo.CONTAINER),
+ QUESTLIST_BOX(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.BOX),
+ QUESTLIST_CONTAINER(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.CONTAINER),
+
SEED_VAULT_TITLE_CONTAINER(WidgetID.SEED_VAULT_GROUP_ID, WidgetID.SeedVault.TITLE_CONTAINER),
SEED_VAULT_ITEM_CONTAINER(WidgetID.SEED_VAULT_GROUP_ID, WidgetID.SeedVault.ITEM_CONTAINER),
SEED_VAULT_ITEM_TEXT(WidgetID.SEED_VAULT_GROUP_ID, WidgetID.SeedVault.ITEM_TEXT),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/questlist/QuestListPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/questlist/QuestListPlugin.java
new file mode 100644
index 0000000000..49bc77d81c
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/questlist/QuestListPlugin.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2019 Spudjb
+ * Copyright (c) 2022 Adam
+ * 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.questlist;
+
+import com.google.common.base.Strings;
+import javax.inject.Inject;
+import net.runelite.api.Client;
+import net.runelite.api.ParamID;
+import net.runelite.api.ScriptID;
+import net.runelite.api.SoundEffectID;
+import net.runelite.api.SpriteID;
+import net.runelite.api.VarClientInt;
+import net.runelite.api.Varbits;
+import net.runelite.api.events.ScriptCallbackEvent;
+import net.runelite.api.events.ScriptPostFired;
+import net.runelite.api.events.VarClientIntChanged;
+import net.runelite.api.events.VarbitChanged;
+import net.runelite.api.widgets.JavaScriptCallback;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetInfo;
+import net.runelite.api.widgets.WidgetPositionMode;
+import net.runelite.api.widgets.WidgetType;
+import net.runelite.client.callback.ClientThread;
+import net.runelite.client.eventbus.Subscribe;
+import net.runelite.client.game.chatbox.ChatboxPanelManager;
+import net.runelite.client.game.chatbox.ChatboxTextInput;
+import net.runelite.client.plugins.Plugin;
+import net.runelite.client.plugins.PluginDescriptor;
+
+@PluginDescriptor(
+ name = "Quest List",
+ description = "Adds a search filter to the quest list"
+)
+public class QuestListPlugin extends Plugin
+{
+ private static final String MENU_OPEN = "Open";
+ private static final String MENU_CLOSE = "Close";
+ private static final String MENU_SEARCH = "Search";
+
+ @Inject
+ private Client client;
+
+ @Inject
+ private ChatboxPanelManager chatboxPanelManager;
+
+ @Inject
+ private ClientThread clientThread;
+
+ private ChatboxTextInput searchInput;
+ private Widget questSearchButton;
+
+ @Override
+ protected void startUp()
+ {
+ clientThread.invoke(this::addQuestButtons);
+ }
+
+ @Override
+ protected void shutDown()
+ {
+ Widget header = client.getWidget(WidgetInfo.QUESTLIST_BOX);
+ if (header != null)
+ {
+ header.deleteAllChildren();
+ }
+ }
+
+ @Subscribe
+ public void onScriptPostFired(ScriptPostFired event)
+ {
+ if (event.getScriptId() == ScriptID.QUESTLIST_INIT)
+ {
+ addQuestButtons();
+ }
+ }
+
+ private void addQuestButtons()
+ {
+ Widget header = client.getWidget(WidgetInfo.QUESTLIST_BOX);
+ if (header != null)
+ {
+ header.deleteAllChildren();
+
+ questSearchButton = header.createChild(-1, WidgetType.GRAPHIC);
+ questSearchButton.setSpriteId(SpriteID.GE_SEARCH);
+ questSearchButton.setOriginalWidth(18);
+ questSearchButton.setOriginalHeight(17);
+ questSearchButton.setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT);
+ questSearchButton.setOriginalX(5);
+ questSearchButton.setOriginalY(0);
+ questSearchButton.setHasListener(true);
+ questSearchButton.setAction(1, MENU_OPEN);
+ questSearchButton.setOnOpListener((JavaScriptCallback) e -> openSearch());
+ questSearchButton.setName(MENU_SEARCH);
+ questSearchButton.revalidate();
+ }
+ }
+
+ @Subscribe
+ public void onVarbitChanged(VarbitChanged varbitChanged)
+ {
+ if (isChatboxOpen() && !isOnQuestTab())
+ {
+ chatboxPanelManager.close();
+ }
+ }
+
+ @Subscribe
+ public void onVarClientIntChanged(VarClientIntChanged varClientIntChanged)
+ {
+ if (varClientIntChanged.getIndex() == VarClientInt.INVENTORY_TAB.getIndex())
+ {
+ if (isChatboxOpen() && !isOnQuestTab())
+ {
+ chatboxPanelManager.close();
+ }
+ }
+ }
+
+ @Subscribe
+ public void onScriptCallbackEvent(ScriptCallbackEvent scriptCallbackEvent)
+ {
+ if (!"questFilter".equals(scriptCallbackEvent.getEventName()) || !isChatboxOpen())
+ {
+ return;
+ }
+
+ final String filter = searchInput.getValue();
+ if (Strings.isNullOrEmpty(filter))
+ {
+ return;
+ }
+
+ final int[] intStack = client.getIntStack();
+ final int intStackSize = client.getIntStackSize();
+
+ final int questStruct = intStack[intStackSize - 1];
+ final String questName = client.getStructComposition(questStruct)
+ .getStringValue(ParamID.QUEST_NAME);
+
+ intStack[intStackSize - 2] = questName.toLowerCase().contains(filter.toLowerCase()) ? 0 : 1;
+ }
+
+ private boolean isOnQuestTab()
+ {
+ return client.getVar(Varbits.QUEST_TAB) == 0 && client.getVar(VarClientInt.INVENTORY_TAB) == 2;
+ }
+
+ private boolean isChatboxOpen()
+ {
+ return searchInput != null && chatboxPanelManager.getCurrentInput() == searchInput;
+ }
+
+ private void closeSearch()
+ {
+ chatboxPanelManager.close();
+ redrawQuests();
+ client.playSoundEffect(SoundEffectID.UI_BOOP);
+ }
+
+ private void openSearch()
+ {
+ client.playSoundEffect(SoundEffectID.UI_BOOP);
+ questSearchButton.setAction(1, MENU_CLOSE);
+ questSearchButton.setOnOpListener((JavaScriptCallback) e -> closeSearch());
+ searchInput = chatboxPanelManager.openTextInput("Search quest list")
+ .onChanged(s -> redrawQuests())
+ .onDone(s -> false)
+ .onClose(() ->
+ {
+ redrawQuests();
+ questSearchButton.setOnOpListener((JavaScriptCallback) e -> openSearch());
+ questSearchButton.setAction(1, MENU_OPEN);
+ })
+ .build();
+ }
+
+ private void redrawQuests()
+ {
+ Widget w = client.getWidget(WidgetInfo.QUESTLIST_CONTAINER);
+ if (w == null)
+ {
+ return;
+ }
+
+ Object[] onVarTransmitListener = w.getOnVarTransmitListener();
+ if (onVarTransmitListener == null)
+ {
+ return;
+ }
+
+ clientThread.invokeLater(() ->
+ client.runScript(onVarTransmitListener));
+ }
+}
diff --git a/runelite-client/src/main/scripts/QuestFilter.hash b/runelite-client/src/main/scripts/QuestFilter.hash
new file mode 100644
index 0000000000..8e30fb9e9e
--- /dev/null
+++ b/runelite-client/src/main/scripts/QuestFilter.hash
@@ -0,0 +1 @@
+3FA5FFC8DB18A42971CED41A9BC7CEA407A0EC98061D56B2822F66CD910E4BAF
\ No newline at end of file
diff --git a/runelite-client/src/main/scripts/QuestFilter.rs2asm b/runelite-client/src/main/scripts/QuestFilter.rs2asm
new file mode 100644
index 0000000000..15fd8f7a98
--- /dev/null
+++ b/runelite-client/src/main/scripts/QuestFilter.rs2asm
@@ -0,0 +1,129 @@
+.id 3238
+.int_stack_count 4
+.string_stack_count 0
+.int_var_count 5 ; +1 for filter result
+.string_var_count 0
+ iconst -1 ; set to 1 to hide, 0 to show
+ iload 0 ; quest struct
+ sconst "questFilter"
+ runelite_callback
+ pop_int ; quest struct
+ istore 4 ; save result
+ ; compare with -1
+ iload 4 ; load result
+ iconst -1
+ if_icmpeq continue
+ ; return value
+ iload 4
+ return
+
+continue:
+ iload 0
+ iconst 611
+ struct_param
+ iconst 1
+ if_icmpeq LABEL6
+ jump LABEL12
+LABEL6:
+ invoke 4025
+ iconst 1
+ if_icmpeq LABEL10
+ jump LABEL12
+LABEL10:
+ iconst 1
+ return
+LABEL12:
+ iload 0
+ iconst 611
+ struct_param
+ iconst 0
+ if_icmpeq LABEL18
+ jump LABEL24
+LABEL18:
+ get_varbit 13774
+ iconst 1
+ if_icmpeq LABEL22
+ jump LABEL24
+LABEL22:
+ iconst 1
+ return
+LABEL24:
+ iload 1
+ iconst 0
+ if_icmpeq LABEL28
+ jump LABEL34
+LABEL28:
+ get_varbit 13775
+ iconst 1
+ if_icmpeq LABEL32
+ jump LABEL34
+LABEL32:
+ iconst 1
+ return
+LABEL34:
+ iload 1
+ iconst 1
+ if_icmpeq LABEL38
+ jump LABEL44
+LABEL38:
+ get_varbit 13776
+ iconst 1
+ if_icmpeq LABEL42
+ jump LABEL44
+LABEL42:
+ iconst 1
+ return
+LABEL44:
+ iload 1
+ iconst 2
+ if_icmpeq LABEL48
+ jump LABEL54
+LABEL48:
+ get_varbit 13777
+ iconst 1
+ if_icmpeq LABEL52
+ jump LABEL54
+LABEL52:
+ iconst 1
+ return
+LABEL54:
+ iload 1
+ iconst 1
+ if_icmpeq LABEL58
+ jump LABEL68
+LABEL58:
+ get_varbit 13778
+ iconst 2
+ if_icmpeq LABEL62
+ jump LABEL68
+LABEL62:
+ iload 2
+ iconst 0
+ if_icmpeq LABEL66
+ jump LABEL68
+LABEL66:
+ iconst 1
+ return
+LABEL68:
+ iload 1
+ iconst 1
+ if_icmpeq LABEL72
+ jump LABEL82
+LABEL72:
+ get_varbit 13779
+ iconst 2
+ if_icmpeq LABEL76
+ jump LABEL82
+LABEL76:
+ iload 3
+ iconst 0
+ if_icmpeq LABEL80
+ jump LABEL82
+LABEL80:
+ iconst 1
+ return
+LABEL82:
+ iconst 0
+ return
+ iconst -1
+ return