diff --git a/.gitignore b/.gitignore index 4bb5ed9..73ea0d4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ eclipse run artifacts +src/test/** \ No newline at end of file diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordEventHandlers.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordEventHandlers.java new file mode 100644 index 0000000..3f1ffcb --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordEventHandlers.java @@ -0,0 +1,90 @@ +package com.hypherionmc.craterlib.core.rpcsdk; + +import com.hypherionmc.craterlib.core.rpcsdk.callbacks.*; +import com.sun.jna.Structure; + +import java.util.Arrays; +import java.util.List; + +/** + * @author HypherionSA + * Class containing references to all available discord event handles. + * Registering a handler is optional, and non-assigned handlers will be ignored + */ +public class DiscordEventHandlers extends Structure { + + // Callback for when the RPC was initialized successfully + public ReadyCallback ready; + + // Callback for when the Discord connection was ended + public DisconnectedCallback disconnected; + + // Callback for when a Discord Error occurs + public ErroredCallback errored; + + // Callback for when a player joins the game + public JoinGameCallback joinGame; + + // Callback for when a player spectates the game + public SpectateGameCallback spectateGame; + + // Callback for when a players request to join your game + public JoinRequestCallback joinRequest; + + /** + * DO NOT TOUCH THIS... EVER! + */ + @Override + protected List getFieldOrder() { + return Arrays.asList( + "ready", + "disconnected", + "errored", + "joinGame", + "spectateGame", + "joinRequest" + ); + } + + public static class Builder { + private final DiscordEventHandlers handlers; + + public Builder() { + this.handlers = new DiscordEventHandlers(); + } + + public Builder ready(ReadyCallback readyCallback) { + handlers.ready = readyCallback; + return this; + } + + public Builder disconnected(DisconnectedCallback disconnectedCallback) { + handlers.disconnected = disconnectedCallback; + return this; + } + + public Builder errored(ErroredCallback erroredCallback) { + handlers.errored = erroredCallback; + return this; + } + + public Builder joinGame(JoinGameCallback joinGameCallback) { + handlers.joinGame = joinGameCallback; + return this; + } + + public Builder spectateGame(SpectateGameCallback spectateGameCallback) { + handlers.spectateGame = spectateGameCallback; + return this; + } + + public Builder joinRequest(JoinRequestCallback joinRequestCallback) { + handlers.joinRequest = joinRequestCallback; + return this; + } + + public DiscordEventHandlers build() { + return handlers; + } + } +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordRPC.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordRPC.java new file mode 100644 index 0000000..f2b9e32 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordRPC.java @@ -0,0 +1,93 @@ +package com.hypherionmc.craterlib.core.rpcsdk; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author HypherionSA + * Java Wrapper of the Discord-RPC Library + */ +public interface DiscordRPC extends Library { + + DiscordRPC INSTANCE = Native.load("discord-rpc", DiscordRPC.class); + + /** + * Open a New RPC Connection + * @param applicationId The ID of the Application the RPC is tied to + * @param handlers Optional Event Callback Handlers + * @param autoRegister Auto Register the running game + * @param steamId Steam ID of the game + */ + void Discord_Initialize(@NotNull String applicationId, @Nullable DiscordEventHandlers handlers, boolean autoRegister, @Nullable String steamId); + + /** + * Shutdown the RPC instance and disconnect from discord + */ + void Discord_Shutdown(); + + /** + * Need to be called manually at least every 2 seconds, to allow RPC updates + * and callback handlers to fire + */ + void Discord_RunCallbacks(); + + /** + * Not sure about this. Believe it needs to be called manually in some circumstances + */ + void Discord_UpdateConnection(); + + /** + * Update the Rich Presence + * @param struct Constructed {@link DiscordRichPresence} + */ + void Discord_UpdatePresence(@Nullable DiscordRichPresence struct); + + /** + * Clear the current Rich Presence + */ + void Discord_ClearPresence(); + + /** + * Respond to Join/Spectate callback + * @param userid The Discord User ID of the user that initiated the request + * @param reply Reply to the request. See {@link DiscordReply} + */ + void Discord_Respond(@NotNull String userid, int reply); + + /** + * Replace the already registered {@link DiscordEventHandlers} + * @param handlers The new handlers to apply + */ + void Discord_UpdateHandlers(@Nullable DiscordEventHandlers handlers); + + /** + * Register the executable of the application/game + * Only applicable when autoRegister is set to false + * @param applicationId The Application ID + * @param command The Launch command of the game + * + * NB: THIS DOES NOT WORK WITH MINECRAFT + */ + void Discord_Register(String applicationId, String command); + + /** + * Register the Steam executable of the application/game + * @param applicationId The Application ID + * @param steamId The Steam ID of the application/game + */ + void Discord_RegisterSteamGame(String applicationId, String steamId); + + public enum DiscordReply { + NO(0), + YES(1), + IGNORE(2); + + public final int reply; + + DiscordReply(int reply) { + this.reply = reply; + } + } +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordRichPresence.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordRichPresence.java new file mode 100644 index 0000000..71fdce2 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordRichPresence.java @@ -0,0 +1,244 @@ +package com.hypherionmc.craterlib.core.rpcsdk; + +import com.hypherionmc.craterlib.core.rpcsdk.helpers.RPCButton; +import com.sun.jna.Structure; +import org.jetbrains.annotations.NotNull; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @author HypherionSA + * Class reprenting a Discord RPC activity + */ +public class DiscordRichPresence extends Structure { + + // First line of text on the RPC + public String state; + + // Second line of text on the RPC + public String details; + + // Time the activity started in UNIX-Timestamp format + public long startTimestamp; + + // Time the activity will end in UNIX-Timestamp format + public long endTimestamp; + + // URL or Asset key of the Large Image + public String largeImageKey; + + // Hover text to display when hovering the Large Image + public String largeImageText; + + // URL or Asset key of the Small Image + public String smallImageKey; + + // Hover text to display when hovering the Small Image + public String smallImageText; + + // Id of the player's party, lobby, or group. + public String partyId; + + // Current size of the player's party, lobby, or group. + public int partySize; + + // Maximum size of the player's party, lobby, or group. + public int partyMax; + + // Unused + public String partyPrivacy; + + // Unused. + public String matchSecret; + + // Unique hashed string for chat invitations and Ask to Join. + public String joinSecret; + + // Unique hashed string for Spectate button. + public String spectateSecret; + + // Label of the First RPC Button + public String button_label_1; + + // URL of the First RPC Button + public String button_url_1; + + // Label of the Second RPC Button + public String button_label_2; + + // URL of the Second RPC Button + public String button_url_2; + + // Unused + public int instance; + + /** + * DO NOT TOUCH THIS... EVER! + */ + @Override + protected List getFieldOrder() { + return Arrays.asList( + "state", + "details", + "startTimestamp", + "endTimestamp", + "largeImageKey", + "largeImageText", + "smallImageKey", + "smallImageText", + "partyId", + "partySize", + "partyMax", + "partyPrivacy", + "matchSecret", + "joinSecret", + "spectateSecret", + "button_label_1", + "button_url_1", + "button_label_2", + "button_url_2", + "instance" + ); + } + + public static class Builder { + private final DiscordRichPresence rpc; + + public Builder(String state) { + rpc = new DiscordRichPresence(); + + if (state != null && !state.isEmpty()) { + rpc.state = state.substring(0, Math.min(state.length(), 128)); + } + } + + public Builder setDetails(String details) { + if (details != null && !details.isEmpty()) { + rpc.details = details.substring(0, Math.min(details.length(), 128)); + } + return this; + } + + public Builder setStartTimestamp(long timestamp) { + rpc.startTimestamp = timestamp; + return this; + } + + public Builder setStartTimestamp(OffsetDateTime timestamp) { + rpc.startTimestamp = timestamp.toEpochSecond(); + return this; + } + + public Builder setEndTimestamp(long timestamp) { + rpc.endTimestamp = timestamp; + return this; + } + + public Builder setEndTimestamp(OffsetDateTime timestamp) { + rpc.endTimestamp = timestamp.toEpochSecond(); + return this; + } + + public Builder setLargeImage(String key) { + return this.setLargeImage(key, ""); + } + + public Builder setLargeImage(@NotNull String key, String text) { + // Null check used for users blatantly ignoring the NotNull marker + if ((text != null && !text.isEmpty()) && key != null) { + throw new IllegalArgumentException("Image key cannot be null when assigning a hover text"); + } + + rpc.largeImageKey = key; + rpc.largeImageText = text; + return this; + } + + public Builder setSmallImage(String key) { + return this.setSmallImage(key, ""); + } + + public Builder setSmallImage(@NotNull String key, String text) { + // Null check used for users blatantly ignoring the NotNull marker + if ((text != null && !text.isEmpty()) && key != null) { + throw new IllegalArgumentException("Image key cannot be null when assigning a hover text"); + } + + rpc.smallImageKey = key; + rpc.smallImageText = text; + return this; + } + + public Builder setParty(String party, int size, int max) { + // Buttons are present, ignore + if ((rpc.button_label_1 != null && rpc.button_label_1.isEmpty()) || (rpc.button_label_2 != null && rpc.button_label_2.isEmpty())) + return this; + + rpc.partyId = party; + rpc.partySize = size; + rpc.partyMax = max; + return this; + } + + public Builder setSecrets(String match, String join, String spectate) { + // Buttons are present, ignore + if ((rpc.button_label_1 != null && rpc.button_label_1.isEmpty()) || (rpc.button_label_2 != null && rpc.button_label_2.isEmpty())) + return this; + + rpc.matchSecret = match; + rpc.joinSecret = join; + rpc.spectateSecret = spectate; + return this; + } + + public Builder setSecrets(String join, String spectate) { + // Buttons are present, ignore + if ((rpc.button_label_1 != null && rpc.button_label_1.isEmpty()) || (rpc.button_label_2 != null && rpc.button_label_2.isEmpty())) + return this; + + rpc.joinSecret = join; + rpc.spectateSecret = spectate; + return this; + } + + public Builder setInstance(boolean i) { + // Buttons are present, ignore + if ((rpc.button_label_1 != null && rpc.button_label_1.isEmpty()) || (rpc.button_label_2 != null && rpc.button_label_2.isEmpty())) + return this; + + rpc.instance = i ? 1 : 0; + return this; + } + + public Builder setButtons(RPCButton button) { + return this.setButtons(Collections.singletonList(button)); + } + + public Builder setButtons(RPCButton button1, RPCButton button2) { + return this.setButtons(Arrays.asList(button1, button2)); + } + + public Builder setButtons(List rpcButtons) { + // Limit to 2 Buttons. Discord Limitation + if (rpcButtons != null && !rpcButtons.isEmpty()) { + int length = Math.min(rpcButtons.size(), 2); + rpc.button_label_1 = rpcButtons.get(0).getLabel(); + rpc.button_url_1 = rpcButtons.get(0).getUrl(); + + if (length == 2) { + rpc.button_label_2 = rpcButtons.get(1).getLabel(); + rpc.button_url_2 = rpcButtons.get(1).getUrl(); + } + } + + return this; + } + + public DiscordRichPresence build() { + return rpc; + } + } +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordUser.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordUser.java new file mode 100644 index 0000000..ee97720 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/DiscordUser.java @@ -0,0 +1,39 @@ +package com.hypherionmc.craterlib.core.rpcsdk; + +import com.sun.jna.Structure; + +import java.util.Arrays; +import java.util.List; + +/** + * @author HypherionSA + * Class representing the Discord User + */ +public class DiscordUser extends Structure { + + // The User ID of the User + public String userId; + + // The Username of the User + public String username; + + // The unique identifier of the user. Discontinued by Discord + @Deprecated + public String discriminator; + + // The avatar has of the user + public String avatar; + + /** + * DO NOT TOUCH THIS... EVER! + */ + @Override + protected List getFieldOrder() { + return Arrays.asList( + "userId", + "username", + "discriminator", + "avatar" + ); + } +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/DisconnectedCallback.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/DisconnectedCallback.java new file mode 100644 index 0000000..f07d879 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/DisconnectedCallback.java @@ -0,0 +1,17 @@ +package com.hypherionmc.craterlib.core.rpcsdk.callbacks; + +import com.sun.jna.Callback; + +/** + * @author HypherionSA + * Callback for when the Discord RPC disconnects + */ +public interface DisconnectedCallback extends Callback { + + /** + * Called when RPC disconnected + * @param errorCode Error code if any + * @param message Details about the disconnection + */ + void apply(int errorCode, String message); +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/ErroredCallback.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/ErroredCallback.java new file mode 100644 index 0000000..e1046fb --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/ErroredCallback.java @@ -0,0 +1,17 @@ +package com.hypherionmc.craterlib.core.rpcsdk.callbacks; + +import com.sun.jna.Callback; + +/** + * @author HypherionSA + * Callback for when the RPC ran into an error + */ +public interface ErroredCallback extends Callback { + + /** + * Called when an RPC error occurs + * @param errorCode Error code if any + * @param message Details about the error + */ + void apply(int errorCode, String message); +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/JoinGameCallback.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/JoinGameCallback.java new file mode 100644 index 0000000..ac35213 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/JoinGameCallback.java @@ -0,0 +1,16 @@ +package com.hypherionmc.craterlib.core.rpcsdk.callbacks; + +import com.sun.jna.Callback; + +/** + * @author HypherionSA + * Callback for when someone was approved to join your game + */ +public interface JoinGameCallback extends Callback { + + /** + * Called when someone joins a game from {@link JoinRequestCallback} + * @param joinSecret Secret or Password required to let the player join the game + */ + void apply(String joinSecret); +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/JoinRequestCallback.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/JoinRequestCallback.java new file mode 100644 index 0000000..ce21860 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/JoinRequestCallback.java @@ -0,0 +1,18 @@ +package com.hypherionmc.craterlib.core.rpcsdk.callbacks; + +import com.hypherionmc.craterlib.core.rpcsdk.DiscordUser; +import com.sun.jna.Callback; + +/** + * @author HypherionSA + * Callback for when someone requests to join your game + */ +public interface JoinRequestCallback extends Callback { + + /** + * Called when someone clicks on the Join Game button + * @param user The Discord User trying to join your game + * @see DiscordUser + */ + void apply(DiscordUser user); +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/ReadyCallback.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/ReadyCallback.java new file mode 100644 index 0000000..89fe19b --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/ReadyCallback.java @@ -0,0 +1,18 @@ +package com.hypherionmc.craterlib.core.rpcsdk.callbacks; + +import com.hypherionmc.craterlib.core.rpcsdk.DiscordUser; +import com.sun.jna.Callback; + +/** + * @author HypherionSA + * Callback for when the RPC has connected successfully + */ +public interface ReadyCallback extends Callback { + + /** + * Called when the RPC is connected and ready to be used + * @param user The user the RPC is displayed on + * @see DiscordUser + */ + void apply(DiscordUser user); +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/SpectateGameCallback.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/SpectateGameCallback.java new file mode 100644 index 0000000..125a698 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/callbacks/SpectateGameCallback.java @@ -0,0 +1,16 @@ +package com.hypherionmc.craterlib.core.rpcsdk.callbacks; + +import com.sun.jna.Callback; + +/** + * @author HypherionSA + * Callback for when someone is requesting to spectate your game + */ +public interface SpectateGameCallback extends Callback { + + /** + * Called when joining the game + * @param spectateSecret Secret or Password required to let the player spectate + */ + void apply(String spectateSecret); +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/helpers/RPCButton.java b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/helpers/RPCButton.java new file mode 100644 index 0000000..74a793c --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/rpcsdk/helpers/RPCButton.java @@ -0,0 +1,54 @@ +package com.hypherionmc.craterlib.core.rpcsdk.helpers; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * @author HypherionSA + * Helper class to add Buttons to Discord Rich Presence + * This can not be used with Join/Spectate + */ +public class RPCButton implements Serializable { + + // The label of the button + private final String label; + + // The URL the button will open when clicked + private final String url; + + protected RPCButton(String label, String url) { + this.label = label; + this.url = url; + } + + /** + * Create a new RPC Button + * @param label The label of the button + * @param url The URL the button will open when clicked + * @return The constructed button + */ + public static RPCButton create(@NotNull String label, @NotNull String url) { + // Null check used here for users blatantly ignoring the NotNull marker + if (label == null || label.isEmpty() || url == null || url.isEmpty()) { + throw new IllegalArgumentException("RPC Buttons require both a label and url"); + } + + label = label.substring(0, Math.min(label.length(), 31)); + return new RPCButton(label, url); + } + + /** + * @return The label assigned to the button + */ + public String getLabel() { + return label; + } + + /** + * @return The URL of the button + */ + public String getUrl() { + return url; + } +} diff --git a/Common/src/main/resources/darwin/libdiscord-rpc.dylib b/Common/src/main/resources/darwin/libdiscord-rpc.dylib new file mode 100644 index 0000000..1d13309 Binary files /dev/null and b/Common/src/main/resources/darwin/libdiscord-rpc.dylib differ diff --git a/Common/src/main/resources/linux-x86-64/libdiscord-rpc.so b/Common/src/main/resources/linux-x86-64/libdiscord-rpc.so new file mode 100644 index 0000000..ca959e1 Binary files /dev/null and b/Common/src/main/resources/linux-x86-64/libdiscord-rpc.so differ diff --git a/Common/src/main/resources/win32-x86-64/discord-rpc.dll b/Common/src/main/resources/win32-x86-64/discord-rpc.dll new file mode 100644 index 0000000..62f5287 Binary files /dev/null and b/Common/src/main/resources/win32-x86-64/discord-rpc.dll differ diff --git a/Common/src/main/resources/win32-x86/discord-rpc.dll b/Common/src/main/resources/win32-x86/discord-rpc.dll new file mode 100644 index 0000000..dd6c3a0 Binary files /dev/null and b/Common/src/main/resources/win32-x86/discord-rpc.dll differ