ge plugin: add fuzzy search option
This commit is contained in:
@@ -222,6 +222,12 @@ public final class ScriptID
|
|||||||
@ScriptArguments(integer = 15)
|
@ScriptArguments(integer = 15)
|
||||||
public static final int GE_OFFERS_SETUP_BUILD = 779;
|
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
|
* Builds the quest list inside the quest tab that shows each quest's progress
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ public class WidgetID
|
|||||||
static final int CONTAINER = 40;
|
static final int CONTAINER = 40;
|
||||||
static final int TITLE = 44;
|
static final int TITLE = 44;
|
||||||
static final int FULL_INPUT = 45;
|
static final int FULL_INPUT = 45;
|
||||||
|
static final int GE_SEARCH_RESULTS = 53;
|
||||||
static final int MESSAGES = 55;
|
static final int MESSAGES = 55;
|
||||||
static final int TRANSPARENT_BACKGROUND_LINES = 56;
|
static final int TRANSPARENT_BACKGROUND_LINES = 56;
|
||||||
static final int INPUT = 57;
|
static final int INPUT = 57;
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ public enum WidgetInfo
|
|||||||
CHATBOX_BUTTONS(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.BUTTONS),
|
CHATBOX_BUTTONS(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.BUTTONS),
|
||||||
CHATBOX_TITLE(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.TITLE),
|
CHATBOX_TITLE(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.TITLE),
|
||||||
CHATBOX_FULL_INPUT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.FULL_INPUT),
|
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_CONTAINER(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.CONTAINER),
|
||||||
CHATBOX_REPORT_TEXT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.REPORT_TEXT),
|
CHATBOX_REPORT_TEXT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.REPORT_TEXT),
|
||||||
CHATBOX_INPUT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.INPUT),
|
CHATBOX_INPUT(WidgetID.CHATBOX_GROUP_ID, WidgetID.Chatbox.INPUT),
|
||||||
|
|||||||
@@ -96,4 +96,26 @@ public interface GrandExchangeConfig extends Config
|
|||||||
{
|
{
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Copyright (c) 2019, Adam <Adam@sigterm.info>
|
* Copyright (c) 2019, Adam <Adam@sigterm.info>
|
||||||
* Copyright (c) 2017, Robbie <https://github.com/rbbi>
|
* Copyright (c) 2017, Robbie <https://github.com/rbbi>
|
||||||
* Copyright (c) 2018, SomeoneWithAnInternetConnection
|
* Copyright (c) 2018, SomeoneWithAnInternetConnection
|
||||||
|
* Copyright (c) 2020, Dennis <me@dennis.dev>
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
@@ -27,13 +28,23 @@
|
|||||||
|
|
||||||
package net.runelite.client.plugins.grandexchange;
|
package net.runelite.client.plugins.grandexchange;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Shorts;
|
||||||
import com.google.common.reflect.TypeToken;
|
import com.google.common.reflect.TypeToken;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
|
import java.awt.Color;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
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.Map;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
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.inject.Inject;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
@@ -49,10 +60,12 @@ import net.runelite.api.ItemComposition;
|
|||||||
import net.runelite.api.MenuAction;
|
import net.runelite.api.MenuAction;
|
||||||
import net.runelite.api.MenuEntry;
|
import net.runelite.api.MenuEntry;
|
||||||
import net.runelite.api.ScriptID;
|
import net.runelite.api.ScriptID;
|
||||||
|
import net.runelite.api.VarClientStr;
|
||||||
import net.runelite.api.events.ChatMessage;
|
import net.runelite.api.events.ChatMessage;
|
||||||
import net.runelite.api.events.FocusChanged;
|
import net.runelite.api.events.FocusChanged;
|
||||||
import net.runelite.api.events.GameStateChanged;
|
import net.runelite.api.events.GameStateChanged;
|
||||||
import net.runelite.api.events.GrandExchangeOfferChanged;
|
import net.runelite.api.events.GrandExchangeOfferChanged;
|
||||||
|
import net.runelite.api.events.GrandExchangeSearched;
|
||||||
import net.runelite.api.events.MenuEntryAdded;
|
import net.runelite.api.events.MenuEntryAdded;
|
||||||
import net.runelite.api.events.ScriptCallbackEvent;
|
import net.runelite.api.events.ScriptCallbackEvent;
|
||||||
import net.runelite.api.events.ScriptPostFired;
|
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.plugins.PluginDescriptor;
|
||||||
import net.runelite.client.ui.ClientToolbar;
|
import net.runelite.client.ui.ClientToolbar;
|
||||||
import net.runelite.client.ui.NavigationButton;
|
import net.runelite.client.ui.NavigationButton;
|
||||||
|
import net.runelite.client.util.ColorUtil;
|
||||||
import net.runelite.client.util.ImageUtil;
|
import net.runelite.client.util.ImageUtil;
|
||||||
import net.runelite.client.util.QuantityFormatter;
|
import net.runelite.client.util.QuantityFormatter;
|
||||||
import net.runelite.client.util.Text;
|
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.item.ItemStats;
|
||||||
import net.runelite.http.api.osbuddy.OSBGrandExchangeClient;
|
import net.runelite.http.api.osbuddy.OSBGrandExchangeClient;
|
||||||
import net.runelite.http.api.osbuddy.OSBGrandExchangeResult;
|
import net.runelite.http.api.osbuddy.OSBGrandExchangeResult;
|
||||||
|
import org.apache.commons.text.similarity.FuzzyScore;
|
||||||
|
|
||||||
@PluginDescriptor(
|
@PluginDescriptor(
|
||||||
name = "Grand Exchange",
|
name = "Grand Exchange",
|
||||||
@@ -105,6 +120,12 @@ public class GrandExchangePlugin extends Plugin
|
|||||||
|
|
||||||
static final String SEARCH_GRAND_EXCHANGE = "Search Grand Exchange";
|
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)
|
@Getter(AccessLevel.PACKAGE)
|
||||||
private NavigationButton button;
|
private NavigationButton button;
|
||||||
|
|
||||||
@@ -156,6 +177,48 @@ public class GrandExchangePlugin extends Plugin
|
|||||||
|
|
||||||
private GrandExchangeClient grandExchangeClient;
|
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)
|
private SavedOffer getOffer(int slot)
|
||||||
{
|
{
|
||||||
String offer = configManager.getConfiguration("geoffer." + client.getUsername().toLowerCase(), Integer.toString(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)
|
public void onScriptPostFired(ScriptPostFired event)
|
||||||
{
|
{
|
||||||
// GE offers setup init
|
// 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;
|
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
|
@Subscribe
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user