diff --git a/runelite-api/src/main/java/net/runelite/api/ScriptID.java b/runelite-api/src/main/java/net/runelite/api/ScriptID.java index 1fb85b0145..494489fadc 100644 --- a/runelite-api/src/main/java/net/runelite/api/ScriptID.java +++ b/runelite-api/src/main/java/net/runelite/api/ScriptID.java @@ -233,4 +233,36 @@ public final class ScriptID */ @ScriptArguments(integer = 2) public static final int TOPLEVEL_RESIZE = 909; + + /** + * Called when the friends list is updated + * + */ + @ScriptArguments(integer = 9) + public static final int FRIENDS_UPDATE = 631; + + /** + * Called when the ignore list is updated + * + */ + @ScriptArguments(integer = 7) + public static final int IGNORE_UPDATE = 630; } diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java index 859ea90bda..d957b3f8d5 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java @@ -215,11 +215,27 @@ public class WidgetID static class FriendList { static final int TITLE = 3; + static final int FULL_CONTAINER = 5; + static final int SORT_BY_NAME_BUTTON = 7; + static final int SORT_BY_LAST_WORLD_CHANGE_BUTTON = 8; + static final int SORT_BY_WORLD_BUTTON = 9; + static final int LEGACY_SORT_BUTTON = 10; + static final int NAMES_CONTAINER = 11; + static final int SCROLL_BAR = 12; + static final int LOADING_TEXT = 13; + static final int PREVIOUS_NAME_HOLDER = 18; } static class IgnoreList { static final int TITLE = 3; + static final int FULL_CONTAINER = 5; + static final int SORT_BY_NAME_BUTTON = 7; + static final int LEGACY_SORT_BUTTON = 8; + static final int NAMES_CONTAINER = 9; + static final int SCROLL_BAR = 10; + static final int LOADING_TEXT = 11; + static final int PREVIOUS_NAME_HOLDER = 16; } static class ClanChat diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java index dec5f89cf6..ea559f8d37 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java @@ -104,8 +104,24 @@ public enum WidgetInfo VOLCANIC_MINE_VENT_C_STATUS(WidgetID.VOLCANIC_MINE_GROUP_ID, WidgetID.VolcanicMine.VENT_C_STATUS), FRIEND_CHAT_TITLE(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.TITLE), + FRIEND_LIST_FULL_CONTAINER(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.FULL_CONTAINER), + FRIEND_LIST_SORT_BY_NAME_BUTTON(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.SORT_BY_NAME_BUTTON), + FRIEND_LIST_SORT_BY_LAST_WORLD_CHANGE_BUTTON(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.SORT_BY_LAST_WORLD_CHANGE_BUTTON), + FRIEND_LIST_SORT_BY_WORLD_BUTTON(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.SORT_BY_WORLD_BUTTON), + FRIEND_LIST_LEGACY_SORT_BUTTON(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.LEGACY_SORT_BUTTON), + FRIEND_LIST_NAMES_CONTAINER(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.NAMES_CONTAINER), + FRIEND_LIST_SCROLL_BAR(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.SCROLL_BAR), + FRIEND_LIST_LOADING_TEXT(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.LOADING_TEXT), + FRIEND_LIST_PREVIOUS_NAME_HOLDER(WidgetID.FRIENDS_LIST_GROUP_ID, WidgetID.FriendList.PREVIOUS_NAME_HOLDER), IGNORE_TITLE(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.TITLE), + IGNORE_FULL_CONTAINER(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.FULL_CONTAINER), + IGNORE_SORT_BY_NAME_BUTTON(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.SORT_BY_NAME_BUTTON), + IGNORE_LEGACY_SORT_BUTTON(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.LEGACY_SORT_BUTTON), + IGNORE_NAMES_CONTAINER(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.NAMES_CONTAINER), + IGNORE_SCROLL_BAR(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.SCROLL_BAR), + IGNORE_LOADING_TEXT(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.LOADING_TEXT), + IGNORE_PREVIOUS_NAME_HOLDER(WidgetID.IGNORE_LIST_GROUP_ID, WidgetID.IgnoreList.PREVIOUS_NAME_HOLDER), EXPLORERS_RING_ALCH_INVENTORY(WidgetID.EXPLORERS_RING_ALCH_GROUP_ID, WidgetID.ExplorersRing.INVENTORY), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesConfig.java new file mode 100644 index 0000000000..abaa5915f7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, ThatGamerBlue + * 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 HOLDER 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.friendnotes; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup(FriendNotesPlugin.CONFIG_GROUP) +public interface FriendNotesConfig extends Config +{ + @ConfigItem( + keyName = "showIcons", + name = "Show Icons", + description = "Show icons on friend or ignore list", + position = 1 + ) + default boolean showIcons() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java index ba0d85cff7..94fe830cc1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/friendnotes/FriendNotesPlugin.java @@ -29,29 +29,40 @@ package net.runelite.client.plugins.friendnotes; import com.google.common.base.Strings; import com.google.common.collect.ObjectArrays; +import com.google.inject.Provides; import java.awt.Color; +import java.awt.image.BufferedImage; +import java.util.Arrays; import javax.annotation.Nullable; import javax.inject.Inject; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.Friend; +import net.runelite.api.GameState; import net.runelite.api.Ignore; +import net.runelite.api.IndexedSprite; import net.runelite.api.MenuAction; import net.runelite.api.MenuEntry; import net.runelite.api.Nameable; +import net.runelite.api.ScriptID; +import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.NameableNameChanged; import net.runelite.api.events.RemovedFriend; +import net.runelite.api.events.ScriptCallbackEvent; import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.callback.ClientThread; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.ImageUtil; import net.runelite.client.util.Text; @Slf4j @@ -61,13 +72,15 @@ import net.runelite.client.util.Text; ) public class FriendNotesPlugin extends Plugin { - private static final String CONFIG_GROUP = "friendNotes"; + static final String CONFIG_GROUP = "friendNotes"; private static final int CHARACTER_LIMIT = 128; private static final String KEY_PREFIX = "note_"; private static final String ADD_NOTE = "Add Note"; private static final String EDIT_NOTE = "Edit Note"; private static final String NOTE_PROMPT_FORMAT = "%s's Notes
" + ColorUtil.prependColorTag("(Limit %s Characters)", new Color(0, 0, 170)); + private static final int ICON_WIDTH = 14; + private static final int ICON_HEIGHT = 12; @Inject private Client client; @@ -84,19 +97,74 @@ public class FriendNotesPlugin extends Plugin @Inject private ChatboxPanelManager chatboxPanelManager; + @Inject + private ClientThread clientThread; + + @Inject + private FriendNotesConfig config; + @Getter private HoveredFriend hoveredFriend = null; + private int iconIdx = -1; + private String currentlyLayouting; + + @Provides + private FriendNotesConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(FriendNotesConfig.class); + } + @Override protected void startUp() throws Exception { overlayManager.add(overlay); + clientThread.invoke(this::loadIcon); + if (client.getGameState() == GameState.LOGGED_IN) + { + rebuildFriendsList(); + rebuildIgnoreList(); + } } @Override protected void shutDown() throws Exception { overlayManager.remove(overlay); + if (client.getGameState() == GameState.LOGGED_IN) + { + rebuildFriendsList(); + rebuildIgnoreList(); + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOGGED_IN) + { + loadIcon(); + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!event.getGroup().equals(CONFIG_GROUP)) + { + return; + } + + switch (event.getKey()) + { + case "showIcons": + if (client.getGameState() == GameState.LOGGED_IN) + { + rebuildFriendsList(); + rebuildIgnoreList(); + } + break; + } } /** @@ -112,6 +180,11 @@ public class FriendNotesPlugin extends Plugin { configManager.setConfiguration(CONFIG_GROUP, KEY_PREFIX + displayName, note); } + if (client.getGameState() == GameState.LOGGED_IN) + { + rebuildFriendsList(); + rebuildIgnoreList(); + } } /** @@ -258,4 +331,104 @@ public class FriendNotesPlugin extends Plugin log.debug("Remove friend: '{}'", displayName); setFriendNote(displayName, null); } + + @Subscribe + public void onScriptCallbackEvent(ScriptCallbackEvent event) + { + if (!config.showIcons() || iconIdx == -1) + { + return; + } + + switch (event.getEventName()) + { + case "friend_cc_settext": + case "ignore_cc_settext": + String[] stringStack = client.getStringStack(); + int stringStackSize = client.getStringStackSize(); + final String rsn = stringStack[stringStackSize - 1]; + final String sanitized = Text.toJagexName(Text.removeTags(rsn)); + currentlyLayouting = sanitized; + if (getFriendNote(sanitized) != null) + { + stringStack[stringStackSize - 1] = rsn + " "; + } + break; + case "friend_cc_setposition": + case "ignore_cc_setposition": + if (currentlyLayouting == null || getFriendNote(currentlyLayouting) == null) + { + return; + } + + int[] intStack = client.getIntStack(); + int intStackSize = client.getIntStackSize(); + int xpos = intStack[intStackSize - 4]; + xpos += ICON_WIDTH + 1; + intStack[intStackSize - 4] = xpos; + break; + } + } + + private void rebuildFriendsList() + { + clientThread.invokeLater(() -> + { + log.debug("Rebuilding friends list"); + client.runScript( + ScriptID.FRIENDS_UPDATE, + WidgetInfo.FRIEND_LIST_FULL_CONTAINER.getPackedId(), + WidgetInfo.FRIEND_LIST_SORT_BY_NAME_BUTTON.getPackedId(), + WidgetInfo.FRIEND_LIST_SORT_BY_LAST_WORLD_CHANGE_BUTTON.getPackedId(), + WidgetInfo.FRIEND_LIST_SORT_BY_WORLD_BUTTON.getPackedId(), + WidgetInfo.FRIEND_LIST_LEGACY_SORT_BUTTON.getPackedId(), + WidgetInfo.FRIEND_LIST_NAMES_CONTAINER.getPackedId(), + WidgetInfo.FRIEND_LIST_SCROLL_BAR.getPackedId(), + WidgetInfo.FRIEND_LIST_LOADING_TEXT.getPackedId(), + WidgetInfo.FRIEND_LIST_PREVIOUS_NAME_HOLDER.getPackedId() + ); + }); + } + + private void rebuildIgnoreList() + { + clientThread.invokeLater(() -> + { + log.debug("Rebuilding ignore list"); + client.runScript( + ScriptID.IGNORE_UPDATE, + WidgetInfo.IGNORE_FULL_CONTAINER.getPackedId(), + WidgetInfo.IGNORE_SORT_BY_NAME_BUTTON.getPackedId(), + WidgetInfo.IGNORE_LEGACY_SORT_BUTTON.getPackedId(), + WidgetInfo.IGNORE_NAMES_CONTAINER.getPackedId(), + WidgetInfo.IGNORE_SCROLL_BAR.getPackedId(), + WidgetInfo.IGNORE_LOADING_TEXT.getPackedId(), + WidgetInfo.IGNORE_PREVIOUS_NAME_HOLDER.getPackedId() + ); + }); + } + + private void loadIcon() + { + final IndexedSprite[] modIcons = client.getModIcons(); + if (iconIdx != -1 || modIcons == null) + { + return; + } + + final BufferedImage iconImg = ImageUtil.getResourceStreamFromClass(getClass(), "note_icon.png"); + if (iconImg == null) + { + return; + } + + final BufferedImage resized = ImageUtil.resizeImage(iconImg, ICON_WIDTH, ICON_HEIGHT); + + final IndexedSprite[] newIcons = Arrays.copyOf(modIcons, modIcons.length + 1); + newIcons[newIcons.length - 1] = ImageUtil.getImageIndexedSprite(resized, client); + + iconIdx = newIcons.length - 1; + client.setModIcons(newIcons); + } + } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/friendnotes/note_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/friendnotes/note_icon.png new file mode 100644 index 0000000000..9b2625aeb7 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/friendnotes/note_icon.png differ diff --git a/runelite-client/src/main/scripts/FriendUpdate.rs2asm b/runelite-client/src/main/scripts/FriendUpdate.rs2asm index e2d838bd66..69cb250b31 100644 --- a/runelite-client/src/main/scripts/FriendUpdate.rs2asm +++ b/runelite-client/src/main/scripts/FriendUpdate.rs2asm @@ -3,6 +3,12 @@ .string_stack_count 0 .int_var_count 16 .string_var_count 2 +; callback "friend_cc_settext" +; Fired just before the client pops the name off the stack +; Modified by the friendnotes plugin to show the icon +; callback "friend_cc_setposition" +; Fired just before the client sets the position of "friend changed their name" icon +; Modified by the friendnotes plugin to offset the name changed icon iload 1 iconst 2 iconst 3 @@ -276,6 +282,8 @@ LABEL218: add istore 10 sload 0 + sconst "friend_cc_settext" + runelite_callback cc_settext iconst 0 iload 13 @@ -356,6 +364,8 @@ LABEL277: add iconst 0 iconst 0 + sconst "friend_cc_setposition" + runelite_callback cc_setposition 1 iconst 1093 cc_setgraphic 1 diff --git a/runelite-client/src/main/scripts/IgnoreUpdate.rs2asm b/runelite-client/src/main/scripts/IgnoreUpdate.rs2asm index c50a07f093..a99a72a2f1 100644 --- a/runelite-client/src/main/scripts/IgnoreUpdate.rs2asm +++ b/runelite-client/src/main/scripts/IgnoreUpdate.rs2asm @@ -3,6 +3,12 @@ .string_stack_count 0 .int_var_count 13 .string_var_count 2 +; callback "ignore_cc_settext" +; Fired just before the client pops the name off the stack +; Modified by the friendnotes plugin to show the icon +; callback "ignore_cc_setposition" +; Fired just before the client sets the position of "ignored person changed their name" icon +; Modified by the friendnotes plugin to offset the name changed icon iload 1 iconst 2 iconst 3 @@ -133,6 +139,8 @@ LABEL101: add istore 8 sload 0 + sconst "ignore_cc_settext" + runelite_callback cc_settext iconst 0 iload 10 @@ -190,6 +198,8 @@ LABEL101: add iconst 0 iconst 0 + sconst "ignore_cc_setposition" + runelite_callback cc_setposition 1 iconst 1093 cc_setgraphic 1