From c801c055510cbaf33269141ef537524499c8f287 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 26 Jul 2019 13:41:39 -0400 Subject: [PATCH] grounditems: add despawn timers Co-authored-by: Matthew Smith --- .../plugins/grounditems/GroundItem.java | 17 +++- .../grounditems/GroundItemsConfig.java | 11 +++ .../grounditems/GroundItemsOverlay.java | 97 +++++++++++++++++++ .../grounditems/GroundItemsPlugin.java | 34 ++++++- .../client/plugins/grounditems/LootType.java | 32 ++++++ 5 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java index 2d81060bcd..fab3857fa6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java @@ -24,6 +24,9 @@ */ package net.runelite.client.plugins.grounditems; +import java.time.Instant; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.Builder; import lombok.Data; import lombok.Value; @@ -43,7 +46,14 @@ class GroundItem private int gePrice; private int offset; private boolean tradeable; - private boolean isMine; + @Nonnull + private LootType lootType; + /** + * Is dropped by me + */ + private boolean isDropped; + @Nullable + private Instant spawnTime; int getHaPrice() { @@ -55,6 +65,11 @@ class GroundItem return gePrice * quantity; } + boolean isMine() + { + return lootType != LootType.UNKNOWN; + } + @Value static class GroundItemKey { 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 f9c3a79e85..41cc1afa7f 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 @@ -358,4 +358,15 @@ public interface GroundItemsConfig extends Config { return false; } + + @ConfigItem( + keyName = "groundItemTimers", + name = "Show despawn timers", + description = "Shows despawn timers for items you've dropped and received as loot", + position = 28 + ) + default boolean groundItemTimers() + { + 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 1406bb1747..9d909c300b 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 @@ -31,6 +31,9 @@ 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.time.temporal.ChronoUnit; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; @@ -50,8 +53,10 @@ 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; +import org.apache.commons.lang3.ArrayUtils; public class GroundItemsOverlay extends Overlay { @@ -66,6 +71,13 @@ public class GroundItemsOverlay extends Overlay private static final int STRING_GAP = 15; // Size of the hidden/highlight boxes private static final int RECTANGLE_SIZE = 8; + private static final Color PUBLIC_TIMER_COLOR = Color.YELLOW; + private static final Color PRIVATE_TIMER_COLOR = Color.GREEN; + private static final int TIMER_OVERLAY_DIAMETER = 10; + private static final Duration DESPAWN_TIME_INSTANCE = Duration.ofMinutes(30); + private static final Duration DESPAWN_TIME_LOOT = Duration.ofMinutes(2); + private static final Duration DESPAWN_TIME_DROP = Duration.ofMinutes(3); + private static final int KRAKEN_REGION = 9116; private final Client client; private final GroundItemsPlugin plugin; @@ -73,6 +85,7 @@ public class GroundItemsOverlay extends Overlay 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 @@ -163,6 +176,7 @@ public class GroundItemsOverlay extends Overlay plugin.setHighlightBoxBounds(null); final boolean onlyShowLoot = config.onlyShowLoot(); + final boolean groundItemTimers = config.groundItemTimers(); for (GroundItem item : groundItemList) { @@ -333,6 +347,11 @@ public class GroundItemsOverlay extends Overlay drawRectangle(graphics, itemHighlightBox, topItem && mouseInHighlightBox ? Color.GREEN : color, highlighted != null, false); } + if (groundItemTimers || plugin.isHotKeyPressed()) + { + drawTimerOverlay(graphics, textX, textY, item); + } + textComponent.setText(itemString); textComponent.setColor(color); textComponent.setPosition(new java.awt.Point(textX, textY)); @@ -342,6 +361,79 @@ public class GroundItemsOverlay extends Overlay return null; } + private void drawTimerOverlay(Graphics2D graphics, int textX, int textY, GroundItem groundItem) + { + // We can only accurately guess despawn times for our own pvm loot and dropped items + if (groundItem.getLootType() != LootType.PVM && !groundItem.isDropped()) + { + return; + } + + // Loot appears to others after 1 minute, and despawns after 2 minutes + // Dropped items appear to others after 1 minute, and despawns after 3 minutes + // Items in instances never appear to anyone and despawn after 30 minutes + + Instant spawnTime = groundItem.getSpawnTime(); + if (spawnTime == null) + { + return; + } + + Instant despawnTime; + Instant now = Instant.now(); + Color fillColor; + if (client.isInInstancedRegion()) + { + // Items in the Kraken instance appear to never despawn? + if (isInKraken()) + { + return; + } + + despawnTime = spawnTime.plus(DESPAWN_TIME_INSTANCE); + fillColor = PRIVATE_TIMER_COLOR; + } + else + { + if (groundItem.isDropped()) + { + despawnTime = spawnTime.plus(DESPAWN_TIME_DROP); + } + else + { + despawnTime = spawnTime.plus(DESPAWN_TIME_LOOT); + } + + // If it has not yet been a minute, the item is private + if (spawnTime.plus(1, ChronoUnit.MINUTES).isAfter(now)) + { + fillColor = PRIVATE_TIMER_COLOR; + } + else + { + fillColor = PUBLIC_TIMER_COLOR; + } + } + + if (now.isBefore(spawnTime) || now.isAfter(despawnTime)) + { + // that's weird + return; + } + + float percent = (float) (now.toEpochMilli() - spawnTime.toEpochMilli()) / (despawnTime.toEpochMilli() - spawnTime.toEpochMilli()); + + progressPieComponent.setDiameter(TIMER_OVERLAY_DIAMETER); + // Shift over to not be on top of the text + int x = textX - TIMER_OVERLAY_DIAMETER; + int y = textY - TIMER_OVERLAY_DIAMETER / 2; + progressPieComponent.setPosition(new Point(x, y)); + progressPieComponent.setFill(fillColor); + progressPieComponent.setBorderColor(fillColor); + progressPieComponent.setProgress(1 - percent); // inverse so pie drains over time + progressPieComponent.render(graphics); + } + private void drawRectangle(Graphics2D graphics, Rectangle rect, Color color, boolean inList, boolean hiddenBox) { graphics.setColor(Color.BLACK); @@ -379,4 +471,9 @@ public class GroundItemsOverlay extends Overlay } + private boolean isInKraken() + { + return ArrayUtils.contains(client.getMapRegions(), KRAKEN_REGION); + } + } 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 4830c3d59d..22899c3a69 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 @@ -27,10 +27,12 @@ package net.runelite.client.plugins.grounditems; import com.google.common.cache.CacheBuilder; import com.google.common.cache.LoadingCache; +import com.google.common.collect.EvictingQueue; 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; @@ -38,6 +40,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; @@ -67,6 +70,7 @@ 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.api.events.MenuOptionClicked; import net.runelite.client.Notifier; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; @@ -164,6 +168,7 @@ public class GroundItemsPlugin extends Plugin private final Map priceChecks = new LinkedHashMap<>(); private LoadingCache highlightedItems; private LoadingCache hiddenItems; + private final Queue droppedItemQueue = EvictingQueue.create(16); // recently dropped items @Provides GroundItemsConfig provideConfig(ConfigManager configManager) @@ -226,6 +231,7 @@ public class GroundItemsPlugin extends Plugin if (existing != null) { existing.setQuantity(existing.getQuantity() + groundItem.getQuantity()); + // The spawn time remains set at the oldest spawn } boolean shouldNotify = !config.onlyShowLoot() && config.highlightedColor().equals(getHighlighted( @@ -259,6 +265,10 @@ public class GroundItemsPlugin extends Plugin else { groundItem.setQuantity(groundItem.getQuantity() - item.getQuantity()); + // When picking up an item when multiple stacks appear on the ground, + // it is not known which item is picked up, so we invalidate the spawn + // time + groundItem.setSpawnTime(null); } } @@ -283,14 +293,14 @@ public class GroundItemsPlugin extends Plugin public void onNpcLootReceived(NpcLootReceived npcLootReceived) { Collection items = npcLootReceived.getItems(); - lootReceived(items); + lootReceived(items, LootType.PVM); } @Subscribe public void onPlayerLootReceived(PlayerLootReceived playerLootReceived) { Collection items = playerLootReceived.getItems(); - lootReceived(items); + lootReceived(items, LootType.PVP); } @Subscribe @@ -341,7 +351,7 @@ public class GroundItemsPlugin extends Plugin }).toArray(MenuEntry[]::new)); } - private void lootReceived(Collection items) + private void lootReceived(Collection items, LootType lootType) { for (ItemStack itemStack : items) { @@ -350,7 +360,7 @@ public class GroundItemsPlugin extends Plugin GroundItem groundItem = collectedGroundItems.get(groundItemKey); if (groundItem != null) { - groundItem.setMine(true); + groundItem.setLootType(lootType); boolean shouldNotify = config.onlyShowLoot() && config.highlightedColor().equals(getHighlighted( groundItem.getName(), @@ -372,6 +382,7 @@ public class GroundItemsPlugin extends Plugin final ItemComposition itemComposition = itemManager.getItemComposition(itemId); final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId; final int alchPrice = Math.round(itemComposition.getPrice() * Constants.HIGH_ALCHEMY_MULTIPLIER); + final boolean dropped = tile.getWorldLocation().equals(client.getLocalPlayer().getWorldLocation()) && droppedItemQueue.remove(itemId); final GroundItem groundItem = GroundItem.builder() .id(itemId) @@ -382,6 +393,9 @@ public class GroundItemsPlugin extends Plugin .haPrice(alchPrice) .height(tile.getItemLayer().getHeight()) .tradeable(itemComposition.isTradeable()) + .lootType(LootType.UNKNOWN) + .isDropped(dropped) + .spawnTime(Instant.now()) .build(); @@ -667,4 +681,16 @@ public class GroundItemsPlugin extends Plugin notificationStringBuilder.append("!"); notifier.notify(notificationStringBuilder.toString()); } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked) + { + if (menuOptionClicked.getMenuAction() == MenuAction.ITEM_DROP) + { + int itemId = menuOptionClicked.getId(); + // Keep a queue of recently dropped items to better detect + // item spawns that are drops + droppedItemQueue.add(itemId); + } + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java new file mode 100644 index 0000000000..abd6c815e0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019, 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; + +enum LootType +{ + UNKNOWN, + PVP, + PVM; +}