├── .clang-format ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── appveyor.yml ├── build.py ├── documentation ├── hard-mode.md └── images │ ├── rp-dev-dashboard.png │ ├── rp-profile-view.png │ └── rp-secret-example.png ├── examples ├── button-clicker │ ├── .gitignore │ ├── Assets │ │ ├── DiscordController.cs │ │ ├── DiscordController.cs.meta │ │ ├── DiscordRpc.cs │ │ ├── DiscordRpc.cs.meta │ │ ├── Editor │ │ │ ├── BuildHelper.cs │ │ │ └── BuildHelper.cs.meta │ │ ├── main.unity │ │ └── main.unity.meta │ ├── ProjectSettings │ │ ├── AudioManager.asset │ │ ├── ClusterInputManager.asset │ │ ├── DynamicsManager.asset │ │ ├── EditorBuildSettings.asset │ │ ├── EditorSettings.asset │ │ ├── GraphicsSettings.asset │ │ ├── InputManager.asset │ │ ├── NavMeshAreas.asset │ │ ├── NetworkManager.asset │ │ ├── Physics2DSettings.asset │ │ ├── ProjectSettings.asset │ │ ├── ProjectVersion.txt │ │ ├── QualitySettings.asset │ │ ├── TagManager.asset │ │ ├── TimeManager.asset │ │ └── UnityConnectSettings.asset │ └── UnityPackageManager │ │ └── manifest.json ├── send-presence │ ├── CMakeLists.txt │ └── send-presence.c └── unrealstatus │ ├── .gitignore │ ├── Config │ ├── DefaultEditor.ini │ ├── DefaultEngine.ini │ └── DefaultGame.ini │ ├── Content │ ├── MainScreenBP.uasset │ ├── MouseGameModeBP.uasset │ ├── MousePlayerControllerBP.uasset │ └── ShowTheUILevel.umap │ ├── Plugins │ └── discordrpc │ │ ├── DiscordRpc.uplugin │ │ ├── Resources │ │ ├── Icon128.png │ │ └── discord.png │ │ └── Source │ │ ├── DiscordRpc │ │ ├── DiscordRpc.Build.cs │ │ ├── Private │ │ │ ├── DiscordRpc.cpp │ │ │ ├── DiscordRpcBlueprint.cpp │ │ │ └── DiscordRpcPrivatePCH.h │ │ └── Public │ │ │ ├── DiscordRpc.h │ │ │ └── DiscordRpcBlueprint.h │ │ └── ThirdParty │ │ └── DiscordRpcLibrary │ │ └── DiscordRpcLibrary.Build.cs │ ├── Source │ ├── unrealstatus.Target.cs │ ├── unrealstatus │ │ ├── unrealstatus.Build.cs │ │ ├── unrealstatus.cpp │ │ ├── unrealstatus.h │ │ ├── unrealstatusGameModeBase.cpp │ │ └── unrealstatusGameModeBase.h │ └── unrealstatusEditor.Target.cs │ └── unrealstatus.uproject ├── include ├── discord_register.h └── discord_rpc.h └── src ├── CMakeLists.txt ├── backoff.h ├── connection.h ├── connection_unix.cpp ├── connection_win.cpp ├── discord_register_linux.cpp ├── discord_register_osx.m ├── discord_register_win.cpp ├── discord_rpc.cpp ├── dllmain.cpp ├── msg_queue.h ├── rpc_connection.cpp ├── rpc_connection.h ├── serialization.cpp └── serialization.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: true 4 | AlignConsecutiveAssignments: false 5 | AlignConsecutiveDeclarations: false 6 | AlignEscapedNewlines: Left 7 | AlignOperands: false 8 | AlignTrailingComments: true 9 | AllowAllParametersOfDeclarationOnNextLine: false 10 | AllowShortBlocksOnASingleLine: false 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: InlineOnly 13 | AllowShortIfStatementsOnASingleLine: false 14 | AllowShortLoopsOnASingleLine: false 15 | AlwaysBreakAfterReturnType: None 16 | AlwaysBreakBeforeMultilineStrings: false 17 | AlwaysBreakTemplateDeclarations: true 18 | BinPackArguments: false 19 | BinPackParameters: false 20 | BreakBeforeBinaryOperators: None 21 | BreakBeforeBraces: Stroustrup 22 | BreakBeforeInheritanceComma: true 23 | BreakBeforeTernaryOperators: true 24 | BreakConstructorInitializers: BeforeComma 25 | BreakStringLiterals: true 26 | ColumnLimit: 100 27 | CommentPragmas: '' 28 | CompactNamespaces: false 29 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 30 | ConstructorInitializerIndentWidth: 2 31 | ContinuationIndentWidth: 2 32 | Cpp11BracedListStyle: true 33 | DerivePointerAlignment: false 34 | DisableFormat: false 35 | FixNamespaceComments: true 36 | ForEachMacros: [] 37 | IndentCaseLabels: false 38 | IncludeCategories: 39 | - Regex: '^("|<)stdafx\.h(pp)?("|>)' 40 | Priority: -1 41 | - Regex: '^<(W|w)indows.h>' 42 | Priority: 1 43 | - Regex: '^<' 44 | Priority: 2 45 | - Regex: '.*' 46 | Priority: 3 47 | IncludeIsMainRegex: '(_test|_win|_linux|_mac|_ios|_osx|_null)?$' 48 | IndentCaseLabels: false 49 | IndentWidth: 4 50 | IndentWrappedFunctionNames: false 51 | KeepEmptyLinesAtTheStartOfBlocks: false 52 | MacroBlockBegin: '' 53 | MacroBlockEnd: '' 54 | MaxEmptyLinesToKeep: 1 55 | NamespaceIndentation: None 56 | PenaltyBreakAssignment: 0 57 | PenaltyBreakBeforeFirstCallParameter: 1 58 | PenaltyBreakComment: 300 59 | PenaltyBreakFirstLessLess: 120 60 | PenaltyBreakString: 1000 61 | PenaltyExcessCharacter: 1000000 62 | PenaltyReturnTypeOnItsOwnLine: 9999999 63 | PointerAlignment: Left 64 | ReflowComments: true 65 | SortIncludes: false 66 | SortUsingDeclarations: true 67 | SpaceAfterCStyleCast: false 68 | SpaceAfterTemplateKeyword: true 69 | SpaceBeforeAssignmentOperators: true 70 | SpaceBeforeParens: ControlStatements 71 | SpaceInEmptyParentheses: false 72 | SpacesBeforeTrailingComments: 1 73 | SpacesInAngles: false 74 | SpacesInCStyleCastParentheses: false 75 | SpacesInContainerLiterals: true 76 | SpacesInParentheses: false 77 | SpacesInSquareBrackets: false 78 | Standard: Cpp11 79 | TabWidth: 4 80 | UseTab: Never 81 | --- 82 | Language: Cpp 83 | --- 84 | Language: ObjC 85 | ObjCBlockIndentWidth: 4 86 | ObjCSpaceAfterProperty: true 87 | ObjCSpaceBeforeProtocolList: false 88 | --- 89 | Language: Java 90 | BasedOnStyle: Google 91 | BreakAfterJavaFieldAnnotations: true 92 | ... 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build*/ 2 | /.vscode/ 3 | /thirdparty/ 4 | .vs/ 5 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | env: 4 | global: 5 | - CLANG_FORMAT_SUFFIX="-dummy" # don't use formatting on Travis, this is 6 | # needed not to use default 3.5 version 7 | # which is too old. 8 | 9 | matrix: 10 | include: 11 | - os: linux 12 | env: MATRIX_EVAL="CC=gcc-5 && CXX=g++-5" 13 | addons: 14 | apt: 15 | sources: 16 | - ubuntu-toolchain-r-test 17 | packages: 18 | - g++-5 19 | - os: linux 20 | env: MATRIX_EVAL="CC=clang-4.0 && CXX=clang++-4.0" 21 | addons: 22 | apt: 23 | sources: 24 | - llvm-toolchain-trusty-4.0 25 | packages: 26 | - clang-4.0 27 | - os: linux 28 | env: MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0" 29 | addons: 30 | apt: 31 | sources: 32 | - llvm-toolchain-trusty-5.0 33 | packages: 34 | - clang-5.0 35 | - os: osx 36 | osx_image: xcode9 37 | 38 | # prevent Travis from overwriting our CXX variables 39 | before_install: 40 | - eval "${MATRIX_EVAL}" 41 | - echo $CXX 42 | 43 | script: 44 | - mkdir build 45 | - cd build 46 | - cmake -DCLANG_FORMAT_SUFFIX=$CLANG_FORMAT_SUFFIX --config Release .. 47 | - cmake --build . -- -j2 48 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.2.0) 2 | project (DiscordRPC) 3 | 4 | include(GNUInstallDirs) 5 | 6 | option(BUILD_EXAMPLES "Build example apps" ON) 7 | 8 | # format 9 | file(GLOB_RECURSE ALL_SOURCE_FILES 10 | examples/*.cpp examples/*.h examples/*.c 11 | include/*.h 12 | src/*.cpp src/*.h src/*.c 13 | ) 14 | 15 | # Set CLANG_FORMAT_SUFFIX if you are using custom clang-format, e.g. clang-format-5.0 16 | find_program(CLANG_FORMAT_CMD clang-format${CLANG_FORMAT_SUFFIX}) 17 | 18 | if (CLANG_FORMAT_CMD) 19 | add_custom_target( 20 | clangformat 21 | COMMAND ${CLANG_FORMAT_CMD} 22 | -i -style=file -fallback-style=none 23 | ${ALL_SOURCE_FILES} 24 | DEPENDS 25 | ${ALL_SOURCE_FILES} 26 | ) 27 | endif(CLANG_FORMAT_CMD) 28 | 29 | # thirdparty stuff 30 | execute_process( 31 | COMMAND mkdir ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty 32 | ERROR_QUIET 33 | ) 34 | 35 | find_file(RAPIDJSONTEST NAMES rapidjson rapidjson-1.1.0 PATHS ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty CMAKE_FIND_ROOT_PATH_BOTH) 36 | if (NOT RAPIDJSONTEST) 37 | message("no rapidjson, download") 38 | set(RJ_TAR_FILE ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/v1.1.0.tar.gz) 39 | file(DOWNLOAD https://github.com/miloyip/rapidjson/archive/v1.1.0.tar.gz ${RJ_TAR_FILE}) 40 | execute_process( 41 | COMMAND ${CMAKE_COMMAND} -E tar xzf ${RJ_TAR_FILE} 42 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty 43 | ) 44 | file(REMOVE ${RJ_TAR_FILE}) 45 | endif(NOT RAPIDJSONTEST) 46 | 47 | find_file(RAPIDJSON NAMES rapidjson rapidjson-1.1.0 PATHS ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty CMAKE_FIND_ROOT_PATH_BOTH) 48 | 49 | add_library(rapidjson STATIC IMPORTED ${RAPIDJSON}) 50 | 51 | # add subdirs 52 | 53 | add_subdirectory(src) 54 | if (BUILD_EXAMPLES) 55 | add_subdirectory(examples/send-presence) 56 | endif(BUILD_EXAMPLES) 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Discord, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord RPC 2 | 3 | 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. 4 | 5 | Included here are some quick demos that implement the very minimal subset to show current status, and 6 | have callbacks for where a more complete game would do more things (joining, spectating, etc). 7 | 8 | ## Documentation 9 | 10 | 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). 11 | 12 | ## Basic Usage 13 | 14 | Zeroith, you should be set up to build things because you are a game developer, right? 15 | 16 | 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. 17 | 18 | ### From package 19 | 20 | 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. 21 | 22 | ### From repo 23 | 24 | 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. 25 | 26 | 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! 27 | 28 | 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: 29 | ```sh 30 | cd 31 | mkdir build 32 | cd build 33 | cmake .. -DCMAKE_INSTALL_PREFIX= 34 | cmake --build . --config Release --target install 35 | ``` 36 | There is a wrapper build script `build.py` that runs `cmake` with a few different options. 37 | 38 | 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`. 39 | 40 | There are some CMake options you might care about: 41 | 42 | | flag | default | does | 43 | |------|---------|------| 44 | | `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. 45 | | `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) 46 | | [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL 47 | 48 | ## Continuous Builds 49 | 50 | Why do we have three of these? Three times the fun! 51 | 52 | | CI | badge | 53 | |----|-------| 54 | | TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc) 55 | | AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc) 56 | | Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc) 57 | 58 | ## Sample: send-presence 59 | 60 | This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command. 61 | 62 | ## Sample: button-clicker 63 | 64 | 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. 65 | 66 | ## Sample: unrealstatus 67 | 68 | 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. 69 | 70 | ## Wrappers and Implementations 71 | 72 | Below is a table of unofficial, community-developed wrappers for and implementations of Rich Presence in various languages. If you would like to have yours added, please make a pull request adding your repository to the table. The repository should include: 73 | 74 | - The code 75 | - A brief ReadMe of how to use it 76 | - A working example 77 | 78 | ###### Rich Presence Wrappers and Implementations 79 | 80 | | Name | Language | 81 | |------|----------| 82 | | [discord-rpc.jar](https://github.com/Vatuu/discord-rpc "Discord-RPC.jar") | Java | 83 | | [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java | 84 | | [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java | 85 | | [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript | 86 | | [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) | 87 | | [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) | 88 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | install: 3 | - python -m pip install click 4 | 5 | build_script: 6 | - mkdir examples\unrealstatus\Plugins\discordrpc\Binaries\ThirdParty\discordrpcLibrary\Win64 7 | - python build.py 8 | 9 | artifacts: 10 | - path: builds\install\win32-dynamic 11 | name: win32-dynamic 12 | - path: builds\install\win32-static 13 | name: win32-static 14 | - path: builds\install\win64-dynamic 15 | name: win64-dynamic 16 | - path: builds\install\win64-static 17 | name: win64-static 18 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import subprocess 5 | import sys 6 | import shutil 7 | import zipfile 8 | from contextlib import contextmanager 9 | import click 10 | 11 | 12 | def get_platform(): 13 | """ a name for the platform """ 14 | if sys.platform.startswith('win'): 15 | return 'win' 16 | elif sys.platform == 'darwin': 17 | return 'osx' 18 | elif sys.platform.startswith('linux'): 19 | return 'linux' 20 | raise Exception('Unsupported platform ' + sys.platform) 21 | 22 | 23 | SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) 24 | # we use Buildkite which sets this env variable by default 25 | IS_BUILD_MACHINE = os.environ.get('CI', '') == 'true' 26 | PLATFORM = get_platform() 27 | INSTALL_ROOT = os.path.join(SCRIPT_PATH, 'builds', 'install') 28 | 29 | 30 | def get_signtool(): 31 | """ get path to code signing tool """ 32 | if PLATFORM == 'win': 33 | sdk_dir = os.environ['WindowsSdkDir'] 34 | return os.path.join(sdk_dir, 'bin', 'x86', 'signtool.exe') 35 | elif PLATFORM == 'osx': 36 | return '/usr/bin/codesign' 37 | 38 | 39 | @contextmanager 40 | def cd(new_dir): 41 | """ Temporarily change current directory """ 42 | if new_dir: 43 | old_dir = os.getcwd() 44 | os.chdir(new_dir) 45 | yield 46 | if new_dir: 47 | os.chdir(old_dir) 48 | 49 | 50 | def mkdir_p(path): 51 | """ mkdir -p """ 52 | if not os.path.isdir(path): 53 | click.secho('Making ' + path, fg='yellow') 54 | os.makedirs(path) 55 | 56 | 57 | @click.group(invoke_without_command=True) 58 | @click.pass_context 59 | @click.option('--clean', is_flag=True) 60 | def cli(ctx, clean): 61 | """ click wrapper for command line stuff """ 62 | if ctx.invoked_subcommand is None: 63 | ctx.invoke(libs, clean=clean) 64 | if IS_BUILD_MACHINE: 65 | ctx.invoke(sign) 66 | ctx.invoke(archive) 67 | 68 | 69 | @cli.command() 70 | @click.pass_context 71 | def unity(ctx): 72 | """ build just dynamic libs for use in unity project """ 73 | ctx.invoke( 74 | libs, 75 | clean=False, 76 | static=False, 77 | shared=True, 78 | skip_formatter=True, 79 | just_release=True 80 | ) 81 | BUILDS = [] 82 | 83 | click.echo('--- Copying libs and header into unity example') 84 | UNITY_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'button-clicker', 'Assets', 'Plugins') 85 | 86 | if sys.platform.startswith('win'): 87 | LIBRARY_NAME = 'discord-rpc.dll' 88 | BUILD_64_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release') 89 | UNITY_64_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86_64') 90 | BUILDS.append({BUILD_64_BASE_PATH: UNITY_64_DLL_PATH}) 91 | 92 | BUILD_32_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win32-dynamic', 'src', 'Release') 93 | UNITY_32_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86') 94 | BUILDS.append({BUILD_32_BASE_PATH: UNITY_32_DLL_PATH}) 95 | 96 | elif sys.platform == 'darwin': 97 | LIBRARY_NAME = 'discord-rpc.bundle' 98 | BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'osx-dynamic', 'src') 99 | UNITY_DLL_PATH = UNITY_PROJECT_PATH 100 | os.rename(os.path.join(BUILD_BASE_PATH, 'libdiscord-rpc.dylib'), os.path.join(BUILD_BASE_PATH, 'discord-rpc.bundle')) 101 | 102 | BUILDS.append({BUILD_BASE_PATH: UNITY_DLL_PATH}) 103 | 104 | elif sys.platform.startswith('linux'): 105 | LIBRARY_NAME = 'discord-rpc.so' 106 | BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'linux-dynamic', 'src') 107 | UNITY_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86') 108 | os.rename(os.path.join(BUILD_BASE_PATH, 'libdiscord-rpc.so'), os.path.join(BUILD_BASE_PATH, 'discord-rpc.so')) 109 | 110 | BUILDS.append({BUILD_BASE_PATH: UNITY_DLL_PATH}) 111 | 112 | else: 113 | raise Exception('Unsupported platform ' + sys.platform) 114 | 115 | for build in BUILDS: 116 | for i in build: 117 | mkdir_p(build[i]) 118 | shutil.copy(os.path.join(i, LIBRARY_NAME), build[i]) 119 | 120 | 121 | @cli.command() 122 | @click.pass_context 123 | def unreal(ctx): 124 | """ build libs and copy them into the unreal project """ 125 | ctx.invoke( 126 | libs, 127 | clean=False, 128 | static=False, 129 | shared=True, 130 | skip_formatter=True, 131 | just_release=True 132 | ) 133 | BUILDS = [] 134 | 135 | click.echo('--- Copying libs and header into unreal example') 136 | UNREAL_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'unrealstatus', 'Plugins', 'discordrpc') 137 | UNREAL_INCLUDE_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Include') 138 | mkdir_p(UNREAL_INCLUDE_PATH) 139 | shutil.copy(os.path.join(SCRIPT_PATH, 'include', 'discord_rpc.h'), UNREAL_INCLUDE_PATH) 140 | 141 | if sys.platform.startswith('win'): 142 | LIBRARY_NAME = 'discord-rpc.lib' 143 | BUILD_64_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release') 144 | UNREAL_64_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win64') 145 | BUILDS.append({BUILD_64_BASE_PATH: UNREAL_64_DLL_PATH}) 146 | 147 | BUILD_32_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win32-dynamic', 'src', 'Release') 148 | UNREAL_32_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win32') 149 | BUILDS.append({BUILD_32_BASE_PATH: UNREAL_32_DLL_PATH}) 150 | 151 | elif sys.platform == 'darwin': 152 | LIBRARY_NAME = 'libdiscord-rpc.dylib' 153 | BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'osx-dynamic', 'src') 154 | UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Mac') 155 | 156 | BUILDS.append({BUILD_BASE_PATH: UNREAL_DLL_PATH}) 157 | 158 | elif sys.platform.startswith('linux'): 159 | LIBRARY_NAME = 'libdiscord-rpc.so' 160 | BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'linux-dynamic', 'src') 161 | UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Linux') 162 | 163 | BUILDS.append({BUILD_BASE_PATH: UNREAL_DLL_PATH}) 164 | 165 | else: 166 | raise Exception('Unsupported platform ' + sys.platform) 167 | 168 | for build in BUILDS: 169 | for i in build: 170 | mkdir_p(build[i]) 171 | shutil.copy(os.path.join(i, LIBRARY_NAME), build[i]) 172 | 173 | 174 | def build_lib(build_name, generator, options, just_release): 175 | """ Create a dir under builds, run build and install in it """ 176 | build_path = os.path.join(SCRIPT_PATH, 'builds', build_name) 177 | install_path = os.path.join(INSTALL_ROOT, build_name) 178 | mkdir_p(build_path) 179 | mkdir_p(install_path) 180 | with cd(build_path): 181 | initial_cmake = [ 182 | 'cmake', 183 | SCRIPT_PATH, 184 | '-DCMAKE_INSTALL_PREFIX=%s' % os.path.join('..', 'install', build_name) 185 | ] 186 | if generator: 187 | initial_cmake.extend(['-G', generator]) 188 | for key in options: 189 | val = options[key] 190 | if type(val) is bool: 191 | val = 'ON' if val else 'OFF' 192 | initial_cmake.append('-D%s=%s' % (key, val)) 193 | click.echo('--- Building ' + build_name) 194 | subprocess.check_call(initial_cmake) 195 | if not just_release: 196 | subprocess.check_call(['cmake', '--build', '.', '--config', 'Debug']) 197 | subprocess.check_call(['cmake', '--build', '.', '--config', 'Release', '--target', 'install']) 198 | 199 | 200 | @cli.command() 201 | def archive(): 202 | """ create zip of install dir """ 203 | click.echo('--- Archiving') 204 | archive_file_path = os.path.join(SCRIPT_PATH, 'builds', 'discord-rpc-%s.zip' % get_platform()) 205 | archive_file = zipfile.ZipFile(archive_file_path, 'w', zipfile.ZIP_DEFLATED) 206 | archive_src_base_path = INSTALL_ROOT 207 | archive_dst_base_path = 'discord-rpc' 208 | with cd(archive_src_base_path): 209 | for path, _, filenames in os.walk('.'): 210 | for fname in filenames: 211 | fpath = os.path.join(path, fname) 212 | dst_path = os.path.normpath(os.path.join(archive_dst_base_path, fpath)) 213 | click.echo('Adding ' + dst_path) 214 | archive_file.write(fpath, dst_path) 215 | 216 | 217 | @cli.command() 218 | def sign(): 219 | """ Do code signing within install directory using our cert """ 220 | tool = get_signtool() 221 | signable_extensions = set() 222 | if PLATFORM == 'win': 223 | signable_extensions.add('.dll') 224 | sign_command_base = [ 225 | tool, 226 | 'sign', 227 | '/n', 'Discord Inc.', 228 | '/a', 229 | '/tr', 'http://timestamp.digicert.com/rfc3161', 230 | '/as', 231 | '/td', 'sha256', 232 | '/fd', 'sha256', 233 | ] 234 | elif PLATFORM == 'osx': 235 | signable_extensions.add('.dylib') 236 | sign_command_base = [ 237 | tool, 238 | '--keychain', os.path.expanduser('~/Library/Keychains/login.keychain'), 239 | '-vvvv', 240 | '--deep', 241 | '--force', 242 | '--sign', 'Developer ID Application: Hammer & Chisel Inc. (53Q6R32WPB)', 243 | ] 244 | else: 245 | click.secho('Not signing things on this platform yet', fg='red') 246 | return 247 | 248 | click.echo('--- Signing') 249 | for path, _, filenames in os.walk(INSTALL_ROOT): 250 | for fname in filenames: 251 | ext = os.path.splitext(fname)[1] 252 | if ext not in signable_extensions: 253 | continue 254 | fpath = os.path.join(path, fname) 255 | click.echo('Sign ' + fpath) 256 | sign_command = sign_command_base + [fpath] 257 | subprocess.check_call(sign_command) 258 | 259 | 260 | @cli.command() 261 | @click.option('--clean', is_flag=True) 262 | @click.option('--static', is_flag=True) 263 | @click.option('--shared', is_flag=True) 264 | @click.option('--skip_formatter', is_flag=True) 265 | @click.option('--just_release', is_flag=True) 266 | def libs(clean, static, shared, skip_formatter, just_release): 267 | """ Do all the builds for this platform """ 268 | if clean: 269 | shutil.rmtree('builds', ignore_errors=True) 270 | 271 | mkdir_p('builds') 272 | 273 | if not (static or shared): 274 | static = True 275 | shared = True 276 | 277 | static_options = {} 278 | dynamic_options = { 279 | 'BUILD_SHARED_LIBS': True, 280 | 'USE_STATIC_CRT': True, 281 | } 282 | 283 | if skip_formatter or IS_BUILD_MACHINE: 284 | static_options['CLANG_FORMAT_SUFFIX'] = 'none' 285 | dynamic_options['CLANG_FORMAT_SUFFIX'] = 'none' 286 | 287 | if IS_BUILD_MACHINE: 288 | just_release = True 289 | 290 | if PLATFORM == 'win': 291 | generator32 = 'Visual Studio 14 2015' 292 | generator64 = 'Visual Studio 14 2015 Win64' 293 | if static: 294 | build_lib('win32-static', generator32, static_options, just_release) 295 | build_lib('win64-static', generator64, static_options, just_release) 296 | if shared: 297 | build_lib('win32-dynamic', generator32, dynamic_options, just_release) 298 | build_lib('win64-dynamic', generator64, dynamic_options, just_release) 299 | elif PLATFORM == 'osx': 300 | if static: 301 | build_lib('osx-static', None, static_options, just_release) 302 | if shared: 303 | build_lib('osx-dynamic', None, dynamic_options, just_release) 304 | elif PLATFORM == 'linux': 305 | if static: 306 | build_lib('linux-static', None, static_options, just_release) 307 | if shared: 308 | build_lib('linux-dynamic', None, dynamic_options, just_release) 309 | 310 | 311 | if __name__ == '__main__': 312 | os.chdir(SCRIPT_PATH) 313 | sys.exit(cli()) 314 | -------------------------------------------------------------------------------- /documentation/hard-mode.md: -------------------------------------------------------------------------------- 1 | # Hard Mode: Roll Your Own Client 2 | 3 | Discord's Rich Presence feature is designed as an obfuscated addition to our existing [RPC infrastructure](https://discordapp.com/developers/docs/topics/rpc). The standalone library and header files make it easy for any dev to drop it into their game. 4 | 5 | Our library communicates with Discord over the local Discord RPC socket. We've already done the work in connecting properly, handling disconnects and reconnects, and other RPC intracacies, but those who have done this implementation for our private alpha Voice and Chat SDK can simply make use of the new RPC commands and events to implement Rich Presence. 6 | 7 | ## Hark! A warning! 8 | 9 | By committing to an RPC-only integration, you decide to forego the work our library and header file have done for you in the way of error handling, state storage, disconnecting and reconnecting, and other quality of life abstractions. While simply implementing the new RPC command and events will enable Rich Presence for your game, we highly suggest that you do your best to mimic the functionality of the SDK the most that you can. It ensure not only code quality on your part, but also an excellent experience on the part of your players. 10 | 11 | ## Application Protocol Registration 12 | 13 | One thing that cannot be explicitly done over RPC is registering an application protocol for your game. If you choose to do an RPC-only implementation, you will have to register your application protocol yourself in the format of `discord-[your_app_id]://`. You can use `Discord_Register()` as a good(?) example of how to properly register an application protocol for use with Discord. For OSX and Linux it is probably simpler to handle the protocol registration as part of your install/packaging. 14 | 15 | ## New RPC Command 16 | 17 | The new RPC command for Rich Presence is `SET_ACTIVITY`. The fields are similar to what is outlined in the SDK; we've combined similar fields into objects for the sake of less data on the wire. 18 | 19 | The one major difference is the `party.size` field. It is an array with a size of two. The first element is the current party size, `partySize` from the main documentation. The second element is the maximum party size, `partyMax` from the main documentation. 20 | 21 | Below is a full example of a `SET_ACTIVITY` command. Field restrictions like size are the same as outlined in the main documentation. 22 | 23 | ``` 24 | { 25 | "cmd": "SET_ACTIVITY", 26 | "args": { 27 | "pid": 9999, // Your application's process id - required field 28 | "activity": { 29 | "state": "In a Group", 30 | "details": "Competitive | In a Match", 31 | "timestamps": { 32 | "start": time(nullptr), 33 | "end": time(nullptr) + ((60 * 5) + 23) 34 | }, 35 | "assets": { 36 | "large_image": "numbani_map", 37 | "large_text": "Numbani", 38 | "small_image": "pharah_profile", 39 | "small_text": "Pharah" 40 | }, 41 | "party": { 42 | "id": GameEngine.GetPartyId(), 43 | "size": [3, 6] 44 | }, 45 | "secrets": { 46 | "join": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f", 47 | "spectate": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0", 48 | "match": "4b2fdce12f639de8bfa7e3591b71a0d679d7c93f" 49 | }, 50 | "instance": true 51 | } 52 | }, 53 | "nonce": "647d814a-4cf8-4fbb-948f-898abd24f55b" 54 | } 55 | ``` 56 | 57 | ## New RPC Events 58 | 59 | The three new RPC events for Rich Presence power the ability to join and spectate your friends' games. 60 | 61 | First is the `ACTIVITY_JOIN` event: 62 | 63 | ```json 64 | { 65 | "cmd": "DISPATCH", 66 | "data": { 67 | "secret": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f" 68 | }, 69 | "evt": "ACTIVITY_JOIN" 70 | } 71 | ``` 72 | 73 | Second is the `ACTIVITY_SPECTATE` event: 74 | 75 | ```json 76 | { 77 | "cmd": "DISPATCH", 78 | "data": { 79 | "secret": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0" 80 | }, 81 | "evt": "ACTIVITY_SPECTATE" 82 | } 83 | ``` 84 | 85 | And third is the `ACTIVITY_JOIN_REQUEST` event: 86 | 87 | ```json 88 | { 89 | "cmd": "DISPATCH", 90 | "data": { 91 | "user": { 92 | "id": "53908232506183680", 93 | "username": "Mason", 94 | "discriminator": "1337", 95 | "avatar": "a_bab14f271d565501444b2ca3be944b25" 96 | } 97 | }, 98 | "evt": "ACTIVITY_JOIN_REQUEST" 99 | } 100 | ``` 101 | 102 | In order to receive these events, you need to [subscribe](https://discordapp.com/developers/docs/topics/rpc#subscribe) to them like so: 103 | 104 | ```json 105 | { 106 | "nonce": "be9a6de3-31d0-4767-a8e9-4818c5690015", 107 | "evt": "ACTIVITY_JOIN", 108 | "cmd": "SUBSCRIBE" 109 | } 110 | ``` 111 | 112 | ```json 113 | { 114 | "nonce": "ae9qdde3-31d0-8989-a8e9-dnakwy174he", 115 | "evt": "ACTIVITY_SPECTATE", 116 | "cmd": "SUBSCRIBE" 117 | } 118 | ``` 119 | 120 | ```json 121 | { 122 | "nonce": "5dc0c062-98c6-47a0-8922-bbb52e9d6afa", 123 | "evt": "ACTIVITY_JOIN_REQUEST", 124 | "cmd": "SUBSCRIBE" 125 | } 126 | ``` 127 | 128 | To unsubscribe from these events, resend with the command `UNSUBSCRIBE` 129 | 130 | ## Responding 131 | 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: 132 | ```json 133 | { 134 | "nonce": "5dc0c062-98c6-47a0-8922-15aerg126", 135 | "cmd": "SEND_ACTIVITY_JOIN_INVITE", 136 | "args": 137 | { 138 | "user_id": "53908232506183680" 139 | } 140 | } 141 | ``` 142 | 143 | To reject the request, use `CLOSE_ACTIVITY_REQUEST`: 144 | ```json 145 | { 146 | "nonce": "5dc0c062-98c6-47a0-8922-dasg256eafg", 147 | "cmd": "CLOSE_ACTIVITY_REQUEST", 148 | "args": 149 | { 150 | "user_id": "53908232506183680" 151 | } 152 | } 153 | ``` 154 | 155 | ## Notes 156 | Here are just some quick notes to help with some common troubleshooting problems. 157 | * 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. 158 | * 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. 159 | * 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. 160 | * All enums are `lower_snake_case`. 161 | * 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. 162 | * [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. 163 | * In the documentation, DISCORD_REPLY_IGNORE is just implemented the same as DISCORD_REPLY_NO. 164 | * 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. 165 | -------------------------------------------------------------------------------- /documentation/images/rp-dev-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/documentation/images/rp-dev-dashboard.png -------------------------------------------------------------------------------- /documentation/images/rp-profile-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/documentation/images/rp-profile-view.png -------------------------------------------------------------------------------- /documentation/images/rp-secret-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/documentation/images/rp-secret-example.png -------------------------------------------------------------------------------- /examples/button-clicker/.gitignore: -------------------------------------------------------------------------------- 1 | /Library/ 2 | /Temp/ 3 | /obj/ 4 | /Assets/Plugins/ 5 | /Assets/Plugins.meta 6 | *.sln 7 | *.csproj 8 | *.userprefs 9 | -------------------------------------------------------------------------------- /examples/button-clicker/Assets/DiscordController.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | [System.Serializable] 4 | public class DiscordJoinEvent : UnityEngine.Events.UnityEvent { } 5 | 6 | [System.Serializable] 7 | public class DiscordSpectateEvent : UnityEngine.Events.UnityEvent { } 8 | 9 | [System.Serializable] 10 | public class DiscordJoinRequestEvent : UnityEngine.Events.UnityEvent { } 11 | 12 | public class DiscordController : MonoBehaviour 13 | { 14 | public DiscordRpc.RichPresence presence = new DiscordRpc.RichPresence(); 15 | public string applicationId; 16 | public string optionalSteamId; 17 | public int callbackCalls; 18 | public int clickCounter; 19 | public DiscordRpc.JoinRequest joinRequest; 20 | public UnityEngine.Events.UnityEvent onConnect; 21 | public UnityEngine.Events.UnityEvent onDisconnect; 22 | public UnityEngine.Events.UnityEvent hasResponded; 23 | public DiscordJoinEvent onJoin; 24 | public DiscordJoinEvent onSpectate; 25 | public DiscordJoinRequestEvent onJoinRequest; 26 | 27 | DiscordRpc.EventHandlers handlers; 28 | 29 | public void OnClick() 30 | { 31 | Debug.Log("Discord: on click!"); 32 | clickCounter++; 33 | 34 | presence.details = string.Format("Button clicked {0} times", clickCounter); 35 | 36 | DiscordRpc.UpdatePresence(presence); 37 | } 38 | 39 | public void RequestRespondYes() 40 | { 41 | Debug.Log("Discord: responding yes to Ask to Join request"); 42 | DiscordRpc.Respond(joinRequest.userId, DiscordRpc.Reply.Yes); 43 | hasResponded.Invoke(); 44 | } 45 | 46 | public void RequestRespondNo() 47 | { 48 | Debug.Log("Discord: responding no to Ask to Join request"); 49 | DiscordRpc.Respond(joinRequest.userId, DiscordRpc.Reply.No); 50 | hasResponded.Invoke(); 51 | } 52 | 53 | public void ReadyCallback() 54 | { 55 | ++callbackCalls; 56 | Debug.Log("Discord: ready"); 57 | onConnect.Invoke(); 58 | } 59 | 60 | public void DisconnectedCallback(int errorCode, string message) 61 | { 62 | ++callbackCalls; 63 | Debug.Log(string.Format("Discord: disconnect {0}: {1}", errorCode, message)); 64 | onDisconnect.Invoke(); 65 | } 66 | 67 | public void ErrorCallback(int errorCode, string message) 68 | { 69 | ++callbackCalls; 70 | Debug.Log(string.Format("Discord: error {0}: {1}", errorCode, message)); 71 | } 72 | 73 | public void JoinCallback(string secret) 74 | { 75 | ++callbackCalls; 76 | Debug.Log(string.Format("Discord: join ({0})", secret)); 77 | onJoin.Invoke(secret); 78 | } 79 | 80 | public void SpectateCallback(string secret) 81 | { 82 | ++callbackCalls; 83 | Debug.Log(string.Format("Discord: spectate ({0})", secret)); 84 | onSpectate.Invoke(secret); 85 | } 86 | 87 | public void RequestCallback(ref DiscordRpc.JoinRequest request) 88 | { 89 | ++callbackCalls; 90 | Debug.Log(string.Format("Discord: join request {0}#{1}: {2}", request.username, request.discriminator, request.userId)); 91 | joinRequest = request; 92 | onJoinRequest.Invoke(request); 93 | } 94 | 95 | void Start() 96 | { 97 | } 98 | 99 | void Update() 100 | { 101 | DiscordRpc.RunCallbacks(); 102 | } 103 | 104 | void OnEnable() 105 | { 106 | Debug.Log("Discord: init"); 107 | callbackCalls = 0; 108 | 109 | handlers = new DiscordRpc.EventHandlers(); 110 | handlers.readyCallback = ReadyCallback; 111 | handlers.disconnectedCallback += DisconnectedCallback; 112 | handlers.errorCallback += ErrorCallback; 113 | handlers.joinCallback += JoinCallback; 114 | handlers.spectateCallback += SpectateCallback; 115 | handlers.requestCallback += RequestCallback; 116 | DiscordRpc.Initialize(applicationId, ref handlers, true, optionalSteamId); 117 | } 118 | 119 | void OnDisable() 120 | { 121 | Debug.Log("Discord: shutdown"); 122 | DiscordRpc.Shutdown(); 123 | } 124 | 125 | void OnDestroy() 126 | { 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/button-clicker/Assets/DiscordController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 27f0a5f59ffffa84c86547736e2e730a 3 | timeCreated: 1501697692 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /examples/button-clicker/Assets/DiscordRpc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | public class DiscordRpc 7 | { 8 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 9 | public delegate void ReadyCallback(); 10 | 11 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 12 | public delegate void DisconnectedCallback(int errorCode, string message); 13 | 14 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 15 | public delegate void ErrorCallback(int errorCode, string message); 16 | 17 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 18 | public delegate void JoinCallback(string secret); 19 | 20 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 21 | public delegate void SpectateCallback(string secret); 22 | 23 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 24 | public delegate void RequestCallback(ref JoinRequest request); 25 | 26 | public struct EventHandlers 27 | { 28 | public ReadyCallback readyCallback; 29 | public DisconnectedCallback disconnectedCallback; 30 | public ErrorCallback errorCallback; 31 | public JoinCallback joinCallback; 32 | public SpectateCallback spectateCallback; 33 | public RequestCallback requestCallback; 34 | } 35 | 36 | [Serializable, StructLayout(LayoutKind.Sequential)] 37 | public struct RichPresenceStruct 38 | { 39 | public IntPtr state; /* max 128 bytes */ 40 | public IntPtr details; /* max 128 bytes */ 41 | public long startTimestamp; 42 | public long endTimestamp; 43 | public IntPtr largeImageKey; /* max 32 bytes */ 44 | public IntPtr largeImageText; /* max 128 bytes */ 45 | public IntPtr smallImageKey; /* max 32 bytes */ 46 | public IntPtr smallImageText; /* max 128 bytes */ 47 | public IntPtr partyId; /* max 128 bytes */ 48 | public int partySize; 49 | public int partyMax; 50 | public IntPtr matchSecret; /* max 128 bytes */ 51 | public IntPtr joinSecret; /* max 128 bytes */ 52 | public IntPtr spectateSecret; /* max 128 bytes */ 53 | public bool instance; 54 | } 55 | 56 | [Serializable] 57 | public struct JoinRequest 58 | { 59 | public string userId; 60 | public string username; 61 | public string discriminator; 62 | public string avatar; 63 | } 64 | 65 | public enum Reply 66 | { 67 | No = 0, 68 | Yes = 1, 69 | Ignore = 2 70 | } 71 | 72 | [DllImport("discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)] 73 | public static extern void Initialize(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId); 74 | 75 | [DllImport("discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)] 76 | public static extern void Shutdown(); 77 | 78 | [DllImport("discord-rpc", EntryPoint = "Discord_RunCallbacks", CallingConvention = CallingConvention.Cdecl)] 79 | public static extern void RunCallbacks(); 80 | 81 | [DllImport("discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)] 82 | private static extern void UpdatePresenceNative(ref RichPresenceStruct presence); 83 | 84 | [DllImport("discord-rpc", EntryPoint = "Discord_ClearPresence", CallingConvention = CallingConvention.Cdecl)] 85 | public static extern void ClearPresence(); 86 | 87 | [DllImport("discord-rpc", EntryPoint = "Discord_Respond", CallingConvention = CallingConvention.Cdecl)] 88 | public static extern void Respond(string userId, Reply reply); 89 | 90 | [DllImport("discord-rpc", EntryPoint = "Discord_UpdateHandlers", CallingConvention = CallingConvention.Cdecl)] 91 | public static extern void UpdateHandlers(ref EventHandlers handlers); 92 | 93 | public static void UpdatePresence(RichPresence presence) 94 | { 95 | var presencestruct = presence.GetStruct(); 96 | UpdatePresenceNative(ref presencestruct); 97 | presence.FreeMem(); 98 | } 99 | 100 | public class RichPresence 101 | { 102 | private RichPresenceStruct _presence; 103 | private readonly List _buffers = new List(10); 104 | 105 | public string state; /* max 128 bytes */ 106 | public string details; /* max 128 bytes */ 107 | public long startTimestamp; 108 | public long endTimestamp; 109 | public string largeImageKey; /* max 32 bytes */ 110 | public string largeImageText; /* max 128 bytes */ 111 | public string smallImageKey; /* max 32 bytes */ 112 | public string smallImageText; /* max 128 bytes */ 113 | public string partyId; /* max 128 bytes */ 114 | public int partySize; 115 | public int partyMax; 116 | public string matchSecret; /* max 128 bytes */ 117 | public string joinSecret; /* max 128 bytes */ 118 | public string spectateSecret; /* max 128 bytes */ 119 | public bool instance; 120 | 121 | /// 122 | /// Get the reprensentation of this instance 123 | /// 124 | /// reprensentation of this instance 125 | internal RichPresenceStruct GetStruct() 126 | { 127 | if (_buffers.Count > 0) 128 | { 129 | FreeMem(); 130 | } 131 | 132 | _presence.state = StrToPtr(state, 128); 133 | _presence.details = StrToPtr(details, 128); 134 | _presence.startTimestamp = startTimestamp; 135 | _presence.endTimestamp = endTimestamp; 136 | _presence.largeImageKey = StrToPtr(largeImageKey, 32); 137 | _presence.largeImageText = StrToPtr(largeImageText, 128); 138 | _presence.smallImageKey = StrToPtr(smallImageKey, 32); 139 | _presence.smallImageText = StrToPtr(smallImageText, 128); 140 | _presence.partyId = StrToPtr(partyId, 128); 141 | _presence.partySize = partySize; 142 | _presence.partyMax = partyMax; 143 | _presence.matchSecret = StrToPtr(matchSecret, 128); 144 | _presence.joinSecret = StrToPtr(joinSecret, 128); 145 | _presence.spectateSecret = StrToPtr(spectateSecret, 128); 146 | _presence.instance = instance; 147 | 148 | return _presence; 149 | } 150 | 151 | /// 152 | /// Returns a pointer to a representation of the given string with a size of maxbytes 153 | /// 154 | /// String to convert 155 | /// Max number of bytes to use 156 | /// Pointer to the UTF-8 representation of 157 | private IntPtr StrToPtr(string input, int maxbytes) 158 | { 159 | if (string.IsNullOrEmpty(input)) return IntPtr.Zero; 160 | var convstr = StrClampBytes(input, maxbytes); 161 | var convbytecnt = Encoding.UTF8.GetByteCount(convstr); 162 | var buffer = Marshal.AllocHGlobal(convbytecnt); 163 | _buffers.Add(buffer); 164 | Marshal.Copy(Encoding.UTF8.GetBytes(convstr), 0, buffer, convbytecnt); 165 | return buffer; 166 | } 167 | 168 | /// 169 | /// Convert string to UTF-8 and add null termination 170 | /// 171 | /// string to convert 172 | /// UTF-8 representation of with added null termination 173 | private static string StrToUtf8NullTerm(string toconv) 174 | { 175 | var str = toconv.Trim(); 176 | var bytes = Encoding.Default.GetBytes(str); 177 | if (bytes.Length > 0 && bytes[bytes.Length - 1] != 0) 178 | { 179 | str += "\0\0"; 180 | } 181 | return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(str)); 182 | } 183 | 184 | /// 185 | /// Clamp the string to the given byte length preserving null termination 186 | /// 187 | /// string to clamp 188 | /// max bytes the resulting string should have (including null termination) 189 | /// null terminated string with a byte length less or equal to 190 | private static string StrClampBytes(string toclamp, int maxbytes) 191 | { 192 | var str = StrToUtf8NullTerm(toclamp); 193 | var strbytes = Encoding.UTF8.GetBytes(str); 194 | 195 | if (strbytes.Length <= maxbytes) 196 | { 197 | return str; 198 | } 199 | 200 | var newstrbytes = new byte[] { }; 201 | Array.Copy(strbytes, 0, newstrbytes, 0, maxbytes - 1); 202 | newstrbytes[newstrbytes.Length - 1] = 0; 203 | newstrbytes[newstrbytes.Length - 2] = 0; 204 | 205 | return Encoding.UTF8.GetString(newstrbytes); 206 | } 207 | 208 | /// 209 | /// Free the allocated memory for conversion to 210 | /// 211 | internal void FreeMem() 212 | { 213 | for (var i = _buffers.Count - 1; i >= 0; i--) 214 | { 215 | Marshal.FreeHGlobal(_buffers[i]); 216 | _buffers.RemoveAt(i); 217 | } 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /examples/button-clicker/Assets/DiscordRpc.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b4474a677de9d80409e98c5393ec5b1e 3 | timeCreated: 1501697692 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /examples/button-clicker/Assets/Editor/BuildHelper.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using System.Diagnostics; 3 | using System.IO; 4 | 5 | [InitializeOnLoad] 6 | public class ScriptBatch 7 | { 8 | static ScriptBatch() 9 | { 10 | EnsureDLL(); 11 | } 12 | 13 | public static bool FileExists(string filename) 14 | { 15 | return new FileInfo(filename).Exists; 16 | } 17 | 18 | public static bool RunRpcBuildScript() 19 | { 20 | UnityEngine.Debug.Log("Try to run build script"); 21 | 22 | Process proc = new Process(); 23 | #if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX 24 | proc.StartInfo.UseShellExecute = false; 25 | // brew installs cmake in /usr/local/bin, which Unity seems to strip from PATH? 26 | string newPath = proc.StartInfo.EnvironmentVariables["PATH"] + ":/usr/local/bin"; 27 | proc.StartInfo.EnvironmentVariables["PATH"] = newPath; 28 | #endif 29 | proc.StartInfo.FileName = "python"; 30 | proc.StartInfo.Arguments = "build.py unity"; 31 | proc.StartInfo.WorkingDirectory = "../.."; 32 | proc.Start(); 33 | proc.WaitForExit(); 34 | return proc.ExitCode == 0; 35 | } 36 | 37 | public static void EnsureDLL() 38 | { 39 | #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN 40 | string[] dstDirs = { "Assets/Plugins", "Assets/Plugins/x86", "Assets/Plugins/x86_64" }; 41 | string[] dstDlls = { "Assets/Plugins/x86/discord-rpc.dll", "Assets/Plugins/x86_64/discord-rpc.dll" }; 42 | string[] srcDlls = { "../../builds/install/win64-dynamic/bin/discord-rpc.dll", "../../builds/install/win64-dynamic/bin/discord-rpc.dll" }; 43 | #elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX 44 | string[] dstDirs = { "Assets/Plugins" }; 45 | string[] dstDlls = { "Assets/Plugins/discord-rpc.bundle" }; 46 | string[] srcDlls = { "../../builds/install/osx-dynamic/lib/libdiscord-rpc.dylib" }; 47 | #else 48 | string[] dstDirs = { "Assets/Plugins", "Assets/Plugins/x86", "Assets/Plugins/x86_64" }; 49 | string[] dstDlls = { "Assets/Plugins/discord-rpc.so" }; 50 | string[] srcDlls = { "../../builds/install/linux-dynamic/lib/libdiscord-rpc.so" }; 51 | #endif 52 | 53 | Debug.Assert(dstDlls.Length == srcDlls.Length); 54 | 55 | bool exists = true; 56 | foreach (string fname in dstDlls) 57 | { 58 | if (!FileExists(fname)) 59 | { 60 | exists = false; 61 | break; 62 | } 63 | } 64 | 65 | if (exists) 66 | { 67 | return; 68 | } 69 | 70 | exists = true; 71 | foreach (string fname in srcDlls) 72 | { 73 | if (!FileExists(fname)) 74 | { 75 | exists = false; 76 | break; 77 | } 78 | } 79 | 80 | if (!exists) 81 | { 82 | if (!RunRpcBuildScript()) 83 | { 84 | UnityEngine.Debug.LogError("Build failed"); 85 | return; 86 | } 87 | } 88 | 89 | // make sure the dirs exist 90 | foreach (string dirname in dstDirs) 91 | { 92 | Directory.CreateDirectory(dirname); 93 | } 94 | 95 | // Copy dlls 96 | for (int i = 0; i < dstDlls.Length; ++i) 97 | { 98 | FileUtil.CopyFileOrDirectory(srcDlls[i], dstDlls[i]); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /examples/button-clicker/Assets/Editor/BuildHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e5aecc4633e5f594b85eaa39f49bb402 3 | timeCreated: 1512071254 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /examples/button-clicker/Assets/main.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3b03d21bb25fa494e8694cd6e4b6d769 3 | timeCreated: 1501696924 4 | licenseType: Free 5 | DefaultImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/AudioManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!11 &1 4 | AudioManager: 5 | m_ObjectHideFlags: 0 6 | m_Volume: 1 7 | Rolloff Scale: 1 8 | Doppler Factor: 1 9 | Default Speaker Mode: 2 10 | m_SampleRate: 0 11 | m_DSPBufferSize: 0 12 | m_VirtualVoiceCount: 512 13 | m_RealVoiceCount: 32 14 | m_SpatializerPlugin: 15 | m_AmbisonicDecoderPlugin: 16 | m_DisableAudio: 0 17 | m_VirtualizeEffects: 1 18 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/ClusterInputManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!236 &1 4 | ClusterInputManager: 5 | m_ObjectHideFlags: 0 6 | m_Inputs: [] 7 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/DynamicsManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!55 &1 4 | PhysicsManager: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 3 7 | m_Gravity: {x: 0, y: -9.81, z: 0} 8 | m_DefaultMaterial: {fileID: 0} 9 | m_BounceThreshold: 2 10 | m_SleepThreshold: 0.005 11 | m_DefaultContactOffset: 0.01 12 | m_DefaultSolverIterations: 6 13 | m_DefaultSolverVelocityIterations: 1 14 | m_QueriesHitBackfaces: 0 15 | m_QueriesHitTriggers: 1 16 | m_EnableAdaptiveForce: 0 17 | m_EnablePCM: 1 18 | m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 19 | m_AutoSimulation: 1 20 | m_AutoSyncTransforms: 1 21 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/EditorBuildSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!1045 &1 4 | EditorBuildSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | m_Scenes: 8 | - enabled: 1 9 | path: Assets/main.unity 10 | guid: 3b03d21bb25fa494e8694cd6e4b6d769 11 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/EditorSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!159 &1 4 | EditorSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 4 7 | m_ExternalVersionControlSupport: Visible Meta Files 8 | m_SerializationMode: 2 9 | m_DefaultBehaviorMode: 1 10 | m_SpritePackerMode: 4 11 | m_SpritePackerPaddingPower: 1 12 | m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd 13 | m_ProjectGenerationRootNamespace: 14 | m_UserGeneratedProjectSuffix: 15 | m_CollabEditorSettings: 16 | inProgressEnabled: 1 17 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/GraphicsSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!30 &1 4 | GraphicsSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 12 7 | m_Deferred: 8 | m_Mode: 1 9 | m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} 10 | m_DeferredReflections: 11 | m_Mode: 1 12 | m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} 13 | m_ScreenSpaceShadows: 14 | m_Mode: 1 15 | m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} 16 | m_LegacyDeferred: 17 | m_Mode: 1 18 | m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} 19 | m_DepthNormals: 20 | m_Mode: 1 21 | m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} 22 | m_MotionVectors: 23 | m_Mode: 1 24 | m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} 25 | m_LightHalo: 26 | m_Mode: 1 27 | m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} 28 | m_LensFlare: 29 | m_Mode: 1 30 | m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} 31 | m_AlwaysIncludedShaders: 32 | - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} 33 | - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} 34 | - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} 35 | - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} 36 | - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} 37 | - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} 38 | m_PreloadedShaders: [] 39 | m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, 40 | type: 0} 41 | m_CustomRenderPipeline: {fileID: 0} 42 | m_TransparencySortMode: 0 43 | m_TransparencySortAxis: {x: 0, y: 0, z: 1} 44 | m_DefaultRenderingPath: 1 45 | m_DefaultMobileRenderingPath: 1 46 | m_TierSettings: [] 47 | m_LightmapStripping: 0 48 | m_FogStripping: 0 49 | m_InstancingStripping: 0 50 | m_LightmapKeepPlain: 1 51 | m_LightmapKeepDirCombined: 1 52 | m_LightmapKeepDynamicPlain: 1 53 | m_LightmapKeepDynamicDirCombined: 1 54 | m_LightmapKeepShadowMask: 1 55 | m_LightmapKeepSubtractive: 1 56 | m_FogKeepLinear: 1 57 | m_FogKeepExp: 1 58 | m_FogKeepExp2: 1 59 | m_AlbedoSwatchInfos: [] 60 | m_LightsUseLinearIntensity: 0 61 | m_LightsUseColorTemperature: 0 62 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/InputManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!13 &1 4 | InputManager: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | m_Axes: 8 | - serializedVersion: 3 9 | m_Name: Horizontal 10 | descriptiveName: 11 | descriptiveNegativeName: 12 | negativeButton: left 13 | positiveButton: right 14 | altNegativeButton: a 15 | altPositiveButton: d 16 | gravity: 3 17 | dead: 0.001 18 | sensitivity: 3 19 | snap: 1 20 | invert: 0 21 | type: 0 22 | axis: 0 23 | joyNum: 0 24 | - serializedVersion: 3 25 | m_Name: Vertical 26 | descriptiveName: 27 | descriptiveNegativeName: 28 | negativeButton: down 29 | positiveButton: up 30 | altNegativeButton: s 31 | altPositiveButton: w 32 | gravity: 3 33 | dead: 0.001 34 | sensitivity: 3 35 | snap: 1 36 | invert: 0 37 | type: 0 38 | axis: 0 39 | joyNum: 0 40 | - serializedVersion: 3 41 | m_Name: Fire1 42 | descriptiveName: 43 | descriptiveNegativeName: 44 | negativeButton: 45 | positiveButton: left ctrl 46 | altNegativeButton: 47 | altPositiveButton: mouse 0 48 | gravity: 1000 49 | dead: 0.001 50 | sensitivity: 1000 51 | snap: 0 52 | invert: 0 53 | type: 0 54 | axis: 0 55 | joyNum: 0 56 | - serializedVersion: 3 57 | m_Name: Fire2 58 | descriptiveName: 59 | descriptiveNegativeName: 60 | negativeButton: 61 | positiveButton: left alt 62 | altNegativeButton: 63 | altPositiveButton: mouse 1 64 | gravity: 1000 65 | dead: 0.001 66 | sensitivity: 1000 67 | snap: 0 68 | invert: 0 69 | type: 0 70 | axis: 0 71 | joyNum: 0 72 | - serializedVersion: 3 73 | m_Name: Fire3 74 | descriptiveName: 75 | descriptiveNegativeName: 76 | negativeButton: 77 | positiveButton: left shift 78 | altNegativeButton: 79 | altPositiveButton: mouse 2 80 | gravity: 1000 81 | dead: 0.001 82 | sensitivity: 1000 83 | snap: 0 84 | invert: 0 85 | type: 0 86 | axis: 0 87 | joyNum: 0 88 | - serializedVersion: 3 89 | m_Name: Jump 90 | descriptiveName: 91 | descriptiveNegativeName: 92 | negativeButton: 93 | positiveButton: space 94 | altNegativeButton: 95 | altPositiveButton: 96 | gravity: 1000 97 | dead: 0.001 98 | sensitivity: 1000 99 | snap: 0 100 | invert: 0 101 | type: 0 102 | axis: 0 103 | joyNum: 0 104 | - serializedVersion: 3 105 | m_Name: Mouse X 106 | descriptiveName: 107 | descriptiveNegativeName: 108 | negativeButton: 109 | positiveButton: 110 | altNegativeButton: 111 | altPositiveButton: 112 | gravity: 0 113 | dead: 0 114 | sensitivity: 0.1 115 | snap: 0 116 | invert: 0 117 | type: 1 118 | axis: 0 119 | joyNum: 0 120 | - serializedVersion: 3 121 | m_Name: Mouse Y 122 | descriptiveName: 123 | descriptiveNegativeName: 124 | negativeButton: 125 | positiveButton: 126 | altNegativeButton: 127 | altPositiveButton: 128 | gravity: 0 129 | dead: 0 130 | sensitivity: 0.1 131 | snap: 0 132 | invert: 0 133 | type: 1 134 | axis: 1 135 | joyNum: 0 136 | - serializedVersion: 3 137 | m_Name: Mouse ScrollWheel 138 | descriptiveName: 139 | descriptiveNegativeName: 140 | negativeButton: 141 | positiveButton: 142 | altNegativeButton: 143 | altPositiveButton: 144 | gravity: 0 145 | dead: 0 146 | sensitivity: 0.1 147 | snap: 0 148 | invert: 0 149 | type: 1 150 | axis: 2 151 | joyNum: 0 152 | - serializedVersion: 3 153 | m_Name: Horizontal 154 | descriptiveName: 155 | descriptiveNegativeName: 156 | negativeButton: 157 | positiveButton: 158 | altNegativeButton: 159 | altPositiveButton: 160 | gravity: 0 161 | dead: 0.19 162 | sensitivity: 1 163 | snap: 0 164 | invert: 0 165 | type: 2 166 | axis: 0 167 | joyNum: 0 168 | - serializedVersion: 3 169 | m_Name: Vertical 170 | descriptiveName: 171 | descriptiveNegativeName: 172 | negativeButton: 173 | positiveButton: 174 | altNegativeButton: 175 | altPositiveButton: 176 | gravity: 0 177 | dead: 0.19 178 | sensitivity: 1 179 | snap: 0 180 | invert: 1 181 | type: 2 182 | axis: 1 183 | joyNum: 0 184 | - serializedVersion: 3 185 | m_Name: Fire1 186 | descriptiveName: 187 | descriptiveNegativeName: 188 | negativeButton: 189 | positiveButton: joystick button 0 190 | altNegativeButton: 191 | altPositiveButton: 192 | gravity: 1000 193 | dead: 0.001 194 | sensitivity: 1000 195 | snap: 0 196 | invert: 0 197 | type: 0 198 | axis: 0 199 | joyNum: 0 200 | - serializedVersion: 3 201 | m_Name: Fire2 202 | descriptiveName: 203 | descriptiveNegativeName: 204 | negativeButton: 205 | positiveButton: joystick button 1 206 | altNegativeButton: 207 | altPositiveButton: 208 | gravity: 1000 209 | dead: 0.001 210 | sensitivity: 1000 211 | snap: 0 212 | invert: 0 213 | type: 0 214 | axis: 0 215 | joyNum: 0 216 | - serializedVersion: 3 217 | m_Name: Fire3 218 | descriptiveName: 219 | descriptiveNegativeName: 220 | negativeButton: 221 | positiveButton: joystick button 2 222 | altNegativeButton: 223 | altPositiveButton: 224 | gravity: 1000 225 | dead: 0.001 226 | sensitivity: 1000 227 | snap: 0 228 | invert: 0 229 | type: 0 230 | axis: 0 231 | joyNum: 0 232 | - serializedVersion: 3 233 | m_Name: Jump 234 | descriptiveName: 235 | descriptiveNegativeName: 236 | negativeButton: 237 | positiveButton: joystick button 3 238 | altNegativeButton: 239 | altPositiveButton: 240 | gravity: 1000 241 | dead: 0.001 242 | sensitivity: 1000 243 | snap: 0 244 | invert: 0 245 | type: 0 246 | axis: 0 247 | joyNum: 0 248 | - serializedVersion: 3 249 | m_Name: Submit 250 | descriptiveName: 251 | descriptiveNegativeName: 252 | negativeButton: 253 | positiveButton: return 254 | altNegativeButton: 255 | altPositiveButton: joystick button 0 256 | gravity: 1000 257 | dead: 0.001 258 | sensitivity: 1000 259 | snap: 0 260 | invert: 0 261 | type: 0 262 | axis: 0 263 | joyNum: 0 264 | - serializedVersion: 3 265 | m_Name: Submit 266 | descriptiveName: 267 | descriptiveNegativeName: 268 | negativeButton: 269 | positiveButton: enter 270 | altNegativeButton: 271 | altPositiveButton: space 272 | gravity: 1000 273 | dead: 0.001 274 | sensitivity: 1000 275 | snap: 0 276 | invert: 0 277 | type: 0 278 | axis: 0 279 | joyNum: 0 280 | - serializedVersion: 3 281 | m_Name: Cancel 282 | descriptiveName: 283 | descriptiveNegativeName: 284 | negativeButton: 285 | positiveButton: escape 286 | altNegativeButton: 287 | altPositiveButton: joystick button 1 288 | gravity: 1000 289 | dead: 0.001 290 | sensitivity: 1000 291 | snap: 0 292 | invert: 0 293 | type: 0 294 | axis: 0 295 | joyNum: 0 296 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/NavMeshAreas.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!126 &1 4 | NavMeshProjectSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | areas: 8 | - name: Walkable 9 | cost: 1 10 | - name: Not Walkable 11 | cost: 1 12 | - name: Jump 13 | cost: 2 14 | - name: 15 | cost: 1 16 | - name: 17 | cost: 1 18 | - name: 19 | cost: 1 20 | - name: 21 | cost: 1 22 | - name: 23 | cost: 1 24 | - name: 25 | cost: 1 26 | - name: 27 | cost: 1 28 | - name: 29 | cost: 1 30 | - name: 31 | cost: 1 32 | - name: 33 | cost: 1 34 | - name: 35 | cost: 1 36 | - name: 37 | cost: 1 38 | - name: 39 | cost: 1 40 | - name: 41 | cost: 1 42 | - name: 43 | cost: 1 44 | - name: 45 | cost: 1 46 | - name: 47 | cost: 1 48 | - name: 49 | cost: 1 50 | - name: 51 | cost: 1 52 | - name: 53 | cost: 1 54 | - name: 55 | cost: 1 56 | - name: 57 | cost: 1 58 | - name: 59 | cost: 1 60 | - name: 61 | cost: 1 62 | - name: 63 | cost: 1 64 | - name: 65 | cost: 1 66 | - name: 67 | cost: 1 68 | - name: 69 | cost: 1 70 | - name: 71 | cost: 1 72 | m_LastAgentTypeID: -887442657 73 | m_Settings: 74 | - serializedVersion: 2 75 | agentTypeID: 0 76 | agentRadius: 0.5 77 | agentHeight: 2 78 | agentSlope: 45 79 | agentClimb: 0.75 80 | ledgeDropHeight: 0 81 | maxJumpAcrossDistance: 0 82 | minRegionArea: 2 83 | manualCellSize: 0 84 | cellSize: 0.16666667 85 | manualTileSize: 0 86 | tileSize: 256 87 | accuratePlacement: 0 88 | m_SettingNames: 89 | - Humanoid 90 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/NetworkManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!149 &1 4 | NetworkManager: 5 | m_ObjectHideFlags: 0 6 | m_DebugLevel: 0 7 | m_Sendrate: 15 8 | m_AssetToPrefab: {} 9 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/Physics2DSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!19 &1 4 | Physics2DSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 3 7 | m_Gravity: {x: 0, y: -9.81} 8 | m_DefaultMaterial: {fileID: 0} 9 | m_VelocityIterations: 8 10 | m_PositionIterations: 3 11 | m_VelocityThreshold: 1 12 | m_MaxLinearCorrection: 0.2 13 | m_MaxAngularCorrection: 8 14 | m_MaxTranslationSpeed: 100 15 | m_MaxRotationSpeed: 360 16 | m_BaumgarteScale: 0.2 17 | m_BaumgarteTimeOfImpactScale: 0.75 18 | m_TimeToSleep: 0.5 19 | m_LinearSleepTolerance: 0.01 20 | m_AngularSleepTolerance: 2 21 | m_DefaultContactOffset: 0.01 22 | m_AutoSimulation: 1 23 | m_QueriesHitTriggers: 1 24 | m_QueriesStartInColliders: 1 25 | m_ChangeStopsCallbacks: 0 26 | m_CallbacksOnDisable: 1 27 | m_AutoSyncTransforms: 1 28 | m_AlwaysShowColliders: 0 29 | m_ShowColliderSleep: 1 30 | m_ShowColliderContacts: 0 31 | m_ShowColliderAABB: 0 32 | m_ContactArrowScale: 0.2 33 | m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} 34 | m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} 35 | m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} 36 | m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} 37 | m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 38 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/ProjectSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!129 &1 4 | PlayerSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 13 7 | productGUID: 5eccc60d3e382a346a65f512d6b81b84 8 | AndroidProfiler: 0 9 | AndroidFilterTouchesWhenObscured: 0 10 | defaultScreenOrientation: 4 11 | targetDevice: 2 12 | useOnDemandResources: 0 13 | accelerometerFrequency: 60 14 | companyName: Discord Inc. 15 | productName: button-clicker 16 | defaultCursor: {fileID: 0} 17 | cursorHotspot: {x: 0, y: 0} 18 | m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} 19 | m_ShowUnitySplashScreen: 1 20 | m_ShowUnitySplashLogo: 1 21 | m_SplashScreenOverlayOpacity: 1 22 | m_SplashScreenAnimation: 1 23 | m_SplashScreenLogoStyle: 1 24 | m_SplashScreenDrawMode: 0 25 | m_SplashScreenBackgroundAnimationZoom: 1 26 | m_SplashScreenLogoAnimationZoom: 1 27 | m_SplashScreenBackgroundLandscapeAspect: 1 28 | m_SplashScreenBackgroundPortraitAspect: 1 29 | m_SplashScreenBackgroundLandscapeUvs: 30 | serializedVersion: 2 31 | x: 0 32 | y: 0 33 | width: 1 34 | height: 1 35 | m_SplashScreenBackgroundPortraitUvs: 36 | serializedVersion: 2 37 | x: 0 38 | y: 0 39 | width: 1 40 | height: 1 41 | m_SplashScreenLogos: [] 42 | m_VirtualRealitySplashScreen: {fileID: 0} 43 | m_HolographicTrackingLossScreen: {fileID: 0} 44 | defaultScreenWidth: 1024 45 | defaultScreenHeight: 768 46 | defaultScreenWidthWeb: 960 47 | defaultScreenHeightWeb: 600 48 | m_StereoRenderingPath: 0 49 | m_ActiveColorSpace: 0 50 | m_MTRendering: 1 51 | m_StackTraceTypes: 010000000100000001000000010000000100000001000000 52 | iosShowActivityIndicatorOnLoading: -1 53 | androidShowActivityIndicatorOnLoading: -1 54 | tizenShowActivityIndicatorOnLoading: -1 55 | iosAppInBackgroundBehavior: 0 56 | displayResolutionDialog: 1 57 | iosAllowHTTPDownload: 1 58 | allowedAutorotateToPortrait: 1 59 | allowedAutorotateToPortraitUpsideDown: 1 60 | allowedAutorotateToLandscapeRight: 1 61 | allowedAutorotateToLandscapeLeft: 1 62 | useOSAutorotation: 1 63 | use32BitDisplayBuffer: 1 64 | disableDepthAndStencilBuffers: 0 65 | androidBlitType: 0 66 | defaultIsFullScreen: 1 67 | defaultIsNativeResolution: 1 68 | macRetinaSupport: 1 69 | runInBackground: 0 70 | captureSingleScreen: 0 71 | muteOtherAudioSources: 0 72 | Prepare IOS For Recording: 0 73 | Force IOS Speakers When Recording: 0 74 | submitAnalytics: 1 75 | usePlayerLog: 1 76 | bakeCollisionMeshes: 0 77 | forceSingleInstance: 0 78 | resizableWindow: 0 79 | useMacAppStoreValidation: 0 80 | macAppStoreCategory: public.app-category.games 81 | gpuSkinning: 0 82 | graphicsJobs: 0 83 | xboxPIXTextureCapture: 0 84 | xboxEnableAvatar: 0 85 | xboxEnableKinect: 0 86 | xboxEnableKinectAutoTracking: 0 87 | xboxEnableFitness: 0 88 | visibleInBackground: 1 89 | allowFullscreenSwitch: 1 90 | graphicsJobMode: 0 91 | macFullscreenMode: 2 92 | d3d9FullscreenMode: 1 93 | d3d11FullscreenMode: 1 94 | xboxSpeechDB: 0 95 | xboxEnableHeadOrientation: 0 96 | xboxEnableGuest: 0 97 | xboxEnablePIXSampling: 0 98 | metalFramebufferOnly: 0 99 | n3dsDisableStereoscopicView: 0 100 | n3dsEnableSharedListOpt: 1 101 | n3dsEnableVSync: 0 102 | ignoreAlphaClear: 0 103 | xboxOneResolution: 0 104 | xboxOneMonoLoggingLevel: 0 105 | xboxOneLoggingLevel: 1 106 | xboxOneDisableEsram: 0 107 | xboxOnePresentImmediateThreshold: 0 108 | videoMemoryForVertexBuffers: 0 109 | psp2PowerMode: 0 110 | psp2AcquireBGM: 1 111 | wiiUTVResolution: 0 112 | wiiUGamePadMSAA: 1 113 | wiiUSupportsNunchuk: 0 114 | wiiUSupportsClassicController: 0 115 | wiiUSupportsBalanceBoard: 0 116 | wiiUSupportsMotionPlus: 0 117 | wiiUSupportsProController: 0 118 | wiiUAllowScreenCapture: 1 119 | wiiUControllerCount: 0 120 | m_SupportedAspectRatios: 121 | 4:3: 1 122 | 5:4: 1 123 | 16:10: 1 124 | 16:9: 1 125 | Others: 1 126 | bundleVersion: 1.0 127 | preloadedAssets: [] 128 | metroInputSource: 0 129 | m_HolographicPauseOnTrackingLoss: 1 130 | xboxOneDisableKinectGpuReservation: 0 131 | xboxOneEnable7thCore: 0 132 | vrSettings: 133 | cardboard: 134 | depthFormat: 0 135 | enableTransitionView: 0 136 | daydream: 137 | depthFormat: 0 138 | useSustainedPerformanceMode: 0 139 | enableVideoLayer: 0 140 | useProtectedVideoMemory: 0 141 | hololens: 142 | depthFormat: 1 143 | protectGraphicsMemory: 0 144 | useHDRDisplay: 0 145 | m_ColorGamuts: 00000000 146 | targetPixelDensity: 0 147 | resolutionScalingMode: 0 148 | androidSupportedAspectRatio: 1 149 | androidMaxAspectRatio: 2.1 150 | applicationIdentifier: {} 151 | buildNumber: {} 152 | AndroidBundleVersionCode: 1 153 | AndroidMinSdkVersion: 16 154 | AndroidTargetSdkVersion: 0 155 | AndroidPreferredInstallLocation: 1 156 | aotOptions: 157 | stripEngineCode: 1 158 | iPhoneStrippingLevel: 0 159 | iPhoneScriptCallOptimization: 0 160 | ForceInternetPermission: 0 161 | ForceSDCardPermission: 0 162 | CreateWallpaper: 0 163 | APKExpansionFiles: 0 164 | keepLoadedShadersAlive: 0 165 | StripUnusedMeshComponents: 0 166 | VertexChannelCompressionMask: 167 | serializedVersion: 2 168 | m_Bits: 238 169 | iPhoneSdkVersion: 988 170 | iOSTargetOSVersionString: 7.0 171 | tvOSSdkVersion: 0 172 | tvOSRequireExtendedGameController: 0 173 | tvOSTargetOSVersionString: 9.0 174 | uIPrerenderedIcon: 0 175 | uIRequiresPersistentWiFi: 0 176 | uIRequiresFullScreen: 1 177 | uIStatusBarHidden: 1 178 | uIExitOnSuspend: 0 179 | uIStatusBarStyle: 0 180 | iPhoneSplashScreen: {fileID: 0} 181 | iPhoneHighResSplashScreen: {fileID: 0} 182 | iPhoneTallHighResSplashScreen: {fileID: 0} 183 | iPhone47inSplashScreen: {fileID: 0} 184 | iPhone55inPortraitSplashScreen: {fileID: 0} 185 | iPhone55inLandscapeSplashScreen: {fileID: 0} 186 | iPadPortraitSplashScreen: {fileID: 0} 187 | iPadHighResPortraitSplashScreen: {fileID: 0} 188 | iPadLandscapeSplashScreen: {fileID: 0} 189 | iPadHighResLandscapeSplashScreen: {fileID: 0} 190 | appleTVSplashScreen: {fileID: 0} 191 | tvOSSmallIconLayers: [] 192 | tvOSLargeIconLayers: [] 193 | tvOSTopShelfImageLayers: [] 194 | tvOSTopShelfImageWideLayers: [] 195 | iOSLaunchScreenType: 0 196 | iOSLaunchScreenPortrait: {fileID: 0} 197 | iOSLaunchScreenLandscape: {fileID: 0} 198 | iOSLaunchScreenBackgroundColor: 199 | serializedVersion: 2 200 | rgba: 0 201 | iOSLaunchScreenFillPct: 100 202 | iOSLaunchScreenSize: 100 203 | iOSLaunchScreenCustomXibPath: 204 | iOSLaunchScreeniPadType: 0 205 | iOSLaunchScreeniPadImage: {fileID: 0} 206 | iOSLaunchScreeniPadBackgroundColor: 207 | serializedVersion: 2 208 | rgba: 0 209 | iOSLaunchScreeniPadFillPct: 100 210 | iOSLaunchScreeniPadSize: 100 211 | iOSLaunchScreeniPadCustomXibPath: 212 | iOSDeviceRequirements: [] 213 | iOSURLSchemes: [] 214 | iOSBackgroundModes: 0 215 | iOSMetalForceHardShadows: 0 216 | metalEditorSupport: 1 217 | metalAPIValidation: 1 218 | iOSRenderExtraFrameOnPause: 0 219 | appleDeveloperTeamID: 220 | iOSManualSigningProvisioningProfileID: 221 | tvOSManualSigningProvisioningProfileID: 222 | appleEnableAutomaticSigning: 0 223 | AndroidTargetDevice: 0 224 | AndroidSplashScreenScale: 0 225 | androidSplashScreen: {fileID: 0} 226 | AndroidKeystoreName: 227 | AndroidKeyaliasName: 228 | AndroidTVCompatibility: 1 229 | AndroidIsGame: 1 230 | AndroidEnableTango: 0 231 | androidEnableBanner: 1 232 | androidUseLowAccuracyLocation: 0 233 | m_AndroidBanners: 234 | - width: 320 235 | height: 180 236 | banner: {fileID: 0} 237 | androidGamepadSupportLevel: 0 238 | resolutionDialogBanner: {fileID: 0} 239 | m_BuildTargetIcons: [] 240 | m_BuildTargetBatching: [] 241 | m_BuildTargetGraphicsAPIs: [] 242 | m_BuildTargetVRSettings: [] 243 | m_BuildTargetEnableVuforiaSettings: [] 244 | openGLRequireES31: 0 245 | openGLRequireES31AEP: 0 246 | m_TemplateCustomTags: {} 247 | mobileMTRendering: 248 | Android: 1 249 | iPhone: 1 250 | tvOS: 1 251 | wiiUTitleID: 0005000011000000 252 | wiiUGroupID: 00010000 253 | wiiUCommonSaveSize: 4096 254 | wiiUAccountSaveSize: 2048 255 | wiiUOlvAccessKey: 0 256 | wiiUTinCode: 0 257 | wiiUJoinGameId: 0 258 | wiiUJoinGameModeMask: 0000000000000000 259 | wiiUCommonBossSize: 0 260 | wiiUAccountBossSize: 0 261 | wiiUAddOnUniqueIDs: [] 262 | wiiUMainThreadStackSize: 3072 263 | wiiULoaderThreadStackSize: 1024 264 | wiiUSystemHeapSize: 128 265 | wiiUTVStartupScreen: {fileID: 0} 266 | wiiUGamePadStartupScreen: {fileID: 0} 267 | wiiUDrcBufferDisabled: 0 268 | wiiUProfilerLibPath: 269 | playModeTestRunnerEnabled: 0 270 | actionOnDotNetUnhandledException: 1 271 | enableInternalProfiler: 0 272 | logObjCUncaughtExceptions: 1 273 | enableCrashReportAPI: 0 274 | cameraUsageDescription: 275 | locationUsageDescription: 276 | microphoneUsageDescription: 277 | switchNetLibKey: 278 | switchSocketMemoryPoolSize: 6144 279 | switchSocketAllocatorPoolSize: 128 280 | switchSocketConcurrencyLimit: 14 281 | switchScreenResolutionBehavior: 2 282 | switchUseCPUProfiler: 0 283 | switchApplicationID: 0x01004b9000490000 284 | switchNSODependencies: 285 | switchTitleNames_0: 286 | switchTitleNames_1: 287 | switchTitleNames_2: 288 | switchTitleNames_3: 289 | switchTitleNames_4: 290 | switchTitleNames_5: 291 | switchTitleNames_6: 292 | switchTitleNames_7: 293 | switchTitleNames_8: 294 | switchTitleNames_9: 295 | switchTitleNames_10: 296 | switchTitleNames_11: 297 | switchPublisherNames_0: 298 | switchPublisherNames_1: 299 | switchPublisherNames_2: 300 | switchPublisherNames_3: 301 | switchPublisherNames_4: 302 | switchPublisherNames_5: 303 | switchPublisherNames_6: 304 | switchPublisherNames_7: 305 | switchPublisherNames_8: 306 | switchPublisherNames_9: 307 | switchPublisherNames_10: 308 | switchPublisherNames_11: 309 | switchIcons_0: {fileID: 0} 310 | switchIcons_1: {fileID: 0} 311 | switchIcons_2: {fileID: 0} 312 | switchIcons_3: {fileID: 0} 313 | switchIcons_4: {fileID: 0} 314 | switchIcons_5: {fileID: 0} 315 | switchIcons_6: {fileID: 0} 316 | switchIcons_7: {fileID: 0} 317 | switchIcons_8: {fileID: 0} 318 | switchIcons_9: {fileID: 0} 319 | switchIcons_10: {fileID: 0} 320 | switchIcons_11: {fileID: 0} 321 | switchSmallIcons_0: {fileID: 0} 322 | switchSmallIcons_1: {fileID: 0} 323 | switchSmallIcons_2: {fileID: 0} 324 | switchSmallIcons_3: {fileID: 0} 325 | switchSmallIcons_4: {fileID: 0} 326 | switchSmallIcons_5: {fileID: 0} 327 | switchSmallIcons_6: {fileID: 0} 328 | switchSmallIcons_7: {fileID: 0} 329 | switchSmallIcons_8: {fileID: 0} 330 | switchSmallIcons_9: {fileID: 0} 331 | switchSmallIcons_10: {fileID: 0} 332 | switchSmallIcons_11: {fileID: 0} 333 | switchManualHTML: 334 | switchAccessibleURLs: 335 | switchLegalInformation: 336 | switchMainThreadStackSize: 1048576 337 | switchPresenceGroupId: 0x01004b9000490000 338 | switchLogoHandling: 0 339 | switchReleaseVersion: 0 340 | switchDisplayVersion: 1.0.0 341 | switchStartupUserAccount: 0 342 | switchTouchScreenUsage: 0 343 | switchSupportedLanguagesMask: 0 344 | switchLogoType: 0 345 | switchApplicationErrorCodeCategory: 346 | switchUserAccountSaveDataSize: 0 347 | switchUserAccountSaveDataJournalSize: 0 348 | switchApplicationAttribute: 0 349 | switchCardSpecSize: 4 350 | switchCardSpecClock: 25 351 | switchRatingsMask: 0 352 | switchRatingsInt_0: 0 353 | switchRatingsInt_1: 0 354 | switchRatingsInt_2: 0 355 | switchRatingsInt_3: 0 356 | switchRatingsInt_4: 0 357 | switchRatingsInt_5: 0 358 | switchRatingsInt_6: 0 359 | switchRatingsInt_7: 0 360 | switchRatingsInt_8: 0 361 | switchRatingsInt_9: 0 362 | switchRatingsInt_10: 0 363 | switchRatingsInt_11: 0 364 | switchLocalCommunicationIds_0: 0x01004b9000490000 365 | switchLocalCommunicationIds_1: 366 | switchLocalCommunicationIds_2: 367 | switchLocalCommunicationIds_3: 368 | switchLocalCommunicationIds_4: 369 | switchLocalCommunicationIds_5: 370 | switchLocalCommunicationIds_6: 371 | switchLocalCommunicationIds_7: 372 | switchParentalControl: 0 373 | switchAllowsScreenshot: 1 374 | switchDataLossConfirmation: 0 375 | switchSupportedNpadStyles: 3 376 | switchSocketConfigEnabled: 0 377 | switchTcpInitialSendBufferSize: 32 378 | switchTcpInitialReceiveBufferSize: 64 379 | switchTcpAutoSendBufferSizeMax: 256 380 | switchTcpAutoReceiveBufferSizeMax: 256 381 | switchUdpSendBufferSize: 9 382 | switchUdpReceiveBufferSize: 42 383 | switchSocketBufferEfficiency: 4 384 | switchSocketInitializeEnabled: 1 385 | switchNetworkInterfaceManagerInitializeEnabled: 1 386 | switchPlayerConnectionEnabled: 1 387 | ps4NPAgeRating: 12 388 | ps4NPTitleSecret: 389 | ps4NPTrophyPackPath: 390 | ps4ParentalLevel: 11 391 | ps4ContentID: ED1633-NPXX51362_00-0000000000000000 392 | ps4Category: 0 393 | ps4MasterVersion: 01.00 394 | ps4AppVersion: 01.00 395 | ps4AppType: 0 396 | ps4ParamSfxPath: 397 | ps4VideoOutPixelFormat: 0 398 | ps4VideoOutInitialWidth: 1920 399 | ps4VideoOutBaseModeInitialWidth: 1920 400 | ps4VideoOutReprojectionRate: 120 401 | ps4PronunciationXMLPath: 402 | ps4PronunciationSIGPath: 403 | ps4BackgroundImagePath: 404 | ps4StartupImagePath: 405 | ps4SaveDataImagePath: 406 | ps4SdkOverride: 407 | ps4BGMPath: 408 | ps4ShareFilePath: 409 | ps4ShareOverlayImagePath: 410 | ps4PrivacyGuardImagePath: 411 | ps4NPtitleDatPath: 412 | ps4RemotePlayKeyAssignment: -1 413 | ps4RemotePlayKeyMappingDir: 414 | ps4PlayTogetherPlayerCount: 0 415 | ps4EnterButtonAssignment: 1 416 | ps4ApplicationParam1: 0 417 | ps4ApplicationParam2: 0 418 | ps4ApplicationParam3: 0 419 | ps4ApplicationParam4: 0 420 | ps4DownloadDataSize: 0 421 | ps4GarlicHeapSize: 2048 422 | ps4ProGarlicHeapSize: 2560 423 | ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ 424 | ps4pnSessions: 1 425 | ps4pnPresence: 1 426 | ps4pnFriends: 1 427 | ps4pnGameCustomData: 1 428 | playerPrefsSupport: 0 429 | restrictedAudioUsageRights: 0 430 | ps4UseResolutionFallback: 0 431 | ps4ReprojectionSupport: 0 432 | ps4UseAudio3dBackend: 0 433 | ps4SocialScreenEnabled: 0 434 | ps4ScriptOptimizationLevel: 0 435 | ps4Audio3dVirtualSpeakerCount: 14 436 | ps4attribCpuUsage: 0 437 | ps4PatchPkgPath: 438 | ps4PatchLatestPkgPath: 439 | ps4PatchChangeinfoPath: 440 | ps4PatchDayOne: 0 441 | ps4attribUserManagement: 0 442 | ps4attribMoveSupport: 0 443 | ps4attrib3DSupport: 0 444 | ps4attribShareSupport: 0 445 | ps4attribExclusiveVR: 0 446 | ps4disableAutoHideSplash: 0 447 | ps4videoRecordingFeaturesUsed: 0 448 | ps4contentSearchFeaturesUsed: 0 449 | ps4attribEyeToEyeDistanceSettingVR: 0 450 | ps4IncludedModules: [] 451 | monoEnv: 452 | psp2Splashimage: {fileID: 0} 453 | psp2NPTrophyPackPath: 454 | psp2NPSupportGBMorGJP: 0 455 | psp2NPAgeRating: 12 456 | psp2NPTitleDatPath: 457 | psp2NPCommsID: 458 | psp2NPCommunicationsID: 459 | psp2NPCommsPassphrase: 460 | psp2NPCommsSig: 461 | psp2ParamSfxPath: 462 | psp2ManualPath: 463 | psp2LiveAreaGatePath: 464 | psp2LiveAreaBackroundPath: 465 | psp2LiveAreaPath: 466 | psp2LiveAreaTrialPath: 467 | psp2PatchChangeInfoPath: 468 | psp2PatchOriginalPackage: 469 | psp2PackagePassword: F69AzBlax3CF3EDNhm3soLBPh71Yexui 470 | psp2KeystoneFile: 471 | psp2MemoryExpansionMode: 0 472 | psp2DRMType: 0 473 | psp2StorageType: 0 474 | psp2MediaCapacity: 0 475 | psp2DLCConfigPath: 476 | psp2ThumbnailPath: 477 | psp2BackgroundPath: 478 | psp2SoundPath: 479 | psp2TrophyCommId: 480 | psp2TrophyPackagePath: 481 | psp2PackagedResourcesPath: 482 | psp2SaveDataQuota: 10240 483 | psp2ParentalLevel: 1 484 | psp2ShortTitle: Not Set 485 | psp2ContentID: IV0000-ABCD12345_00-0123456789ABCDEF 486 | psp2Category: 0 487 | psp2MasterVersion: 01.00 488 | psp2AppVersion: 01.00 489 | psp2TVBootMode: 0 490 | psp2EnterButtonAssignment: 2 491 | psp2TVDisableEmu: 0 492 | psp2AllowTwitterDialog: 1 493 | psp2Upgradable: 0 494 | psp2HealthWarning: 0 495 | psp2UseLibLocation: 0 496 | psp2InfoBarOnStartup: 0 497 | psp2InfoBarColor: 0 498 | psp2ScriptOptimizationLevel: 0 499 | psmSplashimage: {fileID: 0} 500 | splashScreenBackgroundSourceLandscape: {fileID: 0} 501 | splashScreenBackgroundSourcePortrait: {fileID: 0} 502 | spritePackerPolicy: 503 | webGLMemorySize: 256 504 | webGLExceptionSupport: 1 505 | webGLNameFilesAsHashes: 0 506 | webGLDataCaching: 0 507 | webGLDebugSymbols: 0 508 | webGLEmscriptenArgs: 509 | webGLModulesDirectory: 510 | webGLTemplate: APPLICATION:Default 511 | webGLAnalyzeBuildSize: 0 512 | webGLUseEmbeddedResources: 0 513 | webGLUseWasm: 0 514 | webGLCompressionFormat: 1 515 | scriptingDefineSymbols: {} 516 | platformArchitecture: {} 517 | scriptingBackend: {} 518 | incrementalIl2cppBuild: {} 519 | additionalIl2CppArgs: 520 | scriptingRuntimeVersion: 0 521 | apiCompatibilityLevelPerPlatform: {} 522 | m_RenderingPath: 1 523 | m_MobileRenderingPath: 1 524 | metroPackageName: button-clicker 525 | metroPackageVersion: 526 | metroCertificatePath: 527 | metroCertificatePassword: 528 | metroCertificateSubject: 529 | metroCertificateIssuer: 530 | metroCertificateNotAfter: 0000000000000000 531 | metroApplicationDescription: button-clicker 532 | wsaImages: {} 533 | metroTileShortName: 534 | metroCommandLineArgsFile: 535 | metroTileShowName: 0 536 | metroMediumTileShowName: 0 537 | metroLargeTileShowName: 0 538 | metroWideTileShowName: 0 539 | metroDefaultTileSize: 1 540 | metroTileForegroundText: 2 541 | metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} 542 | metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, 543 | a: 1} 544 | metroSplashScreenUseBackgroundColor: 0 545 | platformCapabilities: {} 546 | metroFTAName: 547 | metroFTAFileTypes: [] 548 | metroProtocolName: 549 | metroCompilationOverrides: 1 550 | tizenProductDescription: 551 | tizenProductURL: 552 | tizenSigningProfileName: 553 | tizenGPSPermissions: 0 554 | tizenMicrophonePermissions: 0 555 | tizenDeploymentTarget: 556 | tizenDeploymentTargetType: -1 557 | tizenMinOSVersion: 1 558 | n3dsUseExtSaveData: 0 559 | n3dsCompressStaticMem: 1 560 | n3dsExtSaveDataNumber: 0x12345 561 | n3dsStackSize: 131072 562 | n3dsTargetPlatform: 2 563 | n3dsRegion: 7 564 | n3dsMediaSize: 0 565 | n3dsLogoStyle: 3 566 | n3dsTitle: GameName 567 | n3dsProductCode: 568 | n3dsApplicationId: 0xFF3FF 569 | stvDeviceAddress: 570 | stvProductDescription: 571 | stvProductAuthor: 572 | stvProductAuthorEmail: 573 | stvProductLink: 574 | stvProductCategory: 0 575 | XboxOneProductId: 576 | XboxOneUpdateKey: 577 | XboxOneSandboxId: 578 | XboxOneContentId: 579 | XboxOneTitleId: 580 | XboxOneSCId: 581 | XboxOneGameOsOverridePath: 582 | XboxOnePackagingOverridePath: 583 | XboxOneAppManifestOverridePath: 584 | XboxOnePackageEncryption: 0 585 | XboxOnePackageUpdateGranularity: 2 586 | XboxOneDescription: 587 | XboxOneLanguage: 588 | - enus 589 | XboxOneCapability: [] 590 | XboxOneGameRating: {} 591 | XboxOneIsContentPackage: 0 592 | XboxOneEnableGPUVariability: 0 593 | XboxOneSockets: {} 594 | XboxOneSplashScreen: {fileID: 0} 595 | XboxOneAllowedProductIds: [] 596 | XboxOnePersistentLocalStorageSize: 0 597 | xboxOneScriptCompiler: 0 598 | vrEditorSettings: 599 | daydream: 600 | daydreamIconForeground: {fileID: 0} 601 | daydreamIconBackground: {fileID: 0} 602 | cloudServicesEnabled: {} 603 | facebookSdkVersion: 7.9.4 604 | apiCompatibilityLevel: 2 605 | cloudProjectId: 606 | projectName: 607 | organizationId: 608 | cloudEnabled: 0 609 | enableNativePlatformBackendsForNewInputSystem: 0 610 | disableOldInputManagerSupport: 0 611 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/ProjectVersion.txt: -------------------------------------------------------------------------------- 1 | m_EditorVersion: 2017.2.0f3 2 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/QualitySettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!47 &1 4 | QualitySettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 5 7 | m_CurrentQuality: 5 8 | m_QualitySettings: 9 | - serializedVersion: 2 10 | name: Very Low 11 | pixelLightCount: 0 12 | shadows: 0 13 | shadowResolution: 0 14 | shadowProjection: 1 15 | shadowCascades: 1 16 | shadowDistance: 15 17 | shadowNearPlaneOffset: 3 18 | shadowCascade2Split: 0.33333334 19 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 20 | shadowmaskMode: 0 21 | blendWeights: 1 22 | textureQuality: 1 23 | anisotropicTextures: 0 24 | antiAliasing: 0 25 | softParticles: 0 26 | softVegetation: 0 27 | realtimeReflectionProbes: 0 28 | billboardsFaceCameraPosition: 0 29 | vSyncCount: 0 30 | lodBias: 0.3 31 | maximumLODLevel: 0 32 | particleRaycastBudget: 4 33 | asyncUploadTimeSlice: 2 34 | asyncUploadBufferSize: 4 35 | resolutionScalingFixedDPIFactor: 1 36 | excludedTargetPlatforms: [] 37 | - serializedVersion: 2 38 | name: Low 39 | pixelLightCount: 0 40 | shadows: 0 41 | shadowResolution: 0 42 | shadowProjection: 1 43 | shadowCascades: 1 44 | shadowDistance: 20 45 | shadowNearPlaneOffset: 3 46 | shadowCascade2Split: 0.33333334 47 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 48 | shadowmaskMode: 0 49 | blendWeights: 2 50 | textureQuality: 0 51 | anisotropicTextures: 0 52 | antiAliasing: 0 53 | softParticles: 0 54 | softVegetation: 0 55 | realtimeReflectionProbes: 0 56 | billboardsFaceCameraPosition: 0 57 | vSyncCount: 0 58 | lodBias: 0.4 59 | maximumLODLevel: 0 60 | particleRaycastBudget: 16 61 | asyncUploadTimeSlice: 2 62 | asyncUploadBufferSize: 4 63 | resolutionScalingFixedDPIFactor: 1 64 | excludedTargetPlatforms: [] 65 | - serializedVersion: 2 66 | name: Medium 67 | pixelLightCount: 1 68 | shadows: 1 69 | shadowResolution: 0 70 | shadowProjection: 1 71 | shadowCascades: 1 72 | shadowDistance: 20 73 | shadowNearPlaneOffset: 3 74 | shadowCascade2Split: 0.33333334 75 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 76 | shadowmaskMode: 0 77 | blendWeights: 2 78 | textureQuality: 0 79 | anisotropicTextures: 1 80 | antiAliasing: 0 81 | softParticles: 0 82 | softVegetation: 0 83 | realtimeReflectionProbes: 0 84 | billboardsFaceCameraPosition: 0 85 | vSyncCount: 1 86 | lodBias: 0.7 87 | maximumLODLevel: 0 88 | particleRaycastBudget: 64 89 | asyncUploadTimeSlice: 2 90 | asyncUploadBufferSize: 4 91 | resolutionScalingFixedDPIFactor: 1 92 | excludedTargetPlatforms: [] 93 | - serializedVersion: 2 94 | name: High 95 | pixelLightCount: 2 96 | shadows: 2 97 | shadowResolution: 1 98 | shadowProjection: 1 99 | shadowCascades: 2 100 | shadowDistance: 40 101 | shadowNearPlaneOffset: 3 102 | shadowCascade2Split: 0.33333334 103 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 104 | shadowmaskMode: 1 105 | blendWeights: 2 106 | textureQuality: 0 107 | anisotropicTextures: 1 108 | antiAliasing: 0 109 | softParticles: 0 110 | softVegetation: 1 111 | realtimeReflectionProbes: 1 112 | billboardsFaceCameraPosition: 1 113 | vSyncCount: 1 114 | lodBias: 1 115 | maximumLODLevel: 0 116 | particleRaycastBudget: 256 117 | asyncUploadTimeSlice: 2 118 | asyncUploadBufferSize: 4 119 | resolutionScalingFixedDPIFactor: 1 120 | excludedTargetPlatforms: [] 121 | - serializedVersion: 2 122 | name: Very High 123 | pixelLightCount: 3 124 | shadows: 2 125 | shadowResolution: 2 126 | shadowProjection: 1 127 | shadowCascades: 2 128 | shadowDistance: 70 129 | shadowNearPlaneOffset: 3 130 | shadowCascade2Split: 0.33333334 131 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 132 | shadowmaskMode: 1 133 | blendWeights: 4 134 | textureQuality: 0 135 | anisotropicTextures: 2 136 | antiAliasing: 2 137 | softParticles: 1 138 | softVegetation: 1 139 | realtimeReflectionProbes: 1 140 | billboardsFaceCameraPosition: 1 141 | vSyncCount: 1 142 | lodBias: 1.5 143 | maximumLODLevel: 0 144 | particleRaycastBudget: 1024 145 | asyncUploadTimeSlice: 2 146 | asyncUploadBufferSize: 4 147 | resolutionScalingFixedDPIFactor: 1 148 | excludedTargetPlatforms: [] 149 | - serializedVersion: 2 150 | name: Ultra 151 | pixelLightCount: 4 152 | shadows: 2 153 | shadowResolution: 2 154 | shadowProjection: 1 155 | shadowCascades: 4 156 | shadowDistance: 150 157 | shadowNearPlaneOffset: 3 158 | shadowCascade2Split: 0.33333334 159 | shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} 160 | shadowmaskMode: 1 161 | blendWeights: 4 162 | textureQuality: 0 163 | anisotropicTextures: 2 164 | antiAliasing: 2 165 | softParticles: 1 166 | softVegetation: 1 167 | realtimeReflectionProbes: 1 168 | billboardsFaceCameraPosition: 1 169 | vSyncCount: 1 170 | lodBias: 2 171 | maximumLODLevel: 0 172 | particleRaycastBudget: 4096 173 | asyncUploadTimeSlice: 2 174 | asyncUploadBufferSize: 4 175 | resolutionScalingFixedDPIFactor: 1 176 | excludedTargetPlatforms: [] 177 | m_PerPlatformDefaultQuality: 178 | Android: 2 179 | Nintendo 3DS: 5 180 | Nintendo Switch: 5 181 | PS4: 5 182 | PSM: 5 183 | PSP2: 2 184 | Samsung TV: 2 185 | Standalone: 5 186 | Tizen: 2 187 | Web: 5 188 | WebGL: 3 189 | WiiU: 5 190 | Windows Store Apps: 5 191 | XboxOne: 5 192 | iPhone: 2 193 | tvOS: 2 194 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/TagManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!78 &1 4 | TagManager: 5 | serializedVersion: 2 6 | tags: [] 7 | layers: 8 | - Default 9 | - TransparentFX 10 | - Ignore Raycast 11 | - 12 | - Water 13 | - UI 14 | - 15 | - 16 | - 17 | - 18 | - 19 | - 20 | - 21 | - 22 | - 23 | - 24 | - 25 | - 26 | - 27 | - 28 | - 29 | - 30 | - 31 | - 32 | - 33 | - 34 | - 35 | - 36 | - 37 | - 38 | - 39 | - 40 | m_SortingLayers: 41 | - name: Default 42 | uniqueID: 0 43 | locked: 0 44 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/TimeManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!5 &1 4 | TimeManager: 5 | m_ObjectHideFlags: 0 6 | Fixed Timestep: 0.02 7 | Maximum Allowed Timestep: 0.33333334 8 | m_TimeScale: 1 9 | Maximum Particle Timestep: 0.03 10 | -------------------------------------------------------------------------------- /examples/button-clicker/ProjectSettings/UnityConnectSettings.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!310 &1 4 | UnityConnectSettings: 5 | m_ObjectHideFlags: 0 6 | m_Enabled: 0 7 | m_TestMode: 0 8 | m_TestEventUrl: 9 | m_TestConfigUrl: 10 | m_TestInitMode: 0 11 | CrashReportingSettings: 12 | m_EventUrl: https://perf-events.cloud.unity3d.com/api/events/crashes 13 | m_Enabled: 0 14 | m_CaptureEditorExceptions: 1 15 | UnityPurchasingSettings: 16 | m_Enabled: 0 17 | m_TestMode: 0 18 | UnityAnalyticsSettings: 19 | m_Enabled: 0 20 | m_InitializeOnStartup: 1 21 | m_TestMode: 0 22 | m_TestEventUrl: 23 | m_TestConfigUrl: 24 | UnityAdsSettings: 25 | m_Enabled: 0 26 | m_InitializeOnStartup: 1 27 | m_TestMode: 0 28 | m_EnabledPlatforms: 4294967295 29 | m_IosGameId: 30 | m_AndroidGameId: 31 | m_GameIds: {} 32 | m_GameId: 33 | PerformanceReportingSettings: 34 | m_Enabled: 0 35 | -------------------------------------------------------------------------------- /examples/button-clicker/UnityPackageManager/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /examples/send-presence/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${PROJECT_SOURCE_DIR}/include) 2 | add_executable( 3 | send-presence 4 | MACOSX_BUNDLE 5 | send-presence.c 6 | ) 7 | set_target_properties(send-presence PROPERTIES 8 | MACOSX_BUNDLE_BUNDLE_NAME "Send Presence" 9 | MACOSX_BUNDLE_GUI_IDENTIFIER "com.discordapp.examples.send-presence" 10 | ) 11 | target_link_libraries(send-presence discord-rpc) 12 | 13 | install( 14 | TARGETS send-presence 15 | RUNTIME 16 | DESTINATION "bin" 17 | CONFIGURATIONS Release 18 | BUNDLE 19 | DESTINATION "bin" 20 | CONFIGURATIONS Release 21 | ) -------------------------------------------------------------------------------- /examples/send-presence/send-presence.c: -------------------------------------------------------------------------------- 1 | /* 2 | This is a simple example in C of using the rich presence API asynchronously. 3 | */ 4 | 5 | #define _CRT_SECURE_NO_WARNINGS /* thanks Microsoft */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "discord_rpc.h" 13 | 14 | static const char* APPLICATION_ID = "345229890980937739"; 15 | static int FrustrationLevel = 0; 16 | static int64_t StartTime; 17 | static int SendPresence = 1; 18 | 19 | static int prompt(char* line, size_t size) 20 | { 21 | int res; 22 | char* nl; 23 | printf("\n> "); 24 | fflush(stdout); 25 | res = fgets(line, (int)size, stdin) ? 1 : 0; 26 | line[size - 1] = 0; 27 | nl = strchr(line, '\n'); 28 | if (nl) { 29 | *nl = 0; 30 | } 31 | return res; 32 | } 33 | 34 | static void updateDiscordPresence() 35 | { 36 | if (SendPresence) { 37 | char buffer[256]; 38 | DiscordRichPresence discordPresence; 39 | memset(&discordPresence, 0, sizeof(discordPresence)); 40 | discordPresence.state = "West of House"; 41 | sprintf(buffer, "Frustration level: %d", FrustrationLevel); 42 | discordPresence.details = buffer; 43 | discordPresence.startTimestamp = StartTime; 44 | discordPresence.endTimestamp = time(0) + 5 * 60; 45 | discordPresence.largeImageKey = "canary-large"; 46 | discordPresence.smallImageKey = "ptb-small"; 47 | discordPresence.partyId = "party1234"; 48 | discordPresence.partySize = 1; 49 | discordPresence.partyMax = 6; 50 | discordPresence.matchSecret = "xyzzy"; 51 | discordPresence.joinSecret = "join"; 52 | discordPresence.spectateSecret = "look"; 53 | discordPresence.instance = 0; 54 | Discord_UpdatePresence(&discordPresence); 55 | } else { 56 | Discord_ClearPresence(); 57 | } 58 | } 59 | 60 | static void handleDiscordReady(void) 61 | { 62 | printf("\nDiscord: ready\n"); 63 | } 64 | 65 | static void handleDiscordDisconnected(int errcode, const char* message) 66 | { 67 | printf("\nDiscord: disconnected (%d: %s)\n", errcode, message); 68 | } 69 | 70 | static void handleDiscordError(int errcode, const char* message) 71 | { 72 | printf("\nDiscord: error (%d: %s)\n", errcode, message); 73 | } 74 | 75 | static void handleDiscordJoin(const char* secret) 76 | { 77 | printf("\nDiscord: join (%s)\n", secret); 78 | } 79 | 80 | static void handleDiscordSpectate(const char* secret) 81 | { 82 | printf("\nDiscord: spectate (%s)\n", secret); 83 | } 84 | 85 | static void handleDiscordJoinRequest(const DiscordJoinRequest* request) 86 | { 87 | int response = -1; 88 | char yn[4]; 89 | printf("\nDiscord: join request from %s - %s - %s\n", 90 | request->username, 91 | request->avatar, 92 | request->userId); 93 | do { 94 | printf("Accept? (y/n)"); 95 | if (!prompt(yn, sizeof(yn))) { 96 | break; 97 | } 98 | 99 | if (!yn[0]) { 100 | continue; 101 | } 102 | 103 | if (yn[0] == 'y') { 104 | response = DISCORD_REPLY_YES; 105 | break; 106 | } 107 | 108 | if (yn[0] == 'n') { 109 | response = DISCORD_REPLY_NO; 110 | break; 111 | } 112 | } while (1); 113 | if (response != -1) { 114 | Discord_Respond(request->userId, response); 115 | } 116 | } 117 | 118 | static void discordInit() 119 | { 120 | DiscordEventHandlers handlers; 121 | memset(&handlers, 0, sizeof(handlers)); 122 | handlers.ready = handleDiscordReady; 123 | handlers.disconnected = handleDiscordDisconnected; 124 | handlers.errored = handleDiscordError; 125 | handlers.joinGame = handleDiscordJoin; 126 | handlers.spectateGame = handleDiscordSpectate; 127 | handlers.joinRequest = handleDiscordJoinRequest; 128 | Discord_Initialize(APPLICATION_ID, &handlers, 1, NULL); 129 | } 130 | 131 | static void gameLoop() 132 | { 133 | char line[512]; 134 | char* space; 135 | 136 | StartTime = time(0); 137 | 138 | printf("You are standing in an open field west of a white house.\n"); 139 | while (prompt(line, sizeof(line))) { 140 | if (line[0]) { 141 | if (line[0] == 'q') { 142 | break; 143 | } 144 | 145 | if (line[0] == 't') { 146 | printf("Shutting off Discord.\n"); 147 | Discord_Shutdown(); 148 | continue; 149 | } 150 | 151 | if (line[0] == 'c') { 152 | if (SendPresence) { 153 | printf("Clearing presence information.\n"); 154 | SendPresence = 0; 155 | } else { 156 | printf("Restoring presence information.\n"); 157 | SendPresence = 1; 158 | } 159 | updateDiscordPresence(); 160 | continue; 161 | } 162 | 163 | if (line[0] == 'y') { 164 | printf("Reinit Discord.\n"); 165 | discordInit(); 166 | continue; 167 | } 168 | 169 | if (time(NULL) & 1) { 170 | printf("I don't understand that.\n"); 171 | } 172 | else { 173 | space = strchr(line, ' '); 174 | if (space) { 175 | *space = 0; 176 | } 177 | printf("I don't know the word \"%s\".\n", line); 178 | } 179 | 180 | ++FrustrationLevel; 181 | 182 | updateDiscordPresence(); 183 | } 184 | 185 | #ifdef DISCORD_DISABLE_IO_THREAD 186 | Discord_UpdateConnection(); 187 | #endif 188 | Discord_RunCallbacks(); 189 | } 190 | } 191 | 192 | int main(int argc, char* argv[]) 193 | { 194 | discordInit(); 195 | 196 | gameLoop(); 197 | 198 | Discord_Shutdown(); 199 | return 0; 200 | } 201 | -------------------------------------------------------------------------------- /examples/unrealstatus/.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Visual Studio 2015 database file 5 | *.VC.db 6 | 7 | # Compiled Object files 8 | *.slo 9 | *.lo 10 | *.o 11 | *.obj 12 | 13 | # Precompiled Headers 14 | *.gch 15 | *.pch 16 | 17 | # Compiled Dynamic libraries 18 | *.so 19 | *.dylib 20 | *.dll 21 | 22 | # Fortran module files 23 | *.mod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.ipa 36 | 37 | # These project files can be generated by the engine 38 | *.xcodeproj 39 | *.xcworkspace 40 | *.sln 41 | *.suo 42 | *.opensdf 43 | *.sdf 44 | *.VC.db 45 | *.VC.opendb 46 | 47 | # Precompiled Assets 48 | SourceArt/**/*.png 49 | SourceArt/**/*.tga 50 | 51 | # Binary Files 52 | Binaries/ 53 | 54 | # Builds 55 | Build/* 56 | 57 | # Whitelist PakBlacklist-.txt files 58 | !Build/*/ 59 | Build/*/** 60 | !Build/*/PakBlacklist*.txt 61 | 62 | # Don't ignore icon files in Build 63 | !Build/**/*.ico 64 | 65 | # Built data for maps 66 | *_BuiltData.uasset 67 | 68 | # Configuration files generated by the Editor 69 | Saved/* 70 | 71 | # Compiled source files for the engine to use 72 | Intermediate/ 73 | 74 | # Cache files for the editor to use 75 | DerivedDataCache/ 76 | 77 | # Library headers must be copied automatically by the build script (build.py unreal) 78 | Plugins/DiscordRpc/Source/ThirdParty/DiscordRpcLibrary/Include 79 | -------------------------------------------------------------------------------- /examples/unrealstatus/Config/DefaultEditor.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Config/DefaultEditor.ini -------------------------------------------------------------------------------- /examples/unrealstatus/Config/DefaultEngine.ini: -------------------------------------------------------------------------------- 1 | [URL] 2 | 3 | [/Script/HardwareTargeting.HardwareTargetingSettings] 4 | TargetedHardwareClass=Desktop 5 | AppliedTargetedHardwareClass=Desktop 6 | DefaultGraphicsPerformance=Maximum 7 | AppliedDefaultGraphicsPerformance=Maximum 8 | 9 | [/Script/Engine.EndUserSettings] 10 | bSendAnonymousUsageDataToEpic=False 11 | 12 | [/Script/Engine.PhysicsSettings] 13 | DefaultGravityZ=-980.000000 14 | DefaultTerminalVelocity=4000.000000 15 | DefaultFluidFriction=0.300000 16 | SimulateScratchMemorySize=262144 17 | RagdollAggregateThreshold=4 18 | TriangleMeshTriangleMinAreaThreshold=5.000000 19 | bEnableAsyncScene=False 20 | bEnableShapeSharing=False 21 | bEnablePCM=False 22 | bEnableStabilization=False 23 | bWarnMissingLocks=True 24 | bEnable2DPhysics=False 25 | LockedAxis=Invalid 26 | DefaultDegreesOfFreedom=Full3D 27 | BounceThresholdVelocity=200.000000 28 | FrictionCombineMode=Average 29 | RestitutionCombineMode=Average 30 | MaxAngularVelocity=3600.000000 31 | MaxDepenetrationVelocity=0.000000 32 | ContactOffsetMultiplier=0.010000 33 | MinContactOffset=0.000100 34 | MaxContactOffset=1.000000 35 | bSimulateSkeletalMeshOnDedicatedServer=True 36 | DefaultShapeComplexity=CTF_UseSimpleAndComplex 37 | bDefaultHasComplexCollision=True 38 | bSuppressFaceRemapTable=False 39 | bSupportUVFromHitResults=False 40 | bDisableActiveActors=False 41 | bDisableCCD=False 42 | MaxPhysicsDeltaTime=0.033333 43 | bSubstepping=False 44 | bSubsteppingAsync=False 45 | MaxSubstepDeltaTime=0.016667 46 | MaxSubsteps=6 47 | SyncSceneSmoothingFactor=0.000000 48 | AsyncSceneSmoothingFactor=0.990000 49 | InitialAverageFrameRate=0.016667 50 | 51 | [/Script/EngineSettings.GameMapsSettings] 52 | EditorStartupMap=/Game/ShowTheUILevel.ShowTheUILevel 53 | GameDefaultMap=/Game/ShowTheUILevel.ShowTheUILevel 54 | 55 | -------------------------------------------------------------------------------- /examples/unrealstatus/Config/DefaultGame.ini: -------------------------------------------------------------------------------- 1 | [/Script/EngineSettings.GeneralProjectSettings] 2 | ProjectID=E5977A24492699DF20B8ADBF736AF6C6 3 | ProjectName=Discord RPC Example 4 | CompanyName=Discord Inc. 5 | Homepage="https://discordapp.com/" 6 | CopyrightNotice= 7 | 8 | -------------------------------------------------------------------------------- /examples/unrealstatus/Content/MainScreenBP.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Content/MainScreenBP.uasset -------------------------------------------------------------------------------- /examples/unrealstatus/Content/MouseGameModeBP.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Content/MouseGameModeBP.uasset -------------------------------------------------------------------------------- /examples/unrealstatus/Content/MousePlayerControllerBP.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Content/MousePlayerControllerBP.uasset -------------------------------------------------------------------------------- /examples/unrealstatus/Content/ShowTheUILevel.umap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Content/ShowTheUILevel.umap -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/DiscordRpc.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "Discord RPC", 6 | "Description": "Wrap the Discord RPC library.", 7 | "Category": "Messaging", 8 | "CreatedBy": "Chris Marsh ", 9 | "CreatedByURL": "https://discordapp.com/", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": true, 15 | "Installed": false, 16 | "Modules": [ 17 | { 18 | "Name": "DiscordRpc", 19 | "Type": "Runtime", 20 | "LoadingPhase": "PreDefault", 21 | "WhitelistPlatforms" : 22 | [ 23 | "Win64", 24 | "Linux", 25 | "Mac" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Plugins/discordrpc/Resources/Icon128.png -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Resources/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goeo-/discord-rpc/2ce9fe068bfb51a223465f57c29d15bf5f4f6336/examples/unrealstatus/Plugins/discordrpc/Resources/discord.png -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/DiscordRpc/DiscordRpc.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | using System.IO; 5 | 6 | public class DiscordRpc : ModuleRules 7 | { 8 | #if WITH_FORWARDED_MODULE_RULES_CTOR 9 | public DiscordRpc(ReadOnlyTargetRules Target) : base(Target) 10 | #else 11 | public DiscordRpc(TargetInfo Target) 12 | #endif 13 | { 14 | Definitions.Add("DISCORD_DYNAMIC_LIB=1"); 15 | 16 | PublicIncludePaths.AddRange( 17 | new string[] { 18 | "DiscordRpc/Public" 19 | } 20 | ); 21 | 22 | PrivateIncludePaths.AddRange( 23 | new string[] { 24 | "DiscordRpc/Private" 25 | } 26 | ); 27 | 28 | PublicDependencyModuleNames.AddRange( 29 | new string[] 30 | { 31 | "Core", 32 | "DiscordRpcLibrary" 33 | } 34 | ); 35 | 36 | PrivateDependencyModuleNames.AddRange( 37 | new string[] 38 | { 39 | "CoreUObject", 40 | "Engine", 41 | "Slate", 42 | "SlateCore", 43 | "Projects" 44 | } 45 | ); 46 | 47 | DynamicallyLoadedModuleNames.AddRange( 48 | new string[] 49 | { 50 | // ... add any modules that your module loads dynamically here ... 51 | } 52 | ); 53 | 54 | string BaseDirectory = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "Source", "ThirdParty", "DiscordRpcLibrary")); 55 | PublicIncludePaths.Add(Path.Combine(BaseDirectory, "Include")); 56 | } 57 | } -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/DiscordRpc/Private/DiscordRpc.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "DiscordRpcPrivatePCH.h" 4 | #include "IPluginManager.h" 5 | #include "ModuleManager.h" 6 | 7 | #define LOCTEXT_NAMESPACE "FDiscordRpcModule" 8 | 9 | void FDiscordRpcModule::StartupModule() 10 | { 11 | #if !PLATFORM_LINUX 12 | #if defined(DISCORD_DYNAMIC_LIB) 13 | // Get the base directory of this plugin 14 | FString BaseDir = IPluginManager::Get().FindPlugin("DiscordRpc")->GetBaseDir(); 15 | const FString SDKDir = FPaths::Combine(*BaseDir, TEXT("Source"), TEXT("ThirdParty"), TEXT("DiscordRpcLibrary")); 16 | #if PLATFORM_WINDOWS 17 | const FString LibName = TEXT("discord-rpc"); 18 | const FString LibDir = FPaths::Combine(*SDKDir, TEXT("Win64")); 19 | if (!LoadDependency(LibDir, LibName, DiscordRpcLibraryHandle)) { 20 | FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT(LOCTEXT_NAMESPACE, "Failed to load DiscordRpc plugin. Plug-in will not be functional.")); 21 | FreeDependency(DiscordRpcLibraryHandle); 22 | } 23 | #elif PLATFORM_MAC 24 | const FString LibName = TEXT("libdiscord-rpc"); 25 | const FString LibDir = FPaths::Combine(*SDKDir, TEXT("Mac")); 26 | if (!LoadDependency(LibDir, LibName, DiscordRpcLibraryHandle)) { 27 | FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT(LOCTEXT_NAMESPACE, "Failed to load DiscordRpc plugin. Plug-in will not be functional.")); 28 | FreeDependency(DiscordRpcLibraryHandle); 29 | } 30 | #endif 31 | #endif 32 | #endif 33 | } 34 | 35 | void FDiscordRpcModule::ShutdownModule() 36 | { 37 | // Free the dll handle 38 | #if !PLATFORM_LINUX 39 | #if defined(DISCORD_DYNAMIC_LIB) 40 | FreeDependency(DiscordRpcLibraryHandle); 41 | #endif 42 | #endif 43 | } 44 | 45 | bool FDiscordRpcModule::LoadDependency(const FString& Dir, const FString& Name, void*& Handle) 46 | { 47 | FString Lib = Name + TEXT(".") + FPlatformProcess::GetModuleExtension(); 48 | FString Path = Dir.IsEmpty() ? *Lib : FPaths::Combine(*Dir, *Lib); 49 | 50 | Handle = FPlatformProcess::GetDllHandle(*Path); 51 | 52 | if (Handle == nullptr) 53 | { 54 | return false; 55 | } 56 | 57 | return true; 58 | } 59 | 60 | void FDiscordRpcModule::FreeDependency(void*& Handle) 61 | { 62 | if (Handle != nullptr) 63 | { 64 | FPlatformProcess::FreeDllHandle(Handle); 65 | Handle = nullptr; 66 | } 67 | } 68 | 69 | #undef LOCTEXT_NAMESPACE 70 | 71 | IMPLEMENT_MODULE(FDiscordRpcModule, DiscordRpc) -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/DiscordRpc/Private/DiscordRpcBlueprint.cpp: -------------------------------------------------------------------------------- 1 | #include "DiscordRpcPrivatePCH.h" 2 | #include "DiscordRpcBlueprint.h" 3 | #include "discord_rpc.h" 4 | 5 | DEFINE_LOG_CATEGORY(Discord) 6 | 7 | static UDiscordRpc* self = nullptr; 8 | 9 | static void ReadyHandler() 10 | { 11 | UE_LOG(Discord, Log, TEXT("Discord connected")); 12 | if (self) { 13 | self->IsConnected = true; 14 | self->OnConnected.Broadcast(); 15 | } 16 | } 17 | 18 | static void DisconnectHandler(int errorCode, const char* message) 19 | { 20 | auto msg = FString(message); 21 | UE_LOG(Discord, Log, TEXT("Discord disconnected (%d): %s"), errorCode, *msg); 22 | if (self) { 23 | self->IsConnected = false; 24 | self->OnDisconnected.Broadcast(errorCode, msg); 25 | } 26 | } 27 | 28 | static void ErroredHandler(int errorCode, const char* message) 29 | { 30 | auto msg = FString(message); 31 | UE_LOG(Discord, Log, TEXT("Discord error (%d): %s"), errorCode, *msg); 32 | if (self) { 33 | self->OnErrored.Broadcast(errorCode, msg); 34 | } 35 | } 36 | 37 | static void JoinGameHandler(const char* joinSecret) 38 | { 39 | auto secret = FString(joinSecret); 40 | UE_LOG(Discord, Log, TEXT("Discord join %s"), *secret); 41 | if (self) { 42 | self->OnJoin.Broadcast(secret); 43 | } 44 | } 45 | 46 | static void SpectateGameHandler(const char* spectateSecret) 47 | { 48 | auto secret = FString(spectateSecret); 49 | UE_LOG(Discord, Log, TEXT("Discord spectate %s"), *secret); 50 | if (self) { 51 | self->OnSpectate.Broadcast(secret); 52 | } 53 | } 54 | 55 | static void JoinRequestHandler(const DiscordJoinRequest* request) 56 | { 57 | FDiscordJoinRequestData jr; 58 | jr.userId = ANSI_TO_TCHAR(request->userId); 59 | jr.username = ANSI_TO_TCHAR(request->username); 60 | jr.discriminator = ANSI_TO_TCHAR(request->discriminator); 61 | jr.avatar = ANSI_TO_TCHAR(request->avatar); 62 | UE_LOG(Discord, Log, TEXT("Discord join request from %s - %s#%s"), *jr.userId, *jr.username, *jr.discriminator); 63 | if (self) { 64 | self->OnJoinRequest.Broadcast(jr); 65 | } 66 | } 67 | 68 | void UDiscordRpc::Initialize(const FString& applicationId, 69 | bool autoRegister, 70 | const FString& optionalSteamId) 71 | { 72 | self = this; 73 | IsConnected = false; 74 | DiscordEventHandlers handlers{}; 75 | handlers.ready = ReadyHandler; 76 | handlers.disconnected = DisconnectHandler; 77 | handlers.errored = ErroredHandler; 78 | if (OnJoin.IsBound()) { 79 | handlers.joinGame = JoinGameHandler; 80 | } 81 | if (OnSpectate.IsBound()) { 82 | handlers.spectateGame = SpectateGameHandler; 83 | } 84 | if (OnJoinRequest.IsBound()) { 85 | handlers.joinRequest = JoinRequestHandler; 86 | } 87 | auto appId = StringCast(*applicationId); 88 | auto steamId = StringCast(*optionalSteamId); 89 | Discord_Initialize( 90 | (const char*)appId.Get(), &handlers, autoRegister, (const char*)steamId.Get()); 91 | } 92 | 93 | void UDiscordRpc::Shutdown() 94 | { 95 | Discord_Shutdown(); 96 | self = nullptr; 97 | } 98 | 99 | void UDiscordRpc::RunCallbacks() 100 | { 101 | Discord_RunCallbacks(); 102 | } 103 | 104 | void UDiscordRpc::UpdatePresence() 105 | { 106 | DiscordRichPresence rp{}; 107 | 108 | auto state = StringCast(*RichPresence.state); 109 | rp.state = state.Get(); 110 | 111 | auto details = StringCast(*RichPresence.details); 112 | rp.details = details.Get(); 113 | 114 | auto largeImageKey = StringCast(*RichPresence.largeImageKey); 115 | rp.largeImageKey = largeImageKey.Get(); 116 | 117 | auto largeImageText = StringCast(*RichPresence.largeImageText); 118 | rp.largeImageText = largeImageText.Get(); 119 | 120 | auto smallImageKey = StringCast(*RichPresence.smallImageKey); 121 | rp.smallImageKey = smallImageKey.Get(); 122 | 123 | auto smallImageText = StringCast(*RichPresence.smallImageText); 124 | rp.smallImageText = smallImageText.Get(); 125 | 126 | auto partyId = StringCast(*RichPresence.partyId); 127 | rp.partyId = partyId.Get(); 128 | 129 | auto matchSecret = StringCast(*RichPresence.matchSecret); 130 | rp.matchSecret = matchSecret.Get(); 131 | 132 | auto joinSecret = StringCast(*RichPresence.joinSecret); 133 | rp.joinSecret = joinSecret.Get(); 134 | 135 | auto spectateSecret = StringCast(*RichPresence.spectateSecret); 136 | rp.spectateSecret = spectateSecret.Get(); 137 | rp.startTimestamp = RichPresence.startTimestamp; 138 | rp.endTimestamp = RichPresence.endTimestamp; 139 | rp.partySize = RichPresence.partySize; 140 | rp.partyMax = RichPresence.partyMax; 141 | rp.instance = RichPresence.instance; 142 | 143 | Discord_UpdatePresence(&rp); 144 | } 145 | 146 | void UDiscordRpc::ClearPresence() 147 | { 148 | Discord_ClearPresence(); 149 | } 150 | 151 | void UDiscordRpc::Respond(const FString& userId, int reply) 152 | { 153 | UE_LOG(Discord, Log, TEXT("Responding %d to join request from %s"), reply, *userId); 154 | FTCHARToUTF8 utf8_userid(*userId); 155 | Discord_Respond(utf8_userid.Get(), reply); 156 | } 157 | -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/DiscordRpc/Private/DiscordRpcPrivatePCH.h: -------------------------------------------------------------------------------- 1 | #include "Core.h" 2 | #include "DiscordRpc.h" -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/DiscordRpc/Public/DiscordRpc.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "ModuleManager.h" 6 | 7 | class FDiscordRpcModule : public IModuleInterface { 8 | public: 9 | /** IModuleInterface implementation */ 10 | virtual void StartupModule() override; 11 | virtual void ShutdownModule() override; 12 | 13 | private: 14 | /** Handle to the test dll we will load */ 15 | void* DiscordRpcLibraryHandle; 16 | 17 | /** StartupModule is covered with defines, these functions are the place to put breakpoints */ 18 | static bool LoadDependency(const FString& Dir, const FString& Name, void*& Handle); 19 | static void FreeDependency(void*& Handle); 20 | }; -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/DiscordRpc/Public/DiscordRpcBlueprint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Engine.h" 5 | #include "DiscordRpcBlueprint.generated.h" 6 | 7 | // unreal's header tool hates clang-format 8 | // clang-format off 9 | 10 | /** 11 | * Ask to join callback data 12 | */ 13 | USTRUCT(BlueprintType) 14 | struct FDiscordJoinRequestData { 15 | GENERATED_USTRUCT_BODY() 16 | 17 | UPROPERTY(BlueprintReadOnly) 18 | FString userId; 19 | UPROPERTY(BlueprintReadOnly) 20 | FString username; 21 | UPROPERTY(BlueprintReadOnly) 22 | FString discriminator; 23 | UPROPERTY(BlueprintReadOnly) 24 | FString avatar; 25 | }; 26 | 27 | 28 | DECLARE_LOG_CATEGORY_EXTERN(Discord, Log, All); 29 | 30 | DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDiscordConnected); 31 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordDisconnected, int, errorCode, const FString&, errorMessage); 32 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDiscordErrored, int, errorCode, const FString&, errorMessage); 33 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoin, const FString&, joinSecret); 34 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordSpectate, const FString&, spectateSecret); 35 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDiscordJoinRequest, const FDiscordJoinRequestData&, joinRequest); 36 | 37 | // clang-format on 38 | 39 | /** 40 | * Rich presence data 41 | */ 42 | USTRUCT(BlueprintType) 43 | struct FDiscordRichPresence { 44 | GENERATED_USTRUCT_BODY() 45 | 46 | UPROPERTY(BlueprintReadWrite) 47 | FString state; 48 | UPROPERTY(BlueprintReadWrite) 49 | FString details; 50 | // todo, timestamps are 64bit, does that even matter? 51 | UPROPERTY(BlueprintReadWrite) 52 | int startTimestamp; 53 | UPROPERTY(BlueprintReadWrite) 54 | int endTimestamp; 55 | UPROPERTY(BlueprintReadWrite) 56 | FString largeImageKey; 57 | UPROPERTY(BlueprintReadWrite) 58 | FString largeImageText; 59 | UPROPERTY(BlueprintReadWrite) 60 | FString smallImageKey; 61 | UPROPERTY(BlueprintReadWrite) 62 | FString smallImageText; 63 | UPROPERTY(BlueprintReadWrite) 64 | FString partyId; 65 | UPROPERTY(BlueprintReadWrite) 66 | int partySize; 67 | UPROPERTY(BlueprintReadWrite) 68 | int partyMax; 69 | UPROPERTY(BlueprintReadWrite) 70 | FString matchSecret; 71 | UPROPERTY(BlueprintReadWrite) 72 | FString joinSecret; 73 | UPROPERTY(BlueprintReadWrite) 74 | FString spectateSecret; 75 | UPROPERTY(BlueprintReadWrite) 76 | bool instance; 77 | }; 78 | 79 | /** 80 | * 81 | */ 82 | UCLASS(BlueprintType, meta = (DisplayName = "Discord RPC"), Category = "Discord") 83 | class DISCORDRPC_API UDiscordRpc : public UObject { 84 | GENERATED_BODY() 85 | 86 | public: 87 | UFUNCTION(BlueprintCallable, 88 | meta = (DisplayName = "Initialize connection", Keywords = "Discord rpc"), 89 | Category = "Discord") 90 | void Initialize(const FString& applicationId, 91 | bool autoRegister, 92 | const FString& optionalSteamId); 93 | 94 | UFUNCTION(BlueprintCallable, 95 | meta = (DisplayName = "Shut down connection", Keywords = "Discord rpc"), 96 | Category = "Discord") 97 | void Shutdown(); 98 | 99 | UFUNCTION(BlueprintCallable, 100 | meta = (DisplayName = "Check for callbacks", Keywords = "Discord rpc"), 101 | Category = "Discord") 102 | void RunCallbacks(); 103 | 104 | UFUNCTION(BlueprintCallable, 105 | meta = (DisplayName = "Send presence", Keywords = "Discord rpc"), 106 | Category = "Discord") 107 | void UpdatePresence(); 108 | 109 | UFUNCTION(BlueprintCallable, 110 | meta = (DisplayName = "Clear presence", Keywords = "Discord rpc"), 111 | Category = "Discord") 112 | void ClearPresence(); 113 | 114 | UFUNCTION(BlueprintCallable, 115 | meta = (DisplayName = "Respond to join request", Keywords = "Discord rpc"), 116 | Category = "Discord") 117 | void Respond(const FString& userId, int reply); 118 | 119 | UPROPERTY(BlueprintReadOnly, 120 | meta = (DisplayName = "Is Discord connected", Keywords = "Discord rpc"), 121 | Category = "Discord") 122 | bool IsConnected; 123 | 124 | UPROPERTY(BlueprintAssignable, 125 | meta = (DisplayName = "On connection", Keywords = "Discord rpc"), 126 | Category = "Discord") 127 | FDiscordConnected OnConnected; 128 | 129 | UPROPERTY(BlueprintAssignable, 130 | meta = (DisplayName = "On disconnection", Keywords = "Discord rpc"), 131 | Category = "Discord") 132 | FDiscordDisconnected OnDisconnected; 133 | 134 | UPROPERTY(BlueprintAssignable, 135 | meta = (DisplayName = "On error message", Keywords = "Discord rpc"), 136 | Category = "Discord") 137 | FDiscordErrored OnErrored; 138 | 139 | UPROPERTY(BlueprintAssignable, 140 | meta = (DisplayName = "When Discord user presses join", Keywords = "Discord rpc"), 141 | Category = "Discord") 142 | FDiscordJoin OnJoin; 143 | 144 | UPROPERTY(BlueprintAssignable, 145 | meta = (DisplayName = "When Discord user presses spectate", Keywords = "Discord rpc"), 146 | Category = "Discord") 147 | FDiscordSpectate OnSpectate; 148 | 149 | UPROPERTY(BlueprintAssignable, 150 | meta = (DisplayName = "When Discord another user sends a join request", 151 | Keywords = "Discord rpc"), 152 | Category = "Discord") 153 | FDiscordJoinRequest OnJoinRequest; 154 | 155 | UPROPERTY(BlueprintReadWrite, 156 | meta = (DisplayName = "Rich presence info", Keywords = "Discord rpc"), 157 | Category = "Discord") 158 | FDiscordRichPresence RichPresence; 159 | }; 160 | -------------------------------------------------------------------------------- /examples/unrealstatus/Plugins/discordrpc/Source/ThirdParty/DiscordRpcLibrary/DiscordRpcLibrary.Build.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using System.IO; 4 | using UnrealBuildTool; 5 | 6 | public class DiscordRpcLibrary : ModuleRules 7 | { 8 | #if WITH_FORWARDED_MODULE_RULES_CTOR 9 | public DiscordRpcLibrary(ReadOnlyTargetRules Target) : base(Target) 10 | #else 11 | public DiscordRpcLibrary(TargetInfo Target) 12 | #endif 13 | { 14 | Type = ModuleType.External; 15 | Definitions.Add("DISCORD_DYNAMIC_LIB=1"); 16 | 17 | string BaseDirectory = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "ThirdParty", "DiscordRpcLibrary")); 18 | 19 | if (Target.Platform == UnrealTargetPlatform.Win64) 20 | { 21 | string lib = Path.Combine(BaseDirectory, "Win64"); 22 | 23 | // Include headers 24 | PublicIncludePaths.Add(Path.Combine(BaseDirectory, "Include")); 25 | 26 | // Add the import library 27 | PublicLibraryPaths.Add(lib); 28 | PublicAdditionalLibraries.Add(Path.Combine(lib, "discord-rpc.lib")); 29 | 30 | // Dynamic 31 | RuntimeDependencies.Add(new RuntimeDependency(Path.Combine(lib, "discord-rpc.dll"))); 32 | PublicDelayLoadDLLs.Add("discord-rpc.dll"); 33 | } 34 | else if (Target.Platform == UnrealTargetPlatform.Linux) 35 | { 36 | string lib = Path.Combine(BaseDirectory, "Linux", "x86_64-unknown-linux-gnu"); 37 | 38 | // Include headers 39 | PublicIncludePaths.Add(Path.Combine(BaseDirectory, "Include")); 40 | 41 | // Add the import library 42 | PublicLibraryPaths.Add(lib); 43 | PublicAdditionalLibraries.Add(Path.Combine(lib, "libdiscord-rpc.so")); 44 | RuntimeDependencies.Add(new RuntimeDependency(Path.Combine(lib, "libdiscord-rpc.so"))); 45 | } 46 | else if (Target.Platform == UnrealTargetPlatform.Mac) 47 | { 48 | string lib = Path.Combine(BaseDirectory, "Mac"); 49 | 50 | // Include headers 51 | PublicIncludePaths.Add(Path.Combine(BaseDirectory, "Include")); 52 | 53 | // Add the import library 54 | PublicLibraryPaths.Add(lib); 55 | PublicAdditionalLibraries.Add(Path.Combine(lib, "libdiscord-rpc.dylib")); 56 | RuntimeDependencies.Add(new RuntimeDependency(Path.Combine(lib, "libdiscord-rpc.dylib"))); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatus.Target.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | using System.Collections.Generic; 5 | 6 | public class unrealstatusTarget : TargetRules 7 | { 8 | public unrealstatusTarget(TargetInfo Target) : base(Target) 9 | { 10 | Type = TargetType.Game; 11 | 12 | ExtraModuleNames.AddRange( new string[] { "unrealstatus" } ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatus/unrealstatus.Build.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class unrealstatus : ModuleRules 6 | { 7 | public unrealstatus(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" }); 12 | 13 | PrivateDependencyModuleNames.AddRange(new string[] { }); 14 | 15 | // Uncomment if you are using Slate UI 16 | // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); 17 | 18 | // Uncomment if you are using online features 19 | // PrivateDependencyModuleNames.Add("OnlineSubsystem"); 20 | 21 | // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatus/unrealstatus.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "unrealstatus.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, unrealstatus, "unrealstatus"); 7 | -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatus/unrealstatus.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatus/unrealstatusGameModeBase.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "unrealstatusGameModeBase.h" 4 | -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatus/unrealstatusGameModeBase.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/GameModeBase.h" 7 | #include "unrealstatusGameModeBase.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class UNREALSTATUS_API AunrealstatusGameModeBase : public AGameModeBase { 14 | GENERATED_BODY() 15 | }; 16 | -------------------------------------------------------------------------------- /examples/unrealstatus/Source/unrealstatusEditor.Target.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | using System.Collections.Generic; 5 | 6 | public class unrealstatusEditorTarget : TargetRules 7 | { 8 | public unrealstatusEditorTarget(TargetInfo Target) : base(Target) 9 | { 10 | Type = TargetType.Editor; 11 | 12 | ExtraModuleNames.AddRange( new string[] { "unrealstatus" } ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/unrealstatus/unrealstatus.uproject: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "EngineAssociation": "4.18", 4 | "Category": "", 5 | "Description": "", 6 | "Modules": [ 7 | { 8 | "Name": "unrealstatus", 9 | "Type": "Runtime", 10 | "LoadingPhase": "Default" 11 | } 12 | ], 13 | "TargetPlatforms": [ 14 | "LinuxNoEditor", 15 | "MacNoEditor", 16 | "WindowsNoEditor", 17 | "AllDesktop" 18 | ] 19 | } -------------------------------------------------------------------------------- /include/discord_register.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(DISCORD_DYNAMIC_LIB) 4 | # if defined(_WIN32) 5 | # if defined(DISCORD_BUILDING_SDK) 6 | # define DISCORD_EXPORT __declspec(dllexport) 7 | # else 8 | # define DISCORD_EXPORT __declspec(dllimport) 9 | # endif 10 | # else 11 | # define DISCORD_EXPORT __attribute__((visibility("default"))) 12 | # endif 13 | #else 14 | # define DISCORD_EXPORT 15 | #endif 16 | 17 | #ifdef __cplusplus 18 | extern "C" { 19 | #endif 20 | 21 | DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command); 22 | DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); 23 | 24 | #ifdef __cplusplus 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /include/discord_rpc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // clang-format off 5 | 6 | #if defined(DISCORD_DYNAMIC_LIB) 7 | # if defined(_WIN32) 8 | # if defined(DISCORD_BUILDING_SDK) 9 | # define DISCORD_EXPORT __declspec(dllexport) 10 | # else 11 | # define DISCORD_EXPORT __declspec(dllimport) 12 | # endif 13 | # else 14 | # define DISCORD_EXPORT __attribute__((visibility("default"))) 15 | # endif 16 | #else 17 | # define DISCORD_EXPORT 18 | #endif 19 | 20 | // clang-format on 21 | 22 | #ifdef __cplusplus 23 | extern "C" { 24 | #endif 25 | 26 | typedef struct DiscordRichPresence { 27 | const char* state; /* max 128 bytes */ 28 | const char* details; /* max 128 bytes */ 29 | int64_t startTimestamp; 30 | int64_t endTimestamp; 31 | const char* largeImageKey; /* max 32 bytes */ 32 | const char* largeImageText; /* max 128 bytes */ 33 | const char* smallImageKey; /* max 32 bytes */ 34 | const char* smallImageText; /* max 128 bytes */ 35 | const char* partyId; /* max 128 bytes */ 36 | int partySize; 37 | int partyMax; 38 | const char* matchSecret; /* max 128 bytes */ 39 | const char* joinSecret; /* max 128 bytes */ 40 | const char* spectateSecret; /* max 128 bytes */ 41 | int8_t instance; 42 | } DiscordRichPresence; 43 | 44 | typedef struct DiscordJoinRequest { 45 | const char* userId; 46 | const char* username; 47 | const char* discriminator; 48 | const char* avatar; 49 | } DiscordJoinRequest; 50 | 51 | typedef struct DiscordEventHandlers { 52 | void (*ready)(void); 53 | void (*disconnected)(int errorCode, const char* message); 54 | void (*errored)(int errorCode, const char* message); 55 | void (*joinGame)(const char* joinSecret); 56 | void (*spectateGame)(const char* spectateSecret); 57 | void (*joinRequest)(const DiscordJoinRequest* request); 58 | } DiscordEventHandlers; 59 | 60 | #define DISCORD_REPLY_NO 0 61 | #define DISCORD_REPLY_YES 1 62 | #define DISCORD_REPLY_IGNORE 2 63 | 64 | DISCORD_EXPORT void Discord_Initialize(const char* applicationId, 65 | DiscordEventHandlers* handlers, 66 | int autoRegister, 67 | const char* optionalSteamId); 68 | DISCORD_EXPORT void Discord_Shutdown(void); 69 | 70 | /* checks for incoming messages, dispatches callbacks */ 71 | DISCORD_EXPORT void Discord_RunCallbacks(void); 72 | 73 | /* If you disable the lib starting its own io thread, you'll need to call this from your own */ 74 | #ifdef DISCORD_DISABLE_IO_THREAD 75 | DISCORD_EXPORT void Discord_UpdateConnection(void); 76 | #endif 77 | 78 | DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence); 79 | DISCORD_EXPORT void Discord_ClearPresence(void); 80 | 81 | DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); 82 | 83 | DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers); 84 | 85 | #ifdef __cplusplus 86 | } /* extern "C" */ 87 | #endif 88 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${PROJECT_SOURCE_DIR}/include) 2 | 3 | option(ENABLE_IO_THREAD "Start up a separate I/O thread, otherwise I'd need to call an update function" ON) 4 | option(USE_STATIC_CRT "Use /MT[d] for dynamic library" OFF) 5 | 6 | set(CMAKE_CXX_STANDARD 14) 7 | 8 | set(BASE_RPC_SRC 9 | ${PROJECT_SOURCE_DIR}/include/discord_rpc.h 10 | discord_rpc.cpp 11 | ${PROJECT_SOURCE_DIR}/include/discord_register.h 12 | rpc_connection.h 13 | rpc_connection.cpp 14 | serialization.h 15 | serialization.cpp 16 | connection.h 17 | backoff.h 18 | msg_queue.h 19 | ) 20 | 21 | if (${BUILD_SHARED_LIBS}) 22 | if(WIN32) 23 | set(BASE_RPC_SRC ${BASE_RPC_SRC} dllmain.cpp) 24 | endif(WIN32) 25 | endif(${BUILD_SHARED_LIBS}) 26 | 27 | if(WIN32) 28 | add_definitions(-DDISCORD_WINDOWS) 29 | set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_win.cpp discord_register_win.cpp) 30 | add_library(discord-rpc ${BASE_RPC_SRC}) 31 | if (MSVC) 32 | if(USE_STATIC_CRT) 33 | foreach(CompilerFlag 34 | CMAKE_CXX_FLAGS 35 | CMAKE_CXX_FLAGS_DEBUG 36 | CMAKE_CXX_FLAGS_RELEASE 37 | CMAKE_C_FLAGS 38 | CMAKE_C_FLAGS_DEBUG 39 | CMAKE_C_FLAGS_RELEASE) 40 | string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") 41 | endforeach() 42 | endif(USE_STATIC_CRT) 43 | target_compile_options(discord-rpc PRIVATE /EHsc 44 | /Wall 45 | /wd4100 # unreferenced formal parameter 46 | /wd4514 # unreferenced inline 47 | /wd4625 # copy constructor deleted 48 | /wd5026 # move constructor deleted 49 | /wd4626 # move assignment operator deleted 50 | /wd4668 # not defined preprocessor macro 51 | /wd4710 # function not inlined 52 | /wd4711 # function was inlined 53 | /wd4820 # structure padding 54 | /wd4946 # reinterpret_cast used between related classes 55 | /wd5027 # move assignment operator was implicitly defined as deleted 56 | ) 57 | endif(MSVC) 58 | target_link_libraries(discord-rpc PRIVATE psapi advapi32) 59 | endif(WIN32) 60 | 61 | if(UNIX) 62 | set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_unix.cpp) 63 | 64 | if (APPLE) 65 | add_definitions(-DDISCORD_OSX) 66 | set(BASE_RPC_SRC ${BASE_RPC_SRC} discord_register_osx.m) 67 | else (APPLE) 68 | add_definitions(-DDISCORD_LINUX) 69 | set(BASE_RPC_SRC ${BASE_RPC_SRC} discord_register_linux.cpp) 70 | endif(APPLE) 71 | 72 | add_library(discord-rpc ${BASE_RPC_SRC}) 73 | target_link_libraries(discord-rpc PUBLIC pthread) 74 | target_compile_options(discord-rpc PRIVATE 75 | -g 76 | -Wall 77 | -Wextra 78 | -Wpedantic 79 | -Werror 80 | -Wno-unknown-pragmas # pragma push thing doesn't work on clang 81 | -Wno-old-style-cast # it's fine 82 | -Wno-c++98-compat # that was almost 2 decades ago 83 | -Wno-c++98-compat-pedantic 84 | -Wno-missing-noreturn 85 | -Wno-padded # structure padding 86 | -Wno-covered-switch-default 87 | -Wno-exit-time-destructors # not sure about these 88 | -Wno-global-constructors 89 | ) 90 | 91 | if (${BUILD_SHARED_LIBS}) 92 | target_compile_options(discord-rpc PRIVATE -fPIC) 93 | endif (${BUILD_SHARED_LIBS}) 94 | 95 | if (APPLE) 96 | target_link_libraries(discord-rpc PRIVATE "-framework AppKit") 97 | endif (APPLE) 98 | endif(UNIX) 99 | 100 | target_include_directories(discord-rpc PRIVATE ${RAPIDJSON}/include) 101 | 102 | if (NOT ${ENABLE_IO_THREAD}) 103 | target_compile_definitions(discord-rpc PUBLIC -DDISCORD_DISABLE_IO_THREAD) 104 | endif (NOT ${ENABLE_IO_THREAD}) 105 | 106 | if (${BUILD_SHARED_LIBS}) 107 | target_compile_definitions(discord-rpc PUBLIC -DDISCORD_DYNAMIC_LIB) 108 | target_compile_definitions(discord-rpc PRIVATE -DDISCORD_BUILDING_SDK) 109 | endif(${BUILD_SHARED_LIBS}) 110 | 111 | if (CLANG_FORMAT_CMD) 112 | add_dependencies(discord-rpc clangformat) 113 | endif(CLANG_FORMAT_CMD) 114 | 115 | # install 116 | 117 | install( 118 | TARGETS discord-rpc 119 | EXPORT "discord-rpc" 120 | RUNTIME 121 | DESTINATION "${CMAKE_INSTALL_BINDIR}" 122 | LIBRARY 123 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 124 | ARCHIVE 125 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 126 | INCLUDES 127 | DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" 128 | ) 129 | 130 | install( 131 | FILES 132 | "../include/discord_rpc.h" 133 | "../include/discord_register.h" 134 | DESTINATION "include" 135 | ) 136 | -------------------------------------------------------------------------------- /src/backoff.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct Backoff { 9 | int64_t minAmount; 10 | int64_t maxAmount; 11 | int64_t current; 12 | int fails; 13 | std::mt19937_64 randGenerator; 14 | std::uniform_real_distribution<> randDistribution; 15 | 16 | double rand01() { return randDistribution(randGenerator); } 17 | 18 | Backoff(int64_t min, int64_t max) 19 | : minAmount(min) 20 | , maxAmount(max) 21 | , current(min) 22 | , fails(0) 23 | , randGenerator((uint64_t)time(0)) 24 | { 25 | } 26 | 27 | void reset() 28 | { 29 | fails = 0; 30 | current = minAmount; 31 | } 32 | 33 | int64_t nextDelay() 34 | { 35 | ++fails; 36 | int64_t delay = (int64_t)((double)current * 2.0 * rand01()); 37 | current = std::min(current + delay, maxAmount); 38 | return current; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This is to wrap the platform specific kinds of connect/read/write. 4 | 5 | #include 6 | #include 7 | 8 | // not really connectiony, but need per-platform 9 | int GetProcessId(); 10 | 11 | struct BaseConnection { 12 | static BaseConnection* Create(); 13 | static void Destroy(BaseConnection*&); 14 | bool isOpen{false}; 15 | bool Open(); 16 | bool Close(); 17 | bool Write(const void* data, size_t length); 18 | bool Read(void* data, size_t length); 19 | }; 20 | -------------------------------------------------------------------------------- /src/connection_unix.cpp: -------------------------------------------------------------------------------- 1 | #include "connection.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | int GetProcessId() 13 | { 14 | return ::getpid(); 15 | } 16 | 17 | struct BaseConnectionUnix : public BaseConnection { 18 | int sock{-1}; 19 | }; 20 | 21 | static BaseConnectionUnix Connection; 22 | static sockaddr_un PipeAddr{}; 23 | #ifdef MSG_NOSIGNAL 24 | static int MsgFlags = MSG_NOSIGNAL; 25 | #else 26 | static int MsgFlags = 0; 27 | #endif 28 | 29 | static const char* GetTempPath() 30 | { 31 | const char* temp = getenv("XDG_RUNTIME_DIR"); 32 | temp = temp ? temp : getenv("TMPDIR"); 33 | temp = temp ? temp : getenv("TMP"); 34 | temp = temp ? temp : getenv("TEMP"); 35 | temp = temp ? temp : "/tmp"; 36 | return temp; 37 | } 38 | 39 | /*static*/ BaseConnection* BaseConnection::Create() 40 | { 41 | PipeAddr.sun_family = AF_UNIX; 42 | return &Connection; 43 | } 44 | 45 | /*static*/ void BaseConnection::Destroy(BaseConnection*& c) 46 | { 47 | auto self = reinterpret_cast(c); 48 | self->Close(); 49 | c = nullptr; 50 | } 51 | 52 | bool BaseConnection::Open() 53 | { 54 | const char* tempPath = GetTempPath(); 55 | auto self = reinterpret_cast(this); 56 | self->sock = socket(AF_UNIX, SOCK_STREAM, 0); 57 | if (self->sock == -1) { 58 | return false; 59 | } 60 | fcntl(self->sock, F_SETFL, O_NONBLOCK); 61 | #ifdef SO_NOSIGPIPE 62 | int optval = 1; 63 | setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval)); 64 | #endif 65 | 66 | for (int pipeNum = 0; pipeNum < 10; ++pipeNum) { 67 | snprintf( 68 | PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum); 69 | int err = connect(self->sock, (const sockaddr*)&PipeAddr, sizeof(PipeAddr)); 70 | if (err == 0) { 71 | self->isOpen = true; 72 | return true; 73 | } 74 | } 75 | self->Close(); 76 | return false; 77 | } 78 | 79 | bool BaseConnection::Close() 80 | { 81 | auto self = reinterpret_cast(this); 82 | if (self->sock == -1) { 83 | return false; 84 | } 85 | close(self->sock); 86 | self->sock = -1; 87 | self->isOpen = false; 88 | return true; 89 | } 90 | 91 | bool BaseConnection::Write(const void* data, size_t length) 92 | { 93 | auto self = reinterpret_cast(this); 94 | 95 | if (self->sock == -1) { 96 | return false; 97 | } 98 | 99 | ssize_t sentBytes = send(self->sock, data, length, MsgFlags); 100 | if (sentBytes < 0) { 101 | Close(); 102 | } 103 | return sentBytes == (ssize_t)length; 104 | } 105 | 106 | bool BaseConnection::Read(void* data, size_t length) 107 | { 108 | auto self = reinterpret_cast(this); 109 | 110 | if (self->sock == -1) { 111 | return false; 112 | } 113 | 114 | int res = (int)recv(self->sock, data, length, MsgFlags); 115 | if (res < 0) { 116 | if (errno == EAGAIN) { 117 | return false; 118 | } 119 | Close(); 120 | } 121 | return res == (int)length; 122 | } 123 | -------------------------------------------------------------------------------- /src/connection_win.cpp: -------------------------------------------------------------------------------- 1 | #include "connection.h" 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define NOMCX 5 | #define NOSERVICE 6 | #define NOIME 7 | #include 8 | #include 9 | 10 | int GetProcessId() 11 | { 12 | return (int)::GetCurrentProcessId(); 13 | } 14 | 15 | struct BaseConnectionWin : public BaseConnection { 16 | HANDLE pipe{INVALID_HANDLE_VALUE}; 17 | }; 18 | 19 | static BaseConnectionWin Connection; 20 | 21 | /*static*/ BaseConnection* BaseConnection::Create() 22 | { 23 | return &Connection; 24 | } 25 | 26 | /*static*/ void BaseConnection::Destroy(BaseConnection*& c) 27 | { 28 | auto self = reinterpret_cast(c); 29 | self->Close(); 30 | c = nullptr; 31 | } 32 | 33 | bool BaseConnection::Open() 34 | { 35 | wchar_t pipeName[]{L"\\\\?\\pipe\\discord-ipc-0"}; 36 | const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2; 37 | pipeName[pipeDigit] = L'0'; 38 | auto self = reinterpret_cast(this); 39 | for (;;) { 40 | self->pipe = ::CreateFileW( 41 | pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); 42 | if (self->pipe != INVALID_HANDLE_VALUE) { 43 | self->isOpen = true; 44 | return true; 45 | } 46 | 47 | auto lastError = GetLastError(); 48 | if (lastError == ERROR_FILE_NOT_FOUND) { 49 | if (pipeName[pipeDigit] < L'9') { 50 | pipeName[pipeDigit]++; 51 | continue; 52 | } 53 | } 54 | else if (lastError == ERROR_PIPE_BUSY) { 55 | if (!WaitNamedPipeW(pipeName, 10000)) { 56 | return false; 57 | } 58 | continue; 59 | } 60 | return false; 61 | } 62 | } 63 | 64 | bool BaseConnection::Close() 65 | { 66 | auto self = reinterpret_cast(this); 67 | ::CloseHandle(self->pipe); 68 | self->pipe = INVALID_HANDLE_VALUE; 69 | self->isOpen = false; 70 | return true; 71 | } 72 | 73 | bool BaseConnection::Write(const void* data, size_t length) 74 | { 75 | if (length == 0) { 76 | return true; 77 | } 78 | auto self = reinterpret_cast(this); 79 | assert(self); 80 | if (!self) { 81 | return false; 82 | } 83 | if (self->pipe == INVALID_HANDLE_VALUE) { 84 | return false; 85 | } 86 | assert(data); 87 | if (!data) { 88 | return false; 89 | } 90 | const DWORD bytesLength = (DWORD)length; 91 | DWORD bytesWritten = 0; 92 | return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && 93 | bytesWritten == bytesLength; 94 | } 95 | 96 | bool BaseConnection::Read(void* data, size_t length) 97 | { 98 | assert(data); 99 | if (!data) { 100 | return false; 101 | } 102 | auto self = reinterpret_cast(this); 103 | assert(self); 104 | if (!self) { 105 | return false; 106 | } 107 | if (self->pipe == INVALID_HANDLE_VALUE) { 108 | return false; 109 | } 110 | DWORD bytesAvailable = 0; 111 | if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) { 112 | if (bytesAvailable >= length) { 113 | DWORD bytesToRead = (DWORD)length; 114 | DWORD bytesRead = 0; 115 | if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) { 116 | assert(bytesToRead == bytesRead); 117 | return true; 118 | } 119 | else { 120 | Close(); 121 | } 122 | } 123 | } 124 | else { 125 | Close(); 126 | } 127 | return false; 128 | } 129 | -------------------------------------------------------------------------------- /src/discord_register_linux.cpp: -------------------------------------------------------------------------------- 1 | #include "discord_rpc.h" 2 | #include "discord_register.h" 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | static bool Mkdir(const char* path) 13 | { 14 | int result = mkdir(path, 0755); 15 | if (result == 0) { 16 | return true; 17 | } 18 | if (errno == EEXIST) { 19 | return true; 20 | } 21 | return false; 22 | } 23 | 24 | // we want to register games so we can run them from Discord client as discord-:// 25 | extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command) 26 | { 27 | // Add a desktop file and update some mime handlers so that xdg-open does the right thing. 28 | 29 | const char* home = getenv("HOME"); 30 | if (!home) { 31 | return; 32 | } 33 | 34 | char exePath[1024]; 35 | if (!command || !command[0]) { 36 | if (readlink("/proc/self/exe", exePath, sizeof(exePath)) <= 0) { 37 | return; 38 | } 39 | command = exePath; 40 | } 41 | 42 | const char* destopFileFormat = "[Desktop Entry]\n" 43 | "Name=Game %s\n" 44 | "Exec=%s %%u\n" // note: it really wants that %u in there 45 | "Type=Application\n" 46 | "NoDisplay=true\n" 47 | "Categories=Discord;Games;\n" 48 | "MimeType=x-scheme-handler/discord-%s;\n"; 49 | char desktopFile[2048]; 50 | int fileLen = snprintf( 51 | desktopFile, sizeof(desktopFile), destopFileFormat, applicationId, command, applicationId); 52 | if (fileLen <= 0) { 53 | return; 54 | } 55 | 56 | char desktopFilename[256]; 57 | snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId); 58 | 59 | char desktopFilePath[1024]; 60 | snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home); 61 | if (!Mkdir(desktopFilePath)) { 62 | return; 63 | } 64 | strcat(desktopFilePath, "/share"); 65 | if (!Mkdir(desktopFilePath)) { 66 | return; 67 | } 68 | strcat(desktopFilePath, "/applications"); 69 | if (!Mkdir(desktopFilePath)) { 70 | return; 71 | } 72 | strcat(desktopFilePath, desktopFilename); 73 | 74 | FILE* fp = fopen(desktopFilePath, "w"); 75 | if (fp) { 76 | fwrite(desktopFile, 1, fileLen, fp); 77 | fclose(fp); 78 | } 79 | else { 80 | return; 81 | } 82 | 83 | char xdgMimeCommand[1024]; 84 | snprintf(xdgMimeCommand, 85 | sizeof(xdgMimeCommand), 86 | "xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s", 87 | applicationId, 88 | applicationId); 89 | if (system(xdgMimeCommand) < 0) { 90 | fprintf(stderr, "Failed to register mime handler\n"); 91 | } 92 | } 93 | 94 | extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId) 95 | { 96 | char command[256]; 97 | sprintf(command, "xdg-open steam://rungameid/%s", steamId); 98 | Discord_Register(applicationId, command); 99 | } 100 | -------------------------------------------------------------------------------- /src/discord_register_osx.m: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #import 5 | 6 | #include "discord_register.h" 7 | 8 | static void RegisterCommand(const char* applicationId, const char* command) 9 | { 10 | // There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command 11 | // to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open 12 | // the command therein (will pass to js's window.open, so requires a url-like thing) 13 | 14 | // Note: will not work for sandboxed apps 15 | NSString *home = NSHomeDirectory(); 16 | if (!home) { 17 | return; 18 | } 19 | 20 | NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"] 21 | stringByAppendingPathComponent:@"Application Support"] 22 | stringByAppendingPathComponent:@"discord"] 23 | stringByAppendingPathComponent:@"games"] 24 | stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]] 25 | stringByAppendingPathExtension:@"json"]; 26 | [[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; 27 | 28 | NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command]; 29 | [jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil]; 30 | } 31 | 32 | static void RegisterURL(const char* applicationId) 33 | { 34 | char url[256]; 35 | snprintf(url, sizeof(url), "discord-%s", applicationId); 36 | CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8); 37 | 38 | NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier]; 39 | if (!myBundleId) { 40 | fprintf(stderr, "No bundle id found\n"); 41 | return; 42 | } 43 | 44 | NSURL* myURL = [[NSBundle mainBundle] bundleURL]; 45 | if (!myURL) { 46 | fprintf(stderr, "No bundle url found\n"); 47 | return; 48 | } 49 | 50 | OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId); 51 | if (status != noErr) { 52 | fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status); 53 | return; 54 | } 55 | 56 | status = LSRegisterURL((__bridge CFURLRef)myURL, true); 57 | if (status != noErr) { 58 | fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status); 59 | } 60 | } 61 | 62 | void Discord_Register(const char* applicationId, const char* command) 63 | { 64 | if (command) { 65 | RegisterCommand(applicationId, command); 66 | } 67 | else { 68 | // raii lite 69 | @autoreleasepool { 70 | RegisterURL(applicationId); 71 | } 72 | } 73 | } 74 | 75 | void Discord_RegisterSteamGame(const char* applicationId, const char* steamId) 76 | { 77 | char command[256]; 78 | snprintf(command, 256, "steam://rungameid/%s", steamId); 79 | Discord_Register(applicationId, command); 80 | } 81 | -------------------------------------------------------------------------------- /src/discord_register_win.cpp: -------------------------------------------------------------------------------- 1 | #include "discord_rpc.h" 2 | #include "discord_register.h" 3 | 4 | #define WIN32_LEAN_AND_MEAN 5 | #define NOMCX 6 | #define NOSERVICE 7 | #define NOIME 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /** 14 | * Updated fixes for MinGW and WinXP 15 | * This block is written the way it does not involve changing the rest of the code 16 | * Checked to be compiling 17 | * 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW 18 | * #include guarded, functions redirected to substitutes 19 | * 2) RegSetKeyValueW and LSTATUS are not declared in 20 | * The entire function is rewritten 21 | */ 22 | #ifdef __MINGW32__ 23 | /// strsafe.h fixes 24 | static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) 25 | { 26 | HRESULT ret; 27 | va_list va; 28 | va_start(va, pszFormat); 29 | cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault othervise 30 | ret = vsnwprintf(pszDest, cbDest, pszFormat, va); 31 | pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned 32 | va_end(va); 33 | return ret; 34 | } 35 | #else 36 | #include 37 | #endif // __MINGW32__ 38 | 39 | /// winreg.h fixes 40 | #ifndef LSTATUS 41 | #define LSTATUS LONG 42 | #endif 43 | #ifdef RegSetKeyValueW 44 | #undefine RegSetKeyValueW 45 | #endif 46 | #define RegSetKeyValueW regset 47 | static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) 48 | { 49 | HKEY htkey = hkey, hsubkey = nullptr; 50 | LSTATUS ret; 51 | if (subkey && subkey[0]) 52 | { 53 | if((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) != ERROR_SUCCESS) return ret; 54 | htkey = hsubkey; 55 | } 56 | ret = RegSetValueExW(htkey, name, 0, type, (const BYTE*)data, len); 57 | if (hsubkey && hsubkey != hkey) RegCloseKey(hsubkey); 58 | return ret; 59 | } 60 | 61 | static void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command) 62 | { 63 | // https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx 64 | // we want to register games so we can run them as discord-:// 65 | // Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions. 66 | 67 | wchar_t exeFilePath[MAX_PATH]; 68 | DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH); 69 | wchar_t openCommand[1024]; 70 | 71 | if (command && command[0]) { 72 | StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command); 73 | } 74 | else { 75 | //StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath); 76 | StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath); 77 | } 78 | 79 | wchar_t protocolName[64]; 80 | StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId); 81 | wchar_t protocolDescription[128]; 82 | StringCbPrintfW( 83 | protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId); 84 | wchar_t urlProtocol = 0; 85 | 86 | wchar_t keyName[256]; 87 | StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName); 88 | HKEY key; 89 | auto status = 90 | RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr); 91 | if (status != ERROR_SUCCESS) { 92 | fprintf(stderr, "Error creating key\n"); 93 | return; 94 | } 95 | DWORD len; 96 | LSTATUS result; 97 | len = (DWORD)lstrlenW(protocolDescription) + 1; 98 | result = 99 | RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t)); 100 | if (FAILED(result)) { 101 | fprintf(stderr, "Error writing description\n"); 102 | } 103 | 104 | len = (DWORD)lstrlenW(protocolDescription) + 1; 105 | result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t)); 106 | if (FAILED(result)) { 107 | fprintf(stderr, "Error writing description\n"); 108 | } 109 | 110 | result = RegSetKeyValueW( 111 | key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t)); 112 | if (FAILED(result)) { 113 | fprintf(stderr, "Error writing icon\n"); 114 | } 115 | 116 | len = (DWORD)lstrlenW(openCommand) + 1; 117 | result = RegSetKeyValueW( 118 | key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t)); 119 | if (FAILED(result)) { 120 | fprintf(stderr, "Error writing command\n"); 121 | } 122 | RegCloseKey(key); 123 | } 124 | 125 | extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command) 126 | { 127 | wchar_t appId[32]; 128 | MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); 129 | 130 | wchar_t openCommand[1024]; 131 | const wchar_t* wcommand = nullptr; 132 | if (command && command[0]) { 133 | const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand); 134 | MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen); 135 | wcommand = openCommand; 136 | } 137 | 138 | Discord_RegisterW(appId, wcommand); 139 | } 140 | 141 | extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId) 142 | { 143 | wchar_t appId[32]; 144 | MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); 145 | 146 | wchar_t wSteamId[32]; 147 | MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32); 148 | 149 | HKEY key; 150 | auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key); 151 | if (status != ERROR_SUCCESS) { 152 | fprintf(stderr, "Error opening Steam key\n"); 153 | return; 154 | } 155 | 156 | wchar_t steamPath[MAX_PATH]; 157 | DWORD pathBytes = sizeof(steamPath); 158 | status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE*)steamPath, &pathBytes); 159 | RegCloseKey(key); 160 | if (status != ERROR_SUCCESS || pathBytes < 1) { 161 | fprintf(stderr, "Error reading SteamExe key\n"); 162 | return; 163 | } 164 | 165 | DWORD pathChars = pathBytes / sizeof(wchar_t); 166 | for (DWORD i = 0; i < pathChars; ++i) { 167 | if (steamPath[i] == L'/') { 168 | steamPath[i] = L'\\'; 169 | } 170 | } 171 | 172 | wchar_t command[1024]; 173 | StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId); 174 | 175 | Discord_RegisterW(appId, command); 176 | } 177 | -------------------------------------------------------------------------------- /src/discord_rpc.cpp: -------------------------------------------------------------------------------- 1 | #include "discord_rpc.h" 2 | 3 | #include "backoff.h" 4 | #include "discord_register.h" 5 | #include "msg_queue.h" 6 | #include "rpc_connection.h" 7 | #include "serialization.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #ifndef DISCORD_DISABLE_IO_THREAD 14 | #include 15 | #include 16 | #endif 17 | 18 | constexpr size_t MaxMessageSize{16 * 1024}; 19 | constexpr size_t MessageQueueSize{8}; 20 | constexpr size_t JoinQueueSize{8}; 21 | 22 | struct QueuedMessage { 23 | size_t length; 24 | char buffer[MaxMessageSize]; 25 | 26 | void Copy(const QueuedMessage& other) 27 | { 28 | length = other.length; 29 | if (length) { 30 | memcpy(buffer, other.buffer, length); 31 | } 32 | } 33 | }; 34 | 35 | struct JoinRequest { 36 | // snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null 37 | // terminator = 21 38 | char userId[32]; 39 | // 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null 40 | // terminator = 129 41 | char username[344]; 42 | // 4 decimal digits + 1 null terminator = 5 43 | char discriminator[8]; 44 | // optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35 45 | char avatar[128]; 46 | // Rounded way up because I'm paranoid about games breaking from future changes in these sizes 47 | }; 48 | 49 | static RpcConnection* Connection{nullptr}; 50 | static DiscordEventHandlers QueuedHandlers{}; 51 | static DiscordEventHandlers Handlers{}; 52 | static std::atomic_bool WasJustConnected{false}; 53 | static std::atomic_bool WasJustDisconnected{false}; 54 | static std::atomic_bool GotErrorMessage{false}; 55 | static std::atomic_bool WasJoinGame{false}; 56 | static std::atomic_bool WasSpectateGame{false}; 57 | static char JoinGameSecret[256]; 58 | static char SpectateGameSecret[256]; 59 | static int LastErrorCode{0}; 60 | static char LastErrorMessage[256]; 61 | static int LastDisconnectErrorCode{0}; 62 | static char LastDisconnectErrorMessage[256]; 63 | static std::mutex PresenceMutex; 64 | static std::mutex HandlerMutex; 65 | static QueuedMessage QueuedPresence{}; 66 | static MsgQueue SendQueue; 67 | static MsgQueue JoinAskQueue; 68 | 69 | // We want to auto connect, and retry on failure, but not as fast as possible. This does expoential 70 | // backoff from 0.5 seconds to 1 minute 71 | static Backoff ReconnectTimeMs(500, 60 * 1000); 72 | static auto NextConnect = std::chrono::system_clock::now(); 73 | static int Pid{0}; 74 | static int Nonce{1}; 75 | 76 | #ifndef DISCORD_DISABLE_IO_THREAD 77 | static void Discord_UpdateConnection(void); 78 | class IoThreadHolder { 79 | private: 80 | std::atomic_bool keepRunning{true}; 81 | std::mutex waitForIOMutex; 82 | std::condition_variable waitForIOActivity; 83 | std::thread ioThread; 84 | 85 | public: 86 | void Start() 87 | { 88 | keepRunning.store(true); 89 | ioThread = std::thread([&]() { 90 | const std::chrono::duration maxWait{500LL}; 91 | while (keepRunning.load()) { 92 | Discord_UpdateConnection(); 93 | std::unique_lock lock(waitForIOMutex); 94 | waitForIOActivity.wait_for(lock, maxWait); 95 | } 96 | }); 97 | } 98 | 99 | void Notify() { waitForIOActivity.notify_all(); } 100 | 101 | void Stop() 102 | { 103 | keepRunning.exchange(false); 104 | Notify(); 105 | if (ioThread.joinable()) { 106 | ioThread.join(); 107 | } 108 | } 109 | 110 | ~IoThreadHolder() { Stop(); } 111 | }; 112 | #else 113 | class IoThreadHolder { 114 | public: 115 | void Start() {} 116 | void Stop() {} 117 | void Notify() {} 118 | }; 119 | #endif // DISCORD_DISABLE_IO_THREAD 120 | static IoThreadHolder IoThread; 121 | 122 | static void UpdateReconnectTime() 123 | { 124 | NextConnect = std::chrono::system_clock::now() + 125 | std::chrono::duration{ReconnectTimeMs.nextDelay()}; 126 | } 127 | 128 | #ifdef DISCORD_DISABLE_IO_THREAD 129 | extern "C" DISCORD_EXPORT void Discord_UpdateConnection(void) 130 | #else 131 | static void Discord_UpdateConnection(void) 132 | #endif 133 | { 134 | if (!Connection) { 135 | return; 136 | } 137 | 138 | if (!Connection->IsOpen()) { 139 | if (std::chrono::system_clock::now() >= NextConnect) { 140 | UpdateReconnectTime(); 141 | Connection->Open(); 142 | } 143 | } 144 | else { 145 | // reads 146 | 147 | for (;;) { 148 | JsonDocument message; 149 | 150 | if (!Connection->Read(message)) { 151 | break; 152 | } 153 | 154 | const char* evtName = GetStrMember(&message, "evt"); 155 | const char* nonce = GetStrMember(&message, "nonce"); 156 | 157 | if (nonce) { 158 | // in responses only -- should use to match up response when needed. 159 | 160 | if (evtName && strcmp(evtName, "ERROR") == 0) { 161 | auto data = GetObjMember(&message, "data"); 162 | LastErrorCode = GetIntMember(data, "code"); 163 | StringCopy(LastErrorMessage, GetStrMember(data, "message", "")); 164 | GotErrorMessage.store(true); 165 | } 166 | } 167 | else { 168 | // should have evt == name of event, optional data 169 | if (evtName == nullptr) { 170 | continue; 171 | } 172 | 173 | auto data = GetObjMember(&message, "data"); 174 | 175 | if (strcmp(evtName, "ACTIVITY_JOIN") == 0) { 176 | auto secret = GetStrMember(data, "secret"); 177 | if (secret) { 178 | StringCopy(JoinGameSecret, secret); 179 | WasJoinGame.store(true); 180 | } 181 | } 182 | else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) { 183 | auto secret = GetStrMember(data, "secret"); 184 | if (secret) { 185 | StringCopy(SpectateGameSecret, secret); 186 | WasSpectateGame.store(true); 187 | } 188 | } 189 | else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) { 190 | auto user = GetObjMember(data, "user"); 191 | auto userId = GetStrMember(user, "id"); 192 | auto username = GetStrMember(user, "username"); 193 | auto avatar = GetStrMember(user, "avatar"); 194 | auto joinReq = JoinAskQueue.GetNextAddMessage(); 195 | if (userId && username && joinReq) { 196 | StringCopy(joinReq->userId, userId); 197 | StringCopy(joinReq->username, username); 198 | auto discriminator = GetStrMember(user, "discriminator"); 199 | if (discriminator) { 200 | StringCopy(joinReq->discriminator, discriminator); 201 | } 202 | if (avatar) { 203 | StringCopy(joinReq->avatar, avatar); 204 | } 205 | else { 206 | joinReq->avatar[0] = 0; 207 | } 208 | JoinAskQueue.CommitAdd(); 209 | } 210 | } 211 | } 212 | } 213 | 214 | // writes 215 | if (QueuedPresence.length) { 216 | QueuedMessage local; 217 | { 218 | std::lock_guard guard(PresenceMutex); 219 | local.Copy(QueuedPresence); 220 | QueuedPresence.length = 0; 221 | } 222 | if (!Connection->Write(local.buffer, local.length)) { 223 | // if we fail to send, requeue 224 | std::lock_guard guard(PresenceMutex); 225 | QueuedPresence.Copy(local); 226 | } 227 | } 228 | 229 | while (SendQueue.HavePendingSends()) { 230 | auto qmessage = SendQueue.GetNextSendMessage(); 231 | Connection->Write(qmessage->buffer, qmessage->length); 232 | SendQueue.CommitSend(); 233 | } 234 | } 235 | } 236 | 237 | static void SignalIOActivity() 238 | { 239 | IoThread.Notify(); 240 | } 241 | 242 | static bool RegisterForEvent(const char* evtName) 243 | { 244 | auto qmessage = SendQueue.GetNextAddMessage(); 245 | if (qmessage) { 246 | qmessage->length = 247 | JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName); 248 | SendQueue.CommitAdd(); 249 | SignalIOActivity(); 250 | return true; 251 | } 252 | return false; 253 | } 254 | 255 | static bool DeregisterForEvent(const char* evtName) 256 | { 257 | auto qmessage = SendQueue.GetNextAddMessage(); 258 | if (qmessage) { 259 | qmessage->length = 260 | JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName); 261 | SendQueue.CommitAdd(); 262 | SignalIOActivity(); 263 | return true; 264 | } 265 | return false; 266 | } 267 | 268 | extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId, 269 | DiscordEventHandlers* handlers, 270 | int autoRegister, 271 | const char* optionalSteamId) 272 | { 273 | if (autoRegister) { 274 | if (optionalSteamId && optionalSteamId[0]) { 275 | Discord_RegisterSteamGame(applicationId, optionalSteamId); 276 | } 277 | else { 278 | Discord_Register(applicationId, nullptr); 279 | } 280 | } 281 | 282 | Pid = GetProcessId(); 283 | 284 | { 285 | std::lock_guard guard(HandlerMutex); 286 | 287 | if (handlers) { 288 | QueuedHandlers = *handlers; 289 | } 290 | else { 291 | QueuedHandlers = {}; 292 | } 293 | 294 | Handlers = {}; 295 | 296 | } 297 | 298 | if (Connection) { 299 | return; 300 | } 301 | 302 | Connection = RpcConnection::Create(applicationId); 303 | Connection->onConnect = []() { 304 | Discord_UpdateHandlers(&QueuedHandlers); 305 | WasJustConnected.exchange(true); 306 | ReconnectTimeMs.reset(); 307 | }; 308 | Connection->onDisconnect = [](int err, const char* message) { 309 | LastDisconnectErrorCode = err; 310 | StringCopy(LastDisconnectErrorMessage, message); 311 | { 312 | std::lock_guard guard(HandlerMutex); 313 | Handlers = {}; 314 | } 315 | WasJustDisconnected.exchange(true); 316 | UpdateReconnectTime(); 317 | }; 318 | 319 | IoThread.Start(); 320 | } 321 | 322 | extern "C" DISCORD_EXPORT void Discord_Shutdown(void) 323 | { 324 | if (!Connection) { 325 | return; 326 | } 327 | Connection->onConnect = nullptr; 328 | Connection->onDisconnect = nullptr; 329 | Handlers = {}; 330 | IoThread.Stop(); 331 | RpcConnection::Destroy(Connection); 332 | } 333 | 334 | extern "C" DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence) 335 | { 336 | { 337 | std::lock_guard guard(PresenceMutex); 338 | QueuedPresence.length = JsonWriteRichPresenceObj( 339 | QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence); 340 | } 341 | SignalIOActivity(); 342 | } 343 | 344 | extern "C" DISCORD_EXPORT void Discord_ClearPresence(void) 345 | { 346 | Discord_UpdatePresence(nullptr); 347 | } 348 | 349 | extern "C" DISCORD_EXPORT void Discord_Respond(const char* userId, /* DISCORD_REPLY_ */ int reply) 350 | { 351 | // if we are not connected, let's not batch up stale messages for later 352 | if (!Connection || !Connection->IsOpen()) { 353 | return; 354 | } 355 | auto qmessage = SendQueue.GetNextAddMessage(); 356 | if (qmessage) { 357 | qmessage->length = 358 | JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++); 359 | SendQueue.CommitAdd(); 360 | SignalIOActivity(); 361 | } 362 | } 363 | 364 | extern "C" DISCORD_EXPORT void Discord_RunCallbacks(void) 365 | { 366 | // Note on some weirdness: internally we might connect, get other signals, disconnect any number 367 | // of times inbetween calls here. Externally, we want the sequence to seem sane, so any other 368 | // signals are book-ended by calls to ready and disconnect. 369 | 370 | if (!Connection) { 371 | return; 372 | } 373 | 374 | bool wasDisconnected = WasJustDisconnected.exchange(false); 375 | bool isConnected = Connection->IsOpen(); 376 | 377 | if (isConnected) { 378 | // if we are connected, disconnect cb first 379 | std::lock_guard guard(HandlerMutex); 380 | if (wasDisconnected && Handlers.disconnected) { 381 | Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); 382 | } 383 | } 384 | 385 | if (WasJustConnected.exchange(false)) { 386 | std::lock_guard guard(HandlerMutex); 387 | if (Handlers.ready) { 388 | Handlers.ready(); 389 | } 390 | } 391 | 392 | if (GotErrorMessage.exchange(false)) { 393 | std::lock_guard guard(HandlerMutex); 394 | if (Handlers.errored) { 395 | Handlers.errored(LastErrorCode, LastErrorMessage); 396 | } 397 | } 398 | 399 | if (WasJoinGame.exchange(false)) { 400 | std::lock_guard guard(HandlerMutex); 401 | if (Handlers.joinGame) { 402 | Handlers.joinGame(JoinGameSecret); 403 | } 404 | } 405 | 406 | if (WasSpectateGame.exchange(false)) { 407 | std::lock_guard guard(HandlerMutex); 408 | if (Handlers.spectateGame) { 409 | Handlers.spectateGame(SpectateGameSecret); 410 | } 411 | } 412 | 413 | // Right now this batches up any requests and sends them all in a burst; I could imagine a world 414 | // where the implementer would rather sequentially accept/reject each one before the next invite 415 | // is sent. I left it this way because I could also imagine wanting to process these all and 416 | // maybe show them in one common dialog and/or start fetching the avatars in parallel, and if 417 | // not it should be trivial for the implementer to make a queue themselves. 418 | while (JoinAskQueue.HavePendingSends()) { 419 | auto req = JoinAskQueue.GetNextSendMessage(); 420 | { 421 | std::lock_guard guard(HandlerMutex); 422 | if (Handlers.joinRequest) { 423 | DiscordJoinRequest djr{req->userId, req->username, req->discriminator, req->avatar}; 424 | Handlers.joinRequest(&djr); 425 | } 426 | } 427 | JoinAskQueue.CommitSend(); 428 | } 429 | 430 | if (!isConnected) { 431 | // if we are not connected, disconnect message last 432 | std::lock_guard guard(HandlerMutex); 433 | if (wasDisconnected && Handlers.disconnected) { 434 | Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); 435 | } 436 | } 437 | } 438 | 439 | extern "C" DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* newHandlers) 440 | { 441 | if (newHandlers) { 442 | 443 | #define HANDLE_EVENT_REGISTRATION(handler_name, event) \ 444 | if (!Handlers.handler_name && newHandlers->handler_name) { \ 445 | RegisterForEvent(event); \ 446 | } \ 447 | else if (Handlers.handler_name && !newHandlers->handler_name) { \ 448 | DeregisterForEvent(event); \ 449 | } 450 | 451 | std::lock_guard guard(HandlerMutex); 452 | HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN") 453 | HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE") 454 | HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST") 455 | 456 | #undef HANDLE_EVENT_REGISTRATION 457 | 458 | Handlers = *newHandlers; 459 | } 460 | else 461 | { 462 | std::lock_guard guard(HandlerMutex); 463 | Handlers = {}; 464 | } 465 | return; 466 | } 467 | -------------------------------------------------------------------------------- /src/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // outsmart GCC's missing-declarations warning 4 | BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID); 5 | BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID) 6 | { 7 | return TRUE; 8 | } 9 | -------------------------------------------------------------------------------- /src/msg_queue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // A simple queue. No locks, but only works with a single thread as producer and a single thread as 6 | // a consumer. Mutex up as needed. 7 | 8 | template 9 | class MsgQueue { 10 | ElementType queue_[QueueSize]; 11 | std::atomic_uint nextAdd_{0}; 12 | std::atomic_uint nextSend_{0}; 13 | std::atomic_uint pendingSends_{0}; 14 | 15 | public: 16 | MsgQueue() {} 17 | 18 | ElementType* GetNextAddMessage() 19 | { 20 | // if we are falling behind, bail 21 | if (pendingSends_.load() >= QueueSize) { 22 | return nullptr; 23 | } 24 | auto index = (nextAdd_++) % QueueSize; 25 | return &queue_[index]; 26 | } 27 | void CommitAdd() { ++pendingSends_; } 28 | 29 | bool HavePendingSends() const { return pendingSends_.load() != 0; } 30 | ElementType* GetNextSendMessage() 31 | { 32 | auto index = (nextSend_++) % QueueSize; 33 | return &queue_[index]; 34 | } 35 | void CommitSend() { --pendingSends_; } 36 | }; 37 | -------------------------------------------------------------------------------- /src/rpc_connection.cpp: -------------------------------------------------------------------------------- 1 | #include "rpc_connection.h" 2 | #include "serialization.h" 3 | 4 | #include 5 | 6 | static const int RpcVersion = 1; 7 | static RpcConnection Instance; 8 | 9 | /*static*/ RpcConnection* RpcConnection::Create(const char* applicationId) 10 | { 11 | Instance.connection = BaseConnection::Create(); 12 | StringCopy(Instance.appId, applicationId); 13 | return &Instance; 14 | } 15 | 16 | /*static*/ void RpcConnection::Destroy(RpcConnection*& c) 17 | { 18 | c->Close(); 19 | BaseConnection::Destroy(c->connection); 20 | c = nullptr; 21 | } 22 | 23 | void RpcConnection::Open() 24 | { 25 | if (state == State::Connected) { 26 | return; 27 | } 28 | 29 | if (state == State::Disconnected) { 30 | if (connection->Open()) { 31 | } 32 | else { 33 | return; 34 | } 35 | } 36 | 37 | if (state == State::SentHandshake) { 38 | JsonDocument message; 39 | if (Read(message)) { 40 | auto cmd = GetStrMember(&message, "cmd"); 41 | auto evt = GetStrMember(&message, "evt"); 42 | if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) { 43 | state = State::Connected; 44 | if (onConnect) { 45 | onConnect(); 46 | } 47 | } 48 | } 49 | } 50 | else { 51 | sendFrame.opcode = Opcode::Handshake; 52 | sendFrame.length = (uint32_t)JsonWriteHandshakeObj( 53 | sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId); 54 | 55 | if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) { 56 | state = State::SentHandshake; 57 | } 58 | else { 59 | Close(); 60 | } 61 | } 62 | } 63 | 64 | void RpcConnection::Close() 65 | { 66 | if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) { 67 | onDisconnect(lastErrorCode, lastErrorMessage); 68 | } 69 | connection->Close(); 70 | state = State::Disconnected; 71 | } 72 | 73 | bool RpcConnection::Write(const void* data, size_t length) 74 | { 75 | sendFrame.opcode = Opcode::Frame; 76 | memcpy(sendFrame.message, data, length); 77 | sendFrame.length = (uint32_t)length; 78 | if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) { 79 | Close(); 80 | return false; 81 | } 82 | return true; 83 | } 84 | 85 | bool RpcConnection::Read(JsonDocument& message) 86 | { 87 | if (state != State::Connected && state != State::SentHandshake) { 88 | return false; 89 | } 90 | MessageFrame readFrame; 91 | for (;;) { 92 | bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader)); 93 | if (!didRead) { 94 | if (!connection->isOpen) { 95 | lastErrorCode = (int)ErrorCode::PipeClosed; 96 | StringCopy(lastErrorMessage, "Pipe closed"); 97 | Close(); 98 | } 99 | return false; 100 | } 101 | 102 | if (readFrame.length > 0) { 103 | didRead = connection->Read(readFrame.message, readFrame.length); 104 | if (!didRead) { 105 | lastErrorCode = (int)ErrorCode::ReadCorrupt; 106 | StringCopy(lastErrorMessage, "Partial data in frame"); 107 | Close(); 108 | return false; 109 | } 110 | readFrame.message[readFrame.length] = 0; 111 | } 112 | 113 | switch (readFrame.opcode) { 114 | case Opcode::Close: { 115 | message.ParseInsitu(readFrame.message); 116 | lastErrorCode = GetIntMember(&message, "code"); 117 | StringCopy(lastErrorMessage, GetStrMember(&message, "message", "")); 118 | Close(); 119 | return false; 120 | } 121 | case Opcode::Frame: 122 | message.ParseInsitu(readFrame.message); 123 | return true; 124 | case Opcode::Ping: 125 | readFrame.opcode = Opcode::Pong; 126 | if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) { 127 | Close(); 128 | } 129 | break; 130 | case Opcode::Pong: 131 | break; 132 | case Opcode::Handshake: 133 | default: 134 | // something bad happened 135 | lastErrorCode = (int)ErrorCode::ReadCorrupt; 136 | StringCopy(lastErrorMessage, "Bad ipc frame"); 137 | Close(); 138 | return false; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/rpc_connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "connection.h" 4 | #include "serialization.h" 5 | 6 | // I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much 7 | // smaller. 8 | constexpr size_t MaxRpcFrameSize = 64 * 1024; 9 | 10 | struct RpcConnection { 11 | enum class ErrorCode : int { 12 | Success = 0, 13 | PipeClosed = 1, 14 | ReadCorrupt = 2, 15 | }; 16 | 17 | enum class Opcode : uint32_t { 18 | Handshake = 0, 19 | Frame = 1, 20 | Close = 2, 21 | Ping = 3, 22 | Pong = 4, 23 | }; 24 | 25 | struct MessageFrameHeader { 26 | Opcode opcode; 27 | uint32_t length; 28 | }; 29 | 30 | struct MessageFrame : public MessageFrameHeader { 31 | char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)]; 32 | }; 33 | 34 | enum class State : uint32_t { 35 | Disconnected, 36 | SentHandshake, 37 | AwaitingResponse, 38 | Connected, 39 | }; 40 | 41 | BaseConnection* connection{nullptr}; 42 | State state{State::Disconnected}; 43 | void (*onConnect)(){nullptr}; 44 | void (*onDisconnect)(int errorCode, const char* message){nullptr}; 45 | char appId[64]{}; 46 | int lastErrorCode{0}; 47 | char lastErrorMessage[256]{}; 48 | RpcConnection::MessageFrame sendFrame; 49 | 50 | static RpcConnection* Create(const char* applicationId); 51 | static void Destroy(RpcConnection*&); 52 | 53 | inline bool IsOpen() const { return state == State::Connected; } 54 | 55 | void Open(); 56 | void Close(); 57 | bool Write(const void* data, size_t length); 58 | bool Read(JsonDocument& message); 59 | }; 60 | -------------------------------------------------------------------------------- /src/serialization.cpp: -------------------------------------------------------------------------------- 1 | #include "serialization.h" 2 | #include "connection.h" 3 | #include "discord_rpc.h" 4 | 5 | template 6 | void NumberToString(char* dest, T number) 7 | { 8 | if (!number) { 9 | *dest++ = '0'; 10 | *dest++ = 0; 11 | return; 12 | } 13 | if (number < 0) { 14 | *dest++ = '-'; 15 | number = -number; 16 | } 17 | char temp[32]; 18 | int place = 0; 19 | while (number) { 20 | auto digit = number % 10; 21 | number = number / 10; 22 | temp[place++] = '0' + (char)digit; 23 | } 24 | for (--place; place >= 0; --place) { 25 | *dest++ = temp[place]; 26 | } 27 | *dest = 0; 28 | } 29 | 30 | // it's ever so slightly faster to not have to strlen the key 31 | template 32 | void WriteKey(JsonWriter& w, T& k) 33 | { 34 | w.Key(k, sizeof(T) - 1); 35 | } 36 | 37 | struct WriteObject { 38 | JsonWriter& writer; 39 | WriteObject(JsonWriter& w) 40 | : writer(w) 41 | { 42 | writer.StartObject(); 43 | } 44 | template 45 | WriteObject(JsonWriter& w, T& name) 46 | : writer(w) 47 | { 48 | WriteKey(writer, name); 49 | writer.StartObject(); 50 | } 51 | ~WriteObject() { writer.EndObject(); } 52 | }; 53 | 54 | struct WriteArray { 55 | JsonWriter& writer; 56 | template 57 | WriteArray(JsonWriter& w, T& name) 58 | : writer(w) 59 | { 60 | WriteKey(writer, name); 61 | writer.StartArray(); 62 | } 63 | ~WriteArray() { writer.EndArray(); } 64 | }; 65 | 66 | template 67 | void WriteOptionalString(JsonWriter& w, T& k, const char* value) 68 | { 69 | if (value && value[0]) { 70 | w.Key(k, sizeof(T) - 1); 71 | w.String(value); 72 | } 73 | } 74 | 75 | static void JsonWriteNonce(JsonWriter& writer, int nonce) 76 | { 77 | WriteKey(writer, "nonce"); 78 | char nonceBuffer[32]; 79 | NumberToString(nonceBuffer, nonce); 80 | writer.String(nonceBuffer); 81 | } 82 | 83 | size_t JsonWriteRichPresenceObj(char* dest, 84 | size_t maxLen, 85 | int nonce, 86 | int pid, 87 | const DiscordRichPresence* presence) 88 | { 89 | JsonWriter writer(dest, maxLen); 90 | 91 | { 92 | WriteObject top(writer); 93 | 94 | JsonWriteNonce(writer, nonce); 95 | 96 | WriteKey(writer, "cmd"); 97 | writer.String("SET_ACTIVITY"); 98 | 99 | { 100 | WriteObject args(writer, "args"); 101 | 102 | WriteKey(writer, "pid"); 103 | writer.Int(pid); 104 | 105 | if (presence != nullptr) 106 | { 107 | WriteObject activity(writer, "activity"); 108 | 109 | WriteOptionalString(writer, "state", presence->state); 110 | WriteOptionalString(writer, "details", presence->details); 111 | 112 | if (presence->startTimestamp || presence->endTimestamp) { 113 | WriteObject timestamps(writer, "timestamps"); 114 | 115 | if (presence->startTimestamp) { 116 | WriteKey(writer, "start"); 117 | writer.Int64(presence->startTimestamp); 118 | } 119 | 120 | if (presence->endTimestamp) { 121 | WriteKey(writer, "end"); 122 | writer.Int64(presence->endTimestamp); 123 | } 124 | } 125 | 126 | if ((presence->largeImageKey && presence->largeImageKey[0]) || 127 | (presence->largeImageText && presence->largeImageText[0]) || 128 | (presence->smallImageKey && presence->smallImageKey[0]) || 129 | (presence->smallImageText && presence->smallImageText[0])) { 130 | WriteObject assets(writer, "assets"); 131 | WriteOptionalString(writer, "large_image", presence->largeImageKey); 132 | WriteOptionalString(writer, "large_text", presence->largeImageText); 133 | WriteOptionalString(writer, "small_image", presence->smallImageKey); 134 | WriteOptionalString(writer, "small_text", presence->smallImageText); 135 | } 136 | 137 | if ((presence->partyId && presence->partyId[0]) || presence->partySize || 138 | presence->partyMax) { 139 | WriteObject party(writer, "party"); 140 | WriteOptionalString(writer, "id", presence->partyId); 141 | if (presence->partySize && presence->partyMax) { 142 | WriteArray size(writer, "size"); 143 | writer.Int(presence->partySize); 144 | writer.Int(presence->partyMax); 145 | } 146 | } 147 | 148 | if ((presence->matchSecret && presence->matchSecret[0]) || 149 | (presence->joinSecret && presence->joinSecret[0]) || 150 | (presence->spectateSecret && presence->spectateSecret[0])) { 151 | WriteObject secrets(writer, "secrets"); 152 | WriteOptionalString(writer, "match", presence->matchSecret); 153 | WriteOptionalString(writer, "join", presence->joinSecret); 154 | WriteOptionalString(writer, "spectate", presence->spectateSecret); 155 | } 156 | 157 | writer.Key("instance"); 158 | writer.Bool(presence->instance != 0); 159 | } 160 | } 161 | } 162 | 163 | return writer.Size(); 164 | } 165 | 166 | size_t JsonWriteHandshakeObj(char* dest, size_t maxLen, int version, const char* applicationId) 167 | { 168 | JsonWriter writer(dest, maxLen); 169 | 170 | { 171 | WriteObject obj(writer); 172 | WriteKey(writer, "v"); 173 | writer.Int(version); 174 | WriteKey(writer, "client_id"); 175 | writer.String(applicationId); 176 | } 177 | 178 | return writer.Size(); 179 | } 180 | 181 | size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName) 182 | { 183 | JsonWriter writer(dest, maxLen); 184 | 185 | { 186 | WriteObject obj(writer); 187 | 188 | JsonWriteNonce(writer, nonce); 189 | 190 | WriteKey(writer, "cmd"); 191 | writer.String("SUBSCRIBE"); 192 | 193 | WriteKey(writer, "evt"); 194 | writer.String(evtName); 195 | } 196 | 197 | return writer.Size(); 198 | } 199 | 200 | size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName) 201 | { 202 | JsonWriter writer(dest, maxLen); 203 | 204 | { 205 | WriteObject obj(writer); 206 | 207 | JsonWriteNonce(writer, nonce); 208 | 209 | WriteKey(writer, "cmd"); 210 | writer.String("UNSUBSCRIBE"); 211 | 212 | WriteKey(writer, "evt"); 213 | writer.String(evtName); 214 | } 215 | 216 | return writer.Size(); 217 | } 218 | 219 | size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce) 220 | { 221 | JsonWriter writer(dest, maxLen); 222 | 223 | { 224 | WriteObject obj(writer); 225 | 226 | WriteKey(writer, "cmd"); 227 | if (reply == DISCORD_REPLY_YES) { 228 | writer.String("SEND_ACTIVITY_JOIN_INVITE"); 229 | } 230 | else { 231 | writer.String("CLOSE_ACTIVITY_JOIN_REQUEST"); 232 | } 233 | 234 | WriteKey(writer, "args"); 235 | { 236 | WriteObject args(writer); 237 | 238 | WriteKey(writer, "user_id"); 239 | writer.String(userId); 240 | } 241 | 242 | JsonWriteNonce(writer, nonce); 243 | } 244 | 245 | return writer.Size(); 246 | } 247 | -------------------------------------------------------------------------------- /src/serialization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifndef __MINGW32__ 6 | #pragma warning(push) 7 | 8 | #pragma warning(disable : 4061) // enum is not explicitly handled by a case label 9 | #pragma warning(disable : 4365) // signed/unsigned mismatch 10 | #pragma warning(disable : 4464) // relative include path contains 11 | #pragma warning(disable : 4668) // is not defined as a preprocessor macro 12 | #pragma warning(disable : 6313) // Incorrect operator 13 | #endif // __MINGW32__ 14 | 15 | #include "rapidjson/document.h" 16 | #include "rapidjson/stringbuffer.h" 17 | #include "rapidjson/writer.h" 18 | 19 | #ifndef __MINGW32__ 20 | #pragma warning(pop) 21 | #endif // __MINGW32__ 22 | 23 | // if only there was a standard library function for this 24 | template 25 | inline size_t StringCopy(char (&dest)[Len], const char* src) 26 | { 27 | if (!src || !Len) { 28 | return 0; 29 | } 30 | size_t copied; 31 | char* out = dest; 32 | for (copied = 1; *src && copied < Len; ++copied) { 33 | *out++ = *src++; 34 | } 35 | *out = 0; 36 | return copied - 1; 37 | } 38 | 39 | size_t JsonWriteHandshakeObj(char* dest, size_t maxLen, int version, const char* applicationId); 40 | 41 | // Commands 42 | struct DiscordRichPresence; 43 | size_t JsonWriteRichPresenceObj(char* dest, 44 | size_t maxLen, 45 | int nonce, 46 | int pid, 47 | const DiscordRichPresence* presence); 48 | size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName); 49 | 50 | size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName); 51 | 52 | size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce); 53 | 54 | // I want to use as few allocations as I can get away with, and to do that with RapidJson, you need 55 | // to supply some of your own allocators for stuff rather than use the defaults 56 | 57 | class LinearAllocator { 58 | public: 59 | char* buffer_; 60 | char* end_; 61 | LinearAllocator() 62 | { 63 | assert(0); // needed for some default case in rapidjson, should not use 64 | } 65 | LinearAllocator(char* buffer, size_t size) 66 | : buffer_(buffer) 67 | , end_(buffer + size) 68 | { 69 | } 70 | static const bool kNeedFree = false; 71 | void* Malloc(size_t size) 72 | { 73 | char* res = buffer_; 74 | buffer_ += size; 75 | if (buffer_ > end_) { 76 | buffer_ = res; 77 | return nullptr; 78 | } 79 | return res; 80 | } 81 | void* Realloc(void* originalPtr, size_t originalSize, size_t newSize) 82 | { 83 | if (newSize == 0) { 84 | return nullptr; 85 | } 86 | // allocate how much you need in the first place 87 | assert(!originalPtr && !originalSize); 88 | // unused parameter warning 89 | (void)(originalPtr); 90 | (void)(originalSize); 91 | return Malloc(newSize); 92 | } 93 | static void Free(void* ptr) 94 | { 95 | /* shrug */ 96 | (void)ptr; 97 | } 98 | }; 99 | 100 | template 101 | class FixedLinearAllocator : public LinearAllocator { 102 | public: 103 | char fixedBuffer_[Size]; 104 | FixedLinearAllocator() 105 | : LinearAllocator(fixedBuffer_, Size) 106 | { 107 | } 108 | static const bool kNeedFree = false; 109 | }; 110 | 111 | // wonder why this isn't a thing already, maybe I missed it 112 | class DirectStringBuffer { 113 | public: 114 | using Ch = char; 115 | char* buffer_; 116 | char* end_; 117 | char* current_; 118 | 119 | DirectStringBuffer(char* buffer, size_t maxLen) 120 | : buffer_(buffer) 121 | , end_(buffer + maxLen) 122 | , current_(buffer) 123 | { 124 | } 125 | 126 | void Put(char c) 127 | { 128 | if (current_ < end_) { 129 | *current_++ = c; 130 | } 131 | } 132 | void Flush() {} 133 | size_t GetSize() const { return (size_t)(current_ - buffer_); } 134 | }; 135 | 136 | using MallocAllocator = rapidjson::CrtAllocator; 137 | using PoolAllocator = rapidjson::MemoryPoolAllocator; 138 | using UTF8 = rapidjson::UTF8; 139 | // Writer appears to need about 16 bytes per nested object level (with 64bit size_t) 140 | using StackAllocator = FixedLinearAllocator<2048>; 141 | constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t)); 142 | using JsonWriterBase = 143 | rapidjson::Writer; 144 | class JsonWriter : public JsonWriterBase { 145 | public: 146 | DirectStringBuffer stringBuffer_; 147 | StackAllocator stackAlloc_; 148 | 149 | JsonWriter(char* dest, size_t maxLen) 150 | : JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels) 151 | , stringBuffer_(dest, maxLen) 152 | , stackAlloc_() 153 | { 154 | } 155 | 156 | size_t Size() const { return stringBuffer_.GetSize(); } 157 | }; 158 | 159 | using JsonDocumentBase = rapidjson::GenericDocument; 160 | class JsonDocument : public JsonDocumentBase { 161 | public: 162 | static const int kDefaultChunkCapacity = 32 * 1024; 163 | // json parser will use this buffer first, then allocate more if needed; I seriously doubt we 164 | // send any messages that would use all of this, though. 165 | char parseBuffer_[32 * 1024]; 166 | MallocAllocator mallocAllocator_; 167 | PoolAllocator poolAllocator_; 168 | StackAllocator stackAllocator_; 169 | JsonDocument() 170 | : JsonDocumentBase(rapidjson::kObjectType, 171 | &poolAllocator_, 172 | sizeof(stackAllocator_.fixedBuffer_), 173 | &stackAllocator_) 174 | , poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_) 175 | , stackAllocator_() 176 | { 177 | } 178 | }; 179 | 180 | using JsonValue = rapidjson::GenericValue; 181 | 182 | inline JsonValue* GetObjMember(JsonValue* obj, const char* name) 183 | { 184 | if (obj) { 185 | auto member = obj->FindMember(name); 186 | if (member != obj->MemberEnd() && member->value.IsObject()) { 187 | return &member->value; 188 | } 189 | } 190 | return nullptr; 191 | } 192 | 193 | inline int GetIntMember(JsonValue* obj, const char* name, int notFoundDefault = 0) 194 | { 195 | if (obj) { 196 | auto member = obj->FindMember(name); 197 | if (member != obj->MemberEnd() && member->value.IsInt()) { 198 | return member->value.GetInt(); 199 | } 200 | } 201 | return notFoundDefault; 202 | } 203 | 204 | inline const char* GetStrMember(JsonValue* obj, 205 | const char* name, 206 | const char* notFoundDefault = nullptr) 207 | { 208 | if (obj) { 209 | auto member = obj->FindMember(name); 210 | if (member != obj->MemberEnd() && member->value.IsString()) { 211 | return member->value.GetString(); 212 | } 213 | } 214 | return notFoundDefault; 215 | } 216 | --------------------------------------------------------------------------------