diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordAreaType.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordAreaType.java new file mode 100644 index 0000000000..7beb6810aa --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordAreaType.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018, PandahRS + * 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.discord; + +enum DiscordAreaType +{ + BOSSES, + CITIES, + DUNGEONS, + MINIGAMES; +} 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 b8f082466e..76c393338d 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 @@ -34,23 +34,14 @@ public interface DiscordConfig extends Config @ConfigItem( keyName = "actionTimeout", name = "Action timeout (minutes)", - description = "Configures after how long of not updating status will be reset (in minutes)" + description = "Configures after how long of not updating status will be reset (in minutes)", + position = 1 ) default int actionTimeout() { return 5; } - @ConfigItem( - keyName = "actionDelay", - name = "New action delay (seconds)", - description = "Configures the delay before new action will be considered as valid" - ) - default int actionDelay() - { - return 10; - } - @ConfigItem( keyName = "showSkillActivity", name = "Show activity while 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 d5b2d936bb..b5eb6330f5 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2018, PandahRS * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,103 +25,100 @@ */ package net.runelite.client.plugins.discord; -import com.google.common.collect.ImmutableSet; -import java.util.Set; -import java.util.function.Function; +import java.util.HashMap; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; import net.runelite.api.Skill; -import static net.runelite.api.Skill.AGILITY; -import static net.runelite.api.Skill.ATTACK; -import static net.runelite.api.Skill.CONSTRUCTION; -import static net.runelite.api.Skill.COOKING; -import static net.runelite.api.Skill.CRAFTING; -import static net.runelite.api.Skill.DEFENCE; -import static net.runelite.api.Skill.FARMING; -import static net.runelite.api.Skill.FIREMAKING; -import static net.runelite.api.Skill.FISHING; -import static net.runelite.api.Skill.FLETCHING; -import static net.runelite.api.Skill.HERBLORE; -import static net.runelite.api.Skill.HITPOINTS; -import static net.runelite.api.Skill.HUNTER; -import static net.runelite.api.Skill.MAGIC; -import static net.runelite.api.Skill.MINING; -import static net.runelite.api.Skill.PRAYER; -import static net.runelite.api.Skill.RANGED; -import static net.runelite.api.Skill.RUNECRAFT; -import static net.runelite.api.Skill.SLAYER; -import static net.runelite.api.Skill.SMITHING; -import static net.runelite.api.Skill.STRENGTH; -import static net.runelite.api.Skill.THIEVING; -import static net.runelite.api.Skill.WOODCUTTING; -@RequiredArgsConstructor @AllArgsConstructor @Getter -public enum DiscordGameEventType +enum DiscordGameEventType { - IN_GAME("In Game", false), - IN_MENU("In Menu", false), - TRAINING_ATTACK(ATTACK, DiscordGameEventType::combatSkillChanged), - TRAINING_DEFENCE(DEFENCE, DiscordGameEventType::combatSkillChanged), - TRAINING_STRENGTH(STRENGTH, DiscordGameEventType::combatSkillChanged), - TRAINING_HITPOINTS(HITPOINTS, DiscordGameEventType::combatSkillChanged), - TRAINING_SLAYER(SLAYER, 1, DiscordGameEventType::combatSkillChanged), - TRAINING_RANGED(RANGED, DiscordGameEventType::combatSkillChanged), - TRAINING_MAGIC(MAGIC, DiscordGameEventType::combatSkillChanged), - TRAINING_PRAYER(PRAYER), - TRAINING_COOKING(COOKING), - TRAINING_WOODCUTTING(WOODCUTTING), - TRAINING_FLETCHING(FLETCHING), - TRAINING_FISHING(FISHING), - TRAINING_FIREMAKING(FIREMAKING), - TRAINING_CRAFTING(CRAFTING), - TRAINING_SMITHING(SMITHING), - TRAINING_MINING(MINING), - TRAINING_HERBLORE(HERBLORE), - TRAINING_AGILITY(AGILITY), - TRAINING_THIEVING(THIEVING), - TRAINING_FARMING(FARMING), - TRAINING_RUNECRAFT(RUNECRAFT), - TRAINING_HUNTER(HUNTER), - TRAINING_CONSTRUCTION(CONSTRUCTION); - private static final Set COMBAT_SKILLS = ImmutableSet.of(ATTACK, STRENGTH, DEFENCE, HITPOINTS, SLAYER, RANGED, MAGIC); + IN_GAME("In Game", -3), + IN_MENU("In Menu", -3), + TRAINING_ATTACK(Skill.ATTACK), + TRAINING_DEFENCE(Skill.DEFENCE), + TRAINING_STRENGTH(Skill.STRENGTH), + TRAINING_HITPOINTS(Skill.HITPOINTS, -1), + TRAINING_SLAYER(Skill.SLAYER, 1), + TRAINING_RANGED(Skill.RANGED), + TRAINING_MAGIC(Skill.MAGIC), + TRAINING_PRAYER(Skill.PRAYER), + TRAINING_COOKING(Skill.COOKING), + TRAINING_WOODCUTTING(Skill.WOODCUTTING), + TRAINING_FLETCHING(Skill.FLETCHING), + TRAINING_FISHING(Skill.FISHING), + TRAINING_FIREMAKING(Skill.FIREMAKING), + TRAINING_CRAFTING(Skill.CRAFTING), + TRAINING_SMITHING(Skill.SMITHING), + TRAINING_MINING(Skill.MINING), + TRAINING_HERBLORE(Skill.HERBLORE), + TRAINING_AGILITY(Skill.AGILITY), + TRAINING_THIEVING(Skill.THIEVING), + TRAINING_FARMING(Skill.FARMING), + TRAINING_RUNECRAFT(Skill.RUNECRAFT), + TRAINING_HUNTER(Skill.HUNTER), + TRAINING_CONSTRUCTION(Skill.CONSTRUCTION); - private final String state; - private final String imageKey; + private static final Map FROM_REGION = new HashMap<>(); + + static + { + for (DiscordGameEventType discordGameEventType : DiscordGameEventType.values()) + { + if (discordGameEventType.getRegionIds() == null) + { + continue; + } + + for (int region : discordGameEventType.getRegionIds()) + { + assert !FROM_REGION.containsKey(region); + FROM_REGION.put(region, discordGameEventType); + } + } + } + + private String imageKey; + private String state; private String details; - private boolean considerDelay = true; - private Function isChanged = (l) -> true; - private int priority = 0; + private int priority; + private boolean shouldClear; + private boolean shouldTimeout; - DiscordGameEventType(String state, boolean considerDelay) - { - this.state = state; - this.imageKey = "default"; - this.considerDelay = considerDelay; - } - - DiscordGameEventType(Skill skill, int priority, Function isChanged) - { - this.state = training(skill); - this.imageKey = imageKeyOf(skill); - this.priority = priority; - this.isChanged = isChanged; - } - - DiscordGameEventType(Skill skill, Function isChanged) - { - this.state = training(skill); - this.imageKey = imageKeyOf(skill); - this.isChanged = isChanged; - } + private DiscordAreaType discordAreaType; + private int[] regionIds; DiscordGameEventType(Skill skill) + { + this(skill, 0); + } + + DiscordGameEventType(Skill skill, int priority) { this.state = training(skill); + this.priority = priority; this.imageKey = imageKeyOf(skill); + this.priority = priority; + this.shouldTimeout = true; + } + + DiscordGameEventType(String areaName, DiscordAreaType areaType, int... regionIds) + { + this.details = exploring(areaType, areaName); + this.priority = -2; + this.discordAreaType = areaType; + this.regionIds = regionIds; + this.shouldClear = true; + } + + DiscordGameEventType(String state, int priority) + { + this.details = state; + this.priority = priority; + this.shouldClear = true; } private static String training(final Skill skill) @@ -143,17 +141,21 @@ public enum DiscordGameEventType return "icon_" + what; } - private static boolean combatSkillChanged(final DiscordGameEventType l) + private static String exploring(DiscordAreaType areaType, String areaName) { - for (Skill skill : Skill.values()) + switch (areaType) { - if (l.getState().contains(skill.getName())) - { - return !COMBAT_SKILLS.contains(skill); - } + case BOSSES: + return "Fighting: " + areaName; + case DUNGEONS: + return "Exploring: " + areaName; + case CITIES: + return "Location: " + areaName; + case MINIGAMES: + return "Playing: " + areaName; } - return true; + return ""; } public static DiscordGameEventType fromSkill(final Skill skill) @@ -185,4 +187,9 @@ public enum DiscordGameEventType default: return null; } } + + public static DiscordGameEventType fromRegion(final int regionId) + { + return FROM_REGION.get(regionId); + } } 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 ea194faf3f..f32992514d 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2018, PandahRS * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -29,17 +30,22 @@ import com.google.inject.Inject; import com.google.inject.Provides; import java.awt.image.BufferedImage; import java.time.temporal.ChronoUnit; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import javax.imageio.ImageIO; import net.runelite.api.Client; +import static net.runelite.api.Constants.CHUNK_SIZE; import net.runelite.api.GameState; import net.runelite.api.Skill; +import net.runelite.api.WorldType; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.ExperienceChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.client.RuneLiteProperties; import net.runelite.client.config.ConfigManager; -import net.runelite.client.discord.DiscordService; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.task.Schedule; @@ -60,19 +66,18 @@ public class DiscordPlugin extends Plugin @Inject private DiscordConfig config; - @Inject - private DiscordService discordService; - @Inject private TitleToolbar titleToolbar; @Inject private RuneLiteProperties properties; - private final DiscordState discordState = new DiscordState(); + @Inject + private DiscordState discordState; + private Map skillExp = new HashMap<>(); - private boolean loggedIn = false; private NavigationButton discordButton; + private boolean loginFlag; @Provides private DiscordConfig provideConfig(ConfigManager configManager) @@ -96,21 +101,48 @@ public class DiscordPlugin extends Plugin .build(); titleToolbar.addNavigation(discordButton); - updateGameStatus(client.getGameState(), true); + checkForGameStateUpdate(); } @Override protected void shutDown() throws Exception { titleToolbar.removeNavigation(discordButton); - discordService.clearPresence(); discordState.reset(); } @Subscribe public void onGameStateChanged(GameStateChanged event) { - updateGameStatus(event.getGameState(), false); + switch (event.getGameState()) + { + case LOGIN_SCREEN: + checkForGameStateUpdate(); + return; + case LOGGING_IN: + loginFlag = true; + break; + case LOGGED_IN: + if (loginFlag) + { + loginFlag = false; + checkForGameStateUpdate(); + } + + break; + } + + checkForAreaUpdate(); + } + + @Subscribe + public void configChanged(ConfigChanged event) + { + if (event.getGroup().equalsIgnoreCase("discord")) + { + checkForGameStateUpdate(); + checkForAreaUpdate(); + } } @Subscribe @@ -128,7 +160,7 @@ public class DiscordPlugin extends Plugin if (discordGameEventType != null && config.showSkillingActivity()) { - discordState.triggerEvent(discordGameEventType, config.actionDelay()); + discordState.triggerEvent(discordGameEventType); } } @@ -138,33 +170,89 @@ public class DiscordPlugin extends Plugin ) public void checkForValidStatus() { - if (discordState.checkForTimeout(config.actionTimeout())) - { - updateGameStatus(client.getGameState(), true); - } + discordState.checkForTimeout(); } - @Schedule( - period = 1, - unit = ChronoUnit.SECONDS - ) - public void flushDiscordStatus() + private void checkForGameStateUpdate() { - discordState.flushEvent(discordService); + // 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); } - private void updateGameStatus(GameState gameState, boolean force) + private void checkForAreaUpdate() { - if (gameState == GameState.LOGIN_SCREEN) + if (client.getLocalPlayer() == null) { - skillExp.clear(); - loggedIn = false; - discordState.triggerEvent(DiscordGameEventType.IN_MENU, config.actionDelay()); + return; } - else if (client.getGameState() == GameState.LOGGED_IN && (force || !loggedIn)) + + final int playerRegionID = getCurrentRegion(); + + if (playerRegionID == 0) { - loggedIn = true; - discordState.triggerEvent(DiscordGameEventType.IN_GAME, config.actionDelay()); + return; } + + final DiscordGameEventType discordGameEventType = DiscordGameEventType.fromRegion(playerRegionID); + + if (discordGameEventType == null) + { + // Unknown region, reset to default in-game + discordState.triggerEvent(DiscordGameEventType.IN_GAME); + return; + } + + if (!showArea(discordGameEventType)) + { + return; + } + + discordState.triggerEvent(discordGameEventType); } + + private boolean showArea(final DiscordGameEventType event) + { + if (event == null) + { + return false; + } + + final EnumSet worldType = client.getWorldType(); + + // Do not show location in PVP activities + if (worldType.contains(WorldType.SEASONAL_DEADMAN) || + worldType.contains(WorldType.DEADMAN) || + worldType.contains(WorldType.PVP) || + worldType.contains(WorldType.PVP_HIGH_RISK)) + { + return false; + } + + return true; + } + + private int getCurrentRegion() + { + if (!client.isInInstancedRegion()) + { + return client.getLocalPlayer().getWorldLocation().getRegionID(); + } + + // get chunk data of current chunk + final LocalPoint localPoint = client.getLocalPlayer().getLocalLocation(); + final int[][][] instanceTemplateChunks = client.getInstanceTemplateChunks(); + final int z = client.getPlane(); + final int chunkData = instanceTemplateChunks[z][localPoint.getRegionX() / CHUNK_SIZE][localPoint.getRegionY() / CHUNK_SIZE]; + + // extract world point from chunk data + final int chunkY = (chunkData >> 3 & 0x7FF) * CHUNK_SIZE; + final int chunkX = (chunkData >> 14 & 0x3FF) * CHUNK_SIZE; + + final WorldPoint worldPoint = new WorldPoint(chunkX, chunkY, z); + return worldPoint.getRegionID(); + } + } 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 6573b633d3..6da66d34e9 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 @@ -24,109 +24,136 @@ */ package net.runelite.client.plugins.discord; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ComparisonChain; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; +import java.util.Optional; +import javax.inject.Inject; +import lombok.Data; import net.runelite.client.discord.DiscordPresence; import net.runelite.client.discord.DiscordService; -public class DiscordState +/** + * This class contains data about currently active discord state. + */ +class DiscordState { - private final List lastQueue = new ArrayList<>(); - private DiscordGameEventType lastEvent; - private Instant startOfAction; - private Instant lastAction; - private DiscordPresence lastPresence; - private boolean needsFlush; + @Data + private class EventWithTime + { + private final DiscordGameEventType type; + private final Instant start; + private Instant updated; + } + private final List events = new ArrayList<>(); + private final DiscordService discordService; + private final DiscordConfig config; + private DiscordPresence lastPresence; + + @Inject + private DiscordState(final DiscordService discordService, final DiscordConfig config) + { + this.discordService = discordService; + this.config = config; + } + + /** + * Reset state. + */ void reset() { - lastQueue.clear(); - lastEvent = null; - startOfAction = null; - lastAction = null; + discordService.clearPresence(); + events.clear(); lastPresence = null; - needsFlush = false; } - void flushEvent(DiscordService discordService) + /** + * Trigger new discord state update. + * + * @param eventType discord event type + */ + void triggerEvent(final DiscordGameEventType eventType) { - if (lastPresence != null && needsFlush) - { - needsFlush = false; - discordService.updatePresence(lastPresence); - } - } + final Optional foundEvent = events.stream().filter(e -> e.type == eventType).findFirst(); + EventWithTime event; - void triggerEvent(final DiscordGameEventType eventType, int delay) - { - final boolean first = startOfAction == null; - final boolean changed = eventType != lastEvent && eventType.getIsChanged().apply(lastEvent); - boolean reset = false; - - if (first) + if (foundEvent.isPresent()) { - reset = true; + event = foundEvent.get(); } - else if (changed) + else { - if (eventType.isConsiderDelay()) + event = new EventWithTime(eventType, Instant.now()); + events.add(event); + } + + event.setUpdated(Instant.now()); + + if (event.getType().isShouldClear()) + { + events.removeIf(e -> e.getType() != eventType && e.getType().isShouldClear()); + } + + events.sort((a, b) -> ComparisonChain.start() + .compare(b.getType().getPriority(), a.getType().getPriority()) + .compare(b.getUpdated(), a.getUpdated()) + .result()); + + event = events.get(0); + + String imageKey = null; + String state = null; + String details = null; + + for (EventWithTime eventWithTime : events) + { + if (imageKey == null) { - final Duration actionDelay = Duration.ofSeconds(delay); - final Duration sinceLastAction = Duration.between(lastAction, Instant.now()); - - if (sinceLastAction.compareTo(actionDelay) >= 0) - { - reset = true; - } + imageKey = eventWithTime.getType().getImageKey(); } - else + + if (details == null) { - reset = true; + details = eventWithTime.getType().getDetails(); + } + + if (state == null) + { + state = eventWithTime.getType().getState(); + } + + if (imageKey != null && details != null && state != null) + { + break; } } - if (reset) + final DiscordPresence presence = DiscordPresence.builder() + .state(MoreObjects.firstNonNull(state, "")) + .details(MoreObjects.firstNonNull(details, "")) + .startTimestamp(event.getStart()) + .smallImageKey(MoreObjects.firstNonNull(imageKey, "default")) + .build(); + + // This is to reduce amount of RPC calls + if (!presence.equals(lastPresence)) { - lastQueue.clear(); - startOfAction = Instant.now(); - } - - if (!lastQueue.contains(eventType)) - { - lastQueue.add(eventType); - lastQueue.sort(Comparator.comparingInt(DiscordGameEventType::getPriority)); - } - - lastAction = Instant.now(); - final DiscordGameEventType newEvent = lastQueue.get(lastQueue.size() - 1); - - if (lastEvent != newEvent) - { - lastEvent = newEvent; - - lastPresence = DiscordPresence.builder() - .state(lastEvent.getState()) - .details(lastEvent.getDetails()) - .startTimestamp(startOfAction) - .smallImageKey(newEvent.getImageKey()) - .build(); - - needsFlush = true; + lastPresence = presence; + discordService.updatePresence(presence); } } - boolean checkForTimeout(final int timeout) + /** + * Check for current state timeout and act upon it. + */ + void checkForTimeout() { - if (lastAction == null) - { - return false; - } - - final Duration actionTimeout = Duration.ofMinutes(timeout); - - return Instant.now().isAfter(lastAction.plus(actionTimeout)); + final Duration actionTimeout = Duration.ofMinutes(config.actionTimeout()); + events.removeIf(event -> event.getType().isShouldTimeout() && + event.getUpdated().isAfter(event.getStart().plus(actionTimeout))); } }