Implement Discord Rich Presence SDK from Simple RPC

This commit is contained in:
2023-05-12 19:19:41 +02:00
parent 5b6bf5fd8a
commit ecec9ede6c
16 changed files with 623 additions and 0 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ eclipse
run
artifacts
src/test/**

View File

@@ -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<String> 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;
}
}
}

View File

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

View File

@@ -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<String> 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<RPCButton> 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;
}
}
}

View File

@@ -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<String> getFieldOrder() {
return Arrays.asList(
"userId",
"username",
"discriminator",
"avatar"
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.