diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java index 2b8ba7c95f..b7841dc5f7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java @@ -129,4 +129,24 @@ public interface NpcIndicatorsConfig extends Config { return false; } + + @ConfigItem( + position = 7, + keyName = "notifyOnRespawn", + name = "Notify on Respawn", + description = "Enable notification on respawn") + default boolean getNotifyOnRespawn() + { + return false; + } + + @ConfigItem( + position = 8, + keyName = "notifyOnRespawnDelay", + name = "Notification Delay", + description = "Notify when NPC is x ms from respawning") + default int getNotifyOnRespawnDelay() + { + return -1; + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java index cdcdd34416..aff63ec15f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java @@ -29,6 +29,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.inject.Provides; import java.awt.Color; + +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -38,6 +41,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Singleton; import lombok.AccessLevel; @@ -62,6 +66,7 @@ import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.NpcDefinitionChanged; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; +import net.runelite.client.Notifier; import net.runelite.client.callback.ClientThread; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; @@ -84,6 +89,16 @@ public class NpcIndicatorsPlugin extends Plugin { private static final int MAX_ACTOR_VIEW_RANGE = 15; + // Estimated time of a game tick in seconds + private static final double ESTIMATED_TICK_LENGTH = 0.6; + + private static final NumberFormat TIME_LEFT_FORMATTER = DecimalFormat.getInstance(Locale.getDefault()); + + static + { + ((DecimalFormat)TIME_LEFT_FORMATTER).applyPattern("#0.0"); + } + // Option added to NPC menu private static final String TAG = "Tag"; private static final String UNTAG = "Un-tag"; @@ -126,12 +141,21 @@ public class NpcIndicatorsPlugin extends Plugin @Setter(AccessLevel.PACKAGE) private boolean hotKeyPressed = false; + @Inject + private Notifier notifier; + /** * NPCs to highlight */ @Getter(AccessLevel.PACKAGE) private final Set highlightedNpcs = new HashSet<>(); + /** + * NPCs to notify when close to spawning + */ + @Getter(AccessLevel.PACKAGE) + private final Set pendingNotificationNpcs = new HashSet<>(); + /** * Dead NPCs that should be displayed with a respawn indicator if the config is on. */ @@ -239,6 +263,7 @@ public class NpcIndicatorsPlugin extends Plugin overlayManager.remove(npcSceneOverlay); overlayManager.remove(npcMinimapOverlay); deadNpcsToDisplay.clear(); + pendingNotificationNpcs.clear(); memorizedNpcs.clear(); spawnedNpcsThisTick.clear(); despawnedNpcsThisTick.clear(); @@ -269,6 +294,7 @@ public class NpcIndicatorsPlugin extends Plugin { highlightedNpcs.clear(); deadNpcsToDisplay.clear(); + pendingNotificationNpcs.clear(); memorizedNpcs.forEach((id, npc) -> npc.setDiedOnTick(-1)); lastPlayerLocation = null; skipNextSpawnCheck = true; @@ -398,6 +424,12 @@ public class NpcIndicatorsPlugin extends Plugin if (memorizedNpcs.containsKey(npc.getIndex())) { despawnedNpcsThisTick.add(npc); + MemorizedNpc mn = memorizedNpcs.get(npc.getIndex()); + + if (!mn.getPossibleRespawnLocations().isEmpty()) + { + pendingNotificationNpcs.add(mn); + } } highlightedNpcs.remove(npc); @@ -417,6 +449,7 @@ public class NpcIndicatorsPlugin extends Plugin { removeOldHighlightedRespawns(); validateSpawnedNpcs(); + checkNotifyNpcs(); lastTickUpdate = Instant.now(); lastPlayerLocation = client.getLocalPlayer().getWorldLocation(); } @@ -494,6 +527,16 @@ public class NpcIndicatorsPlugin extends Plugin highlightedNpcs.remove(npc); } + public double getTimeLeftForNpc(MemorizedNpc npc) + { + final Instant now = Instant.now(); + final double baseTick = NpcIndicatorsPlugin.ESTIMATED_TICK_LENGTH * ( + npc.getDiedOnTick() + npc.getRespawnTime() - client.getTickCount() + ); + final double sinceLast = (now.toEpochMilli() - lastTickUpdate.toEpochMilli()) / 1000.0; + return Math.max(0.0, baseTick - sinceLast); + } + private void memorizeNpc(NPC npc) { final int npcIndex = npc.getIndex(); @@ -569,6 +612,33 @@ public class NpcIndicatorsPlugin extends Plugin } } + public String formatTime(double time) + { + return TIME_LEFT_FORMATTER.format(time); + } + + private void checkNotifyNpcs() + { + if (!config.getNotifyOnRespawn()) + { + return; + } + + final double notifyDelay = ((double)config.getNotifyOnRespawnDelay()) / 1000; + final String notifyDelayStr = notifyDelay > 0 + ? " is less than " + formatTime(notifyDelay) + " seconds from respawn" + : " respawned."; + + for (MemorizedNpc npc : pendingNotificationNpcs) + { + if (getTimeLeftForNpc(npc) <= notifyDelay) + { + pendingNotificationNpcs.remove(npc); + notifier.notify(npc.getNpcNames() + notifyDelayStr); + } + } + } + private void validateSpawnedNpcs() { if (skipNextSpawnCheck) @@ -577,6 +647,7 @@ public class NpcIndicatorsPlugin extends Plugin } else { + for (NPC npc : despawnedNpcsThisTick) { if (!teleportGraphicsObjectSpawnedThisTick.isEmpty() && teleportGraphicsObjectSpawnedThisTick.contains(npc.getWorldLocation())) @@ -666,4 +737,4 @@ public class NpcIndicatorsPlugin extends Plugin this.highlightMenuNames = config.highlightMenuNames(); this.showRespawnTimer = config.showRespawnTimer(); } -} \ No newline at end of file +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java index 89d3cf051d..5f539664f8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java @@ -63,12 +63,7 @@ public class NpcSceneOverlay extends Overlay // a dark background private static final Color TEXT_COLOR = Color.WHITE; - private static final NumberFormat TIME_LEFT_FORMATTER = DecimalFormat.getInstance(Locale.US); - static - { - ((DecimalFormat) TIME_LEFT_FORMATTER).applyPattern("#0.0"); - } private final Client client; private final NpcIndicatorsPlugin plugin; @@ -128,11 +123,8 @@ public class NpcSceneOverlay extends Overlay OverlayUtil.renderPolygon(graphics, poly, color); } - final Instant now = Instant.now(); - final double baseTick = ((npc.getDiedOnTick() + npc.getRespawnTime()) - client.getTickCount()) * (Constants.GAME_TICK_LENGTH / 1000.0); - final double sinceLast = (now.toEpochMilli() - plugin.getLastTickUpdate().toEpochMilli()) / 1000.0; - final double timeLeft = Math.max(0.0, baseTick - sinceLast); - final String timeLeftStr = TIME_LEFT_FORMATTER.format(timeLeft); + + final String timeLeftStr = plugin.formatTime(plugin.getTimeLeftForNpc(npc)); final int textWidth = graphics.getFontMetrics().stringWidth(timeLeftStr); final int textHeight = graphics.getFontMetrics().getAscent(); diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPluginTest.java index f5a6b06000..cbeff49ddd 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPluginTest.java @@ -33,6 +33,8 @@ import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; import net.runelite.api.Client; import static org.junit.Assert.assertEquals; + +import net.runelite.client.Notifier; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,6 +56,10 @@ public class NpcIndicatorsPluginTest @Bind private NpcIndicatorsConfig npcIndicatorsConfig; + @Mock + @Bind + private Notifier notifier; + @Inject private NpcIndicatorsPlugin npcIndicatorsPlugin;