From 2d40f274e3f8fa697401389885e80f9de4941264 Mon Sep 17 00:00:00 2001 From: spudjb <48890194+spudjb@users.noreply.github.com> Date: Sun, 24 Mar 2019 19:42:39 +0100 Subject: [PATCH] Add quest list plugin This plugin adds two features to the quest list: * The ability to search for a specific quest * The ability to hide already completed quests --- .../java/net/runelite/api/VarClientInt.java | 2 + .../main/java/net/runelite/api/Varbits.java | 7 +- .../net/runelite/api/widgets/WidgetID.java | 9 + .../net/runelite/api/widgets/WidgetInfo.java | 7 +- .../plugins/questlist/QuestListPlugin.java | 384 ++++++++++++++++++ 5 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/questlist/QuestListPlugin.java diff --git a/runelite-api/src/main/java/net/runelite/api/VarClientInt.java b/runelite-api/src/main/java/net/runelite/api/VarClientInt.java index 1731b79046..0652f3b0e0 100644 --- a/runelite-api/src/main/java/net/runelite/api/VarClientInt.java +++ b/runelite-api/src/main/java/net/runelite/api/VarClientInt.java @@ -46,6 +46,8 @@ public enum VarClientInt MEMBERSHIP_STATUS(103), + INVENTORY_TAB(171), + WORLD_MAP_SEARCH_FOCUSED(190); private final int index; diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java index ca053bd207..88ce84f272 100644 --- a/runelite-api/src/main/java/net/runelite/api/Varbits.java +++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java @@ -475,7 +475,12 @@ public enum Varbits * 0 = buy * 1 = sell */ - GE_OFFER_CREATION_TYPE(4397); + GE_OFFER_CREATION_TYPE(4397), + + /** + * The active tab within the quest interface + */ + QUEST_TAB(8168); /** * The raw varbit ID. 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 e6a73dc211..d56fa63c6c 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 @@ -127,6 +127,7 @@ public class WidgetID public static final int FULLSCREEN_MAP_GROUP_ID = 165; public static final int QUESTLIST_GROUP_ID = 399; public static final int SKILLS_GROUP_ID = 320; + public static final int QUESTTAB_GROUP_ID = 629; static class WorldMap { @@ -751,8 +752,16 @@ public class WidgetID static class QuestList { + static final int BOX = 0; + static final int SCROLLBAR = 3; + static final int CONTAINER = 5; static final int FREE_CONTAINER = 6; static final int MEMBERS_CONTAINER = 7; static final int MINIQUEST_CONTAINER = 8; } + + static class QuestTab + { + static final int QUEST_TAB = 3; + } } 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 fb9b005bb5..db8c9e59e4 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 @@ -460,9 +460,14 @@ public enum WidgetInfo FULLSCREEN_MAP_ROOT(WidgetID.FULLSCREEN_MAP_GROUP_ID, WidgetID.FullScreenMap.ROOT), + QUESTLIST_BOX(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.BOX), + QUESTLIST_CONTAINER(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.CONTAINER), + QUESTLIST_SCROLLBAR(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.SCROLLBAR), QUESTLIST_FREE_CONTAINER(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.FREE_CONTAINER), QUESTLIST_MEMBERS_CONTAINER(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.MEMBERS_CONTAINER), - QUESTLIST_MINIQUEST_CONTAINER(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.MINIQUEST_CONTAINER); + QUESTLIST_MINIQUEST_CONTAINER(WidgetID.QUESTLIST_GROUP_ID, WidgetID.QuestList.MINIQUEST_CONTAINER), + + QUESTTAB_QUEST_TAB(WidgetID.QUESTTAB_GROUP_ID, WidgetID.QuestTab.QUEST_TAB); private final int groupId; private final int childId; 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..800de03d81 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/questlist/QuestListPlugin.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2019 Spudjb + * 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.collect.ImmutableList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.List; +import java.util.stream.Collectors; +import javax.inject.Inject; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.GameState; +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.GameStateChanged; +import net.runelite.api.events.VarClientIntChanged; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.events.WidgetLoaded; +import net.runelite.api.widgets.JavaScriptCallback; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetID; +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; +import net.runelite.client.util.Text; + +@PluginDescriptor( + name = "Quest List", + description = "Adds searching and filtering to the quest list" +) +public class QuestListPlugin extends Plugin +{ + private static final int ENTRY_PADDING = 8; + private static final List QUEST_HEADERS = ImmutableList.of("Free Quests", "Members' Quests", "Miniquests"); + + private static final int SPRITE_SHOW_COMPLETED = SpriteID.OPTIONS_ZOOM_SLIDER_THUMB; + private static final int SPRITE_HIDE_COMPLETED = SpriteID.SQUARE_CHECK_BOX; + + private static final String MENU_OPEN = "Open"; + private static final String MENU_CLOSE = "Close"; + + private static final String MENU_SHOW = "Show"; + private static final String MENU_HIDE = "Hide"; + + private static final String MENU_SEARCH = "Search"; + private static final String MENU_COMPLETED = "Completed Quests"; + + @Inject + private Client client; + + @Inject + private ChatboxPanelManager chatboxPanelManager; + + @Inject + private ClientThread clientThread; + + private ChatboxTextInput searchInput; + private Widget questSearchButton; + private Widget questHideCompletedButton; + + private EnumMap> questSet; + + private boolean hideCompleted; + + @Subscribe + public void onGameStateChanged(GameStateChanged e) + { + if (e.getGameState() == GameState.LOGGING_IN) + { + hideCompleted = false; + } + } + + @Subscribe + public void onWidgetLoaded(WidgetLoaded widgetLoaded) + { + if (widgetLoaded.getGroupId() == WidgetID.QUESTLIST_GROUP_ID) + { + Widget header = client.getWidget(WidgetInfo.QUESTLIST_BOX); + if (header != null) + { + 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(); + + questHideCompletedButton = header.createChild(-1, WidgetType.GRAPHIC); + redrawHideCompletedButton(); + + questHideCompletedButton.setOriginalWidth(18); + questHideCompletedButton.setOriginalHeight(17); + questHideCompletedButton.setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT); + questHideCompletedButton.setOriginalX(25); + questHideCompletedButton.setOriginalY(0); + questHideCompletedButton.setHasListener(true); + questHideCompletedButton.setOnOpListener((JavaScriptCallback) e -> toggleHideCompleted()); + questHideCompletedButton.setName(MENU_COMPLETED); + + questHideCompletedButton.revalidate(); + + questSet = new EnumMap<>(QuestContainer.class); + + if (!header.isHidden()) + { + updateFilter(); + } + } + } + } + + @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(); + } + } + } + + private void toggleHideCompleted() + { + hideCompleted = !hideCompleted; + redrawHideCompletedButton(); + + updateFilter(); + client.playSoundEffect(SoundEffectID.UI_BOOP); + } + + private void redrawHideCompletedButton() + { + questHideCompletedButton.setSpriteId(hideCompleted ? SPRITE_HIDE_COMPLETED : SPRITE_SHOW_COMPLETED); + questHideCompletedButton.setAction(1, hideCompleted ? MENU_SHOW : MENU_HIDE); + } + + 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() + { + updateFilter(""); + chatboxPanelManager.close(); + client.playSoundEffect(SoundEffectID.UI_BOOP); + } + + private void openSearch() + { + updateFilter(""); + client.playSoundEffect(SoundEffectID.UI_BOOP); + questSearchButton.setAction(1, MENU_CLOSE); + questSearchButton.setOnOpListener((JavaScriptCallback) e -> closeSearch()); + searchInput = chatboxPanelManager.openTextInput("Search quest list") + .onChanged(s -> clientThread.invokeLater(() -> updateFilter(s))) + .onClose(() -> + { + clientThread.invokeLater(() -> updateFilter("")); + questSearchButton.setOnOpListener((JavaScriptCallback) e -> openSearch()); + questSearchButton.setAction(1, MENU_OPEN); + }) + .build(); + } + + private void updateFilter() + { + String filter = ""; + if (isChatboxOpen()) + { + filter = searchInput.getValue(); + } + + updateFilter(filter); + } + + private void updateFilter(String filter) + { + filter = filter.toLowerCase(); + final Widget container = client.getWidget(WidgetInfo.QUESTLIST_CONTAINER); + + final Widget freeList = client.getWidget(QuestContainer.FREE_QUESTS.widgetInfo); + final Widget memberList = client.getWidget(QuestContainer.MEMBER_QUESTS.widgetInfo); + final Widget miniList = client.getWidget(QuestContainer.MINI_QUESTS.widgetInfo); + + if (container == null || freeList == null || memberList == null || miniList == null) + { + return; + } + + updateList(QuestContainer.FREE_QUESTS, filter); + updateList(QuestContainer.MEMBER_QUESTS, filter); + updateList(QuestContainer.MINI_QUESTS, filter); + + memberList.setOriginalY(freeList.getOriginalY() + freeList.getOriginalHeight() + ENTRY_PADDING); + miniList.setOriginalY(memberList.getOriginalY() + memberList.getOriginalHeight() + ENTRY_PADDING); + + // originalHeight is changed within updateList so revalidate all lists + freeList.revalidate(); + memberList.revalidate(); + miniList.revalidate(); + + int y = miniList.getRelativeY() + miniList.getHeight() + 10; + + int newHeight = 0; + if (container.getScrollHeight() > 0) + { + newHeight = (container.getScrollY() * y) / container.getScrollHeight(); + } + + container.setScrollHeight(y); + container.revalidateScroll(); + + client.runScript( + ScriptID.UPDATE_SCROLLBAR, + WidgetInfo.QUESTLIST_SCROLLBAR.getId(), + WidgetInfo.QUESTLIST_CONTAINER.getId(), + newHeight + ); + } + + private void updateList(QuestContainer questContainer, String filter) + { + Widget list = client.getWidget(questContainer.widgetInfo); + if (list == null) + { + return; + } + + Collection quests = questSet.get(questContainer); + + if (quests != null) + { + // Check to make sure the list hasn't been rebuild since we were last her + // Do this by making sure the list's dynamic children are the same as when we last saw them + if (quests.stream().noneMatch(w -> + { + Widget codeWidget = w.getQuest(); + if (codeWidget == null) + { + return false; + } + return list.getChild(codeWidget.getIndex()) == codeWidget; + })) + { + quests = null; + } + } + + if (quests == null) + { + // Find all of the widgets that we care about, sorting by their Y value + quests = Arrays.stream(list.getDynamicChildren()) + .sorted(Comparator.comparing(Widget::getRelativeY)) + .filter(w -> !w.isSelfHidden() && !QUEST_HEADERS.contains(w.getText())) + .map(w -> new QuestWidget(w, Text.removeTags(w.getText()).toLowerCase())) + .collect(Collectors.toList()); + questSet.put(questContainer, quests); + } + + // offset because of header + int y = 20; + for (QuestWidget questInfo : quests) + { + Widget quest = questInfo.getQuest(); + QuestState questState = QuestState.getByColor(quest.getTextColor()); + + boolean hidden = !(filter.isEmpty() + || questInfo.getTitle().contains(filter)) + || (filter.isEmpty() && hideCompleted && questState == QuestState.COMPLETE); + + quest.setHidden(hidden); + quest.setOriginalY(y); + quest.revalidate(); + + if (!hidden) + { + y += quest.getHeight(); + } + } + + list.setOriginalHeight(y); + } + + @AllArgsConstructor + @Getter + private enum QuestContainer + { + FREE_QUESTS(WidgetInfo.QUESTLIST_FREE_CONTAINER), + MEMBER_QUESTS(WidgetInfo.QUESTLIST_MEMBERS_CONTAINER), + MINI_QUESTS(WidgetInfo.QUESTLIST_MINIQUEST_CONTAINER); + + private final WidgetInfo widgetInfo; + } + + @AllArgsConstructor + @Getter + private enum QuestState + { + NOT_STARTED(0xff0000), IN_PROGRESS(0xffff00), COMPLETE(0xdc10d); + + private final int color; + + static QuestState getByColor(int color) + { + for (QuestState value : values()) + { + if (value.getColor() == color) + { + return value; + } + } + + return null; + } + } + + @Data + @AllArgsConstructor + private static class QuestWidget + { + private Widget quest; + private String title; + } +}