Programming: Implementing Discord rich-presence into your mod Last edited 4 months ago2022-02-18 18:17:10 UTC

Important notes:

Due to... reasons, this guide will not use the latest Discord "Game SDK", but their older, rich-presence-only SDK. This shouldn't cause any major problems for general GoldSrc purposes, though.
I also based the code off of this Source engine implementation; think of this as a slightly enhanced port of it.
If you're using Solokiller's Half-Life Updated code, you'll have to do some things differently. I'll explain it as we go.

If you care about a technical explanation behind this, then feel free to keep reading, but if you don't, skip over to the TECHNICAL GIBBERISH OVER mark.

Using a system very creatively called "Discord Manager", or DiscordMan for short, I've hopefully made this easy to implement and edit.

(nearly) Everything works on the client-side; when the game calls InitInput, it runs the DiscordMan_Startup function and sets up everything
needed for the basic rich presence implementation to work; a timer, handlers (which are unused here as they aren't required, but feel free to mess
around with them), your Discord game ID initialization and default logo; it then gets shutdown in the HUD_Shutdown function.

It "works", but it's pretty boring, so I decided to make it a little more dynamic.

Every frame in the HUD_Frame function, the DiscordMan_Update function is called, which will use a set of map-controlled CVars that the game changes
on the fly in order to change the chapter & area names, as well as the preview image in Discord. It works pretty well and looks neat.

These CVars are also defined in InitInput, and they are rpc_chapter, rpc_area and rpc_image.

They're all defined in the map properties, but the system handles these (or the lack thereof) a little differently for each:
rpc_chapter will display nothing when blank (default), or a custom string if defined;
rpc_area will display the map file name when blank (default), or a string of up to 64 characters long if defined;
and rpc_image will display your default logo (defined in the "discord_manager.cpp" file, default), or any other asset you put in your mod's Discord
application rich presence page.

That's basically everything; once you implement the code and add three lines to your FGD file (or put the keyvalues in with SmartEdit off, not
gonna stop you), everything is basically ready to go, as long as you already have a game set up on the Discord developer page.

TECHNICAL GIBBERISH OVER

First, if you haven't done this already, you'll need to create your application on the Discord developer page.
If you need help with this process, go take a look at their official documentation here; look at the "Get Set Up" section.

Then download the required files:
The Discord RPC SDK and my RPC manager.

In the SDK, you'll see a few folders. Open up "win32-dynamic". From here, put the LIB file somewhere easily accessible in your source code directory, and the DLL file in
your root game folder (next to the EXE). I'd rather have it be in the mod's own DLL folder, but I'm not entirely sure how to change that...
Make sure to edit your client project's linker config so it actually uses the LIB file.

And now, the actual code:

Make a new folder in your client source code called "discord", and put the contents of the SDK's "include" folder in there. Then, extract my RPC manager's files directly
into your client source code directory.

This won't work as-is, so time to do a few tweaks:
Open up "discord_manager.cpp". First, change the const char* defaultLogo line to an image you uploaded to your app's dev page. Your mod's logo should work pretty well.
After that, go down to the Discord_Initialize line. You'll see that the first argument is your game's ID; you can also pull this from your app's dev page (listed as
application ID in the general information tab). Make sure that it's a string and not a number!

Now over to "cdll_int.cpp". Include the DiscordMan header file somewhere near the other includes, like so:
// jay - discord rpc
#include "discord_manager.h"
Scroll down to the HUD_Frame function, and put DiscordMan_Update(); at the bottom. This will make the update function get called every frame.

Open "input.cpp"; we also need to include the manager header, as well as define the new cvars, so add these lines next to the other includes:
// jay - discord rpc
#include "discord_manager.h"
cvar_t* rpc_chapter;
cvar_t* rpc_area;
cvar_t* rpc_image;
Then, go to InitInput. Below the m_side line, paste this:
// jay - discord rpc
gEngfuncs.Con_Printf("Initializing Discord RPC CVars\n");
rpc_chapter = gEngfuncs.pfnRegisterVariable("rpc_chapter", "", FCVAR_CLIENTDLL);
rpc_area = gEngfuncs.pfnRegisterVariable("rpc_area", "", FCVAR_CLIENTDLL);
rpc_image = gEngfuncs.pfnRegisterVariable("rpc_image", "", FCVAR_CLIENTDLL);
This will add our CVars.

