diff --git a/examples/simple/CMakeLists.txt b/examples/simple/CMakeLists.txt new file mode 100644 index 0000000..e891682 --- /dev/null +++ b/examples/simple/CMakeLists.txt @@ -0,0 +1,3 @@ +include_directories(${PROJECT_SOURCE_DIR}/include) +add_executable(simple-client simple.c) +target_link_libraries(simple-client discord-rpc) diff --git a/examples/simple/simple.c b/examples/simple/simple.c new file mode 100644 index 0000000..73a1019 --- /dev/null +++ b/examples/simple/simple.c @@ -0,0 +1,89 @@ +/* + This is a simple example in C of using the rich presence API syncronously. +*/ + +#define _CRT_SECURE_NO_WARNINGS /* thanks Microsoft */ + +#include +#include +#include +#include + +#include "discord-rpc.h" + +static const char* APPLICATION_ID = "12345678910"; +static int FrustrationLevel = 0; + +static void updateDiscordPresence() { + char buffer[256]; + DiscordRichPresence discordPresence; + memset(&discordPresence, 0, sizeof(discordPresence)); + discordPresence.state = "West of House"; + sprintf(buffer, "Frustration level: %d", FrustrationLevel); + discordPresence.details = buffer; + Discord_UpdatePresence(&discordPresence); +} + +static void handleDiscordReady() { + printf("Discord: ready\n"); +} + +static void handleDiscordDisconnected() { + printf("Discord: disconnected\n"); +} + +static void handleDiscordWantsPresence() { + printf("Discord: requests presence\n"); + updateDiscordPresence(); +} + +static int prompt(char* line, size_t size) { + int res; + char* nl; + printf("\n> "); + fflush(stdout); + res = fgets(line, size, stdin) ? 1 : 0; + line[size - 1] = 0; + nl = strchr(line, '\n'); + if (nl) { + *nl = 0; + } + return res; +} + +static void gameLoop() { + char line[512]; + char* space; + + printf("You are standing in an open field west of a white house.\n"); + while (prompt(line, sizeof(line))) { + if (time(NULL) & 1) { + printf("I don't understand that.\n"); + } else { + space = strchr(line, ' '); + if (space) { + *space = 0; + } + printf("I don't know the word \"%s\".\n", line); + } + + ++FrustrationLevel; + + updateDiscordPresence(); + } +} + +int main() { + DiscordEventHandlers handlers; + memset(&handlers, 0, sizeof(handlers)); + handlers.ready = handleDiscordReady; + handlers.disconnected = handleDiscordDisconnected; + handlers.wantsPresence = handleDiscordWantsPresence; + Discord_Initialize(APPLICATION_ID, &handlers); + + gameLoop(); + + Discord_Shutdown(); + return 0; +} + diff --git a/examples/simplest/CMakeLists.txt b/examples/simplest/CMakeLists.txt index d3c9023..757ada0 100644 --- a/examples/simplest/CMakeLists.txt +++ b/examples/simplest/CMakeLists.txt @@ -1,3 +1,3 @@ include_directories(${PROJECT_SOURCE_DIR}/include) -add_executable(simple-client simple.c) -target_link_libraries(simple-client discord-rpc-sync) +add_executable(simplest-client simple.c) +target_link_libraries(simplest-client discord-rpc-simple) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index da94152..93eeaaf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +1,9 @@ include_directories(${PROJECT_SOURCE_DIR}/include) -add_library(discord-rpc-sync STATIC ${PROJECT_SOURCE_DIR}/include/discord-rpc.h discord-rpc-sync.cpp) + +add_library(discord-rpc-simple STATIC ${PROJECT_SOURCE_DIR}/include/discord-rpc.h discord-rpc-simple.cpp) + +set(BASE_RPC_SRC ${PROJECT_SOURCE_DIR}/include/discord-rpc.h discord-rpc.cpp yolojson.h connection.h) + +if(WIN32) + add_library(discord-rpc STATIC ${BASE_RPC_SRC} connection_win_sync.cpp) +endif(WIN32) diff --git a/src/connection.h b/src/connection.h new file mode 100644 index 0000000..c59f0a1 --- /dev/null +++ b/src/connection.h @@ -0,0 +1,24 @@ +#pragma once + +// This is to wrap the platform specific kinds of connect/read/write. + +#include + +struct RpcMessageFrame { + uint32_t length; + char message[64 * 1024 - sizeof(uint32_t)]; +}; + +struct RpcConnection { + void (*onConnect)() = nullptr; + void (*onDisconnect)() = nullptr; + + static RpcConnection* Create(); + static void Destroy(RpcConnection*&); + void Open(); + void Close(); + void Write(const void* data, size_t length); + + RpcMessageFrame* GetNextFrame(); + void WriteFrame(RpcMessageFrame* frame); +}; diff --git a/src/connection_win_sync.cpp b/src/connection_win_sync.cpp new file mode 100644 index 0000000..5c81bb6 --- /dev/null +++ b/src/connection_win_sync.cpp @@ -0,0 +1,90 @@ +#include "connection.h" + +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOSERVICE +#define NOIME +#include + +struct WinRpcConnection : public RpcConnection { + HANDLE pipe{INVALID_HANDLE_VALUE}; + RpcMessageFrame frame; +}; + +static const wchar_t* PipeName = L"\\\\.\\pipe\\DiscordRpcServer"; + +/*static*/ RpcConnection* RpcConnection::Create() +{ + return new WinRpcConnection; +} + +/*static*/ void RpcConnection::Destroy(RpcConnection*& c) +{ + auto self = reinterpret_cast(c); + delete self; + c = nullptr; +} + +void RpcConnection::Open() +{ + auto self = reinterpret_cast(this); + for (;;) { + self->pipe = ::CreateFileW(PipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + if (self->pipe != INVALID_HANDLE_VALUE) { + if (self->onConnect) { + self->onConnect(); + } + break; + } + + if (GetLastError() != ERROR_PIPE_BUSY) { + printf("Could not open pipe. Error: %d\n", GetLastError()); + return; + } + + if (!WaitNamedPipeW(PipeName, 10000)) { + printf("Could not open pipe: 10 second wait timed out.\n"); + return; + } + } +} + +void RpcConnection::Close() +{ + auto self = reinterpret_cast(this); + ::CloseHandle(self->pipe); + self->pipe = INVALID_HANDLE_VALUE; + if (self->onDisconnect) { + self->onDisconnect(); + } +} + +void RpcConnection::Write(const void* data, size_t length) +{ + auto self = reinterpret_cast(this); + + if (self->pipe == INVALID_HANDLE_VALUE) { + self->Open(); + if (self->pipe == INVALID_HANDLE_VALUE) { + return; + } + } + BOOL success = ::WriteFile(self->pipe, data, length, nullptr, nullptr); + if (!success) { + self->Close(); + } +} + +RpcMessageFrame* RpcConnection::GetNextFrame() +{ + auto self = reinterpret_cast(this); + return &(self->frame); +} + +void RpcConnection::WriteFrame(RpcMessageFrame* frame) +{ + auto self = reinterpret_cast(this); + self->Write(frame, frame->length); +} diff --git a/src/discord-rpc-sync.cpp b/src/discord-rpc-simple.cpp similarity index 100% rename from src/discord-rpc-sync.cpp rename to src/discord-rpc-simple.cpp diff --git a/src/discord-rpc.cpp b/src/discord-rpc.cpp new file mode 100644 index 0000000..dd09c5c --- /dev/null +++ b/src/discord-rpc.cpp @@ -0,0 +1,40 @@ +#include "discord-rpc.h" + +#include + +#include "connection.h" +#include "yolojson.h" + +static RpcConnection* MyConnection = nullptr; +static char ApplicationId[64]{}; +static DiscordEventHandlers Handlers{}; + +void Discord_Initialize(const char* applicationId, DiscordEventHandlers* handlers) +{ + StringCopy(ApplicationId, applicationId, sizeof(ApplicationId)); + if (handlers) { + Handlers = *handlers; + } + else { + Handlers = {}; + } + + MyConnection = RpcConnection::Create(); + MyConnection->Open(); +} + +void Discord_Shutdown() +{ + Handlers = {}; + MyConnection->Close(); + RpcConnection::Destroy(MyConnection); +} + +void Discord_UpdatePresence(const DiscordRichPresence* presence) +{ + auto frame = MyConnection->GetNextFrame(); + char* jsonWrite = frame->message; + JsonWriteRichPresenceObj(jsonWrite, presence); + frame->length = sizeof(uint32_t) + (jsonWrite - frame->message); + MyConnection->WriteFrame(frame); +} diff --git a/src/yolojson.h b/src/yolojson.h new file mode 100644 index 0000000..34f315f --- /dev/null +++ b/src/yolojson.h @@ -0,0 +1,185 @@ +#pragma once + +// if only there was a standard library function for this +inline size_t StringCopy(char* dest, const char* src, size_t maxBytes = UINT32_MAX) { + if (!dest || !src || !maxBytes) { + return 0; + } + size_t copied; + for (copied = 1; *src && copied < maxBytes; ++copied) { + *dest++ = *src++; + } + *dest = 0; + return copied - 1; +} + +inline void JsonWriteEscapedString(char*& dest, const char* src) +{ + for (char c = *src++; c; c = *src++) { + switch (c) { + case '\"': + case '\\': + *dest++ = '\\'; + *dest++ = c; + break; + case '\b': + *dest++ = '\\'; + *dest++ = 'b'; + break; + case '\f': + *dest++ = '\\'; + *dest++ = 'f'; + break; + case '\n': + *dest++ = '\\'; + *dest++ = 'n'; + break; + case '\r': + *dest++ = '\\'; + *dest++ = 'r'; + break; + case '\t': + *dest++ = '\\'; + *dest++ = 't'; + break; + default: + *dest++ = c; + break; + } + } +} + +template void JsonWriteNumber(char*& dest, T number) +{ + if (!number) { + *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; +} + +inline void JsonWritePropName(char*& dest, const char* name) +{ + *dest++ = '"'; + dest += StringCopy(dest, name); + *dest++ = '"'; + *dest++ = ':'; + *dest++ = ' '; +} + +inline void JsonWritePropSep(char*& dest) +{ + *dest++ = ','; + *dest++ = ' '; +} + +inline void JsonWriteStringProp(char*& dest, const char* name, const char* value) +{ + JsonWritePropName(dest, name); + *dest++ = '"'; + JsonWriteEscapedString(dest, value); + *dest++ = '"'; + JsonWritePropSep(dest); +} + +template +void JsonWriteNumberAsStringProp(char*& dest, const char* name, T value) +{ + JsonWritePropName(dest, name); + *dest++ = '"'; + JsonWriteNumber(dest, value); + *dest++ = '"'; + JsonWritePropSep(dest); +} + +template +void JsonWriteNumberProp(char*& dest, const char* name, T value) +{ + JsonWritePropName(dest, name); + JsonWriteNumber(dest, value); + JsonWritePropSep(dest); +} + +inline void JsonWriteBoolProp(char*& dest, const char* name, bool value) +{ + JsonWritePropName(dest, name); + dest += StringCopy(dest, value ? "true" : "false"); + JsonWritePropSep(dest); +} + +inline void JsonWriteRichPresenceObj(char*& dest, const DiscordRichPresence* presence) +{ + *dest++ = '{'; + + if (presence->state) { + JsonWriteStringProp(dest, "state", presence->state); + } + + if (presence->details) { + JsonWriteStringProp(dest, "details", presence->details); + } + + if (presence->startTimestamp) { + JsonWriteNumberAsStringProp(dest, "start_timestamp", presence->startTimestamp); + } + + if (presence->endTimestamp) { + JsonWriteNumberAsStringProp(dest, "end_timestamp", presence->endTimestamp); + } + + if (presence->largeImageKey) { + JsonWriteStringProp(dest, "large_image_key", presence->largeImageKey); + } + + if (presence->largeImageText) { + JsonWriteStringProp(dest, "large_image_text", presence->largeImageText); + } + + if (presence->smallImageKey) { + JsonWriteStringProp(dest, "small_image_key", presence->smallImageKey); + } + + if (presence->smallImageText) { + JsonWriteStringProp(dest, "small_image_text", presence->smallImageText); + } + + if (presence->partyId) { + JsonWriteStringProp(dest, "party_id", presence->partyId); + } + + if (presence->partyMax) { + JsonWriteNumberProp(dest, "party_size", presence->partySize); + JsonWriteNumberProp(dest, "party_max", presence->partyMax); + } + + if (presence->matchSecret) { + JsonWriteStringProp(dest, "match_secret", presence->matchSecret); + } + + if (presence->joinSecret) { + JsonWriteStringProp(dest, "join_secret", presence->joinSecret); + } + + if (presence->spectateSecret) { + JsonWriteStringProp(dest, "spectate_secret", presence->spectateSecret); + } + + JsonWriteBoolProp(dest, "instance", presence->instance != 0); + + dest -= 1; + *(dest - 1) = '}'; +}