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 <slusnucky@gmail.com>
Co-authored-by: Jordan Atwood <jordan.atwood423@gmail.com>
This commit is contained in:
Matthew C
2020-10-24 09:56:35 +09:00
committed by GitHub
parent 10d36ea36a
commit 5a863f358f
5 changed files with 187 additions and 44 deletions

View File

@@ -24,6 +24,7 @@
*/ */
package net.runelite.client.plugins.discord; package net.runelite.client.plugins.discord;
import lombok.AllArgsConstructor;
import net.runelite.client.config.Config; import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigItem;
@@ -32,11 +33,38 @@ import net.runelite.client.config.Units;
@ConfigGroup("discord") @ConfigGroup("discord")
public interface DiscordConfig extends Config 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( @ConfigItem(
keyName = "actionTimeout", keyName = "actionTimeout",
name = "Action timeout", name = "Activity timeout",
description = "Configures after how long of not updating status will be reset (in minutes)", description = "Configures after how long of not updating activity will be reset (in minutes)",
position = 1 position = 2
) )
@Units(Units.MINUTES) @Units(Units.MINUTES)
default int actionTimeout() default int actionTimeout()
@@ -44,17 +72,6 @@ public interface DiscordConfig extends Config
return 5; 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( @ConfigItem(
keyName = "showSkillActivity", keyName = "showSkillActivity",
name = "Skilling", name = "Skilling",

View File

@@ -42,8 +42,8 @@ import net.runelite.api.Varbits;
enum DiscordGameEventType enum DiscordGameEventType
{ {
IN_GAME("In Game", -3), IN_MENU("In Menu", -3, true, true, true, false, true),
IN_MENU("In Menu", -3), IN_GAME("In Game", -3, true, false, false, false, true),
PLAYING_DEADMAN("Playing Deadman Mode", -3), PLAYING_DEADMAN("Playing Deadman Mode", -3),
PLAYING_PVP("Playing in a PVP world", -3), PLAYING_PVP("Playing in a PVP world", -3),
TRAINING_ATTACK(Skill.ATTACK), TRAINING_ATTACK(Skill.ATTACK),
@@ -462,9 +462,32 @@ enum DiscordGameEventType
private String details; private String details;
private int priority; 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; private boolean shouldClear;
/**
* Determines if event should be processed when it timeouts based on action timeout
*/
private boolean shouldTimeout; 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 @Nullable
private DiscordAreaType discordAreaType; private DiscordAreaType discordAreaType;
@@ -496,11 +519,20 @@ enum DiscordGameEventType
this.shouldClear = true; 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.state = state;
this.priority = priority; 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) DiscordGameEventType(String areaName, DiscordAreaType areaType, Varbits varbits)

View File

@@ -106,7 +106,7 @@ public class DiscordPlugin extends Plugin
@Inject @Inject
private OkHttpClient okHttpClient; private OkHttpClient okHttpClient;
private Map<Skill, Integer> skillExp = new HashMap<>(); private final Map<Skill, Integer> skillExp = new HashMap<>();
private NavigationButton discordButton; private NavigationButton discordButton;
private boolean loginFlag; private boolean loginFlag;
@@ -129,6 +129,7 @@ public class DiscordPlugin extends Plugin
.build(); .build();
clientToolbar.addNavigation(discordButton); clientToolbar.addNavigation(discordButton);
resetState();
checkForGameStateUpdate(); checkForGameStateUpdate();
checkForAreaUpdate(); checkForAreaUpdate();
@@ -144,7 +145,7 @@ public class DiscordPlugin extends Plugin
protected void shutDown() throws Exception protected void shutDown() throws Exception
{ {
clientToolbar.removeNavigation(discordButton); clientToolbar.removeNavigation(discordButton);
discordState.reset(); resetState();
partyService.changeParty(null); partyService.changeParty(null);
wsClient.unregisterMessage(DiscordUserInfo.class); wsClient.unregisterMessage(DiscordUserInfo.class);
} }
@@ -155,6 +156,7 @@ public class DiscordPlugin extends Plugin
switch (event.getGameState()) switch (event.getGameState())
{ {
case LOGIN_SCREEN: case LOGIN_SCREEN:
resetState();
checkForGameStateUpdate(); checkForGameStateUpdate();
return; return;
case LOGGING_IN: case LOGGING_IN:
@@ -164,9 +166,9 @@ public class DiscordPlugin extends Plugin
if (loginFlag) if (loginFlag)
{ {
loginFlag = false; loginFlag = false;
resetState();
checkForGameStateUpdate(); checkForGameStateUpdate();
} }
break; break;
} }
@@ -178,6 +180,7 @@ public class DiscordPlugin extends Plugin
{ {
if (event.getGroup().equalsIgnoreCase("discord")) if (event.getGroup().equalsIgnoreCase("discord"))
{ {
resetState();
checkForGameStateUpdate(); checkForGameStateUpdate();
checkForAreaUpdate(); checkForAreaUpdate();
} }
@@ -362,10 +365,13 @@ public class DiscordPlugin extends Plugin
discordState.refresh(); discordState.refresh();
} }
private void resetState()
{
discordState.reset();
}
private void checkForGameStateUpdate() private void checkForGameStateUpdate()
{ {
// Game state update does also full reset of discord state
discordState.reset();
discordState.triggerEvent(client.getGameState() == GameState.LOGGED_IN discordState.triggerEvent(client.getGameState() == GameState.LOGGED_IN
? DiscordGameEventType.IN_GAME ? DiscordGameEventType.IN_GAME
: DiscordGameEventType.IN_MENU); : DiscordGameEventType.IN_MENU);

View File

@@ -32,6 +32,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import lombok.Data; import lombok.Data;
import net.runelite.client.RuneLiteProperties; import net.runelite.client.RuneLiteProperties;
@@ -49,7 +51,7 @@ class DiscordState
private static class EventWithTime private static class EventWithTime
{ {
private final DiscordGameEventType type; private final DiscordGameEventType type;
private final Instant start; private Instant start;
private Instant updated; private Instant updated;
} }
@@ -57,7 +59,7 @@ class DiscordState
private final List<EventWithTime> events = new ArrayList<>(); private final List<EventWithTime> events = new ArrayList<>();
private final DiscordService discordService; private final DiscordService discordService;
private final DiscordConfig config; private final DiscordConfig config;
private PartyService party; private final PartyService party;
private DiscordPresence lastPresence; private DiscordPresence lastPresence;
@Inject @Inject
@@ -115,7 +117,7 @@ class DiscordState
void triggerEvent(final DiscordGameEventType eventType) void triggerEvent(final DiscordGameEventType eventType)
{ {
final Optional<EventWithTime> foundEvent = events.stream().filter(e -> e.type == eventType).findFirst(); final Optional<EventWithTime> foundEvent = events.stream().filter(e -> e.type == eventType).findFirst();
EventWithTime event; final EventWithTime event;
if (foundEvent.isPresent()) if (foundEvent.isPresent())
{ {
@@ -123,8 +125,8 @@ class DiscordState
} }
else else
{ {
event = new EventWithTime(eventType, Instant.now()); event = new EventWithTime(eventType);
event.setStart(Instant.now());
events.add(event); events.add(event);
} }
@@ -132,7 +134,12 @@ class DiscordState
if (event.getType().isShouldClear()) 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() events.sort((a, b) -> ComparisonChain.start()
@@ -140,7 +147,18 @@ class DiscordState
.compare(b.getUpdated(), a.getUpdated()) .compare(b.getUpdated(), a.getUpdated())
.result()); .result());
event = events.get(0); updatePresenceWithLatestEvent();
}
private void updatePresenceWithLatestEvent()
{
if (events.isEmpty())
{
reset();
return;
}
final EventWithTime event = events.get(0);
String imageKey = null; String imageKey = null;
String state = null; String state = null;
@@ -176,11 +194,35 @@ class DiscordState
.state(MoreObjects.firstNonNull(state, "")) .state(MoreObjects.firstNonNull(state, ""))
.details(MoreObjects.firstNonNull(details, "")) .details(MoreObjects.firstNonNull(details, ""))
.largeImageText(RuneLiteProperties.getTitle() + " v" + versionShortHand) .largeImageText(RuneLiteProperties.getTitle() + " v" + versionShortHand)
.startTimestamp(config.hideElapsedTime() ? null : event.getStart())
.smallImageKey(imageKey) .smallImageKey(imageKey)
.partyMax(PARTY_MAX) .partyMax(PARTY_MAX)
.partySize(party.getMembers().size()); .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()) if (!party.isInParty() || party.isPartyOwner())
{ {
presenceBuilder.partyId(partyId.toString()); presenceBuilder.partyId(partyId.toString());
@@ -209,19 +251,29 @@ class DiscordState
final Duration actionTimeout = Duration.ofMinutes(config.actionTimeout()); final Duration actionTimeout = Duration.ofMinutes(config.actionTimeout());
final Instant now = Instant.now(); 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 (removedAny || updatedAny.get())
if (DiscordGameEventType.IN_MENU.getState().equals(eventWithTime.getType().getState()) && now.isAfter(eventWithTime.getStart().plus(actionTimeout)))
{ {
final DiscordPresence presence = lastPresence updatePresenceWithLatestEvent();
.toBuilder()
.startTimestamp(null)
.build();
lastPresence = presence;
discordService.updatePresence(presence);
} }
} }
} }

View File

@@ -35,6 +35,7 @@ import net.runelite.api.Client;
import net.runelite.client.discord.DiscordPresence; import net.runelite.client.discord.DiscordPresence;
import net.runelite.client.discord.DiscordService; import net.runelite.client.discord.DiscordService;
import net.runelite.client.ws.PartyService; import net.runelite.client.ws.PartyService;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -77,10 +78,10 @@ public class DiscordStateTest
} }
@Test @Test
public void testStatusTimeout() public void testStatusReset()
{ {
when(discordConfig.actionTimeout()).thenReturn(0); when(discordConfig.actionTimeout()).thenReturn(-1);
when(discordConfig.hideElapsedTime()).thenReturn(false); when(discordConfig.elapsedTimeType()).thenReturn(DiscordConfig.ElapsedTimeType.ACTIVITY);
discordState.triggerEvent(DiscordGameEventType.IN_MENU); discordState.triggerEvent(DiscordGameEventType.IN_MENU);
verify(discordService).updatePresence(any(DiscordPresence.class)); verify(discordService).updatePresence(any(DiscordPresence.class));
@@ -91,4 +92,39 @@ public class DiscordStateTest
List<DiscordPresence> captured = captor.getAllValues(); List<DiscordPresence> captured = captor.getAllValues();
assertNull(captured.get(captured.size() - 1).getEndTimestamp()); 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<DiscordPresence> 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());
}
} }