From 68fa0ef253f2643a05df85d5d5bdf7aac8fe58db Mon Sep 17 00:00:00 2001 From: James <38226001+james-munson@users.noreply.github.com> Date: Mon, 20 May 2019 13:31:20 -0700 Subject: [PATCH] Added sorting on the item overlay, items now appear in the order that they'll be picked up (#320) Changed item menu sorting: Hidden items now appear at the bottom of the menu, so that hidden items don't get picked up over lower value non-hidden items Added a config option to put hidden items below "Walk here" this option makes it so that you can't accidentally pick up hidden items Added a config option to sort by GE value this option means that you'll always pick up highest GE value items first with left click this option also sorts the overlay by GE value --- .../grounditems/GroundItemsConfig.java | 900 ++++----- .../grounditems/GroundItemsOverlay.java | 958 +++++----- .../grounditems/GroundItemsPlugin.java | 1608 +++++++++-------- 3 files changed, 1793 insertions(+), 1673 deletions(-) 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()); + } +}