From 70fd10b6bbade4fdf8dea63ec816128e89f972de Mon Sep 17 00:00:00 2001 From: Manatsawin Hanmongkolchai Date: Mon, 4 Nov 2019 02:50:09 +0700 Subject: [PATCH] thieving: Add chest respawn timer (#1855) * Add chest respawn timer * Update copyright * Rearrange config * Remove chestOverlay when unloading * Avoid reading settings every frame * Minor fixes * Remove eventbus from overlay, store endTime only and respawnTime is computed * Update chestOverlay on startup * Invert the percent to ensure we're counting up * Compute respawn for every world This should be more optimized - we use less memory and iterate less * Remove lazy getter * Bugfixes --- .../client/plugins/thieving/Chest.java | 74 +++++++++++ .../client/plugins/thieving/ChestOverlay.java | 125 ++++++++++++++++++ .../client/plugins/thieving/ChestRespawn.java | 54 ++++++++ .../plugins/thieving/ThievingConfig.java | 56 +++++++- .../plugins/thieving/ThievingPlugin.java | 62 ++++++++- 5 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/thieving/Chest.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestRespawn.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/Chest.java b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/Chest.java new file mode 100644 index 0000000000..9c0f9592f2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/Chest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019, whs + * 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.thieving; + +import com.google.common.collect.ImmutableMap; +import java.time.Duration; +import java.util.Map; +import lombok.Getter; +import net.runelite.api.ObjectID; + +enum Chest +{ + TEN_COIN(Duration.ofMillis(6000), ObjectID.CHEST_11735), + FIFTY_COIN(Duration.ofMillis(46000), ObjectID.CHEST_11737), + NATURE_RUNE(Duration.ofMillis(10000), ObjectID.CHEST_11736), + STEEL_ARROWTIPS(Duration.ofMillis(77000), ObjectID.CHEST_11742), + AVERAGE_CHEST(Duration.ofMillis(90000), ObjectID.CHEST_22697), + BLOOD_RUNE(Duration.ofMillis(120000), ObjectID.CHEST_11738), + ARDOUGNE_CASTLE(Duration.ofMillis(400000), ObjectID.CHEST_11739), // FIXME: Please time + RICH_CHEST(Duration.ofMillis(300000), ObjectID.CHEST_22681), // FIXME: Please time + ROGUE_CASTLE(Duration.ofMillis(10000), ObjectID.CHEST_26757); // FIXME: Please time + + private static final Map CHESTS; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (Chest chest : values()) + { + for (int id : chest.ids) + { + builder.put(id, chest); + } + } + CHESTS = builder.build(); + } + + @Getter + private final Duration respawnTime; + private final int[] ids; + + Chest(Duration respawnTime, int... ids) + { + this.respawnTime = respawnTime; + this.ids = ids; + } + + static Chest of(int id) + { + return CHESTS.get(id); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestOverlay.java new file mode 100644 index 0000000000..20d084df2b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestOverlay.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019, whs + * 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.thieving; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Iterator; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.AccessLevel; +import lombok.Setter; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +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.components.ProgressPieComponent; + +@Singleton +class ChestOverlay extends Overlay +{ + private final Client client; + private final List respawns; + + @Setter(AccessLevel.PACKAGE) + private Color pieFillColor; + @Setter(AccessLevel.PACKAGE) + private Color pieBorderColor; + @Setter(AccessLevel.PACKAGE) + private boolean respawnPieInverted; + @Setter(AccessLevel.PACKAGE) + private int respawnPieDiameter; + + @Inject + private ChestOverlay(final Client client, final ThievingPlugin plugin) + { + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + this.respawns = plugin.getRespawns(); + this.client = client; + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (respawns.isEmpty()) + { + return null; + } + + Instant now = Instant.now(); + for (Iterator it = respawns.iterator(); it.hasNext(); ) + { + ChestRespawn chestRespawn = it.next(); + + float percent = 1.0f - (now.until(chestRespawn.getEndTime(), ChronoUnit.MILLIS) / (float) chestRespawn.getRespawnTime()); + if (percent > 1.0f) + { + it.remove(); + continue; + } + + if (chestRespawn.getWorld() != client.getWorld()) + { + continue; + } + + WorldPoint worldPoint = chestRespawn.getWorldPoint(); + LocalPoint loc = LocalPoint.fromWorld(client, worldPoint); + if (loc == null) + { + continue; + } + + Point point = Perspective.localToCanvas(client, loc, client.getPlane(), 0); + if (point == null) + { + continue; + } + + if (respawnPieInverted) + { + percent = 1.0f - percent; + } + + ProgressPieComponent ppc = new ProgressPieComponent(); + ppc.setDiameter(respawnPieDiameter); + ppc.setBorderColor(pieBorderColor); + ppc.setFill(pieFillColor); + ppc.setPosition(point); + ppc.setProgress(percent); + ppc.render(graphics); + } + return null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestRespawn.java b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestRespawn.java new file mode 100644 index 0000000000..f8231dd5c0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ChestRespawn.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019, whs + * 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.thieving; + +import java.time.Instant; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +@RequiredArgsConstructor +@Getter(AccessLevel.PACKAGE) +class ChestRespawn +{ + private final Chest chest; + private final WorldPoint worldPoint; + private final Instant endTime; + private final int world; + + private long respawnTime = -1; + + long getRespawnTime() + { + if (respawnTime != -1) + { + return respawnTime; + } + + respawnTime = chest.getRespawnTime().toMillis(); + return respawnTime; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingConfig.java index c44e3f844f..31e6709818 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingConfig.java @@ -26,15 +26,18 @@ */ package net.runelite.client.plugins.thieving; +import java.awt.Color; +import net.runelite.client.config.Alpha; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; @ConfigGroup("thieving") public interface ThievingConfig extends Config { - @ConfigItem - ( + @ConfigItem( position = 1, keyName = "statTimeout", name = "Reset stats (minutes)", @@ -44,4 +47,53 @@ public interface ThievingConfig extends Config { return 5; } + + @ConfigSection( + name = "Chest", + description = "", + position = 2, + keyName = "chestSection" + ) + default boolean chestSection() + { + return false; + } + + @Alpha + @ConfigItem( + keyName = "respawnColor", + name = "Respawn timer color", + description = "Configures the color of the respawn timer", + section = "chestSection" + ) + default Color respawnColor() + { + return Color.YELLOW; + } + + @ConfigItem( + keyName = "respawnPieInverted", + name = "Invert respawn timer", + description = "Configures whether the respawn timer goes from empty to full or the other way around", + section = "chestSection" + ) + default boolean respawnPieInverted() + { + return false; + } + + @Range( + min = 1, + max = 50 + ) + @ConfigItem( + keyName = "respawnPieDiameter", + name = "Respawn pie diameter", + description = "Configures how big the respawn timer pie is", + section = "chestSection" + ) + default int respawnPieDiameter() + { + return 30; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingPlugin.java index c7718bfaa7..a92fd4f3e1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/thieving/ThievingPlugin.java @@ -26,16 +26,23 @@ */ package net.runelite.client.plugins.thieving; +import com.google.inject.Provides; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; import lombok.AccessLevel; import lombok.Getter; -import com.google.inject.Provides; import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.GameState; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameObjectDespawned; +import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; @@ -46,8 +53,7 @@ import net.runelite.client.plugins.PluginType; import net.runelite.client.plugins.xptracker.XpTrackerPlugin; import net.runelite.client.ui.overlay.OverlayManager; -@PluginDescriptor -( +@PluginDescriptor( name = "Thieving", description = "Show thieving overlay", tags = {"overlay", "skilling", "thieving", "pickpocketing"}, @@ -58,12 +64,18 @@ import net.runelite.client.ui.overlay.OverlayManager; @PluginDependency(XpTrackerPlugin.class) public class ThievingPlugin extends Plugin { + @Inject + private Client client; + @Inject private ThievingConfig config; @Inject private ThievingOverlay overlay; + @Inject + private ChestOverlay chestOverlay; + @Inject private OverlayManager overlayManager; @@ -73,7 +85,11 @@ public class ThievingPlugin extends Plugin @Getter(AccessLevel.PACKAGE) private ThievingSession session; + @Getter(AccessLevel.PACKAGE) + private final List respawns = new ArrayList<>(); + private int statTimeout; + private boolean recentlyLoggedIn = false; @Provides ThievingConfig getConfig(ConfigManager configManager) @@ -88,8 +104,14 @@ public class ThievingPlugin extends Plugin this.statTimeout = config.statTimeout(); + chestOverlay.setPieFillColor(config.respawnColor()); + chestOverlay.setPieBorderColor(config.respawnColor().darker()); + chestOverlay.setRespawnPieInverted(config.respawnPieInverted()); + chestOverlay.setRespawnPieDiameter(config.respawnPieDiameter()); + session = null; overlayManager.add(overlay); + overlayManager.add(chestOverlay); } @Override @@ -98,18 +120,31 @@ public class ThievingPlugin extends Plugin eventBus.unregister(this); overlayManager.remove(overlay); + overlayManager.remove(chestOverlay); session = null; } private void addSubscriptions() { eventBus.subscribe(ConfigChanged.class, this, this::onConfigChanged); + eventBus.subscribe(GameStateChanged.class, this, this::onGameStateChanged); eventBus.subscribe(GameTick.class, this, this::onGameTick); eventBus.subscribe(ChatMessage.class, this, this::onChatMessage); + eventBus.subscribe(GameObjectDespawned.class, this, this::onGameObjectDespawned); + } + + private void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOGGED_IN) + { + recentlyLoggedIn = true; + } } private void onGameTick(GameTick gameTick) { + recentlyLoggedIn = false; + if (session == null || this.statTimeout == 0) { return; @@ -156,6 +191,23 @@ public class ThievingPlugin extends Plugin } } + private void onGameObjectDespawned(GameObjectDespawned event) + { + if (client.getGameState() != GameState.LOGGED_IN || recentlyLoggedIn) + { + return; + } + + final GameObject object = event.getGameObject(); + + Chest chest = Chest.of(object.getId()); + if (chest != null) + { + ChestRespawn chestRespawn = new ChestRespawn(chest, object.getWorldLocation(), Instant.now().plus(chest.getRespawnTime()), client.getWorld()); + respawns.add(chestRespawn); + } + } + private void onConfigChanged(ConfigChanged event) { if (!"thieving".equals(event.getGroup())) @@ -164,6 +216,10 @@ public class ThievingPlugin extends Plugin } this.statTimeout = config.statTimeout(); + chestOverlay.setPieFillColor(config.respawnColor()); + chestOverlay.setPieBorderColor(config.respawnColor().darker()); + chestOverlay.setRespawnPieInverted(config.respawnPieInverted()); + chestOverlay.setRespawnPieDiameter(config.respawnPieDiameter()); } }