diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cc80dc20..81227f3f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,6 +81,7 @@ jobs: gtest gmock sparsehash-devel + rapidjson-devel - name: Install kdsingleapplication-qt6-devel if: matrix.opensuse_version == 'tumbleweed' run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel @@ -206,6 +207,7 @@ jobs: gtest-devel gmock-devel sparsehash-devel + rapidjson-devel - name: Checkout uses: actions/checkout@v4 with: @@ -300,7 +302,8 @@ jobs: desktop-file-utils appstream appstream-util - hicolor-icon-theme + hicolor-icon-themepen + rapidjson - name: Remove files run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*} - name: Checkout @@ -396,6 +399,7 @@ jobs: appstream-util hicolor-icon-theme gtest + rapidjson - name: Build and install KDSingleApplication if: matrix.mageia_version == '9' run: | @@ -494,6 +498,7 @@ jobs: qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools + rapidjson-dev - name: Build and install KDSingleApplication run: | git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication @@ -585,6 +590,7 @@ jobs: qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools + rapidjson-dev - name: Build and install KDSingleApplication run: | git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication @@ -678,6 +684,7 @@ jobs: gstreamer1.0-alsa gstreamer1.0-pulseaudio libkdsingleapplication-qt6-dev + rapidjson-dev - name: Install keyboxd if: matrix.ubuntu_version == 'noble' env: @@ -730,7 +737,7 @@ jobs: with: usesh: true mem: 4096 - prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash + prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson run: | set -e git config --global --add safe.directory ${GITHUB_WORKSPACE} @@ -755,7 +762,7 @@ jobs: with: usesh: true mem: 4096 - prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash + prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash rapidjson run: | set -e export LDFLAGS="-L/usr/local/lib" diff --git a/thirdparty/discord-rpc/CMakeLists.txt b/thirdparty/discord-rpc/CMakeLists.txt new file mode 100644 index 000000000..92612bbc6 --- /dev/null +++ b/thirdparty/discord-rpc/CMakeLists.txt @@ -0,0 +1,3 @@ +find_package(RapidJSON REQUIRED) + +add_subdirectory(src) diff --git a/thirdparty/discord-rpc/LICENSE b/thirdparty/discord-rpc/LICENSE new file mode 100644 index 000000000..17fca3d50 --- /dev/null +++ b/thirdparty/discord-rpc/LICENSE @@ -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. diff --git a/thirdparty/discord-rpc/README.md b/thirdparty/discord-rpc/README.md new file mode 100644 index 000000000..60def3c3a --- /dev/null +++ b/thirdparty/discord-rpc/README.md @@ -0,0 +1,162 @@ +# Discord RPC + +## Fork Notice + +This library was slightly modified for Strawberry Music Player with some extra features from the new API and shared library support/more unnecessary components removed. The original repository is [here](https://github.com/discord/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 + mkdir build + cd build + cmake .. -DCMAKE_INSTALL_PREFIX= + 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) | diff --git a/thirdparty/discord-rpc/include/discord_register.h b/thirdparty/discord-rpc/include/discord_register.h new file mode 100644 index 000000000..4eb3c3caf --- /dev/null +++ b/thirdparty/discord-rpc/include/discord_register.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void Discord_Register(const char* applicationId, const char* command); +void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); + +#ifdef __cplusplus +} +#endif diff --git a/thirdparty/discord-rpc/include/discord_rpc.h b/thirdparty/discord-rpc/include/discord_rpc.h new file mode 100644 index 000000000..f47709a17 --- /dev/null +++ b/thirdparty/discord-rpc/include/discord_rpc.h @@ -0,0 +1,73 @@ +#pragma once +#include + +// clang-format off + +// clang-format on + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct DiscordRichPresence { + int type; + const char* name; /* max 128 bytes */ + 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 + +void Discord_Initialize(const char* applicationId, + DiscordEventHandlers* handlers, + int autoRegister, + const char* optionalSteamId); +void Discord_Shutdown(void); + +/* checks for incoming messages, dispatches callbacks */ +void Discord_RunCallbacks(void); + +void Discord_UpdatePresence(const DiscordRichPresence* presence); +void Discord_ClearPresence(void); + +void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); + +void Discord_UpdateHandlers(DiscordEventHandlers* handlers); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/thirdparty/discord-rpc/src/CMakeLists.txt b/thirdparty/discord-rpc/src/CMakeLists.txt new file mode 100644 index 000000000..33da8f28b --- /dev/null +++ b/thirdparty/discord-rpc/src/CMakeLists.txt @@ -0,0 +1,87 @@ +include_directories(${PROJECT_SOURCE_DIR}/include) + +set(CMAKE_CXX_STANDARD 17) + +set(BASE_RPC_SRC + ../include/discord_rpc.h + discord_rpc.cpp + ../include/discord_register.h + rpc_connection.h + rpc_connection.cpp + serialization.h + serialization.cpp + connection.h + backoff.h + msg_queue.h +) + +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 + -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 (APPLE) + target_link_libraries(discord-rpc PRIVATE "-framework AppKit") + endif (APPLE) +endif(UNIX) + +target_include_directories(discord-rpc PRIVATE ../include ${RAPIDJSON_INCLUDE_DIRS}) diff --git a/thirdparty/discord-rpc/src/backoff.h b/thirdparty/discord-rpc/src/backoff.h new file mode 100644 index 000000000..a3e736fb7 --- /dev/null +++ b/thirdparty/discord-rpc/src/backoff.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +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; + } +}; diff --git a/thirdparty/discord-rpc/src/connection.h b/thirdparty/discord-rpc/src/connection.h new file mode 100644 index 000000000..30f851a8e --- /dev/null +++ b/thirdparty/discord-rpc/src/connection.h @@ -0,0 +1,19 @@ +#pragma once + +// This is to wrap the platform specific kinds of connect/read/write. + +#include +#include + +// 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); +}; diff --git a/thirdparty/discord-rpc/src/connection_unix.cpp b/thirdparty/discord-rpc/src/connection_unix.cpp new file mode 100644 index 000000000..0c72a6072 --- /dev/null +++ b/thirdparty/discord-rpc/src/connection_unix.cpp @@ -0,0 +1,117 @@ +#include "connection.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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(c); + self->Close(); + c = nullptr; +} + +bool BaseConnection::Open() { + const char *tempPath = GetTempPath(); + auto self = reinterpret_cast(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(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(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(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; +} diff --git a/thirdparty/discord-rpc/src/connection_win.cpp b/thirdparty/discord-rpc/src/connection_win.cpp new file mode 100644 index 000000000..ab216479e --- /dev/null +++ b/thirdparty/discord-rpc/src/connection_win.cpp @@ -0,0 +1,121 @@ +#include "connection.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOSERVICE +#define NOIME +#include +#include + +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(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(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(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(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(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; +} diff --git a/thirdparty/discord-rpc/src/discord_register_linux.cpp b/thirdparty/discord-rpc/src/discord_register_linux.cpp new file mode 100644 index 000000000..65028221f --- /dev/null +++ b/thirdparty/discord-rpc/src/discord_register_linux.cpp @@ -0,0 +1,99 @@ +#include "discord_rpc.h" +#include "discord_register.h" +#include + +#include +#include +#include +#include +#include +#include + +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-:// +extern "C" void Discord_Register(const char *applicationId, const char *command) { + // Add a desktop file and update some mime handlers so that xdg-open does the right thing. + + 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" void Discord_RegisterSteamGame(const char *applicationId, + const char *steamId) { + char command[256]; + sprintf(command, "xdg-open steam://rungameid/%s", steamId); + Discord_Register(applicationId, command); +} diff --git a/thirdparty/discord-rpc/src/discord_register_osx.m b/thirdparty/discord-rpc/src/discord_register_osx.m new file mode 100644 index 000000000..d71010286 --- /dev/null +++ b/thirdparty/discord-rpc/src/discord_register_osx.m @@ -0,0 +1,80 @@ +#include +#include + +#import + +#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); +} diff --git a/thirdparty/discord-rpc/src/discord_register_win.cpp b/thirdparty/discord-rpc/src/discord_register_win.cpp new file mode 100644 index 000000000..a5f121505 --- /dev/null +++ b/thirdparty/discord-rpc/src/discord_register_win.cpp @@ -0,0 +1,181 @@ +#include "discord_rpc.h" +#include "discord_register.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOSERVICE +#define NOIME +#include +#include +#include + +/** + * 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 substitutes + * 2) RegSetKeyValueW and LSTATUS are not declared in + * The entire function is rewritten + */ +#ifdef __MINGW32__ +# include +/// 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 +# include +#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-:// + // 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" 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" 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); +} diff --git a/thirdparty/discord-rpc/src/discord_rpc.cpp b/thirdparty/discord-rpc/src/discord_rpc.cpp new file mode 100644 index 000000000..c8cd27eac --- /dev/null +++ b/thirdparty/discord-rpc/src/discord_rpc.cpp @@ -0,0 +1,474 @@ +#include "discord_rpc.h" + +#include "backoff.h" +#include "discord_register.h" +#include "msg_queue.h" +#include "rpc_connection.h" +#include "serialization.h" + +#include +#include +#include + +#include +#include + +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 SendQueue; +static MsgQueue 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 }; + +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 maxWait { 500LL }; + Discord_UpdateConnection(); + while (keepRunning.load()) { + std::unique_lock 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(); } +}; +static IoThreadHolder *IoThread { nullptr }; + +static void UpdateReconnectTime() { + NextConnect = std::chrono::system_clock::now() + + std::chrono::duration { ReconnectTimeMs.nextDelay() }; +} + +static void Discord_UpdateConnection(void) { + 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 guard(PresenceMutex); + local.Copy(QueuedPresence); + } + if (!Connection->Write(local.buffer, local.length)) { + // if we fail to send, requeue + std::lock_guard 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" 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 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" 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" void Discord_UpdatePresence(const DiscordRichPresence *presence) { + { + std::lock_guard guard(PresenceMutex); + QueuedPresence.length = JsonWriteRichPresenceObj( + QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence); + UpdatePresence.exchange(true); + } + SignalIOActivity(); +} + +extern "C" void Discord_ClearPresence(void) { + Discord_UpdatePresence(nullptr); +} + +extern "C" 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" 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 guard(HandlerMutex); + if (wasDisconnected && Handlers.disconnected) { + Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); + } + } + + if (WasJustConnected.exchange(false)) { + std::lock_guard 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 guard(HandlerMutex); + if (Handlers.errored) { + Handlers.errored(LastErrorCode, LastErrorMessage); + } + } + + if (WasJoinGame.exchange(false)) { + std::lock_guard guard(HandlerMutex); + if (Handlers.joinGame) { + Handlers.joinGame(JoinGameSecret); + } + } + + if (WasSpectateGame.exchange(false)) { + std::lock_guard 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 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 guard(HandlerMutex); + if (wasDisconnected && Handlers.disconnected) { + Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); + } + } +} + +extern "C" 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 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 guard(HandlerMutex); + Handlers = {}; + } + return; +} diff --git a/thirdparty/discord-rpc/src/msg_queue.h b/thirdparty/discord-rpc/src/msg_queue.h new file mode 100644 index 000000000..77f380e70 --- /dev/null +++ b/thirdparty/discord-rpc/src/msg_queue.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +// 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 +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_; } +}; diff --git a/thirdparty/discord-rpc/src/rpc_connection.cpp b/thirdparty/discord-rpc/src/rpc_connection.cpp new file mode 100644 index 000000000..92b767ab6 --- /dev/null +++ b/thirdparty/discord-rpc/src/rpc_connection.cpp @@ -0,0 +1,131 @@ +#include "rpc_connection.h" +#include "serialization.h" + +#include + +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; + } + } +} diff --git a/thirdparty/discord-rpc/src/rpc_connection.h b/thirdparty/discord-rpc/src/rpc_connection.h new file mode 100644 index 000000000..fd7d05130 --- /dev/null +++ b/thirdparty/discord-rpc/src/rpc_connection.h @@ -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); +}; diff --git a/thirdparty/discord-rpc/src/serialization.cpp b/thirdparty/discord-rpc/src/serialization.cpp new file mode 100644 index 000000000..1cd3a4645 --- /dev/null +++ b/thirdparty/discord-rpc/src/serialization.cpp @@ -0,0 +1,244 @@ +#include "serialization.h" +#include "connection.h" +#include "discord_rpc.h" + +template +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 +void WriteKey(JsonWriter &w, T &k) { + w.Key(k, sizeof(T) - 1); +} + +struct WriteObject { + JsonWriter &writer; + WriteObject(JsonWriter &w) + : writer(w) { + writer.StartObject(); + } + template + WriteObject(JsonWriter &w, T &name) + : writer(w) { + WriteKey(writer, name); + writer.StartObject(); + } + ~WriteObject() { writer.EndObject(); } +}; + +struct WriteArray { + JsonWriter &writer; + template + WriteArray(JsonWriter &w, T &name) + : writer(w) { + WriteKey(writer, name); + writer.StartArray(); + } + ~WriteArray() { writer.EndArray(); } +}; + +template +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"); + + if (presence->type >= 0 && presence->type <= 5) { + WriteKey(writer, "type"); + writer.Int(presence->type); + } + + WriteOptionalString(writer, "name", presence->name); + 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(); +} diff --git a/thirdparty/discord-rpc/src/serialization.h b/thirdparty/discord-rpc/src/serialization.h new file mode 100644 index 000000000..3b461c54a --- /dev/null +++ b/thirdparty/discord-rpc/src/serialization.h @@ -0,0 +1,195 @@ +#pragma once + +#include + +#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 +#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 +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 +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; +using UTF8 = rapidjson::UTF8; +// 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; +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; +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; + +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; +}