diff --git a/.idea/.gitignore b/.idea/.gitignore
index e1baec5fbf2..0ce16e3fd71 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -45,3 +45,6 @@ workspace.xml
# Automatically created when the checkout directy name is different from git origin?
/.name
+
+# Discord files. It's user-specific and not-for-sharing
+discord.xml
diff --git a/facades/PC/src/main/resources/logback.xml b/facades/PC/src/main/resources/logback.xml
index 10fe1541d74..7b4252d833d 100644
--- a/facades/PC/src/main/resources/logback.xml
+++ b/facades/PC/src/main/resources/logback.xml
@@ -50,4 +50,5 @@
+
diff --git a/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCBuffer.java b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCBuffer.java
new file mode 100644
index 00000000000..9e5428cba0c
--- /dev/null
+++ b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCBuffer.java
@@ -0,0 +1,155 @@
+// Copyright 2020 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.engine.subsystem.discordrpc;
+
+import java.time.OffsetDateTime;
+
+/**
+ * A threaded-safe shared buffer used to store information for {@link DiscordRPCThread} to be processed as {@link com.jagrosh.discordipc.entities.RichPresence}
+ *
+ * It helps avoiding allocating unnecessary objects for the rich presence.
+ */
+public final class DiscordRPCBuffer {
+ private String details;
+ private String state;
+ private OffsetDateTime startTimestamp;
+ private int partySize;
+ private int partyMax;
+ private boolean changed;
+
+ public DiscordRPCBuffer() {
+ reset();
+ }
+
+ /**
+ * Resets the buffer data
+ */
+ public synchronized void reset() {
+ this.details = null;
+ this.state = null;
+ this.startTimestamp = null;
+ this.changed = true;
+ this.partySize = -1;
+ this.partyMax = -1;
+ }
+
+ /**
+ * Sets the details of the current game
+ *
+ * @param details Details about the current game (null for nothing)
+ */
+ public synchronized void setDetails(String details) {
+ this.details = details;
+ }
+
+ /**
+ * Gets the details about the current game
+ *
+ * @return Detail about the current game
+ */
+ public synchronized String getDetails() {
+ return details;
+ }
+
+ /**
+ * Sets the current party status
+ *
+ * @param state The current party status (null for nothing)
+ */
+ public synchronized void setState(String state) {
+ this.state = state;
+ this.changed = true;
+ }
+
+ /**
+ * Returns the current party status
+ *
+ * @return The current party status
+ */
+ public synchronized String getState() {
+ return state;
+ }
+
+ /**
+ * Sets the start of the game
+ *
+ * @param startTimestamp The time when that action has start or null to hide it
+ */
+ public synchronized void setStartTimestamp(OffsetDateTime startTimestamp) {
+ this.startTimestamp = startTimestamp;
+ this.changed = true;
+ }
+
+ /**
+ * Returns the start of the game
+ *
+ * @return The start of the game
+ */
+ public synchronized OffsetDateTime getStartTimestamp() {
+ return startTimestamp;
+ }
+
+ /**
+ * Sets the current party size
+ *
+ * @param partySize The current party size
+ */
+ public synchronized void setPartySize(int partySize) {
+ this.partySize = partySize;
+ this.changed = true;
+ }
+
+ /**
+ * Returns the current party size
+ *
+ * @return The party size
+ */
+ public synchronized int getPartySize() {
+ return partySize;
+ }
+
+ /**
+ * Sets the maximum number of the players in the party
+ *
+ * @param partyMax The number of the players
+ */
+ public synchronized void setPartyMax(int partyMax) {
+ this.partyMax = partyMax;
+ this.changed = true;
+ }
+
+ /**
+ * Returns the maximum number of players in the party
+ *
+ * @return The maximum number of players in the party
+ */
+ public synchronized int getPartyMax() {
+ return partyMax;
+ }
+
+ /**
+ * Check if the buffer has changed
+ *
+ * @return if the buffer has changed
+ */
+ public synchronized boolean hasChanged() {
+ return changed;
+ }
+
+ /**
+ * Check if the buffer is empty
+ *
+ * @return if the buffer is empty
+ */
+ public synchronized boolean isEmpty() {
+ return this.details == null && this.state == null && this.startTimestamp == null;
+ }
+
+ /**
+ * Resets the buffer's change state to false
+ */
+ synchronized void resetState() {
+ this.changed = false;
+ }
+}
diff --git a/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSubSystem.java b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSubSystem.java
index 0e98ec75234..d9b13e82427 100644
--- a/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSubSystem.java
+++ b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSubSystem.java
@@ -15,13 +15,6 @@
*/
package org.terasology.engine.subsystem.discordrpc;
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.LoggerContext;
-import com.jagrosh.discordipc.IPCClient;
-import com.jagrosh.discordipc.IPCListener;
-import com.jagrosh.discordipc.entities.RichPresence;
-import com.jagrosh.discordipc.entities.pipe.Pipe;
-import com.jagrosh.discordipc.entities.pipe.WindowsPipe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.config.Config;
@@ -38,281 +31,136 @@
* Subsystem that manages Discord RPC in the game client, such as status or connection.
* This subsystem can be enhanced further to improve game presentation in rich presence.
*
+ * It communicates with the thread safely using thread-safe shared buffer.
+ *
* @see EngineSubsystem
*/
-public class DiscordRPCSubSystem implements EngineSubsystem, IPCListener, Runnable, PropertyChangeListener {
-
+public final class DiscordRPCSubSystem implements EngineSubsystem, PropertyChangeListener {
private static final Logger logger = LoggerFactory.getLogger(DiscordRPCSubSystem.class);
- private static final long DISCORD_APP_CLIENT_ID = 515274721080639504L;
- private static final String DISCORD_APP_LARGE_IMAGE = "ss_6";
- private static final int RECONNECT_TRIES = 5;
private static DiscordRPCSubSystem instance;
- private IPCClient ipcClient;
- private boolean ready;
- private boolean autoReconnect;
- private Thread reconnectThread;
- private RichPresence lastRichPresence;
- private boolean reconnecting;
- private int reconnectTries = 1;
- private boolean connectedBefore;
private Config config;
- private String lastState;
- private boolean dontTryAgain;
- private boolean enabled;
+ private DiscordRPCThread thread;
public DiscordRPCSubSystem() throws IllegalStateException {
if (instance != null) {
throw new IllegalStateException("More then one instance in the DiscordRPC");
}
- lastRichPresence = null;
- ipcClient = new IPCClient(DISCORD_APP_CLIENT_ID);
- ipcClient.setListener(this);
- autoReconnect = true;
- reconnectThread = new Thread(this);
- reconnectThread.setName("DISCORD-RPC-RECONNECT");
- reconnectThread.start();
- instance = this;
- enabled = false;
- dontTryAgain = true;
- }
- public void sendRichPresence(RichPresence richPresence) {
- this.lastRichPresence = richPresence;
- if (!ready || lastRichPresence == null || !enabled) {
- return;
- }
- ipcClient.sendRichPresence(lastRichPresence);
+ instance = this;
}
@Override
- public void onReady(IPCClient client) {
- if (reconnecting) {
- logger.info("Discord RPC >> Reconnected!");
- reconnectTries = 1;
- } else {
- logger.info("Discord RPC >> Connected!");
- connectedBefore = true;
- }
- this.ipcClient = client;
- if (!ready) {
- ready = true;
- }
- if (lastRichPresence == null) {
- RichPresence.Builder builder = new RichPresence.Builder();
- builder.setLargeImage(DISCORD_APP_LARGE_IMAGE);
- lastRichPresence = builder.build();
- }
- client.sendRichPresence(lastRichPresence);
- }
+ public void initialise(GameEngine engine, Context rootContext) {
+ logger.info("Initializing...");
- @Override
- public void onDisconnect(IPCClient client, Throwable t) {
- if (ready) {
- ready = false;
+ thread = new DiscordRPCThread();
+ thread.getBuffer().setState("In Main Menu");
+
+ config = rootContext.get(Config.class);
+
+ if (config.getPlayer().isDiscordPresence()) {
+ thread.enable();
+ } else {
+ logger.info("Discord RPC is disabled! No connection is being made during initialization.");
+ thread.disable();
}
- logger.info("Discord RPC >> Disconnected!");
+ thread.start();
}
@Override
- public void run() {
- while (autoReconnect) {
- try {
- // Ignore if the Discord RPC is not enabled
- if (!enabled) {
- if (ready) {
- getInstance().ipcClient.close();
- }
- Thread.sleep(1000);
- continue;
- }
-
- // Don't retry to do any connect to the RPC till something happen to do it
- if (dontTryAgain) {
- Thread.sleep(1000);
- continue;
- }
-
- // Connect if the connect on init didn't connect successfully
- if (!connectedBefore && !ready) {
- try {
- ipcClient.connect();
- } catch (Exception ex) {
- } // Ignore the not able to connect to continue our process
- Thread.sleep(15 * 1000);
- if (!ready) {
- reconnectTries += 1;
- if (reconnectTries >= RECONNECT_TRIES) {
- dontTryAgain = true;
- }
- }
- continue;
- }
+ public synchronized void postInitialise(Context context) {
+ config = context.get(Config.class);
+ config.getPlayer().subscribe(this);
- // Ping to make sure that the RPC is alive
- if (ready) {
- Thread.sleep(5000);
- ipcClient.sendRichPresence(this.lastRichPresence);
- } else {
- reconnecting = true;
- int timeout = (reconnectTries * 2) * 1000;
- logger.info("Discord RPC >> Reconnecting... (Timeout: " + timeout + "ms)");
- try {
- ipcClient.connect();
- } catch (Exception ex) {
- if (reconnectTries <= RECONNECT_TRIES) {
- reconnectTries += 1;
- }
- if (reconnectTries >= RECONNECT_TRIES) {
- dontTryAgain = true;
- }
- Thread.sleep(timeout);
- }
- }
- } catch (InterruptedException ex) { // Ignore the interrupted exceptions
- } catch (Exception ex) {
- logger.trace(ex.getMessage(), ex.getCause());
- }
+ if (config.getPlayer().isDiscordPresence()) {
+ thread.enable();
+ } else {
+ thread.disable();
}
}
@Override
- public void initialise(GameEngine engine, Context rootContext) {
- disableLogger(IPCClient.class);
- disableLogger(WindowsPipe.class);
- disableLogger(Pipe.class);
- Config c = rootContext.get(Config.class);
- enabled = c.getPlayer().isDiscordPresence();
- if (!enabled) {
- return;
+ public void propertyChange(PropertyChangeEvent evt) {
+ if (evt.getPropertyName().equals(PlayerConfig.DISCORD_PRESENCE)) {
+ thread.setEnabled((boolean) evt.getNewValue());
}
- try {
- logger.info("Discord RPC >> Connecting...");
- ipcClient.connect();
- dontTryAgain = false;
- } catch (Exception ex) { } // Ignore due to reconnect thread
}
@Override
- public void postInitialise(Context context) {
- config = context.get(Config.class);
- config.getPlayer().subscribe(this);
- setState("In Lobby");
+ public synchronized void preShutdown() {
+ thread.disable();
+ thread.stop();
}
@Override
- public void preShutdown() {
- autoReconnect = false;
- reconnectThread.interrupt();
- if (ready) {
- ipcClient.close();
- }
+ public String getName() {
+ return "DiscordRPC";
}
/**
- * To disable the logger from some classes that throw errors and some other spam stuff into our console.
+ * Re-discovers the discord ipc in case the player started the discord client after running the game.
+ * And, the re-connecting process failed to connect.
*
+ * This should be called once by {@link DiscordRPCSystem}
*/
- private void disableLogger(Class> clazz) {
- LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
- Logger l = loggerContext.getLogger(clazz);
- ((ch.qos.logback.classic.Logger) l).setLevel(Level.OFF);
+ public static void discover() {
+ getInstance().thread.discover();
}
- @Override
- public String getName() {
- return "DiscordRPC";
+ /**
+ * Resets the current rich presence data
+ */
+ public static void reset() {
+ getInstance().thread.getBuffer().reset();
}
- public static DiscordRPCSubSystem getInstance() {
- return instance;
+ /**
+ * Sets the name of the gameplay the player is playing (e.g. Custom, Josharias Survival, etc...)
+ * @param name the name of the gameplay
+ */
+ public static void setGameplayName(String name) {
+ getInstance().thread.getBuffer().setDetails("Game: " + name);
}
+ /**
+ * Sets the current game/party status for the player (e.g. Playing Solo, Idle, etc...)
+ *
+ * @param state The current game/party status
+ */
public static void setState(String state) {
- setState(state, true);
- }
-
- public static void setState(String state, boolean timestamp) {
- if (instance == null) {
- return;
- }
- RichPresence.Builder builder = new RichPresence.Builder();
- if (state != null) {
- builder.setState(state);
- if (getInstance().lastState == null || (getInstance().lastState != null && !getInstance().lastState.equals(state))) {
- getInstance().lastState = state;
- }
- }
- if (getInstance().config != null) {
- String playerName = getInstance().config.getPlayer().getName();
- builder.setDetails("Name: " + playerName);
- }
- if (timestamp) {
- builder.setStartTimestamp(OffsetDateTime.now());
- }
-
- builder.setLargeImage(DISCORD_APP_LARGE_IMAGE);
- getInstance().sendRichPresence(builder.build());
- }
-
- public static void updateState() {
- if (getInstance() == null) {
- return;
- }
- setState(getInstance().lastState);
- }
-
- public static void tryToDiscover() {
- if (getInstance() == null) {
- return;
- }
- if (getInstance().dontTryAgain && getInstance().enabled) {
- getInstance().dontTryAgain = false;
- getInstance().reconnectTries = 0;
- }
- }
-
- public static void enable() {
- setEnabled(true);
+ getInstance().thread.getBuffer().setState(state);
}
- public static void disable() {
- setEnabled(false);
+ /**
+ * Sets an elapsed time since the player's state
+ *
+ * @param timestamp The elapsed time since player's action. `null` to disable it.
+ */
+ public static void setStartTimestamp(OffsetDateTime timestamp) {
+ getInstance().thread.getBuffer().setStartTimestamp(timestamp);
}
- public static void setEnabled(boolean enable) {
- if (getInstance() == null) {
- return;
- }
- getInstance().enabled = enable;
- if (!enable) {
- getInstance().reconnectTries = 0;
- } else {
- tryToDiscover();
- }
+ /**
+ * Sets the party information on the buffer
+ *
+ * @param size The number of the players in the party
+ * @param max The maximum number of the players in the party
+ */
+ public static void setPartyInfo(int size, int max) {
+ DiscordRPCBuffer buffer = getInstance().thread.getBuffer();
+ buffer.setPartySize(size);
+ buffer.setPartyMax(max);
}
- public static boolean isEnabled() {
- if (getInstance() == null) {
- return false;
- }
- return getInstance().enabled;
+ /**
+ * Resets the party information on the buffer
+ */
+ public static void resetPartyInfo() {
+ setPartyInfo(-1, -1);
}
- @Override
- public void propertyChange(PropertyChangeEvent evt) {
- if (evt.getPropertyName().equals(PlayerConfig.DISCORD_PRESENCE)) {
- boolean discordPresence = (boolean) evt.getNewValue();
- if (isEnabled() != discordPresence) {
- if (discordPresence) {
- enable();
- } else {
- disable();
- }
- }
- }
- if (evt.getPropertyName().equals(PlayerConfig.PLAYER_NAME)) {
- updateState();
- }
+ private static DiscordRPCSubSystem getInstance() {
+ return instance;
}
}
diff --git a/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSystem.java b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSystem.java
index 5a6dd8d1a12..ccd210c80ac 100644
--- a/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSystem.java
+++ b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCSystem.java
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
package org.terasology.engine.subsystem.discordrpc;
+import org.terasology.entitySystem.entity.EntityManager;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.systems.BaseComponentSystem;
@@ -9,11 +10,17 @@
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.game.Game;
import org.terasology.logic.afk.AfkEvent;
+import org.terasology.logic.delay.DelayManager;
+import org.terasology.logic.delay.PeriodicActionTriggeredEvent;
import org.terasology.logic.players.LocalPlayer;
+import org.terasology.logic.players.event.LocalPlayerInitializedEvent;
+import org.terasology.network.ClientComponent;
import org.terasology.network.NetworkMode;
import org.terasology.network.NetworkSystem;
import org.terasology.registry.In;
+import java.time.OffsetDateTime;
+
/**
* It's a system that runs when a single player or multi player game has been started to process some stuff throw the
* {@link DiscordRPCSubSystem}.
@@ -21,7 +28,9 @@
* @see DiscordRPCSubSystem
*/
@RegisterSystem(RegisterMode.CLIENT)
-public class DiscordRPCSystem extends BaseComponentSystem {
+public final class DiscordRPCSystem extends BaseComponentSystem {
+ private static final String UPDATE_PARTY_SIZE_ID = "discord-rpc:party-size";
+ private static final long UPDATE_PARTY_SIZE_PERIOD = 25L * 1000L;
@In
private Game game;
@@ -32,61 +41,97 @@ public class DiscordRPCSystem extends BaseComponentSystem {
@In
private NetworkSystem networkSystem;
- public String getGame() {
- NetworkMode networkMode = networkSystem.getMode();
- String mode = "Playing Online";
- if (networkMode == NetworkMode.DEDICATED_SERVER) {
- mode = "Hosting | " + game.getName();
- } else if (networkMode == NetworkMode.NONE) {
- mode = "Solo | " + game.getName();
+ @In
+ private EntityManager entityManager;
+
+ @In
+ private DelayManager delayManager;
+
+ private int onlinePlayers;
+
+ @Override
+ public void initialise() {
+ onlinePlayers = 1;
+
+ DiscordRPCSubSystem.discover();
+ }
+
+ @Override
+ public void preBegin() {
+ DiscordRPCSubSystem.setGameplayName("Custom");
+ DiscordRPCSubSystem.setState(null);
+ DiscordRPCSubSystem.setStartTimestamp(null);
+ }
+
+ @Override
+ public void postBegin() {
+ DiscordRPCSubSystem.setStartTimestamp(OffsetDateTime.now());
+ setPartyState();
+ }
+
+ @Override
+ public void shutdown() {
+ if (delayManager.hasPeriodicAction(player.getClientEntity(), UPDATE_PARTY_SIZE_ID)) {
+ delayManager.cancelPeriodicAction(player.getClientEntity(), UPDATE_PARTY_SIZE_ID);
+ }
+
+ DiscordRPCSubSystem.reset();
+ DiscordRPCSubSystem.setState("In Main Menu");
+ DiscordRPCSubSystem.setStartTimestamp(null);
+ }
+
+ @ReceiveEvent
+ public void onPlayerInitialized(LocalPlayerInitializedEvent event, EntityRef player) {
+ /* Adds the periodic action when the player is hosting or playing online to update party size */
+ if (networkSystem.getMode() != NetworkMode.NONE) {
+ delayManager.addPeriodicAction(player, UPDATE_PARTY_SIZE_ID, 0, UPDATE_PARTY_SIZE_PERIOD);
}
- return mode;
}
@ReceiveEvent
public void onAfk(AfkEvent event, EntityRef entityRef) {
- if (requireConnection() && player.getClientEntity().equals(entityRef)) {
+ if (isServer() && player.getClientEntity().equals(entityRef)) {
return;
}
+
if (event.isAfk()) {
- disableDiscord();
+ DiscordRPCSubSystem.setState("Idle");
+ DiscordRPCSubSystem.resetPartyInfo();
} else {
- enableDiscord();
+ setPartyState();
}
}
- private boolean requireConnection() {
- NetworkMode networkMode = networkSystem.getMode();
- return networkMode != NetworkMode.CLIENT && networkMode != NetworkMode.DEDICATED_SERVER;
- }
-
- private void enableDiscord() {
- DiscordRPCSubSystem.tryToDiscover();
- DiscordRPCSubSystem.setState("Idle", true);
+ @ReceiveEvent
+ public void onPeriodicTrigger(PeriodicActionTriggeredEvent event, EntityRef entity) {
+ if (event.getActionId().equals(UPDATE_PARTY_SIZE_ID)) {
+ onlinePlayers = 0;
+ entityManager.getEntitiesWith(ClientComponent.class).forEach(ignored -> onlinePlayers++);
+ DiscordRPCSubSystem.setPartyInfo(onlinePlayers, 99);
+ }
}
- private void disableDiscord() {
- DiscordRPCSubSystem.tryToDiscover();
- DiscordRPCSubSystem.setState(getGame(), true);
- }
+ private void setPartyState() {
+ final NetworkMode networkMode = networkSystem.getMode();
- @Override
- public void initialise() {
- DiscordRPCSubSystem.tryToDiscover();
- }
+ String mode = "Playing Online";
+ if (networkMode == NetworkMode.DEDICATED_SERVER) {
+ mode = "Hosting";
+ } else if (networkMode == NetworkMode.NONE) {
+ mode = "Playing Solo";
+ DiscordRPCSubSystem.setPartyInfo(1, 1);
+ }
- @Override
- public void preBegin() {
- DiscordRPCSubSystem.setState(getGame(), false);
- }
+ DiscordRPCSubSystem.setState(mode);
+ if (networkMode != NetworkMode.NONE) {
- @Override
- public void postBegin() {
- DiscordRPCSubSystem.setState(getGame(), true);
+ /* The player is playing online or hosting a game */
+ DiscordRPCSubSystem.setPartyInfo(onlinePlayers, 99);
+ }
}
- @Override
- public void shutdown() {
- DiscordRPCSubSystem.setState("In Lobby");
+ private boolean isServer() {
+ NetworkMode networkMode = networkSystem.getMode();
+ return networkMode != NetworkMode.CLIENT && networkMode != NetworkMode.DEDICATED_SERVER;
}
}
diff --git a/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCThread.java b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCThread.java
new file mode 100644
index 00000000000..85f651f411c
--- /dev/null
+++ b/subsystems/DiscordRPC/src/main/java/org/terasology/engine/subsystem/discordrpc/DiscordRPCThread.java
@@ -0,0 +1,284 @@
+// Copyright 2020 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.engine.subsystem.discordrpc;
+
+import com.jagrosh.discordipc.IPCClient;
+import com.jagrosh.discordipc.IPCListener;
+import com.jagrosh.discordipc.entities.RichPresence;
+import com.jagrosh.discordipc.exceptions.NoDiscordClientException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class DiscordRPCThread implements IPCListener, Runnable {
+ private static final Logger logger = LoggerFactory.getLogger(DiscordRPCThread.class);
+ private static final long DISCORD_APP_CLIENT_ID = 515274721080639504L;
+ private static final String DISCORD_APP_DEFAULT_IMAGE = "ss_6";
+ private static final int MAX_RECONNECT_TRIES = 5;
+
+ private final Thread thread;
+ private final IPCClient ipcClient;
+ private final DiscordRPCBuffer buffer;
+ private RichPresence lastRichPresence;
+
+ private int tries;
+
+ private boolean enabled;
+ private boolean waiting;
+ private boolean connectedBefore;
+ private boolean connected;
+ private boolean autoReconnect;
+
+ public DiscordRPCThread() {
+ thread = new Thread(this);
+ thread.setName("DISCORD-RPC-THREAD");
+
+ ipcClient = new IPCClient(DISCORD_APP_CLIENT_ID);
+ ipcClient.setListener(this);
+
+ buffer = new DiscordRPCBuffer();
+
+ lastRichPresence = null;
+
+ tries = 0;
+
+ enabled = false;
+ waiting = false;
+ connectedBefore = false;
+ connected = false;
+ autoReconnect = false;
+ }
+
+ public void start() {
+ thread.start();
+ }
+
+ public synchronized void stop() {
+ synchronized (thread) {
+ thread.interrupt();
+ }
+ }
+
+ public synchronized void discover() {
+ if (enabled && connected) {
+ return;
+ }
+
+ reset(true);
+
+ connectedBefore = true;
+ }
+
+ public synchronized void enable() {
+ if (enabled) {
+ return;
+ }
+
+ enabled = true;
+ autoReconnect = true;
+
+ if (waiting && thread.isAlive()) {
+ synchronized (thread) {
+ thread.notify();
+ }
+ }
+ }
+
+ public synchronized void disable() {
+ disable(false);
+ }
+
+ public synchronized void disable(boolean keepConnectionAlive) {
+ if (!enabled) {
+ return;
+ }
+
+ enabled = false;
+
+ reset(!keepConnectionAlive);
+ if (!keepConnectionAlive) {
+ autoReconnect = false;
+ }
+
+ if (waiting && thread.isAlive()) {
+ synchronized (thread) {
+ thread.notify();
+ }
+ }
+ }
+
+ @Override
+ public void onReady(IPCClient ignored) {
+ if (connectedBefore) {
+ logger.info("Re-connected to Discord RPC!");
+ } else {
+ logger.info("Connected to Discord RPC!");
+ }
+
+ connectedBefore = true;
+ connected = true;
+ }
+
+ @Override
+ public void onDisconnect(IPCClient client, Throwable t) {
+ connected = false;
+ logger.info("Discord RPC lost connection: Disconnected!");
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ logger.info("Waiting for auto-connecting...");
+ /* If auto-connect is disabled the thread won't get notified*/
+ while (!autoReconnect) {
+ try {
+ synchronized (thread) {
+ waiting = true;
+ thread.wait();
+ waiting = false;
+ }
+ } catch (InterruptedException ignored) {
+ return; // End when the thread is being interrupted
+ }
+ }
+
+ logger.info("Waiting for enabling...");
+ /* Check if the subsystem is enabled */
+ while (!enabled) {
+ try {
+ synchronized (thread) {
+ waiting = true;
+ thread.wait();
+ waiting = false;
+ }
+ } catch (InterruptedException ignored) {
+ return; // End when the thread is being interrupted
+ }
+ }
+
+ logger.info("Waiting for connection...");
+ /* Auto-Connect to the IPC with reconnect process */
+ while (!connected) {
+ synchronized (ipcClient) {
+ try {
+ if (!connectedBefore) {
+ logger.info("Connecting to Discord RPC...");
+ } else {
+ logger.info("Re-connecting to Discord RPC...");
+ }
+
+ ipcClient.connect();
+
+ tries = 0;
+ autoReconnect = true;
+ } catch (NoDiscordClientException ignored) {
+ // TODO implement reconnect process
+ if (tries >= MAX_RECONNECT_TRIES) {
+ autoReconnect = false;
+ tries = 0;
+ break;
+ } else {
+ tries++;
+ try {
+ Thread.sleep(2000L * tries);
+ } catch (InterruptedException ignored2) {
+ ipcClient.close();
+ return; // End when the thread is being interrupted
+ }
+
+ // Retry to connect again
+ }
+ }
+ }
+ }
+
+ /* Go to the beginning to trigger auto reconnect loop */
+ if (!autoReconnect) {
+ continue;
+ }
+
+ logger.info("Updating the rich presence and keep the connection alive...");
+ /* Update the rich presence and keeping the connection alive */
+ while (connected) {
+ synchronized (this) {
+ /* Ping the ipc connection with an rich presnece to keep the connection alive */
+ if (enabled) {
+ /* Allocate a new rich presence when the buffer has changed */
+ if (buffer.hasChanged() && buffer.isEmpty()) {
+ lastRichPresence = null;
+ buffer.resetState();
+ } else if (buffer.hasChanged()) {
+ lastRichPresence = build();
+ buffer.resetState();
+ }
+
+ ipcClient.sendRichPresence(lastRichPresence);
+ } else {
+ ipcClient.sendRichPresence(null);
+ }
+ }
+
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException ignored) {
+ synchronized (ipcClient) {
+ ipcClient.close();
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ public synchronized void setEnabled(boolean enabled) {
+ if (this.enabled != enabled) {
+ if (enabled) {
+ enable();
+ } else {
+ disable(true);
+ }
+ }
+
+ this.enabled = enabled;
+ }
+
+ public synchronized boolean isEnabled() {
+ return enabled;
+ }
+
+ public synchronized DiscordRPCBuffer getBuffer() {
+ return buffer;
+ }
+
+ private RichPresence build() {
+ RichPresence.Builder builder = new RichPresence.Builder()
+ .setLargeImage(DISCORD_APP_DEFAULT_IMAGE);
+
+ if (buffer.getDetails() != null)
+ builder.setDetails(buffer.getDetails());
+
+ if (buffer.getState() != null)
+ builder.setState(buffer.getState());
+
+ if (buffer.getStartTimestamp() != null)
+ builder.setStartTimestamp(buffer.getStartTimestamp());
+
+ int partySize = buffer.getPartySize();
+ int partyMax = buffer.getPartyMax();
+ if (partySize > 0 && partyMax > 0) {
+ builder.setParty("null", partySize, partyMax);
+ }
+
+ return builder.build();
+ }
+
+ private void reset(boolean resetConnection) {
+ tries = 0;
+
+ autoReconnect = true;
+ if (resetConnection) {
+ connectedBefore = false;
+ connected = false;
+ }
+ }
+}