Now below all the vanilla init function calls, specifically right below the V_Init(); line, add this:
// jay - discord rpc
gEngfuncs.Con_Printf("Starting up Discord RPC\n");
DiscordMan_Startup();
For the last change on the client side, scroll a bit further down to the HUD_Shutdown function, and add this at the top:
// jay - discord rpc
gEngfuncs.Con_Printf("Shutting down Discord RPC");
DiscordMan_Kill();
Now, if you compile the code and open the game, you should see rich presence working it's magic. Mess around with the CVars and see what it looks like in Discord.
However, we're still missing a pretty crucial part of the system; the game still doesn't update the status on it's own.

This is where the server code comes in (has differences for Half-Life Updated!):
Open "cbase.h" and go all the way to the bottom. You should see the CWorld class definition there. If you're using the vanilla code or Xash, add these lines to the
bottom of the class:
// jay - discord rpc
virtual int        Save(CSave& save);
virtual int        Restore(CRestore& restore);
static    TYPEDESCRIPTION m_SaveData[];

string_t m_iszChapter;
string_t m_iszArea;
string_t m_iszImage;
If you're using HL Updated, add this instead:
// jay - discord rpc
bool Save(CSave& save) override;
bool Restore(CRestore& restore) override;
static    TYPEDESCRIPTION m_SaveData[];

string_t m_iszChapter;
string_t m_iszArea;
string_t m_iszImage;
This will allow the worldspawn entity, or the map, to read and store the new keyvalues for the system. We also implement save & restore functionality here, so it
properly tracks your status when loading a save.

Finally, over in the "world.cpp" file, look for the CWorld function definitions. You can find them by looking for a big comment right above them:
//=======================
// CWorld
//
// This spawns first when each level begins.
//=======================
In here, right below the LINK_ENTITY_TO_CLASS line, add this chunk of code:
// jay - discord rpc
// If we don't do save & restore, everything gets reset when loading a save
TYPEDESCRIPTION    CWorld::m_SaveData[] =
{
    DEFINE_FIELD(CWorld, m_iszChapter, FIELD_STRING),
    DEFINE_FIELD(CWorld, m_iszArea, FIELD_STRING),
    DEFINE_FIELD(CWorld, m_iszImage, FIELD_STRING),
};
IMPLEMENT_SAVERESTORE(CWorld, CBaseEntity);
After that, put this at the bottom of CWorld::Precache :
// jay - discord rpc
CVAR_SET_STRING("rpc_chapter", m_iszChapter ? STRING(m_iszChapter) : "");
CVAR_SET_STRING("rpc_area", m_iszArea ? STRING(m_iszArea) : "");
CVAR_SET_STRING("rpc_image", m_iszImage ? STRING(m_iszImage) : "");
This will set our CVars to whatever the mapper put into their map properties page when we load the map.

We're still missing the final piece of the puzzle; our keyvalues. This is what lets the map properties interact with the entity.
Go to the bottom of the CWorld::KeyValue function. If you're using vanilla code or Xash, look for this final else statement:
else
    CBaseEntity::KeyValue( pkvd );
and put this right above it, so that it's also below the defaultteam keyvalue definition:
// jay - discord rpc
else if (FStrEq(pkvd->szKeyName, "rpc_chapter"))
{
    m_iszChapter = ALLOC_STRING(pkvd->szValue);
    pkvd->fHandled = TRUE;
}
else if (FStrEq(pkvd->szKeyName, "rpc_area"))
{
    m_iszArea = ALLOC_STRING(pkvd->szValue);
    pkvd->fHandled = TRUE;
}
else if (FStrEq(pkvd->szKeyName, "rpc_image"))
{
    m_iszImage = ALLOC_STRING(pkvd->szValue);
    pkvd->fHandled = TRUE;
}
If you're using HL Updated, then like I mentioned earlier, put that snippet below the defaultteam keyvalue definition, so the next line is return ;
also change pkvd->fHandled = TRUE; to return true; .

If you did everything correctly, congratulations! The code is done. You can now compile it and live happily ever after.
Just kidding, you still need to add a few things to your FGD. Go to the worldspawn definition, and add these three keyvalues:
rpc_chapter(string) : "RPC Chapter" : : "Chapter name to display in Discord."
rpc_area(string) : "RPC Area" : : "Area name to display in Discord. Shows map file name if empty."
rpc_image(string) : "RPC Image" : : "Image to display in Discord."
Now you're actually done. Go ahead and try it out!

2 Comments

Commented 4 months ago2022-02-16 18:15:07 UTC Comment #104130
So it just shows people on Discord that someone is playing your mod?
Commented 4 months ago2022-02-17 20:40:11 UTC Comment #104131
So it just shows people on Discord that someone is playing your mod?
Pretty much, it's just a neat little detail that might make your mod ever so slightly more well known through your Discord status

You must log in to post a comment. You can login or register a new account.