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 6d80df0ca0..8243ac28d1 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,17 +29,23 @@ import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; +import java.util.List; import javax.inject.Inject; import net.runelite.api.Actor; +import net.runelite.api.AnimationID; import static net.runelite.api.AnimationID.*; import net.runelite.api.Client; import net.runelite.api.GameState; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; import net.runelite.api.Player; import net.runelite.api.Skill; import net.runelite.api.Varbits; import net.runelite.api.events.AnimationChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.InteractingChanged; import net.runelite.client.Notifier; import net.runelite.client.config.ConfigManager; import net.runelite.client.plugins.Plugin; @@ -64,10 +70,10 @@ public class IdleNotifierPlugin extends Plugin @Inject private IdleNotifierConfig config; - private Actor lastOpponent; private Instant lastAnimating; + private int lastAnimation = AnimationID.IDLE; private Instant lastInteracting; - private boolean notifyIdle = false; + private Actor lastInteract; private boolean notifyHitpoints = true; private boolean notifyPrayer = true; private boolean notifyIdleLogout = true; @@ -192,8 +198,50 @@ public class IdleNotifierPlugin extends Plugin /* Prayer */ case USING_GILDED_ALTAR: resetTimers(); - notifyIdle = true; + lastAnimation = animation; break; + case IDLE: + break; + default: + // On unknown animation simply assume the animation is invalid and dont throw notification + lastAnimation = IDLE; + } + } + + @Subscribe + public void onInteractingChanged(InteractingChanged event) + { + final Actor source = event.getSource(); + if (source != client.getLocalPlayer()) + { + return; + } + + final Actor target = event.getTarget(); + + // Reset last interact + if (target != null) + { + lastInteract = null; + } + + final boolean isNpc = target instanceof NPC; + + // If this is not NPC, do not process as we are not interested in other entities + if (!isNpc) + { + return; + } + + final NPC npc = (NPC) target; + final NPCComposition npcComposition = npc.getComposition(); + final List npcMenuActions = Arrays.asList(npcComposition.getActions()); + + if (npcMenuActions.contains("Attack")) + { + // Player is most likely in combat with attack-able NPC + resetTimers(); + lastInteract = target; } } @@ -206,6 +254,9 @@ public class IdleNotifierPlugin extends Plugin switch (state) { + case LOGIN_SCREEN: + resetTimers(); + break; case LOGGING_IN: case HOPPING: case CONNECTION_LOST: @@ -216,7 +267,9 @@ public class IdleNotifierPlugin extends Plugin { sixHourWarningTime = Instant.now().plus(SIX_HOUR_LOGOUT_WARNING_AFTER_DURATION); ready = false; + resetTimers(); } + break; } } @@ -227,8 +280,9 @@ public class IdleNotifierPlugin extends Plugin final Player local = client.getLocalPlayer(); final Duration waitDuration = Duration.ofMillis(config.getIdleNotificationDelay()); - if (client.getGameState() != GameState.LOGGED_IN || local == null) + if (client.getGameState() != GameState.LOGGED_IN || local == null || client.getMouseIdleTicks() < 10) { + resetTimers(); return; } @@ -315,32 +369,27 @@ public class IdleNotifierPlugin extends Plugin private boolean checkOutOfCombat(Duration waitDuration, Player local) { - Actor opponent = local.getInteracting(); - boolean isPlayer = opponent instanceof Player; - - if (opponent != null - && !isPlayer - && opponent.getCombatLevel() > 0) + if (lastInteract == null) { - resetTimers(); - lastOpponent = opponent; - } - else if (opponent == null) - { - lastOpponent = null; + return false; } - if (lastOpponent != null && opponent == lastOpponent) + final Actor interact = local.getInteracting(); + + if (interact == null) + { + if (lastInteracting != null && Instant.now().compareTo(lastInteracting.plus(waitDuration)) >= 0) + { + lastInteract = null; + lastInteracting = null; + return true; + } + } + else { lastInteracting = Instant.now(); } - if (lastInteracting != null && Instant.now().compareTo(lastInteracting.plus(waitDuration)) >= 0) - { - lastInteracting = null; - return true; - } - return false; } @@ -388,34 +437,46 @@ public class IdleNotifierPlugin extends Plugin private boolean checkAnimationIdle(Duration waitDuration, Player local) { - if (notifyIdle) + if (lastAnimation == IDLE) { - if (lastAnimating != null) + return false; + } + + final int animation = local.getAnimation(); + + if (animation == IDLE) + { + if (lastAnimating != null && Instant.now().compareTo(lastAnimating.plus(waitDuration)) >= 0) { - if (Instant.now().compareTo(lastAnimating.plus(waitDuration)) >= 0) - { - notifyIdle = false; - lastAnimating = null; - return true; - } - } - else if (local.getAnimation() == IDLE) - { - lastAnimating = Instant.now(); + lastAnimation = IDLE; + lastAnimating = null; + return true; } } + else + { + lastAnimating = Instant.now(); + } return false; } private void resetTimers() { + final Player local = client.getLocalPlayer(); + // Reset animation idle timer - notifyIdle = false; lastAnimating = null; + if (client.getGameState() == GameState.LOGIN_SCREEN || local == null || local.getAnimation() != lastAnimation) + { + lastAnimation = IDLE; + } // Reset combat idle timer - lastOpponent = null; lastInteracting = null; + if (client.getGameState() == GameState.LOGIN_SCREEN || local == null || local.getInteracting() != lastInteract) + { + lastInteract = null; + } } } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPluginTest.java new file mode 100644 index 0000000000..dc787ce1f8 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPluginTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import net.runelite.api.AnimationID; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Player; +import net.runelite.api.events.AnimationChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.InteractingChanged; +import net.runelite.client.Notifier; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.Matchers.any; +import org.mockito.Mock; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class IdleNotifierPluginTest +{ + private static final String PLAYER_NAME = "Deathbeam"; + + @Mock + @Bind + private Client client; + + @Mock + @Bind + private IdleNotifierConfig config; + + @Mock + @Bind + private Notifier notifier; + + @Inject + private IdleNotifierPlugin plugin; + + @Mock + private NPC monster; + + @Mock + private NPC randomEvent; + + @Mock + private Player player; + + @Before + public void setUp() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + + // Mock monster + final String[] monsterActions = new String[] { "Attack", "Examine" }; + final NPCComposition monsterComp = mock(NPCComposition.class); + when(monsterComp.getActions()).thenReturn(monsterActions); + when(monster.getComposition()).thenReturn(monsterComp); + + // Mock random event + final String[] randomEventActions = new String[] { "Talk-to", "Dismiss", "Examine" }; + final NPCComposition randomEventComp = mock(NPCComposition.class); + when(randomEventComp.getActions()).thenReturn(randomEventActions); + when(randomEvent.getComposition()).thenReturn(randomEventComp); + + // Mock player + when(player.getName()).thenReturn(PLAYER_NAME); + when(player.getAnimation()).thenReturn(AnimationID.IDLE); + when(client.getLocalPlayer()).thenReturn(player); + + // Mock config + when(config.animationIdle()).thenReturn(true); + when(config.combatIdle()).thenReturn(true); + when(config.getIdleNotificationDelay()).thenReturn(0); + when(config.getHitpointsThreshold()).thenReturn(42); + when(config.getPrayerThreshold()).thenReturn(42); + + // Mock client + when(client.getGameState()).thenReturn(GameState.LOGGED_IN); + when(client.getMouseIdleTicks()).thenReturn(42); + } + + @Test + public void checkAnimationIdle() + { + when(player.getAnimation()).thenReturn(AnimationID.WOODCUTTING_BRONZE); + AnimationChanged animationChanged = new AnimationChanged(); + animationChanged.setActor(player); + plugin.onAnimationChanged(animationChanged); + plugin.onGameTick(new GameTick()); + when(player.getAnimation()).thenReturn(AnimationID.IDLE); + plugin.onAnimationChanged(animationChanged); + plugin.onGameTick(new GameTick()); + verify(notifier).notify("[" + PLAYER_NAME + "] is now idle!"); + } + + @Test + public void checkAnimationReset() + { + when(player.getAnimation()).thenReturn(AnimationID.WOODCUTTING_BRONZE); + AnimationChanged animationChanged = new AnimationChanged(); + animationChanged.setActor(player); + plugin.onAnimationChanged(animationChanged); + plugin.onGameTick(new GameTick()); + when(player.getAnimation()).thenReturn(AnimationID.LOOKING_INTO); + plugin.onAnimationChanged(animationChanged); + plugin.onGameTick(new GameTick()); + when(player.getAnimation()).thenReturn(AnimationID.IDLE); + plugin.onAnimationChanged(animationChanged); + plugin.onGameTick(new GameTick()); + verify(notifier, times(0)).notify(any()); + } + + @Test + public void checkAnimationLogout() + { + when(player.getAnimation()).thenReturn(AnimationID.WOODCUTTING_BRONZE); + AnimationChanged animationChanged = new AnimationChanged(); + animationChanged.setActor(player); + plugin.onAnimationChanged(animationChanged); + plugin.onInteractingChanged(new InteractingChanged(player, monster)); + plugin.onGameTick(new GameTick()); + + // Logout + when(client.getGameState()).thenReturn(GameState.LOGIN_SCREEN); + GameStateChanged gameStateChanged = new GameStateChanged(); + gameStateChanged.setGameState(GameState.LOGIN_SCREEN); + plugin.onGameStateChanged(gameStateChanged); + + // Log back in + when(client.getGameState()).thenReturn(GameState.LOGGED_IN); + gameStateChanged.setGameState(GameState.LOGGED_IN); + plugin.onGameStateChanged(gameStateChanged); + + // Tick + when(player.getAnimation()).thenReturn(AnimationID.IDLE); + plugin.onAnimationChanged(animationChanged); + plugin.onGameTick(new GameTick()); + verify(notifier, times(0)).notify(any()); + } + + @Test + public void checkCombatIdle() + { + when(player.getInteracting()).thenReturn(monster); + plugin.onInteractingChanged(new InteractingChanged(player, monster)); + plugin.onGameTick(new GameTick()); + when(player.getInteracting()).thenReturn(null); + plugin.onInteractingChanged(new InteractingChanged(player, null)); + plugin.onGameTick(new GameTick()); + verify(notifier).notify("[" + PLAYER_NAME + "] is now out of combat!"); + } + + @Test + public void checkCombatReset() + { + when(player.getInteracting()).thenReturn(monster); + plugin.onInteractingChanged(new InteractingChanged(player, monster)); + plugin.onGameTick(new GameTick()); + when(player.getInteracting()).thenReturn(randomEvent); + plugin.onInteractingChanged(new InteractingChanged(player, randomEvent)); + plugin.onGameTick(new GameTick()); + when(player.getInteracting()).thenReturn(null); + plugin.onInteractingChanged(new InteractingChanged(player, null)); + plugin.onGameTick(new GameTick()); + verify(notifier, times(0)).notify(any()); + } + + @Test + public void checkCombatLogout() + { + plugin.onInteractingChanged(new InteractingChanged(player, monster)); + when(player.getInteracting()).thenReturn(monster); + plugin.onGameTick(new GameTick()); + + // Logout + when(client.getGameState()).thenReturn(GameState.LOGIN_SCREEN); + GameStateChanged gameStateChanged = new GameStateChanged(); + gameStateChanged.setGameState(GameState.LOGIN_SCREEN); + plugin.onGameStateChanged(gameStateChanged); + + // Log back in + when(client.getGameState()).thenReturn(GameState.LOGGED_IN); + gameStateChanged.setGameState(GameState.LOGGED_IN); + plugin.onGameStateChanged(gameStateChanged); + + // Tick + when(player.getInteracting()).thenReturn(null); + plugin.onInteractingChanged(new InteractingChanged(player, null)); + plugin.onGameTick(new GameTick()); + verify(notifier, times(0)).notify(any()); + } +} \ No newline at end of file