ge plugin: add fuzzy search option

This commit is contained in:
Dennis
2020-04-02 18:27:48 +02:00
committed by GitHub
parent 8aad85d768
commit b81caff06f
6 changed files with 249 additions and 2 deletions

View File

@@ -222,6 +222,12 @@ public final class ScriptID
@ScriptArguments(integer = 15)
public static final int GE_OFFERS_SETUP_BUILD = 779;
/**
* Builds the grand exchange item search widget
*/
@ScriptArguments(integer = 2)
public static final int GE_ITEM_SEARCH = 752;
/**
* Builds the quest list inside the quest tab that shows each quest's progress
*/

View File

@@ -466,6 +466,7 @@ public class WidgetID
static final int CONTAINER = 40;
static final int TITLE = 44;
static final int FULL_INPUT = 45;
static final int GE_SEARCH_RESULTS = 53;
static final int MESSAGES = 55;
static final int TRANSPARENT_BACKGROUND_LINES = 56;
static final int INPUT = 57;

View File

@@ -351,6 +351,7 @@ public enum WidgetInfo
CHATBOX_BUTTONS(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.BUTTONS),
CHATBOX_TITLE(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.TITLE),
CHATBOX_FULL_INPUT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.FULL_INPUT),
CHATBOX_GE_SEARCH_RESULTS(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.GE_SEARCH_RESULTS),
CHATBOX_CONTAINER(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.CONTAINER),
CHATBOX_REPORT_TEXT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.REPORT_TEXT),
CHATBOX_INPUT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.INPUT),

View File

