From 5a863f358f1221a537282269e3946d7d12cabe70 Mon Sep 17 00:00:00 2001 From: Matthew C <66925241+Matthew-nop@users.noreply.github.com> Date: Sat, 24 Oct 2020 09:56:35 +0900 Subject: [PATCH] discord: Fix action timeout, add in game time elapsed option (#12471) Co-authored-by: Matthew C <66925241+Matthew-nop@users.noreply.github.com> Co-authored-by: Tomas Slusny Co-authored-by: Jordan Atwood --- .../client/plugins/discord/DiscordConfig.java | 45 +++++++--- .../plugins/discord/DiscordGameEventType.java | 40 ++++++++- .../client/plugins/discord/DiscordPlugin.java | 16 ++-- .../client/plugins/discord/DiscordState.java | 88 +++++++++++++++---- .../plugins/discord/DiscordStateTest.java | 42 ++++++++- 5 files changed, 187 insertions(+), 44 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordConfig.java index 62b04c7e97..770fe49cde 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordConfig.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.discord; +import lombok.AllArgsConstructor; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; @@ -32,11 +33,38 @@ import net.runelite.client.config.Units; @ConfigGroup("discord") public interface DiscordConfig extends Config { + @AllArgsConstructor + enum ElapsedTimeType + { + TOTAL("Total elapsed time"), + ACTIVITY("Per activity"), + HIDDEN("Hide elapsed time"); + + private final String value; + + @Override + public String toString() + { + return value; + } + } + + @ConfigItem( + keyName = "elapsedTime", + name = "Elapsed Time", + description = "Configures elapsed time shown.", + position = 1 + ) + default ElapsedTimeType elapsedTimeType() + { + return ElapsedTimeType.ACTIVITY; + } + @ConfigItem( keyName = "actionTimeout", - name = "Action timeout", - description = "Configures after how long of not updating status will be reset (in minutes)", - position = 1 + name = "Activity timeout", + description = "Configures after how long of not updating activity will be reset (in minutes)", + position = 2 ) @Units(Units.MINUTES) default int actionTimeout() @@ -44,17 +72,6 @@ public interface DiscordConfig extends Config return 5; } - @ConfigItem( - keyName = "hideElapsedTime", - name = "Hide elapsed time", - description = "Configures if the elapsed time of your activity should be hidden.", - position = 2 - ) - default boolean hideElapsedTime() - { - return false; - } - @ConfigItem( keyName = "showSkillActivity", name = "Skilling", diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordGameEventType.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordGameEventType.java index d1a808868e..e298743344 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordGameEventType.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordGameEventType.java @@ -42,8 +42,8 @@ import net.runelite.api.Varbits; enum DiscordGameEventType { - IN_GAME("In Game", -3), - IN_MENU("In Menu", -3), + IN_MENU("In Menu", -3, true, true, true, false, true), + IN_GAME("In Game", -3, true, false, false, false, true), PLAYING_DEADMAN("Playing Deadman Mode", -3), PLAYING_PVP("Playing in a PVP world", -3), TRAINING_ATTACK(Skill.ATTACK), @@ -462,9 +462,32 @@ enum DiscordGameEventType private String details; private int priority; + + /** + * Marks this event as root event, e.g event that should be used for total time tracking + */ + private boolean root; + + /** + * Determines if event should clear other clearable events when triggered + */ private boolean shouldClear; + + /** + * Determines if event should be processed when it timeouts based on action timeout + */ private boolean shouldTimeout; + /** + * Determines if event start time should be reset when processed + */ + private boolean shouldRestart; + + /** + * Determines if event should be cleared when processed + */ + private boolean shouldBeCleared = true; + @Nullable private DiscordAreaType discordAreaType; @@ -496,11 +519,20 @@ enum DiscordGameEventType this.shouldClear = true; } - DiscordGameEventType(String state, int priority) + DiscordGameEventType(String state, int priority, boolean shouldClear, boolean shouldTimeout, boolean shouldRestart, boolean shouldBeCleared, boolean root) { this.state = state; this.priority = priority; - this.shouldClear = true; + this.shouldClear = shouldClear; + this.shouldTimeout = shouldTimeout; + this.shouldRestart = shouldRestart; + this.shouldBeCleared = shouldBeCleared; + this.root = root; + } + + DiscordGameEventType(String state, int priority) + { + this(state, priority, true, false, false, true, false); } DiscordGameEventType(String areaName, DiscordAreaType areaType, Varbits varbits) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java index 3b86dea2fe..e2b0cb157b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java @@ -106,7 +106,7 @@ public class DiscordPlugin extends Plugin @Inject private OkHttpClient okHttpClient; - private Map skillExp = new HashMap<>(); + private final Map skillExp = new HashMap<>(); private NavigationButton discordButton; private boolean loginFlag; @@ -129,6 +129,7 @@ public class DiscordPlugin extends Plugin .build(); clientToolbar.addNavigation(discordButton); + resetState(); checkForGameStateUpdate(); checkForAreaUpdate(); @@ -144,7 +145,7 @@ public class DiscordPlugin extends Plugin protected void shutDown() throws Exception { clientToolbar.removeNavigation(discordButton); - discordState.reset(); + resetState(); partyService.changeParty(null); wsClient.unregisterMessage(DiscordUserInfo.class); } @@ -155,6 +156,7 @@ public class DiscordPlugin extends Plugin switch (event.getGameState()) { case LOGIN_SCREEN: + resetState(); checkForGameStateUpdate(); return; case LOGGING_IN: @@ -164,9 +166,9 @@ public class DiscordPlugin extends Plugin if (loginFlag) { loginFlag = false; + resetState(); checkForGameStateUpdate(); } - break; } @@ -178,6 +180,7 @@ public class DiscordPlugin extends Plugin { if (event.getGroup().equalsIgnoreCase("discord")) { + resetState(); checkForGameStateUpdate(); checkForAreaUpdate(); } @@ -362,10 +365,13 @@ public class DiscordPlugin extends Plugin discordState.refresh(); } + private void resetState() + { + discordState.reset(); + } + private void checkForGameStateUpdate() { - // Game state update does also full reset of discord state - discordState.reset(); discordState.triggerEvent(client.getGameState() == GameState.LOGGED_IN ? DiscordGameEventType.IN_GAME : DiscordGameEventType.IN_MENU); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java index 0b49445f52..671107833e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java @@ -32,6 +32,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import javax.inject.Inject; import lombok.Data; import net.runelite.client.RuneLiteProperties; @@ -49,7 +51,7 @@ class DiscordState private static class EventWithTime { private final DiscordGameEventType type; - private final Instant start; + private Instant start; private Instant updated; } @@ -57,7 +59,7 @@ class DiscordState private final List events = new ArrayList<>(); private final DiscordService discordService; private final DiscordConfig config; - private PartyService party; + private final PartyService party; private DiscordPresence lastPresence; @Inject @@ -115,7 +117,7 @@ class DiscordState void triggerEvent(final DiscordGameEventType eventType) { final Optional foundEvent = events.stream().filter(e -> e.type == eventType).findFirst(); - EventWithTime event; + final EventWithTime event; if (foundEvent.isPresent()) { @@ -123,8 +125,8 @@ class DiscordState } else { - event = new EventWithTime(eventType, Instant.now()); - + event = new EventWithTime(eventType); + event.setStart(Instant.now()); events.add(event); } @@ -132,7 +134,12 @@ class DiscordState if (event.getType().isShouldClear()) { - events.removeIf(e -> e.getType() != eventType && e.getType().isShouldClear()); + events.removeIf(e -> e.getType() != eventType && e.getType().isShouldBeCleared()); + } + + if (event.getType().isShouldRestart()) + { + event.setStart(Instant.now()); } events.sort((a, b) -> ComparisonChain.start() @@ -140,7 +147,18 @@ class DiscordState .compare(b.getUpdated(), a.getUpdated()) .result()); - event = events.get(0); + updatePresenceWithLatestEvent(); + } + + private void updatePresenceWithLatestEvent() + { + if (events.isEmpty()) + { + reset(); + return; + } + + final EventWithTime event = events.get(0); String imageKey = null; String state = null; @@ -176,11 +194,35 @@ class DiscordState .state(MoreObjects.firstNonNull(state, "")) .details(MoreObjects.firstNonNull(details, "")) .largeImageText(RuneLiteProperties.getTitle() + " v" + versionShortHand) - .startTimestamp(config.hideElapsedTime() ? null : event.getStart()) .smallImageKey(imageKey) .partyMax(PARTY_MAX) .partySize(party.getMembers().size()); + final Instant startTime; + switch (config.elapsedTimeType()) + { + case HIDDEN: + startTime = null; + break; + case TOTAL: + // We are tracking total time spent instead of per activity time so try to find + // root event as this indicates start of tracking and find last updated one + // to determine correct state we are in + startTime = events.stream() + .filter(e -> e.getType().isRoot()) + .sorted((a, b) -> b.getUpdated().compareTo(a.getUpdated())) + .map(EventWithTime::getStart) + .findFirst() + .orElse(event.getStart()); + break; + case ACTIVITY: + default: + startTime = event.getStart(); + break; + } + + presenceBuilder.startTimestamp(startTime); + if (!party.isInParty() || party.isPartyOwner()) { presenceBuilder.partyId(partyId.toString()); @@ -209,19 +251,29 @@ class DiscordState final Duration actionTimeout = Duration.ofMinutes(config.actionTimeout()); final Instant now = Instant.now(); - final EventWithTime eventWithTime = events.get(0); + final AtomicBoolean updatedAny = new AtomicBoolean(); - events.removeIf(event -> event.getType().isShouldTimeout() && now.isAfter(event.getUpdated().plus(actionTimeout))); + final boolean removedAny = events.removeAll(events.stream() + // Find only events that should time out + .filter(event -> event.getType().isShouldTimeout() && now.isAfter(event.getUpdated().plus(actionTimeout))) + // Reset start times on timed events that should restart + .peek(event -> + { + if (event.getType().isShouldRestart()) + { + event.setStart(null); + updatedAny.set(true); + } + }) + // Now filter out events that should restart as we do not want to remove them + .filter(event -> !event.getType().isShouldRestart()) + .filter(event -> event.getType().isShouldBeCleared()) + .collect(Collectors.toList()) + ); - assert DiscordGameEventType.IN_MENU.getState() != null; - if (DiscordGameEventType.IN_MENU.getState().equals(eventWithTime.getType().getState()) && now.isAfter(eventWithTime.getStart().plus(actionTimeout))) + if (removedAny || updatedAny.get()) { - final DiscordPresence presence = lastPresence - .toBuilder() - .startTimestamp(null) - .build(); - lastPresence = presence; - discordService.updatePresence(presence); + updatePresenceWithLatestEvent(); } } } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java index 81c6e36dc8..8ce42350e6 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/discord/DiscordStateTest.java @@ -35,6 +35,7 @@ import net.runelite.api.Client; import net.runelite.client.discord.DiscordPresence; import net.runelite.client.discord.DiscordService; import net.runelite.client.ws.PartyService; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import org.junit.Before; import org.junit.Test; @@ -77,10 +78,10 @@ public class DiscordStateTest } @Test - public void testStatusTimeout() + public void testStatusReset() { - when(discordConfig.actionTimeout()).thenReturn(0); - when(discordConfig.hideElapsedTime()).thenReturn(false); + when(discordConfig.actionTimeout()).thenReturn(-1); + when(discordConfig.elapsedTimeType()).thenReturn(DiscordConfig.ElapsedTimeType.ACTIVITY); discordState.triggerEvent(DiscordGameEventType.IN_MENU); verify(discordService).updatePresence(any(DiscordPresence.class)); @@ -91,4 +92,39 @@ public class DiscordStateTest List captured = captor.getAllValues(); assertNull(captured.get(captured.size() - 1).getEndTimestamp()); } + + @Test + public void testStatusTimeout() + { + when(discordConfig.actionTimeout()).thenReturn(-1); + when(discordConfig.elapsedTimeType()).thenReturn(DiscordConfig.ElapsedTimeType.ACTIVITY); + + discordState.triggerEvent(DiscordGameEventType.TRAINING_AGILITY); + verify(discordService).updatePresence(any(DiscordPresence.class)); + + discordState.checkForTimeout(); + verify(discordService, times(1)).clearPresence(); + } + + @Test + public void testAreaChange() + { + when(discordConfig.elapsedTimeType()).thenReturn(DiscordConfig.ElapsedTimeType.TOTAL); + + // Start with state of IN_GAME + ArgumentCaptor captor = ArgumentCaptor.forClass(DiscordPresence.class); + discordState.triggerEvent(DiscordGameEventType.IN_GAME); + verify(discordService, times(1)).updatePresence(captor.capture()); + assertEquals(DiscordGameEventType.IN_GAME.getState(), captor.getValue().getState()); + + // IN_GAME -> CITY + discordState.triggerEvent(DiscordGameEventType.CITY_VARROCK); + verify(discordService, times(2)).updatePresence(captor.capture()); + assertEquals(DiscordGameEventType.CITY_VARROCK.getState(), captor.getValue().getState()); + + // CITY -> IN_GAME + discordState.triggerEvent(DiscordGameEventType.IN_GAME); + verify(discordService, times(3)).updatePresence(captor.capture()); + assertEquals(DiscordGameEventType.IN_GAME.getState(), captor.getValue().getState()); + } }