/* * Copyright (c) 2018, Rheon <https://github.com/Rheon-D> * Copyright (c) 2018, Adam <[email protected]> * 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 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 @PluginDescriptor( name = "Friend Notes", description = "Store notes about your friends" ) public class FriendNotesPlugin extends Plugin { 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<br>" + 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; @Inject private ConfigManager configManager; @Inject private OverlayManager overlayManager; @Inject private FriendNoteOverlay overlay; @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; } } /** * Set a friend note, or unset by passing a null/empty note. */ private void setFriendNote(String displayName, String note) { if (Strings.isNullOrEmpty(note)) { configManager.unsetConfiguration(CONFIG_GROUP, KEY_PREFIX + displayName); } else { configManager.setConfiguration(CONFIG_GROUP, KEY_PREFIX + displayName, note); } if (client.getGameState() == GameState.LOGGED_IN) { rebuildFriendsList(); rebuildIgnoreList(); } } /** * Get the friend note of a display name, or null if no friend note exists for it. */ @Nullable private String getFriendNote(String displayName) { return configManager.getConfiguration(CONFIG_GROUP, KEY_PREFIX + displayName); } /** * Migrate a friend note to a new display name, and remove the previous one. * If current name already has a note, or previous name had none, do nothing. */ private void migrateFriendNote(String currentDisplayName, String prevDisplayName) { final String currentNote = getFriendNote(currentDisplayName); if (currentNote == null) { final String prevNote = getFriendNote(prevDisplayName); if (prevNote != null) { log.debug("Update friend's username: '{}' -> '{}'", prevDisplayName, currentDisplayName); setFriendNote(prevDisplayName, null); setFriendNote(currentDisplayName, prevNote); } } } /** * Set the currently hovered display name, if a friend note exists for it. */ private void setHoveredFriend(String displayName) { hoveredFriend = null; if (!Strings.isNullOrEmpty(displayName)) { final String note = getFriendNote(displayName); if (note != null) { hoveredFriend = new HoveredFriend(displayName, note); } } } @Subscribe public void onMenuEntryAdded(MenuEntryAdded event) { final int groupId = WidgetInfo.TO_GROUP(event.getActionParam1()); // Look for "Message" on friends list if ((groupId == WidgetInfo.FRIENDS_LIST.getGroupId() && event.getOption().equals("Message")) || (groupId == WidgetInfo.IGNORE_LIST.getGroupId() && event.getOption().equals("Delete"))) { // Friends have color tags setHoveredFriend(Text.toJagexName(Text.removeTags(event.getTarget()))); // Build "Add Note" or "Edit Note" menu entry final MenuEntry addNote = new MenuEntry(); addNote.setOption(hoveredFriend == null || hoveredFriend.getNote() == null ? ADD_NOTE : EDIT_NOTE); addNote.setType(MenuAction.RUNELITE.getId()); addNote.setTarget(event.getTarget()); //Preserve color codes here addNote.setParam0(event.getActionParam0()); addNote.setParam1(event.getActionParam1()); // Add menu entry final MenuEntry[] menuEntries = ObjectArrays.concat(client.getMenuEntries(), addNote); client.setMenuEntries(menuEntries); } else if (hoveredFriend != null) { hoveredFriend = null; } } @Subscribe public void onMenuOptionClicked(MenuOptionClicked event) { final int groupId = WidgetInfo.TO_GROUP(event.getWidgetId()); if (groupId == WidgetInfo.FRIENDS_LIST.getGroupId() || groupId == WidgetInfo.IGNORE_LIST.getGroupId()) { if (Strings.isNullOrEmpty(event.getMenuTarget())) { return; } // Handle clicks on "Add Note" or "Edit Note" if (event.getMenuOption().equals(ADD_NOTE) || event.getMenuOption().equals(EDIT_NOTE)) { event.consume(); //Friends have color tags final String sanitizedTarget = Text.toJagexName(Text.removeTags(event.getMenuTarget())); final String note = getFriendNote(sanitizedTarget); // Open the new chatbox input dialog chatboxPanelManager.openTextInput(String.format(NOTE_PROMPT_FORMAT, sanitizedTarget, CHARACTER_LIMIT)) .value(Strings.nullToEmpty(note)) .onDone((content) -> { if (content == null) { return; } content = Text.removeTags(content).trim(); log.debug("Set note for '{}': '{}'", sanitizedTarget, content); setFriendNote(sanitizedTarget, content); }).build(); } } } @Subscribe public void onNameableNameChanged(NameableNameChanged event) { final Nameable nameable = event.getNameable(); if (nameable instanceof Friend || nameable instanceof Ignore) { // Migrate a friend's note to their new display name String name = nameable.getName(); String prevName = nameable.getPrevName(); if (prevName != null) { migrateFriendNote( Text.toJagexName(name), Text.toJagexName(prevName) ); } } } @Subscribe public void onRemovedFriend(RemovedFriend event) { // Delete a friend's note if they are removed final String displayName = Text.toJagexName(event.getNameable().getName()); 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 + " <img=" + iconIdx + ">"; } 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); } }