Skip to content

Script for Ren'Py projects to be able to interact with Discord Rich Presence.

Notifications You must be signed in to change notification settings

Lezalith/RenPy-Discord-Presence

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 

Repository files navigation

Discord Rich Presence Support for Ren'Py Projects

This script contains a discord namespace containing functions that interact with Discord Rich Presence. Discord Rich Presence in the player's Playing a game status on Discord. The code is very intuitive and simple to use, but can only be run on Ren'Py 8, as the module this script depends on doesn't have a Py2 version. That's okay though - I recommend you to update to Ren'Py 8 in any case!

An Application set up on the Discord Developer Portal is required for every game supporting Rich Presence. After the App is created, you will receive the necessary Application ID to insert into settings.rpy, and it is also where all the images you plan on displaying in the presence need to be uploaded first. What it is and how to work with it is covered under the Wiki tab on this GitHub page, local copy of which is included in the code files.

Finally, this project is under the MIT License. This means you can use, modify and/or distribute this script, as long as I am credited ("Lezalith" is enough, but a link to my website with Ren'Py content, LezCave.com, is greatly appreciated!) and as long as the LICENSE.txt file stays included.

Table of Contents:

Download

To get the script, download one of the releases on the right side of the GitHub page, under the Releases section. Here are the files that you need to put into your game folder:

  • python-packages folder contains the pypresence modules that handle everything Discord-related. pypresence

  • RenPy_Discord_Presence folder contains this script's files:

    • rich_presence.rpy holds the entire script's code.
    • settings.rpy contains the two Related Variables described below.
    • discord_developer_portal.md, local copy of the Wiki tab.
    • README.md, local copy of this document (in Clean Versions only, it is in the base folder in Project Versions).
    • LICENSE.TXT, a file containing the MIT License that I ask you keep when downloading the files.

There are two versions for every release:

Project Version

Project Version contains the whole code of this repository. It is a project that can be launched from the Ren'Py Launcher and that shows how simple it is to update the presence status from both screens and labels, utilizing the set and update functions and their screen actions counterparts, both described below. Simply launch the project and keep an eye on your Discord profile.

All of the code related to the project is located in its script.rpy file, with insignificants modifications to screens.rpy and gui.rpy.

Clean Version

Clean Version does not contain project files and only contains the files listed above, meaning you can just copy everything over to your own project and you're good to go!

Related Variables

There are some important variables in the settings.rpy file that you need to visit. Here is what they do:

application_id takes a string with an Application ID of your Application set up on Discord Developer Portal. This has to be set in order for this script to work, having an invalid ID set will throw an error when launching the game.

define discord.application_id = "10208ABCDEFGHIJ2795"

main_menu_state takes a dictionary. Keys are strings of properties corresponding to presence elements, and values are their values.

This is the state shown in the presence anytime the game launches and/or enters the main menu. Below is what it looks like by default.

define discord.main_menu_state = { "details" : "In the Main Menu.",
                                       "large_image" : "lezalith"}

There's also the start_state. Just like main_menu_state, this is a set of properties, ones that are set when the game starts. Script acknowledges this by reaching the start label, name of which you need to set in the start_label variable. It can also be a list of label names instead, in case you have multiple labels this game can Start at.

Unlike main_menu_state, start_state doesn't have to be set, and start_label can be set to None to ignore its functionality.

define discord.start_state = { "details" : "Reading the Story.",
                                       "large_image" : "lezalith"}

define discord.start_label = "start"

Finally, there are log variables controlling what is printed out. Prints are recorded in the game's console and in the log.txt file. There are three of them:

# Shows whether the Presence was initialized and closed correctly.
define discord.log_important = True 

# Records properties whenever they change with set or update methods.
define discord.log_properties = True 

# Notes whenever the properties get rolled back or loaded from a save file, and what they were restored into.
define discord.log_restore = True 

I recommend leaving these on, it's information your players can pass on to you in case they're having troubles with your game.

Interacting With The Presence

Functions

Functions used to interact with the presence are defined inside the discord namespace. They're meant to be used inside labels and init python blocks. Here are the core two:

discord.set takes property names corresponding to presence elements for arguments. All elements are listed below. If some properties are already set in the presence, they are discarded and only the passed properties are kept.

An exception to this is the start property, which sticks to time since this session's launch unless specified.

discord.set(details = "Setting new Discord Rich Presence.")

discord.update takes property names for arguments the same way discord.set does. Difference between the two is that discord.update keeps the current properties as they are and only changes the ones provided.

discord.set(details = "Setting new Discord Rich Presence.")
discord.update(state = "State got changed while details stayed the same!")

Screen Actions

discord.set and discord.update have Screen Actions counterparts, intuitive alternatives to Function(discord.set) and Function(discord.update).

Same rules apply for both Actions:

  • They are sensitive as long as Presence was successfully initialized.
  • They are selected if properties given to the Action inside the screen match currently shown properties. Exception to this is start - if it's not specified inside the screen but is present in currently shown properties, it is considered to be "start_time in the comparison.

Here's an example of a simple screen with both Actions present. Almost identical example is below.

screen both_screen_actions():

    vbox:
        align (0.5, 0.5)

        textbutton "This one sets the state only.":
            action discord.Set(state = "Example State set.")

        textbutton "This sets details and leaves the state alone.":
            action discord.Update(details = "Details added while state was kept!")

Basic Rich Presence Elements

There are many things that can be shown inside Rich Presence. Below is a screenshot of a couple elements of Rich Presence highlighted, with their property name equivalent below. All the property names are listed below the screenshot with a short example using discord.update.

In the preview project, dictionary with all the properties for this example is stored in the discord.first_example variable and discord.main_menu_state is redirected to it.

first_presence_example

details takes a string, and is the upper line of text shown in the Presence.

discord.update(details = "Testing Discord Rich Presence.")

state takes a string, and is the lower line of text shown in the Presence.

discord.update(state = "It's super easy in Ren'Py 8!")

large_image takes a string that needs to correspond with an image uploaded onto the Discord Application. It is the larger image shown on the left side. If it doesn't find an image with that name, it displays a placeholder question mark image.

discord.update(large_image = "lezalith")

small_image takes a string that needs to correspond with an image uploaded onto the Discord Application. It is the smaller image, shown at the bottom right of the large_image. Unlike large_image, if it doesn't find an image with that name it shows nothing.

If no large_image is set or found, small_image is used in its place and no smaller image at the bottom right is shown.

discord.update(large_image = "lezalith", small_image = "lezalith")

start is a time from which Time Elapsed is calculated. It can take four different values:

  • "start_time" makes Time Elapsed refer to the start_time variable, where time since this game session's launch is recorded.
  • "new_time" inserts a new unix timestamp, reseting Time Elapsed to 0:0. It does not overwrite the recorded start_time by itself, but you can change that one directly if you need to for some reason.
  • None which results in Time Elapsed being hidden.
  • Unix timestamp from which Time Elapsed is calculated.

If start is not specified in the properties displayed, it is always present with the value of "start_time".

discord.update(start = "start_time")

Overall, state shown on the screenshot can be accomplished with the following properties passed:

discord.set(details = "Testing Discord Rich Presence.",
            state = "It's super easy in Ren'Py 8!",
            large_image = "lezalith",
            large_text = "Large Image Tooltip!",
            small_image = "lezalith",
            small_text = "Small Image Tooltip!")

As you can see, there are two more property names included there that I haven't mentioned - large_text and small_text. These are text tooltips that appear when the respective images are hovered by a cursor.

Advanced Rich Presence Elements

The final screenshot covers all the remaining rich presence properties. state was already covered above, however it is required for the party_size property to work.

In the preview project, dictionary with all the properties for this example is stored in the discord.second_example variable.

second_presence_example

end takes an unix timestamp and calculates Time Left until that timestamp.

Setting this pushes start completely aside and Time Elapsed with it and displays Time Left instead.

discord.update(end = time.time() + 3000)

party_size takes a list of two ints. This is used for multiplayer games, where the two numbers represent the current party size and max party size respectively. We can still use it with visual novels if we're creative enough.

For the party_size to be visible, state has to be also provided.

discord.update(state = "Reading a Chapter", party_size = [1, 5])

buttons takes a list of up to two dicts, and allows for buttons to be added into the Presence. The dict consists of two keys, label being the text written inside the button, and url being the link opened upon clicking it.

Note: Clicking buttons in the Presence in your own profile does nothing. Have others test them for you if you have button(s) in your projects!

discord.update(buttons = [ dict(label = "Discord Presence Example Button", url = "https://github.com/Lezalith/RenPy_Discord_Presence"),
                           dict(label = "Lezalith's Promotion Button!", url = "https://www.lezcave.com") ]

Putting all of that together, state shown on the second example screenshot was created with these properties:

discord.set(state = "Reading a Chapter",
            end = time.time() + 3000,
            party_size = [1, 5],
            buttons = [ dict(label = "Discord Presence Example Button", url = "https://github.com/Lezalith/RenPy_Discord_Presence"),
                        dict(label = "Lezalith's Promotion Button!", url = "https://www.lezcave.com") ])

Important Notes

Discord Not Installed

Discord presence only works for users who have the Discord desktop application installed. For players that do not have Discord installed, this entire code will simply do nothing. discord namespace is still defined with all the properties and methods, but none of the methods do anything.

This means that players who have Discord (or have it but aren't logged in) can enjoy the benefits while those who do not aren't hindered in any way.

Saving, Loading and Rollback

All presence properties are compatible with both saving games and rollbacking dialogue:

  • Loading a saved game will restore the properties that were present when the game was saved. start possibly preserves the value of start_time and refers to time since starting this game session.
  • Rollbacking past a change in the presence will return the presence to the original state, as would be expected.

Limitations

config.label_callback Variable Taken

This script uses the config.label_callback variable to set start_state, meaning you can't use it yourself. If you're fine with start_state functionality not present, you can delete its define statement in rich_presence.rpy.

This will be fixed with Ren'Py 8.1 - a new variable called config.label_callbacks will be present, one that can take multiple functions just like all the other callbacks. The line is already prepared in the file, and I'll release an updated version of this script myself once Ren'Py 8.1 comes out.

Update Delays

A Discord Desktop Application running on the same machine as the game that's updating the presence, has updates on the player's profile shown instantly. This is not always the case for other Discord Desktop Apps - even ones where the account of the player themselves are logged in, curiously enough.

How often this delay occurs seems to correlate with the frequency of the presence updates and especially affects the clear method, but you shouldn't worry about it too much, as it seems to be about 12s on average when it does occur.

Too Many Connections

Connecting to the Discord Rich Presence multiple times in quick succession will result in the connection not being established. In practice, this happens when you...

  • ...launch and quit...
  • ...reload...
  • ...start and return to the main menu in...

...the game too many (approximately 4) times too fast (span of approximately 40s).

This makes the game unresponsive for the approximate span since the oldest successful connection. Restarting the game doesn't fix this, and the game won't launch again until the timer runs out.

My guess is that it's a precaution against malicious exploits on Discord's part and cannot be affected by Python code, but as is the case with Update Delays it's not a big issue - while possible for creators (especially through reloads), players should fulfill these requirements incredibly rarely, if ever.

Examples

Screen Example

Here is an example of two textbuttons in a screen, both using the discord.Set Screen Action.

screen screen_example():

    vbox:
        align (0.5, 0.5)

        textbutton "This one sets the state only.":
            action discord.Set(state = "Example State.")

        textbutton "This sets details and leaves the state alone.":
            action discord.Set(details = "Example Details.")

Label Examples

Below are the two example labels found inside this preview project.

Long Basic Label

Here is the first label, showcasing rollback and save/load functionality.

label long_example():

    $ discord.set(details = "Inside a Preview", state = "Save & Load")

    "Welcome to the Save/Load preview label."

    "Feel free to save in any of the following places - just right-click and select a save slot."

    scene scarian_island

    $ discord.set(details = "Hanging out on Scarian Island", state = "Swimming")

    "An island in the middle of the ocean."

    "Swim around as much as you want, you won't find any sharks here!"

    "Moving on..."

    scene cerise_chantry

    $ discord.set(details = "Walking around Cerise", state = "Visiting the Garden")

    "A peaceful chantry with a rose garden."

    "Can you hear the sound of silence?"

    "Of course you can, this project has no audio."

    "Moving on..."

    scene gamboge_peninsula

    $ discord.set(details = "On Gamboge Peninsula", state = "Sunbathing")

    "A desert oasis with a strange name."

    "Be careful not to get a heatstroke."

    "Moving on..."

    scene minty_meadows

    $ discord.set(details = "Traversing Minty Meadows", state = "Chasing Cats")

    "A beautiful land filled with flowers and bushes."

    "For some reason, there are cats running everywhere around you."

    "Okay, that's all the locations. Ready to head back?"

    scene 

    $ discord.set(details = "Inside a Preview", state = "Save & Load")

    "You can now right-click again and {b}load{/b} a previously saved game."

    "Presence properties will be reverted to those present when you saved, and you can even rollback further from there!"

    "Moving past this line returns you to the main screen, restoring {b}`main_menu_state{/b} with {b}start{/b} set to {b}\"start_time\"{/b}."

    return

Functionality Label

And here is the second label, showing off all the functionality. Everything is explained by the dialogue lines.

label functionality_example():

    "This label shows how discord presence can be changed inside labels!"

    "Since it is set as the {b}start_label{/b} in {b}settings.rpy{/b}, entering it has already set presence to {b}start_state{/b}."

    "After this line, {b}discord.set{/b} will be called to set presence to only contain a {b}state{/b} text line."

    $ discord.set(state = "Update after the first line.")

    "And it is so."

    "After this line, {b}discord.update{/b} is called to change the {b}details{/b} property, while keeping others as they are."

    $ discord.update(details = "This wasn't here before!")

    "Presence now contains one more piece of info."

    "After this line, {b}start{/b} property is changed to {b}\"new_time\"{/b}, which will reset Time Elapsed to 0:0."

    $ discord.update(start = "new_time")

    "The time since the first launch can always be restored by changing {b}start{/b} property to {b}\"start_time\"{/b}"

    "Done after this line."

    $ discord.update(start = "start_time")

    "Finally, changing the {b}start{/b} property to {b}None{/b} hides Time Elapsed altogether."

    "Again, done after this line."

    $ discord.update(start = None)

    "As you can see, Time Elapsed is no longer visible."

    "Last method is {b}discord.clear{/b}, which can be called to clear the presence of all properties."

    "This hides the game completely, as if nothing was being played."

    $ discord.clear()

    "Presence all cleared and hidden!"

    "You will now return to the main menu. {b}main_menu_state{/b} will be restored with {b}start{/b} being automatically set to {b}\"start_time\"{/b}."

    return

Support

If you need help setting up this script or the App on Discord's Dev Portal, you can contact me on Discord itself. Either write me a DM, Lezalith (LezCave.com)#2853, or preferably join my server! It's a server dedicated to my website, LezCave.com, where I post Ren'Py scripts and tutorials. Come say hello!

A good alternative is pinging me on the official Ren'Py Discord server, where I'm one of the Moderators, so I'm very active there. You can send me an email at Lezalith@gmail.com if you want to be extra formal, but I don't check my emails very often. Consider also letting me know on Discord that you've sent me one, so I notice it and can reply to it.

Finally, if you want to be extra supportive, you can donate a dollar or two to my Paypal.me. I'll use the money to manage my website and dedicate more time to posting new articles!

About

Script for Ren'Py projects to be able to interact with Discord Rich Presence.

Resources

Stars

Watchers

Forks

Packages

No packages published