@@ -96,4 +96,26 @@ public interface GrandExchangeConfig extends Config
{
return false;
}
@ConfigItem(
position = 7,
keyName = "highlightSearchMatch",
name = "Highlight Search Match",
description = "Highlights the search match with an underline"
)
default boolean highlightSearchMatch()
{
return true;
}
@ConfigItem(
position = 8,
keyName = "geSearchMode",
name = "Search Mode",
description = "The search mode to use for the GE"
)
default GrandExchangeSearchMode geSearchMode()
{
return GrandExchangeSearchMode.DEFAULT;
}
}

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2019, Adam <Adam@sigterm.info>
* Copyright (c) 2017, Robbie <https://github.com/rbbi>
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2020, Dennis <me@dennis.dev>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -27,13 +28,23 @@
package net.runelite.client.plugins.grandexchange;
import com.google.common.primitives.Shorts;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.inject.Inject;
import javax.swing.SwingUtilities;
import lombok.AccessLevel;
@@ -49,10 +60,12 @@ import net.runelite.api.ItemComposition;
import net.runelite.api.MenuAction;
import net.runelite.api.MenuEntry;
import net.runelite.api.ScriptID;
import net.runelite.api.VarClientStr;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.FocusChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GrandExchangeOfferChanged;
import net.runelite.api.events.GrandExchangeSearched;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.api.events.ScriptPostFired;
@@ -75,6 +88,7 @@ import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.QuantityFormatter;
import net.runelite.client.util.Text;
@@ -83,6 +97,7 @@ import net.runelite.http.api.ge.GrandExchangeTrade;
import net.runelite.http.api.item.ItemStats;
import net.runelite.http.api.osbuddy.OSBGrandExchangeClient;
import net.runelite.http.api.osbuddy.OSBGrandExchangeResult;
import org.apache.commons.text.similarity.FuzzyScore;
@PluginDescriptor(
name = "Grand Exchange",
@@ -105,6 +120,12 @@ public class GrandExchangePlugin extends Plugin
static final String SEARCH_GRAND_EXCHANGE = "Search Grand Exchange";
private static final int MAX_RESULT_COUNT = 250;
private static final FuzzyScore FUZZY = new FuzzyScore(Locale.ENGLISH);
private static final Color FUZZY_HIGHLIGHT_COLOR = new Color(0x800000);
@Getter(AccessLevel.PACKAGE)
private NavigationButton button;
@@ -156,6 +177,48 @@ public class GrandExchangePlugin extends Plugin
private GrandExchangeClient grandExchangeClient;
private boolean wasFuzzySearch;
/**
* Logic from {@link org.apache.commons.text.similarity.FuzzyScore}
*/
private static List<Integer> findFuzzyIndices(String term, String query)
{
List<Integer> indices = new ArrayList<>();
// fuzzy logic is case insensitive. We normalize the Strings to lower
// case right from the start. Turning characters to lower case
// via Character.toLowerCase(char) is unfortunately insufficient
// as it does not accept a locale.
final String termLowerCase = term.toLowerCase();
final String queryLowerCase = query.toLowerCase();
// the position in the term which will be scanned next for potential
// query character matches
int termIndex = 0;
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++)
{
final char queryChar = queryLowerCase.charAt(queryIndex);
for (; termIndex < termLowerCase.length(); termIndex++)
{
final char termChar = termLowerCase.charAt(termIndex);
if (queryChar == termChar)
{
indices.add(termIndex);
// we can leave the nested loop. Every character in the
// query can match at most one character in the term.
break;
}
}
}
return indices;
}
private SavedOffer getOffer(int slot)
{
String offer = configManager.getConfiguration("geoffer." + client.getUsername().toLowerCase(), Integer.toString(slot));
@@ -441,11 +504,131 @@ public class GrandExchangePlugin extends Plugin
public void onScriptPostFired(ScriptPostFired event)
{
// GE offers setup init
if (event.getScriptId() != ScriptID.GE_OFFERS_SETUP_BUILD)
if (event.getScriptId() == ScriptID.GE_OFFERS_SETUP_BUILD)
{
rebuildGeText();
}
else if (event.getScriptId() == ScriptID.GE_ITEM_SEARCH && config.highlightSearchMatch())
{
highlightSearchMatches();
}
}
private void highlightSearchMatches()
{
if (!wasFuzzySearch)
{
return;
}
rebuildGeText();
String input = client.getVar(VarClientStr.INPUT_TEXT);
String underlineTag = "<u=" + ColorUtil.colorToHexCode(FUZZY_HIGHLIGHT_COLOR) + ">";
Widget results = client.getWidget(WidgetInfo.CHATBOX_GE_SEARCH_RESULTS);
Widget[] children = results.getDynamicChildren();
int resultCount = children.length / 3;
for (int i = 0; i < resultCount; i++)
{
Widget itemNameWidget = children[i * 3 + 1];
String itemName = itemNameWidget.getText();
List<Integer> indices;
String otherName = itemName.replace('-', ' ');
if (!itemName.contains("-") || FUZZY.fuzzyScore(itemName, input) >= FUZZY.fuzzyScore(otherName, input))
{
indices = findFuzzyIndices(itemName, input);
}
else
{
indices = findFuzzyIndices(otherName, input);
}
Collections.reverse(indices);
StringBuilder newItemName = new StringBuilder(itemName);
for (int index : indices)
{
if (wasFuzzySearch && (itemName.charAt(index) == ' ' || itemName.charAt(index) == '-'))
{
continue;
}
newItemName.insert(index + 1, "</u>");
newItemName.insert(index, underlineTag);
}
itemNameWidget.setText(newItemName.toString());
}
}
@Subscribe
public void onGrandExchangeSearched(GrandExchangeSearched event)
{
wasFuzzySearch = false;
GrandExchangeSearchMode searchMode = config.geSearchMode();
final String input = client.getVar(VarClientStr.INPUT_TEXT);
if (searchMode == GrandExchangeSearchMode.DEFAULT || input.isEmpty())
{
return;
}
event.consume();
client.setGeSearchResultIndex(0);
int resultCount = 0;
if (searchMode == GrandExchangeSearchMode.FUZZY_FALLBACK)
{
List<Integer> ids = IntStream.range(0, client.getItemCount())
.mapToObj(itemManager::getItemComposition)
.filter(item -> item.isTradeable() && item.getNote() == -1
&& item.getName().toLowerCase().contains(input))
.limit(MAX_RESULT_COUNT + 1)
.sorted(Comparator.comparing(ItemComposition::getName))
.map(ItemComposition::getId)
.collect(Collectors.toList());
if (ids.size() > MAX_RESULT_COUNT)
{
client.setGeSearchResultCount(-1);
client.setGeSearchResultIds(null);
}
else
{
resultCount = ids.size();
client.setGeSearchResultCount(resultCount);
client.setGeSearchResultIds(Shorts.toArray(ids));
}
}
if (resultCount == 0)
{
// We do this so that for example the items "Anti-venom ..." are still at the top
// when searching "anti venom"
ToIntFunction<ItemComposition> getScore = item ->
{
int score = FUZZY.fuzzyScore(item.getName(), input);
if (item.getName().contains("-"))
{
return Math.max(FUZZY.fuzzyScore(item.getName().replace('-', ' '), input), score);
}
return score;
};
List<Integer> ids = IntStream.range(0, client.getItemCount())
.mapToObj(itemManager::getItemComposition)
.filter(item -> item.isTradeable() && item.getNote() == -1)
.filter(item -> getScore.applyAsInt(item) > 0)
.sorted(Comparator.comparingInt(getScore).reversed()
.thenComparing(ItemComposition::getName))
.limit(MAX_RESULT_COUNT)
.map(ItemComposition::getId)
.collect(Collectors.toList());
client.setGeSearchResultCount(ids.size());
client.setGeSearchResultIds(Shorts.toArray(ids));
wasFuzzySearch = true;
}
}
@Subscribe

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2020, Dennis <me@dennis.dev>
* 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.grandexchange;
public enum GrandExchangeSearchMode
{
DEFAULT,
FUZZY_FALLBACK,
FUZZY_ONLY
}