15 KiB
Introducing Rich Presence
How easy is it for people to play your game together? With Rich Presence from Discord—a new feature in the ever-expanding GameBridge suite—it just got so easy, a caveman Junior Dev could do it.
So, what is it?
Rich Presence allows you to leverage the totally overhauled "Now Playing" section in a Discord user's profile to help people play your game together. Rich game data—including duration, score, current boss or map, and so much more—lives inside a user's profile alongside buttons that let you spectate or join a friend's game directly from Discord, or get a notification when it's over.
It also features beautiful embeds right in your chat window with real-time information about open party slots and the party's in-game status. No more wondering if there's room for you to join, or if they're mid-game, or about to finish. It's a living invitation to play together!
So, how does it work?
Note: While the SDK for Rich Presence is not currently publically available, the documentation here accurately reflects any eventual implementation you would need to do. That means that you can have your codebase all ready to be hooked up to the SDK before having access to it.
We worked hard to make using Discord's Rich Presence system as easy as possible. All you need is our header file—what you will interact with—and our library—where we did all the hard work for you—and you are ready to roll!
In the header file, you'll find five event-emitting callbacks:
ready()
errored()
disconnected()
joinGame()
spectateGame()
These five callbacks make up the entirety of what you need to implement. Behind the scenes, we do all the heavy lifting for you.
Whatcha dooooooin'?
The first step in implementing Rich Presence is registering your callback functions to the five DiscordEventHandlers
and then calling Discord_Initialize()
with your APPLICATION_ID
:
void InitDiscord()
{
DiscordEventHandlers handlers;
memset(&handlers, 0, sizeof(handlers));
handlers.ready = handleDiscordReady;
handlers.disconnected = handleDiscordDisconnected;
handlers.errored = handleDiscordError;
Discord_Initialize(APPLICATION_ID, &handlers, 1);
}
The Discord_Initialize()
function has a parameter autoRegister
. Marking this field as true lets Discord register an application protocol in the format discord-[your app id]://
so that joining and spectating will work even when the game is closed. Once the game launches, we'll call your joinGame()
and spectateGame()
functions to continue the flow. If your game needs custom parameters on launch, such as an auth ticket, you'll need to register your own protocol on the system. More information on this coming soon. For now, just ask us!
The core of Discord's Rich Presence SDK is the Discord_UpdatePresence()
function. This is what sends your game data up to Discord to be seen and used by others. You should call Discord_UpdatePresence()
with the necessary data in your ready()
callback and any time something in the presence payload changes. The payload is outlined in detail in a later section, but for now, here's an example of a super rich presence:
void UpdatePresence()
{
DiscordRichPresence discordPresence;
discordPresence.state = "In a Group";
discordPresence.details = "Competitive | In a Match";
discordPresence.endTimestamp = time(nullptr) + ((60 * 5) + 23);
discordPresence.partyId = GameEngine.GetPartyId();
discordPresence.partySize = 3;
discordPresence.partyMax = 6;
discordPresence.matchSecret = "4b2fdce12f639de8bfa7e3591b71a0d679d7c93f";
discordPresence.spectateSecret = "e7eb30d2ee025ed05c71ea495f770b76454ee4e0";
discordPresence.instance = 1;
Discord_UpdatePresence(&discordPresence);
}
Note: In order for a user to send a join or spectate invite, Discord needs a
joinSecret
orspectateSecret
. This means you should callDiscord_UpdatePresence()
even when a user is not actively in a group or game match. That way, Discord has all the data it needs to properly route a party request to other players.
Knock knock! Who's there? You!
Let's get into the meat of the callback functions:
ready()
lets you know when your game is connected to Discord, usually on a first connection or a reconnect.
joinGame()
and spectateGame()
are up to you to handle and implement in your game's infrastructure. For these functions, we send you back either the joinSecret
or spectateSecret
, depending on which action happens. You should then reverse your secret back into usable data and handle it as necessary. More on what those secrets are and how to generate them later.
disconnect()
and errored()
don't actually require you to do anything. If for some reason your client disconnects from Discord or encounters an error, we'll reconnect automatically. However, we will send you the events if you'd like to display them, log them, or ignore the problem like a good developer.
The header file also contains the Discord_RunCallbacks()
function. This invokes any pending callbacks from Discord on the calling thread (it's thread safe!), giving you ultimate control over your events. If you aren't sure what to do, just call it once per frame in your game's main loop. As Captain Planet says, the power is yours!
Movin' with the payload
Earlier, we showed an example payload of some really rich presence data. The full list of fields is as follows:
typedef struct DiscordRichPresence {
const char* state; /* max 128 bytes */
const char* details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char* largeImageKey; /* max 32 bytes */
const char* largeImageText; /* max 128 bytes */
const char* smallImageKey; /* max 32 bytes */
const char* smallImageText; /* max 128 bytes */
const char* partyId; /* max 128 bytes */
int partySize;
int partyMax;
const char* matchSecret; /* max 128 bytes */
const char* joinSecret; /* max 128 bytes */
const char* spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
Ooo, let's break it down!
state
is the user's current context. This could be "In Game," "In Queue," "Looking to Play," or any other states you want to denote.
details
is a catch-all field that you can customize as you please. It's the first line of data displayed under the name of your game, and should display the most prominent data. For example, "Competitive | Match Type (Score)".
startTimestamp
is the Unix timestamp at which the player entered their most recent instanced state, for example, a new game. Providing startTimestamp
will cause the display time in the presence popout to count up from 00:00
.
endTimestamp
is also a Unix timestamp, but denotes at what time the player's current instanced state will end. Send this if you want us to do the math and display the timer as counting down to 00:00
from your timestamp.
largeImageKey
and smallImageKey
are the key values for the artwork you have uploaded to your Developer Dashboard (more on that later!). largeImageText
and smallImageText
are the mouseover tooltips on the corresponding artwork.
partyId
is the public id for the player's current party. Discord uses this to power dynamic party status in the chat embeds.
partySize
is the current size of the player's party. Sending 0
is the same as omitting it.
partyMax
is the maximum allowed size for a party. Sending 0
means there is no limit on the size.
matchSecret
works in tandem with instance
to power the notification piece of Rich Presence. It's also used as an internal identifier for the player's current match/dungeon/raid so we can match presences for things like the joinGame()
embed. When you send us a matchSecret
with instance
set to true
, we know a further change in matchSecret
means the player is done with whatever they were doing, or finally gave up on the Water Temple, and we can send a notification to anyone who subscribed.
joinSecret
and spectateSecret
are unique, non-guessable data that serve as Discord's way to make callbacks to your game. These could be hashed or encrypted values of a player's id for spectateSecret
, a lobby id and password for joinSecret
, or any other data your game needs. Discord sends these values back to you in the appropriate callback functions. More on these in the next section.
instance
helps Discord be smart about notifications and display. Setting it to true
tells us to show the "Notify me" button and alert whoever clicks it when the matchSecret
changes.
We've chosen the fields based on common data between the most popular games on Discord, but they are by no means rigid. Play with them to fit the data you'd like to display for your game! You can also omit any of the fields, and the UI will gracefully adjust. Below is an image that shows which fields go where when sending a full data payload with spectating and notifications enabled; use it for reference for your own data:
Secrets, secrets are no fun
Security is of the utmost importance to us here at Discord, and we know it is for you, too. That's why we want to make sure that you properly understand matchSecret
, joinSecret
, and spectateSecret
so that your game data is safe and secure over the wire.
Secrets are obfuscated data of your choosing. They could be match ids, player ids, lobby ids, or your favorite color in hexadecimal over and over. You should send us data that someone else's game client would need to join or spectate their friend. If you can't or don't want to support those actions, you don't need to send us secrets. In fact, you could skip the rest of this section, but we all know that the real magic is learning.
Your secrets on Discord need to be well-kept. When someone wants to join a friend's game, they hit the "Join" button. Discord checks to make sure their permissions are in order, and then passes their client the joinSecret
so the callback can fire on their machine, and their game client can handle it.
To keep security on the up and up, Discord requires that you properly hash/encode/encrypt/put-a-padlock-on-and-swallow-the-key-but-wait-then-how-would-you-open-it your secrets. There are more than a couple of ways of going about this, but here's a couple points to muse over:
- What does your game require to join a party, spectate a match, or other actions available through Rich Presence?
The secrets you send us can be whatever data you want, so make life easy for yourself! If you need a matchId
, playerId
, lobbyPassword
, and bloodType
(we hope not), hash all of those up as your secret. Then, when you reverse the hash on callback, you'll have everything you need to complete the action.
- How good is your security?
Just because you're doing it doesn't necessarily mean you're doing it in the best—and safest—way possible. Create your secrets on your server, not on your client, and consider using strong encryption methods (anyone seen my private key?) to keep your data safe. Sodium, a multi-language, cross-compilable crypto library, is a great place to start. It's available straight from GitHub, as a pre-built library for some common editors, and on NuGet. It's so easy to use, even this example in C isn't scary:
#include <sodium.h>
unsigned char* myMessage = "a super secret message for Discord";
printf("Original text: %s \n \n", myMessage);
int messageLength = strlen(myMessage) + 1; // Accounting for null terminator byte
int ciphertextLength = crypto_secretbox_MACBYTES + messageLength;
unsigned char nonce[crypto_secretbox_NONCEBYTES]; // Buffer for your nonce
unsigned char key[crypto_secretbox_KEYBYTES]; // Buffer for your key
unsigned char* ciphertext = malloc(ciphertextLength);
randombytes_buf(nonce, sizeof nonce);
randombytes_buf(key, sizeof key);
crypto_secretbox_easy(ciphertext, myMessage, messageLength, nonce, key);
char* hex = malloc(2 * ciphertextLength + 1);
int hexLength = 2 * ciphertextLength + 1;
sodium_bin2hex(hex, hexLength, ciphertext, ciphertextLength);
printf("Encrypted: %s \n \n", hex);
We encrypt the data, and then convert it to hex for the sake of readability. Your key is then stored as hex
, which is what you should send to Discord. And to unencrypt on callback:
unsigned char* decrypted = malloc(messageLength);
unsigned char* bin = malloc(ciphertextLength);
sodium_hex2bin(bin, ciphertextLength, hex, hexLength, "", NULL, NULL);
crypto_secretbox_open_easy(decrypted, bin, ciphertextLength, nonce, key);
printf("Decrypted: %s", decrypted);
free(ciphertext); // Don't forget to free up the memory when you're done!
free(decrypted);
free(hex);
free(bin);
Running this code snippet gives the following output:
Discord maintains the highest standards of security for data, so we require that you hold yourself to those same standards if you wish to implement these secrets. Remember, it's for your benefit as well! We wouldn't want evildoers ending up in places they shouldn't be!
There's a time and a place for everything
All fields in the DiscordRichPresence
object are entirely optional. Anything you choose to omit simply won't be displayed; the UI will dynamically adapt from the full Rich Presence object all the way down to the basic presence view. We highly encourage you to include as many fields as you can to make your game as attractive and interactive for other players as possible. However, if you choose to be a bit more selective, here's a handy table for what each piece of Rich Presence requires:
Field | Custom Artwork | Notification | Spectating | Joining |
---|---|---|---|---|
state | ✓ | ✓ | ✓ | |
details | ||||
startTimestamp | ||||
endTimestamp | ||||
largeImageKey | ✓ | |||
smallImageKey | ✓ | |||
largeImageText | ✓ | |||
smallImageText | ✓ | |||
partyId | ✓ | |||
partySize | ✓ | |||
partyMax | ✓ | |||
matchSecret | ✓ | |||
joinSecret | ✓ | |||
spectateSecret | ✓ | |||
instance | ✓ |
Your New Developer Dashboard
Included with the launch of Rich Presence is an overhaul of Discord's Developer Dashboard. We want to make Rich Presence and the rest of the GameBridge Suite as easy as possible to use. Our first step is helping you ditch your CDN. You're welcome.
OK, well, not entirely. But! Discord will host any and all artwork that you need to have the very richest of presences. Upload an image, tag it with a key—preferrably one you can remember—and bam. It's ready for Rich Presence use.
It's always better when we're together
Discord's Rich Presence system is an awesome way to get gamers together in your game. We've done the heavy lifting on the engineering side to make it easy-as-pie for you to implement. So get to coding and make your game's presence as rich as it can be!