Compare commits

..

No commits in common. "master" and "v2.1.1" have entirely different histories.

29 changed files with 284 additions and 819 deletions

1
.gitignore vendored
View File

@ -2,4 +2,3 @@
/.vscode/ /.vscode/
/thirdparty/ /thirdparty/
.vs/ .vs/
.DS_Store

View File

@ -43,5 +43,5 @@ before_install:
script: script:
- mkdir build - mkdir build
- cd build - cd build
- cmake -DCLANG_FORMAT_SUFFIX=$CLANG_FORMAT_SUFFIX -DWARNINGS_AS_ERRORS=On --config Release .. - cmake -DCLANG_FORMAT_SUFFIX=$CLANG_FORMAT_SUFFIX --config Release ..
- cmake --build . -- -j2 - cmake --build . -- -j2

View File

@ -28,23 +28,23 @@ endif(CLANG_FORMAT_CMD)
# thirdparty stuff # thirdparty stuff
execute_process( execute_process(
COMMAND mkdir ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty COMMAND mkdir ${CMAKE_SOURCE_DIR}/thirdparty
ERROR_QUIET ERROR_QUIET
) )
find_file(RAPIDJSONTEST NAMES rapidjson rapidjson-1.1.0 PATHS ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty CMAKE_FIND_ROOT_PATH_BOTH) find_file(RAPIDJSONTEST NAMES rapidjson rapidjson-1.1.0 PATHS ${CMAKE_SOURCE_DIR}/thirdparty CMAKE_FIND_ROOT_PATH_BOTH)
if (NOT RAPIDJSONTEST) if (NOT RAPIDJSONTEST)
message("no rapidjson, download") message("no rapidjson, download")
set(RJ_TAR_FILE ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/v1.1.0.tar.gz) set(RJ_TAR_FILE ${CMAKE_SOURCE_DIR}/thirdparty/v1.1.0.tar.gz)
file(DOWNLOAD https://github.com/miloyip/rapidjson/archive/v1.1.0.tar.gz ${RJ_TAR_FILE}) file(DOWNLOAD https://github.com/miloyip/rapidjson/archive/v1.1.0.tar.gz ${RJ_TAR_FILE})
execute_process( execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xzf ${RJ_TAR_FILE} COMMAND ${CMAKE_COMMAND} -E tar xzf ${RJ_TAR_FILE}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/thirdparty
) )
file(REMOVE ${RJ_TAR_FILE}) file(REMOVE ${RJ_TAR_FILE})
endif(NOT RAPIDJSONTEST) endif(NOT RAPIDJSONTEST)
find_file(RAPIDJSON NAMES rapidjson rapidjson-1.1.0 PATHS ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty CMAKE_FIND_ROOT_PATH_BOTH) find_file(RAPIDJSON NAMES rapidjson rapidjson-1.1.0 PATHS ${CMAKE_SOURCE_DIR}/thirdparty CMAKE_FIND_ROOT_PATH_BOTH)
add_library(rapidjson STATIC IMPORTED ${RAPIDJSON}) add_library(rapidjson STATIC IMPORTED ${RAPIDJSON})

101
README.md
View File

