mirror of
https://github.com/DrBeef/Raze.git
synced 2024-11-25 05:21:28 +00:00
- implement discord rich presence, expand window title code. (still wip)
This commit is contained in:
parent
3fda2b032c
commit
da5d9fcde2
35 changed files with 3199 additions and 3 deletions
|
@ -327,6 +327,11 @@ if (HAVE_VULKAN)
|
|||
add_subdirectory( libraries/glslang/OGLCompilersDLL )
|
||||
endif()
|
||||
|
||||
add_subdirectory( libraries/discordrpc )
|
||||
set( DRPC_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/discordrpc/include" )
|
||||
set( DRPC_LIBRARIES discord-rpc )
|
||||
set( DRPC_LIBRARY discord-rpc )
|
||||
|
||||
if( ZLIB_FOUND AND NOT FORCE_INTERNAL_ZLIB )
|
||||
message( STATUS "Using system zlib, includes found at ${ZLIB_INCLUDE_DIR}" )
|
||||
else()
|
||||
|
|
92
libraries/discordrpc/.clang-format
Normal file
92
libraries/discordrpc/.clang-format
Normal file
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: true
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: false
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Stroustrup
|
||||
BreakBeforeInheritanceComma: true
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializers: BeforeComma
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: ''
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 2
|
||||
ContinuationIndentWidth: 2
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros: []
|
||||
IndentCaseLabels: false
|
||||
IncludeCategories:
|
||||
- Regex: '^("|<)stdafx\.h(pp)?("|>)'
|
||||
Priority: -1
|
||||
- Regex: '^<(W|w)indows.h>'
|
||||
Priority: 1
|
||||
- Regex: '^<'
|
||||
Priority: 2
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
IncludeIsMainRegex: '(_test|_win|_linux|_mac|_ios|_osx|_null)?$'
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
PenaltyBreakAssignment: 0
|
||||
PenaltyBreakBeforeFirstCallParameter: 1
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 9999999
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp11
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
---
|
||||
Language: Cpp
|
||||
---
|
||||
Language: ObjC
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: false
|
||||
---
|
||||
Language: Java
|
||||
BasedOnStyle: Google
|
||||
BreakAfterJavaFieldAnnotations: true
|
||||
...
|
5
libraries/discordrpc/.gitignore
vendored
Normal file
5
libraries/discordrpc/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/build*/
|
||||
/.vscode/
|
||||
/thirdparty/
|
||||
.vs/
|
||||
.DS_Store
|
47
libraries/discordrpc/.travis.yml
Normal file
47
libraries/discordrpc/.travis.yml
Normal file
|
@ -0,0 +1,47 @@
|
|||
language: cpp
|
||||
|
||||
env:
|
||||
global:
|
||||
- CLANG_FORMAT_SUFFIX="-dummy" # don't use formatting on Travis, this is
|
||||
# needed not to use default 3.5 version
|
||||
# which is too old.
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: MATRIX_EVAL="CC=gcc-5 && CXX=g++-5"
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-5
|
||||
- os: linux
|
||||
env: MATRIX_EVAL="CC=clang-4.0 && CXX=clang++-4.0"
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- llvm-toolchain-trusty-4.0
|
||||
packages:
|
||||
- clang-4.0
|
||||
- os: linux
|
||||
env: MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0"
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- llvm-toolchain-trusty-5.0
|
||||
packages:
|
||||
- clang-5.0
|
||||
- os: osx
|
||||
osx_image: xcode9
|
||||
|
||||
# prevent Travis from overwriting our CXX variables
|
||||
before_install:
|
||||
- eval "${MATRIX_EVAL}"
|
||||
- echo $CXX
|
||||
|
||||
script:
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake -DCLANG_FORMAT_SUFFIX=$CLANG_FORMAT_SUFFIX -DWARNINGS_AS_ERRORS=On --config Release ..
|
||||
- cmake --build . -- -j2
|
30
libraries/discordrpc/CMakeLists.txt
Normal file
30
libraries/discordrpc/CMakeLists.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
cmake_minimum_required (VERSION 3.2.0)
|
||||
project (DiscordRPC)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# format
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||
include/*.h
|
||||
src/*.cpp src/*.h src/*.c
|
||||
)
|
||||
|
||||
# Set CLANG_FORMAT_SUFFIX if you are using custom clang-format, e.g. clang-format-5.0
|
||||
find_program(CLANG_FORMAT_CMD clang-format${CLANG_FORMAT_SUFFIX})
|
||||
|
||||
if (CLANG_FORMAT_CMD)
|
||||
add_custom_target(
|
||||
clangformat
|
||||
COMMAND ${CLANG_FORMAT_CMD}
|
||||
-i -style=file -fallback-style=none
|
||||
${ALL_SOURCE_FILES}
|
||||
DEPENDS
|
||||
${ALL_SOURCE_FILES}
|
||||
)
|
||||
endif(CLANG_FORMAT_CMD)
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../source/common/thirdparty)
|
||||
|
||||
# add subdirs
|
||||
|
||||
add_subdirectory(src)
|
19
libraries/discordrpc/LICENSE
Normal file
19
libraries/discordrpc/LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright 2017 Discord, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
158
libraries/discordrpc/README.md
Normal file
158
libraries/discordrpc/README.md
Normal file
|
@ -0,0 +1,158 @@
|
|||
# Discord RPC
|
||||
|
||||
## Deprecation Notice
|
||||
|
||||
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
|
||||
|
||||
---
|
||||
|
||||
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
|
||||
|
||||
Included here are some quick demos that implement the very minimal subset to show current status, and
|
||||
have callbacks for where a more complete game would do more things (joining, spectating, etc).
|
||||
|
||||
## Documentation
|
||||
|
||||
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Zeroith, you should be set up to build things because you are a game developer, right?
|
||||
|
||||
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
|
||||
|
||||
### Unreal Engine 4 Setup
|
||||
|
||||
To use the Rich Presense plugin with Unreal Engine Projects:
|
||||
|
||||
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
|
||||
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
|
||||
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
|
||||
4. Follow the steps below for each OS
|
||||
5. Build your UE4 project
|
||||
6. Launch the editor, and enable the Discord plugin.
|
||||
|
||||
#### Windows
|
||||
|
||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
|
||||
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
|
||||
|
||||
#### Mac
|
||||
|
||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
|
||||
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
|
||||
|
||||
#### Linux
|
||||
|
||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
|
||||
- Inside, create another folder `x86_64-unknown-linux-gnu`
|
||||
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
|
||||
|
||||
### Unity Setup
|
||||
|
||||
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
|
||||
|
||||
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
|
||||
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
|
||||
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
|
||||
|
||||
We've got our `Plugins` folder ready, so let's get platform-specific!
|
||||
|
||||
#### Windows
|
||||
|
||||
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
|
||||
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
|
||||
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
|
||||
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
|
||||
8. Done!
|
||||
|
||||
#### MacOS
|
||||
|
||||
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
|
||||
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
|
||||
6. Done!
|
||||
|
||||
#### Linux
|
||||
|
||||
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
|
||||
5. Done!
|
||||
|
||||
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
|
||||
|
||||
### From package
|
||||
|
||||
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
|
||||
|
||||
### From repo
|
||||
|
||||
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
|
||||
|
||||
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
|
||||
|
||||
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
|
||||
|
||||
```sh
|
||||
cd <path to discord-rpc>
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
|
||||
cmake --build . --config Release --target install
|
||||
```
|
||||
|
||||
There is a wrapper build script `build.py` that runs `cmake` with a few different options.
|
||||
|
||||
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
|
||||
|
||||
There are some CMake options you might care about:
|
||||
|
||||
| flag | default | does |
|
||||
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. |
|
||||
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) |
|
||||
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL |
|
||||
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
|
||||
|
||||
## Continuous Builds
|
||||
|
||||
Why do we have three of these? Three times the fun!
|
||||
|
||||
| CI | badge |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc) |
|
||||
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
|
||||
| Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc) |
|
||||
|
||||
## Sample: send-presence
|
||||
|
||||
This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command.
|
||||
|
||||
## Sample: button-clicker
|
||||
|
||||
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders.
|
||||
|
||||
## Sample: unrealstatus
|
||||
|
||||
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders.
|
||||
|
||||
## Wrappers and Implementations
|
||||
|
||||
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:
|
||||
|
||||
- The code
|
||||
- A brief ReadMe of how to use it
|
||||
- A working example
|
||||
|
||||
###### Rich Presence Wrappers and Implementations
|
||||
|
||||
| Name | Language |
|
||||
| ------------------------------------------------------------------------- | --------------------------------- |
|
||||
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# |
|
||||
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
|
||||
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
|
||||
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
|
||||
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
|
||||
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
|
||||
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
|
||||
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
|
||||
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
|
||||
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |
|
17
libraries/discordrpc/appveyor.yml
Normal file
17
libraries/discordrpc/appveyor.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
version: '{build}'
|
||||
install:
|
||||
- python -m pip install click
|
||||
|
||||
build_script:
|
||||
- mkdir examples\unrealstatus\Plugins\discordrpc\Binaries\ThirdParty\discordrpcLibrary\Win64
|
||||
- python build.py
|
||||
|
||||
artifacts:
|
||||
- path: builds\install\win32-dynamic
|
||||
name: win32-dynamic
|
||||
- path: builds\install\win32-static
|
||||
name: win32-static
|
||||
- path: builds\install\win64-dynamic
|
||||
name: win64-dynamic
|
||||
- path: builds\install\win64-static
|
||||
name: win64-static
|
304
libraries/discordrpc/build.py
Normal file
304
libraries/discordrpc/build.py
Normal file
|
@ -0,0 +1,304 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
import zipfile
|
||||
from contextlib import contextmanager
|
||||
import click
|
||||
|
||||
|
||||
def get_platform():
|
||||
""" a name for the platform """
|
||||
if sys.platform.startswith('win'):
|
||||
return 'win'
|
||||
elif sys.platform == 'darwin':
|
||||
return 'osx'
|
||||
elif sys.platform.startswith('linux'):
|
||||
return 'linux'
|
||||
raise Exception('Unsupported platform ' + sys.platform)
|
||||
|
||||
|
||||
SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||
# we use Buildkite which sets this env variable by default
|
||||
IS_BUILD_MACHINE = os.environ.get('CI', '') == 'true'
|
||||
PLATFORM = get_platform()
|
||||
INSTALL_ROOT = os.path.join(SCRIPT_PATH, 'builds', 'install')
|
||||
|
||||
|
||||
def get_signtool():
|
||||
""" get path to code signing tool """
|
||||
if PLATFORM == 'win':
|
||||
sdk_dir = 'c:\\Program Files (x86)\\Windows Kits\\10' # os.environ['WindowsSdkDir']
|
||||
return os.path.join(sdk_dir, 'bin', 'x86', 'signtool.exe')
|
||||
elif PLATFORM == 'osx':
|
||||
return '/usr/bin/codesign'
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cd(new_dir):
|
||||
""" Temporarily change current directory """
|
||||
if new_dir:
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(new_dir)
|
||||
yield
|
||||
if new_dir:
|
||||
os.chdir(old_dir)
|
||||
|
||||
|
||||
def mkdir_p(path):
|
||||
""" mkdir -p """
|
||||
if not os.path.isdir(path):
|
||||
click.secho('Making ' + path, fg='yellow')
|
||||
os.makedirs(path)
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.pass_context
|
||||
@click.option('--clean', is_flag=True)
|
||||
def cli(ctx, clean):
|
||||
""" click wrapper for command line stuff """
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.invoke(libs, clean=clean)
|
||||
if IS_BUILD_MACHINE:
|
||||
ctx.invoke(sign)
|
||||
ctx.invoke(archive)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def unity(ctx):
|
||||
""" build just dynamic libs for use in unity project """
|
||||
ctx.invoke(libs, clean=False, static=False, shared=True, skip_formatter=True, just_release=True)
|
||||
BUILDS = []
|
||||
|
||||
click.echo('--- Copying libs and header into unity example')
|
||||
UNITY_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'button-clicker', 'Assets', 'Plugins')
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
LIBRARY_NAME = 'discord-rpc.dll'
|
||||
BUILD_64_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release')
|
||||
UNITY_64_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86_64')
|
||||
BUILDS.append({BUILD_64_BASE_PATH: UNITY_64_DLL_PATH})
|
||||
|
||||
BUILD_32_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win32-dynamic', 'src', 'Release')
|
||||
UNITY_32_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86')
|
||||
BUILDS.append({BUILD_32_BASE_PATH: UNITY_32_DLL_PATH})
|
||||
|
||||
elif sys.platform == 'darwin':
|
||||
LIBRARY_NAME = 'discord-rpc.bundle'
|
||||
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'osx-dynamic', 'src')
|
||||
UNITY_DLL_PATH = UNITY_PROJECT_PATH
|
||||
os.rename(
|
||||
os.path.join(BUILD_BASE_PATH, 'libdiscord-rpc.dylib'), os.path.join(BUILD_BASE_PATH, 'discord-rpc.bundle'))
|
||||
|
||||
BUILDS.append({BUILD_BASE_PATH: UNITY_DLL_PATH})
|
||||
|
||||
elif sys.platform.startswith('linux'):
|
||||
LIBRARY_NAME = 'discord-rpc.so'
|
||||
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'linux-dynamic', 'src')
|
||||
UNITY_DLL_PATH = os.path.join(UNITY_PROJECT_PATH, 'x86')
|
||||
os.rename(os.path.join(BUILD_BASE_PATH, 'libdiscord-rpc.so'), os.path.join(BUILD_BASE_PATH, 'discord-rpc.so'))
|
||||
|
||||
BUILDS.append({BUILD_BASE_PATH: UNITY_DLL_PATH})
|
||||
|
||||
else:
|
||||
raise Exception('Unsupported platform ' + sys.platform)
|
||||
|
||||
for build in BUILDS:
|
||||
for i in build:
|
||||
mkdir_p(build[i])
|
||||
shutil.copy(os.path.join(i, LIBRARY_NAME), build[i])
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def unreal(ctx):
|
||||
""" build libs and copy them into the unreal project """
|
||||
ctx.invoke(libs, clean=False, static=False, shared=True, skip_formatter=True, just_release=True)
|
||||
BUILDS = []
|
||||
|
||||
click.echo('--- Copying libs and header into unreal example')
|
||||
UNREAL_PROJECT_PATH = os.path.join(SCRIPT_PATH, 'examples', 'unrealstatus', 'Plugins', 'discordrpc')
|
||||
UNREAL_INCLUDE_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Include')
|
||||
mkdir_p(UNREAL_INCLUDE_PATH)
|
||||
shutil.copy(os.path.join(SCRIPT_PATH, 'include', 'discord_rpc.h'), UNREAL_INCLUDE_PATH)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
LIBRARY_NAME = 'discord-rpc.lib'
|
||||
BUILD_64_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win64-dynamic', 'src', 'Release')
|
||||
UNREAL_64_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win64')
|
||||
BUILDS.append({BUILD_64_BASE_PATH: UNREAL_64_DLL_PATH})
|
||||
|
||||
BUILD_32_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'win32-dynamic', 'src', 'Release')
|
||||
UNREAL_32_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Win32')
|
||||
BUILDS.append({BUILD_32_BASE_PATH: UNREAL_32_DLL_PATH})
|
||||
|
||||
elif sys.platform == 'darwin':
|
||||
LIBRARY_NAME = 'libdiscord-rpc.dylib'
|
||||
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'osx-dynamic', 'src')
|
||||
UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Mac')
|
||||
|
||||
BUILDS.append({BUILD_BASE_PATH: UNREAL_DLL_PATH})
|
||||
|
||||
elif sys.platform.startswith('linux'):
|
||||
LIBRARY_NAME = 'libdiscord-rpc.so'
|
||||
BUILD_BASE_PATH = os.path.join(SCRIPT_PATH, 'builds', 'linux-dynamic', 'src')
|
||||
UNREAL_DLL_PATH = os.path.join(UNREAL_PROJECT_PATH, 'Source', 'ThirdParty', 'DiscordRpcLibrary', 'Linux')
|
||||
|
||||
BUILDS.append({BUILD_BASE_PATH: UNREAL_DLL_PATH})
|
||||
|
||||
else:
|
||||
raise Exception('Unsupported platform ' + sys.platform)
|
||||
|
||||
for build in BUILDS:
|
||||
for i in build:
|
||||
mkdir_p(build[i])
|
||||
shutil.copy(os.path.join(i, LIBRARY_NAME), build[i])
|
||||
|
||||
|
||||
def build_lib(build_name, generator, options, just_release):
|
||||
""" Create a dir under builds, run build and install in it """
|
||||
build_path = os.path.join(SCRIPT_PATH, 'builds', build_name)
|
||||
install_path = os.path.join(INSTALL_ROOT, build_name)
|
||||
mkdir_p(build_path)
|
||||
mkdir_p(install_path)
|
||||
with cd(build_path):
|
||||
initial_cmake = ['cmake', SCRIPT_PATH, '-DCMAKE_INSTALL_PREFIX=%s' % os.path.join('..', 'install', build_name)]
|
||||
if generator:
|
||||
initial_cmake.extend(['-G', generator])
|
||||
for key in options:
|
||||
val = options[key]
|
||||
if type(val) is bool:
|
||||
val = 'ON' if val else 'OFF'
|
||||
initial_cmake.append('-D%s=%s' % (key, val))
|
||||
click.echo('--- Building ' + build_name)
|
||||
subprocess.check_call(initial_cmake)
|
||||
if not just_release:
|
||||
subprocess.check_call(['cmake', '--build', '.', '--config', 'Debug'])
|
||||
subprocess.check_call(['cmake', '--build', '.', '--config', 'Release', '--target', 'install'])
|
||||
|
||||
|
||||
@cli.command()
|
||||
def archive():
|
||||
""" create zip of install dir """
|
||||
click.echo('--- Archiving')
|
||||
archive_file_path = os.path.join(SCRIPT_PATH, 'builds', 'discord-rpc-%s.zip' % get_platform())
|
||||
archive_file = zipfile.ZipFile(archive_file_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
archive_src_base_path = INSTALL_ROOT
|
||||
archive_dst_base_path = 'discord-rpc'
|
||||
with cd(archive_src_base_path):
|
||||
for path, _, filenames in os.walk('.'):
|
||||
for fname in filenames:
|
||||
fpath = os.path.join(path, fname)
|
||||
dst_path = os.path.normpath(os.path.join(archive_dst_base_path, fpath))
|
||||
click.echo('Adding ' + dst_path)
|
||||
archive_file.write(fpath, dst_path)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def sign():
|
||||
""" Do code signing within install directory using our cert """
|
||||
tool = get_signtool()
|
||||
signable_extensions = set()
|
||||
if PLATFORM == 'win':
|
||||
signable_extensions.add('.dll')
|
||||
sign_command_base = [
|
||||
tool,
|
||||
'sign',
|
||||
'/n',
|
||||
'Discord Inc.',
|
||||
'/a',
|
||||
'/tr',
|
||||
'http://timestamp.digicert.com/rfc3161',
|
||||
'/as',
|
||||
'/td',
|
||||
'sha256',
|
||||
'/fd',
|
||||
'sha256',
|
||||
]
|
||||
elif PLATFORM == 'osx':
|
||||
signable_extensions.add('.dylib')
|
||||
sign_command_base = [
|
||||
tool,
|
||||
'--keychain',
|
||||
os.path.expanduser('~/Library/Keychains/login.keychain'),
|
||||
'-vvvv',
|
||||
'--deep',
|
||||
'--force',
|
||||
'--sign',
|
||||
'Developer ID Application: Hammer & Chisel Inc. (53Q6R32WPB)',
|
||||
]
|
||||
else:
|
||||
click.secho('Not signing things on this platform yet', fg='red')
|
||||
return
|
||||
|
||||
click.echo('--- Signing')
|
||||
for path, _, filenames in os.walk(INSTALL_ROOT):
|
||||
for fname in filenames:
|
||||
ext = os.path.splitext(fname)[1]
|
||||
if ext not in signable_extensions:
|
||||
continue
|
||||
fpath = os.path.join(path, fname)
|
||||
click.echo('Sign ' + fpath)
|
||||
sign_command = sign_command_base + [fpath]
|
||||
subprocess.check_call(sign_command)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--clean', is_flag=True)
|
||||
@click.option('--static', is_flag=True)
|
||||
@click.option('--shared', is_flag=True)
|
||||
@click.option('--skip_formatter', is_flag=True)
|
||||
@click.option('--just_release', is_flag=True)
|
||||
def libs(clean, static, shared, skip_formatter, just_release):
|
||||
""" Do all the builds for this platform """
|
||||
if clean:
|
||||
shutil.rmtree('builds', ignore_errors=True)
|
||||
|
||||
mkdir_p('builds')
|
||||
|
||||
if not (static or shared):
|
||||
static = True
|
||||
shared = True
|
||||
|
||||
static_options = {}
|
||||
dynamic_options = {
|
||||
'BUILD_SHARED_LIBS': True,
|
||||
'USE_STATIC_CRT': True,
|
||||
}
|
||||
|
||||
if skip_formatter or IS_BUILD_MACHINE:
|
||||
static_options['CLANG_FORMAT_SUFFIX'] = 'none'
|
||||
dynamic_options['CLANG_FORMAT_SUFFIX'] = 'none'
|
||||
|
||||
if IS_BUILD_MACHINE:
|
||||
just_release = True
|
||||
static_options['WARNINGS_AS_ERRORS'] = True
|
||||
dynamic_options['WARNINGS_AS_ERRORS'] = True
|
||||
|
||||
if PLATFORM == 'win':
|
||||
generator32 = 'Visual Studio 14 2015'
|
||||
generator64 = 'Visual Studio 14 2015 Win64'
|
||||
if static:
|
||||
build_lib('win32-static', generator32, static_options, just_release)
|
||||
build_lib('win64-static', generator64, static_options, just_release)
|
||||
if shared:
|
||||
build_lib('win32-dynamic', generator32, dynamic_options, just_release)
|
||||
build_lib('win64-dynamic', generator64, dynamic_options, just_release)
|
||||
elif PLATFORM == 'osx':
|
||||
if static:
|
||||
build_lib('osx-static', None, static_options, just_release)
|
||||
if shared:
|
||||
build_lib('osx-dynamic', None, dynamic_options, just_release)
|
||||
elif PLATFORM == 'linux':
|
||||
if static:
|
||||
build_lib('linux-static', None, static_options, just_release)
|
||||
if shared:
|
||||
build_lib('linux-dynamic', None, dynamic_options, just_release)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.chdir(SCRIPT_PATH)
|
||||
sys.exit(cli())
|
164
libraries/discordrpc/documentation/hard-mode.md
Normal file
164
libraries/discordrpc/documentation/hard-mode.md
Normal file
|
@ -0,0 +1,164 @@
|
|||
# Hard Mode: Roll Your Own Client
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Hark! A warning!
|
||||
|
||||
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.
|
||||
|
||||
## Application Protocol Registration
|
||||
|
||||
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.
|
||||
|
||||
## New RPC Command
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Below is a full example of a `SET_ACTIVITY` command. Field restrictions like size are the same as outlined in the main documentation.
|
||||
|
||||
```
|
||||
{
|
||||
"cmd": "SET_ACTIVITY",
|
||||
"args": {
|
||||
"pid": 9999, // Your application's process id - required field
|
||||
"activity": {
|
||||
"state": "In a Group",
|
||||
"details": "Competitive | In a Match",
|
||||
"timestamps": {
|
||||
"start": time(nullptr),
|
||||
"end": time(nullptr) + ((60 * 5) + 23)
|
||||
},
|
||||
"assets": {
|
||||
"large_image": "numbani_map",
|
||||
"large_text": "Numbani",
|
||||
"small_image": "pharah_profile",
|
||||
"small_text": "Pharah"
|
||||
},
|
||||
"party": {
|
||||
"id": GameEngine.GetPartyId(),
|
||||
"size": [3, 6]
|
||||
},
|
||||
"secrets": {
|
||||
"join": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f",
|
||||
"spectate": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0",
|
||||
"match": "4b2fdce12f639de8bfa7e3591b71a0d679d7c93f"
|
||||
},
|
||||
"instance": true
|
||||
}
|
||||
},
|
||||
"nonce": "647d814a-4cf8-4fbb-948f-898abd24f55b"
|
||||
}
|
||||
```
|
||||
|
||||
## New RPC Events
|
||||
|
||||
The three new RPC events for Rich Presence power the ability to join and spectate your friends' games.
|
||||
|
||||
First is the `ACTIVITY_JOIN` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd": "DISPATCH",
|
||||
"data": {
|
||||
"secret": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f"
|
||||
},
|
||||
"evt": "ACTIVITY_JOIN"
|
||||
}
|
||||
```
|
||||
|
||||
Second is the `ACTIVITY_SPECTATE` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd": "DISPATCH",
|
||||
"data": {
|
||||
"secret": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0"
|
||||
},
|
||||
"evt": "ACTIVITY_SPECTATE"
|
||||
}
|
||||
```
|
||||
|
||||
And third is the `ACTIVITY_JOIN_REQUEST` event:
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd": "DISPATCH",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "53908232506183680",
|
||||
"username": "Mason",
|
||||
"discriminator": "1337",
|
||||
"avatar": "a_bab14f271d565501444b2ca3be944b25"
|
||||
}
|
||||
},
|
||||
"evt": "ACTIVITY_JOIN_REQUEST"
|
||||
}
|
||||
```
|
||||
|
||||
In order to receive these events, you need to [subscribe](https://discordapp.com/developers/docs/topics/rpc#subscribe) to them like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"nonce": "be9a6de3-31d0-4767-a8e9-4818c5690015",
|
||||
"evt": "ACTIVITY_JOIN",
|
||||
"cmd": "SUBSCRIBE"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"nonce": "ae9qdde3-31d0-8989-a8e9-dnakwy174he",
|
||||
"evt": "ACTIVITY_SPECTATE",
|
||||
"cmd": "SUBSCRIBE"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"nonce": "5dc0c062-98c6-47a0-8922-bbb52e9d6afa",
|
||||
"evt": "ACTIVITY_JOIN_REQUEST",
|
||||
"cmd": "SUBSCRIBE"
|
||||
}
|
||||
```
|
||||
|
||||
To unsubscribe from these events, resend with the command `UNSUBSCRIBE`
|
||||
|
||||
## Responding
|
||||
A discord user will request access to the game. If the ACTIVITY_JOIN_REQUEST has been subscribed too, the ACTIVITY_JOIN_REQUEST event will be sent to the host's game. Accept it with following model:
|
||||
```json
|
||||
{
|
||||
"nonce": "5dc0c062-98c6-47a0-8922-15aerg126",
|
||||
"cmd": "SEND_ACTIVITY_JOIN_INVITE",
|
||||
"args":
|
||||
{
|
||||
"user_id": "53908232506183680"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To reject the request, use `CLOSE_ACTIVITY_REQUEST`:
|
||||
```json
|
||||
{
|
||||
"nonce": "5dc0c062-98c6-47a0-8922-dasg256eafg",
|
||||
"cmd": "CLOSE_ACTIVITY_REQUEST",
|
||||
"args":
|
||||
{
|
||||
"user_id": "53908232506183680"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
Here are just some quick notes to help with some common troubleshooting problems.
|
||||
* IPC will echo back every command you send as a response. Use this as a lock-step feature to avoid flooding messages. Can be used to validate messages such as the Presence or Subscribes.
|
||||
* The pipe expects for frames to be written in a single byte array. You cannot do multiple `stream.Write(opcode);` `stream.Write(length);` as it will break the pipe. Instead create a buffer, write the data to the buffer, then send the entire buffer to the stream.
|
||||
* Discord can be on any pipe ranging from `discord-ipc-0` to `discord-ipc-9`. It is a good idea to try and connect to each one and keeping the first one you connect too. For multiple clients (eg Discord and Canary), you might want to add a feature to manually select the pipe so you can more easily debug the application.
|
||||
* All enums are `lower_snake_case`.
|
||||
* The opcode and length in the header are `Little Endian Unsigned Integers (32bits)`. In some languages, you must convert them as they can be architecture specific.
|
||||
* [Discord Rich Presence How-To](https://discordapp.com/developers/docs/rich-presence/how-to) contains a lot of the information this document doesn't. For example, it will tell you about the response payload.
|
||||
* In the documentation, DISCORD_REPLY_IGNORE is just implemented the same as DISCORD_REPLY_NO.
|
||||
* You can test the Join / Spectate feature by enabling them in your profile and whitelisting a test account. Use Canary to run 2 accounts on the same machine.
|
BIN
libraries/discordrpc/documentation/images/rp-dev-dashboard.png
Normal file
BIN
libraries/discordrpc/documentation/images/rp-dev-dashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 KiB |
BIN
libraries/discordrpc/documentation/images/rp-profile-view.png
Normal file
BIN
libraries/discordrpc/documentation/images/rp-profile-view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
libraries/discordrpc/documentation/images/rp-secret-example.png
Normal file
BIN
libraries/discordrpc/documentation/images/rp-secret-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
26
libraries/discordrpc/include/discord_register.h
Normal file
26
libraries/discordrpc/include/discord_register.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#if defined(DISCORD_DYNAMIC_LIB)
|
||||
#if defined(_WIN32)
|
||||
#if defined(DISCORD_BUILDING_SDK)
|
||||
#define DISCORD_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define DISCORD_EXPORT __declspec(dllimport)
|
||||
#endif
|
||||
#else
|
||||
#define DISCORD_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
#else
|
||||
#define DISCORD_EXPORT
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command);
|
||||
DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
90
libraries/discordrpc/include/discord_rpc.h
Normal file
90
libraries/discordrpc/include/discord_rpc.h
Normal file
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
#if defined(DISCORD_DYNAMIC_LIB)
|
||||
# if defined(_WIN32)
|
||||
# if defined(DISCORD_BUILDING_SDK)
|
||||
# define DISCORD_EXPORT __declspec(dllexport)
|
||||
# else
|
||||
# define DISCORD_EXPORT __declspec(dllimport)
|
||||
# endif
|
||||
# else
|
||||
# define DISCORD_EXPORT __attribute__((visibility("default")))
|
||||
# endif
|
||||
#else
|
||||
# define DISCORD_EXPORT
|
||||
#endif
|
||||
|
||||
// clang-format on
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
const char* state; /* max 128 bytes */
|
||||
const char* details; /* max 128 bytes */
|
||||
int64_t startTimestamp;
|
||||
int64_t endTimestamp;
|
||||
const char* largeImageKey; /* max 32 bytes */
|
||||
const char* largeImageText; /* max 128 bytes */
|
||||
const char* smallImageKey; /* max 32 bytes */
|
||||
const char* smallImageText; /* max 128 bytes */
|
||||
const char* partyId; /* max 128 bytes */
|
||||
int partySize;
|
||||
int partyMax;
|
||||
int partyPrivacy;
|
||||
const char* matchSecret; /* max 128 bytes */
|
||||
const char* joinSecret; /* max 128 bytes */
|
||||
const char* spectateSecret; /* max 128 bytes */
|
||||
int8_t instance;
|
||||
} DiscordRichPresence;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char* userId;
|
||||
const char* username;
|
||||
const char* discriminator;
|
||||
const char* avatar;
|
||||
} DiscordUser;
|
||||
|
||||
typedef struct DiscordEventHandlers {
|
||||
void (*ready)(const DiscordUser* request);
|
||||
void (*disconnected)(int errorCode, const char* message);
|
||||
void (*errored)(int errorCode, const char* message);
|
||||
void (*joinGame)(const char* joinSecret);
|
||||
void (*spectateGame)(const char* spectateSecret);
|
||||
void (*joinRequest)(const DiscordUser* request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#define DISCORD_PARTY_PRIVATE 0
|
||||
#define DISCORD_PARTY_PUBLIC 1
|
||||
|
||||
DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
|
||||
DiscordEventHandlers* handlers,
|
||||
int autoRegister,
|
||||
const char* optionalSteamId);
|
||||
DISCORD_EXPORT void Discord_Shutdown(void);
|
||||
|
||||
/* checks for incoming messages, dispatches callbacks */
|
||||
DISCORD_EXPORT void Discord_RunCallbacks(void);
|
||||
|
||||
/* If you disable the lib starting its own io thread, you'll need to call this from your own */
|
||||
#ifdef DISCORD_DISABLE_IO_THREAD
|
||||
DISCORD_EXPORT void Discord_UpdateConnection(void);
|
||||
#endif
|
||||
|
||||
DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence);
|
||||
DISCORD_EXPORT void Discord_ClearPresence(void);
|
||||
|
||||
DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
147
libraries/discordrpc/src/CMakeLists.txt
Normal file
147
libraries/discordrpc/src/CMakeLists.txt
Normal file
|
@ -0,0 +1,147 @@
|
|||
include_directories(${PROJECT_SOURCE_DIR}/include)
|
||||
|
||||
option(ENABLE_IO_THREAD "Start up a separate I/O thread, otherwise I'd need to call an update function" ON)
|
||||
option(USE_STATIC_CRT "Use /MT[d] for dynamic library" OFF)
|
||||
option(WARNINGS_AS_ERRORS "When enabled, compiles with `-Werror` (on *nix platforms)." OFF)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 14)
|
||||
|
||||
set(BASE_RPC_SRC
|
||||
${PROJECT_SOURCE_DIR}/include/discord_rpc.h
|
||||
discord_rpc.cpp
|
||||
${PROJECT_SOURCE_DIR}/include/discord_register.h
|
||||
rpc_connection.h
|
||||
rpc_connection.cpp
|
||||
serialization.h
|
||||
serialization.cpp
|
||||
connection.h
|
||||
backoff.h
|
||||
msg_queue.h
|
||||
)
|
||||
|
||||
if (${BUILD_SHARED_LIBS})
|
||||
if(WIN32)
|
||||
set(BASE_RPC_SRC ${BASE_RPC_SRC} dllmain.cpp)
|
||||
endif(WIN32)
|
||||
endif(${BUILD_SHARED_LIBS})
|
||||
|
||||
if(WIN32)
|
||||
add_definitions(-DDISCORD_WINDOWS)
|
||||
set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_win.cpp discord_register_win.cpp)
|
||||
add_library(discord-rpc ${BASE_RPC_SRC})
|
||||
if (MSVC)
|
||||
if(USE_STATIC_CRT)
|
||||
foreach(CompilerFlag
|
||||
CMAKE_CXX_FLAGS
|
||||
CMAKE_CXX_FLAGS_DEBUG
|
||||
CMAKE_CXX_FLAGS_RELEASE
|
||||
CMAKE_C_FLAGS
|
||||
CMAKE_C_FLAGS_DEBUG
|
||||
CMAKE_C_FLAGS_RELEASE)
|
||||
string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
|
||||
endforeach()
|
||||
endif(USE_STATIC_CRT)
|
||||
target_compile_options(discord-rpc PRIVATE /EHsc
|
||||
/Wall
|
||||
/wd4100 # unreferenced formal parameter
|
||||
/wd4514 # unreferenced inline
|
||||
/wd4625 # copy constructor deleted
|
||||
/wd5026 # move constructor deleted
|
||||
/wd4626 # move assignment operator deleted
|
||||
/wd4668 # not defined preprocessor macro
|
||||
/wd4710 # function not inlined
|
||||
/wd4711 # function was inlined
|
||||
/wd4820 # structure padding
|
||||
/wd4946 # reinterpret_cast used between related classes
|
||||
/wd5027 # move assignment operator was implicitly defined as deleted
|
||||
)
|
||||
endif(MSVC)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif(WIN32)
|
||||
|
||||
if(UNIX)
|
||||
set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_unix.cpp)
|
||||
|
||||
if (APPLE)
|
||||
add_definitions(-DDISCORD_OSX)
|
||||
set(BASE_RPC_SRC ${BASE_RPC_SRC} discord_register_osx.m)
|
||||
else (APPLE)
|
||||
add_definitions(-DDISCORD_LINUX)
|
||||
set(BASE_RPC_SRC ${BASE_RPC_SRC} discord_register_linux.cpp)
|
||||
endif(APPLE)
|
||||
|
||||
add_library(discord-rpc ${BASE_RPC_SRC})
|
||||
target_link_libraries(discord-rpc PUBLIC pthread)
|
||||
|
||||
if (APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit, -mmacosx-version-min=10.10")
|
||||
endif (APPLE)
|
||||
|
||||
target_compile_options(discord-rpc PRIVATE
|
||||
-g
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
)
|
||||
|
||||
if (${WARNINGS_AS_ERRORS})
|
||||
target_compile_options(discord-rpc PRIVATE -Werror)
|
||||
endif (${WARNINGS_AS_ERRORS})
|
||||
|
||||
target_compile_options(discord-rpc PRIVATE
|
||||
-Wno-unknown-pragmas # pragma push thing doesn't work on clang
|
||||
-Wno-old-style-cast # it's fine
|
||||
-Wno-c++98-compat # that was almost 2 decades ago
|
||||
-Wno-c++98-compat-pedantic
|
||||
-Wno-missing-noreturn
|
||||
-Wno-padded # structure padding
|
||||
-Wno-covered-switch-default
|
||||
-Wno-exit-time-destructors # not sure about these
|
||||
-Wno-global-constructors
|
||||
)
|
||||
|
||||
if (${BUILD_SHARED_LIBS})
|
||||
target_compile_options(discord-rpc PRIVATE -fPIC)
|
||||
endif (${BUILD_SHARED_LIBS})
|
||||
|
||||
if (APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||
endif (APPLE)
|
||||
endif(UNIX)
|
||||
|
||||
target_include_directories(discord-rpc PRIVATE ${RAPIDJSON}/include)
|
||||
|
||||
if (NOT ${ENABLE_IO_THREAD})
|
||||
target_compile_definitions(discord-rpc PUBLIC -DDISCORD_DISABLE_IO_THREAD)
|
||||
endif (NOT ${ENABLE_IO_THREAD})
|
||||
|
||||
if (${BUILD_SHARED_LIBS})
|
||||
target_compile_definitions(discord-rpc PUBLIC -DDISCORD_DYNAMIC_LIB)
|
||||
target_compile_definitions(discord-rpc PRIVATE -DDISCORD_BUILDING_SDK)
|
||||
endif(${BUILD_SHARED_LIBS})
|
||||
|
||||
if (CLANG_FORMAT_CMD)
|
||||
add_dependencies(discord-rpc clangformat)
|
||||
endif(CLANG_FORMAT_CMD)
|
||||
|
||||
# install
|
||||
|
||||
install(
|
||||
TARGETS discord-rpc
|
||||
EXPORT "discord-rpc"
|
||||
RUNTIME
|
||||
DESTINATION "${CMAKE_INSTALL_BINDIR}"
|
||||
LIBRARY
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
ARCHIVE
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
INCLUDES
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
"../include/discord_rpc.h"
|
||||
"../include/discord_register.h"
|
||||
DESTINATION "include"
|
||||
)
|
40
libraries/discordrpc/src/backoff.h
Normal file
40
libraries/discordrpc/src/backoff.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
struct Backoff {
|
||||
int64_t minAmount;
|
||||
int64_t maxAmount;
|
||||
int64_t current;
|
||||
int fails;
|
||||
std::mt19937_64 randGenerator;
|
||||
std::uniform_real_distribution<> randDistribution;
|
||||
|
||||
double rand01() { return randDistribution(randGenerator); }
|
||||
|
||||
Backoff(int64_t min, int64_t max)
|
||||
: minAmount(min)
|
||||
, maxAmount(max)
|
||||
, current(min)
|
||||
, fails(0)
|
||||
, randGenerator((uint64_t)time(0))
|
||||
{
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
fails = 0;
|
||||
current = minAmount;
|
||||
}
|
||||
|
||||
int64_t nextDelay()
|
||||
{
|
||||
++fails;
|
||||
int64_t delay = (int64_t)((double)current * 2.0 * rand01());
|
||||
current = std::min(current + delay, maxAmount);
|
||||
return current;
|
||||
}
|
||||
};
|
19
libraries/discordrpc/src/connection.h
Normal file
19
libraries/discordrpc/src/connection.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
// This is to wrap the platform specific kinds of connect/read/write.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// not really connectiony, but need per-platform
|
||||
int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection* Create();
|
||||
static void Destroy(BaseConnection*&);
|
||||
bool isOpen{false};
|
||||
bool Open();
|
||||
bool Close();
|
||||
bool Write(const void* data, size_t length);
|
||||
bool Read(void* data, size_t length);
|
||||
};
|
125
libraries/discordrpc/src/connection_unix.cpp
Normal file
125
libraries/discordrpc/src/connection_unix.cpp
Normal file
|
@ -0,0 +1,125 @@
|
|||
#include "connection.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int GetProcessId()
|
||||
{
|
||||
return ::getpid();
|
||||
}
|
||||
|
||||
struct BaseConnectionUnix : public BaseConnection {
|
||||
int sock{-1};
|
||||
};
|
||||
|
||||
static BaseConnectionUnix Connection;
|
||||
static sockaddr_un PipeAddr{};
|
||||
#ifdef MSG_NOSIGNAL
|
||||
static int MsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
static int MsgFlags = 0;
|
||||
#endif
|
||||
|
||||
static const char* GetTempPath()
|
||||
{
|
||||
const char* temp = getenv("XDG_RUNTIME_DIR");
|
||||
temp = temp ? temp : getenv("TMPDIR");
|
||||
temp = temp ? temp : getenv("TMP");
|
||||
temp = temp ? temp : getenv("TEMP");
|
||||
temp = temp ? temp : "/tmp";
|
||||
return temp;
|
||||
}
|
||||
|
||||
/*static*/ BaseConnection* BaseConnection::Create()
|
||||
{
|
||||
PipeAddr.sun_family = AF_UNIX;
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
/*static*/ void BaseConnection::Destroy(BaseConnection*& c)
|
||||
{
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
}
|
||||
|
||||
bool BaseConnection::Open()
|
||||
{
|
||||
const char* tempPath = GetTempPath();
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
fcntl(self->sock, F_SETFL, O_NONBLOCK);
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int optval = 1;
|
||||
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
||||
#endif
|
||||
|
||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
||||
snprintf(
|
||||
PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
||||
int err = connect(self->sock, (const sockaddr*)&PipeAddr, sizeof(PipeAddr));
|
||||
if (err == 0) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self->Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BaseConnection::Close()
|
||||
{
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
close(self->sock);
|
||||
self->sock = -1;
|
||||
self->isOpen = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void* data, size_t length)
|
||||
{
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
|
||||
if (sentBytes < 0) {
|
||||
Close();
|
||||
}
|
||||
return sentBytes == (ssize_t)length;
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void* data, size_t length)
|
||||
{
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int res = (int)recv(self->sock, data, length, MsgFlags);
|
||||
if (res < 0) {
|
||||
if (errno == EAGAIN) {
|
||||
return false;
|
||||
}
|
||||
Close();
|
||||
}
|
||||
else if (res == 0) {
|
||||
Close();
|
||||
}
|
||||
return res == (int)length;
|
||||
}
|
128
libraries/discordrpc/src/connection_win.cpp
Normal file
128
libraries/discordrpc/src/connection_win.cpp
Normal file
|
@ -0,0 +1,128 @@
|
|||
#include "connection.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
#include <assert.h>
|
||||
#include <windows.h>
|
||||
|
||||
int GetProcessId()
|
||||
{
|
||||
return (int)::GetCurrentProcessId();
|
||||
}
|
||||
|
||||
struct BaseConnectionWin : public BaseConnection {
|
||||
HANDLE pipe{INVALID_HANDLE_VALUE};
|
||||
};
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
|
||||
/*static*/ BaseConnection* BaseConnection::Create()
|
||||
{
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
/*static*/ void BaseConnection::Destroy(BaseConnection*& c)
|
||||
{
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
}
|
||||
|
||||
bool BaseConnection::Open()
|
||||
{
|
||||
wchar_t pipeName[]{L"\\\\?\\pipe\\discord-ipc-0"};
|
||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(
|
||||
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto lastError = GetLastError();
|
||||
if (lastError == ERROR_FILE_NOT_FOUND) {
|
||||
if (pipeName[pipeDigit] < L'9') {
|
||||
pipeName[pipeDigit]++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (lastError == ERROR_PIPE_BUSY) {
|
||||
if (!WaitNamedPipeW(pipeName, 10000)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool BaseConnection::Close()
|
||||
{
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void* data, size_t length)
|
||||
{
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
}
|
||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const DWORD bytesLength = (DWORD)length;
|
||||
DWORD bytesWritten = 0;
|
||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE &&
|
||||
bytesWritten == bytesLength;
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void* data, size_t length)
|
||||
{
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
}
|
||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
DWORD bytesAvailable = 0;
|
||||
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
|
||||
if (bytesAvailable >= length) {
|
||||
DWORD bytesToRead = (DWORD)length;
|
||||
DWORD bytesRead = 0;
|
||||
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
|
||||
assert(bytesToRead == bytesRead);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
return false;
|
||||
}
|
102
libraries/discordrpc/src/discord_register_linux.cpp
Normal file
102
libraries/discordrpc/src/discord_register_linux.cpp
Normal file
|
@ -0,0 +1,102 @@
|
|||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static bool Mkdir(const char* path)
|
||||
{
|
||||
int result = mkdir(path, 0755);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (errno == EEXIST) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// we want to register games so we can run them from Discord client as discord-<appid>://
|
||||
extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command)
|
||||
{
|
||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
||||
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
char exePath[1024];
|
||||
if (!command || !command[0]) {
|
||||
ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
||||
if (size <= 0 || size >= (ssize_t)sizeof(exePath)) {
|
||||
return;
|
||||
}
|
||||
exePath[size] = '\0';
|
||||
command = exePath;
|
||||
}
|
||||
|
||||
const char* desktopFileFormat = "[Desktop Entry]\n"
|
||||
"Name=Game %s\n"
|
||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
||||
"Type=Application\n"
|
||||
"NoDisplay=true\n"
|
||||
"Categories=Discord;Games;\n"
|
||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
||||
char desktopFile[2048];
|
||||
int fileLen = snprintf(
|
||||
desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
||||
if (fileLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char desktopFilename[256];
|
||||
snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
||||
|
||||
char desktopFilePath[1024];
|
||||
snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/share");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/applications");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, desktopFilename);
|
||||
|
||||
FILE* fp = fopen(desktopFilePath, "w");
|
||||
if (fp) {
|
||||
fwrite(desktopFile, 1, fileLen, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
char xdgMimeCommand[1024];
|
||||
snprintf(xdgMimeCommand,
|
||||
sizeof(xdgMimeCommand),
|
||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
||||
applicationId,
|
||||
applicationId);
|
||||
if (system(xdgMimeCommand) < 0) {
|
||||
fprintf(stderr, "Failed to register mime handler\n");
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId,
|
||||
const char* steamId)
|
||||
{
|
||||
char command[256];
|
||||
sprintf(command, "xdg-open steam://rungameid/%s", steamId);
|
||||
Discord_Register(applicationId, command);
|
||||
}
|
80
libraries/discordrpc/src/discord_register_osx.m
Normal file
80
libraries/discordrpc/src/discord_register_osx.m
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include "discord_register.h"
|
||||
|
||||
static void RegisterCommand(const char* applicationId, const char* command)
|
||||
{
|
||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
||||
|
||||
// Note: will not work for sandboxed apps
|
||||
NSString *home = NSHomeDirectory();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
||||
stringByAppendingPathComponent:@"Application Support"]
|
||||
stringByAppendingPathComponent:@"discord"]
|
||||
stringByAppendingPathComponent:@"games"]
|
||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
||||
stringByAppendingPathExtension:@"json"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
|
||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
||||
}
|
||||
|
||||
static void RegisterURL(const char* applicationId)
|
||||
{
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
||||
|
||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!myBundleId) {
|
||||
fprintf(stderr, "No bundle id found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
||||
if (!myURL) {
|
||||
fprintf(stderr, "No bundle url found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
||||
}
|
||||
}
|
||||
|
||||
void Discord_Register(const char* applicationId, const char* command)
|
||||
{
|
||||
if (command) {
|
||||
RegisterCommand(applicationId, command);
|
||||
}
|
||||
else {
|
||||
// raii lite
|
||||
@autoreleasepool {
|
||||
RegisterURL(applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
|
||||
{
|
||||
char command[256];
|
||||
snprintf(command, 256, "steam://rungameid/%s", steamId);
|
||||
Discord_Register(applicationId, command);
|
||||
}
|
186
libraries/discordrpc/src/discord_register_win.cpp
Normal file
186
libraries/discordrpc/src/discord_register_win.cpp
Normal file
|
@ -0,0 +1,186 @@
|
|||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <cstdio>
|
||||
|
||||
/**
|
||||
* Updated fixes for MinGW and WinXP
|
||||
* This block is written the way it does not involve changing the rest of the code
|
||||
* Checked to be compiling
|
||||
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
|
||||
* #include guarded, functions redirected to <string.h> substitutes
|
||||
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
|
||||
* The entire function is rewritten
|
||||
*/
|
||||
#ifdef __MINGW32__
|
||||
#include <wchar.h>
|
||||
/// strsafe.h fixes
|
||||
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...)
|
||||
{
|
||||
HRESULT ret;
|
||||
va_list va;
|
||||
va_start(va, pszFormat);
|
||||
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
|
||||
// othervise
|
||||
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
|
||||
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
|
||||
va_end(va);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
#include <cwchar>
|
||||
#include <strsafe.h>
|
||||
#endif // __MINGW32__
|
||||
|
||||
/// winreg.h fixes
|
||||
#ifndef LSTATUS
|
||||
#define LSTATUS LONG
|
||||
#endif
|
||||
#ifdef RegSetKeyValueW
|
||||
#undefine RegSetKeyValueW
|
||||
#endif
|
||||
#define RegSetKeyValueW regset
|
||||
static LSTATUS regset(HKEY hkey,
|
||||
LPCWSTR subkey,
|
||||
LPCWSTR name,
|
||||
DWORD type,
|
||||
const void* data,
|
||||
DWORD len)
|
||||
{
|
||||
HKEY htkey = hkey, hsubkey = nullptr;
|
||||
LSTATUS ret;
|
||||
if (subkey && subkey[0]) {
|
||||
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
|
||||
ERROR_SUCCESS)
|
||||
return ret;
|
||||
htkey = hsubkey;
|
||||
}
|
||||
ret = RegSetValueExW(htkey, name, 0, type, (const BYTE*)data, len);
|
||||
if (hsubkey && hsubkey != hkey)
|
||||
RegCloseKey(hsubkey);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
|
||||
{
|
||||
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
|
||||
// we want to register games so we can run them as discord-<appid>://
|
||||
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
|
||||
|
||||
wchar_t exeFilePath[MAX_PATH];
|
||||
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
||||
wchar_t openCommand[1024];
|
||||
|
||||
if (command && command[0]) {
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
||||
}
|
||||
else {
|
||||
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
||||
}
|
||||
|
||||
wchar_t protocolName[64];
|
||||
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
||||
wchar_t protocolDescription[128];
|
||||
StringCbPrintfW(
|
||||
protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||
wchar_t urlProtocol = 0;
|
||||
|
||||
wchar_t keyName[256];
|
||||
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
||||
HKEY key;
|
||||
auto status =
|
||||
RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error creating key\n");
|
||||
return;
|
||||
}
|
||||
DWORD len;
|
||||
LSTATUS result;
|
||||
len = (DWORD)lstrlenW(protocolDescription) + 1;
|
||||
result =
|
||||
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
len = (DWORD)lstrlenW(protocolDescription) + 1;
|
||||
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
result = RegSetKeyValueW(
|
||||
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing icon\n");
|
||||
}
|
||||
|
||||
len = (DWORD)lstrlenW(openCommand) + 1;
|
||||
result = RegSetKeyValueW(
|
||||
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing command\n");
|
||||
}
|
||||
RegCloseKey(key);
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command)
|
||||
{
|
||||
wchar_t appId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t openCommand[1024];
|
||||
const wchar_t* wcommand = nullptr;
|
||||
if (command && command[0]) {
|
||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
||||
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
|
||||
wcommand = openCommand;
|
||||
}
|
||||
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId,
|
||||
const char* steamId)
|
||||
{
|
||||
wchar_t appId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t wSteamId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32);
|
||||
|
||||
HKEY key;
|
||||
auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error opening Steam key\n");
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t steamPath[MAX_PATH];
|
||||
DWORD pathBytes = sizeof(steamPath);
|
||||
status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE*)steamPath, &pathBytes);
|
||||
RegCloseKey(key);
|
||||
if (status != ERROR_SUCCESS || pathBytes < 1) {
|
||||
fprintf(stderr, "Error reading SteamExe key\n");
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD pathChars = pathBytes / sizeof(wchar_t);
|
||||
for (DWORD i = 0; i < pathChars; ++i) {
|
||||
if (steamPath[i] == L'/') {
|
||||
steamPath[i] = L'\\';
|
||||
}
|
||||
}
|
||||
|
||||
wchar_t command[1024];
|
||||
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId);
|
||||
|
||||
Discord_RegisterW(appId, command);
|
||||
}
|
504
libraries/discordrpc/src/discord_rpc.cpp
Normal file
504
libraries/discordrpc/src/discord_rpc.cpp
Normal file
|
@ -0,0 +1,504 @@
|
|||
#include "discord_rpc.h"
|
||||
|
||||
#include "backoff.h"
|
||||
#include "discord_register.h"
|
||||
#include "msg_queue.h"
|
||||
#include "rpc_connection.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#ifndef DISCORD_DISABLE_IO_THREAD
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#endif
|
||||
|
||||
constexpr size_t MaxMessageSize{16 * 1024};
|
||||
constexpr size_t MessageQueueSize{8};
|
||||
constexpr size_t JoinQueueSize{8};
|
||||
|
||||
struct QueuedMessage {
|
||||
size_t length;
|
||||
char buffer[MaxMessageSize];
|
||||
|
||||
void Copy(const QueuedMessage& other)
|
||||
{
|
||||
length = other.length;
|
||||
if (length) {
|
||||
memcpy(buffer, other.buffer, length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct User {
|
||||
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
|
||||
// terminator = 21
|
||||
char userId[32];
|
||||
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
|
||||
// terminator = 129
|
||||
char username[344];
|
||||
// 4 decimal digits + 1 null terminator = 5
|
||||
char discriminator[8];
|
||||
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
|
||||
char avatar[128];
|
||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||
};
|
||||
|
||||
static RpcConnection* Connection{nullptr};
|
||||
static DiscordEventHandlers QueuedHandlers{};
|
||||
static DiscordEventHandlers Handlers{};
|
||||
static std::atomic_bool WasJustConnected{false};
|
||||
static std::atomic_bool WasJustDisconnected{false};
|
||||
static std::atomic_bool GotErrorMessage{false};
|
||||
static std::atomic_bool WasJoinGame{false};
|
||||
static std::atomic_bool WasSpectateGame{false};
|
||||
static std::atomic_bool UpdatePresence{false};
|
||||
static char JoinGameSecret[256];
|
||||
static char SpectateGameSecret[256];
|
||||
static int LastErrorCode{0};
|
||||
static char LastErrorMessage[256];
|
||||
static int LastDisconnectErrorCode{0};
|
||||
static char LastDisconnectErrorMessage[256];
|
||||
static std::mutex PresenceMutex;
|
||||
static std::mutex HandlerMutex;
|
||||
static QueuedMessage QueuedPresence{};
|
||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential
|
||||
// backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid{0};
|
||||
static int Nonce{1};
|
||||
|
||||
#ifndef DISCORD_DISABLE_IO_THREAD
|
||||
static void Discord_UpdateConnection(void);
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning{true};
|
||||
std::mutex waitForIOMutex;
|
||||
std::condition_variable waitForIOActivity;
|
||||
std::thread ioThread;
|
||||
|
||||
public:
|
||||
void Start()
|
||||
{
|
||||
keepRunning.store(true);
|
||||
ioThread = std::thread([&]() {
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait{500LL};
|
||||
Discord_UpdateConnection();
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Notify() { waitForIOActivity.notify_all(); }
|
||||
|
||||
void Stop()
|
||||
{
|
||||
keepRunning.exchange(false);
|
||||
Notify();
|
||||
if (ioThread.joinable()) {
|
||||
ioThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
#else
|
||||
class IoThreadHolder {
|
||||
public:
|
||||
void Start() {}
|
||||
void Stop() {}
|
||||
void Notify() {}
|
||||
};
|
||||
#endif // DISCORD_DISABLE_IO_THREAD
|
||||
static IoThreadHolder* IoThread{nullptr};
|
||||
|
||||
static void UpdateReconnectTime()
|
||||
{
|
||||
NextConnect = std::chrono::system_clock::now() +
|
||||
std::chrono::duration<int64_t, std::milli>{ReconnectTimeMs.nextDelay()};
|
||||
}
|
||||
|
||||
#ifdef DISCORD_DISABLE_IO_THREAD
|
||||
extern "C" DISCORD_EXPORT void Discord_UpdateConnection(void)
|
||||
#else
|
||||
static void Discord_UpdateConnection(void)
|
||||
#endif
|
||||
{
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Connection->IsOpen()) {
|
||||
if (std::chrono::system_clock::now() >= NextConnect) {
|
||||
UpdateReconnectTime();
|
||||
Connection->Open();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// reads
|
||||
|
||||
for (;;) {
|
||||
JsonDocument message;
|
||||
|
||||
if (!Connection->Read(message)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const char* evtName = GetStrMember(&message, "evt");
|
||||
const char* nonce = GetStrMember(&message, "nonce");
|
||||
|
||||
if (nonce) {
|
||||
// in responses only -- should use to match up response when needed.
|
||||
|
||||
if (evtName && strcmp(evtName, "ERROR") == 0) {
|
||||
auto data = GetObjMember(&message, "data");
|
||||
LastErrorCode = GetIntMember(data, "code");
|
||||
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
|
||||
GotErrorMessage.store(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// should have evt == name of event, optional data
|
||||
if (evtName == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto data = GetObjMember(&message, "data");
|
||||
|
||||
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
|
||||
auto secret = GetStrMember(data, "secret");
|
||||
if (secret) {
|
||||
StringCopy(JoinGameSecret, secret);
|
||||
WasJoinGame.store(true);
|
||||
}
|
||||
}
|
||||
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
|
||||
auto secret = GetStrMember(data, "secret");
|
||||
if (secret) {
|
||||
StringCopy(SpectateGameSecret, secret);
|
||||
WasSpectateGame.store(true);
|
||||
}
|
||||
}
|
||||
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
|
||||
auto user = GetObjMember(data, "user");
|
||||
auto userId = GetStrMember(user, "id");
|
||||
auto username = GetStrMember(user, "username");
|
||||
auto avatar = GetStrMember(user, "avatar");
|
||||
auto joinReq = JoinAskQueue.GetNextAddMessage();
|
||||
if (userId && username && joinReq) {
|
||||
StringCopy(joinReq->userId, userId);
|
||||
StringCopy(joinReq->username, username);
|
||||
auto discriminator = GetStrMember(user, "discriminator");
|
||||
if (discriminator) {
|
||||
StringCopy(joinReq->discriminator, discriminator);
|
||||
}
|
||||
if (avatar) {
|
||||
StringCopy(joinReq->avatar, avatar);
|
||||
}
|
||||
else {
|
||||
joinReq->avatar[0] = 0;
|
||||
}
|
||||
JoinAskQueue.CommitAdd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writes
|
||||
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
|
||||
QueuedMessage local;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
local.Copy(QueuedPresence);
|
||||
}
|
||||
if (!Connection->Write(local.buffer, local.length)) {
|
||||
// if we fail to send, requeue
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.Copy(local);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
}
|
||||
|
||||
while (SendQueue.HavePendingSends()) {
|
||||
auto qmessage = SendQueue.GetNextSendMessage();
|
||||
Connection->Write(qmessage->buffer, qmessage->length);
|
||||
SendQueue.CommitSend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void SignalIOActivity()
|
||||
{
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Notify();
|
||||
}
|
||||
}
|
||||
|
||||
static bool RegisterForEvent(const char* evtName)
|
||||
{
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool DeregisterForEvent(const char* evtName)
|
||||
{
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
|
||||
DiscordEventHandlers* handlers,
|
||||
int autoRegister,
|
||||
const char* optionalSteamId)
|
||||
{
|
||||
IoThread = new (std::nothrow) IoThreadHolder();
|
||||
if (IoThread == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRegister) {
|
||||
if (optionalSteamId && optionalSteamId[0]) {
|
||||
Discord_RegisterSteamGame(applicationId, optionalSteamId);
|
||||
}
|
||||
else {
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
Pid = GetProcessId();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
|
||||
if (handlers) {
|
||||
QueuedHandlers = *handlers;
|
||||
}
|
||||
else {
|
||||
QueuedHandlers = {};
|
||||
}
|
||||
|
||||
Handlers = {};
|
||||
}
|
||||
|
||||
if (Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
Connection = RpcConnection::Create(applicationId);
|
||||
Connection->onConnect = [](JsonDocument& readyMessage) {
|
||||
Discord_UpdateHandlers(&QueuedHandlers);
|
||||
if (QueuedPresence.length > 0) {
|
||||
UpdatePresence.exchange(true);
|
||||
SignalIOActivity();
|
||||
}
|
||||
auto data = GetObjMember(&readyMessage, "data");
|
||||
auto user = GetObjMember(data, "user");
|
||||
auto userId = GetStrMember(user, "id");
|
||||
auto username = GetStrMember(user, "username");
|
||||
auto avatar = GetStrMember(user, "avatar");
|
||||
if (userId && username) {
|
||||
StringCopy(connectedUser.userId, userId);
|
||||
StringCopy(connectedUser.username, username);
|
||||
auto discriminator = GetStrMember(user, "discriminator");
|
||||
if (discriminator) {
|
||||
StringCopy(connectedUser.discriminator, discriminator);
|
||||
}
|
||||
if (avatar) {
|
||||
StringCopy(connectedUser.avatar, avatar);
|
||||
}
|
||||
else {
|
||||
connectedUser.avatar[0] = 0;
|
||||
}
|
||||
}
|
||||
WasJustConnected.exchange(true);
|
||||
ReconnectTimeMs.reset();
|
||||
};
|
||||
Connection->onDisconnect = [](int err, const char* message) {
|
||||
LastDisconnectErrorCode = err;
|
||||
StringCopy(LastDisconnectErrorMessage, message);
|
||||
WasJustDisconnected.exchange(true);
|
||||
UpdateReconnectTime();
|
||||
};
|
||||
|
||||
IoThread->Start();
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_Shutdown(void)
|
||||
{
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
Connection->onConnect = nullptr;
|
||||
Connection->onDisconnect = nullptr;
|
||||
Handlers = {};
|
||||
QueuedPresence.length = 0;
|
||||
UpdatePresence.exchange(false);
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Stop();
|
||||
delete IoThread;
|
||||
IoThread = nullptr;
|
||||
}
|
||||
|
||||
RpcConnection::Destroy(Connection);
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.length = JsonWriteRichPresenceObj(
|
||||
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
SignalIOActivity();
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_ClearPresence(void)
|
||||
{
|
||||
Discord_UpdatePresence(nullptr);
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_Respond(const char* userId, /* DISCORD_REPLY_ */ int reply)
|
||||
{
|
||||
// if we are not connected, let's not batch up stale messages for later
|
||||
if (!Connection || !Connection->IsOpen()) {
|
||||
return;
|
||||
}
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_RunCallbacks(void)
|
||||
{
|
||||
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
|
||||
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
|
||||
// signals are book-ended by calls to ready and disconnect.
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
bool isConnected = Connection->IsOpen();
|
||||
|
||||
if (isConnected) {
|
||||
// if we are connected, disconnect cb first
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (wasDisconnected && Handlers.disconnected) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du{connectedUser.userId,
|
||||
connectedUser.username,
|
||||
connectedUser.discriminator,
|
||||
connectedUser.avatar};
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
|
||||
if (GotErrorMessage.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.errored) {
|
||||
Handlers.errored(LastErrorCode, LastErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasJoinGame.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinGame) {
|
||||
Handlers.joinGame(JoinGameSecret);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasSpectateGame.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.spectateGame) {
|
||||
Handlers.spectateGame(SpectateGameSecret);
|
||||
}
|
||||
}
|
||||
|
||||
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
|
||||
// where the implementer would rather sequentially accept/reject each one before the next invite
|
||||
// is sent. I left it this way because I could also imagine wanting to process these all and
|
||||
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
|
||||
// not it should be trivial for the implementer to make a queue themselves.
|
||||
while (JoinAskQueue.HavePendingSends()) {
|
||||
auto req = JoinAskQueue.GetNextSendMessage();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
DiscordUser du{req->userId, req->username, req->discriminator, req->avatar};
|
||||
Handlers.joinRequest(&du);
|
||||
}
|
||||
}
|
||||
JoinAskQueue.CommitSend();
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
// if we are not connected, disconnect message last
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (wasDisconnected && Handlers.disconnected) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* newHandlers)
|
||||
{
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
|
||||
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
|
||||
|
||||
#undef HANDLE_EVENT_REGISTRATION
|
||||
|
||||
Handlers = *newHandlers;
|
||||
}
|
||||
else {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
Handlers = {};
|
||||
}
|
||||
return;
|
||||
}
|
8
libraries/discordrpc/src/dllmain.cpp
Normal file
8
libraries/discordrpc/src/dllmain.cpp
Normal file
|
@ -0,0 +1,8 @@
|
|||
#include <windows.h>
|
||||
|
||||
// outsmart GCC's missing-declarations warning
|
||||
BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID);
|
||||
BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
36
libraries/discordrpc/src/msg_queue.h
Normal file
36
libraries/discordrpc/src/msg_queue.h
Normal file
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
||||
// a consumer. Mutex up as needed.
|
||||
|
||||
template <typename ElementType, size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_{0};
|
||||
std::atomic_uint nextSend_{0};
|
||||
std::atomic_uint pendingSends_{0};
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
ElementType* GetNextAddMessage()
|
||||
{
|
||||
// if we are falling behind, bail
|
||||
if (pendingSends_.load() >= QueueSize) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = (nextAdd_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitAdd() { ++pendingSends_; }
|
||||
|
||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
||||
ElementType* GetNextSendMessage()
|
||||
{
|
||||
auto index = (nextSend_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitSend() { --pendingSends_; }
|
||||
};
|
137
libraries/discordrpc/src/rpc_connection.cpp
Normal file
137
libraries/discordrpc/src/rpc_connection.cpp
Normal file
|
@ -0,0 +1,137 @@
|
|||
#include "rpc_connection.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
static const int RpcVersion = 1;
|
||||
static RpcConnection Instance;
|
||||
|
||||
/*static*/ RpcConnection* RpcConnection::Create(const char* applicationId)
|
||||
{
|
||||
Instance.connection = BaseConnection::Create();
|
||||
StringCopy(Instance.appId, applicationId);
|
||||
return &Instance;
|
||||
}
|
||||
|
||||
/*static*/ void RpcConnection::Destroy(RpcConnection*& c)
|
||||
{
|
||||
c->Close();
|
||||
BaseConnection::Destroy(c->connection);
|
||||
c = nullptr;
|
||||
}
|
||||
|
||||
void RpcConnection::Open()
|
||||
{
|
||||
if (state == State::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::Disconnected && !connection->Open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::SentHandshake) {
|
||||
JsonDocument message;
|
||||
if (Read(message)) {
|
||||
auto cmd = GetStrMember(&message, "cmd");
|
||||
auto evt = GetStrMember(&message, "evt");
|
||||
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
|
||||
state = State::Connected;
|
||||
if (onConnect) {
|
||||
onConnect(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendFrame.opcode = Opcode::Handshake;
|
||||
sendFrame.length = (uint32_t)JsonWriteHandshakeObj(
|
||||
sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId);
|
||||
|
||||
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
|
||||
state = State::SentHandshake;
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RpcConnection::Close()
|
||||
{
|
||||
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
||||
onDisconnect(lastErrorCode, lastErrorMessage);
|
||||
}
|
||||
connection->Close();
|
||||
state = State::Disconnected;
|
||||
}
|
||||
|
||||
bool RpcConnection::Write(const void* data, size_t length)
|
||||
{
|
||||
sendFrame.opcode = Opcode::Frame;
|
||||
memcpy(sendFrame.message, data, length);
|
||||
sendFrame.length = (uint32_t)length;
|
||||
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RpcConnection::Read(JsonDocument& message)
|
||||
{
|
||||
if (state != State::Connected && state != State::SentHandshake) {
|
||||
return false;
|
||||
}
|
||||
MessageFrame readFrame;
|
||||
for (;;) {
|
||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
||||
if (!didRead) {
|
||||
if (!connection->isOpen) {
|
||||
lastErrorCode = (int)ErrorCode::PipeClosed;
|
||||
StringCopy(lastErrorMessage, "Pipe closed");
|
||||
Close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readFrame.length > 0) {
|
||||
didRead = connection->Read(readFrame.message, readFrame.length);
|
||||
if (!didRead) {
|
||||
lastErrorCode = (int)ErrorCode::ReadCorrupt;
|
||||
StringCopy(lastErrorMessage, "Partial data in frame");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
readFrame.message[readFrame.length] = 0;
|
||||
}
|
||||
|
||||
switch (readFrame.opcode) {
|
||||
case Opcode::Close: {
|
||||
message.ParseInsitu(readFrame.message);
|
||||
lastErrorCode = GetIntMember(&message, "code");
|
||||
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
case Opcode::Frame:
|
||||
message.ParseInsitu(readFrame.message);
|
||||
return true;
|
||||
case Opcode::Ping:
|
||||
readFrame.opcode = Opcode::Pong;
|
||||
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
|
||||
Close();
|
||||
}
|
||||
break;
|
||||
case Opcode::Pong:
|
||||
break;
|
||||
case Opcode::Handshake:
|
||||
default:
|
||||
// something bad happened
|
||||
lastErrorCode = (int)ErrorCode::ReadCorrupt;
|
||||
StringCopy(lastErrorMessage, "Bad ipc frame");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
59
libraries/discordrpc/src/rpc_connection.h
Normal file
59
libraries/discordrpc/src/rpc_connection.h
Normal file
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include "connection.h"
|
||||
#include "serialization.h"
|
||||
|
||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much
|
||||
// smaller.
|
||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
||||
|
||||
struct RpcConnection {
|
||||
enum class ErrorCode : int {
|
||||
Success = 0,
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
};
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
struct MessageFrameHeader {
|
||||
Opcode opcode;
|
||||
uint32_t length;
|
||||
};
|
||||
|
||||
struct MessageFrame : public MessageFrameHeader {
|
||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
||||
};
|
||||
|
||||
enum class State : uint32_t {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
AwaitingResponse,
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection* connection{nullptr};
|
||||
State state{State::Disconnected};
|
||||
void (*onConnect)(JsonDocument& message){nullptr};
|
||||
void (*onDisconnect)(int errorCode, const char* message){nullptr};
|
||||
char appId[64]{};
|
||||
int lastErrorCode{0};
|
||||
char lastErrorMessage[256]{};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection* Create(const char* applicationId);
|
||||
static void Destroy(RpcConnection*&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
bool Write(const void* data, size_t length);
|
||||
bool Read(JsonDocument& message);
|
||||
};
|
250
libraries/discordrpc/src/serialization.cpp
Normal file
250
libraries/discordrpc/src/serialization.cpp
Normal file
|
@ -0,0 +1,250 @@
|
|||
#include "serialization.h"
|
||||
#include "connection.h"
|
||||
#include "discord_rpc.h"
|
||||
|
||||
template <typename T>
|
||||
void NumberToString(char* dest, T number)
|
||||
{
|
||||
if (!number) {
|
||||
*dest++ = '0';
|
||||
*dest++ = 0;
|
||||
return;
|
||||
}
|
||||
if (number < 0) {
|
||||
*dest++ = '-';
|
||||
number = -number;
|
||||
}
|
||||
char temp[32];
|
||||
int place = 0;
|
||||
while (number) {
|
||||
auto digit = number % 10;
|
||||
number = number / 10;
|
||||
temp[place++] = '0' + (char)digit;
|
||||
}
|
||||
for (--place; place >= 0; --place) {
|
||||
*dest++ = temp[place];
|
||||
}
|
||||
*dest = 0;
|
||||
}
|
||||
|
||||
// it's ever so slightly faster to not have to strlen the key
|
||||
template <typename T>
|
||||
void WriteKey(JsonWriter& w, T& k)
|
||||
{
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
}
|
||||
|
||||
struct WriteObject {
|
||||
JsonWriter& writer;
|
||||
WriteObject(JsonWriter& w)
|
||||
: writer(w)
|
||||
{
|
||||
writer.StartObject();
|
||||
}
|
||||
template <typename T>
|
||||
WriteObject(JsonWriter& w, T& name)
|
||||
: writer(w)
|
||||
{
|
||||
WriteKey(writer, name);
|
||||
writer.StartObject();
|
||||
}
|
||||
~WriteObject() { writer.EndObject(); }
|
||||
};
|
||||
|
||||
struct WriteArray {
|
||||
JsonWriter& writer;
|
||||
template <typename T>
|
||||
WriteArray(JsonWriter& w, T& name)
|
||||
: writer(w)
|
||||
{
|
||||
WriteKey(writer, name);
|
||||
writer.StartArray();
|
||||
}
|
||||
~WriteArray() { writer.EndArray(); }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void WriteOptionalString(JsonWriter& w, T& k, const char* value)
|
||||
{
|
||||
if (value && value[0]) {
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
w.String(value);
|
||||
}
|
||||
}
|
||||
|
||||
static void JsonWriteNonce(JsonWriter& writer, int nonce)
|
||||
{
|
||||
WriteKey(writer, "nonce");
|
||||
char nonceBuffer[32];
|
||||
NumberToString(nonceBuffer, nonce);
|
||||
writer.String(nonceBuffer);
|
||||
}
|
||||
|
||||
size_t JsonWriteRichPresenceObj(char* dest,
|
||||
size_t maxLen,
|
||||
int nonce,
|
||||
int pid,
|
||||
const DiscordRichPresence* presence)
|
||||
{
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject top(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("SET_ACTIVITY");
|
||||
|
||||
{
|
||||
WriteObject args(writer, "args");
|
||||
|
||||
WriteKey(writer, "pid");
|
||||
writer.Int(pid);
|
||||
|
||||
if (presence != nullptr) {
|
||||
WriteObject activity(writer, "activity");
|
||||
|
||||
WriteOptionalString(writer, "state", presence->state);
|
||||
WriteOptionalString(writer, "details", presence->details);
|
||||
|
||||
if (presence->startTimestamp || presence->endTimestamp) {
|
||||
WriteObject timestamps(writer, "timestamps");
|
||||
|
||||
if (presence->startTimestamp) {
|
||||
WriteKey(writer, "start");
|
||||
writer.Int64(presence->startTimestamp);
|
||||
}
|
||||
|
||||
if (presence->endTimestamp) {
|
||||
WriteKey(writer, "end");
|
||||
writer.Int64(presence->endTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
|
||||
(presence->largeImageText && presence->largeImageText[0]) ||
|
||||
(presence->smallImageKey && presence->smallImageKey[0]) ||
|
||||
(presence->smallImageText && presence->smallImageText[0])) {
|
||||
WriteObject assets(writer, "assets");
|
||||
WriteOptionalString(writer, "large_image", presence->largeImageKey);
|
||||
WriteOptionalString(writer, "large_text", presence->largeImageText);
|
||||
WriteOptionalString(writer, "small_image", presence->smallImageKey);
|
||||
WriteOptionalString(writer, "small_text", presence->smallImageText);
|
||||
}
|
||||
|
||||
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
|
||||
presence->partyMax || presence->partyPrivacy) {
|
||||
WriteObject party(writer, "party");
|
||||
WriteOptionalString(writer, "id", presence->partyId);
|
||||
if (presence->partySize && presence->partyMax) {
|
||||
WriteArray size(writer, "size");
|
||||
writer.Int(presence->partySize);
|
||||
writer.Int(presence->partyMax);
|
||||
}
|
||||
|
||||
if (presence->partyPrivacy) {
|
||||
WriteKey(writer, "privacy");
|
||||
writer.Int(presence->partyPrivacy);
|
||||
}
|
||||
}
|
||||
|
||||
if ((presence->matchSecret && presence->matchSecret[0]) ||
|
||||
(presence->joinSecret && presence->joinSecret[0]) ||
|
||||
(presence->spectateSecret && presence->spectateSecret[0])) {
|
||||
WriteObject secrets(writer, "secrets");
|
||||
WriteOptionalString(writer, "match", presence->matchSecret);
|
||||
WriteOptionalString(writer, "join", presence->joinSecret);
|
||||
WriteOptionalString(writer, "spectate", presence->spectateSecret);
|
||||
}
|
||||
|
||||
writer.Key("instance");
|
||||
writer.Bool(presence->instance != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char* dest, size_t maxLen, int version, const char* applicationId)
|
||||
{
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
WriteKey(writer, "v");
|
||||
writer.Int(version);
|
||||
WriteKey(writer, "client_id");
|
||||
writer.String(applicationId);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
||||
|
||||
size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName)
|
||||
{
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("SUBSCRIBE");
|
||||
|
||||
WriteKey(writer, "evt");
|
||||
writer.String(evtName);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName)
|
||||
{
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("UNSUBSCRIBE");
|
||||
|
||||
WriteKey(writer, "evt");
|
||||
writer.String(evtName);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
||||
|
||||
size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce)
|
||||
{
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
if (reply == DISCORD_REPLY_YES) {
|
||||
writer.String("SEND_ACTIVITY_JOIN_INVITE");
|
||||
}
|
||||
else {
|
||||
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
|
||||
}
|
||||
|
||||
WriteKey(writer, "args");
|
||||
{
|
||||
WriteObject args(writer);
|
||||
|
||||
WriteKey(writer, "user_id");
|
||||
writer.String(userId);
|
||||
}
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
216
libraries/discordrpc/src/serialization.h
Normal file
216
libraries/discordrpc/src/serialization.h
Normal file
|
@ -0,0 +1,216 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef __MINGW32__
|
||||
#pragma warning(push)
|
||||
|
||||
#pragma warning(disable : 4061) // enum is not explicitly handled by a case label
|
||||
#pragma warning(disable : 4365) // signed/unsigned mismatch
|
||||
#pragma warning(disable : 4464) // relative include path contains
|
||||
#pragma warning(disable : 4668) // is not defined as a preprocessor macro
|
||||
#pragma warning(disable : 6313) // Incorrect operator
|
||||
#pragma warning(disable : 5045) // Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified
|
||||
#endif // __MINGW32__
|
||||
|
||||
#include "rapidjson/document.h"
|
||||
#include "rapidjson/stringbuffer.h"
|
||||
#include "rapidjson/writer.h"
|
||||
|
||||
#ifndef __MINGW32__
|
||||
#pragma warning(pop)
|
||||
#endif // __MINGW32__
|
||||
|
||||
// if only there was a standard library function for this
|
||||
template <size_t Len>
|
||||
inline size_t StringCopy(char (&dest)[Len], const char* src)
|
||||
{
|
||||
if (!src || !Len) {
|
||||
return 0;
|
||||
}
|
||||
size_t copied;
|
||||
char* out = dest;
|
||||
for (copied = 1; *src && copied < Len; ++copied) {
|
||||
*out++ = *src++;
|
||||
}
|
||||
*out = 0;
|
||||
return copied - 1;
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char* dest, size_t maxLen, int version, const char* applicationId);
|
||||
|
||||
// Commands
|
||||
struct DiscordRichPresence;
|
||||
size_t JsonWriteRichPresenceObj(char* dest,
|
||||
size_t maxLen,
|
||||
int nonce,
|
||||
int pid,
|
||||
const DiscordRichPresence* presence);
|
||||
size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName);
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName);
|
||||
|
||||
size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce);
|
||||
|
||||
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
|
||||
// to supply some of your own allocators for stuff rather than use the defaults
|
||||
|
||||
class LinearAllocator {
|
||||
public:
|
||||
char* buffer_;
|
||||
char* end_;
|
||||
LinearAllocator()
|
||||
{
|
||||
assert(0); // needed for some default case in rapidjson, should not use
|
||||
}
|
||||
LinearAllocator(char* buffer, size_t size)
|
||||
: buffer_(buffer)
|
||||
, end_(buffer + size)
|
||||
{
|
||||
}
|
||||
static const bool kNeedFree = false;
|
||||
void* Malloc(size_t size)
|
||||
{
|
||||
char* res = buffer_;
|
||||
buffer_ += size;
|
||||
if (buffer_ > end_) {
|
||||
buffer_ = res;
|
||||
return nullptr;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
void* Realloc(void* originalPtr, size_t originalSize, size_t newSize)
|
||||
{
|
||||
if (newSize == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// allocate how much you need in the first place
|
||||
assert(!originalPtr && !originalSize);
|
||||
// unused parameter warning
|
||||
(void)(originalPtr);
|
||||
(void)(originalSize);
|
||||
return Malloc(newSize);
|
||||
}
|
||||
static void Free(void* ptr)
|
||||
{
|
||||
/* shrug */
|
||||
(void)ptr;
|
||||
}
|
||||
};
|
||||
|
||||
template <size_t Size>
|
||||
class FixedLinearAllocator : public LinearAllocator {
|
||||
public:
|
||||
char fixedBuffer_[Size];
|
||||
FixedLinearAllocator()
|
||||
: LinearAllocator(fixedBuffer_, Size)
|
||||
{
|
||||
}
|
||||
static const bool kNeedFree = false;
|
||||
};
|
||||
|
||||
// wonder why this isn't a thing already, maybe I missed it
|
||||
class DirectStringBuffer {
|
||||
public:
|
||||
using Ch = char;
|
||||
char* buffer_;
|
||||
char* end_;
|
||||
char* current_;
|
||||
|
||||
DirectStringBuffer(char* buffer, size_t maxLen)
|
||||
: buffer_(buffer)
|
||||
, end_(buffer + maxLen)
|
||||
, current_(buffer)
|
||||
{
|
||||
}
|
||||
|
||||
void Put(char c)
|
||||
{
|
||||
if (current_ < end_) {
|
||||
*current_++ = c;
|
||||
}
|
||||
}
|
||||
void Flush() {}
|
||||
size_t GetSize() const { return (size_t)(current_ - buffer_); }
|
||||
};
|
||||
|
||||
using MallocAllocator = rapidjson::CrtAllocator;
|
||||
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
|
||||
using UTF8 = rapidjson::UTF8<char>;
|
||||
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
|
||||
using StackAllocator = FixedLinearAllocator<2048>;
|
||||
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
|
||||
using JsonWriterBase =
|
||||
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
|
||||
class JsonWriter : public JsonWriterBase {
|
||||
public:
|
||||
DirectStringBuffer stringBuffer_;
|
||||
StackAllocator stackAlloc_;
|
||||
|
||||
JsonWriter(char* dest, size_t maxLen)
|
||||
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels)
|
||||
, stringBuffer_(dest, maxLen)
|
||||
, stackAlloc_()
|
||||
{
|
||||
}
|
||||
|
||||
size_t Size() const { return stringBuffer_.GetSize(); }
|
||||
};
|
||||
|
||||
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
|
||||
class JsonDocument : public JsonDocumentBase {
|
||||
public:
|
||||
static const int kDefaultChunkCapacity = 32 * 1024;
|
||||
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
|
||||
// send any messages that would use all of this, though.
|
||||
char parseBuffer_[32 * 1024];
|
||||
MallocAllocator mallocAllocator_;
|
||||
PoolAllocator poolAllocator_;
|
||||
StackAllocator stackAllocator_;
|
||||
JsonDocument()
|
||||
: JsonDocumentBase(rapidjson::kObjectType,
|
||||
&poolAllocator_,
|
||||
sizeof(stackAllocator_.fixedBuffer_),
|
||||
&stackAllocator_)
|
||||
, poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_)
|
||||
, stackAllocator_()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
|
||||
|
||||
inline JsonValue* GetObjMember(JsonValue* obj, const char* name)
|
||||
{
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsObject()) {
|
||||
return &member->value;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline int GetIntMember(JsonValue* obj, const char* name, int notFoundDefault = 0)
|
||||
{
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsInt()) {
|
||||
return member->value.GetInt();
|
||||
}
|
||||
}
|
||||
return notFoundDefault;
|
||||
}
|
||||
|
||||
inline const char* GetStrMember(JsonValue* obj,
|
||||
const char* name,
|
||||
const char* notFoundDefault = nullptr)
|
||||
{
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsString()) {
|
||||
return member->value.GetString();
|
||||
}
|
||||
}
|
||||
return notFoundDefault;
|
||||
}
|
|
@ -377,7 +377,7 @@ add_custom_target( revision_check ALL
|
|||
# required libraries
|
||||
|
||||
|
||||
set( PROJECT_LIBRARIES ${PROJECT_LIBRARIES} "${ZLIB_LIBRARIES}" "${JPEG_LIBRARIES}" "${BZIP2_LIBRARIES}" "${CMAKE_DL_LIBS}" "${TESS_LIBRARIES}" )
|
||||
set( PROJECT_LIBRARIES ${PROJECT_LIBRARIES} "${ZLIB_LIBRARIES}" "${JPEG_LIBRARIES}" "${BZIP2_LIBRARIES}" "${CMAKE_DL_LIBS}" "${TESS_LIBRARIES}" "${DRPC_LIBRARIES}")
|
||||
if (HAVE_VULKAN)
|
||||
set( PROJECT_LIBRARIES ${PROJECT_LIBRARIES} "glslang" "SPIRV" "OGLCompiler")
|
||||
endif()
|
||||
|
@ -436,7 +436,7 @@ else()
|
|||
endif()
|
||||
|
||||
|
||||
include_directories( "${ZLIB_INCLUDE_DIR}" "${ZMUSIC_INCLUDE_DIR}" "${BZIP2_INCLUDE_DIR}" "${LZMA_INCLUDE_DIR}" "${JPEG_INCLUDE_DIR}" "${GDTOA_INCLUDE_DIR}" "${TESS_INCLUDE_DIR}" )
|
||||
include_directories( "${ZLIB_INCLUDE_DIR}" "${ZMUSIC_INCLUDE_DIR}" "${BZIP2_INCLUDE_DIR}" "${LZMA_INCLUDE_DIR}" "${JPEG_INCLUDE_DIR}" "${GDTOA_INCLUDE_DIR}" "${TESS_INCLUDE_DIR}" "${DRPC_INCLUDE_DIR}" )
|
||||
|
||||
if (WIN32)
|
||||
include_directories( "platform/win32" )
|
||||
|
@ -1002,6 +1002,8 @@ set (FASTMATH_SOURCES ${FASTMATH_SOURCES})
|
|||
|
||||
set (PCH_SOURCES
|
||||
|
||||
common/thirdparty/richpresence.cpp
|
||||
|
||||
glbackend/glbackend.cpp
|
||||
glbackend/gl_texture.cpp
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ struct FStartupInfo
|
|||
FString Song;
|
||||
FString con;
|
||||
FString def;
|
||||
FString DiscordAppId = nullptr;
|
||||
FString SteamAppId = nullptr;
|
||||
int Type;
|
||||
int LoadLights = -1;
|
||||
int LoadBrightmaps = -1;
|
||||
|
|
128
source/common/thirdparty/richpresence.cpp
vendored
Normal file
128
source/common/thirdparty/richpresence.cpp
vendored
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
** richpresence.cpp
|
||||
**
|
||||
** handles rich presence for Discord. does nothing but transmit the appid,
|
||||
** game, and current level.
|
||||
**
|
||||
**---------------------------------------------------------------------------
|
||||
** Copyright 2022 Rachael Alexanderson
|
||||
** Copyright 2017 Discord
|
||||
** All rights reserved.
|
||||
**
|
||||
** Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions
|
||||
** are met:
|
||||
**
|
||||
** 1. Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** 2. Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in the
|
||||
** documentation and/or other materials provided with the distribution.
|
||||
** 3. The name of the author may not be used to endorse or promote products
|
||||
** derived from this software without specific prior written permission.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
**---------------------------------------------------------------------------
|
||||
**
|
||||
*/
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include "common/engine/printf.h"
|
||||
#include "discord_rpc.h"
|
||||
#include "version.h"
|
||||
|
||||
static int64_t StartTime = 0;
|
||||
static bool didInit = false;
|
||||
|
||||
static void handleDiscordReady(const DiscordUser* connectedUser)
|
||||
{
|
||||
Printf("\nDiscord: connected to user %s#%s - %s\n",
|
||||
connectedUser->username,
|
||||
connectedUser->discriminator,
|
||||
connectedUser->userId);
|
||||
}
|
||||
|
||||
static void handleDiscordDisconnected(int errcode, const char* message)
|
||||
{
|
||||
Printf("\nDiscord: disconnected (%d: %s)\n", errcode, message);
|
||||
}
|
||||
|
||||
static void handleDiscordError(int errcode, const char* message)
|
||||
{
|
||||
Printf("\nDiscord: error (%d: %s)\n", errcode, message);
|
||||
}
|
||||
|
||||
static void handleDiscordSpectate(const char* secret)
|
||||
{
|
||||
Printf("\nDiscord: spectate (%s)\n", secret);
|
||||
}
|
||||
|
||||
static void handleDiscordJoin(const char* secret)
|
||||
{
|
||||
Printf("\nDiscord: join (%s)\n", secret);
|
||||
}
|
||||
|
||||
static void handleDiscordJoinRequest(const DiscordUser* request)
|
||||
{
|
||||
// we can't join in-game
|
||||
int response = DISCORD_REPLY_NO;
|
||||
Discord_Respond(request->userId, response);
|
||||
Printf("\nDiscord: join request from %s#%s - %s\n",
|
||||
request->username,
|
||||
request->discriminator,
|
||||
request->userId);
|
||||
}
|
||||
|
||||
void I_UpdateDiscordPresence(bool SendPresence, const char* curstatus, const char* appid, const char* steamappid)
|
||||
{
|
||||
const char* curappid = DEFAULT_DISCORD_APP_ID;
|
||||
|
||||
if (appid && appid[0] != '\0')
|
||||
curappid = appid;
|
||||
|
||||
if (!didInit && !SendPresence)
|
||||
return; // we haven't initted, there's nothing to do here, just exit
|
||||
|
||||
if (!didInit)
|
||||
{
|
||||
didInit = true;
|
||||
DiscordEventHandlers handlers;
|
||||
memset(&handlers, 0, sizeof(handlers));
|
||||
handlers.ready = handleDiscordReady;
|
||||
handlers.disconnected = handleDiscordDisconnected;
|
||||
handlers.errored = handleDiscordError;
|
||||
handlers.joinGame = handleDiscordJoin;
|
||||
handlers.spectateGame = handleDiscordSpectate;
|
||||
handlers.joinRequest = handleDiscordJoinRequest;
|
||||
Discord_Initialize(curappid, &handlers, 1, steamappid);
|
||||
}
|
||||
|
||||
if (SendPresence)
|
||||
{
|
||||
DiscordRichPresence discordPresence;
|
||||
memset(&discordPresence, 0, sizeof(discordPresence));
|
||||
discordPresence.state = "Playing";
|
||||
if (!StartTime)
|
||||
StartTime = time(0);
|
||||
discordPresence.startTimestamp = StartTime;
|
||||
discordPresence.details = curstatus;
|
||||
discordPresence.largeImageKey = "game-image";
|
||||
discordPresence.instance = 0;
|
||||
Discord_UpdatePresence(&discordPresence);
|
||||
}
|
||||
else
|
||||
{
|
||||
Discord_ClearPresence();
|
||||
}
|
||||
}
|
||||
|
|
@ -109,6 +109,17 @@ CUSTOM_CVAR(Int, mouse_capturemode, 1, CVAR_GLOBALCONFIG | CVAR_ARCHIVE)
|
|||
}
|
||||
}
|
||||
|
||||
void I_UpdateWindowTitle();
|
||||
|
||||
CUSTOM_CVAR (Bool, i_discordrpc, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
|
||||
{
|
||||
I_UpdateWindowTitle();
|
||||
}
|
||||
CUSTOM_CVAR(Int, I_FriendlyWindowTitle, 1, CVAR_GLOBALCONFIG|CVAR_ARCHIVE|CVAR_NOINITCALL)
|
||||
{
|
||||
I_UpdateWindowTitle();
|
||||
}
|
||||
|
||||
// The last remains of sdlayer.cpp
|
||||
GameInterface* gi;
|
||||
int myconnectindex, numplayers;
|
||||
|
@ -149,6 +160,7 @@ void LoadVoxelModels();
|
|||
void MarkMap();
|
||||
void BuildFogTable();
|
||||
void ParseGLDefs();
|
||||
void I_UpdateDiscordPresence(bool SendPresence, const char* curstatus, const char* appid, const char* steamappid);
|
||||
|
||||
DStatusBarCore* StatusBar;
|
||||
|
||||
|
@ -1080,7 +1092,7 @@ int RunGame()
|
|||
I_FatalError("No palette found.");
|
||||
|
||||
FMaterial::SetLayerCallback(setpalettelayer);
|
||||
if (GameStartupInfo.Name.IsNotEmpty()) I_SetWindowTitle(GameStartupInfo.Name);
|
||||
I_UpdateWindowTitle();
|
||||
DeleteStartupScreen();
|
||||
|
||||
V_Init2();
|
||||
|
@ -1661,3 +1673,59 @@ void InitBuildTiles()
|
|||
// need to find a better way to handle this thing.
|
||||
}
|
||||
|
||||
static FString LevelName;
|
||||
|
||||
void I_UpdateWindowTitle()
|
||||
{
|
||||
FString titlestr;
|
||||
if (!(GameStartupInfo.Name.IsNotEmpty()))
|
||||
return;
|
||||
switch (I_FriendlyWindowTitle)
|
||||
{
|
||||
case 1:
|
||||
if (LevelName.IsNotEmpty())
|
||||
{
|
||||
titlestr.Format("%s - %s", LevelName.GetChars(), GameStartupInfo.Name.GetChars());
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case 2:
|
||||
titlestr = GameStartupInfo.Name;
|
||||
break;
|
||||
default:
|
||||
I_UpdateDiscordPresence(false, NULL, GameStartupInfo.DiscordAppId.GetChars(), GameStartupInfo.SteamAppId.GetChars());
|
||||
I_SetWindowTitle(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip out any color escape sequences before setting a window title
|
||||
TArray<char> copy(titlestr.Len() + 1);
|
||||
const char* srcp = titlestr;
|
||||
char* dstp = copy.Data();
|
||||
|
||||
while (*srcp != 0)
|
||||
{
|
||||
|
||||
if (*srcp != TEXTCOLOR_ESCAPE)
|
||||
{
|
||||
*dstp++ = *srcp++;
|
||||
}
|
||||
else if (srcp[1] == '[')
|
||||
{
|
||||
srcp += 2;
|
||||
while (*srcp != ']' && *srcp != 0) srcp++;
|
||||
if (*srcp == ']') srcp++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (srcp[1] != 0) srcp += 2;
|
||||
else break;
|
||||
}
|
||||
}
|
||||
*dstp = 0;
|
||||
if (i_discordrpc)
|
||||
I_UpdateDiscordPresence(true, copy.Data(), GameStartupInfo.DiscordAppId.GetChars(), GameStartupInfo.SteamAppId.GetChars());
|
||||
else
|
||||
I_UpdateDiscordPresence(false, nullptr, nullptr, nullptr);
|
||||
I_SetWindowTitle(copy.Data());
|
||||
}
|
||||
|
|
|
@ -87,6 +87,8 @@ const char *GetVersionString();
|
|||
#define GAME_DIR ".config/" GAMENAMELOWERCASE
|
||||
#endif
|
||||
|
||||
#define DEFAULT_DISCORD_APP_ID "954282576464449556"
|
||||
|
||||
const int SAVEPICWIDTH = 240;
|
||||
const int SAVEPICHEIGHT = 180;
|
||||
const int VID_MIN_WIDTH = 640;
|
||||
|
|
Loading…
Reference in a new issue