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 new file mode 100644 index 0000000000..c8460dedd8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "discord", + name = "Discord", + description = "Configuration for Discord plugin" +) +public interface DiscordConfig extends Config +{ + @ConfigItem( + keyName = "enabled", + name = "Enabled", + description = "Configures whether or not Discord plugin is enabled" + ) + default boolean enabled() + { + return true; + } + + @ConfigItem( + keyName = "actionTimeout", + name = "Action timeout (minutes)", + description = "Configures after how long of not updating status will be reset (in minutes)" + ) + 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; + } +} 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 new file mode 100644 index 0000000000..7b221346b2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordGameEventType.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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; + +import com.google.common.collect.Sets; +import java.util.Set; +import java.util.function.Function; +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 +{ + IN_GAME("In Game", false), + IN_MENU("In Menu", false), + TRAINING_ATTACK(training(ATTACK), DiscordGameEventType::combatSkillChanged), + TRAINING_DEFENCE(training(DEFENCE), DiscordGameEventType::combatSkillChanged), + TRAINING_STRENGTH(training(STRENGTH), DiscordGameEventType::combatSkillChanged), + TRAINING_HITPOINTS(training(HITPOINTS), DiscordGameEventType::combatSkillChanged), + TRAINING_SLAYER(training(SLAYER), 1, DiscordGameEventType::combatSkillChanged), + TRAINING_RANGED(training(RANGED), DiscordGameEventType::combatSkillChanged), + TRAINING_MAGIC(training(MAGIC), DiscordGameEventType::combatSkillChanged), + TRAINING_PRAYER(training(PRAYER)), + TRAINING_COOKING(training(COOKING)), + TRAINING_WOODCUTTING(training(WOODCUTTING)), + TRAINING_FLETCHING(training(FLETCHING)), + TRAINING_FISHING(training(FISHING)), + TRAINING_FIREMAKING(training(FIREMAKING)), + TRAINING_CRAFTING(training(CRAFTING)), + TRAINING_SMITHING(training(SMITHING)), + TRAINING_MINING(training(MINING)), + TRAINING_HERBLORE(training(HERBLORE)), + TRAINING_AGILITY(training(AGILITY)), + TRAINING_THIEVING(training(THIEVING)), + TRAINING_FARMING(training(FARMING)), + TRAINING_RUNECRAFT(training(RUNECRAFT)), + TRAINING_HUNTER(training(HUNTER)), + TRAINING_CONSTRUCTION(training(CONSTRUCTION)); + + private static final Set COMBAT_SKILLS = Sets.newHashSet(ATTACK, STRENGTH, DEFENCE, HITPOINTS, SLAYER, RANGED, MAGIC); + private final String state; + private String details; + private boolean considerDelay = true; + private Function isChanged = (l) -> true; + private int priority = 0; + + DiscordGameEventType(String state, boolean considerDelay) + { + this.state = state; + this.considerDelay = considerDelay; + } + + DiscordGameEventType(String state, int priority, Function isChanged) + { + this.state = state; + this.priority = priority; + this.isChanged = isChanged; + } + + DiscordGameEventType(String state, Function isChanged) + { + this.state = state; + this.isChanged = isChanged; + } + + private static String training(final Skill skill) + { + return training(skill.getName()); + } + + private static String training(final String what) + { + return "Training: " + what; + } + + static boolean combatSkillChanged(final DiscordGameEventType l) + { + for (Skill skill : Skill.values()) + { + if (l.getState().contains(skill.getName())) + { + return !COMBAT_SKILLS.contains(skill); + } + } + + return true; + } + + public static DiscordGameEventType fromSkill(final Skill skill) + { + switch (skill) + { + case ATTACK: return TRAINING_ATTACK; + case DEFENCE: return TRAINING_DEFENCE; + case STRENGTH: return TRAINING_STRENGTH; + case RANGED: return TRAINING_RANGED; + case PRAYER: return TRAINING_PRAYER; + case MAGIC: return TRAINING_MAGIC; + case COOKING: return TRAINING_COOKING; + case WOODCUTTING: return TRAINING_WOODCUTTING; + case FLETCHING: return TRAINING_FLETCHING; + case FISHING: return TRAINING_FISHING; + case FIREMAKING: return TRAINING_FIREMAKING; + case CRAFTING: return TRAINING_CRAFTING; + case SMITHING: return TRAINING_SMITHING; + case MINING: return TRAINING_MINING; + case HERBLORE: return TRAINING_HERBLORE; + case AGILITY: return TRAINING_AGILITY; + case THIEVING: return TRAINING_THIEVING; + case SLAYER: return TRAINING_SLAYER; + case FARMING: return TRAINING_FARMING; + case RUNECRAFT: return TRAINING_RUNECRAFT; + case HUNTER: return TRAINING_HUNTER; + case CONSTRUCTION: return TRAINING_CONSTRUCTION; + } + + return null; + } +} 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 new file mode 100644 index 0000000000..b8f8666e4e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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; + +import com.google.common.eventbus.Subscribe; +import com.google.inject.Inject; +import com.google.inject.Provides; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.Skill; +import net.runelite.api.events.ExperienceChanged; +import net.runelite.api.events.GameStateChanged; +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; + +@PluginDescriptor( + name = "Discord plugin" +) +public class DiscordPlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private DiscordConfig config; + + @Inject + private DiscordService discordService; + + private final DiscordState discordState = new DiscordState(); + private Map skillExp = new HashMap<>(); + private boolean loggedIn = false; + + @Provides + private DiscordConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(DiscordConfig.class); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (!config.enabled()) + { + return; + } + + updateGameStatus(event.getGameState(), false); + } + + @Subscribe + public void onXpChanged(ExperienceChanged event) + { + final int exp = client.getSkillExperience(event.getSkill()); + final Integer previous = skillExp.put(event.getSkill(), exp); + + if (previous == null || previous >= exp) + { + return; + } + + if (!config.enabled()) + { + return; + } + + final DiscordGameEventType discordGameEventType = DiscordGameEventType.fromSkill(event.getSkill()); + + if (discordGameEventType != null) + { + discordState.triggerEvent(discordGameEventType, config.actionDelay()); + } + } + + @Schedule( + period = 1, + unit = ChronoUnit.MINUTES + ) + public void checkForValidStatus() + { + if (!config.enabled()) + { + return; + } + + if (discordState.checkForTimeout(config.actionTimeout())) + { + updateGameStatus(client.getGameState(), true); + } + } + + @Schedule( + period = 1, + unit = ChronoUnit.SECONDS + ) + public void flushDiscordStatus() + { + if (!config.enabled()) + { + return; + } + + discordState.flushEvent(discordService); + } + + private void updateGameStatus(GameState gameState, boolean force) + { + if (gameState == GameState.LOGIN_SCREEN) + { + skillExp.clear(); + loggedIn = false; + discordState.triggerEvent(DiscordGameEventType.IN_MENU, config.actionDelay()); + } + else if (client.getGameState() == GameState.LOGGED_IN && (force || !loggedIn)) + { + loggedIn = true; + discordState.triggerEvent(DiscordGameEventType.IN_GAME, config.actionDelay()); + } + } +} 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 new file mode 100644 index 0000000000..630b2c4097 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import net.runelite.client.discord.DiscordPresence; +import net.runelite.client.discord.DiscordService; + +public class DiscordState +{ + private final List lastQueue = new ArrayList<>(); + private DiscordGameEventType lastEvent; + private Instant startOfAction; + private Instant lastAction; + private DiscordPresence lastPresence; + private boolean needsFlush; + + void flushEvent(DiscordService discordService) + { + if (lastPresence != null && needsFlush) + { + needsFlush = false; + discordService.updatePresence(lastPresence); + } + } + + 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) + { + reset = true; + } + else if (changed) + { + if (eventType.isConsiderDelay()) + { + final Duration actionDelay = Duration.ofSeconds(delay); + final Duration sinceLastAction = Duration.between(lastAction, Instant.now()); + + if (sinceLastAction.compareTo(actionDelay) >= 0) + { + reset = true; + } + } + else + { + reset = true; + } + } + + if (reset) + { + 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) + .build(); + + needsFlush = true; + } + } + + boolean checkForTimeout(final int timeout) + { + if (lastAction == null) + { + return false; + } + + final Duration actionTimeout = Duration.ofMinutes(timeout); + + if (Instant.now().compareTo(lastAction.plus(actionTimeout)) >= 0) + { + return true; + } + + return false; + } +}