@ -1,11 +1,5 @@
# Discord RPC # Discord RPC
## Deprecation Notice
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
---
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs. This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
Included here are some quick demos that implement the very minimal subset to show current status, and Included here are some quick demos that implement the very minimal subset to show current status, and
@ -13,7 +7,7 @@ have callbacks for where a more complete game would do more things (joining, spe
## Documentation ## Documentation
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md). The most up to date documentation for Rich Presence can always be found in our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)!
## Basic Usage ## Basic Usage
@ -21,76 +15,13 @@ Zeroith, you should be set up to build things because you are a game developer,
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function. First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
### Unreal Engine 4 Setup
To use the Rich Presense plugin with Unreal Engine Projects:
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
4. Follow the steps below for each OS
5. Build your UE4 project
6. Launch the editor, and enable the Discord plugin.
#### Windows
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
#### Mac
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
#### Linux
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
- Inside, create another folder `x86_64-unknown-linux-gnu`
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
### Unity Setup
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
We've got our `Plugins` folder ready, so let's get platform-specific!
#### Windows
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
8. Done!
#### MacOS
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
6. Done!
#### Linux
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
5. Done!
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
### From package ### From package
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game. Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
### From repo ### From repo
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this: There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
```sh ```sh
cd <path to discord-rpc> cd <path to discord-rpc>
mkdir build mkdir build
@ -98,7 +29,6 @@ There's a [CMake](https://cmake.org/download/) file that should be able to gener
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to> cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
cmake --build . --config Release --target install cmake --build . --config Release --target install
``` ```
There is a wrapper build script `build.py` that runs `cmake` with a few different options. There is a wrapper build script `build.py` that runs `cmake` with a few different options.
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`. Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
@ -106,21 +36,20 @@ Usually, I run `build.py` to get things started, then use the generated project
There are some CMake options you might care about: There are some CMake options you might care about:
| flag | default | does | | flag | default | does |
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | |------|---------|------|
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. | | `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself.
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) | | `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option)
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL | | [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
## Continuous Builds ## Continuous Builds
Why do we have three of these? Three times the fun! Why do we have three of these? Three times the fun!
| CI | badge | | CI | badge |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | |----|-------|
| TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc) | | TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc)
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc) | | AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc)
| Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc) | | Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc)
## Sample: send-presence ## Sample: send-presence
@ -128,11 +57,11 @@ This is a text adventure "game" that inits/deinits the connection to Discord, an
## Sample: button-clicker ## Sample: button-clicker
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders. This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button.
## Sample: unrealstatus ## Sample: unrealstatus
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders. This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI.
## Wrappers and Implementations ## Wrappers and Implementations
@ -145,14 +74,10 @@ Below is a table of unofficial, community-developed wrappers for and implementat
###### Rich Presence Wrappers and Implementations ###### Rich Presence Wrappers and Implementations
| Name | Language | | Name | Language |
| ------------------------------------------------------------------------- | --------------------------------- | |------|----------|
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# | | [discord-rpc.jar](https://github.com/Vatuu/discord-rpc "Discord-RPC.jar") | Java |
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java | | [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java | | [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript | | [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) | | [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) | | [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |

137
build.py
View File

@ -30,7 +30,7 @@ INSTALL_ROOT = os.path.join(SCRIPT_PATH, 'builds', 'install')
def get_signtool(): def get_signtool():
""" get path to code signing tool """ """ get path to code signing tool """
if PLATFORM == 'win': if PLATFORM == 'win':
sdk_dir = 'c:\\Program Files (x86)\\Windows Kits\\10' # os.environ['WindowsSdkDir'] sdk_dir = os.environ['WindowsSdkDir']
return os.path.join(sdk_dir, 'bin', 'x86', 'signtool.exe') return os.path.join(sdk_dir, 'bin', 'x86', 'signtool.exe')
elif PLATFORM == 'osx': elif PLATFORM == 'osx':
return '/usr/bin/codesign' return '/usr/bin/codesign'
@ -66,96 +66,55 @@ def cli(ctx, clean):
ctx.invoke(archive) ctx.invoke(archive)
@cli.command()
def unity():
""" todo: build unity project """
pass
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def unity(ctx): def for_unity(ctx):
""" build just dynamic libs for use in unity project """ """ build just dynamic libs for use in unity project """
ctx.invoke(libs, clean=False, static=False, shared=True, skip_formatter=True, just_release=True) ctx.invoke(
BUILDS = [] libs,
clean=False,
click.echo('--- Copying libs and header into unity example') static=False,
UNITY_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'button-clicker', 'Assets', 'Plugins') shared=True,
skip_formatter=True,
if sys.platform.startswith('win'): just_release=True
LIBRARY_NAME = 'discord-rpc.dll' )
BUILD_64_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release')
UNITY_64_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86_64')
BUILDS.append({BUILD_64_BASE_PATH: UNITY_64_DLL_PATH})
BUILD_32_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win32-dynamic', 'src', 'Release')
UNITY_32_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86')
BUILDS.append({BUILD_32_BASE_PATH: UNITY_32_DLL_PATH})
elif sys.platform == 'darwin':
LIBRARY_NAME = 'discord-rpc.bundle'
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'osx-dynamic', 'src')
UNITY_DLL_PATH = UNITY_PROJECT_PATH
os.rename(
os.path.join(BUILD_BASE_PATH, 'libdiscord-rpc.dylib'), os.path.join(BUILD_BASE_PATH, 'discord-rpc.bundle'))
BUILDS.append({BUILD_BASE_PATH: UNITY_DLL_PATH})
elif sys.platform.startswith('linux'):
LIBRARY_NAME = 'discord-rpc.so'
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'linux-dynamic', 'src')
UNITY_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86')
os.rename(os.path.join(BUILD_BASE_PATH, 'libdiscord-rpc.so'), os.path.join(BUILD_BASE_PATH, 'discord-rpc.so'))
BUILDS.append({BUILD_BASE_PATH: UNITY_DLL_PATH})
else:
raise Exception('Unsupported platform ' + sys.platform)
for build in BUILDS:
for i in build:
mkdir_p(build[i])
shutil.copy(os.path.join(i, LIBRARY_NAME), build[i])
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def unreal(ctx): def unreal(ctx):
""" build libs and copy them into the unreal project """ """ build libs and copy them into the unreal project """
ctx.invoke(libs, clean=False, static=False, shared=True, skip_formatter=True, just_release=True) ctx.invoke(
BUILDS = [] libs,
clean=False,
static=False,
shared=True,
skip_formatter=True,
just_release=True
)
click.echo('--- Copying libs and header into unreal example') click.echo('--- Copying libs and header into unreal example')
UNREAL_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'unrealstatus', 'Plugins', 'discordrpc') UNREAL_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'unrealstatus', 'Plugins', 'discordrpc')
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release')
UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win64')
mkdir_p(UNREAL_DLL_PATH)
shutil.copy(os.path.join(BUILD_BASE_PATH, 'discord-rpc.dll'), UNREAL_DLL_PATH)
UNREAL_INCLUDE_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Include') UNREAL_INCLUDE_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Include')
mkdir_p(UNREAL_INCLUDE_PATH) mkdir_p(UNREAL_INCLUDE_PATH)
shutil.copy(os.path.join(SCRIPT_PATH, 'include', 'discord_rpc.h'), UNREAL_INCLUDE_PATH) shutil.copy(os.path.join(SCRIPT_PATH, 'include', 'discord-rpc.h'), UNREAL_INCLUDE_PATH)
if sys.platform.startswith('win'): UNREAL_LIB_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win64')
LIBRARY_NAME = 'discord-rpc.lib' mkdir_p(UNREAL_LIB_PATH)
BUILD_64_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release') shutil.copy(os.path.join(BUILD_BASE_PATH, 'discord-rpc.lib'), UNREAL_LIB_PATH)
UNREAL_64_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win64')
BUILDS.append({BUILD_64_BASE_PATH: UNREAL_64_DLL_PATH})
BUILD_32_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win32-dynamic', 'src', 'Release')
UNREAL_32_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win32')
BUILDS.append({BUILD_32_BASE_PATH: UNREAL_32_DLL_PATH})
elif sys.platform == 'darwin':
LIBRARY_NAME = 'libdiscord-rpc.dylib'
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'osx-dynamic', 'src')
UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Mac')
BUILDS.append({BUILD_BASE_PATH: UNREAL_DLL_PATH})
elif sys.platform.startswith('linux'):
LIBRARY_NAME = 'libdiscord-rpc.so'
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'linux-dynamic', 'src')
UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Linux')
BUILDS.append({BUILD_BASE_PATH: UNREAL_DLL_PATH})
else:
raise Exception('Unsupported platform ' + sys.platform)
for build in BUILDS:
for i in build:
mkdir_p(build[i])
shutil.copy(os.path.join(i, LIBRARY_NAME), build[i])
def build_lib(build_name, generator, options, just_release): def build_lib(build_name, generator, options, just_release):
@ -165,7 +124,11 @@ def build_lib(build_name, generator, options, just_release):
mkdir_p(build_path) mkdir_p(build_path)
mkdir_p(install_path) mkdir_p(install_path)
with cd(build_path): with cd(build_path):
initial_cmake = ['cmake', SCRIPT_PATH, '-DCMAKE_INSTALL_PREFIX=%s' % os.path.join('..', 'install', build_name)] initial_cmake = [
'cmake',
SCRIPT_PATH,
'-DCMAKE_INSTALL_PREFIX=%s' % os.path.join('..', 'install', build_name)
]
if generator: if generator:
initial_cmake.extend(['-G', generator]) initial_cmake.extend(['-G', generator])
for key in options: for key in options:
@ -207,28 +170,22 @@ def sign():
sign_command_base = [ sign_command_base = [
tool, tool,
'sign', 'sign',
'/n', '/n', 'Hammer & Chisel Inc.',
'Discord Inc.',
'/a', '/a',
'/tr', '/tr', 'http://timestamp.digicert.com/rfc3161',
'http://timestamp.digicert.com/rfc3161',
'/as', '/as',
'/td', '/td', 'sha256',
'sha256', '/fd', 'sha256',
'/fd',
'sha256',
] ]
elif PLATFORM == 'osx': elif PLATFORM == 'osx':
signable_extensions.add('.dylib') signable_extensions.add('.dylib')
sign_command_base = [ sign_command_base = [
tool, tool,
'--keychain', '--keychain', os.path.expanduser('~/Library/Keychains/login.keychain'),
os.path.expanduser('~/Library/Keychains/login.keychain'),
'-vvvv', '-vvvv',
'--deep', '--deep',
'--force', '--force',
'--sign', '--sign', 'Developer ID Application: Hammer & Chisel Inc. (53Q6R32WPB)',
'Developer ID Application: Hammer & Chisel Inc. (53Q6R32WPB)',
] ]
else: else:
click.secho('Not signing things on this platform yet', fg='red') click.secho('Not signing things on this platform yet', fg='red')
@ -275,8 +232,6 @@ def libs(clean, static, shared, skip_formatter, just_release):
if IS_BUILD_MACHINE: if IS_BUILD_MACHINE:
just_release = True just_release = True
static_options['WARNINGS_AS_ERRORS'] = True
dynamic_options['WARNINGS_AS_ERRORS'] = True
if PLATFORM == 'win': if PLATFORM == 'win':
generator32 = 'Visual Studio 14 2015' generator32 = 'Visual Studio 14 2015'

View File

@ -93,7 +93,8 @@ And third is the `ACTIVITY_JOIN_REQUEST` event:
"username": "Mason", "username": "Mason",
"discriminator": "1337", "discriminator": "1337",
"avatar": "a_bab14f271d565501444b2ca3be944b25" "avatar": "a_bab14f271d565501444b2ca3be944b25"
} },
"secret": "e459ca99273f59909dd16ed97865f3ad"
}, },
"evt": "ACTIVITY_JOIN_REQUEST" "evt": "ACTIVITY_JOIN_REQUEST"
} }
@ -124,41 +125,3 @@ In order to receive these events, you need to [subscribe](https://discordapp.com
"cmd": "SUBSCRIBE" "cmd": "SUBSCRIBE"
} }
``` ```
To unsubscribe from these events, resend with the command `UNSUBSCRIBE`
## Responding
A discord user will request access to the game. If the ACTIVITY_JOIN_REQUEST has been subscribed too, the ACTIVITY_JOIN_REQUEST event will be sent to the host's game. Accept it with following model:
```json
{
"nonce": "5dc0c062-98c6-47a0-8922-15aerg126",
"cmd": "SEND_ACTIVITY_JOIN_INVITE",
"args":
{
"user_id": "53908232506183680"
}
}
```
To reject the request, use `CLOSE_ACTIVITY_REQUEST`:
```json
{
"nonce": "5dc0c062-98c6-47a0-8922-dasg256eafg",
"cmd": "CLOSE_ACTIVITY_REQUEST",
"args":
{
"user_id": "53908232506183680"
}
}
```
## Notes
Here are just some quick notes to help with some common troubleshooting problems.
* IPC will echo back every command you send as a response. Use this as a lock-step feature to avoid flooding messages. Can be used to validate messages such as the Presence or Subscribes.
* The pipe expects for frames to be written in a single byte array. You cannot do multiple `stream.Write(opcode);` `stream.Write(length);` as it will break the pipe. Instead create a buffer, write the data to the buffer, then send the entire buffer to the stream.
* Discord can be on any pipe ranging from `discord-ipc-0` to `discord-ipc-9`. It is a good idea to try and connect to each one and keeping the first one you connect too. For multiple clients (eg Discord and Canary), you might want to add a feature to manually select the pipe so you can more easily debug the application.
* All enums are `lower_snake_case`.
* The opcode and length in the header are `Little Endian Unsigned Integers (32bits)`. In some languages, you must convert them as they can be architecture specific.
* [Discord Rich Presence How-To](https://discordapp.com/developers/docs/rich-presence/how-to) contains a lot of the information this document doesn't. For example, it will tell you about the response payload.
* In the documentation, DISCORD_REPLY_IGNORE is just implemented the same as DISCORD_REPLY_NO.
* You can test the Join / Spectate feature by enabling them in your profile and whitelisting a test account. Use Canary to run 2 accounts on the same machine.

View File

@ -27,7 +27,7 @@ public class ScriptBatch
proc.StartInfo.EnvironmentVariables["PATH"] = newPath; proc.StartInfo.EnvironmentVariables["PATH"] = newPath;
#endif #endif
proc.StartInfo.FileName = "python"; proc.StartInfo.FileName = "python";
proc.StartInfo.Arguments = "build.py unity"; proc.StartInfo.Arguments = "build.py for_unity";
proc.StartInfo.WorkingDirectory = "../.."; proc.StartInfo.WorkingDirectory = "../..";
proc.Start(); proc.Start();
proc.WaitForExit(); proc.WaitForExit();
@ -46,8 +46,8 @@ public class ScriptBatch
string[] srcDlls = { "../../builds/install/osx-dynamic/lib/libdiscord-rpc.dylib" }; string[] srcDlls = { "../../builds/install/osx-dynamic/lib/libdiscord-rpc.dylib" };
#else #else
string[] dstDirs = { "Assets/Plugins", "Assets/Plugins/x86", "Assets/Plugins/x86_64" }; string[] dstDirs = { "Assets/Plugins", "Assets/Plugins/x86", "Assets/Plugins/x86_64" };
string[] dstDlls = { "Assets/Plugins/discord-rpc.so" }; string[] dstDlls = { "Assets/Plugins/x86/discord-rpc.so", "Assets/Plugins/x86_64/discord-rpc.so" };
string[] srcDlls = { "../../builds/install/linux-dynamic/lib/libdiscord-rpc.so" }; string[] srcDlls = { "../../builds/install/linux-dynamic/bin/discord-rpc.dll", "../../builds/install/win64-dynamic/bin/discord-rpc.dll" };
#endif #endif
Debug.Assert(dstDlls.Length == srcDlls.Length); Debug.Assert(dstDlls.Length == srcDlls.Length);

View File

@ -7,15 +7,16 @@ public class DiscordJoinEvent : UnityEngine.Events.UnityEvent<string> { }
public class DiscordSpectateEvent : UnityEngine.Events.UnityEvent<string> { } public class DiscordSpectateEvent : UnityEngine.Events.UnityEvent<string> { }
[System.Serializable] [System.Serializable]
public class DiscordJoinRequestEvent : UnityEngine.Events.UnityEvent<DiscordRpc.DiscordUser> { } public class DiscordJoinRequestEvent : UnityEngine.Events.UnityEvent<DiscordRpc.JoinRequest> { }
public class DiscordController : MonoBehaviour public class DiscordController : MonoBehaviour
{ {
public DiscordRpc.RichPresence presence = new DiscordRpc.RichPresence(); public DiscordRpc.RichPresence presence;
public string applicationId; public string applicationId;
public string optionalSteamId; public string optionalSteamId;
public int callbackCalls;
public int clickCounter; public int clickCounter;
public DiscordRpc.DiscordUser joinRequest; public DiscordRpc.JoinRequest joinRequest;
public UnityEngine.Events.UnityEvent onConnect; public UnityEngine.Events.UnityEvent onConnect;
public UnityEngine.Events.UnityEvent onDisconnect; public UnityEngine.Events.UnityEvent onDisconnect;
public UnityEngine.Events.UnityEvent hasResponded; public UnityEngine.Events.UnityEvent hasResponded;
@ -31,13 +32,8 @@ public class DiscordController : MonoBehaviour
clickCounter++; clickCounter++;
presence.details = string.Format("Button clicked {0} times", clickCounter); presence.details = string.Format("Button clicked {0} times", clickCounter);
presence.joinSecret = "aSecret";
presence.partyId = "aPartyId";
presence.partySize = 1;
presence.partyMax = 3;
presence.partyPrivacy = DiscordRpc.PartyPrivacy.Public;
DiscordRpc.UpdatePresence(presence); DiscordRpc.UpdatePresence(ref presence);
} }
public void RequestRespondYes() public void RequestRespondYes()
@ -54,37 +50,43 @@ public class DiscordController : MonoBehaviour
hasResponded.Invoke(); hasResponded.Invoke();
} }
public void ReadyCallback(ref DiscordRpc.DiscordUser connectedUser) public void ReadyCallback()
{ {
Debug.Log(string.Format("Discord: connected to {0}#{1}: {2}", connectedUser.username, connectedUser.discriminator, connectedUser.userId)); ++callbackCalls;
Debug.Log("Discord: ready");
onConnect.Invoke(); onConnect.Invoke();
} }
public void DisconnectedCallback(int errorCode, string message) public void DisconnectedCallback(int errorCode, string message)
{ {
++callbackCalls;
Debug.Log(string.Format("Discord: disconnect {0}: {1}", errorCode, message)); Debug.Log(string.Format("Discord: disconnect {0}: {1}", errorCode, message));
onDisconnect.Invoke(); onDisconnect.Invoke();
} }
public void ErrorCallback(int errorCode, string message) public void ErrorCallback(int errorCode, string message)
{ {
++callbackCalls;
Debug.Log(string.Format("Discord: error {0}: {1}", errorCode, message)); Debug.Log(string.Format("Discord: error {0}: {1}", errorCode, message));
} }
public void JoinCallback(string secret) public void JoinCallback(string secret)
{ {
++callbackCalls;
Debug.Log(string.Format("Discord: join ({0})", secret)); Debug.Log(string.Format("Discord: join ({0})", secret));
onJoin.Invoke(secret); onJoin.Invoke(secret);
} }
public void SpectateCallback(string secret) public void SpectateCallback(string secret)
{ {
++callbackCalls;
Debug.Log(string.Format("Discord: spectate ({0})", secret)); Debug.Log(string.Format("Discord: spectate ({0})", secret));
onSpectate.Invoke(secret); onSpectate.Invoke(secret);
} }
public void RequestCallback(ref DiscordRpc.DiscordUser request) public void RequestCallback(ref DiscordRpc.JoinRequest request)
{ {
++callbackCalls;
Debug.Log(string.Format("Discord: join request {0}#{1}: {2}", request.username, request.discriminator, request.userId)); Debug.Log(string.Format("Discord: join request {0}#{1}: {2}", request.username, request.discriminator, request.userId));
joinRequest = request; joinRequest = request;
onJoinRequest.Invoke(request); onJoinRequest.Invoke(request);
@ -102,8 +104,10 @@ public class DiscordController : MonoBehaviour
void OnEnable() void OnEnable()
{ {
Debug.Log("Discord: init"); Debug.Log("Discord: init");
callbackCalls = 0;
handlers = new DiscordRpc.EventHandlers(); handlers = new DiscordRpc.EventHandlers();
handlers.readyCallback += ReadyCallback; handlers.readyCallback = ReadyCallback;
handlers.disconnectedCallback += DisconnectedCallback; handlers.disconnectedCallback += DisconnectedCallback;
handlers.errorCallback += ErrorCallback; handlers.errorCallback += ErrorCallback;
handlers.joinCallback += JoinCallback; handlers.joinCallback += JoinCallback;

View File

@ -1,70 +1,57 @@
using System; using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using AOT;
public class DiscordRpc public class DiscordRpc
{ {
[MonoPInvokeCallback(typeof(OnReadyInfo))] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public static void ReadyCallback(ref DiscordUser connectedUser) { Callbacks.readyCallback(ref connectedUser); } public delegate void ReadyCallback();
public delegate void OnReadyInfo(ref DiscordUser connectedUser);
[MonoPInvokeCallback(typeof(OnDisconnectedInfo))] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public static void DisconnectedCallback(int errorCode, string message) { Callbacks.disconnectedCallback(errorCode, message); } public delegate void DisconnectedCallback(int errorCode, string message);
public delegate void OnDisconnectedInfo(int errorCode, string message);
[MonoPInvokeCallback(typeof(OnErrorInfo))] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public static void ErrorCallback(int errorCode, string message) { Callbacks.errorCallback(errorCode, message); } public delegate void ErrorCallback(int errorCode, string message);
public delegate void OnErrorInfo(int errorCode, string message);
[MonoPInvokeCallback(typeof(OnJoinInfo))] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public static void JoinCallback(string secret) { Callbacks.joinCallback(secret); } public delegate void JoinCallback(string secret);
public delegate void OnJoinInfo(string secret);
[MonoPInvokeCallback(typeof(OnSpectateInfo))] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public static void SpectateCallback(string secret) { Callbacks.spectateCallback(secret); } public delegate void SpectateCallback(string secret);
public delegate void OnSpectateInfo(string secret);
[MonoPInvokeCallback(typeof(OnRequestInfo))] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public static void RequestCallback(ref DiscordUser request) { Callbacks.requestCallback(ref request); } public delegate void RequestCallback(ref JoinRequest request);
public delegate void OnRequestInfo(ref DiscordUser request);
static EventHandlers Callbacks { get; set; }
public struct EventHandlers public struct EventHandlers
{ {
public OnReadyInfo readyCallback; public ReadyCallback readyCallback;
public OnDisconnectedInfo disconnectedCallback; public DisconnectedCallback disconnectedCallback;
public OnErrorInfo errorCallback; public ErrorCallback errorCallback;
public OnJoinInfo joinCallback; public JoinCallback joinCallback;
public OnSpectateInfo spectateCallback; public SpectateCallback spectateCallback;
public OnRequestInfo requestCallback; public RequestCallback requestCallback;
} }
[Serializable, StructLayout(LayoutKind.Sequential)] [System.Serializable]
public struct RichPresenceStruct public struct RichPresence
{ {
public IntPtr state; /* max 128 bytes */ public string state; /* max 128 bytes */
public IntPtr details; /* max 128 bytes */ public string details; /* max 128 bytes */
public long startTimestamp; public long startTimestamp;
public long endTimestamp; public long endTimestamp;
public IntPtr largeImageKey; /* max 32 bytes */ public string largeImageKey; /* max 32 bytes */
public IntPtr largeImageText; /* max 128 bytes */ public string largeImageText; /* max 128 bytes */
public IntPtr smallImageKey; /* max 32 bytes */ public string smallImageKey; /* max 32 bytes */
public IntPtr smallImageText; /* max 128 bytes */ public string smallImageText; /* max 128 bytes */
public IntPtr partyId; /* max 128 bytes */ public string partyId; /* max 128 bytes */
public int partySize; public int partySize;
public int partyMax; public int partyMax;
public int partyPrivacy; public string matchSecret; /* max 128 bytes */
public IntPtr matchSecret; /* max 128 bytes */ public string joinSecret; /* max 128 bytes */
public IntPtr joinSecret; /* max 128 bytes */ public string spectateSecret; /* max 128 bytes */
public IntPtr spectateSecret; /* max 128 bytes */
public bool instance; public bool instance;
} }
[Serializable] [System.Serializable]
public struct DiscordUser public struct JoinRequest
{ {
public string userId; public string userId;
public string username; public string username;
@ -79,29 +66,8 @@ public class DiscordRpc
Ignore = 2 Ignore = 2
} }
public enum PartyPrivacy
{
Private = 0,
Public = 1
}
public static void Initialize(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId)
{
Callbacks = handlers;
EventHandlers staticEventHandlers = new EventHandlers();
staticEventHandlers.readyCallback += DiscordRpc.ReadyCallback;
staticEventHandlers.disconnectedCallback += DiscordRpc.DisconnectedCallback;
staticEventHandlers.errorCallback += DiscordRpc.ErrorCallback;
staticEventHandlers.joinCallback += DiscordRpc.JoinCallback;
staticEventHandlers.spectateCallback += DiscordRpc.SpectateCallback;
staticEventHandlers.requestCallback += DiscordRpc.RequestCallback;
InitializeInternal(applicationId, ref staticEventHandlers, autoRegister, optionalSteamId);
}
[DllImport("discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)] [DllImport("discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)]
static extern void InitializeInternal(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId); public static extern void Initialize(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId);
[DllImport("discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)] [DllImport("discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)]
public static extern void Shutdown(); public static extern void Shutdown();
@ -110,122 +76,12 @@ public class DiscordRpc
public static extern void RunCallbacks(); public static extern void RunCallbacks();
[DllImport("discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)] [DllImport("discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)]
private static extern void UpdatePresenceNative(ref RichPresenceStruct presence); public static extern void UpdatePresence(ref RichPresence presence);
[DllImport("discord-rpc", EntryPoint = "Discord_ClearPresence", CallingConvention = CallingConvention.Cdecl)] [DllImport("discord-rpc", EntryPoint = "Discord_ClearPresence", CallingConvention = CallingConvention.Cdecl)]
public static extern void ClearPresence(); public static extern void ClearPresence();
[DllImport("discord-rpc", EntryPoint = "Discord_Respond", CallingConvention = CallingConvention.Cdecl)] [DllImport("discord-rpc", EntryPoint = "Discord_Respond", CallingConvention = CallingConvention.Cdecl)]
public static extern void Respond(string userId, Reply reply); public static extern void Respond(string userId, Reply reply);
[DllImport("discord-rpc", EntryPoint = "Discord_UpdateHandlers", CallingConvention = CallingConvention.Cdecl)]
public static extern void UpdateHandlers(ref EventHandlers handlers);
public static void UpdatePresence(RichPresence presence)
{
var presencestruct = presence.GetStruct();
UpdatePresenceNative(ref presencestruct);
presence.FreeMem();
} }
public class RichPresence
{
private RichPresenceStruct _presence;
private readonly List<IntPtr> _buffers = new List<IntPtr>(10);
public string state; /* max 128 bytes */
public string details; /* max 128 bytes */
public long startTimestamp;
public long endTimestamp;
public string largeImageKey; /* max 32 bytes */
public string largeImageText; /* max 128 bytes */
public string smallImageKey; /* max 32 bytes */
public string smallImageText; /* max 128 bytes */
public string partyId; /* max 128 bytes */
public int partySize;
public int partyMax;
public PartyPrivacy partyPrivacy;
public string matchSecret; /* max 128 bytes */
public string joinSecret; /* max 128 bytes */
public string spectateSecret; /* max 128 bytes */
public bool instance;
/// <summary>
/// Get the <see cref="RichPresenceStruct"/> reprensentation of this instance
/// </summary>
/// <returns><see cref="RichPresenceStruct"/> reprensentation of this instance</returns>
internal RichPresenceStruct GetStruct()
{
if (_buffers.Count > 0)
{
FreeMem();
}
_presence.state = StrToPtr(state);
_presence.details = StrToPtr(details);
_presence.startTimestamp = startTimestamp;
_presence.endTimestamp = endTimestamp;
_presence.largeImageKey = StrToPtr(largeImageKey);
_presence.largeImageText = StrToPtr(largeImageText);
_presence.smallImageKey = StrToPtr(smallImageKey);
_presence.smallImageText = StrToPtr(smallImageText);
_presence.partyId = StrToPtr(partyId);
_presence.partySize = partySize;
_presence.partyMax = partyMax;
_presence.partyPrivacy = (int)partyPrivacy;
_presence.matchSecret = StrToPtr(matchSecret);
_presence.joinSecret = StrToPtr(joinSecret);
_presence.spectateSecret = StrToPtr(spectateSecret);
_presence.instance = instance;
return _presence;
}
/// <summary>
/// Returns a pointer to a representation of the given string with a size of maxbytes
/// </summary>
/// <param name="input">String to convert</param>
/// <returns>Pointer to the UTF-8 representation of <see cref="input"/></returns>
private IntPtr StrToPtr(string input)
{
if (string.IsNullOrEmpty(input)) return IntPtr.Zero;
var convbytecnt = Encoding.UTF8.GetByteCount(input);
var buffer = Marshal.AllocHGlobal(convbytecnt + 1);
for (int i = 0; i < convbytecnt + 1; i++)
{
Marshal.WriteByte(buffer, i, 0);
}
_buffers.Add(buffer);
Marshal.Copy(Encoding.UTF8.GetBytes(input), 0, buffer, convbytecnt);
return buffer;
}
/// <summary>
/// Convert string to UTF-8 and add null termination
/// </summary>
/// <param name="toconv">string to convert</param>
/// <returns>UTF-8 representation of <see cref="toconv"/> with added null termination</returns>
private static string StrToUtf8NullTerm(string toconv)
{
var str = toconv.Trim();
var bytes = Encoding.Default.GetBytes(str);
if (bytes.Length > 0 && bytes[bytes.Length - 1] != 0)
{
str += "\0\0";
}
return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(str));
}
/// <summary>
/// Free the allocated memory for conversion to <see cref="RichPresenceStruct"/>
/// </summary>
internal void FreeMem()
{
for (var i = _buffers.Count - 1; i >= 0; i--)
{
Marshal.FreeHGlobal(_buffers[i]);
_buffers.RemoveAt(i);
}
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
This is a simple example in C of using the rich presence API asynchronously. This is a simple example in C of using the rich presence API asyncronously.
*/ */
#define _CRT_SECURE_NO_WARNINGS /* thanks Microsoft */ #define _CRT_SECURE_NO_WARNINGS /* thanks Microsoft */
@ -9,7 +9,7 @@
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include "discord_rpc.h" #include "discord-rpc.h"
static const char* APPLICATION_ID = "345229890980937739"; static const char* APPLICATION_ID = "345229890980937739";
static int FrustrationLevel = 0; static int FrustrationLevel = 0;
@ -47,24 +47,19 @@ static void updateDiscordPresence()
discordPresence.partyId = "party1234"; discordPresence.partyId = "party1234";
discordPresence.partySize = 1; discordPresence.partySize = 1;
discordPresence.partyMax = 6; discordPresence.partyMax = 6;
discordPresence.partyPrivacy = DISCORD_PARTY_PUBLIC;
discordPresence.matchSecret = "xyzzy"; discordPresence.matchSecret = "xyzzy";
discordPresence.joinSecret = "join"; discordPresence.joinSecret = "join";
discordPresence.spectateSecret = "look"; discordPresence.spectateSecret = "look";
discordPresence.instance = 0; discordPresence.instance = 0;
Discord_UpdatePresence(&discordPresence); Discord_UpdatePresence(&discordPresence);
} } else {
else {
Discord_ClearPresence(); Discord_ClearPresence();
} }
} }
static void handleDiscordReady(const DiscordUser* connectedUser) static void handleDiscordReady(void)
{ {
printf("\nDiscord: connected to user %s#%s - %s\n", printf("\nDiscord: ready\n");
connectedUser->username,
connectedUser->discriminator,
connectedUser->userId);
} }
static void handleDiscordDisconnected(int errcode, const char* message) static void handleDiscordDisconnected(int errcode, const char* message)
@ -87,13 +82,13 @@ static void handleDiscordSpectate(const char* secret)
printf("\nDiscord: spectate (%s)\n", secret); printf("\nDiscord: spectate (%s)\n", secret);
} }
static void handleDiscordJoinRequest(const DiscordUser* request) static void handleDiscordJoinRequest(const DiscordJoinRequest* request)
{ {
int response = -1; int response = -1;
char yn[4]; char yn[4];
printf("\nDiscord: join request from %s#%s - %s\n", printf("\nDiscord: join request from %s - %s - %s\n",
request->username, request->username,
request->discriminator, request->avatar,
request->userId); request->userId);
do { do {
printf("Accept? (y/n)"); printf("Accept? (y/n)");
@ -157,8 +152,7 @@ static void gameLoop()
if (SendPresence) { if (SendPresence) {
printf("Clearing presence information.\n"); printf("Clearing presence information.\n");
SendPresence = 0; SendPresence = 0;
} } else {
else {
printf("Restoring presence information.\n"); printf("Restoring presence information.\n");
SendPresence = 1; SendPresence = 1;
} }

View File

@ -12,26 +12,19 @@ void FDiscordRpcModule::StartupModule()
#if defined(DISCORD_DYNAMIC_LIB) #if defined(DISCORD_DYNAMIC_LIB)
// Get the base directory of this plugin // Get the base directory of this plugin
FString BaseDir = IPluginManager::Get().FindPlugin("DiscordRpc")->GetBaseDir(); FString BaseDir = IPluginManager::Get().FindPlugin("DiscordRpc")->GetBaseDir();
const FString SDKDir = const FString SDKDir = FPaths::Combine(*BaseDir, TEXT("Source"), TEXT("ThirdParty"), TEXT("DiscordRpcLibrary"));
FPaths::Combine(*BaseDir, TEXT("Source"), TEXT("ThirdParty"), TEXT("DiscordRpcLibrary"));
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
const FString LibName = TEXT("discord-rpc"); const FString LibName = TEXT("discord-rpc");
const FString LibDir = FPaths::Combine(*SDKDir, TEXT("Win64")); const FString LibDir = FPaths::Combine(*SDKDir, TEXT("Win64"));
if (!LoadDependency(LibDir, LibName, DiscordRpcLibraryHandle)) { if (!LoadDependency(LibDir, LibName, DiscordRpcLibraryHandle)) {
FMessageDialog::Open( FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT(LOCTEXT_NAMESPACE, "Failed to load DiscordRpc plugin. Plug-in will not be functional."));
EAppMsgType::Ok,
LOCTEXT(LOCTEXT_NAMESPACE,
"Failed to load DiscordRpc plugin. Plug-in will not be functional."));
FreeDependency(DiscordRpcLibraryHandle); FreeDependency(DiscordRpcLibraryHandle);
} }
#elif PLATFORM_MAC #elif PLATFORM_MAC
const FString LibName = TEXT("libdiscord-rpc"); const FString LibName = TEXT("libdiscord-rpc");
const FString LibDir = FPaths::Combine(*SDKDir, TEXT("Mac")); const FString LibDir = FPaths::Combine(*SDKDir, TEXT("Mac"));
if (!LoadDependency(LibDir, LibName, DiscordRpcLibraryHandle)) { if (!LoadDependency(LibDir, LibName, DiscordRpcLibraryHandle)) {
FMessageDialog::Open( FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT(LOCTEXT_NAMESPACE, "Failed to load DiscordRpc plugin. Plug-in will not be functional."));
EAppMsgType::Ok,
LOCTEXT(LOCTEXT_NAMESPACE,
"Failed to load DiscordRpc plugin. Plug-in will not be functional."));
FreeDependency(DiscordRpcLibraryHandle); FreeDependency(DiscordRpcLibraryHandle);
} }
#endif #endif
@ -56,7 +49,8 @@ bool FDiscordRpcModule::LoadDependency(const FString& Dir, const FString& Name,
Handle = FPlatformProcess::GetDllHandle(*Path); Handle = FPlatformProcess::GetDllHandle(*Path);
if (Handle == nullptr) { if (Handle == nullptr)
{
return false; return false;
} }
@ -65,7 +59,8 @@ bool FDiscordRpcModule::LoadDependency(const FString& Dir, const FString& Name,
void FDiscordRpcModule::FreeDependency(void*& Handle) void FDiscordRpcModule::FreeDependency(void*& Handle)
{ {
if (Handle != nullptr) { if (Handle != nullptr)
{
FPlatformProcess::FreeDllHandle(Handle); FPlatformProcess::FreeDllHandle(Handle);
Handle = nullptr; Handle = nullptr;
} }

View File

@ -1,27 +1,17 @@
#include "DiscordRpcPrivatePCH.h" #include "DiscordRpcPrivatePCH.h"
#include "DiscordRpcBlueprint.h" #include "DiscordRpcBlueprint.h"
#include "discord_rpc.h" #include "discord-rpc.h"
DEFINE_LOG_CATEGORY(Discord) DEFINE_LOG_CATEGORY(Discord)
static UDiscordRpc* self = nullptr; static UDiscordRpc* self = nullptr;
static void ReadyHandler(const DiscordUser* connectedUser) static void ReadyHandler()
{ {
FDiscordUserData ud; UE_LOG(Discord, Log, TEXT("Discord connected"));
ud.userId = ANSI_TO_TCHAR(connectedUser->userId);
ud.username = ANSI_TO_TCHAR(connectedUser->username);
ud.discriminator = ANSI_TO_TCHAR(connectedUser->discriminator);
ud.avatar = ANSI_TO_TCHAR(connectedUser->avatar);
UE_LOG(Discord,
Log,
TEXT("Discord connected to %s - %s#%s"),
*ud.userId,
*ud.username,
*ud.discriminator);
if (self) { if (self) {
self->IsConnected = true; self->IsConnected = true;
self->OnConnected.Broadcast(ud); self->OnConnected.Broadcast();
} }
} }
@ -62,21 +52,16 @@ static void SpectateGameHandler(const char* spectateSecret)
} }
} }
static void JoinRequestHandler(const DiscordUser* request) static void JoinRequestHandler(const DiscordJoinRequest* request)
{ {
FDiscordUserData ud; FDiscordJoinRequestData jr;
ud.userId = ANSI_TO_TCHAR(request->userId); jr.userId = ANSI_TO_TCHAR(request->userId);
ud.username = ANSI_TO_TCHAR(request->username); jr.username = ANSI_TO_TCHAR(request->username);
ud.discriminator = ANSI_TO_TCHAR(request->discriminator); jr.discriminator = ANSI_TO_TCHAR(request->discriminator);
ud.avatar = ANSI_TO_TCHAR(request->avatar); jr.avatar = ANSI_TO_TCHAR(request->avatar);
UE_LOG(Discord, UE_LOG(Discord, Log, TEXT("Discord join request from %s#%s"), *jr.username, *jr.discriminator);
Log,
TEXT("Discord join request from %s - %s#%s"),
*ud.userId,
*ud.username,
*ud.discriminator);
if (self) { if (self) {
self->OnJoinRequest.Broadcast(ud); self->OnJoinRequest.Broadcast(jr);
} }
} }
@ -149,11 +134,11 @@ void UDiscordRpc::UpdatePresence()
auto spectateSecret = StringCast<ANSICHAR>(*RichPresence.spectateSecret); auto spectateSecret = StringCast<ANSICHAR>(*RichPresence.spectateSecret);
rp.spectateSecret = spectateSecret.Get(); rp.spectateSecret = spectateSecret.Get();
rp.startTimestamp = RichPresence.startTimestamp; rp.startTimestamp = RichPresence.startTimestamp;
rp.endTimestamp = RichPresence.endTimestamp; rp.endTimestamp = RichPresence.endTimestamp;
rp.partySize = RichPresence.partySize; rp.partySize = RichPresence.partySize;
rp.partyMax = RichPresence.partyMax; rp.partyMax = RichPresence.partyMax;
rp.partyPrivacy = (int)RichPresence.partyPrivacy;
rp.instance = RichPresence.instance; rp.instance = RichPresence.instance;
Discord_UpdatePresence(&rp); Discord_UpdatePresence(&rp);
@ -163,10 +148,3 @@ void UDiscordRpc::ClearPresence()
{ {
Discord_ClearPresence(); Discord_ClearPresence();
} }
void UDiscordRpc::Respond(const FString& userId, int reply)
{
UE_LOG(Discord, Log, TEXT("Responding %d to join request from %s"), reply, *userId);
FTCHARToUTF8 utf8_userid(*userId);
Discord_Respond(utf8_userid.Get(), reply);
}

View File

@ -11,7 +11,7 @@
* Ask to join callback data * Ask to join callback data
*/ */
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FDiscordUserData { struct FDiscordJoinRequestData {
GENERATED_USTRUCT_BODY() GENERATED_USTRUCT_BODY()
UPROPERTY(BlueprintReadOnly) UPROPERTY(BlueprintReadOnly)
@ -24,35 +24,15 @@ struct FDiscordUserData {
FString avatar; FString avatar;
}; };
/**
* Valid response codes for Respond function
*/
UENUM(BlueprintType)
enum class EDiscordJoinResponseCodes : uint8
{
DISCORD_REPLY_NO UMETA(DisplayName="No"),
DISCORD_REPLY_YES UMETA(DisplayName="Yes"),
DISCORD_REPLY_IGNORE UMETA(DisplayName="Ignore")
};
/**
* Valid party privacy values
*/
UENUM(BlueprintType)
enum class EDiscordPartyPrivacy: uint8
{
DISCORD_PARTY_PRIVATE UMETA(DisplayName="Private"),
DISCORD_PARTY_PUBLIC UMETA(DisplayName="Public")
};
DECLARE_LOG_CATEGORY_EXTERN(Discord, Log, All); DECLARE_LOG_CATEGORY_EXTERN(Discord, Log, All);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordConnected, const FDiscordUserData&, joinRequest); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDiscordConnected);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordDisconnected, int, errorCode, const FString&, errorMessage); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordDisconnected, int, errorCode, const FString&, errorMessage);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordErrored, int, errorCode, const FString&, errorMessage); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordErrored, int, errorCode, const FString&, errorMessage);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoin, const FString&, joinSecret); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoin, const FString&, joinSecret);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordSpectate, const FString&, spectateSecret); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordSpectate, const FString&, spectateSecret);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoinRequest, const FDiscordUserData&, joinRequest); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoinRequest, const FDiscordJoinRequestData&, joinRequest);
// clang-format on // clang-format on
@ -87,8 +67,6 @@ struct FDiscordRichPresence {
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
int partyMax; int partyMax;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
EDiscordPartyPrivacy partyPrivacy;
UPROPERTY(BlueprintReadWrite)
FString matchSecret; FString matchSecret;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
FString joinSecret; FString joinSecret;
@ -133,11 +111,6 @@ public:
Category = "Discord") Category = "Discord")
void ClearPresence(); void ClearPresence();
UFUNCTION(BlueprintCallable,
meta = (DisplayName = "Respond to join request", Keywords = "Discord rpc"),
Category = "Discord")
void Respond(const FString& userId, int reply);
UPROPERTY(BlueprintReadOnly, UPROPERTY(BlueprintReadOnly,
meta = (DisplayName = "Is Discord connected", Keywords = "Discord rpc"), meta = (DisplayName = "Is Discord connected", Keywords = "Discord rpc"),
Category = "Discord") Category = "Discord")

View File

@ -35,34 +35,31 @@ typedef struct DiscordRichPresence {
const char* partyId; /* max 128 bytes */ const char* partyId; /* max 128 bytes */
int partySize; int partySize;
int partyMax; int partyMax;
int partyPrivacy;
const char* matchSecret; /* max 128 bytes */ const char* matchSecret; /* max 128 bytes */
const char* joinSecret; /* max 128 bytes */ const char* joinSecret; /* max 128 bytes */
const char* spectateSecret; /* max 128 bytes */ const char* spectateSecret; /* max 128 bytes */
int8_t instance; int8_t instance;
} DiscordRichPresence; } DiscordRichPresence;
typedef struct DiscordUser { typedef struct DiscordJoinRequest {
const char* userId; const char* userId;
const char* username; const char* username;
const char* discriminator; const char* discriminator;
const char* avatar; const char* avatar;
} DiscordUser; } DiscordJoinRequest;
typedef struct DiscordEventHandlers { typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser* request); void (*ready)(void);
void (*disconnected)(int errorCode, const char* message); void (*disconnected)(int errorCode, const char* message);
void (*errored)(int errorCode, const char* message); void (*errored)(int errorCode, const char* message);
void (*joinGame)(const char* joinSecret); void (*joinGame)(const char* joinSecret);
void (*spectateGame)(const char* spectateSecret); void (*spectateGame)(const char* spectateSecret);
void (*joinRequest)(const DiscordUser* request); void (*joinRequest)(const DiscordJoinRequest* request);
} DiscordEventHandlers; } DiscordEventHandlers;
#define DISCORD_REPLY_NO 0 #define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1 #define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2 #define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
DISCORD_EXPORT void Discord_Initialize(const char* applicationId, DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers, DiscordEventHandlers* handlers,
@ -83,8 +80,6 @@ DISCORD_EXPORT void Discord_ClearPresence(void);
DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
#ifdef __cplusplus #ifdef __cplusplus
} /* extern "C" */ } /* extern "C" */
#endif #endif

View File

@ -1,26 +0,0 @@
#pragma once
#if defined(DISCORD_DYNAMIC_LIB)
#if defined(_WIN32)
#if defined(DISCORD_BUILDING_SDK)
#define DISCORD_EXPORT __declspec(dllexport)
#else
#define DISCORD_EXPORT __declspec(dllimport)
#endif
#else
#define DISCORD_EXPORT __attribute__((visibility("default")))
#endif
#else
#define DISCORD_EXPORT
#endif
#ifdef __cplusplus
extern "C" {
#endif
DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command);
DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
#ifdef __cplusplus
}
#endif

View File

@ -2,14 +2,13 @@ include_directories(${PROJECT_SOURCE_DIR}/include)
option(ENABLE_IO_THREAD "Start up a separate I/O thread, otherwise I'd need to call an update function" ON) option(ENABLE_IO_THREAD "Start up a separate I/O thread, otherwise I'd need to call an update function" ON)
option(USE_STATIC_CRT "Use /MT[d] for dynamic library" OFF) option(USE_STATIC_CRT "Use /MT[d] for dynamic library" OFF)
option(WARNINGS_AS_ERRORS "When enabled, compiles with `-Werror` (on *nix platforms)." OFF)
set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD 14)
set(BASE_RPC_SRC set(BASE_RPC_SRC
${PROJECT_SOURCE_DIR}/include/discord_rpc.h ${PROJECT_SOURCE_DIR}/include/discord-rpc.h
discord_rpc.cpp discord-rpc.cpp
${PROJECT_SOURCE_DIR}/include/discord_register.h discord_register.h
rpc_connection.h rpc_connection.h
rpc_connection.cpp rpc_connection.cpp
serialization.h serialization.h
@ -56,7 +55,7 @@ if(WIN32)
/wd5027 # move assignment operator was implicitly defined as deleted /wd5027 # move assignment operator was implicitly defined as deleted
) )
endif(MSVC) endif(MSVC)
target_link_libraries(discord-rpc PRIVATE psapi advapi32) target_link_libraries(discord-rpc PRIVATE psapi)
endif(WIN32) endif(WIN32)
if(UNIX) if(UNIX)
@ -72,23 +71,12 @@ if(UNIX)
add_library(discord-rpc ${BASE_RPC_SRC}) add_library(discord-rpc ${BASE_RPC_SRC})
target_link_libraries(discord-rpc PUBLIC pthread) target_link_libraries(discord-rpc PUBLIC pthread)
if (APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit, -mmacosx-version-min=10.10")
endif (APPLE)
target_compile_options(discord-rpc PRIVATE target_compile_options(discord-rpc PRIVATE
-g -g
-Wall -Wall
-Wextra -Wextra
-Wpedantic -Wpedantic
) -Werror
if (${WARNINGS_AS_ERRORS})
target_compile_options(discord-rpc PRIVATE -Werror)
endif (${WARNINGS_AS_ERRORS})
target_compile_options(discord-rpc PRIVATE
-Wno-unknown-pragmas # pragma push thing doesn't work on clang -Wno-unknown-pragmas # pragma push thing doesn't work on clang
-Wno-old-style-cast # it's fine -Wno-old-style-cast # it's fine
-Wno-c++98-compat # that was almost 2 decades ago -Wno-c++98-compat # that was almost 2 decades ago
@ -141,7 +129,6 @@ install(
install( install(
FILES FILES
"../include/discord_rpc.h" "../include/discord-rpc.h"
"../include/discord_register.h"
DESTINATION "include" DESTINATION "include"
) )

View File

@ -118,8 +118,5 @@ bool BaseConnection::Read(void* data, size_t length)
} }
Close(); Close();
} }
else if (res == 0) {
Close();
}
return res == (int)length; return res == (int)length;
} }

View File

@ -1,4 +1,4 @@
#include "discord_rpc.h" #include "discord-rpc.h"
#include "backoff.h" #include "backoff.h"
#include "discord_register.h" #include "discord_register.h"
@ -32,7 +32,7 @@ struct QueuedMessage {
} }
}; };
struct User { struct JoinRequest {
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null // snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21 // terminator = 21
char userId[32]; char userId[32];
@ -47,14 +47,12 @@ struct User {
}; };
static RpcConnection* Connection{nullptr}; static RpcConnection* Connection{nullptr};
static DiscordEventHandlers QueuedHandlers{};
static DiscordEventHandlers Handlers{}; static DiscordEventHandlers Handlers{};
static std::atomic_bool WasJustConnected{false}; static std::atomic_bool WasJustConnected{false};
static std::atomic_bool WasJustDisconnected{false}; static std::atomic_bool WasJustDisconnected{false};
static std::atomic_bool GotErrorMessage{false}; static std::atomic_bool GotErrorMessage{false};
static std::atomic_bool WasJoinGame{false}; static std::atomic_bool WasJoinGame{false};
static std::atomic_bool WasSpectateGame{false}; static std::atomic_bool WasSpectateGame{false};
static std::atomic_bool UpdatePresence{false};
static char JoinGameSecret[256]; static char JoinGameSecret[256];
static char SpectateGameSecret[256]; static char SpectateGameSecret[256];
static int LastErrorCode{0}; static int LastErrorCode{0};
@ -62,11 +60,9 @@ static char LastErrorMessage[256];
static int LastDisconnectErrorCode{0}; static int LastDisconnectErrorCode{0};
static char LastDisconnectErrorMessage[256]; static char LastDisconnectErrorMessage[256];
static std::mutex PresenceMutex; static std::mutex PresenceMutex;
static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence{}; static QueuedMessage QueuedPresence{};
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue; static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<User, JoinQueueSize> JoinAskQueue; static MsgQueue<JoinRequest, JoinQueueSize> JoinAskQueue;
static User connectedUser;
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential // We want to auto connect, and retry on failure, but not as fast as possible. This does expoential
// backoff from 0.5 seconds to 1 minute // backoff from 0.5 seconds to 1 minute
@ -90,11 +86,10 @@ public:
keepRunning.store(true); keepRunning.store(true);
ioThread = std::thread([&]() { ioThread = std::thread([&]() {
const std::chrono::duration<int64_t, std::milli> maxWait{500LL}; const std::chrono::duration<int64_t, std::milli> maxWait{500LL};
Discord_UpdateConnection();
while (keepRunning.load()) { while (keepRunning.load()) {
Discord_UpdateConnection();
std::unique_lock<std::mutex> lock(waitForIOMutex); std::unique_lock<std::mutex> lock(waitForIOMutex);
waitForIOActivity.wait_for(lock, maxWait); waitForIOActivity.wait_for(lock, maxWait);
Discord_UpdateConnection();
} }
}); });
} }
@ -120,7 +115,7 @@ public:
void Notify() {} void Notify() {}
}; };
#endif // DISCORD_DISABLE_IO_THREAD #endif // DISCORD_DISABLE_IO_THREAD
static IoThreadHolder* IoThread{nullptr}; static IoThreadHolder IoThread;
static void UpdateReconnectTime() static void UpdateReconnectTime()
{ {
@ -215,17 +210,17 @@ static void Discord_UpdateConnection(void)
} }
// writes // writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) { if (QueuedPresence.length) {
QueuedMessage local; QueuedMessage local;
{ PresenceMutex.lock();
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence); local.Copy(QueuedPresence);
} QueuedPresence.length = 0;
PresenceMutex.unlock();
if (!Connection->Write(local.buffer, local.length)) { if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue // if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex); PresenceMutex.lock();
QueuedPresence.Copy(local); QueuedPresence.Copy(local);
UpdatePresence.exchange(true); PresenceMutex.unlock();
} }
} }
@ -239,9 +234,7 @@ static void Discord_UpdateConnection(void)
static void SignalIOActivity() static void SignalIOActivity()
{ {
if (IoThread != nullptr) { IoThread.Notify();
IoThread->Notify();
}
} }
static bool RegisterForEvent(const char* evtName) static bool RegisterForEvent(const char* evtName)
@ -257,29 +250,11 @@ static bool RegisterForEvent(const char* evtName)
return false; return false;
} }
static bool DeregisterForEvent(const char* evtName)
{
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length =
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId, extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers, DiscordEventHandlers* handlers,
int autoRegister, int autoRegister,
const char* optionalSteamId) const char* optionalSteamId)
{ {
IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) {
return;
}
if (autoRegister) { if (autoRegister) {
if (optionalSteamId && optionalSteamId[0]) { if (optionalSteamId && optionalSteamId[0]) {
Discord_RegisterSteamGame(applicationId, optionalSteamId); Discord_RegisterSteamGame(applicationId, optionalSteamId);
@ -291,16 +266,10 @@ extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
Pid = GetProcessId(); Pid = GetProcessId();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (handlers) { if (handlers) {
QueuedHandlers = *handlers; Handlers = *handlers;
} }
else { else {
QueuedHandlers = {};
}
Handlers = {}; Handlers = {};
} }
@ -309,33 +278,21 @@ extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
} }
Connection = RpcConnection::Create(applicationId); Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](JsonDocument& readyMessage) { Connection->onConnect = []() {
Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true);
SignalIOActivity();
}
auto data = GetObjMember(&readyMessage, "data");
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
if (userId && username) {
StringCopy(connectedUser.userId, userId);
StringCopy(connectedUser.username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(connectedUser.discriminator, discriminator);
}
if (avatar) {
StringCopy(connectedUser.avatar, avatar);
}
else {
connectedUser.avatar[0] = 0;
}
}
WasJustConnected.exchange(true); WasJustConnected.exchange(true);
ReconnectTimeMs.reset(); ReconnectTimeMs.reset();
if (Handlers.joinGame) {
RegisterForEvent("ACTIVITY_JOIN");
}
if (Handlers.spectateGame) {
RegisterForEvent("ACTIVITY_SPECTATE");
}
if (Handlers.joinRequest) {
RegisterForEvent("ACTIVITY_JOIN_REQUEST");
}
}; };
Connection->onDisconnect = [](int err, const char* message) { Connection->onDisconnect = [](int err, const char* message) {
LastDisconnectErrorCode = err; LastDisconnectErrorCode = err;
@ -344,7 +301,7 @@ extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
UpdateReconnectTime(); UpdateReconnectTime();
}; };
IoThread->Start(); IoThread.Start();
} }
extern "C" DISCORD_EXPORT void Discord_Shutdown(void) extern "C" DISCORD_EXPORT void Discord_Shutdown(void)
@ -355,25 +312,16 @@ extern "C" DISCORD_EXPORT void Discord_Shutdown(void)
Connection->onConnect = nullptr; Connection->onConnect = nullptr;
Connection->onDisconnect = nullptr; Connection->onDisconnect = nullptr;
Handlers = {}; Handlers = {};
QueuedPresence.length = 0; IoThread.Stop();
UpdatePresence.exchange(false);
if (IoThread != nullptr) {
IoThread->Stop();
delete IoThread;
IoThread = nullptr;
}
RpcConnection::Destroy(Connection); RpcConnection::Destroy(Connection);
} }
extern "C" DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence) extern "C" DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence)
{ {
{ PresenceMutex.lock();
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.length = JsonWriteRichPresenceObj( QueuedPresence.length = JsonWriteRichPresenceObj(
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence); QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
UpdatePresence.exchange(true); PresenceMutex.unlock();
}
SignalIOActivity(); SignalIOActivity();
} }
@ -412,43 +360,26 @@ extern "C" DISCORD_EXPORT void Discord_RunCallbacks(void)
if (isConnected) { if (isConnected) {
// if we are connected, disconnect cb first // if we are connected, disconnect cb first
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) { if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
} }
} }
if (WasJustConnected.exchange(false)) { if (WasJustConnected.exchange(false) && Handlers.ready) {
std::lock_guard<std::mutex> guard(HandlerMutex); Handlers.ready();
if (Handlers.ready) {
DiscordUser du{connectedUser.userId,
connectedUser.username,
connectedUser.discriminator,
connectedUser.avatar};
Handlers.ready(&du);
}
} }
if (GotErrorMessage.exchange(false)) { if (GotErrorMessage.exchange(false) && Handlers.errored) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.errored) {
Handlers.errored(LastErrorCode, LastErrorMessage); Handlers.errored(LastErrorCode, LastErrorMessage);
} }
}
if (WasJoinGame.exchange(false)) { if (WasJoinGame.exchange(false) && Handlers.joinGame) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinGame) {
Handlers.joinGame(JoinGameSecret); Handlers.joinGame(JoinGameSecret);
} }
}
if (WasSpectateGame.exchange(false)) { if (WasSpectateGame.exchange(false) && Handlers.spectateGame) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.spectateGame) {
Handlers.spectateGame(SpectateGameSecret); Handlers.spectateGame(SpectateGameSecret);
} }
}
// Right now this batches up any requests and sends them all in a burst; I could imagine a world // Right now this batches up any requests and sends them all in a burst; I could imagine a world
// where the implementer would rather sequentially accept/reject each one before the next invite // where the implementer would rather sequentially accept/reject each one before the next invite
@ -457,48 +388,17 @@ extern "C" DISCORD_EXPORT void Discord_RunCallbacks(void)
// not it should be trivial for the implementer to make a queue themselves. // not it should be trivial for the implementer to make a queue themselves.
while (JoinAskQueue.HavePendingSends()) { while (JoinAskQueue.HavePendingSends()) {
auto req = JoinAskQueue.GetNextSendMessage(); auto req = JoinAskQueue.GetNextSendMessage();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) { if (Handlers.joinRequest) {
DiscordUser du{req->userId, req->username, req->discriminator, req->avatar}; DiscordJoinRequest djr{req->userId, req->username, req->discriminator, req->avatar};
Handlers.joinRequest(&du); Handlers.joinRequest(&djr);
}
} }
JoinAskQueue.CommitSend(); JoinAskQueue.CommitSend();
} }
if (!isConnected) { if (!isConnected) {
// if we are not connected, disconnect message last // if we are not connected, disconnect message last
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) { if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
} }
} }
} }
extern "C" DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* newHandlers)
{
if (newHandlers) {
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
if (!Handlers.handler_name && newHandlers->handler_name) { \
RegisterForEvent(event); \
} \
else if (Handlers.handler_name && !newHandlers->handler_name) { \
DeregisterForEvent(event); \
}
std::lock_guard<std::mutex> guard(HandlerMutex);
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
#undef HANDLE_EVENT_REGISTRATION
Handlers = *newHandlers;
}
else {
std::lock_guard<std::mutex> guard(HandlerMutex);
Handlers = {};
}
return;
}

12
src/discord_register.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char* applicationId, const char* command);
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
#ifdef __cplusplus
}
#endif

View File

@ -1,5 +1,4 @@
#include "discord_rpc.h" #include "discord-rpc.h"
#include "discord_register.h"
#include <stdio.h> #include <stdio.h>
#include <errno.h> #include <errno.h>
@ -9,7 +8,7 @@
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
static bool Mkdir(const char* path) bool Mkdir(const char* path)
{ {
int result = mkdir(path, 0755); int result = mkdir(path, 0755);
if (result == 0) { if (result == 0) {
@ -22,7 +21,7 @@ static bool Mkdir(const char* path)
} }
// we want to register games so we can run them from Discord client as discord-<appid>:// // we want to register games so we can run them from Discord client as discord-<appid>://
extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command) extern "C" void Discord_Register(const char* applicationId, const char* command)
{ {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing. // Add a desktop file and update some mime handlers so that xdg-open does the right thing.
@ -33,15 +32,13 @@ extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const
char exePath[1024]; char exePath[1024];
if (!command || !command[0]) { if (!command || !command[0]) {
ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath)); if (readlink("/proc/self/exe", exePath, sizeof(exePath)) <= 0) {
if (size <= 0 || size >= (ssize_t)sizeof(exePath)) {
return; return;
} }
exePath[size] = '\0';
command = exePath; command = exePath;
} }
const char* desktopFileFormat = "[Desktop Entry]\n" const char* destopFileFormat = "[Desktop Entry]\n"
"Name=Game %s\n" "Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there "Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n" "Type=Application\n"
@ -50,7 +47,7 @@ extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const
"MimeType=x-scheme-handler/discord-%s;\n"; "MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]; char desktopFile[2048];
int fileLen = snprintf( int fileLen = snprintf(
desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId); desktopFile, sizeof(desktopFile), destopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) { if (fileLen <= 0) {
return; return;
} }
@ -93,8 +90,7 @@ extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const
} }
} }
extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, extern "C" void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
const char* steamId)
{ {
char command[256]; char command[256];
sprintf(command, "xdg-open steam://rungameid/%s", steamId); sprintf(command, "xdg-open steam://rungameid/%s", steamId);

View File

@ -1,5 +1,4 @@
#include "discord_rpc.h" #include "discord-rpc.h"
#include "discord_register.h"
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#define NOMCX #define NOMCX
@ -7,7 +6,8 @@
#define NOIME #define NOIME
#include <windows.h> #include <windows.h>
#include <psapi.h> #include <psapi.h>
#include <cstdio> #include <cwchar>
#include <stdio.h>
/** /**
* Updated fixes for MinGW and WinXP * Updated fixes for MinGW and WinXP
@ -19,22 +19,14 @@
* The entire function is rewritten * The entire function is rewritten
*/ */
#ifdef __MINGW32__ #ifdef __MINGW32__
#include <wchar.h>
/// strsafe.h fixes /// strsafe.h fixes
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) #define StringCbPrintfW snwprintf
LPWSTR StringCbCopyW(LPWSTR a, size_t l, LPCWSTR b)
{ {
HRESULT ret; a[l-1] = 0;
va_list va; return wcsncpy(a, b, l - 1); // does not set the last byte to 0 on overflow, so it's set to 0 above
va_start(va, pszFormat);
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
// othervise
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
va_end(va);
return ret;
} }
#else #else
#include <cwchar>
#include <strsafe.h> #include <strsafe.h>
#endif // __MINGW32__ #endif // __MINGW32__
@ -46,28 +38,21 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
#undefine RegSetKeyValueW #undefine RegSetKeyValueW
#endif #endif
#define RegSetKeyValueW regset #define RegSetKeyValueW regset
static LSTATUS regset(HKEY hkey, LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len)
LPCWSTR subkey,
LPCWSTR name,
DWORD type,
const void* data,
DWORD len)
{ {
HKEY htkey = hkey, hsubkey = nullptr; HKEY hsubkey = NULL;
LSTATUS ret; LSTATUS ret;
if (subkey && subkey[0]) { if (subkey && subkey[0]) /* need to create the subkey */
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) != {
ERROR_SUCCESS) if ((ret = RegCreateKeyW( hkey, subkey, &hsubkey )) != ERROR_SUCCESS) return ret;
return ret; hkey = hsubkey;
htkey = hsubkey;
} }
ret = RegSetValueExW(htkey, name, 0, type, (const BYTE*)data, len); ret = RegSetValueExW( hkey, name, 0, type, (const BYTE*)data, len );
if (hsubkey && hsubkey != hkey) if (hsubkey) RegCloseKey( hsubkey );
RegCloseKey(hsubkey);
return ret; return ret;
} }
static void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command) void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
{ {
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
// we want to register games so we can run them as discord-<appid>:// // we want to register games so we can run them as discord-<appid>://
@ -81,8 +66,7 @@ static void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* comma
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command); StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
} }
else { else {
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath); StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
} }
wchar_t protocolName[64]; wchar_t protocolName[64];
@ -131,7 +115,7 @@ static void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* comma
RegCloseKey(key); RegCloseKey(key);
} }
extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command) extern "C" void Discord_Register(const char* applicationId, const char* command)
{ {
wchar_t appId[32]; wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
@ -147,8 +131,7 @@ extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const
Discord_RegisterW(appId, wcommand); Discord_RegisterW(appId, wcommand);
} }
extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, extern "C" void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
const char* steamId)
{ {
wchar_t appId[32]; wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);

View File

@ -1,7 +1,5 @@
#include <windows.h> #include <windows.h>
// outsmart GCC's missing-declarations warning
BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID);
BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID) BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID)
{ {
return TRUE; return TRUE;

View File

@ -7,7 +7,7 @@
template <typename ElementType, size_t QueueSize> template <typename ElementType, size_t QueueSize>
class MsgQueue { class MsgQueue {
ElementType queue_[QueueSize]; ElementType queue_[QueueSize]{};
std::atomic_uint nextAdd_{0}; std::atomic_uint nextAdd_{0};
std::atomic_uint nextSend_{0}; std::atomic_uint nextSend_{0};
std::atomic_uint pendingSends_{0}; std::atomic_uint pendingSends_{0};

View File

@ -26,9 +26,13 @@ void RpcConnection::Open()
return; return;
} }
if (state == State::Disconnected && !connection->Open()) { if (state == State::Disconnected) {
if (connection->Open()) {
}
else {
return; return;
} }
}
if (state == State::SentHandshake) { if (state == State::SentHandshake) {
JsonDocument message; JsonDocument message;
@ -38,7 +42,7 @@ void RpcConnection::Open()
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) { if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
state = State::Connected; state = State::Connected;
if (onConnect) { if (onConnect) {
onConnect(message); onConnect();
} }
} }
} }

View File

@ -40,7 +40,7 @@ struct RpcConnection {
BaseConnection* connection{nullptr}; BaseConnection* connection{nullptr};
State state{State::Disconnected}; State state{State::Disconnected};
void (*onConnect)(JsonDocument& message){nullptr}; void (*onConnect)(){nullptr};
void (*onDisconnect)(int errorCode, const char* message){nullptr}; void (*onDisconnect)(int errorCode, const char* message){nullptr};
char appId[64]{}; char appId[64]{};
int lastErrorCode{0}; int lastErrorCode{0};

View File

@ -1,6 +1,6 @@
#include "serialization.h" #include "serialization.h"
#include "connection.h" #include "connection.h"
#include "discord_rpc.h" #include "discord-rpc.h"
template <typename T> template <typename T>
void NumberToString(char* dest, T number) void NumberToString(char* dest, T number)
@ -102,7 +102,8 @@ size_t JsonWriteRichPresenceObj(char* dest,
WriteKey(writer, "pid"); WriteKey(writer, "pid");
writer.Int(pid); writer.Int(pid);
if (presence != nullptr) { if (presence != nullptr)
{
WriteObject activity(writer, "activity"); WriteObject activity(writer, "activity");
WriteOptionalString(writer, "state", presence->state); WriteOptionalString(writer, "state", presence->state);
@ -134,18 +135,15 @@ size_t JsonWriteRichPresenceObj(char* dest,
} }
if ((presence->partyId && presence->partyId[0]) || presence->partySize || if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) { presence->partyMax) {
WriteObject party(writer, "party"); WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId); WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) { if (presence->partySize) {
WriteArray size(writer, "size"); WriteArray size(writer, "size");
writer.Int(presence->partySize); writer.Int(presence->partySize);
if (0 < presence->partyMax) {
writer.Int(presence->partyMax); writer.Int(presence->partyMax);
} }
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
} }
} }
@ -201,25 +199,6 @@ size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const cha
return writer.Size(); return writer.Size();
} }
size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName)
{
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce) size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce)
{ {
JsonWriter writer(dest, maxLen); JsonWriter writer(dest, maxLen);

View File

@ -47,8 +47,6 @@ size_t JsonWriteRichPresenceObj(char* dest,
const DiscordRichPresence* presence); const DiscordRichPresence* presence);
size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName); size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName);
size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName);
size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce); size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need // I want to use as few allocations as I can get away with, and to do that with RapidJson, you need