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:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user