diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierConfig.java index f8ad288e45..6e5a9f6974 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierConfig.java @@ -32,10 +32,10 @@ import net.runelite.client.config.ConfigItem; public interface IdleNotifierConfig extends Config { @ConfigItem( - keyName = "animationidle", - name = "Idle Animation Notifications", - description = "Configures if idle animation notifications are enabled", - position = 1 + keyName = "animationidle", + name = "Idle Animation Notifications", + description = "Configures if idle animation notifications are enabled", + position = 1 ) default boolean animationIdle() { @@ -43,21 +43,32 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "animationidlesound", - name = "Idle Animation Sound", - description = "Plays a custom sound accompanying Idle Animation notifications", - position = 2 + keyName = "outOfItemsIdle", + name = "Out of Items Idle Notifications", + position = 2, + description = "Configures if notifications for running out of items for another action are enabled." ) - default boolean animationIdleSound() + default boolean outOfItemsIdle() + { + return true; + } + + @ConfigItem( + keyName = "animationidlesound", + name = "Idle Animation Sound", + description = "Plays a custom sound accompanying Idle Animation notifications", + position = 3 + ) + default boolean animationIdleSound() { return false; } @ConfigItem( - keyName = "interactionidle", - name = "Idle Interaction Notifications", - description = "Configures if idle interaction notifications are enabled e.g. combat, fishing", - position = 3 + keyName = "interactionidle", + name = "Idle Interaction Notifications", + description = "Configures if idle interaction notifications are enabled e.g. combat, fishing", + position = 4 ) default boolean interactionIdle() { @@ -65,21 +76,21 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "interactionidlesound", - name = "Idle Interaction Sound", - description = "Plays a custom sound accompanying Idle Interaction notifications", - position = 4 + keyName = "interactionidlesound", + name = "Idle Interaction Sound", + description = "Plays a custom sound accompanying Idle Interaction notifications", + position = 5 ) - default boolean interactionIdleSound() + default boolean interactionIdleSound() { return false; } @ConfigItem( - keyName = "logoutidle", - name = "Idle Logout Notifications", - description = "Configures if the idle logout notifications are enabled", - position = 5 + keyName = "logoutidle", + name = "Idle Logout Notifications", + description = "Configures if the idle logout notifications are enabled", + position = 6 ) default boolean logoutIdle() { @@ -87,21 +98,21 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "outofcombatsound", - name = "Out of Combat Sound", - description = "Plays a custom sound whenever you leave combat", - position = 6 + keyName = "outofcombatsound", + name = "Out of Combat Sound", + description = "Plays a custom sound whenever you leave combat", + position = 7 ) - default boolean outOfCombatSound() + default boolean outOfCombatSound() { return false; } @ConfigItem( - position = 7, - keyName = "skullNotification", - name = "Skull Notification", - description = "Receive a notification when you skull." + position = 8, + keyName = "skullNotification", + name = "Skull Notification", + description = "Receive a notification when you skull." ) default boolean showSkullNotification() { @@ -109,10 +120,10 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - position = 8, - keyName = "unskullNotification", - name = "Unskull Notification", - description = "Receive a notification when you unskull." + position = 9, + keyName = "unskullNotification", + name = "Unskull Notification", + description = "Receive a notification when you unskull." ) default boolean showUnskullNotification() { @@ -120,10 +131,10 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "timeout", - name = "Idle Notification Delay (ms)", - description = "The notification delay after the player is idle", - position = 9 + keyName = "timeout", + name = "Idle Notification Delay (ms)", + description = "The notification delay after the player is idle", + position = 10 ) default int getIdleNotificationDelay() { @@ -131,10 +142,10 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "hitpoints", - name = "Hitpoints Notification Threshold", - description = "The amount of hitpoints to send a notification at. A value of 0 will disable notification.", - position = 10 + keyName = "hitpoints", + name = "Hitpoints Notification Threshold", + description = "The amount of hitpoints to send a notification at. A value of 0 will disable notification.", + position = 11 ) default int getHitpointsThreshold() { @@ -142,21 +153,21 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "playHealthSound", - name = "Play sound for Low Health", - description = "Will play a sound for every Low Health notification sent", - position = 12 + keyName = "playHealthSound", + name = "Play sound for Low Health", + description = "Will play a sound for every Low Health notification sent", + position = 12 ) - default boolean getPlayHealthSound() + default boolean getPlayHealthSound() { return false; } @ConfigItem( - keyName = "prayer", - name = "Prayer Notification Threshold", - description = "The amount of prayer points to send a notification at. A value of 0 will disable notification.", - position = 12 + keyName = "prayer", + name = "Prayer Notification Threshold", + description = "The amount of prayer points to send a notification at. A value of 0 will disable notification.", + position = 13 ) default int getPrayerThreshold() { @@ -164,21 +175,21 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "playPrayerSound", - name = "Play sound for Low Prayer", - description = "Will play a sound for every Low Prayer notification sent", - position = 13 + keyName = "playPrayerSound", + name = "Play sound for Low Prayer", + description = "Will play a sound for every Low Prayer notification sent", + position = 14 ) - default boolean getPlayPrayerSound() + default boolean getPlayPrayerSound() { return false; } @ConfigItem( - keyName = "oxygen", - name = "Oxygen Notification Threshold", - position = 14, - description = "The amount of remaining oxygen to send a notification at. A value of 0 will disable notification." + keyName = "oxygen", + name = "Oxygen Notification Threshold", + position = 15, + description = "The amount of remaining oxygen to send a notification at. A value of 0 will disable notification." ) default int getOxygenThreshold() { @@ -186,10 +197,10 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "spec", - name = "Special Attack Energy Notification Threshold", - position = 15, - description = "The amount of spec energy reached to send a notification at. A value of 0 will disable notification." + keyName = "spec", + name = "Special Attack Energy Notification Threshold", + position = 16, + description = "The amount of spec energy reached to send a notification at. A value of 0 will disable notification." ) default int getSpecEnergyThreshold() { @@ -197,34 +208,34 @@ public interface IdleNotifierConfig extends Config } @ConfigItem( - keyName = "specSound", - name = "Special Attack Energy Sound", - description = "Plays a custom sound accompanying Special Attack energy notifications", - position = 16 + keyName = "specSound", + name = "Special Attack Energy Sound", + description = "Plays a custom sound accompanying Special Attack energy notifications", + position = 17 ) - default boolean getSpecSound() + default boolean getSpecSound() { return false; } @ConfigItem( - keyName = "overspec", - name = "Over Special Energy Notification", - description = "Will repeat notifications for any value over the special energy threshold", - position = 17 + keyName = "overspec", + name = "Over Special Energy Notification", + description = "Will repeat notifications for any value over the special energy threshold", + position = 18 ) - default boolean getOverSpecEnergy() + default boolean getOverSpecEnergy() { return false; } @ConfigItem( - keyName = "pkers", - name = "PKer Notifier", - position = 18, - description = "Notifies if an attackable player based on your level range appears on screen.", - group = "PvP", - warning = "This will not notify you if the player is in your cc or is online on your friends list." + keyName = "pkers", + name = "PKer Notifier", + position = 19, + description = "Notifies if an attackable player based on your level range appears on screen.", + group = "PvP", + warning = "This will not notify you if the player is in your cc or is online on your friends list." ) default boolean notifyPkers() { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java index 3d84a2f099..e2091dd882 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java @@ -29,6 +29,7 @@ import com.google.inject.Provides; import java.awt.TrayIcon; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.List; @@ -44,6 +45,9 @@ import net.runelite.api.Constants; import net.runelite.api.GameState; import net.runelite.api.GraphicID; import net.runelite.api.Hitsplat; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemContainer; import net.runelite.api.NPC; import net.runelite.api.NPCDefinition; import net.runelite.api.Player; @@ -58,6 +62,7 @@ import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; import net.runelite.api.events.HitsplatApplied; import net.runelite.api.events.InteractingChanged; +import net.runelite.api.events.ItemContainerChanged; import net.runelite.api.events.PlayerSpawned; import net.runelite.api.events.SpotAnimationChanged; import net.runelite.client.Notifier; @@ -117,7 +122,12 @@ public class IdleNotifierPlugin extends Plugin private int lastCombatCountdown = 0; private Instant sixHourWarningTime; private boolean ready; + private Instant lastTimeItemsUsedUp; + private List itemIdsPrevious = new ArrayList<>(); + private List itemQuantitiesPrevious = new ArrayList<>(); + private final List itemQuantitiesChange = new ArrayList<>(); private boolean lastInteractWasCombat; + private boolean interactingNotified; private SkullIcon lastTickSkull = null; private boolean isFirstTick = true; @@ -146,6 +156,7 @@ public class IdleNotifierPlugin extends Plugin private boolean getSpecSound; private boolean getOverSpecEnergy; private boolean notifyPkers; + private boolean outOfItemsIdle; @Provides IdleNotifierConfig provideConfig(ConfigManager configManager) @@ -222,6 +233,17 @@ public class IdleNotifierPlugin extends Plugin /* Fishing */ case FISHING_CRUSHING_INFERNAL_EELS: case FISHING_CUTTING_SACRED_EELS: + case FISHING_BIG_NET: + case FISHING_NET: + case FISHING_POLE_CAST: + case FISHING_CAGE: + case FISHING_HARPOON: + case FISHING_BARBTAIL_HARPOON: + case FISHING_DRAGON_HARPOON: + case FISHING_INFERNAL_HARPOON: + case FISHING_OILY_ROD: + case FISHING_KARAMBWAN: + case FISHING_BAREHAND: /* Mining(Normal) */ case MINING_BRONZE_PICKAXE: case MINING_IRON_PICKAXE: @@ -236,7 +258,7 @@ public class IdleNotifierPlugin extends Plugin case MINING_3A_PICKAXE: case DENSE_ESSENCE_CHIPPING: case DENSE_ESSENCE_CHISELING: - /* Herblore */ + /* Herblore */ case HERBLORE_PESTLE_AND_MORTAR: case HERBLORE_POTIONMAKING: case HERBLORE_MAKE_TAR: @@ -258,13 +280,14 @@ public class IdleNotifierPlugin extends Plugin case FARMING_HARVEST_FRUIT_TREE: case FARMING_HARVEST_FLOWER: case FARMING_HARVEST_ALLOTMENT: - /* Misc */ + /* Misc */ case PISCARILIUS_CRANE_REPAIR: case HOME_MAKE_TABLET: case SAND_COLLECTION: resetTimers(); lastAnimation = animation; lastAnimating = Instant.now(); + interactingNotified = false; break; case MAGIC_LUNAR_SHARED: if (graphic == GraphicID.BAKE_PIE) @@ -272,10 +295,12 @@ public class IdleNotifierPlugin extends Plugin resetTimers(); lastAnimation = animation; lastAnimating = Instant.now(); + interactingNotified = false; break; } case IDLE: lastAnimating = Instant.now(); + interactingNotified = false; break; default: // On unknown animation simply assume the animation is invalid and dont throw notification @@ -299,6 +324,84 @@ public class IdleNotifierPlugin extends Plugin } } + @Subscribe + public void onItemContainerChanged(ItemContainerChanged event) + { + ItemContainer itemContainer = event.getItemContainer(); + + if (itemContainer != client.getItemContainer(InventoryID.INVENTORY) || !config.outOfItemsIdle()) + { + return; + } + + Item[] items = itemContainer.getItems(); + ArrayList itemQuantities = new ArrayList<>(); + ArrayList itemIds = new ArrayList<>(); + + // Populate list of items in inventory without duplicates + for (Item value : items) + { + int itemId = OutOfItemsMapping.mapFirst(value.getId()); + if (itemIds.indexOf(itemId) == -1) // -1 if item not yet in list + { + itemIds.add(itemId); + } + } + + // Populate quantity of each item in inventory + for (int j = 0; j < itemIds.size(); j++) + { + itemQuantities.add(0); + for (Item item : items) + { + if (itemIds.get(j) == OutOfItemsMapping.mapFirst(item.getId())) + { + itemQuantities.set(j, itemQuantities.get(j) + item.getQuantity()); + } + } + } + + itemQuantitiesChange.clear(); + + // Calculate the quantity of each item consumed by the last action + if (!itemIdsPrevious.isEmpty()) + { + for (int i = 0; i < itemIdsPrevious.size(); i++) + { + int id = itemIdsPrevious.get(i); + int currentIndex = itemIds.indexOf(id); + int currentQuantity; + if (currentIndex != -1) // -1 if item is no longer in inventory + { + currentQuantity = itemQuantities.get(currentIndex); + } + else + { + currentQuantity = 0; + } + itemQuantitiesChange.add(currentQuantity - itemQuantitiesPrevious.get(i)); + } + } + else + { + itemIdsPrevious = itemIds; + itemQuantitiesPrevious = itemQuantities; + return; + } + + // Check we have enough items left for another action. + for (int i = 0; i < itemQuantitiesPrevious.size(); i++) + { + if (-itemQuantitiesChange.get(i) * 2 > itemQuantitiesPrevious.get(i)) + { + lastTimeItemsUsedUp = Instant.now(); + return; + } + } + itemIdsPrevious = itemIds; + itemQuantitiesPrevious = itemQuantities; + } + @Subscribe public void onInteractingChanged(InteractingChanged event) { @@ -432,6 +535,7 @@ public class IdleNotifierPlugin extends Plugin || client.getKeyboardIdleTicks() < 10) { resetTimers(); + resetOutOfItemsIdleChecks(); return; } @@ -445,14 +549,13 @@ public class IdleNotifierPlugin extends Plugin notifier.notify("[" + local.getName() + "] is about to log out from being online for 6 hours!"); } - if (this.animationIdle && checkAnimationIdle(waitDuration, local)) + if (this.outOfItemsIdle && checkOutOfItemsIdle(waitDuration)) { - notifier.notify("[" + local.getName() + "] is now idle!"); - if (this.animationIdleSound) - { - soundManager.playSound(Sound.IDLE); - } + notifier.notify("[" + local.getName() + "] has run out of items!"); + // If this triggers, don't also trigger animation idle notification afterwards. + lastAnimation = IDLE; } + if (this.interactionIdle && checkInteractionIdle(waitDuration, local)) { if (lastInteractWasCombat) @@ -471,6 +574,16 @@ public class IdleNotifierPlugin extends Plugin soundManager.playSound(Sound.IDLE); } } + interactingNotified = true; + } + + if (this.animationIdle && checkAnimationIdle(waitDuration, local)) + { + notifier.notify("[" + local.getName() + "] is now idle!"); + if (this.animationIdleSound) + { + soundManager.playSound(Sound.IDLE); + } } if (checkLowHitpoints()) @@ -689,7 +802,7 @@ public class IdleNotifierPlugin extends Plugin private boolean checkAnimationIdle(Duration waitDuration, Player local) { - if (lastAnimation == IDLE) + if (lastAnimation == IDLE || interactingNotified) { return false; } @@ -713,6 +826,23 @@ public class IdleNotifierPlugin extends Plugin return false; } + private boolean checkOutOfItemsIdle(Duration waitDuration) + { + if (lastTimeItemsUsedUp == null) + { + return false; + } + + if (Instant.now().compareTo(lastTimeItemsUsedUp.plus(waitDuration)) >= 0) + { + resetTimers(); + resetOutOfItemsIdleChecks(); + return true; + } + + return false; + } + private void resetTimers() { final Player local = client.getLocalPlayer(); @@ -732,6 +862,14 @@ public class IdleNotifierPlugin extends Plugin } } + private void resetOutOfItemsIdleChecks() + { + lastTimeItemsUsedUp = null; + itemQuantitiesChange.clear(); + itemIdsPrevious.clear(); + itemQuantitiesPrevious.clear(); + } + private void skullNotifier() { final Player local = client.getLocalPlayer(); @@ -796,5 +934,6 @@ public class IdleNotifierPlugin extends Plugin this.getSpecSound = config.getSpecSound(); this.getOverSpecEnergy = config.getOverSpecEnergy(); this.notifyPkers = config.notifyPkers(); + this.outOfItemsIdle = config.outOfItemsIdle(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/OutOfItemsMapping.java b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/OutOfItemsMapping.java new file mode 100644 index 0000000000..10738f1273 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/OutOfItemsMapping.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019, Twiglet1022 + * 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.idlenotifier; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import java.util.Collection; +import static net.runelite.api.ItemID.*; + +public enum OutOfItemsMapping +{ + AERIAL_FISHING_CUTTING(BLUEGILL, COMMON_TENCH, MOTTLED_EEL, GREATER_SIREN); + + private static final Multimap MAPPINGS = HashMultimap.create(); + private final int groupedItemKey; + private final int[] groupedItemIDs; + + static + { + for (final OutOfItemsMapping item : values()) + { + for (int itemId : item.groupedItemIDs) + { + MAPPINGS.put(itemId, item.groupedItemKey); + } + } + } + + OutOfItemsMapping(int groupedItemKey, int... groupedItemIDs) + { + this.groupedItemKey = groupedItemKey; + this.groupedItemIDs = groupedItemIDs; + } + + /** + * Some actions consume multiple different items. To properly handle these + * cases for the out of items notification the different items must be + * recognised as belonging to a single group. + * + * Map an item that is part of a group of items that are consumed by a single + * action to the first item in that group. + */ + public static int mapFirst(int itemId) + { + final Collection mapping = MAPPINGS.get(itemId); + + if (mapping == null || mapping.isEmpty()) + { + return itemId; + } + + return mapping.iterator().next(); + } + +} \ No newline at end of file