diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java index ffacca5af2..48804f3ced 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java @@ -1,439 +1,461 @@ -/* - * Copyright (c) 2017, Aria - * 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.grounditems; - -import java.awt.Color; -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItem; -import net.runelite.client.plugins.grounditems.config.ItemHighlightMode; -import net.runelite.client.plugins.grounditems.config.MenuHighlightMode; -import net.runelite.client.plugins.grounditems.config.PriceDisplayMode; -import net.runelite.client.plugins.grounditems.config.TimerDisplayMode; -import net.runelite.client.plugins.grounditems.config.ValueCalculationMode; - -@ConfigGroup("grounditems") -public interface GroundItemsConfig extends Config -{ - @ConfigItem( - keyName = "highlightedItems", - name = "Highlighted Items", - description = "Configures specifically highlighted ground items. Format: (item), (item)", - position = 0 - ) - default String getHighlightItems() - { - return ""; - } - - @ConfigItem( - keyName = "highlightedItems", - name = "", - description = "" - ) - void setHighlightedItem(String key); - - @ConfigItem( - keyName = "hiddenItems", - name = "Hidden Items", - description = "Configures hidden ground items. Format: (item), (item)", - position = 1 - ) - default String getHiddenItems() - { - return "Vial, Ashes, Coins, Bones, Bucket, Jug, Seaweed"; - } - - @ConfigItem( - keyName = "hiddenItems", - name = "", - description = "" - ) - void setHiddenItems(String key); - - @ConfigItem( - keyName = "showHighlightedOnly", - name = "Show Highlighted items only", - description = "Configures whether or not to draw items only on your highlighted list", - position = 2 - ) - default boolean showHighlightedOnly() - { - return false; - } - - @ConfigItem( - keyName = "dontHideUntradeables", - name = "Do not hide untradeables", - description = "Configures whether or not untradeable items ignore hiding under settings", - position = 3 - ) - default boolean dontHideUntradeables() - { - return true; - } - - @ConfigItem( - keyName = "showMenuItemQuantities", - name = "Show Menu Item Quantities", - description = "Configures whether or not to show the item quantities in the menu", - position = 4 - ) - default boolean showMenuItemQuantities() - { - return true; - } - - @ConfigItem( - keyName = "recolorMenuHiddenItems", - name = "Recolor Menu Hidden Items", - description = "Configures whether or not hidden items in right click menu will be recolored", - position = 5 - ) - default boolean recolorMenuHiddenItems() - { - return false; - } - - @ConfigItem( - keyName = "highlightTiles", - name = "Highlight Tiles", - description = "Configures whether or not to highlight tiles containing ground items", - position = 6 - ) - default boolean highlightTiles() - { - return false; - } - - @ConfigItem( - keyName = "notifyHighlightedDrops", - name = "Notify for Highlighted drops", - description = "Configures whether or not to notify for drops on your highlighted list", - position = 7 - ) - default boolean notifyHighlightedDrops() - { - return false; - } - - @ConfigItem( - keyName = "priceDisplayMode", - name = "Price Display Mode", - description = "Configures what price types are shown alongside of ground item name", - position = 8 - ) - default PriceDisplayMode priceDisplayMode() - { - return PriceDisplayMode.BOTH; - } - - @ConfigItem( - keyName = "itemHighlightMode", - name = "Item Highlight Mode", - description = "Configures how ground items will be highlighted", - position = 9 - ) - default ItemHighlightMode itemHighlightMode() - { - return ItemHighlightMode.BOTH; - } - - @ConfigItem( - keyName = "menuHighlightMode", - name = "Menu Highlight Mode", - description = "Configures what to highlight in right-click menu", - position = 10 - ) - default MenuHighlightMode menuHighlightMode() - { - return MenuHighlightMode.NAME; - } - - @ConfigItem( - keyName = "highlightValueCalculation", - name = "Highlight Value Calculation", - description = "Configures which coin value is used to determine highlight color", - position = 11 - ) - default ValueCalculationMode valueCalculationMode() - { - return ValueCalculationMode.HIGHEST; - } - - @ConfigItem( - keyName = "highlightOverValue2", - name = "Highlight > Value", - description = "Configures highlighted ground items over either GE or HA value", - position = 12 - ) - default int getHighlightOverValue() - { - return 0; - } - - @ConfigItem( - keyName = "hideUnderValue", - name = "Hide < Value", - description = "Configures hidden ground items under both GE and HA value", - position = 13 - ) - default int getHideUnderValue() - { - return 0; - } - - @ConfigItem( - keyName = "defaultColor", - name = "Default items color", - description = "Configures the color for default, non-highlighted items", - position = 14 - ) - default Color defaultColor() - { - return Color.WHITE; - } - - @ConfigItem( - keyName = "highlightedColor", - name = "Highlighted items color", - description = "Configures the color for highlighted items", - position = 15 - ) - default Color highlightedColor() - { - return Color.decode("#AA00FF"); - } - - @ConfigItem( - keyName = "hiddenColor", - name = "Hidden items color", - description = "Configures the color for hidden items in right-click menu and when holding ALT", - position = 16 - ) - default Color hiddenColor() - { - return Color.GRAY; - } - - @ConfigItem( - keyName = "lowValueColor", - name = "Low value items color", - description = "Configures the color for low value items", - position = 17 - ) - default Color lowValueColor() - { - return Color.decode("#66B2FF"); - } - - @ConfigItem( - keyName = "lowValuePrice", - name = "Low value price", - description = "Configures the start price for low value items", - position = 18 - ) - default int lowValuePrice() - { - return 20000; - } - - @ConfigItem( - keyName = "notifyLowValueDrops", - name = "Notify for low value drops", - description = "Configures whether or not to notify for drops of low value", - position = 19 - ) - default boolean notifyLowValueDrops() - { - return false; - } - - @ConfigItem( - keyName = "mediumValueColor", - name = "Medium value items color", - description = "Configures the color for medium value items", - position = 20 - ) - default Color mediumValueColor() - { - return Color.decode("#99FF99"); - } - - @ConfigItem( - keyName = "mediumValuePrice", - name = "Medium value price", - description = "Configures the start price for medium value items", - position = 21 - ) - default int mediumValuePrice() - { - return 100000; - } - - @ConfigItem( - keyName = "notifyMediumValueDrops", - name = "Notify for medium value drops", - description = "Configures whether or not to notify for drops of medium value", - position = 22 - ) - default boolean notifyMediumValueDrops() - { - return false; - } - - @ConfigItem( - keyName = "highValueColor", - name = "High value items color", - description = "Configures the color for high value items", - position = 23 - ) - default Color highValueColor() - { - return Color.decode("#FF9600"); - } - - @ConfigItem( - keyName = "highValuePrice", - name = "High value price", - description = "Configures the start price for high value items", - position = 24 - ) - default int highValuePrice() - { - return 1000000; - } - - @ConfigItem( - keyName = "notifyHighValueDrops", - name = "Notify for high value drops", - description = "Configures whether or not to notify for drops of high value", - position = 25 - ) - default boolean notifyHighValueDrops() - { - return false; - } - - @ConfigItem( - keyName = "insaneValueColor", - name = "Insane value items color", - description = "Configures the color for insane value items", - position = 26 - ) - default Color insaneValueColor() - { - return Color.decode("#FF66B2"); - } - - @ConfigItem( - keyName = "insaneValuePrice", - name = "Insane value price", - description = "Configures the start price for insane value items", - position = 27 - ) - default int insaneValuePrice() - { - return 10000000; - } - - @ConfigItem( - keyName = "notifyInsaneValueDrops", - name = "Notify for insane value drops", - description = "Configures whether or not to notify for drops of insane value", - position = 28 - ) - default boolean notifyInsaneValueDrops() - { - return false; - } - - @ConfigItem( - keyName = "onlyShowLoot", - name = "Only show loot", - description = "Only shows drops from NPCs and players", - position = 29 - ) - default boolean onlyShowLoot() - { - return false; - } - - @ConfigItem( - keyName = "doubleTapDelay", - name = "Delay for double-tap ALT to hide", - description = "Decrease this number if you accidentally hide ground items often. (0 = Disabled)", - position = 30 - ) - default int doubleTapDelay() - { - return 250; - } - - @ConfigItem( - keyName = "collapseEntries", - name = "Collapse ground item menu entries", - description = "Collapses ground item menu entries together and appends count", - position = 31 - ) - default boolean collapseEntries() - { - return false; - } - - @ConfigItem( - keyName = "removeIgnored", - name = "Hide Ignored", - description = "Remove take option for items that are on the hidden items list.", - position = 32 - ) - default boolean removeIgnored() - { - return false; - } - - @ConfigItem( - keyName = "toggleOutline", - name = "Text Outline", - description = "Use an outline around text instead of a text shadow", - position = 33 - ) - default boolean toggleOutline() - { - return false; - } - - @ConfigItem( - keyName = "showGroundItemDuration", - name = "Show time remaining", - description = "Turn on a countdown timer to show how long an item will remain on the ground", - position = 34 - ) - default TimerDisplayMode showGroundItemDuration() - { - return TimerDisplayMode.HOTKEY_PRESSED; - } -} +/* + * Copyright (c) 2017, Aria + * 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.grounditems; + +import java.awt.Color; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.plugins.grounditems.config.ItemHighlightMode; +import net.runelite.client.plugins.grounditems.config.MenuHighlightMode; +import net.runelite.client.plugins.grounditems.config.PriceDisplayMode; +import net.runelite.client.plugins.grounditems.config.TimerDisplayMode; +import net.runelite.client.plugins.grounditems.config.ValueCalculationMode; + +@ConfigGroup("grounditems") +public interface GroundItemsConfig extends Config +{ + @ConfigItem( + keyName = "highlightedItems", + name = "Highlighted Items", + description = "Configures specifically highlighted ground items. Format: (item), (item)", + position = 0 + ) + default String getHighlightItems() + { + return ""; + } + + @ConfigItem( + keyName = "highlightedItems", + name = "", + description = "" + ) + void setHighlightedItem(String key); + + @ConfigItem( + keyName = "hiddenItems", + name = "Hidden Items", + description = "Configures hidden ground items. Format: (item), (item)", + position = 1 + ) + default String getHiddenItems() + { + return "Vial, Ashes, Coins, Bones, Bucket, Jug, Seaweed"; + } + + @ConfigItem( + keyName = "hiddenItems", + name = "", + description = "" + ) + void setHiddenItems(String key); + + @ConfigItem( + keyName = "showHighlightedOnly", + name = "Show Highlighted items only", + description = "Configures whether or not to draw items only on your highlighted list", + position = 2 + ) + default boolean showHighlightedOnly() + { + return false; + } + + @ConfigItem( + keyName = "dontHideUntradeables", + name = "Do not hide untradeables", + description = "Configures whether or not untradeable items ignore hiding under settings", + position = 3 + ) + default boolean dontHideUntradeables() + { + return true; + } + + @ConfigItem( + keyName = "showMenuItemQuantities", + name = "Show Menu Item Quantities", + description = "Configures whether or not to show the item quantities in the menu", + position = 4 + ) + default boolean showMenuItemQuantities() + { + return true; + } + + @ConfigItem( + keyName = "recolorMenuHiddenItems", + name = "Recolor Menu Hidden Items", + description = "Configures whether or not hidden items in right click menu will be recolored", + position = 5 + ) + default boolean recolorMenuHiddenItems() + { + return false; + } + + @ConfigItem( + keyName = "highlightTiles", + name = "Highlight Tiles", + description = "Configures whether or not to highlight tiles containing ground items", + position = 6 + ) + default boolean highlightTiles() + { + return false; + } + + @ConfigItem( + keyName = "notifyHighlightedDrops", + name = "Notify for Highlighted drops", + description = "Configures whether or not to notify for drops on your highlighted list", + position = 7 + ) + default boolean notifyHighlightedDrops() + { + return false; + } + + @ConfigItem( + keyName = "priceDisplayMode", + name = "Price Display Mode", + description = "Configures what price types are shown alongside of ground item name", + position = 8 + ) + default PriceDisplayMode priceDisplayMode() + { + return PriceDisplayMode.BOTH; + } + + @ConfigItem( + keyName = "itemHighlightMode", + name = "Item Highlight Mode", + description = "Configures how ground items will be highlighted", + position = 9 + ) + default ItemHighlightMode itemHighlightMode() + { + return ItemHighlightMode.BOTH; + } + + @ConfigItem( + keyName = "menuHighlightMode", + name = "Menu Highlight Mode", + description = "Configures what to highlight in right-click menu", + position = 10 + ) + default MenuHighlightMode menuHighlightMode() + { + return MenuHighlightMode.NAME; + } + + @ConfigItem( + keyName = "highlightValueCalculation", + name = "Highlight Value Calculation", + description = "Configures which coin value is used to determine highlight color", + position = 11 + ) + default ValueCalculationMode valueCalculationMode() + { + return ValueCalculationMode.HIGHEST; + } + + @ConfigItem( + keyName = "highlightOverValue2", + name = "Highlight > Value", + description = "Configures highlighted ground items over either GE or HA value", + position = 12 + ) + default int getHighlightOverValue() + { + return 0; + } + + @ConfigItem( + keyName = "hideUnderValue", + name = "Hide < Value", + description = "Configures hidden ground items under both GE and HA value", + position = 13 + ) + default int getHideUnderValue() + { + return 0; + } + + @ConfigItem( + keyName = "defaultColor", + name = "Default items color", + description = "Configures the color for default, non-highlighted items", + position = 14 + ) + default Color defaultColor() + { + return Color.WHITE; + } + + @ConfigItem( + keyName = "highlightedColor", + name = "Highlighted items color", + description = "Configures the color for highlighted items", + position = 15 + ) + default Color highlightedColor() + { + return Color.decode("#AA00FF"); + } + + @ConfigItem( + keyName = "hiddenColor", + name = "Hidden items color", + description = "Configures the color for hidden items in right-click menu and when holding ALT", + position = 16 + ) + default Color hiddenColor() + { + return Color.GRAY; + } + + @ConfigItem( + keyName = "lowValueColor", + name = "Low value items color", + description = "Configures the color for low value items", + position = 17 + ) + default Color lowValueColor() + { + return Color.decode("#66B2FF"); + } + + @ConfigItem( + keyName = "lowValuePrice", + name = "Low value price", + description = "Configures the start price for low value items", + position = 18 + ) + default int lowValuePrice() + { + return 20000; + } + + @ConfigItem( + keyName = "notifyLowValueDrops", + name = "Notify for low value drops", + description = "Configures whether or not to notify for drops of low value", + position = 19 + ) + default boolean notifyLowValueDrops() + { + return false; + } + + @ConfigItem( + keyName = "mediumValueColor", + name = "Medium value items color", + description = "Configures the color for medium value items", + position = 20 + ) + default Color mediumValueColor() + { + return Color.decode("#99FF99"); + } + + @ConfigItem( + keyName = "mediumValuePrice", + name = "Medium value price", + description = "Configures the start price for medium value items", + position = 21 + ) + default int mediumValuePrice() + { + return 100000; + } + + @ConfigItem( + keyName = "notifyMediumValueDrops", + name = "Notify for medium value drops", + description = "Configures whether or not to notify for drops of medium value", + position = 22 + ) + default boolean notifyMediumValueDrops() + { + return false; + } + + @ConfigItem( + keyName = "highValueColor", + name = "High value items color", + description = "Configures the color for high value items", + position = 23 + ) + default Color highValueColor() + { + return Color.decode("#FF9600"); + } + + @ConfigItem( + keyName = "highValuePrice", + name = "High value price", + description = "Configures the start price for high value items", + position = 24 + ) + default int highValuePrice() + { + return 1000000; + } + + @ConfigItem( + keyName = "notifyHighValueDrops", + name = "Notify for high value drops", + description = "Configures whether or not to notify for drops of high value", + position = 25 + ) + default boolean notifyHighValueDrops() + { + return false; + } + + @ConfigItem( + keyName = "insaneValueColor", + name = "Insane value items color", + description = "Configures the color for insane value items", + position = 26 + ) + default Color insaneValueColor() + { + return Color.decode("#FF66B2"); + } + + @ConfigItem( + keyName = "insaneValuePrice", + name = "Insane value price", + description = "Configures the start price for insane value items", + position = 27 + ) + default int insaneValuePrice() + { + return 10000000; + } + + @ConfigItem( + keyName = "notifyInsaneValueDrops", + name = "Notify for insane value drops", + description = "Configures whether or not to notify for drops of insane value", + position = 28 + ) + default boolean notifyInsaneValueDrops() + { + return false; + } + + @ConfigItem( + keyName = "onlyShowLoot", + name = "Only show loot", + description = "Only shows drops from NPCs and players", + position = 29 + ) + default boolean onlyShowLoot() + { + return false; + } + + @ConfigItem( + keyName = "doubleTapDelay", + name = "Delay for double-tap ALT to hide", + description = "Decrease this number if you accidentally hide ground items often. (0 = Disabled)", + position = 30 + ) + default int doubleTapDelay() + { + return 250; + } + + @ConfigItem( + keyName = "collapseEntries", + name = "Collapse ground item menu entries", + description = "Collapses ground item menu entries together and appends count", + position = 31 + ) + default boolean collapseEntries() + { + return false; + } + + @ConfigItem( + keyName = "removeIgnored", + name = "Hide Ignored", + description = "Remove take option for items that are on the hidden items list.", + position = 32 + ) + default boolean removeIgnored() + { + return false; + } + + @ConfigItem( + keyName = "toggleOutline", + name = "Text Outline", + description = "Use an outline around text instead of a text shadow", + position = 33 + ) + default boolean toggleOutline() + { + return false; + } + + @ConfigItem( + keyName = "showGroundItemDuration", + name = "Show time remaining", + description = "Turn on a countdown timer to show how long an item will remain on the ground", + position = 34 + ) + default TimerDisplayMode showGroundItemDuration() + { + return TimerDisplayMode.HOTKEY_PRESSED; + } + + @ConfigItem( + keyName = "sortByGEPrice", + name = "Sort by GE price", + description = "Sorts ground items by GE price, instead of alch value", + position = 35 + ) + default boolean sortByGEPrice() + { + return false; + } + + @ConfigItem( + keyName = "rightClickHidden", + name = "Right click hidden items", + description = "Places hidden items below the 'Walk here' option, making it so that you need to right click to pick them up", + position = 36 + ) + default boolean rightClickHidden() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java index 024d9455b1..ac7f8b2f75 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java @@ -1,475 +1,483 @@ -/* - * Copyright (c) 2017, Aria - * 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.client.plugins.grounditems; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FontMetrics; -import java.awt.Graphics2D; -import java.awt.Polygon; -import java.awt.Rectangle; -import java.time.Duration; -import java.time.Instant; -import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import javax.inject.Inject; -import net.runelite.api.Client; -import net.runelite.api.Perspective; -import net.runelite.api.Player; -import net.runelite.api.Point; -import net.runelite.api.Tile; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldPoint; -import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.MENU; -import net.runelite.client.plugins.grounditems.config.PriceDisplayMode; -import net.runelite.client.plugins.grounditems.config.TimerDisplayMode; -import net.runelite.client.ui.overlay.Overlay; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPosition; -import net.runelite.client.ui.overlay.OverlayUtil; -import net.runelite.client.ui.overlay.components.BackgroundComponent; -import net.runelite.client.ui.overlay.components.ProgressPieComponent; -import net.runelite.client.ui.overlay.components.TextComponent; -import net.runelite.client.util.StackFormatter; - -public class GroundItemsOverlay extends Overlay -{ - private static final int MAX_DISTANCE = 2500; - // We must offset the text on the z-axis such that - // it doesn't obscure the ground items below it. - private static final int OFFSET_Z = 20; - // The game won't send anything higher than this value to the plugin - - // so we replace any item quantity higher with "Lots" instead. - private static final int MAX_QUANTITY = 65535; - // The 15 pixel gap between each drawn ground item. - private static final int STRING_GAP = 15; - // Size of the hidden/highlight boxes - private static final int RECTANGLE_SIZE = 8; - - private static final int TIMER_OVERLAY_DIAMETER = 10; - private static final int PUBLIC_ITEM_DURATION_MILLIS = 60000; - private static final float WARNING_THRESHOLD = 0.25f; - private static final Color PUBLIC_TIMER_COLOR = Color.YELLOW; - private static final Color PRIVATE_TIMER_COLOR = Color.GREEN; - private static final Color PUBLIC_WARNING_TIMER_COLOR = Color.RED; - - private final Client client; - private final GroundItemsPlugin plugin; - private final GroundItemsConfig config; - private final StringBuilder itemStringBuilder = new StringBuilder(); - private final BackgroundComponent backgroundComponent = new BackgroundComponent(); - private final TextComponent textComponent = new TextComponent(); - private final ProgressPieComponent progressPieComponent = new ProgressPieComponent(); - private final Map offsetMap = new HashMap<>(); - - @Inject - private GroundItemsOverlay(Client client, GroundItemsPlugin plugin, GroundItemsConfig config) - { - setPosition(OverlayPosition.DYNAMIC); - setLayer(OverlayLayer.ABOVE_SCENE); - this.client = client; - this.plugin = plugin; - this.config = config; - } - - @Override - public Dimension render(Graphics2D graphics) - { - final boolean dontShowOverlay = (config.itemHighlightMode() == MENU || plugin.isHideAll()) && !plugin.isHotKeyPressed(); - - if (dontShowOverlay && !config.highlightTiles()) - { - return null; - } - - final FontMetrics fm = graphics.getFontMetrics(); - final Player player = client.getLocalPlayer(); - - if (player == null || client.getViewportWidget() == null) - { - return null; - } - - offsetMap.clear(); - final LocalPoint localLocation = player.getLocalLocation(); - final Point mousePos = client.getMouseCanvasPosition(); - Collection groundItemList = GroundItemsPlugin.getCollectedGroundItems().values(); - GroundItem topGroundItem = null; - - if (plugin.isHotKeyPressed()) - { - // Make copy of ground items because we are going to modify them here, and the array list supports our - // desired behaviour here - groundItemList = new ArrayList<>(groundItemList); - final java.awt.Point awtMousePos = new java.awt.Point(mousePos.getX(), mousePos.getY()); - GroundItem groundItem = null; - - for (GroundItem item : groundItemList) - { - item.setOffset(offsetMap.compute(item.getLocation(), (k, v) -> v != null ? v + 1 : 0)); - - if (groundItem != null) - { - continue; - } - - if (plugin.getTextBoxBounds() != null - && item.equals(plugin.getTextBoxBounds().getValue()) - && plugin.getTextBoxBounds().getKey().contains(awtMousePos)) - { - groundItem = item; - continue; - } - - if (plugin.getHiddenBoxBounds() != null - && item.equals(plugin.getHiddenBoxBounds().getValue()) - && plugin.getHiddenBoxBounds().getKey().contains(awtMousePos)) - { - groundItem = item; - continue; - } - - if (plugin.getHighlightBoxBounds() != null - && item.equals(plugin.getHighlightBoxBounds().getValue()) - && plugin.getHighlightBoxBounds().getKey().contains(awtMousePos)) - { - groundItem = item; - } - } - - if (groundItem != null) - { - groundItemList.remove(groundItem); - groundItemList.add(groundItem); - topGroundItem = groundItem; - } - } - - plugin.setTextBoxBounds(null); - plugin.setHiddenBoxBounds(null); - plugin.setHighlightBoxBounds(null); - - final boolean onlyShowLoot = config.onlyShowLoot(); - - for (GroundItem item : groundItemList) - { - final LocalPoint groundPoint = LocalPoint.fromWorld(client, item.getLocation()); - - if (groundPoint == null || localLocation.distanceTo(groundPoint) > MAX_DISTANCE - || (onlyShowLoot && !item.isMine())) - { - continue; - } - - final Color highlighted = plugin.getHighlighted(item.getName(), item.getGePrice(), item.getHaPrice()); - final Color hidden = plugin.getHidden(item.getName(), item.getGePrice(), item.getHaPrice(), item.isTradeable()); - - if (highlighted == null && !plugin.isHotKeyPressed()) - { - // Do not display hidden items - if (hidden != null) - { - continue; - } - - // Do not display non-highlighted items - if (config.showHighlightedOnly()) - { - continue; - } - } - - final Color color = plugin.getItemColor(highlighted, hidden); - - if (config.highlightTiles()) - { - final Polygon poly = Perspective.getCanvasTilePoly(client, groundPoint); - - if (poly != null) - { - OverlayUtil.renderPolygon(graphics, poly, color); - } - } - - if (dontShowOverlay) - { - continue; - } - - itemStringBuilder.append(item.getName()); - - if (item.getQuantity() > 1) - { - if (item.getQuantity() >= MAX_QUANTITY) - { - itemStringBuilder.append(" (Lots!)"); - } - else - { - itemStringBuilder.append(" (") - .append(StackFormatter.quantityToStackSize(item.getQuantity())) - .append(")"); - } - } - - if (config.priceDisplayMode() == PriceDisplayMode.BOTH) - { - if (item.getGePrice() > 0) - { - itemStringBuilder.append(" (EX: ") - .append(StackFormatter.quantityToStackSize(item.getGePrice())) - .append(" gp)"); - } - - if (item.getHaPrice() > 0) - { - itemStringBuilder.append(" (HA: ") - .append(StackFormatter.quantityToStackSize(item.getHaPrice())) - .append(" gp)"); - } - } - else if (config.priceDisplayMode() != PriceDisplayMode.OFF) - { - final int price = config.priceDisplayMode() == PriceDisplayMode.GE - ? item.getGePrice() - : item.getHaPrice(); - - if (price > 0) - { - itemStringBuilder - .append(" (") - .append(StackFormatter.quantityToStackSize(price)) - .append(" gp)"); - } - } - - final String itemString = itemStringBuilder.toString(); - itemStringBuilder.setLength(0); - - if (item.getHeight() == -1) - { - final Tile[][][] sceneTiles = client.getScene().getTiles(); - final Tile itemTile = sceneTiles[client.getPlane()][groundPoint.getSceneX()][groundPoint.getSceneY()]; - item.setHeight(itemTile.getItemLayer().getHeight()); - } - - final Point textPoint = Perspective.getCanvasTextLocation(client, - graphics, - groundPoint, - itemString, - item.getHeight() + OFFSET_Z); - - if (textPoint == null) - { - continue; - } - - final int offset = plugin.isHotKeyPressed() - ? item.getOffset() - : offsetMap.compute(item.getLocation(), (k, v) -> v != null ? v + 1 : 0); - - final int textX = textPoint.getX(); - final int textY = textPoint.getY() - (STRING_GAP * offset); - - if (plugin.isHotKeyPressed()) - { - final int stringWidth = fm.stringWidth(itemString); - final int stringHeight = fm.getHeight(); - - // Item bounds - int x = textX - 2; - int y = textY - stringHeight - 2; - int width = stringWidth + 4; - int height = stringHeight + 4; - final Rectangle itemBounds = new Rectangle(x, y, width, height); - - // Hidden box - x += width + 2; - y = textY - (RECTANGLE_SIZE + stringHeight) / 2; - width = height = RECTANGLE_SIZE; - final Rectangle itemHiddenBox = new Rectangle(x, y, width, height); - - // Highlight box - x += width + 2; - final Rectangle itemHighlightBox = new Rectangle(x, y, width, height); - - boolean mouseInBox = itemBounds.contains(mousePos.getX(), mousePos.getY()); - boolean mouseInHiddenBox = itemHiddenBox.contains(mousePos.getX(), mousePos.getY()); - boolean mouseInHighlightBox = itemHighlightBox.contains(mousePos.getX(), mousePos.getY()); - - if (mouseInBox) - { - plugin.setTextBoxBounds(new SimpleEntry<>(itemBounds, item)); - } - else if (mouseInHiddenBox) - { - plugin.setHiddenBoxBounds(new SimpleEntry<>(itemHiddenBox, item)); - - } - else if (mouseInHighlightBox) - { - plugin.setHighlightBoxBounds(new SimpleEntry<>(itemHighlightBox, item)); - } - - boolean topItem = topGroundItem == item; - - // Draw background if hovering - if (topItem && (mouseInBox || mouseInHiddenBox || mouseInHighlightBox)) - { - backgroundComponent.setRectangle(itemBounds); - backgroundComponent.render(graphics); - } - - // Draw hidden box - drawRectangle(graphics, itemHiddenBox, topItem && mouseInHiddenBox ? Color.RED : color, hidden != null, true); - - // Draw highlight box - drawRectangle(graphics, itemHighlightBox, topItem && mouseInHighlightBox ? Color.GREEN : color, highlighted != null, false); - } - - if (config.showGroundItemDuration() == TimerDisplayMode.ALWAYS - || (config.showGroundItemDuration() == TimerDisplayMode.HOTKEY_PRESSED && plugin.isHotKeyPressed())) - { - drawTimerOverlay(graphics, new java.awt.Point(textX, textY), item); - } - - if (config.toggleOutline()) - { - graphics.setColor(Color.BLACK); - graphics.drawString(itemString, textX + 1, textY + 1); - graphics.drawString(itemString, textX - 1, textY - 1); - graphics.drawString(itemString, textX - 1, textY + 1); - graphics.drawString(itemString, textX + 1, textY - 1); - } - - textComponent.setText(itemString); - textComponent.setColor(color); - textComponent.setPosition(new java.awt.Point(textX, textY)); - textComponent.render(graphics); - } - - return null; - } - - private void drawRectangle(Graphics2D graphics, Rectangle rect, Color color, boolean inList, boolean hiddenBox) - { - graphics.setColor(Color.BLACK); - graphics.drawRect(rect.x + 1, rect.y + 1, rect.width, rect.height); - - graphics.setColor(color); - graphics.draw(rect); - - if (inList) - { - graphics.fill(rect); - } - - graphics.setColor(Color.WHITE); - // Minus symbol - graphics.drawLine - ( - rect.x + 2, - rect.y + (rect.height / 2), - rect.x + rect.width - 2, - rect.y + (rect.height / 2) - ); - - if (!hiddenBox) - { - // Plus symbol - graphics.drawLine - ( - rect.x + (rect.width / 2), - rect.y + 2, - rect.x + (rect.width / 2), - rect.y + rect.height - 2 - ); - } - - } - - private void drawTimerOverlay(Graphics2D graphics, java.awt.Point location, GroundItem item) - { - progressPieComponent.setDiameter(TIMER_OVERLAY_DIAMETER); - - int x = (int) location.getX() - TIMER_OVERLAY_DIAMETER; - int y = (int) location.getY() - TIMER_OVERLAY_DIAMETER / 2; - - progressPieComponent.setPosition(new Point(x, y)); - - double millisOnGround = Duration.between(item.getDroppedInstant(), Instant.now()).toMillis(); - boolean isPubliclyVisible = !item.isAlwaysPrivate() && millisOnGround > item.getDurationMillis(); - double timeLeftRelative; - Color fillColor; - - if (isPubliclyVisible || !item.isOwnedByPlayer()) - { - if (item.isOwnedByPlayer()) - { - timeLeftRelative = getTimeLeftRelative(millisOnGround - PUBLIC_ITEM_DURATION_MILLIS, PUBLIC_ITEM_DURATION_MILLIS); - - } - else - { - timeLeftRelative = getTimeLeftRelative(millisOnGround, PUBLIC_ITEM_DURATION_MILLIS); - } - - if (timeLeftRelative < WARNING_THRESHOLD) - { - fillColor = PUBLIC_WARNING_TIMER_COLOR; - } - else - { - fillColor = PUBLIC_TIMER_COLOR; - } - } - else - { - timeLeftRelative = getTimeLeftRelative(millisOnGround, item.getDurationMillis()); - fillColor = PRIVATE_TIMER_COLOR; - - } - - // don't draw timer for any permanently spawned items or broken edge cases - if (timeLeftRelative > 1 || timeLeftRelative < 0) - { - return; - } - - progressPieComponent.setFill(fillColor); - progressPieComponent.setBorderColor(fillColor); - progressPieComponent.setProgress(timeLeftRelative); - progressPieComponent.render(graphics); - } - - private double getTimeLeftRelative(double millisOnGround, int duration) - { - return (duration - millisOnGround) / duration; - } -} +/* + * Copyright (c) 2017, Aria + * 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.client.plugins.grounditems; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.time.Duration; +import java.time.Instant; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.Comparator; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.Player; +import net.runelite.api.Point; +import net.runelite.api.Tile; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.MENU; +import net.runelite.client.plugins.grounditems.config.PriceDisplayMode; +import net.runelite.client.plugins.grounditems.config.TimerDisplayMode; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.ui.overlay.components.BackgroundComponent; +import net.runelite.client.ui.overlay.components.ProgressPieComponent; +import net.runelite.client.ui.overlay.components.TextComponent; +import net.runelite.client.util.StackFormatter; + +public class GroundItemsOverlay extends Overlay +{ + private static final int MAX_DISTANCE = 2500; + // We must offset the text on the z-axis such that + // it doesn't obscure the ground items below it. + private static final int OFFSET_Z = 20; + // The game won't send anything higher than this value to the plugin - + // so we replace any item quantity higher with "Lots" instead. + private static final int MAX_QUANTITY = 65535; + // The 15 pixel gap between each drawn ground item. + private static final int STRING_GAP = 15; + // Size of the hidden/highlight boxes + private static final int RECTANGLE_SIZE = 8; + + private static final int TIMER_OVERLAY_DIAMETER = 10; + private static final int PUBLIC_ITEM_DURATION_MILLIS = 60000; + private static final float WARNING_THRESHOLD = 0.25f; + private static final Color PUBLIC_TIMER_COLOR = Color.YELLOW; + private static final Color PRIVATE_TIMER_COLOR = Color.GREEN; + private static final Color PUBLIC_WARNING_TIMER_COLOR = Color.RED; + + private final Client client; + private final GroundItemsPlugin plugin; + private final GroundItemsConfig config; + private final StringBuilder itemStringBuilder = new StringBuilder(); + private final BackgroundComponent backgroundComponent = new BackgroundComponent(); + private final TextComponent textComponent = new TextComponent(); + private final ProgressPieComponent progressPieComponent = new ProgressPieComponent(); + private final Map offsetMap = new HashMap<>(); + + @Inject + private GroundItemsOverlay(Client client, GroundItemsPlugin plugin, GroundItemsConfig config) + { + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + this.client = client; + this.plugin = plugin; + this.config = config; + } + + @Override + public Dimension render(Graphics2D graphics) + { + final boolean dontShowOverlay = (config.itemHighlightMode() == MENU || plugin.isHideAll()) && !plugin.isHotKeyPressed(); + + if (dontShowOverlay && !config.highlightTiles()) + { + return null; + } + + final FontMetrics fm = graphics.getFontMetrics(); + final Player player = client.getLocalPlayer(); + + if (player == null || client.getViewportWidget() == null) + { + return null; + } + + offsetMap.clear(); + final LocalPoint localLocation = player.getLocalLocation(); + final Point mousePos = client.getMouseCanvasPosition(); + Collection groundItemList = GroundItemsPlugin.getCollectedGroundItems().values(); + GroundItem topGroundItem = null; + + if (plugin.isHotKeyPressed()) + { + // Make copy of ground items because we are going to modify them here, and the array list supports our + // desired behaviour here + groundItemList = new ArrayList<>(groundItemList); + final java.awt.Point awtMousePos = new java.awt.Point(mousePos.getX(), mousePos.getY()); + GroundItem groundItem = null; + + for (GroundItem item : groundItemList) + { + item.setOffset(offsetMap.compute(item.getLocation(), (k, v) -> v != null ? v + 1 : 0)); + + if (groundItem != null) + { + continue; + } + + if (plugin.getTextBoxBounds() != null + && item.equals(plugin.getTextBoxBounds().getValue()) + && plugin.getTextBoxBounds().getKey().contains(awtMousePos)) + { + groundItem = item; + continue; + } + + if (plugin.getHiddenBoxBounds() != null + && item.equals(plugin.getHiddenBoxBounds().getValue()) + && plugin.getHiddenBoxBounds().getKey().contains(awtMousePos)) + { + groundItem = item; + continue; + } + + if (plugin.getHighlightBoxBounds() != null + && item.equals(plugin.getHighlightBoxBounds().getValue()) + && plugin.getHighlightBoxBounds().getKey().contains(awtMousePos)) + { + groundItem = item; + } + } + + if (groundItem != null) + { + groundItemList.remove(groundItem); + groundItemList.add(groundItem); + topGroundItem = groundItem; + } + } + + plugin.setTextBoxBounds(null); + plugin.setHiddenBoxBounds(null); + plugin.setHighlightBoxBounds(null); + + final boolean onlyShowLoot = config.onlyShowLoot(); + + List groundItemListAsList = new ArrayList<>(groundItemList); // make a copy so we can non-destructively modify the list + + Comparator compareByHaPrice = Comparator.comparingInt(GroundItem::getHaPrice); + Comparator compareByGePrice = Comparator.comparingInt(GroundItem::getGePrice); + groundItemListAsList.sort(config.sortByGEPrice() ? compareByGePrice : compareByHaPrice); + + for (GroundItem item : groundItemListAsList) + { + final LocalPoint groundPoint = LocalPoint.fromWorld(client, item.getLocation()); + + if (groundPoint == null || localLocation.distanceTo(groundPoint) > MAX_DISTANCE + || (onlyShowLoot && !item.isMine())) + { + continue; + } + + final Color highlighted = plugin.getHighlighted(item.getName(), item.getGePrice(), item.getHaPrice()); + final Color hidden = plugin.getHidden(item.getName(), item.getGePrice(), item.getHaPrice(), item.isTradeable()); + + if (highlighted == null && !plugin.isHotKeyPressed()) + { + // Do not display hidden items + if (hidden != null) + { + continue; + } + + // Do not display non-highlighted items + if (config.showHighlightedOnly()) + { + continue; + } + } + + final Color color = plugin.getItemColor(highlighted, hidden); + + if (config.highlightTiles()) + { + final Polygon poly = Perspective.getCanvasTilePoly(client, groundPoint); + + if (poly != null) + { + OverlayUtil.renderPolygon(graphics, poly, color); + } + } + + if (dontShowOverlay) + { + continue; + } + + itemStringBuilder.append(item.getName()); + + if (item.getQuantity() > 1) + { + if (item.getQuantity() >= MAX_QUANTITY) + { + itemStringBuilder.append(" (Lots!)"); + } + else + { + itemStringBuilder.append(" (") + .append(StackFormatter.quantityToStackSize(item.getQuantity())) + .append(")"); + } + } + + if (config.priceDisplayMode() == PriceDisplayMode.BOTH) + { + if (item.getGePrice() > 0) + { + itemStringBuilder.append(" (EX: ") + .append(StackFormatter.quantityToStackSize(item.getGePrice())) + .append(" gp)"); + } + + if (item.getHaPrice() > 0) + { + itemStringBuilder.append(" (HA: ") + .append(StackFormatter.quantityToStackSize(item.getHaPrice())) + .append(" gp)"); + } + } + else if (config.priceDisplayMode() != PriceDisplayMode.OFF) + { + final int price = config.priceDisplayMode() == PriceDisplayMode.GE + ? item.getGePrice() + : item.getHaPrice(); + + if (price > 0) + { + itemStringBuilder + .append(" (") + .append(StackFormatter.quantityToStackSize(price)) + .append(" gp)"); + } + } + + final String itemString = itemStringBuilder.toString(); + itemStringBuilder.setLength(0); + + if (item.getHeight() == -1) + { + final Tile[][][] sceneTiles = client.getScene().getTiles(); + final Tile itemTile = sceneTiles[client.getPlane()][groundPoint.getSceneX()][groundPoint.getSceneY()]; + item.setHeight(itemTile.getItemLayer().getHeight()); + } + + final Point textPoint = Perspective.getCanvasTextLocation(client, + graphics, + groundPoint, + itemString, + item.getHeight() + OFFSET_Z); + + if (textPoint == null) + { + continue; + } + + final int offset = plugin.isHotKeyPressed() + ? item.getOffset() + : offsetMap.compute(item.getLocation(), (k, v) -> v != null ? v + 1 : 0); + + final int textX = textPoint.getX(); + final int textY = textPoint.getY() - (STRING_GAP * offset); + + if (plugin.isHotKeyPressed()) + { + final int stringWidth = fm.stringWidth(itemString); + final int stringHeight = fm.getHeight(); + + // Item bounds + int x = textX - 2; + int y = textY - stringHeight - 2; + int width = stringWidth + 4; + int height = stringHeight + 4; + final Rectangle itemBounds = new Rectangle(x, y, width, height); + + // Hidden box + x += width + 2; + y = textY - (RECTANGLE_SIZE + stringHeight) / 2; + width = height = RECTANGLE_SIZE; + final Rectangle itemHiddenBox = new Rectangle(x, y, width, height); + + // Highlight box + x += width + 2; + final Rectangle itemHighlightBox = new Rectangle(x, y, width, height); + + boolean mouseInBox = itemBounds.contains(mousePos.getX(), mousePos.getY()); + boolean mouseInHiddenBox = itemHiddenBox.contains(mousePos.getX(), mousePos.getY()); + boolean mouseInHighlightBox = itemHighlightBox.contains(mousePos.getX(), mousePos.getY()); + + if (mouseInBox) + { + plugin.setTextBoxBounds(new SimpleEntry<>(itemBounds, item)); + } + else if (mouseInHiddenBox) + { + plugin.setHiddenBoxBounds(new SimpleEntry<>(itemHiddenBox, item)); + + } + else if (mouseInHighlightBox) + { + plugin.setHighlightBoxBounds(new SimpleEntry<>(itemHighlightBox, item)); + } + + boolean topItem = topGroundItem == item; + + // Draw background if hovering + if (topItem && (mouseInBox || mouseInHiddenBox || mouseInHighlightBox)) + { + backgroundComponent.setRectangle(itemBounds); + backgroundComponent.render(graphics); + } + + // Draw hidden box + drawRectangle(graphics, itemHiddenBox, topItem && mouseInHiddenBox ? Color.RED : color, hidden != null, true); + + // Draw highlight box + drawRectangle(graphics, itemHighlightBox, topItem && mouseInHighlightBox ? Color.GREEN : color, highlighted != null, false); + } + + if (config.showGroundItemDuration() == TimerDisplayMode.ALWAYS + || (config.showGroundItemDuration() == TimerDisplayMode.HOTKEY_PRESSED && plugin.isHotKeyPressed())) + { + drawTimerOverlay(graphics, new java.awt.Point(textX, textY), item); + } + + if (config.toggleOutline()) + { + graphics.setColor(Color.BLACK); + graphics.drawString(itemString, textX + 1, textY + 1); + graphics.drawString(itemString, textX - 1, textY - 1); + graphics.drawString(itemString, textX - 1, textY + 1); + graphics.drawString(itemString, textX + 1, textY - 1); + } + + textComponent.setText(itemString); + textComponent.setColor(color); + textComponent.setPosition(new java.awt.Point(textX, textY)); + textComponent.render(graphics); + } + + return null; + } + + private void drawRectangle(Graphics2D graphics, Rectangle rect, Color color, boolean inList, boolean hiddenBox) + { + graphics.setColor(Color.BLACK); + graphics.drawRect(rect.x + 1, rect.y + 1, rect.width, rect.height); + + graphics.setColor(color); + graphics.draw(rect); + + if (inList) + { + graphics.fill(rect); + } + + graphics.setColor(Color.WHITE); + // Minus symbol + graphics.drawLine + ( + rect.x + 2, + rect.y + (rect.height / 2), + rect.x + rect.width - 2, + rect.y + (rect.height / 2) + ); + + if (!hiddenBox) + { + // Plus symbol + graphics.drawLine + ( + rect.x + (rect.width / 2), + rect.y + 2, + rect.x + (rect.width / 2), + rect.y + rect.height - 2 + ); + } + + } + + private void drawTimerOverlay(Graphics2D graphics, java.awt.Point location, GroundItem item) + { + progressPieComponent.setDiameter(TIMER_OVERLAY_DIAMETER); + + int x = (int) location.getX() - TIMER_OVERLAY_DIAMETER; + int y = (int) location.getY() - TIMER_OVERLAY_DIAMETER / 2; + + progressPieComponent.setPosition(new Point(x, y)); + + double millisOnGround = Duration.between(item.getDroppedInstant(), Instant.now()).toMillis(); + boolean isPubliclyVisible = !item.isAlwaysPrivate() && millisOnGround > item.getDurationMillis(); + double timeLeftRelative; + Color fillColor; + + if (isPubliclyVisible || !item.isOwnedByPlayer()) + { + if (item.isOwnedByPlayer()) + { + timeLeftRelative = getTimeLeftRelative(millisOnGround - PUBLIC_ITEM_DURATION_MILLIS, PUBLIC_ITEM_DURATION_MILLIS); + + } + else + { + timeLeftRelative = getTimeLeftRelative(millisOnGround, PUBLIC_ITEM_DURATION_MILLIS); + } + + if (timeLeftRelative < WARNING_THRESHOLD) + { + fillColor = PUBLIC_WARNING_TIMER_COLOR; + } + else + { + fillColor = PUBLIC_TIMER_COLOR; + } + } + else + { + timeLeftRelative = getTimeLeftRelative(millisOnGround, item.getDurationMillis()); + fillColor = PRIVATE_TIMER_COLOR; + + } + + // don't draw timer for any permanently spawned items or broken edge cases + if (timeLeftRelative > 1 || timeLeftRelative < 0) + { + return; + } + + progressPieComponent.setFill(fillColor); + progressPieComponent.setBorderColor(fillColor); + progressPieComponent.setProgress(timeLeftRelative); + progressPieComponent.render(graphics); + } + + private double getTimeLeftRelative(double millisOnGround, int duration) + { + return (duration - millisOnGround) / duration; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java index 3b8a06ec13..fc5b592e11 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java @@ -1,759 +1,849 @@ -/* - * Copyright (c) 2017, Aria - * 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.client.plugins.grounditems; - -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.LoadingCache; -import com.google.inject.Provides; -import java.awt.Color; -import java.awt.Rectangle; -import static java.lang.Boolean.TRUE; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.Item; -import net.runelite.api.ItemComposition; -import net.runelite.api.ItemID; -import net.runelite.api.ItemLayer; -import net.runelite.api.MenuAction; -import net.runelite.api.MenuEntry; -import net.runelite.api.Node; -import net.runelite.api.Player; -import net.runelite.api.Scene; -import net.runelite.api.Tile; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ClientTick; -import net.runelite.api.events.ConfigChanged; -import net.runelite.api.events.FocusChanged; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemQuantityChanged; -import net.runelite.api.events.ItemSpawned; -import net.runelite.api.events.MenuEntryAdded; -import net.runelite.client.Notifier; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.NpcLootReceived; -import net.runelite.client.events.PlayerLootReceived; -import net.runelite.client.game.ItemManager; -import net.runelite.client.game.ItemStack; -import net.runelite.client.input.KeyManager; -import net.runelite.client.input.MouseManager; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.OVERLAY; -import net.runelite.client.plugins.grounditems.config.MenuHighlightMode; -import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.BOTH; -import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.NAME; -import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.OPTION; -import net.runelite.client.plugins.grounditems.config.ValueCalculationMode; -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.util.ColorUtil; -import net.runelite.client.util.StackFormatter; -import net.runelite.client.util.Text; - -@PluginDescriptor( - name = "Ground Items", - description = "Highlight ground items and/or show price information", - tags = {"grand", "exchange", "high", "alchemy", "prices", "highlight", "overlay"} -) -public class GroundItemsPlugin extends Plugin -{ - // Used when getting High Alchemy value - multiplied by general store price. - private static final float HIGH_ALCHEMY_CONSTANT = 0.6f; - // ItemID for coins - private static final int COINS = ItemID.COINS_995; - - // items stay on the ground for 30 mins in an instance - private static final int INSTANCE_DURATION_MILLIS = 45 * 60 * 1000; - //untradeables stay on the ground for 150 seconds (http://oldschoolrunescape.wikia.com/wiki/Item#Dropping_and_Destroying) - private static final int UNTRADEABLE_DURATION_MILLIS = 150 * 1000; - //items stay on the ground for 1 hour after death - private static final int DEATH_DURATION_MILLIS = 60 * 60 * 1000; - private static final int NORMAL_DURATION_MILLIS = 60 * 1000; - - // Ground item menu options - private static final int FIRST_OPTION = MenuAction.GROUND_ITEM_FIRST_OPTION.getId(); - private static final int SECOND_OPTION = MenuAction.GROUND_ITEM_SECOND_OPTION.getId(); - private static final int THIRD_OPTION = MenuAction.GROUND_ITEM_THIRD_OPTION.getId(); // this is Take - private static final int FOURTH_OPTION = MenuAction.GROUND_ITEM_FOURTH_OPTION.getId(); - private static final int FIFTH_OPTION = MenuAction.GROUND_ITEM_FIFTH_OPTION.getId(); - private static final int EXAMINE_ITEM = MenuAction.EXAMINE_ITEM_GROUND.getId(); - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private Map.Entry textBoxBounds; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private Map.Entry hiddenBoxBounds; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private Map.Entry highlightBoxBounds; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private boolean hotKeyPressed; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private boolean hideAll; - - private List hiddenItemList = new CopyOnWriteArrayList<>(); - private List highlightedItemsList = new CopyOnWriteArrayList<>(); - - @Inject - private GroundItemInputListener inputListener; - - @Inject - private MouseManager mouseManager; - - @Inject - private KeyManager keyManager; - - @Inject - private Client client; - - @Inject - private ItemManager itemManager; - - @Inject - private OverlayManager overlayManager; - - @Inject - private GroundItemsConfig config; - - @Inject - private GroundItemsOverlay overlay; - - @Inject - private Notifier notifier; - - @Getter - public static final Map collectedGroundItems = new LinkedHashMap<>(); - private final Map priceChecks = new LinkedHashMap<>(); - private LoadingCache highlightedItems; - private LoadingCache hiddenItems; - - @Provides - GroundItemsConfig provideConfig(ConfigManager configManager) - { - return configManager.getConfig(GroundItemsConfig.class); - } - - @Override - protected void startUp() - { - overlayManager.add(overlay); - reset(); - mouseManager.registerMouseListener(inputListener); - keyManager.registerKeyListener(inputListener); - } - - @Override - protected void shutDown() throws Exception - { - overlayManager.remove(overlay); - mouseManager.unregisterMouseListener(inputListener); - keyManager.unregisterKeyListener(inputListener); - highlightedItems.invalidateAll(); - highlightedItems = null; - hiddenItems.invalidateAll(); - hiddenItems = null; - hiddenItemList = null; - highlightedItemsList = null; - collectedGroundItems.clear(); - } - - @Subscribe - public void onConfigChanged(ConfigChanged event) - { - if (event.getGroup().equals("grounditems")) - { - reset(); - } - } - - @Subscribe - public void onGameStateChanged(final GameStateChanged event) - { - if (event.getGameState() == GameState.LOADING) - { - collectedGroundItems.clear(); - } - } - - @Subscribe - public void onItemSpawned(ItemSpawned itemSpawned) - { - Item item = itemSpawned.getItem(); - Tile tile = itemSpawned.getTile(); - - GroundItem groundItem = buildGroundItem(tile, item); - - GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation()); - GroundItem existing = collectedGroundItems.putIfAbsent(groundItemKey, groundItem); - if (existing != null) - { - existing.setQuantity(existing.getQuantity() + groundItem.getQuantity()); - } - - boolean shouldNotify = !config.onlyShowLoot() && config.highlightedColor().equals(getHighlighted( - groundItem.getName(), - groundItem.getGePrice(), - groundItem.getHaPrice())); - - if (config.notifyHighlightedDrops() && shouldNotify) - { - notifyHighlightedItem(groundItem); - } - } - - @Subscribe - public void onItemDespawned(ItemDespawned itemDespawned) - { - Item item = itemDespawned.getItem(); - Tile tile = itemDespawned.getTile(); - - GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation()); - GroundItem groundItem = collectedGroundItems.get(groundItemKey); - if (groundItem == null) - { - return; - } - - if (groundItem.getQuantity() <= item.getQuantity()) - { - collectedGroundItems.remove(groundItemKey); - } - else - { - groundItem.setQuantity(groundItem.getQuantity() - item.getQuantity()); - } - } - - @Subscribe - public void onItemQuantityChanged(ItemQuantityChanged itemQuantityChanged) - { - Item item = itemQuantityChanged.getItem(); - Tile tile = itemQuantityChanged.getTile(); - int oldQuantity = itemQuantityChanged.getOldQuantity(); - int newQuantity = itemQuantityChanged.getNewQuantity(); - - int diff = newQuantity - oldQuantity; - GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation()); - GroundItem groundItem = collectedGroundItems.get(groundItemKey); - if (groundItem != null) - { - groundItem.setQuantity(groundItem.getQuantity() + diff); - } - } - - @Subscribe - public void onNpcLootReceived(NpcLootReceived npcLootReceived) - { - npcLootReceived.getItems().forEach(item -> - { - GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), npcLootReceived.getNpc().getWorldLocation()); - if (collectedGroundItems.containsKey(groundItemKey)) - { - collectedGroundItems.get(groundItemKey).setOwnedByPlayer(true); - } - } - ); - - Collection items = npcLootReceived.getItems(); - lootReceived(items); - lootNotifier(items); - } - - @Subscribe - public void onPlayerLootReceived(PlayerLootReceived playerLootReceived) - { - Collection items = playerLootReceived.getItems(); - lootReceived(items); - lootNotifier(items); - } - - private void lootNotifier(Collection items) - { - ItemComposition composition; - for (ItemStack is : items) - { - composition = itemManager.getItemComposition(is.getId()); - Color itemColor = getHighlighted(composition.getName(), itemManager.getItemPrice(is.getId()) * is.getQuantity(), Math.round(composition.getPrice() * HIGH_ALCHEMY_CONSTANT) * is.getQuantity()); - if (itemColor != null) - { - if (config.notifyHighlightedDrops() && itemColor.equals(config.highlightedColor())) - { - sendLootNotification(composition.getName(), "highlighted"); - } - else if (config.notifyLowValueDrops() && itemColor.equals(config.lowValueColor())) - { - sendLootNotification(composition.getName(), "low value"); - } - else if (config.notifyMediumValueDrops() && itemColor.equals(config.mediumValueColor())) - { - sendLootNotification(composition.getName(), "medium value"); - } - else if (config.notifyHighValueDrops() && itemColor.equals(config.highValueColor())) - { - sendLootNotification(composition.getName(), "high value"); - } - else if (config.notifyInsaneValueDrops() && itemColor.equals(config.insaneValueColor())) - { - sendLootNotification(composition.getName(), "insane value"); - } - } - } - } - - private void sendLootNotification(String itemName, String message) - { - String notification = "[" + client.getLocalPlayer().getName() + "] " + - "Received a " + message + " item: " + itemName; - notifier.notify(notification); - } - - @Subscribe - public void onClientTick(ClientTick event) - { - if (!config.collapseEntries()) - { - return; - } - - final MenuEntry[] menuEntries = client.getMenuEntries(); - final List newEntries = new ArrayList<>(menuEntries.length); - - outer: - for (int i = menuEntries.length - 1; i >= 0; i--) - { - MenuEntry menuEntry = menuEntries[i]; - - int menuType = menuEntry.getType(); - if (menuType == FIRST_OPTION || menuType == SECOND_OPTION || menuType == THIRD_OPTION - || menuType == FOURTH_OPTION || menuType == FIFTH_OPTION || menuType == EXAMINE_ITEM) - { - for (MenuEntryWithCount entryWCount : newEntries) - { - if (entryWCount.getEntry().equals(menuEntry)) - { - entryWCount.increment(); - continue outer; - } - } - } - - newEntries.add(new MenuEntryWithCount(menuEntry)); - } - - Collections.reverse(newEntries); - - client.setMenuEntries(newEntries.stream().map(e -> - { - final MenuEntry entry = e.getEntry(); - final int count = e.getCount(); - if (count > 1) - { - entry.setTarget(entry.getTarget() + " x " + count); - } - - return entry; - }).toArray(MenuEntry[]::new)); - } - - private void lootReceived(Collection items) - { - for (ItemStack itemStack : items) - { - WorldPoint location = WorldPoint.fromLocal(client, itemStack.getLocation()); - GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(itemStack.getId(), location); - GroundItem groundItem = collectedGroundItems.get(groundItemKey); - if (groundItem != null) - { - groundItem.setMine(true); - - boolean shouldNotify = config.onlyShowLoot() && config.highlightedColor().equals(getHighlighted( - groundItem.getName(), - groundItem.getGePrice(), - groundItem.getHaPrice())); - - if (config.notifyHighlightedDrops() && shouldNotify) - { - notifyHighlightedItem(groundItem); - } - } - } - } - - private GroundItem buildGroundItem(final Tile tile, final Item item) - { - // Collect the data for the item - final int itemId = item.getId(); - final ItemComposition itemComposition = itemManager.getItemComposition(itemId); - final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId; - final int alchPrice = Math.round(itemComposition.getPrice() * HIGH_ALCHEMY_CONSTANT); - int durationMillis; - if (client.isInInstancedRegion()) - { - durationMillis = INSTANCE_DURATION_MILLIS; - } - else if (!itemComposition.isTradeable() && realItemId != COINS) - { - durationMillis = UNTRADEABLE_DURATION_MILLIS; - } - else - { - durationMillis = NORMAL_DURATION_MILLIS; - } - - WorldPoint playerLocation = client.getLocalPlayer().getWorldLocation(); - - final GroundItem groundItem = GroundItem.builder() - .id(itemId) - .location(tile.getWorldLocation()) - .itemId(realItemId) - .quantity(item.getQuantity()) - .name(itemComposition.getName()) - .haPrice(alchPrice) - .height(-1) - .tradeable(itemComposition.isTradeable()) - .droppedInstant(Instant.now()) - .durationMillis(durationMillis) - .isAlwaysPrivate(client.isInInstancedRegion() || (!itemComposition.isTradeable() && realItemId != COINS)) - .isOwnedByPlayer(tile.getWorldLocation().equals(playerLocation)) - .build(); - - - // Update item price in case it is coins - if (realItemId == COINS) - { - groundItem.setHaPrice(1); - groundItem.setGePrice(1); - } - else - { - groundItem.setGePrice(itemManager.getItemPrice(realItemId)); - } - - return groundItem; - } - - private void reset() - { - // gets the hidden items from the text box in the config - hiddenItemList = Text.fromCSV(config.getHiddenItems()); - - // gets the highlighted items from the text box in the config - highlightedItemsList = Text.fromCSV(config.getHighlightItems()); - - highlightedItems = CacheBuilder.newBuilder() - .maximumSize(512L) - .expireAfterAccess(10, TimeUnit.MINUTES) - .build(new WildcardMatchLoader(highlightedItemsList)); - - hiddenItems = CacheBuilder.newBuilder() - .maximumSize(512L) - .expireAfterAccess(10, TimeUnit.MINUTES) - .build(new WildcardMatchLoader(hiddenItemList)); - - // Cache colors - priceChecks.clear(); - - if (config.insaneValuePrice() > 0) - { - priceChecks.put(config.insaneValuePrice(), config.insaneValueColor()); - } - - if (config.highValuePrice() > 0) - { - priceChecks.put(config.highValuePrice(), config.highValueColor()); - } - - if (config.mediumValuePrice() > 0) - { - priceChecks.put(config.mediumValuePrice(), config.mediumValueColor()); - } - - if (config.lowValuePrice() > 0) - { - priceChecks.put(config.lowValuePrice(), config.lowValueColor()); - } - - if (config.getHighlightOverValue() > 0) - { - priceChecks.put(config.getHighlightOverValue(), config.highlightedColor()); - } - } - - @Subscribe - public void onMenuEntryAdded(MenuEntryAdded event) - { - if (config.itemHighlightMode() != OVERLAY - && event.getOption().equals("Take") - && event.getType() == THIRD_OPTION) - { - int itemId = event.getIdentifier(); - Scene scene = client.getScene(); - Tile tile = scene.getTiles()[client.getPlane()][event.getActionParam0()][event.getActionParam1()]; - ItemLayer itemLayer = tile.getItemLayer(); - - if (itemLayer == null) - { - return; - } - - MenuEntry[] menuEntries = client.getMenuEntries(); - MenuEntry lastEntry = menuEntries[menuEntries.length - 1]; - - int quantity = 1; - Node current = itemLayer.getBottom(); - - while (current instanceof Item) - { - Item item = (Item) current; - if (item.getId() == itemId) - { - quantity = item.getQuantity(); - } - current = current.getNext(); - } - - final ItemComposition itemComposition = itemManager.getItemComposition(itemId); - final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemComposition.getId(); - final int itemPrice = itemManager.getItemPrice(realItemId); - final int price = itemPrice <= 0 ? itemComposition.getPrice() : itemPrice; - final int haPrice = Math.round(itemComposition.getPrice() * HIGH_ALCHEMY_CONSTANT) * quantity; - final int gePrice = quantity * price; - final Color hidden = getHidden(itemComposition.getName(), gePrice, haPrice, itemComposition.isTradeable()); - final Color highlighted = getHighlighted(itemComposition.getName(), gePrice, haPrice); - final Color color = getItemColor(highlighted, hidden); - final boolean canBeRecolored = highlighted != null || (hidden != null && config.recolorMenuHiddenItems()); - - if (color != null && canBeRecolored && !color.equals(config.defaultColor())) - { - final MenuHighlightMode mode = config.menuHighlightMode(); - - if (mode == BOTH || mode == OPTION) - { - lastEntry.setOption(ColorUtil.prependColorTag("Take", color)); - } - - if (mode == BOTH || mode == NAME) - { - String target = lastEntry.getTarget().substring(lastEntry.getTarget().indexOf(">") + 1); - lastEntry.setTarget(ColorUtil.prependColorTag(target, color)); - } - } - - if (config.showMenuItemQuantities() && itemComposition.isStackable() && quantity > 1) - { - lastEntry.setTarget(lastEntry.getTarget() + " (" + quantity + ")"); - } - - if (config.removeIgnored() && event.getOption().equals("Take") && hiddenItemList.contains(Text.removeTags(event.getTarget()))) - { - menuEntries = removeOption(event.getOption(), event.getTarget()); - } - - client.setMenuEntries(menuEntries); - } - } - - private MenuEntry[] removeOption(String option, String target) - { - MenuEntry[] entries = client.getMenuEntries(); - int j = 0; - if (entries.length > 1) - { - MenuEntry[] newEntries = new MenuEntry[entries.length - 1]; - for (int i = 0; i < entries.length; ++i) - { - if (!(entries[i].getOption().equals(option) && entries[i].getTarget().equals(target))) - { - newEntries[j++] = entries[i]; - } - } - - return newEntries; - } - else - { - return entries; - } - } - - void updateList(String item, boolean hiddenList) - { - final Set hiddenItemSet = new HashSet<>(hiddenItemList); - final Set highlightedItemSet = new HashSet<>(highlightedItemsList); - - if (hiddenList) - { - highlightedItemSet.removeIf(item::equalsIgnoreCase); - } - else - { - hiddenItemSet.removeIf(item::equalsIgnoreCase); - } - - final Set items = hiddenList ? hiddenItemSet : highlightedItemSet; - - if (!items.removeIf(item::equalsIgnoreCase)) - { - items.add(item); - } - - config.setHiddenItems(Text.toCSV(hiddenItemSet)); - config.setHighlightedItem(Text.toCSV(highlightedItemSet)); - } - - Color getHighlighted(String item, int gePrice, int haPrice) - { - if (TRUE.equals(highlightedItems.getUnchecked(item))) - { - return config.highlightedColor(); - } - - // Explicit hide takes priority over implicit highlight - if (TRUE.equals(hiddenItems.getUnchecked(item))) - { - return null; - } - - ValueCalculationMode mode = config.valueCalculationMode(); - for (Map.Entry entry : priceChecks.entrySet()) - { - switch (mode) - { - case GE: - if (gePrice > entry.getKey()) - { - return entry.getValue(); - } - break; - case HA: - if (haPrice > entry.getKey()) - { - return entry.getValue(); - } - break; - default: // case HIGHEST - if (gePrice > entry.getKey() || haPrice > entry.getKey()) - { - return entry.getValue(); - } - break; - } - } - - return null; - } - - Color getHidden(String item, int gePrice, int haPrice, boolean isTradeable) - { - final boolean isExplicitHidden = TRUE.equals(hiddenItems.getUnchecked(item)); - final boolean isExplicitHighlight = TRUE.equals(highlightedItems.getUnchecked(item)); - final boolean canBeHidden = gePrice > 0 || isTradeable || !config.dontHideUntradeables(); - final boolean underGe = gePrice < config.getHideUnderValue(); - final boolean underHa = haPrice < config.getHideUnderValue(); - - // Explicit highlight takes priority over implicit hide - return isExplicitHidden || (!isExplicitHighlight && canBeHidden && underGe && underHa) - ? config.hiddenColor() - : null; - } - - Color getItemColor(Color highlighted, Color hidden) - { - if (highlighted != null) - { - return highlighted; - } - - if (hidden != null) - { - return hidden; - } - - return config.defaultColor(); - } - - @Subscribe - public void onFocusChanged(FocusChanged focusChanged) - { - if (!focusChanged.isFocused()) - { - setHotKeyPressed(false); - } - } - - private void notifyHighlightedItem(GroundItem item) - { - final Player local = client.getLocalPlayer(); - final StringBuilder notificationStringBuilder = new StringBuilder() - .append("[") - .append(local.getName()) - .append("] received a highlighted drop: ") - .append(item.getName()); - - if (item.getQuantity() > 1) - { - notificationStringBuilder.append(" x ").append(item.getQuantity()); - - - if (item.getQuantity() > (int) Character.MAX_VALUE) - { - notificationStringBuilder.append(" (Lots!)"); - } - else - { - notificationStringBuilder.append(" (") - .append(StackFormatter.quantityToStackSize(item.getQuantity())) - .append(")"); - } - } - - notificationStringBuilder.append("!"); - notifier.notify(notificationStringBuilder.toString()); - } -} +/* + * Copyright (c) 2017, Aria + * 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.client.plugins.grounditems; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import com.google.inject.Provides; +import java.awt.Color; +import java.awt.Rectangle; +import static java.lang.Boolean.TRUE; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.Item; +import net.runelite.api.ItemComposition; +import net.runelite.api.ItemID; +import net.runelite.api.ItemLayer; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.Node; +import net.runelite.api.Player; +import net.runelite.api.Scene; +import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ClientTick; +import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.FocusChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.ItemDespawned; +import net.runelite.api.events.ItemQuantityChanged; +import net.runelite.api.events.ItemSpawned; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.client.Notifier; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.NpcLootReceived; +import net.runelite.client.events.PlayerLootReceived; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemStack; +import net.runelite.client.input.KeyManager; +import net.runelite.client.input.MouseManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.OVERLAY; +import net.runelite.client.plugins.grounditems.config.MenuHighlightMode; +import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.BOTH; +import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.NAME; +import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.OPTION; +import net.runelite.client.plugins.grounditems.config.ValueCalculationMode; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.StackFormatter; +import net.runelite.client.util.Text; + +@PluginDescriptor( + name = "Ground Items", + description = "Highlight ground items and/or show price information", + tags = {"grand", "exchange", "high", "alchemy", "prices", "highlight", "overlay"} +) +public class GroundItemsPlugin extends Plugin +{ + // Used when getting High Alchemy value - multiplied by general store price. + private static final float HIGH_ALCHEMY_CONSTANT = 0.6f; + // ItemID for coins + private static final int COINS = ItemID.COINS_995; + + // items stay on the ground for 30 mins in an instance + private static final int INSTANCE_DURATION_MILLIS = 45 * 60 * 1000; + //untradeables stay on the ground for 150 seconds (http://oldschoolrunescape.wikia.com/wiki/Item#Dropping_and_Destroying) + private static final int UNTRADEABLE_DURATION_MILLIS = 150 * 1000; + //items stay on the ground for 1 hour after death + private static final int DEATH_DURATION_MILLIS = 60 * 60 * 1000; + private static final int NORMAL_DURATION_MILLIS = 60 * 1000; + + // Ground item menu options + private static final int FIRST_OPTION = MenuAction.GROUND_ITEM_FIRST_OPTION.getId(); + private static final int SECOND_OPTION = MenuAction.GROUND_ITEM_SECOND_OPTION.getId(); + private static final int THIRD_OPTION = MenuAction.GROUND_ITEM_THIRD_OPTION.getId(); // this is Take + private static final int FOURTH_OPTION = MenuAction.GROUND_ITEM_FOURTH_OPTION.getId(); + private static final int FIFTH_OPTION = MenuAction.GROUND_ITEM_FIFTH_OPTION.getId(); + private static final int EXAMINE_ITEM = MenuAction.EXAMINE_ITEM_GROUND.getId(); + private static final int WALK = MenuAction.WALK.getId(); + + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + private Map.Entry textBoxBounds; + + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + private Map.Entry hiddenBoxBounds; + + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + private Map.Entry highlightBoxBounds; + + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + private boolean hotKeyPressed; + + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + private boolean hideAll; + + private List hiddenItemList = new CopyOnWriteArrayList<>(); + private List highlightedItemsList = new CopyOnWriteArrayList<>(); + + @Inject + private GroundItemInputListener inputListener; + + @Inject + private MouseManager mouseManager; + + @Inject + private KeyManager keyManager; + + @Inject + private Client client; + + @Inject + private ItemManager itemManager; + + @Inject + private OverlayManager overlayManager; + + @Inject + private GroundItemsConfig config; + + @Inject + private GroundItemsOverlay overlay; + + @Inject + private Notifier notifier; + + @Getter + public static final Map collectedGroundItems = new LinkedHashMap<>(); + private final Map priceChecks = new LinkedHashMap<>(); + private LoadingCache highlightedItems; + private LoadingCache hiddenItems; + + @Provides + GroundItemsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(GroundItemsConfig.class); + } + + @Override + protected void startUp() + { + overlayManager.add(overlay); + reset(); + mouseManager.registerMouseListener(inputListener); + keyManager.registerKeyListener(inputListener); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(overlay); + mouseManager.unregisterMouseListener(inputListener); + keyManager.unregisterKeyListener(inputListener); + highlightedItems.invalidateAll(); + highlightedItems = null; + hiddenItems.invalidateAll(); + hiddenItems = null; + hiddenItemList = null; + highlightedItemsList = null; + collectedGroundItems.clear(); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("grounditems")) + { + reset(); + } + } + + @Subscribe + public void onGameStateChanged(final GameStateChanged event) + { + if (event.getGameState() == GameState.LOADING) + { + collectedGroundItems.clear(); + } + } + + @Subscribe + public void onItemSpawned(ItemSpawned itemSpawned) + { + Item item = itemSpawned.getItem(); + Tile tile = itemSpawned.getTile(); + + GroundItem groundItem = buildGroundItem(tile, item); + + GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation()); + GroundItem existing = collectedGroundItems.putIfAbsent(groundItemKey, groundItem); + if (existing != null) + { + existing.setQuantity(existing.getQuantity() + groundItem.getQuantity()); + } + + boolean shouldNotify = !config.onlyShowLoot() && config.highlightedColor().equals(getHighlighted( + groundItem.getName(), + groundItem.getGePrice(), + groundItem.getHaPrice())); + + if (config.notifyHighlightedDrops() && shouldNotify) + { + notifyHighlightedItem(groundItem); + } + } + + @Subscribe + public void onItemDespawned(ItemDespawned itemDespawned) + { + Item item = itemDespawned.getItem(); + Tile tile = itemDespawned.getTile(); + + GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation()); + GroundItem groundItem = collectedGroundItems.get(groundItemKey); + if (groundItem == null) + { + return; + } + + if (groundItem.getQuantity() <= item.getQuantity()) + { + collectedGroundItems.remove(groundItemKey); + } + else + { + groundItem.setQuantity(groundItem.getQuantity() - item.getQuantity()); + } + } + + @Subscribe + public void onItemQuantityChanged(ItemQuantityChanged itemQuantityChanged) + { + Item item = itemQuantityChanged.getItem(); + Tile tile = itemQuantityChanged.getTile(); + int oldQuantity = itemQuantityChanged.getOldQuantity(); + int newQuantity = itemQuantityChanged.getNewQuantity(); + + int diff = newQuantity - oldQuantity; + GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), tile.getWorldLocation()); + GroundItem groundItem = collectedGroundItems.get(groundItemKey); + if (groundItem != null) + { + groundItem.setQuantity(groundItem.getQuantity() + diff); + } + } + + @Subscribe + public void onNpcLootReceived(NpcLootReceived npcLootReceived) + { + npcLootReceived.getItems().forEach(item -> + { + GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), npcLootReceived.getNpc().getWorldLocation()); + if (collectedGroundItems.containsKey(groundItemKey)) + { + collectedGroundItems.get(groundItemKey).setOwnedByPlayer(true); + } + } + ); + + Collection items = npcLootReceived.getItems(); + lootReceived(items); + lootNotifier(items); + } + + @Subscribe + public void onPlayerLootReceived(PlayerLootReceived playerLootReceived) + { + Collection items = playerLootReceived.getItems(); + lootReceived(items); + lootNotifier(items); + } + + private void lootNotifier(Collection items) + { + ItemComposition composition; + for (ItemStack is : items) + { + composition = itemManager.getItemComposition(is.getId()); + Color itemColor = getHighlighted(composition.getName(), itemManager.getItemPrice(is.getId()) * is.getQuantity(), Math.round(composition.getPrice() * HIGH_ALCHEMY_CONSTANT) * is.getQuantity()); + if (itemColor != null) + { + if (config.notifyHighlightedDrops() && itemColor.equals(config.highlightedColor())) + { + sendLootNotification(composition.getName(), "highlighted"); + } + else if (config.notifyLowValueDrops() && itemColor.equals(config.lowValueColor())) + { + sendLootNotification(composition.getName(), "low value"); + } + else if (config.notifyMediumValueDrops() && itemColor.equals(config.mediumValueColor())) + { + sendLootNotification(composition.getName(), "medium value"); + } + else if (config.notifyHighValueDrops() && itemColor.equals(config.highValueColor())) + { + sendLootNotification(composition.getName(), "high value"); + } + else if (config.notifyInsaneValueDrops() && itemColor.equals(config.insaneValueColor())) + { + sendLootNotification(composition.getName(), "insane value"); + } + } + } + } + + private void sendLootNotification(String itemName, String message) + { + String notification = "[" + client.getLocalPlayer().getName() + "] " + + "Received a " + message + " item: " + itemName; + notifier.notify(notification); + } + + @Subscribe + public void onClientTick(ClientTick event) + { + final MenuEntry[] menuEntries = client.getMenuEntries(); + final List newEntries = new ArrayList<>(menuEntries.length); + + outer: + for (int i = menuEntries.length - 1; i >= 0; i--) + { + MenuEntry menuEntry = menuEntries[i]; + + if (config.collapseEntries()) + { + int menuType = menuEntry.getType(); + if (menuType == FIRST_OPTION || menuType == SECOND_OPTION || menuType == THIRD_OPTION + || menuType == FOURTH_OPTION || menuType == FIFTH_OPTION || menuType == EXAMINE_ITEM) + { + for (MenuEntryWithCount entryWCount : newEntries) + { + if (entryWCount.getEntry().equals(menuEntry)) + { + entryWCount.increment(); + continue outer; + } + } + } + } + + newEntries.add(new MenuEntryWithCount(menuEntry)); + } + + Collections.reverse(newEntries); + + newEntries.sort((a, b) -> + { + final int aMenuType = a.getEntry().getType(); + if (aMenuType == FIRST_OPTION || aMenuType == SECOND_OPTION || aMenuType == THIRD_OPTION + || aMenuType == FOURTH_OPTION || aMenuType == FIFTH_OPTION || aMenuType == EXAMINE_ITEM + || aMenuType == WALK) + { // only check for item related menu types, so we don't sort other stuff + final int bMenuType = b.getEntry().getType(); + if (bMenuType == FIRST_OPTION || bMenuType == SECOND_OPTION || bMenuType == THIRD_OPTION + || bMenuType == FOURTH_OPTION || bMenuType == FIFTH_OPTION || bMenuType == EXAMINE_ITEM + || bMenuType == WALK) + { + final MenuEntry aEntry = a.getEntry(); + final int aId = aEntry.getIdentifier(); + final boolean aHidden = isItemIdHidden(aId); + final int aQuantity = getCollapsedItemQuantity(aId, aEntry.getTarget()); + + final MenuEntry bEntry = b.getEntry(); + final int bId = bEntry.getIdentifier(); + final boolean bHidden = isItemIdHidden(bId); + final int bQuantity = getCollapsedItemQuantity(bId, bEntry.getTarget()); + + // only put items below walk if the config is set for it + if (config.rightClickHidden()) + { + if (aHidden && bMenuType == WALK) + return -1; + if (bHidden && aMenuType == WALK) + return 1; + } + + // sort hidden items below non-hidden items + if (aHidden && !bHidden && bMenuType != WALK) + return -1; + if (bHidden && !aHidden && aMenuType != WALK) + return 1; + + + // RS sorts by alch price by default, so no need to sort if config not set + if (config.sortByGEPrice()) + return (getGePriceFromItemId(aId) * aQuantity) - (getGePriceFromItemId(bId) * bQuantity); + } + } + + return 0; + }); + + client.setMenuEntries(newEntries.stream().map(e -> + { + final MenuEntry entry = e.getEntry(); + + if (config.collapseEntries()) + { + final int count = e.getCount(); + if (count > 1) + { + entry.setTarget(entry.getTarget() + " x " + count); + } + } + + return entry; + }).toArray(MenuEntry[]::new)); + } + + private void lootReceived(Collection items) + { + for (ItemStack itemStack : items) + { + WorldPoint location = WorldPoint.fromLocal(client, itemStack.getLocation()); + GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(itemStack.getId(), location); + GroundItem groundItem = collectedGroundItems.get(groundItemKey); + if (groundItem != null) + { + groundItem.setMine(true); + + boolean shouldNotify = config.onlyShowLoot() && config.highlightedColor().equals(getHighlighted( + groundItem.getName(), + groundItem.getGePrice(), + groundItem.getHaPrice())); + + if (config.notifyHighlightedDrops() && shouldNotify) + { + notifyHighlightedItem(groundItem); + } + } + } + } + + private GroundItem buildGroundItem(final Tile tile, final Item item) + { + // Collect the data for the item + final int itemId = item.getId(); + final ItemComposition itemComposition = itemManager.getItemComposition(itemId); + final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId; + final int alchPrice = Math.round(itemComposition.getPrice() * HIGH_ALCHEMY_CONSTANT); + int durationMillis; + if (client.isInInstancedRegion()) + { + durationMillis = INSTANCE_DURATION_MILLIS; + } + else if (!itemComposition.isTradeable() && realItemId != COINS) + { + durationMillis = UNTRADEABLE_DURATION_MILLIS; + } + else + { + durationMillis = NORMAL_DURATION_MILLIS; + } + + WorldPoint playerLocation = client.getLocalPlayer().getWorldLocation(); + + final GroundItem groundItem = GroundItem.builder() + .id(itemId) + .location(tile.getWorldLocation()) + .itemId(realItemId) + .quantity(item.getQuantity()) + .name(itemComposition.getName()) + .haPrice(alchPrice) + .height(-1) + .tradeable(itemComposition.isTradeable()) + .droppedInstant(Instant.now()) + .durationMillis(durationMillis) + .isAlwaysPrivate(client.isInInstancedRegion() || (!itemComposition.isTradeable() && realItemId != COINS)) + .isOwnedByPlayer(tile.getWorldLocation().equals(playerLocation)) + .build(); + + + // Update item price in case it is coins + if (realItemId == COINS) + { + groundItem.setHaPrice(1); + groundItem.setGePrice(1); + } + else + { + groundItem.setGePrice(itemManager.getItemPrice(realItemId)); + } + + return groundItem; + } + + private void reset() + { + // gets the hidden items from the text box in the config + hiddenItemList = Text.fromCSV(config.getHiddenItems()); + + // gets the highlighted items from the text box in the config + highlightedItemsList = Text.fromCSV(config.getHighlightItems()); + + highlightedItems = CacheBuilder.newBuilder() + .maximumSize(512L) + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(new WildcardMatchLoader(highlightedItemsList)); + + hiddenItems = CacheBuilder.newBuilder() + .maximumSize(512L) + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(new WildcardMatchLoader(hiddenItemList)); + + // Cache colors + priceChecks.clear(); + + if (config.insaneValuePrice() > 0) + { + priceChecks.put(config.insaneValuePrice(), config.insaneValueColor()); + } + + if (config.highValuePrice() > 0) + { + priceChecks.put(config.highValuePrice(), config.highValueColor()); + } + + if (config.mediumValuePrice() > 0) + { + priceChecks.put(config.mediumValuePrice(), config.mediumValueColor()); + } + + if (config.lowValuePrice() > 0) + { + priceChecks.put(config.lowValuePrice(), config.lowValueColor()); + } + + if (config.getHighlightOverValue() > 0) + { + priceChecks.put(config.getHighlightOverValue(), config.highlightedColor()); + } + } + + @Subscribe + public void onMenuEntryAdded(MenuEntryAdded event) + { + if (config.itemHighlightMode() != OVERLAY + && event.getOption().equals("Take") + && event.getType() == THIRD_OPTION) + { + int itemId = event.getIdentifier(); + Scene scene = client.getScene(); + Tile tile = scene.getTiles()[client.getPlane()][event.getActionParam0()][event.getActionParam1()]; + ItemLayer itemLayer = tile.getItemLayer(); + + if (itemLayer == null) + { + return; + } + + MenuEntry[] menuEntries = client.getMenuEntries(); + MenuEntry lastEntry = menuEntries[menuEntries.length - 1]; + + int quantity = 1; + Node current = itemLayer.getBottom(); + + while (current instanceof Item) + { + Item item = (Item) current; + if (item.getId() == itemId) + { + quantity = item.getQuantity(); + } + current = current.getNext(); + } + + final ItemComposition itemComposition = itemManager.getItemComposition(itemId); + final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemComposition.getId(); + final int itemPrice = itemManager.getItemPrice(realItemId); + final int price = itemPrice <= 0 ? itemComposition.getPrice() : itemPrice; + final int haPrice = Math.round(itemComposition.getPrice() * HIGH_ALCHEMY_CONSTANT) * quantity; + final int gePrice = quantity * price; + final Color hidden = getHidden(itemComposition.getName(), gePrice, haPrice, itemComposition.isTradeable()); + final Color highlighted = getHighlighted(itemComposition.getName(), gePrice, haPrice); + final Color color = getItemColor(highlighted, hidden); + final boolean canBeRecolored = highlighted != null || (hidden != null && config.recolorMenuHiddenItems()); + + if (color != null && canBeRecolored && !color.equals(config.defaultColor())) + { + final MenuHighlightMode mode = config.menuHighlightMode(); + + if (mode == BOTH || mode == OPTION) + { + lastEntry.setOption(ColorUtil.prependColorTag("Take", color)); + } + + if (mode == BOTH || mode == NAME) + { + String target = lastEntry.getTarget().substring(lastEntry.getTarget().indexOf(">") + 1); + lastEntry.setTarget(ColorUtil.prependColorTag(target, color)); + } + } + + if (config.showMenuItemQuantities() && itemComposition.isStackable() && quantity > 1) + { + lastEntry.setTarget(lastEntry.getTarget() + " (" + quantity + ")"); + } + + if (config.removeIgnored() && event.getOption().equals("Take") && hiddenItemList.contains(Text.removeTags(event.getTarget()))) + { + menuEntries = removeOption(event.getOption(), event.getTarget()); + } + + client.setMenuEntries(menuEntries); + } + } + + private MenuEntry[] removeOption(String option, String target) + { + MenuEntry[] entries = client.getMenuEntries(); + int j = 0; + if (entries.length > 1) + { + MenuEntry[] newEntries = new MenuEntry[entries.length - 1]; + for (int i = 0; i < entries.length; ++i) + { + if (!(entries[i].getOption().equals(option) && entries[i].getTarget().equals(target))) + { + newEntries[j++] = entries[i]; + } + } + + return newEntries; + } + else + { + return entries; + } + } + + void updateList(String item, boolean hiddenList) + { + final Set hiddenItemSet = new HashSet<>(hiddenItemList); + final Set highlightedItemSet = new HashSet<>(highlightedItemsList); + + if (hiddenList) + { + highlightedItemSet.removeIf(item::equalsIgnoreCase); + } + else + { + hiddenItemSet.removeIf(item::equalsIgnoreCase); + } + + final Set items = hiddenList ? hiddenItemSet : highlightedItemSet; + + if (!items.removeIf(item::equalsIgnoreCase)) + { + items.add(item); + } + + config.setHiddenItems(Text.toCSV(hiddenItemSet)); + config.setHighlightedItem(Text.toCSV(highlightedItemSet)); + } + + Color getHighlighted(String item, int gePrice, int haPrice) + { + if (TRUE.equals(highlightedItems.getUnchecked(item))) + { + return config.highlightedColor(); + } + + // Explicit hide takes priority over implicit highlight + if (TRUE.equals(hiddenItems.getUnchecked(item))) + { + return null; + } + + ValueCalculationMode mode = config.valueCalculationMode(); + for (Map.Entry entry : priceChecks.entrySet()) + { + switch (mode) + { + case GE: + if (gePrice > entry.getKey()) + { + return entry.getValue(); + } + break; + case HA: + if (haPrice > entry.getKey()) + { + return entry.getValue(); + } + break; + default: // case HIGHEST + if (gePrice > entry.getKey() || haPrice > entry.getKey()) + { + return entry.getValue(); + } + break; + } + } + + return null; + } + + Color getHidden(String item, int gePrice, int haPrice, boolean isTradeable) + { + final boolean isExplicitHidden = TRUE.equals(hiddenItems.getUnchecked(item)); + final boolean isExplicitHighlight = TRUE.equals(highlightedItems.getUnchecked(item)); + final boolean canBeHidden = gePrice > 0 || isTradeable || !config.dontHideUntradeables(); + final boolean underGe = gePrice < config.getHideUnderValue(); + final boolean underHa = haPrice < config.getHideUnderValue(); + + // Explicit highlight takes priority over implicit hide + return isExplicitHidden || (!isExplicitHighlight && canBeHidden && underGe && underHa) + ? config.hiddenColor() + : null; + } + + private int getGePriceFromItemId(int itemId) + { + final ItemComposition itemComposition = itemManager.getItemComposition(itemId); + final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId; + + return itemManager.getItemPrice(realItemId); + } + + private boolean isItemIdHidden(int itemId) + { + final ItemComposition itemComposition = itemManager.getItemComposition(itemId); + final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId; + final int alchPrice = Math.round(itemComposition.getPrice() * HIGH_ALCHEMY_CONSTANT); + final int gePrice = itemManager.getItemPrice(realItemId); + + return getHidden(itemComposition.getName(), gePrice, alchPrice, itemComposition.isTradeable()) != null; + } + + private int getCollapsedItemQuantity(int itemId, String item) + { + final ItemComposition itemComposition = itemManager.getItemComposition(itemId); + final boolean itemNameIncludesQuantity = Pattern.compile("\\(\\d+\\)").matcher(itemComposition.getName()).find(); + + Matcher matcher = Pattern.compile("\\((\\d+)\\)").matcher(item); + int matches = 0; + String lastMatch = "1"; + while (matcher.find()) + { + // so that "Prayer Potion (4)" returns 1 instead of 4 and "Coins (25)" returns 25 instead of 1 + if (!itemNameIncludesQuantity || matches >= 1) + lastMatch = matcher.group(1); + + matches++; + } + + return Integer.parseInt(lastMatch); + } + + Color getItemColor(Color highlighted, Color hidden) + { + if (highlighted != null) + { + return highlighted; + } + + if (hidden != null) + { + return hidden; + } + + return config.defaultColor(); + } + + @Subscribe + public void onFocusChanged(FocusChanged focusChanged) + { + if (!focusChanged.isFocused()) + { + setHotKeyPressed(false); + } + } + + private void notifyHighlightedItem(GroundItem item) + { + final Player local = client.getLocalPlayer(); + final StringBuilder notificationStringBuilder = new StringBuilder() + .append("[") + .append(local.getName()) + .append("] received a highlighted drop: ") + .append(item.getName()); + + if (item.getQuantity() > 1) + { + notificationStringBuilder.append(" x ").append(item.getQuantity()); + + + if (item.getQuantity() > (int) Character.MAX_VALUE) + { + notificationStringBuilder.append(" (Lots!)"); + } + else + { + notificationStringBuilder.append(" (") + .append(StackFormatter.quantityToStackSize(item.getQuantity())) + .append(")"); + } + } + + notificationStringBuilder.append("!"); + notifier.notify(notificationStringBuilder.toString()); + } +}