diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java index 063e833f89..31feace4cf 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -1709,4 +1709,19 @@ public interface Client extends GameEngine * Makes all widgets behave as if they are {@link WidgetConfig#WIDGET_USE_TARGET} */ void setAllWidgetsAreOpTargetable(boolean value); + + /** + * Sets the result count for GE search + */ + void setGeSearchResultCount(int count); + + /** + * Sets the array of item ids for GE search + */ + void setGeSearchResultIds(short[] ids); + + /** + * Sets the starting index in the item id array for GE search + */ + void setGeSearchResultIndex(int index); } diff --git a/runelite-api/src/main/java/net/runelite/api/events/GrandExchangeSearched.java b/runelite-api/src/main/java/net/runelite/api/events/GrandExchangeSearched.java new file mode 100644 index 0000000000..aa96680d1c --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/events/GrandExchangeSearched.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019, Ron Young + * 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; + +/** + * An event where the Grand Exchange has been searched. + */ +@Data +public class GrandExchangeSearched +{ + /** + * Whether or not the event has been consumed by a subscriber. + */ + private boolean consumed; + + /** + * Marks the event as having been consumed. + *

+ * Setting this state indicates that a plugin has set the GE + * search results and that the event will not be passed on + * for handling by vanilla client code. + */ + public void consume() + { + this.consumed = true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemVariationMapping.java b/runelite-client/src/main/java/net/runelite/client/game/ItemVariationMapping.java index 9629bd99ac..a26f844a91 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/ItemVariationMapping.java +++ b/runelite-client/src/main/java/net/runelite/client/game/ItemVariationMapping.java @@ -26,11 +26,14 @@ package net.runelite.client.game; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.Map; @@ -40,6 +43,7 @@ import java.util.Map; public class ItemVariationMapping { private static final Map MAPPINGS; + private static final Multimap INVERTED_MAPPINGS; static { @@ -52,6 +56,7 @@ public class ItemVariationMapping final Map> itemVariations = gson.fromJson(new InputStreamReader(geLimitData), typeToken.getType()); ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + ImmutableMultimap.Builder invertedBuilder = new ImmutableMultimap.Builder<>(); for (Collection value : itemVariations.values()) { final Iterator iterator = value.iterator(); @@ -59,9 +64,15 @@ public class ItemVariationMapping while (iterator.hasNext()) { - builder.put(iterator.next(), base); + final int id = iterator.next(); + builder.put(id, base); + invertedBuilder.put(base, id); } + + invertedBuilder.put(base, base); } + + INVERTED_MAPPINGS = invertedBuilder.build(); MAPPINGS = builder.build(); } @@ -75,4 +86,15 @@ public class ItemVariationMapping { return MAPPINGS.getOrDefault(itemId, itemId); } + + /** + * Get item ids for provided variation item id. + * + * @param itemId the item id + * @return the item ids + */ + public static Collection getVariations(int itemId) + { + return INVERTED_MAPPINGS.asMap().getOrDefault(itemId, Collections.singletonList(itemId)); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java index e0a795e1aa..26e2612955 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java @@ -27,12 +27,15 @@ package net.runelite.client.plugins.banktags; import com.google.common.collect.Lists; +import com.google.common.primitives.Shorts; import com.google.inject.Provides; import java.awt.event.KeyEvent; import java.awt.event.MouseWheelEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -50,6 +53,7 @@ import net.runelite.client.events.ConfigChanged; import net.runelite.api.events.DraggingWidgetChanged; import net.runelite.api.events.FocusChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.GrandExchangeSearched; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.ScriptCallbackEvent; @@ -62,6 +66,7 @@ import net.runelite.client.callback.ClientThread; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemVariationMapping; import net.runelite.client.game.SpriteManager; import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.input.KeyListener; @@ -93,6 +98,8 @@ public class BankTagsPlugin extends Plugin implements MouseWheelListener, KeyLis public static final String TAG_TABS_CONFIG = "tagtabs"; public static final String VAR_TAG_SUFFIX = "*"; + private static final int MAX_RESULT_COUNT = 250; + private static final String SEARCH_BANK_INPUT_TEXT = "Show items whose names or tags contain the following text:
" + "(To show only tagged items, start your search with 'tag:')"; @@ -248,6 +255,33 @@ public class BankTagsPlugin extends Plugin implements MouseWheelListener, KeyLis shiftPressed = false; } + @Subscribe + public void onGrandExchangeSearched(GrandExchangeSearched event) + { + final String input = client.getVar(VarClientStr.INPUT_TEXT); + if (!input.startsWith(TAG_SEARCH)) + { + return; + } + + event.consume(); + + final String tag = input.substring(TAG_SEARCH.length()).trim(); + final Set ids = tagManager.getItemsForTag(tag) + .stream() + .mapToInt(Math::abs) + .mapToObj(ItemVariationMapping::getVariations) + .flatMap(Collection::stream) + .distinct() + .filter(i -> itemManager.getItemComposition(i).isTradeable()) + .limit(MAX_RESULT_COUNT) + .collect(Collectors.toCollection(TreeSet::new)); + + client.setGeSearchResultIndex(0); + client.setGeSearchResultCount(ids.size()); + client.setGeSearchResultIds(Shorts.toArray(ids)); + } + @Subscribe public void onScriptCallbackEvent(ScriptCallbackEvent event) { diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/banktags/BankTagsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/banktags/BankTagsPluginTest.java new file mode 100644 index 0000000000..18198c9530 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/banktags/BankTagsPluginTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, Ron Young + * 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.banktags; + +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import javax.inject.Inject; +import net.runelite.api.Client; +import static net.runelite.api.ItemID.ABYSSAL_WHIP; +import net.runelite.api.events.ScriptCallbackEvent; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.banktags.tabs.TabInterface; +import net.runelite.client.plugins.cluescrolls.ClueScrollService; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BankTagsPluginTest +{ + @Mock + @Bind + private Client client; + + @Mock + @Bind + private ItemManager itemManager; + + @Mock + @Bind + private BankTagsConfig bankTagsConfig; + + @Mock + @Bind + private RuneLiteConfig runeLiteConfig; + + @Mock + @Bind + private TabInterface tabInterface; + + @Mock + @Bind + private ClueScrollService clueScrollService; + + @Mock + @Bind + private ConfigManager configManager; + + @Inject + private TagManager tagManager; + + @Inject + private BankTagsPlugin bankTagsPlugin; + + private final ScriptCallbackEvent EVENT = new ScriptCallbackEvent(); + + @Before + public void before() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + + EVENT.setEventName("bankSearchFilter"); + + when(itemManager.canonicalize(ABYSSAL_WHIP)).thenReturn(ABYSSAL_WHIP); + when(client.getIntStackSize()).thenReturn(2); + when(client.getStringStackSize()).thenReturn(1); + } + + @Test + public void testExplicitSearch() + { + when(client.getIntStack()).thenReturn(new int[]{0, ABYSSAL_WHIP}); + when(client.getStringStack()).thenReturn(new String[]{"tag:whip"}); + + when(configManager.getConfiguration(BankTagsPlugin.CONFIG_GROUP, + TagManager.ITEM_KEY_PREFIX + ABYSSAL_WHIP)).thenReturn("herb,bossing,whip"); + bankTagsPlugin.onScriptCallbackEvent(EVENT); + assertEquals(1, client.getIntStack()[0]); + + // Search should be found at the start of the tag + when(client.getIntStack()).thenReturn(new int[]{0, ABYSSAL_WHIP}); + when(configManager.getConfiguration(BankTagsPlugin.CONFIG_GROUP, + TagManager.ITEM_KEY_PREFIX + ABYSSAL_WHIP)).thenReturn("herb,bossing,whip long tag"); + bankTagsPlugin.onScriptCallbackEvent(EVENT); + assertEquals(1, client.getIntStack()[0]); + + // Search should not be be found in the middle of the tag + // and explicit search does not allow fall through + when(configManager.getConfiguration(BankTagsPlugin.CONFIG_GROUP, + TagManager.ITEM_KEY_PREFIX + ABYSSAL_WHIP)).thenReturn("herb,bossing whip"); + bankTagsPlugin.onScriptCallbackEvent(EVENT); + assertEquals(0, client.getIntStack()[0]); + } + + @Test + public void testFallThrough() + { + when(client.getIntStack()).thenReturn(new int[]{1, ABYSSAL_WHIP}); + when(client.getStringStack()).thenReturn(new String[]{"whip"}); + + when(configManager.getConfiguration(BankTagsPlugin.CONFIG_GROUP, + TagManager.ITEM_KEY_PREFIX + ABYSSAL_WHIP)).thenReturn("herb,bossing"); + + assertFalse(tagManager.findTag(ABYSSAL_WHIP, "whip")); + bankTagsPlugin.onScriptCallbackEvent(EVENT); + assertEquals(1, client.getIntStack()[0]); + } + + @Test + public void testNonExplicitSearch() + { + when(client.getIntStack()).thenReturn(new int[]{0, ABYSSAL_WHIP}); + when(client.getStringStack()).thenReturn(new String[]{"whip"}); + + when(configManager.getConfiguration(BankTagsPlugin.CONFIG_GROUP, + TagManager.ITEM_KEY_PREFIX + ABYSSAL_WHIP)).thenReturn("herb,bossing,whip long tag"); + + bankTagsPlugin.onScriptCallbackEvent(EVENT); + assertEquals(1, client.getIntStack()[0]); + } +}