diff options
author | Dawid Potocki <dawid@dawidpotocki.com> | 2024-03-05 14:09:27 +1300 |
---|---|---|
committer | Dawid Potocki <dawid@dawidpotocki.com> | 2024-03-05 20:34:15 +1300 |
commit | 063e15900bda8453fb0fc6751e78d064501ccbae (patch) | |
tree | a4cd5f01dbca33a262333aff10e1e035217a30c8 /externals/discord-rpc/src | |
parent | 537296095ab24eddcb196b5ef98004f91de9c8c2 (diff) |
Diffstat (limited to 'externals/discord-rpc/src')
-rw-r--r-- | externals/discord-rpc/src/CMakeLists.txt | 147 | ||||
-rw-r--r-- | externals/discord-rpc/src/backoff.h | 40 | ||||
-rw-r--r-- | externals/discord-rpc/src/connection.h | 19 | ||||
-rw-r--r-- | externals/discord-rpc/src/connection_unix.cpp | 125 | ||||
-rw-r--r-- | externals/discord-rpc/src/connection_win.cpp | 128 | ||||
-rw-r--r-- | externals/discord-rpc/src/discord_register_linux.cpp | 102 | ||||
-rw-r--r-- | externals/discord-rpc/src/discord_register_osx.m | 80 | ||||
-rw-r--r-- | externals/discord-rpc/src/discord_register_win.cpp | 186 | ||||
-rw-r--r-- | externals/discord-rpc/src/discord_rpc.cpp | 504 | ||||
-rw-r--r-- | externals/discord-rpc/src/dllmain.cpp | 8 | ||||
-rw-r--r-- | externals/discord-rpc/src/msg_queue.h | 36 | ||||
-rw-r--r-- | externals/discord-rpc/src/rpc_connection.cpp | 137 | ||||
-rw-r--r-- | externals/discord-rpc/src/rpc_connection.h | 59 | ||||
-rw-r--r-- | externals/discord-rpc/src/serialization.cpp | 250 | ||||
-rw-r--r-- | externals/discord-rpc/src/serialization.h | 215 |
15 files changed, 2036 insertions, 0 deletions
diff --git a/externals/discord-rpc/src/CMakeLists.txt b/externals/discord-rpc/src/CMakeLists.txt new file mode 100644 index 0000000000..290d761157 --- /dev/null +++ b/externals/discord-rpc/src/CMakeLists.txt @@ -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" +) diff --git a/externals/discord-rpc/src/backoff.h b/externals/discord-rpc/src/backoff.h new file mode 100644 index 0000000000..a3e736fb7b --- /dev/null +++ b/externals/discord-rpc/src/backoff.h @@ -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; + } +}; diff --git a/externals/discord-rpc/src/connection.h b/externals/discord-rpc/src/connection.h new file mode 100644 index 0000000000..a8f99b9f10 --- /dev/null +++ b/externals/discord-rpc/src/connection.h @@ -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); +}; diff --git a/externals/discord-rpc/src/connection_unix.cpp b/externals/discord-rpc/src/connection_unix.cpp new file mode 100644 index 0000000000..85dace3ccc --- /dev/null +++ b/externals/discord-rpc/src/connection_unix.cpp @@ -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; +} diff --git a/externals/discord-rpc/src/connection_win.cpp b/externals/discord-rpc/src/connection_win.cpp new file mode 100644 index 0000000000..2dd2750c06 --- /dev/null +++ b/externals/discord-rpc/src/connection_win.cpp @@ -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; +} diff --git a/externals/discord-rpc/src/discord_register_linux.cpp b/externals/discord-rpc/src/discord_register_linux.cpp new file mode 100644 index 0000000000..dd92eea0d4 --- /dev/null +++ b/externals/discord-rpc/src/discord_register_linux.cpp @@ -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); +} diff --git a/externals/discord-rpc/src/discord_register_osx.m b/externals/discord-rpc/src/discord_register_osx.m new file mode 100644 index 0000000000..d710102865 --- /dev/null +++ b/externals/discord-rpc/src/discord_register_osx.m @@ -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); +} diff --git a/externals/discord-rpc/src/discord_register_win.cpp b/externals/discord-rpc/src/discord_register_win.cpp new file mode 100644 index 0000000000..0b1c4a13da --- /dev/null +++ b/externals/discord-rpc/src/discord_register_win.cpp @@ -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); +} diff --git a/externals/discord-rpc/src/discord_rpc.cpp b/externals/discord-rpc/src/discord_rpc.cpp new file mode 100644 index 0000000000..03924538c3 --- /dev/null +++ b/externals/discord-rpc/src/discord_rpc.cpp @@ -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; +} diff --git a/externals/discord-rpc/src/dllmain.cpp b/externals/discord-rpc/src/dllmain.cpp new file mode 100644 index 0000000000..fbfc2950d7 --- /dev/null +++ b/externals/discord-rpc/src/dllmain.cpp @@ -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; +} diff --git a/externals/discord-rpc/src/msg_queue.h b/externals/discord-rpc/src/msg_queue.h new file mode 100644 index 0000000000..77f380e705 --- /dev/null +++ b/externals/discord-rpc/src/msg_queue.h @@ -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_; } +}; diff --git a/externals/discord-rpc/src/rpc_connection.cpp b/externals/discord-rpc/src/rpc_connection.cpp new file mode 100644 index 0000000000..0933162169 --- /dev/null +++ b/externals/discord-rpc/src/rpc_connection.cpp @@ -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; + } + } +} diff --git a/externals/discord-rpc/src/rpc_connection.h b/externals/discord-rpc/src/rpc_connection.h new file mode 100644 index 0000000000..bbdd05c792 --- /dev/null +++ b/externals/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/externals/discord-rpc/src/serialization.cpp b/externals/discord-rpc/src/serialization.cpp new file mode 100644 index 0000000000..70efa637f3 --- /dev/null +++ b/externals/discord-rpc/src/serialization.cpp @@ -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(); +} diff --git a/externals/discord-rpc/src/serialization.h b/externals/discord-rpc/src/serialization.h new file mode 100644 index 0000000000..9c462dc283 --- /dev/null +++ b/externals/discord-rpc/src/serialization.h @@ -0,0 +1,215 @@ +#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 +#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; +} |