20 Commits

Author SHA1 Message Date
7dc663a170 Static link crt 2017-11-09 14:24:30 -08:00
f872b4e49c fix event names and add ACTIVITY_JOIN_REQUEST (#10)
* fix event names and add ACTIVITY_JOIN_REQUEST

* Update hard-mode.md

* fix typo
2017-11-09 13:32:17 -08:00
ca5d70a5f9 Add more -Wflags 2017-11-09 13:08:05 -08:00
ee9c504d1c Change -Weverything to -Wall for more compilers 2017-11-09 13:08:05 -08:00
127eadcb89 Added VS2015 C++ redist dependency info 2017-11-07 09:59:04 -08:00
a7808a20ed Fix some sizes on join request strings. 2017-11-03 13:40:30 -07:00
3bdb88d918 Unity ajt fix 2017-11-02 11:59:45 -07:00
aa79c70bf9 Adding MIT license. 2017-11-01 15:25:14 -07:00
a089aab53e Update unity. 2017-10-17 13:34:03 -07:00
dafd85c39f Rename avatarUrl -> avatar for API consistency 2017-10-17 13:34:03 -07:00
b1d6a7c0fc Normalize white-space 2017-10-17 09:37:56 -07:00
e4b3ef63b7 Added request to join functionality 2017-10-17 09:37:56 -07:00
86ca320cb9 These should have been extern C. 2017-10-13 10:02:12 -07:00
6cdc830544 Missed a couple wanrings. Build example dll 2017-10-12 16:14:16 -07:00
990c8d4be6 Enable warnings, clang edition. 2017-10-12 16:08:08 -07:00
6fa00223ad Enable all warnings, turn the noisy ones back off, fix the others. 2017-10-12 15:39:31 -07:00
39ff0bf3e4 Ask to join (#4)
This adds a new callback `joinRequest` and a new API function `Discord_Respond` to reply to it.
2017-10-12 13:06:55 -07:00
25b6f1dcde Miscellaneous Unity Fixes (#3)
* Use simplified attribute names and ensure function calls are made with CallingConvention.Cdecl

* Remove unused imports
2017-09-28 08:15:15 -07:00
e7cdfaa64e I knew I shouldn't have checked in this dll 2017-09-21 14:04:46 -07:00
a5a56bcf68 Also do registering on OSX and Linux. 2017-09-14 08:59:32 -07:00
25 changed files with 659 additions and 123 deletions

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright 2017 Discord, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -24,6 +24,8 @@ function.
Download a release package, extract it, add `/include` to your compile includes, `/lib` to your
linker paths, and link with `discord-rpc`.
Note that the release packages were compiled using Visual Studio 2015, so the [Visual C++ Redistributable for VS2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145) will be a requirement for your game. If you wish to avoid this dependency, you should compile the libraries yourself using whatever dependencies are already in your game.
### From repo
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for

9
build.py Normal file → Executable file
View File

@ -1,3 +1,5 @@
#!/usr/bin/env python
import click
import os
import subprocess
@ -45,7 +47,7 @@ def build_lib(build_name, generator, options):
def create_archive():
archive_file_path = os.path.join(SCRIPT_PATH, 'builds', 'discord-rpc.zip')
archive_file_path = os.path.join(SCRIPT_PATH, 'builds', 'discord-rpc-%s.zip' % sys.platform)
archive_file = zipfile.ZipFile(archive_file_path, 'w', zipfile.ZIP_DEFLATED)
archive_src_base_path = os.path.join(SCRIPT_PATH, 'builds', 'install')
archive_dst_base_path = 'discord-rpc'
@ -64,6 +66,8 @@ def main(clean):
if clean:
shutil.rmtree('builds', ignore_errors=True)
mkdir_p('builds')
if sys.platform.startswith('win'):
generator32 = 'Visual Studio 14 2015'
generator64 = 'Visual Studio 14 2015 Win64'
@ -79,6 +83,9 @@ def main(clean):
shutil.copy(src_dll, dst_dll)
dst_dll = os.path.join(SCRIPT_PATH, 'examples', 'unrealstatus', 'Plugins', 'discordrpc', 'Binaries', 'ThirdParty', 'discordrpcLibrary', 'Win64', 'discord-rpc.dll')
shutil.copy(src_dll, dst_dll)
elif sys.platform == 'darwin':
build_lib('osx-static', None, {})
build_lib('osx-dynamic', None, {'BUILD_DYNAMIC_LIB': True})
create_archive()

View File

@ -10,7 +10,7 @@ By committing to an RPC-only integration, you decide to forego the work our libr
## Application Protocol Registration
One thing that cannot be explicitly done over RPC is registering an application protocol for your game. If you choose to do an RPC-only implementation, you will have to register your application protocol yourself in the format of `discord-[your_app_id]://`. You can use `Discord_Register()` from `src/discord-register.cpp` as a good example of how to properly register an application protocol for use with Discord.
One thing that cannot be explicitly done over RPC is registering an application protocol for your game. If you choose to do an RPC-only implementation, you will have to register your application protocol yourself in the format of `discord-[your_app_id]://`. You can use `Discord_Register()` as a good(?) example of how to properly register an application protocol for use with Discord. For OSX and Linux it is probably simpler to handle the protocol registration as part of your install/packaging.
## New RPC Command
@ -56,7 +56,9 @@ Below is a full example of a `SET_ACTIVITY` command. Field restrictions like siz
## New RPC Events
The two new RPC events for Rich Presence power the ability to join and spectate your friends' games. First is the `GAME_JOIN` event:
The three new RPC events for Rich Presence power the ability to join and spectate your friends' games.
First is the `ACTIVITY_JOIN` event:
```json
{
@ -64,11 +66,11 @@ The two new RPC events for Rich Presence power the ability to join and spectate
"data": {
"secret": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f"
},
"evnt": "GAME_JOIN"
"evnt": "ACTIVITY_JOIN"
}
```
And second is the `GAME_SPECTATE` event:
Second is the `ACTIVITY_SPECTATE` event:
```json
{
@ -76,7 +78,25 @@ And second is the `GAME_SPECTATE` event:
"data": {
"secret": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0"
},
"evnt": "GAME_SPECTATE"
"evnt": "ACTIVITY_SPECTATE"
}
```
And third is the `ACTIVITY_JOIN_REQUEST` event:
```json
{
"cmd": "DISPATCH",
"data": {
"user": {
"id": "53908232506183680",
"username": "Mason",
"discriminator": "1337",
"avatar": "a_bab14f271d565501444b2ca3be944b25"
},
"secret": "e459ca99273f59909dd16ed97865f3ad"
},
"evnt": "ACTIVITY_JOIN_REQUEST"
}
```
@ -85,7 +105,7 @@ In order to receive these events, you need to [subscribe](https://discordapp.com
```json
{
"nonce": "be9a6de3-31d0-4767-a8e9-4818c5690015",
"evt": "GAME_JOIN",
"evt": "ACTIVITY_JOIN",
"cmd": "SUBSCRIBE"
}
```
@ -93,7 +113,15 @@ In order to receive these events, you need to [subscribe](https://discordapp.com
```json
{
"nonce": "ae9qdde3-31d0-8989-a8e9-dnakwy174he",
"evt": "GAME_SPECTATE",
"evt": "ACTIVITY_SPECTATE",
"cmd": "SUBSCRIBE"
}
```
```json
{
"nonce": "5dc0c062-98c6-47a0-8922-bbb52e9d6afa",
"evt": "ACTIVITY_JOIN_REQUEST",
"cmd": "SUBSCRIBE"
}
```

View File

@ -1,8 +1,16 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine;
public class DiscordController : MonoBehaviour {
[System.Serializable]
public class DiscordJoinEvent : UnityEngine.Events.UnityEvent<string> { }
[System.Serializable]
public class DiscordSpectateEvent : UnityEngine.Events.UnityEvent<string> { }
[System.Serializable]
public class DiscordJoinRequestEvent : UnityEngine.Events.UnityEvent<DiscordRpc.JoinRequest> { }
public class DiscordController : MonoBehaviour
{
public DiscordRpc.RichPresence presence;
public string applicationId;
public string optionalSteamId;
@ -10,6 +18,9 @@ public class DiscordController : MonoBehaviour {
public int clickCounter;
public UnityEngine.Events.UnityEvent onConnect;
public UnityEngine.Events.UnityEvent onDisconnect;
public DiscordJoinEvent onJoin;
public DiscordJoinEvent onSpectate;
public DiscordJoinRequestEvent onJoinRequest;
DiscordRpc.EventHandlers handlers;
@ -47,18 +58,29 @@ public class DiscordController : MonoBehaviour {
{
++callbackCalls;
Debug.Log(string.Format("Discord: join ({0})", secret));
onJoin.Invoke(secret);
}
public void SpectateCallback(string secret)
{
++callbackCalls;
Debug.Log(string.Format("Discord: spectate ({0})", secret));
onSpectate.Invoke(secret);
}
void Start () {
public void RequestCallback(DiscordRpc.JoinRequest request)
{
++callbackCalls;
Debug.Log(string.Format("Discord: join request {0}: {1}", request.username, request.userId));
onJoinRequest.Invoke(request);
}
void Update () {
void Start()
{
}
void Update()
{
DiscordRpc.RunCallbacks();
}
@ -73,6 +95,7 @@ public class DiscordController : MonoBehaviour {
handlers.errorCallback += ErrorCallback;
handlers.joinCallback += JoinCallback;
handlers.spectateCallback += SpectateCallback;
handlers.requestCallback += RequestCallback;
DiscordRpc.Initialize(applicationId, ref handlers, true, optionalSteamId);
}

View File

@ -2,21 +2,24 @@
public class DiscordRpc
{
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ReadyCallback();
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void DisconnectedCallback(int errorCode, string message);
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ErrorCallback(int errorCode, string message);
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void JoinCallback(string secret);
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void SpectateCallback(string secret);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void RequestCallback(JoinRequest request);
public struct EventHandlers
{
public ReadyCallback readyCallback;
@ -24,6 +27,7 @@ public class DiscordRpc
public ErrorCallback errorCallback;
public JoinCallback joinCallback;
public SpectateCallback spectateCallback;
public RequestCallback requestCallback;
}
[System.Serializable]
@ -46,16 +50,34 @@ public class DiscordRpc
public bool instance;
}
[DllImport("discord-rpc", EntryPoint = "Discord_Initialize")]
[System.Serializable]
public struct JoinRequest
{
public string userId;
public string username;
public string avatar;
}
public enum Reply
{
No = 0,
Yes = 1,
Ignore = 2
}
[DllImport("discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)]
public static extern void Initialize(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId);
[DllImport("discord-rpc", EntryPoint = "Discord_Shutdown")]
[DllImport("discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)]
public static extern void Shutdown();
[DllImport("discord-rpc", EntryPoint = "Discord_RunCallbacks")]
[DllImport("discord-rpc", EntryPoint = "Discord_RunCallbacks", CallingConvention = CallingConvention.Cdecl)]
public static extern void RunCallbacks();
[DllImport("discord-rpc", EntryPoint = "Discord_UpdatePresence")]
[DllImport("discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)]
public static extern void UpdatePresence(ref RichPresence presence);
[DllImport("discord-rpc", EntryPoint = "Discord_Respond", CallingConvention = CallingConvention.Cdecl)]
public static extern void Respond(string userId, Reply reply);
}

View File

@ -703,6 +703,21 @@ MonoBehaviour:
m_CallState: 2
m_TypeName: UnityEngine.Events.UnityEvent, UnityEngine, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null
onJoin:
m_PersistentCalls:
m_Calls: []
m_TypeName: DiscordJoinEvent, Assembly-CSharp, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null
onSpectate:
m_PersistentCalls:
m_Calls: []
m_TypeName: DiscordJoinEvent, Assembly-CSharp, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null
onJoinRequest:
m_PersistentCalls:
m_Calls: []
m_TypeName: DiscordJoinRequestEvent, Assembly-CSharp, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null
--- !u!4 &1929635630
Transform:
m_ObjectHideFlags: 0

View File

@ -1 +1 @@
m_EditorVersion: 2017.1.0f3
m_EditorVersion: 2017.1.1f1

View File

@ -1,5 +1,13 @@
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(send-presence send-presence.c)
add_executable(
send-presence
MACOSX_BUNDLE
send-presence.c
)
set_target_properties(send-presence PROPERTIES
MACOSX_BUNDLE_BUNDLE_NAME "Send Presence"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.discordapp.examples.send-presence"
)
target_link_libraries(send-presence discord-rpc)
install(
@ -7,4 +15,7 @@ install(
RUNTIME
DESTINATION "bin"
CONFIGURATIONS Release
BUNDLE
DESTINATION "bin"
CONFIGURATIONS Release
)

View File

@ -15,6 +15,21 @@ static const char* APPLICATION_ID = "345229890980937739";
static int FrustrationLevel = 0;
static int64_t StartTime;
static int prompt(char* line, size_t size)
{
int res;
char* nl;
printf("\n> ");
fflush(stdout);
res = fgets(line, (int)size, stdin) ? 1 : 0;
line[size - 1] = 0;
nl = strchr(line, '\n');
if (nl) {
*nl = 0;
}
return res;
}
static void updateDiscordPresence()
{
char buffer[256];
@ -62,19 +77,37 @@ static void handleDiscordSpectate(const char* secret)
printf("\nDiscord: spectate (%s)\n", secret);
}
static int prompt(char* line, size_t size)
static void handleDiscordJoinRequest(const DiscordJoinRequest* request)
{
int res;
char* nl;
printf("\n> ");
fflush(stdout);
res = fgets(line, (int)size, stdin) ? 1 : 0;
line[size - 1] = 0;
nl = strchr(line, '\n');
if (nl) {
*nl = 0;
int response = -1;
char yn[4];
printf("\nDiscord: join request from %s - %s - %s\n",
request->username,
request->avatar,
request->userId);
do {
printf("Accept? (y/n)");
if (!prompt(yn, sizeof(yn))) {
break;
}
if (!yn[0]) {
continue;
}
if (yn[0] == 'y') {
response = DISCORD_REPLY_YES;
break;
}
if (yn[0] == 'n') {
response = DISCORD_REPLY_NO;
break;
}
} while (1);
if (response != -1) {
Discord_Respond(request->userId, response);
}
return res;
}
static void discordInit()
@ -86,6 +119,7 @@ static void discordInit()
handlers.errored = handleDiscordError;
handlers.joinGame = handleDiscordJoin;
handlers.spectateGame = handleDiscordSpectate;
handlers.joinRequest = handleDiscordJoinRequest;
Discord_Initialize(APPLICATION_ID, &handlers, 1, NULL);
}

View File

@ -41,30 +41,43 @@ typedef struct DiscordRichPresence {
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordJoinRequest {
const char* userId;
const char* username;
const char* avatar;
} DiscordJoinRequest;
typedef struct DiscordEventHandlers {
void (*ready)();
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 DiscordJoinRequest* request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId);
DISCORD_EXPORT void Discord_Shutdown();
DISCORD_EXPORT void Discord_Shutdown(void);
/* checks for incoming messages, dispatches callbacks */
DISCORD_EXPORT void Discord_RunCallbacks();
DISCORD_EXPORT void Discord_RunCallbacks(void);
/* If you disable the lib starting its own io thread, you'll need to call this from your own */
#ifdef DISCORD_DISABLE_IO_THREAD
DISCORD_EXPORT void Discord_UpdateConnection();
DISCORD_EXPORT void Discord_UpdateConnection(void);
#endif
DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence);
DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
#ifdef __cplusplus
} /* extern "C" */
#endif

View File

@ -6,32 +6,80 @@ option(BUILD_DYNAMIC_LIB "Build library as a DLL" OFF)
set(BASE_RPC_SRC
${PROJECT_SOURCE_DIR}/include/discord-rpc.h
discord-rpc.cpp
discord-register.h
discord-register.cpp
discord_register.h
rpc_connection.h
rpc_connection.cpp
serialization.h
serialization.cpp
connection.h
backoff.h
msg_queue.h
)
if (${BUILD_DYNAMIC_LIB})
set(RPC_LIBRARY_TYPE SHARED)
set(BASE_RPC_SRC ${BASE_RPC_SRC} dllmain.cpp)
if(WIN32)
set(BASE_RPC_SRC ${BASE_RPC_SRC} dllmain.cpp)
endif(WIN32)
else(${BUILD_DYNAMIC_LIB})
set(RPC_LIBRARY_TYPE STATIC)
endif(${BUILD_DYNAMIC_LIB})
if(WIN32)
add_library(discord-rpc ${RPC_LIBRARY_TYPE} ${BASE_RPC_SRC} connection_win.cpp)
target_compile_options(discord-rpc PRIVATE /W4)
add_definitions(-DDISCORD_WINDOWS)
set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_win.cpp discord_register_win.cpp)
add_library(discord-rpc ${RPC_LIBRARY_TYPE} ${BASE_RPC_SRC})
target_compile_options(discord-rpc PRIVATE /EHsc
/MT
/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(WIN32)
if(UNIX)
add_library(discord-rpc ${RPC_LIBRARY_TYPE} ${BASE_RPC_SRC} connection_unix.cpp)
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 ${RPC_LIBRARY_TYPE} ${BASE_RPC_SRC})
target_link_libraries(discord-rpc PUBLIC pthread)
target_compile_options(discord-rpc PRIVATE -g -Wall -std=c++14)
target_compile_options(discord-rpc PRIVATE
-g
-Wall
-Wextra
-Wpedantic
-Werror
-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
)
target_compile_options(discord-rpc PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-std=c++14>)
if (APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif (APPLE)
endif(UNIX)
target_include_directories(discord-rpc PRIVATE ${RAPIDJSON}/include)

View File

@ -20,7 +20,7 @@ struct Backoff {
, maxAmount(max)
, current(min)
, fails(0)
, randGenerator(time(0))
, randGenerator((uint64_t)time(0))
{
}

View File

@ -111,7 +111,7 @@ bool BaseConnection::Read(void* data, size_t length)
return false;
}
int res = recv(self->sock, data, length, MsgFlags);
int res = (int)recv(self->sock, data, length, MsgFlags);
if (res < 0) {
if (errno == EAGAIN) {
return false;

View File

@ -9,7 +9,7 @@
int GetProcessId()
{
return ::GetCurrentProcessId();
return (int)::GetCurrentProcessId();
}
struct BaseConnectionWin : public BaseConnection {

View File

@ -1,7 +1,8 @@
#include "discord-rpc.h"
#include "backoff.h"
#include "discord-register.h"
#include "discord_register.h"
#include "msg_queue.h"
#include "rpc_connection.h"
#include "serialization.h"
@ -16,6 +17,7 @@
constexpr size_t MaxMessageSize{16 * 1024};
constexpr size_t MessageQueueSize{8};
constexpr size_t JoinQueueSize{8};
struct QueuedMessage {
size_t length;
@ -30,6 +32,18 @@ struct QueuedMessage {
}
};
struct JoinRequest {
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21
char userId[22];
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129
char username[130];
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
char avatar[36];
// +1 on each because: it's even / I'm paranoid
};
static RpcConnection* Connection{nullptr};
static DiscordEventHandlers Handlers{};
static std::atomic_bool WasJustConnected{false};
@ -45,10 +59,9 @@ static int LastDisconnectErrorCode{0};
static char LastDisconnectErrorMessage[256];
static std::mutex PresenceMutex;
static QueuedMessage QueuedPresence{};
static QueuedMessage SendQueue[MessageQueueSize]{};
static std::atomic_uint SendQueueNextAdd{0};
static std::atomic_uint SendQueueNextSend{0};
static std::atomic_uint SendQueuePendingSends{0};
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<JoinRequest, JoinQueueSize> JoinAskQueue;
// 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);
@ -69,31 +82,11 @@ static void UpdateReconnectTime()
std::chrono::duration<int64_t, std::milli>{ReconnectTimeMs.nextDelay()};
}
static QueuedMessage* SendQueueGetNextAddMessage()
{
// if we are not connected, let's not batch up stale messages for later
if (!Connection || !Connection->IsOpen()) {
return nullptr;
}
// if we are falling behind, bail
if (SendQueuePendingSends.load() >= MessageQueueSize) {
return nullptr;
}
auto index = (SendQueueNextAdd++) % MessageQueueSize;
return &SendQueue[index];
}
static QueuedMessage* SendQueueGetNextSendMessage()
{
auto index = (SendQueueNextSend++) % MessageQueueSize;
return &SendQueue[index];
}
static void SendQueueCommitMessage()
{
SendQueuePendingSends++;
}
extern "C" void Discord_UpdateConnection()
#ifdef DISCORD_DISABLE_IO_THREAD
extern "C" DISCORD_EXPORT void Discord_UpdateConnection(void)
#else
static void Discord_UpdateConnection(void)
#endif
{
if (!Connection) {
return;
@ -134,7 +127,7 @@ extern "C" void Discord_UpdateConnection()
continue;
}
if (strcmp(evtName, "GAME_JOIN") == 0) {
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
auto data = GetObjMember(&message, "data");
auto secret = GetStrMember(data, "secret");
if (secret) {
@ -142,7 +135,7 @@ extern "C" void Discord_UpdateConnection()
WasJoinGame.store(true);
}
}
else if (strcmp(evtName, "GAME_SPECTATE") == 0) {
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
auto data = GetObjMember(&message, "data");
auto secret = GetStrMember(data, "secret");
if (secret) {
@ -150,6 +143,25 @@ extern "C" void Discord_UpdateConnection()
WasSpectateGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
auto data = GetObjMember(&message, "data");
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);
if (avatar) {
StringCopy(joinReq->avatar, avatar);
}
else {
joinReq->avatar[0] = 0;
}
JoinAskQueue.CommitAdd();
}
}
}
}
@ -168,16 +180,16 @@ extern "C" void Discord_UpdateConnection()
}
}
while (SendQueuePendingSends.load()) {
auto qmessage = SendQueueGetNextSendMessage();
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
--SendQueuePendingSends;
SendQueue.CommitSend();
}
}
}
#ifndef DISCORD_DISABLE_IO_THREAD
void DiscordRpcIo()
static void DiscordRpcIo(void)
{
const std::chrono::duration<int64_t, std::milli> maxWait{500LL};
@ -190,30 +202,30 @@ void DiscordRpcIo()
}
#endif
void SignalIOActivity()
static void SignalIOActivity()
{
#ifndef DISCORD_DISABLE_IO_THREAD
WaitForIOActivity.notify_all();
#endif
}
bool RegisterForEvent(const char* evtName)
static bool RegisterForEvent(const char* evtName)
{
auto qmessage = SendQueueGetNextAddMessage();
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length =
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueueCommitMessage();
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
extern "C" void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId)
extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId)
{
if (autoRegister) {
if (optionalSteamId && optionalSteamId[0]) {
@ -243,11 +255,15 @@ extern "C" void Discord_Initialize(const char* applicationId,
ReconnectTimeMs.reset();
if (Handlers.joinGame) {
RegisterForEvent("GAME_JOIN");
RegisterForEvent("ACTIVITY_JOIN");
}
if (Handlers.spectateGame) {
RegisterForEvent("GAME_SPECTATE");
RegisterForEvent("ACTIVITY_SPECTATE");
}
if (Handlers.joinRequest) {
RegisterForEvent("ACTIVITY_JOIN_REQUEST");
}
};
Connection->onDisconnect = [](int err, const char* message) {
@ -263,7 +279,7 @@ extern "C" void Discord_Initialize(const char* applicationId,
#endif
}
extern "C" void Discord_Shutdown()
extern "C" DISCORD_EXPORT void Discord_Shutdown()
{
if (!Connection) {
return;
@ -281,7 +297,7 @@ extern "C" void Discord_Shutdown()
RpcConnection::Destroy(Connection);
}
extern "C" void Discord_UpdatePresence(const DiscordRichPresence* presence)
extern "C" DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence)
{
PresenceMutex.lock();
QueuedPresence.length = JsonWriteRichPresenceObj(
@ -290,7 +306,22 @@ extern "C" void Discord_UpdatePresence(const DiscordRichPresence* presence)
SignalIOActivity();
}
extern "C" void Discord_RunCallbacks()
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()
{
// 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
@ -326,6 +357,20 @@ extern "C" void Discord_RunCallbacks()
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();
if (Handlers.joinRequest) {
DiscordJoinRequest djr{req->userId, req->username, req->avatar};
Handlers.joinRequest(&djr);
}
JoinAskQueue.CommitSend();
}
if (!isConnected) {
// if we are not connected, disconnect message last
if (wasDisconnected && Handlers.disconnected) {

View File

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

View File

@ -0,0 +1,96 @@
#include "discord-rpc.h"
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
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" 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]) {
if (readlink("/proc/self/exe", exePath, sizeof(exePath)) <= 0) {
return;
}
command = exePath;
}
const char* destopFileFormat = "[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), destopFileFormat, 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);
system(xdgMimeCommand);
}
extern "C" void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
{
char command[256];
sprintf(command, "xdg-open steam://run/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@ -0,0 +1,97 @@
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static bool Mkdir(const char* path)
{
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
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)
const char* home = getenv("HOME");
if (!home) {
return;
}
char path[2048];
sprintf(path, "%s/Library/Application Support/discord", home);
Mkdir(path);
strcat(path, "/games");
Mkdir(path);
strcat(path, "/");
strcat(path, applicationId);
strcat(path, ".json");
FILE* f = fopen(path, "w");
if (f) {
char jsonBuffer[2048];
int len = snprintf(jsonBuffer, sizeof(jsonBuffer), "{\"command\": \"%s\"}", command);
fwrite(jsonBuffer, (size_t)len, 1, f);
fclose(f);
}
}
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
void* pool = [[NSAutoreleasePool alloc] init];
RegisterURL(applicationId);
[(id)pool drain];
}
}
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
{
char command[256];
sprintf(command, "steam://run/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@ -1,7 +1,6 @@
#include "discord-rpc.h"
#include <stdio.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
@ -10,9 +9,7 @@
#include <Psapi.h>
#include <Strsafe.h>
#pragma comment(lib, "Psapi.lib")
#endif
#ifdef _WIN32
void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
{
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
@ -20,9 +17,8 @@ void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
wchar_t exeFilePath[MAX_PATH];
int exeLen = GetModuleFileNameExW(GetCurrentProcess(), nullptr, exeFilePath, MAX_PATH);
DWORD exeLen = GetModuleFileNameExW(GetCurrentProcess(), nullptr, exeFilePath, MAX_PATH);
wchar_t openCommand[1024];
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
if (command && command[0]) {
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
@ -49,14 +45,14 @@ void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
}
DWORD len;
LSTATUS result;
len = lstrlenW(protocolDescription) + 1;
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 = lstrlenW(protocolDescription) + 1;
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");
@ -68,7 +64,7 @@ void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
fprintf(stderr, "Error writing icon\n");
}
len = lstrlenW(openCommand) + 1;
len = (DWORD)lstrlenW(openCommand) + 1;
result = RegSetKeyValueW(
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
if (FAILED(result)) {
@ -76,12 +72,9 @@ void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command)
}
RegCloseKey(key);
}
#endif
void Discord_Register(const char* applicationId, const char* command)
extern "C" void Discord_Register(const char* applicationId, const char* command)
{
#ifdef _WIN32
wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
@ -94,12 +87,10 @@ void Discord_Register(const char* applicationId, const char* command)
}
Discord_RegisterW(appId, wcommand);
#endif
}
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
extern "C" void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
{
#ifdef _WIN32
wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
@ -133,5 +124,4 @@ void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://run/%s", steamPath, wSteamId);
Discord_RegisterW(appId, command);
#endif
}

36
src/msg_queue.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
template <typename ElementType, size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize]{};
std::atomic_uint nextAdd_{0};
std::atomic_uint nextSend_{0};
std::atomic_uint pendingSends_{0};
public:
MsgQueue() {}
ElementType* GetNextAddMessage()
{
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType* GetNextSendMessage()
{
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};

View File

@ -129,6 +129,7 @@ bool RpcConnection::Read(JsonDocument& message)
break;
case Opcode::Pong:
break;
case Opcode::Handshake:
default:
// something bad happened
lastErrorCode = (int)ErrorCode::ReadCorrupt;

View File

@ -72,7 +72,7 @@ void WriteOptionalString(JsonWriter& w, T& k, const char* value)
}
}
void JsonWriteNonce(JsonWriter& writer, int nonce)
static void JsonWriteNonce(JsonWriter& writer, int nonce)
{
WriteKey(writer, "nonce");
char nonceBuffer[32];
@ -197,3 +197,32 @@ size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const cha
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();
}

View File

@ -2,10 +2,20 @@
#include <stdint.h>
#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
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
#pragma warning(pop)
// if only there was a standard library function for this
template <size_t Len>
inline size_t StringCopy(char (&dest)[Len], const char* src)
@ -33,6 +43,8 @@ size_t JsonWriteRichPresenceObj(char* dest,
const DiscordRichPresence* presence);
size_t JsonWriteSubscribeCommand(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
@ -109,7 +121,7 @@ public:
}
}
void Flush() {}
size_t GetSize() const { return current_ - buffer_; }
size_t GetSize() const { return (size_t)(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;