From 15ad949123dab0a04bde03d7185195cdbef69478 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 3 Feb 2018 20:08:56 +0100 Subject: [PATCH 1/2] Add Discord RPC service Add injectable Discord RPC service that will broadcast Discord events through event bus and have API for setting Discord Rich Presence status. Signed-off-by: Tomas Slusny --- runelite-client/pom.xml | 5 + .../java/net/runelite/client/RuneLite.java | 13 +- .../net/runelite/client/RuneLiteModule.java | 4 +- .../runelite/client/RuneLiteProperties.java | 6 + .../client/discord/DiscordPresence.java | 147 +++++++++++++++ .../client/discord/DiscordReplyType.java | 46 +++++ .../client/discord/DiscordService.java | 178 ++++++++++++++++++ .../discord/events/DiscordDisconnected.java | 44 +++++ .../client/discord/events/DiscordErrored.java | 44 +++++ .../discord/events/DiscordJoinGame.java | 39 ++++ .../discord/events/DiscordJoinRequest.java | 54 ++++++ .../client/discord/events/DiscordReady.java | 35 ++++ .../discord/events/DiscordSpectateGame.java | 39 ++++ .../net/runelite/client/runelite.properties | 1 + 14 files changed, 647 insertions(+), 8 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/DiscordPresence.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/DiscordReplyType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/events/DiscordDisconnected.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/events/DiscordErrored.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinGame.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinRequest.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/events/DiscordReady.java create mode 100644 runelite-client/src/main/java/net/runelite/client/discord/events/DiscordSpectateGame.java diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index 155e995408..e99f58a31b 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -120,6 +120,11 @@ http-api ${project.version} + + net.runelite + discord + 1.0 + junit diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index a7f10fa357..342b70e8a1 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -32,18 +32,18 @@ import com.google.inject.Injector; import java.applet.Applet; import java.io.File; import java.util.Optional; -import java.util.concurrent.ScheduledExecutorService; import javax.inject.Singleton; import javax.swing.SwingUtilities; import joptsimple.OptionParser; import joptsimple.OptionSet; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; -import net.runelite.client.events.ClientUILoaded; import net.runelite.client.account.SessionManager; import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; +import net.runelite.client.discord.DiscordService; +import net.runelite.client.events.ClientUILoaded; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ClientUI; @@ -78,9 +78,6 @@ public class RuneLite @Inject private ChatMessageManager chatMessageManager; - @Inject - private ScheduledExecutorService executor; - @Inject private OverlayRenderer overlayRenderer; @@ -90,6 +87,9 @@ public class RuneLite @Inject private RuneLiteConfig runeliteConfig; + @Inject + private DiscordService discordService; + Client client; ClientUI gui; Notifier notifier; @@ -133,6 +133,9 @@ public class RuneLite // Load swing UI SwingUtilities.invokeAndWait(() -> setGui(ClientUI.create(properties, client))); + // Initialize Discord service + discordService.init(); + // Load default configuration configManager.load(); diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java index abd211e8d9..a901557d8a 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java @@ -36,14 +36,13 @@ import net.runelite.api.Client; import net.runelite.client.account.SessionManager; import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.game.ItemManager; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.task.Scheduler; import net.runelite.client.ui.ClientUI; -import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import net.runelite.client.util.QueryRunner; -import net.runelite.client.config.RuneLiteConfig; @Slf4j public class RuneLiteModule extends AbstractModule @@ -56,7 +55,6 @@ public class RuneLiteModule extends AbstractModule bind(MenuManager.class); bind(ChatMessageManager.class); bind(ItemManager.class); - bind(InfoBoxManager.class); bind(Scheduler.class); bind(PluginManager.class); bind(RuneLiteProperties.class); diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java index f01c70a946..41677620d6 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -39,6 +39,7 @@ public class RuneLiteProperties private static final String RUNELITE_TITLE = "runelite.title"; private static final String RUNELITE_VERSION = "runelite.version"; private static final String RUNESCAPE_VERSION = "runescape.version"; + private static final String DISCORD_APP_ID = "runelite.discord.appid"; private final Properties properties = new Properties(); @@ -70,4 +71,9 @@ public class RuneLiteProperties { return properties.getProperty(RUNESCAPE_VERSION); } + + public String getDiscordAppId() + { + return properties.getProperty(DISCORD_APP_ID); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/discord/DiscordPresence.java b/runelite-client/src/main/java/net/runelite/client/discord/DiscordPresence.java new file mode 100644 index 0000000000..15b6989078 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/DiscordPresence.java @@ -0,0 +1,147 @@ +/* + * 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.discord; + +import java.time.Instant; +import lombok.Builder; +import lombok.Value; + +/** + * Represents Discord Rich Presence RPC data + */ +@Builder +@Value +public class DiscordPresence +{ + /** + * The user's current party status. + * Example: "Looking to Play", "Playing Solo", "In a Group" + * + * Maximum: 128 characters + */ + private String state; + + /** + * What the player is currently doing. + * Example: "Competitive - Captain's Mode", "In Queue", "Unranked PvP" + * + * Maximum: 128 characters + */ + private String details; + + /** + * Unix timestamp (seconds) for the start of the game. + */ + private Instant startTimestamp; + + /** + * Unix timestamp (seconds) for the start of the game. + */ + private Instant endTimestamp; + + /** + * Name of the uploaded image for the large profile artwork. + * Example: "default" + * + * Maximum: 32 characters + */ + private String largeImageKey; + + /** + * Tooltip for the largeImageKey. + * Example: "Blade's Edge Arena", "Numbani", "Danger Zone" + * + * Maximum: 128 characters + */ + private String largeImageText; + + /** + * Name of the uploaded image for the small profile artwork. + * Example: "rogue" + * + * Maximum: 32 characters + */ + private String smallImageKey; + + /** + * Tooltip for the smallImageKey. + * Example: "Rogue - Level 100" + * + * Maximum: 128 characters + */ + private String smallImageText; + + /** + * ID of the player's party, lobby, or group. + * Example: "ae488379-351d-4a4f-ad32-2b9b01c91657" + * + * Maximum: 128 characters + */ + private String partyId; + + /** + * Current size of the player's party, lobby, or group. + * Example: 1 + */ + private int partySize; + + /** + * Maximum size of the player's party, lobby, or group. + * Example: 5 + */ + private int partyMax; + + /** + * Unique hashed string for Spectate and Join. + * Required to enable match interactive buttons in the user's presence. + * Example: "MmhuZToxMjMxMjM6cWl3amR3MWlqZA==" + * + * Maximum: 128 characters + */ + private String matchSecret; + + /** + * Unique hashed string for Spectate button. + * This will enable the "Spectate" button on the user's presence if whitelisted. + * Example: "MTIzNDV8MTIzNDV8MTMyNDU0" + * + * Maximum: 128 characters + */ + private String joinSecret; + + /** + * Unique hashed string for chat invitations and Ask to Join. + * This will enable the "Ask to Join" button on the user's presence if whitelisted. + * Example: "MTI4NzM0OjFpMmhuZToxMjMxMjM=" + * + * Maximum: 128 characters + */ + private String spectateSecret; + + /** + * Marks the matchSecret as a game session with a specific beginning and end. + */ + private boolean instance; +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/DiscordReplyType.java b/runelite-client/src/main/java/net/runelite/client/discord/DiscordReplyType.java new file mode 100644 index 0000000000..50f85154af --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/DiscordReplyType.java @@ -0,0 +1,46 @@ +/* + * 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.discord; + +/** + * Discord reply type for request + */ +public enum DiscordReplyType +{ + /** + * Used to decline a request + */ + NO, + + /** + * Used to accept a request + */ + YES, + + /** + * Currently unused response, treated like NO. + */ + IGNORE +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java b/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java new file mode 100644 index 0000000000..3c7a7ab684 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java @@ -0,0 +1,178 @@ +/* + * 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.discord; + +import com.google.common.base.Strings; +import com.google.common.eventbus.EventBus; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.discord.events.DiscordDisconnected; +import net.runelite.client.discord.events.DiscordErrored; +import net.runelite.client.discord.events.DiscordJoinGame; +import net.runelite.client.discord.events.DiscordJoinRequest; +import net.runelite.client.discord.events.DiscordReady; +import net.runelite.client.discord.events.DiscordSpectateGame; +import net.runelite.discord.DiscordEventHandlers; +import net.runelite.discord.DiscordRPC; +import net.runelite.discord.DiscordRichPresence; + +@Singleton +@Slf4j +public class DiscordService implements AutoCloseable +{ + @Inject + private EventBus eventBus; + + @Inject + private RuneLiteProperties runeLiteProperties; + + @Inject + private ScheduledExecutorService executorService; + + private final DiscordEventHandlers discordEventHandlers = new DiscordEventHandlers(); + private final DiscordRPC discordRPC = DiscordRPC.INSTANCE; + + /** + * Initializes the Discord service, sets up the event handlers and starts worker thread that will poll discord + * events every 2 seconds. + * Before closing the application it is recommended to call {@link #close()} + */ + public void init() + { + log.info("Initializing Discord RPC service."); + discordEventHandlers.ready = this::ready; + discordEventHandlers.disconnected = this::disconnected; + discordEventHandlers.errored = this::errored; + discordEventHandlers.joinGame = this::joinGame; + discordEventHandlers.spectateGame = this::spectateGame; + discordEventHandlers.joinRequest = this::joinRequest; + discordRPC.Discord_Initialize(runeLiteProperties.getDiscordAppId(), discordEventHandlers, true, null); + executorService.scheduleAtFixedRate(discordRPC::Discord_RunCallbacks, 0, 2, TimeUnit.SECONDS); + } + + /** + * Shuts the RPC connection down. + * If not currently connected, this does nothing. + */ + @Override + public void close() + { + discordRPC.Discord_Shutdown(); + } + + /** + * Updates the currently set presence of the logged in user. + *
Note that the client only updates its presence every 15 seconds + * and queues all additional presence updates. + * + * @param discordPresence The new presence to use + */ + public void updatePresence(DiscordPresence discordPresence) + { + final DiscordRichPresence discordRichPresence = new DiscordRichPresence(); + discordRichPresence.state = discordPresence.getState(); + discordRichPresence.details = discordPresence.getDetails(); + discordRichPresence.startTimestamp = discordPresence.getStartTimestamp() != null + ? discordPresence.getStartTimestamp().getEpochSecond() + : 0; + discordRichPresence.endTimestamp = discordPresence.getEndTimestamp() != null + ? discordPresence.getEndTimestamp().getEpochSecond() + : 0; + discordRichPresence.largeImageKey = Strings.isNullOrEmpty(discordPresence.getLargeImageKey()) + ? "default" + : discordPresence.getLargeImageKey(); + discordRichPresence.largeImageText = discordPresence.getLargeImageText(); + discordRichPresence.smallImageKey = Strings.isNullOrEmpty(discordPresence.getSmallImageKey()) + ? "default" + : discordPresence.getLargeImageKey(); + discordRichPresence.smallImageText = discordPresence.getSmallImageText(); + discordRichPresence.partyId = discordPresence.getPartyId(); + discordRichPresence.partySize = discordPresence.getPartySize(); + discordRichPresence.partyMax = discordPresence.getPartyMax(); + discordRichPresence.matchSecret = discordPresence.getMatchSecret(); + discordRichPresence.joinSecret = discordPresence.getJoinSecret(); + discordRichPresence.spectateSecret = discordPresence.getSpectateSecret(); + discordRichPresence.instance = (byte) (discordPresence.isInstance() ? 1 : 0); + discordRPC.Discord_UpdatePresence(discordRichPresence); + } + + /** + * Clears the currently set presence. + */ + public void clearPresence() + { + discordRPC.Discord_ClearPresence(); + } + + /** + * Responds to the given user with the specified reply type. + * + * @param userId The id of the user to respond to + * @param reply The reply type + */ + public void respondToRequest(String userId, DiscordReplyType reply) + { + discordRPC.Discord_Respond(userId, reply.ordinal()); + } + + private void ready() + { + log.info("Discord RPC service is ready."); + eventBus.post(new DiscordReady()); + } + + private void disconnected(int errorCode, String message) + { + eventBus.post(new DiscordDisconnected(errorCode, message)); + } + + private void errored(int errorCode, String message) + { + eventBus.post(new DiscordErrored(errorCode, message)); + } + + private void joinGame(String joinSecret) + { + eventBus.post(new DiscordJoinGame(joinSecret)); + } + + private void spectateGame(String spectateSecret) + { + eventBus.post(new DiscordSpectateGame(spectateSecret)); + } + + private void joinRequest(net.runelite.discord.DiscordJoinRequest joinRequest) + { + eventBus.post(new DiscordJoinRequest( + joinRequest.userId, + joinRequest.username, + joinRequest.discriminator, + joinRequest.avatar)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordDisconnected.java b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordDisconnected.java new file mode 100644 index 0000000000..cc4d110a70 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordDisconnected.java @@ -0,0 +1,44 @@ +/* + * 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.discord.events; + +import lombok.Value; + +/** + * Called when the RPC connection has been severed + */ +@Value +public class DiscordDisconnected +{ + /** + * Discord error code + */ + private int errorCode; + + /** + * Error message + */ + private String message; +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordErrored.java b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordErrored.java new file mode 100644 index 0000000000..3c50464dbd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordErrored.java @@ -0,0 +1,44 @@ +/* + * 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.discord.events; + +import lombok.Value; + +/** + * Called when an internal error is caught within the SDK + */ +@Value +public class DiscordErrored +{ + /** + * Discord error code. + */ + private int errorCode; + + /** + * Error message + */ + private String message; +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinGame.java b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinGame.java new file mode 100644 index 0000000000..a9d9421f8a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinGame.java @@ -0,0 +1,39 @@ +/* + * 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.discord.events; + +import lombok.Value; + +/** + * Called when the logged in user joined a game + */ +@Value +public class DiscordJoinGame +{ + /** + * Obfuscated data of your choosing used as join secret + */ + private String joinSecret; +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinRequest.java b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinRequest.java new file mode 100644 index 0000000000..1b71368ac7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordJoinRequest.java @@ -0,0 +1,54 @@ +/* + * 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.discord.events; + +import lombok.Value; + +/** + * Called when another discord user wants to join the game of the logged in user + */ +@Value +public class DiscordJoinRequest +{ + /** + * The userId for the user that requests to join + */ + private String userId; + + /** + * The username of the user that requests to join + */ + private String username; + + /** + * The discriminator of the user that requests to join + */ + private String discriminator; + + /** + * The avatar of the user that requests to join + */ + private String avatar; +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordReady.java b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordReady.java new file mode 100644 index 0000000000..133edb1d7f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordReady.java @@ -0,0 +1,35 @@ +/* + * 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.discord.events; + +import lombok.Value; + +/** + * Called when the RPC connection has been established + */ +@Value +public class DiscordReady +{ +} diff --git a/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordSpectateGame.java b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordSpectateGame.java new file mode 100644 index 0000000000..bcff4a74bd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/discord/events/DiscordSpectateGame.java @@ -0,0 +1,39 @@ +/* + * 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.discord.events; + +import lombok.Value; + +/** + * Called when the logged in user joined to spectate a game + */ +@Value +public class DiscordSpectateGame +{ + /** + * Obfuscated data of your choosing used as spectate secret + */ + private String spectateSecret; +} diff --git a/runelite-client/src/main/resources/net/runelite/client/runelite.properties b/runelite-client/src/main/resources/net/runelite/client/runelite.properties index 684a13c581..a904c2d233 100644 --- a/runelite-client/src/main/resources/net/runelite/client/runelite.properties +++ b/runelite-client/src/main/resources/net/runelite/client/runelite.properties @@ -1,3 +1,4 @@ runelite.title=RuneLite runelite.version=${project.version} runescape.version=${rs.version} +runelite.discord.appid=409416265891971072 From ea06adf9f4232eeab8bc67b9b26dd5cabd1fc8ed Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 3 Feb 2018 23:41:28 +0100 Subject: [PATCH 2/2] Add Discord plugin Add Discord plugin that updates the Discord rich presence currently only for trained skills. Signed-off-by: Tomas Slusny --- .../client/plugins/discord/DiscordConfig.java | 67 +++++++ .../plugins/discord/DiscordGameEventType.java | 168 ++++++++++++++++++ .../client/plugins/discord/DiscordPlugin.java | 148 +++++++++++++++ .../client/plugins/discord/DiscordState.java | 126 +++++++++++++ 4 files changed, 509 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordGameEventType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java 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; + } +}