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;
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",

View File

@@ -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)

View File

@@ -106,7 +106,7 @@ public class DiscordPlugin extends Plugin
@Inject
private OkHttpClient okHttpClient;
private Map<Skill, Integer> skillExp = new HashMap<>();
private final Map<Skill, Integer> 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);

View File

@@ -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<EventWithTime> 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<EventWithTime> 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();
}
}
}

View File

@@ -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<DiscordPresence> 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<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());
}
}