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; + } + } +}