ge plugin: add fuzzy search option
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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