From 0ac809fae2f406f0ffcccbf6540284f8260300b0 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 11 May 2018 13:24:28 -0400 Subject: [PATCH 1/2] runelite-api: expose friends --- .../main/java/net/runelite/api/Client.java | 2 ++ .../net/runelite/mixins/RSClientMixin.java | 24 +++++++++++++++ .../runelite/rs/api/RSFriendContainer.java | 29 +++++++++++++++++++ .../net/runelite/rs/api/RSFriendManager.java | 3 ++ 4 files changed, 58 insertions(+) create mode 100644 runescape-api/src/main/java/net/runelite/rs/api/RSFriendContainer.java 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 0a8aefef12..b86203a0f6 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -303,6 +303,8 @@ public interface Client extends GameEngine ClanMember[] getClanMembers(); + Friend[] getFriends(); + boolean isClanMember(String name); Preferences getPreferences(); diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java index 91f3a51c7e..625a2eafcf 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java @@ -29,6 +29,7 @@ import java.util.List; import javax.annotation.Nullable; import net.runelite.api.ChatMessageType; import net.runelite.api.ClanMember; +import net.runelite.api.Friend; import net.runelite.api.GameState; import net.runelite.api.GrandExchangeOffer; import net.runelite.api.GraphicsObject; @@ -91,11 +92,14 @@ import static net.runelite.client.callback.Hooks.eventBus; import net.runelite.rs.api.RSClanMemberManager; import net.runelite.rs.api.RSClient; import net.runelite.rs.api.RSDeque; +import net.runelite.rs.api.RSFriendContainer; +import net.runelite.rs.api.RSFriendManager; import net.runelite.rs.api.RSHashTable; import net.runelite.rs.api.RSIndexedSprite; import net.runelite.rs.api.RSItemContainer; import net.runelite.rs.api.RSNPC; import net.runelite.rs.api.RSName; +import net.runelite.rs.api.RSNameable; import net.runelite.rs.api.RSPlayer; import net.runelite.rs.api.RSWidget; @@ -576,6 +580,26 @@ public abstract class RSClientMixin implements RSClient return clanMemberManager != null ? (ClanMember[]) getClanMemberManager().getNameables() : null; } + @Inject + @Override + public Friend[] getFriends() + { + final RSFriendManager friendManager = getFriendManager(); + if (friendManager == null) + { + return null; + } + + final RSFriendContainer friendContainer = friendManager.getFriendContainer(); + if (friendContainer == null) + { + return null; + } + + RSNameable[] nameables = friendContainer.getNameables(); + return (Friend[]) nameables; + } + @Inject @Override public boolean isClanMember(String name) diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSFriendContainer.java b/runescape-api/src/main/java/net/runelite/rs/api/RSFriendContainer.java new file mode 100644 index 0000000000..165730c323 --- /dev/null +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSFriendContainer.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018, Adam + * 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.rs.api; + +public interface RSFriendContainer extends RSNameableContainer +{ +} diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSFriendManager.java b/runescape-api/src/main/java/net/runelite/rs/api/RSFriendManager.java index 2538d02d10..f066564128 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSFriendManager.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSFriendManager.java @@ -29,6 +29,9 @@ import net.runelite.mapping.Import; public interface RSFriendManager extends FriendManager { + @Import("friendContainer") + RSFriendContainer getFriendContainer(); + @Import("isFriended") boolean isFriended(RSName var1, boolean var2); } From e92b37829bc5b320c69e0f99c70d374770abedb1 Mon Sep 17 00:00:00 2001 From: pettenge Date: Wed, 9 May 2018 16:30:37 -0400 Subject: [PATCH 2/2] hiscore plugin: autocomplete name lookup based on friends, clan members, and players runelite/runelite#785 --- .../client/plugins/hiscore/HiscoreConfig.java | 13 +- .../client/plugins/hiscore/HiscorePanel.java | 11 + .../client/plugins/hiscore/HiscorePlugin.java | 20 ++ .../plugins/hiscore/NameAutocompleter.java | 267 ++++++++++++++++++ 4 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/hiscore/NameAutocompleter.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscoreConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscoreConfig.java index fe086ae8e0..8c300289b9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscoreConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscoreConfig.java @@ -67,4 +67,15 @@ public interface HiscoreConfig extends Config { return true; } -} + + @ConfigItem( + position = 4, + keyName = "autocomplete", + name = "Autocomplete", + description = "Predict names when typing a name to lookup" + ) + default boolean autocomplete() + { + return true; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java index 889058b681..948b2dd8ee 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java @@ -33,6 +33,7 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; +import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; @@ -343,6 +344,16 @@ public class HiscorePanel extends PluginPanel add(endpointPanel); } + void addInputKeyListener(KeyListener l) + { + this.input.addKeyListener(l); + } + + void removeInputKeyListener(KeyListener l) + { + this.input.removeKeyListener(l); + } + private void changeDetail(String skillName, HiscoreSkill skill) { if (result == null || result.getPlayer() == null) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java index ef56ee671c..499a8777e6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePlugin.java @@ -81,6 +81,9 @@ public class HiscorePlugin extends Plugin private NavigationButton navButton; private HiscorePanel hiscorePanel; + @Inject + private NameAutocompleter autocompleter; + @Provides HiscoreConfig provideConfig(ConfigManager configManager) { @@ -110,11 +113,16 @@ public class HiscorePlugin extends Plugin { menuManager.addPlayerMenuItem(LOOKUP); } + if (config.autocomplete()) + { + hiscorePanel.addInputKeyListener(autocompleter); + } } @Override protected void shutDown() throws Exception { + hiscorePanel.removeInputKeyListener(autocompleter); pluginToolbar.removeNavigation(navButton); menuManager.removePlayerMenuItem(LOOKUP); } @@ -130,6 +138,18 @@ public class HiscorePlugin extends Plugin { menuManager.addPlayerMenuItem(LOOKUP); } + + if (event.getKey().equals("autocomplete")) + { + if (config.autocomplete()) + { + hiscorePanel.addInputKeyListener(autocompleter); + } + else + { + hiscorePanel.removeInputKeyListener(autocompleter); + } + } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/NameAutocompleter.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/NameAutocompleter.java new file mode 100644 index 0000000000..f5a4cba504 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/NameAutocompleter.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2018, John Pettenger + * 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.hiscore; + +import com.google.inject.Inject; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ClanMember; +import net.runelite.api.Client; +import net.runelite.api.Friend; +import net.runelite.api.Player; + +@Slf4j +class NameAutocompleter implements KeyListener +{ + /** + * Non-breaking space character. + */ + private static final String NBSP = Character.toString((char)160); + + /** + * Character class for characters that cannot be in an RSN. + */ + private static final Pattern INVALID_CHARS = Pattern.compile("[^a-zA-Z0-9_ -]"); + + private final Client client; + + /** + * The name currently being autocompleted. + */ + private String autocompleteName; + + /** + * Pattern for the name currently being autocompleted. + */ + private Pattern autocompleteNamePattern; + + @Inject + private NameAutocompleter(@Nullable Client client) + { + this.client = client; + } + + @Override + public void keyPressed(KeyEvent e) + { + + } + + @Override + public void keyReleased(KeyEvent e) + { + + } + + @Override + public void keyTyped(KeyEvent e) + { + final JTextComponent input = (JTextComponent)e.getSource(); + final String inputText = input.getText(); + + // Only autocomplete if the selection end is at the end of the text. + if (input.getSelectionEnd() != inputText.length()) + { + return; + } + + // Character to be inserted at the selection start. + final String charToInsert = Character.toString(e.getKeyChar()); + + // Don't attempt to autocomplete if the name is invalid. + // This condition is also true when the user presses a key like backspace. + if (INVALID_CHARS.matcher(charToInsert).find() + || INVALID_CHARS.matcher(inputText).find()) + { + return; + } + + // Check if we are already autocompleting. + if (autocompleteName != null && autocompleteNamePattern.matcher(inputText).matches()) + { + if (isExpectedNext(input, charToInsert)) + { + final int insertIndex = input.getSelectionStart(); + SwingUtilities.invokeLater(() -> + { + try + { + // Insert the character and move the selection. + Document doc = input.getDocument(); + doc.remove(insertIndex, 1); + doc.insertString(insertIndex, charToInsert, null); + input.select(insertIndex + 1, input.getSelectionEnd()); + } + catch (BadLocationException ex) + { + log.warn("Could not insert character.", ex); + } + }); + // Prevent default behavior. + e.consume(); + } + else // Character to insert does not match current autocompletion. Look for another name. + { + newAutocomplete(e); + } + } + else // Search for a name to autocomplete + { + newAutocomplete(e); + } + } + + private void newAutocomplete(KeyEvent e) + { + final JTextComponent input = (JTextComponent)e.getSource(); + final String inputText = input.getText(); + final String nameStart = inputText.substring(0, input.getSelectionStart()) + e.getKeyChar(); + + if (findAutocompleteName(nameStart)) + { + // Assert this.autocompleteName != null + final String name = this.autocompleteName; + SwingUtilities.invokeLater(() -> + { + try + { + input.getDocument().insertString( + nameStart.length(), + name.substring(nameStart.length()), + null); + input.select(nameStart.length(), name.length()); + } + catch (BadLocationException ex) + { + log.warn("Could not autocomplete name.", ex); + } + }); + } + } + + private boolean findAutocompleteName(String nameStart) + { + final Pattern pattern; + Optional autocompleteName; + + // Pattern to match names that start with nameStart. + // Allows spaces to be represented as common whitespaces, underscores, + // hyphens, or non-breaking spaces. + // Matching non-breaking spaces is necessary because the API + // returns non-breaking spaces when a name has whitespace. + pattern = Pattern.compile( + "(?i)^" + nameStart.replaceAll("[ _-]", "[ _" + NBSP + "-]") + ".+?"); + + if (client == null) + { + return false; + } + + autocompleteName = Optional.empty(); + + // TODO: Search lookup history + + Friend[] friends = client.getFriends(); + if (friends != null) + { + autocompleteName = Arrays.stream(friends) + .filter(Objects::nonNull) + .map(Friend::getName) + .filter(n -> pattern.matcher(n).matches()) + .findFirst(); + } + + // Search clan if a friend wasn't found + if (!autocompleteName.isPresent()) + { + final ClanMember[] clannies = client.getClanMembers(); + if (clannies != null) + { + autocompleteName = Arrays.stream(clannies) + .filter(Objects::nonNull) + .map(ClanMember::getUsername) + .filter(n -> pattern.matcher(n).matches()) + .findFirst(); + } + } + + // Search cached players if a clannie wasn't found. + if (!autocompleteName.isPresent()) + { + final Player[] cachedPlayers = client.getCachedPlayers(); + autocompleteName = Arrays.stream(cachedPlayers) + .filter(Objects::nonNull) + .map(Player::getName) + .filter(n -> pattern.matcher(n).matches()) + .findFirst(); + } + + if (autocompleteName.isPresent()) + { + this.autocompleteName = autocompleteName.get().replace(NBSP, " "); + this.autocompleteNamePattern = Pattern.compile( + "(?i)^" + this.autocompleteName.replaceAll("[ _-]", "[ _-]") + "$"); + } + else + { + this.autocompleteName = null; + this.autocompleteNamePattern = null; + } + + return autocompleteName.isPresent(); + } + + private boolean isExpectedNext(JTextComponent input, String nextChar) + { + String expected; + if (input.getSelectionStart() < input.getSelectionEnd()) + { + try + { + expected = input.getText(input.getSelectionStart(), 1); + } + catch (BadLocationException ex) + { + log.warn("Could not get first character from input selection.", ex); + return false; + } + } + else + { + expected = ""; + } + return nextChar.equalsIgnoreCase(expected); + } +} \ No newline at end of file