diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/ElapsedTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/ElapsedTimer.java new file mode 100644 index 0000000000..0b4ec17517 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/ElapsedTimer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019, winterdaze + * 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.timers; + +import lombok.Getter; +import net.runelite.client.ui.overlay.infobox.InfoBox; +import java.awt.image.BufferedImage; +import java.awt.Color; +import java.time.Duration; +import java.time.Instant; +import org.apache.commons.lang3.time.DurationFormatUtils; + +@Getter +class ElapsedTimer extends InfoBox +{ + private final Instant startTime; + private final Instant lastTime; + + // Creates a timer that counts up if lastTime is null, or a paused timer if lastTime is defined + ElapsedTimer(BufferedImage image, TimersPlugin plugin, Instant startTime, Instant lastTime) + { + super(image, plugin); + this.startTime = startTime; + this.lastTime = lastTime; + } + + @Override + public String getText() + { + if (startTime == null) + { + return null; + } + + Duration time = Duration.between(startTime, lastTime == null ? Instant.now() : lastTime); + final String formatString = time.toHours() > 0 ? "HH:mm" : "mm:ss"; + return DurationFormatUtils.formatDuration(time.toMillis(), formatString, true); + } + + @Override + public Color getTextColor() + { + return Color.WHITE; + } + + @Override + public String getTooltip() + { + Duration time = Duration.between(startTime, lastTime == null ? Instant.now() : lastTime); + return "Elapsed time: " + DurationFormatUtils.formatDuration(time.toMillis(), "HH:mm:ss", true); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java index 9b8f4a557b..d57ea0523d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java @@ -27,6 +27,7 @@ package net.runelite.client.plugins.timers; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import java.time.Instant; @ConfigGroup("timers") public interface TimersConfig extends Config @@ -191,6 +192,46 @@ public interface TimersConfig extends Config return true; } + @ConfigItem( + keyName = "showTzhaarTimers", + name = "Fight Caves and Inferno timers", + description = "Display elapsed time in the Fight Caves and Inferno" + ) + default boolean showTzhaarTimers() + { + return true; + } + + @ConfigItem( + keyName = "tzhaarStartTime", + name = "", + description = "", + hidden = true + ) + Instant tzhaarStartTime(); + + @ConfigItem( + keyName = "tzhaarStartTime", + name = "", + description = "" + ) + void tzhaarStartTime(Instant tzhaarStartTime); + + @ConfigItem( + keyName = "tzhaarLastTime", + name = "", + description = "", + hidden = true + ) + Instant tzhaarLastTime(); + + @ConfigItem( + keyName = "tzhaarLastTime", + name = "", + description = "" + ) + void tzhaarLastTime(Instant tzhaarLastTime); + @ConfigItem( keyName = "showStaffOfTheDead", name = "Staff of the Dead timer", diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java index fe2b8a6184..973d21e936 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java @@ -1,6 +1,7 @@ /* * Copyright (c) 2017, Seth * Copyright (c) 2018, Jordan Atwood + * Copyright (c) 2019, winterdaze * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -43,6 +44,8 @@ import net.runelite.api.InventoryID; import net.runelite.api.Item; import net.runelite.api.ItemContainer; import net.runelite.api.ItemID; +import static net.runelite.api.ItemID.FIRE_CAPE; +import static net.runelite.api.ItemID.INFERNAL_CAPE; import net.runelite.api.NPC; import net.runelite.api.NpcID; import net.runelite.api.Player; @@ -75,11 +78,12 @@ import net.runelite.client.plugins.PluginDescriptor; import static net.runelite.client.plugins.timers.GameIndicator.VENGEANCE_ACTIVE; import static net.runelite.client.plugins.timers.GameTimer.*; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import org.apache.commons.lang3.ArrayUtils; @PluginDescriptor( name = "Timers", description = "Show various timers in an infobox", - tags = {"combat", "items", "magic", "potions", "prayer", "overlay", "abyssal", "sire"} + tags = {"combat", "items", "magic", "potions", "prayer", "overlay", "abyssal", "sire", "inferno", "fight", "caves", "cape", "timer", "tzhaar"} ) @Slf4j public class TimersPlugin extends Plugin @@ -117,6 +121,13 @@ public class TimersPlugin extends Plugin private static final int VENOM_VALUE_CUTOFF = -40; // Antivenom < -40 <= Antipoison < 0 private static final int POISON_TICK_LENGTH = 30; + private static final int FIGHT_CAVES_REGION_ID = 9551; + private static final int INFERNO_REGION_ID = 9043; + private static final Pattern TZHAAR_WAVE_MESSAGE = Pattern.compile("Wave: (\\d+)"); + private static final String TZHAAR_DEFEATED_MESSAGE = "You have been defeated!"; + private static final Pattern TZHAAR_COMPLETE_MESSAGE = Pattern.compile("Your (TzTok-Jad|TzKal-Zuk) kill count is:"); + private static final Pattern TZHAAR_PAUSED_MESSAGE = Pattern.compile("The (Inferno|Fight Cave) has been paused. You may now log out."); + private TimerTimer freezeTimer; private int freezeTime = -1; // time frozen, in game ticks @@ -134,6 +145,7 @@ public class TimersPlugin extends Plugin private int lastAnimation; private boolean loggedInRace; private boolean widgetHiddenChangedOnPvpWorld; + private ElapsedTimer tzhaarTimer; @Inject private ItemManager itemManager; @@ -168,6 +180,7 @@ public class TimersPlugin extends Plugin widgetHiddenChangedOnPvpWorld = false; lastPoisonVarp = 0; nextPoisonTick = 0; + removeTzhaarTimer(); staminaTimer = null; } @@ -370,6 +383,11 @@ public class TimersPlugin extends Plugin removeGameTimer(ANTIPOISON); removeGameTimer(ANTIVENOM); } + + if (!config.showTzhaarTimers()) + { + removeTzhaarTimer(); + } } @Subscribe @@ -639,6 +657,90 @@ public class TimersPlugin extends Plugin } } } + + if (config.showTzhaarTimers()) + { + String message = event.getMessage(); + Matcher matcher = TZHAAR_COMPLETE_MESSAGE.matcher(message); + + if (message.contains(TZHAAR_DEFEATED_MESSAGE) || matcher.matches()) + { + removeTzhaarTimer(); + config.tzhaarStartTime(null); + config.tzhaarLastTime(null); + return; + } + + Instant now = Instant.now(); + matcher = TZHAAR_PAUSED_MESSAGE.matcher(message); + if (matcher.find()) + { + config.tzhaarLastTime(now); + createTzhaarTimer(); + return; + } + + matcher = TZHAAR_WAVE_MESSAGE.matcher(message); + if (!matcher.find()) + { + return; + } + + if (config.tzhaarStartTime() == null) + { + int wave = Integer.parseInt(matcher.group(1)); + if (wave == 1) + { + config.tzhaarStartTime(now); + createTzhaarTimer(); + } + } + else if (config.tzhaarLastTime() != null) + { + log.debug("Unpausing tzhaar timer"); + + // Advance start time by how long it has been paused + Instant tzhaarStartTime = config.tzhaarStartTime(); + tzhaarStartTime = tzhaarStartTime.plus(Duration.between(config.tzhaarLastTime(), now)); + config.tzhaarStartTime(tzhaarStartTime); + + config.tzhaarLastTime(null); + createTzhaarTimer(); + } + } + } + + private boolean isInFightCaves() + { + return client.getMapRegions() != null && ArrayUtils.contains(client.getMapRegions(), FIGHT_CAVES_REGION_ID); + } + + private boolean isInInferno() + { + return client.getMapRegions() != null && ArrayUtils.contains(client.getMapRegions(), INFERNO_REGION_ID); + } + + private void createTzhaarTimer() + { + removeTzhaarTimer(); + + int imageItem = isInFightCaves() ? FIRE_CAPE : (isInInferno() ? INFERNAL_CAPE : -1); + if (imageItem == -1) + { + return; + } + + tzhaarTimer = new ElapsedTimer(itemManager.getImage(imageItem), this, config.tzhaarStartTime(), config.tzhaarLastTime()); + infoBoxManager.addInfoBox(tzhaarTimer); + } + + private void removeTzhaarTimer() + { + if (tzhaarTimer != null) + { + infoBoxManager.removeInfoBox(tzhaarTimer); + tzhaarTimer = null; + } } @Subscribe @@ -670,8 +772,7 @@ public class TimersPlugin extends Plugin widgetHiddenChangedOnPvpWorld = false; Widget widget = client.getWidget(PVP_WORLD_SAFE_ZONE); - if (widget != null - && !widget.isSelfHidden()) + if (widget != null && !widget.isSelfHidden()) { log.debug("Entered safe zone in PVP world, clearing Teleblock timer."); removeTbTimers(); @@ -683,8 +784,24 @@ public class TimersPlugin extends Plugin { switch (gameStateChanged.getGameState()) { + case LOADING: + if (tzhaarTimer != null && !isInFightCaves() && !isInInferno()) + { + removeTzhaarTimer(); + config.tzhaarStartTime(null); + config.tzhaarLastTime(null); + } + break; case HOPPING: case LOGIN_SCREEN: + // pause tzhaar timer if logged out without pausing + if (config.tzhaarStartTime() != null && config.tzhaarLastTime() == null) + { + config.tzhaarLastTime(Instant.now()); + log.debug("Pausing tzhaar timer"); + } + + removeTzhaarTimer(); // will be readded by the wave message removeTbTimers(); break; case LOGGED_IN: diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/timers/ElapsedTimerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timers/ElapsedTimerTest.java new file mode 100644 index 0000000000..fddb31beb6 --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/timers/ElapsedTimerTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Jordan + * 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.timers; + +import java.time.Instant; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +public class ElapsedTimerTest +{ + @Test + public void testGetText() + { + final Instant now = Instant.now(); + final Instant fiveSecondsAgo = now.minusSeconds(5); + final Instant fiveMinutesAgo = now.minusSeconds(5 * 60); + final Instant oneHourAgo = now.minusSeconds(60 * 60); + final Instant fiveHoursAgo = now.minusSeconds(5 * 60 * 60); + + assertEquals("00:00", timerText(now, now)); + assertEquals("00:00", timerText(now, null)); + assertEquals("00:05", timerText(fiveSecondsAgo, now)); + assertEquals("00:05", timerText(fiveSecondsAgo, null)); + assertEquals("04:55", timerText(fiveMinutesAgo, fiveSecondsAgo)); + assertEquals("05:00", timerText(fiveMinutesAgo, now)); + assertEquals("05:00", timerText(fiveMinutesAgo, null)); + assertEquals("55:00", timerText(oneHourAgo, fiveMinutesAgo)); + assertEquals("59:55", timerText(oneHourAgo, fiveSecondsAgo)); + assertEquals("01:00", timerText(oneHourAgo, now)); + assertEquals("01:00", timerText(oneHourAgo, null)); + assertEquals("04:00", timerText(fiveHoursAgo, oneHourAgo)); + assertEquals("04:55", timerText(fiveHoursAgo, fiveMinutesAgo)); + assertEquals("04:59", timerText(fiveHoursAgo, fiveSecondsAgo)); + assertEquals("05:00", timerText(fiveHoursAgo, now)); + assertEquals("05:00", timerText(fiveHoursAgo, null)); + } + + private static String timerText(final Instant startTime, final Instant lastTime) + { + return new ElapsedTimer(null, null, startTime, lastTime).getText(); + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java index 9ef79a29a3..bddcee697a 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java @@ -29,6 +29,7 @@ import com.google.inject.Inject; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; import java.time.Duration; +import java.time.Instant; import java.util.EnumSet; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; @@ -42,18 +43,24 @@ import net.runelite.client.game.SpriteManager; import net.runelite.client.ui.overlay.infobox.InfoBox; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; import org.mockito.Mock; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; 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.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; @RunWith(MockitoJUnitRunner.class) public class TimersPluginTest @@ -63,6 +70,7 @@ public class TimersPluginTest private static final String HALF_TELEBLOCK_MESSAGE = "A Tele Block spell has been cast on you by Runelite. It will expire in 2 minutes, 30 seconds."; private static final String TRANSPARENT_CHATBOX_FULL_TELEBLOCK_MESSAGE = "A Tele Block spell has been cast on you by Alexsuperfly. It will expire in 5 minutes."; private static final String TRANSPARENT_CHATBOX_TELEBLOCK_REMOVED_MESSAGE = "Your Tele Block has been removed because you killed Alexsuperfly."; + private static final int FIGHT_CAVES_REGION_ID = 9551; @Inject private TimersPlugin timersPlugin; @@ -233,4 +241,66 @@ public class TimersPluginTest int mins = (int) infoBox.getDuration().toMinutes(); assertTrue(mins == 1 || mins == 2); } + + @Test + public void testTzhaarTimer() + { + when(timersConfig.showTzhaarTimers()).thenReturn(true); + when(client.getMapRegions()).thenReturn(new int[]{FIGHT_CAVES_REGION_ID}); + + class InstantRef + { + Instant i; + } + + InstantRef startTime = new InstantRef(); + when(timersConfig.tzhaarStartTime()).then(a -> startTime.i); + doAnswer((Answer) invocationOnMock -> + { + Object argument = invocationOnMock.getArguments()[0]; + startTime.i = (Instant) argument; + return null; + }).when(timersConfig).tzhaarStartTime(nullable(Instant.class)); + + InstantRef lastTime = new InstantRef(); + when(timersConfig.tzhaarLastTime()).then(a -> lastTime.i); + doAnswer((Answer) invocationOnMock -> + { + Object argument = invocationOnMock.getArguments()[0]; + lastTime.i = (Instant) argument; + return null; + }).when(timersConfig).tzhaarLastTime(nullable(Instant.class)); + + // test timer creation: verify the infobox was added and that it is an ElapsedTimer + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "Wave: 1", "", 0); + timersPlugin.onChatMessage(chatMessage); + ArgumentCaptor captor = ArgumentCaptor.forClass(InfoBox.class); + verify(infoBoxManager, times(1)).addInfoBox(captor.capture()); + assertTrue(captor.getValue() instanceof ElapsedTimer); + + // test timer pause: verify the added ElapsedTimer has a non-null lastTime + chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "The Inferno has been paused. You may now log out.", "", 0); + timersPlugin.onChatMessage(chatMessage); + verify(infoBoxManager, times(1)).removeInfoBox(captor.capture()); + verify(infoBoxManager, times(2)).addInfoBox(captor.capture()); + assertTrue(captor.getValue() instanceof ElapsedTimer); + ElapsedTimer timer = (ElapsedTimer) captor.getValue(); + assertNotEquals(timer.getLastTime(), null); + Instant oldTime = ((ElapsedTimer) captor.getValue()).getStartTime(); + + // test timer unpause: verify the last time is null after being unpaused + chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "Wave: 2", "", 0); + timersPlugin.onChatMessage(chatMessage); + verify(infoBoxManager, times(2)).removeInfoBox(captor.capture()); + verify(infoBoxManager, times(3)).addInfoBox(captor.capture()); + assertTrue(captor.getValue() instanceof ElapsedTimer); + timer = (ElapsedTimer) captor.getValue(); + assertNull(timer.getLastTime()); + + // test timer remove: verify the infobox was removed (and no more were added) + chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "You have been defeated!", "", 0); + timersPlugin.onChatMessage(chatMessage); + verify(infoBoxManager, times(3)).removeInfoBox(captor.capture()); + verify(infoBoxManager, times(3)).addInfoBox(captor.capture()); + } } \ No newline at end of file