Merge pull request #669 from TwilitRealm/remove-discord-rpc

Remove discord-rpc/rapidjson; roll our own
This commit is contained in:
TakaRikka
2026-05-04 23:43:24 -07:00
committed by GitHub
7 changed files with 990 additions and 114 deletions
+10 -41
View File
@@ -297,8 +297,10 @@ set(GAME_INCLUDE_DIRS
extern
${CMAKE_BINARY_DIR})
find_package(Threads REQUIRED)
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt)
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
Threads::Threads)
list(APPEND GAME_LIBS libzstd_static)
@@ -320,46 +322,13 @@ if (DUSK_MOVIE_SUPPORT)
list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1)
endif ()
option(DUSK_ENABLE_DISCORD_RPC "Enable Discord Rich Presence support" ON)
if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS)
FetchContent_Populate(discord_rpc
URL https://github.com/discord/discord-rpc/archive/refs/tags/v3.4.0.tar.gz
URL_HASH SHA256=e13427019027acd187352dacba6c65953af66fdf3c35fcf38fc40b454a9d7855
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# RapidJSON is a git submodule absent from the discord-rpc tarball; fetch separately.
FetchContent_Populate(rapidjson
URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.tar.gz
URL_HASH SHA256=bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
if (NOT TARGET discord-rpc)
set(_drpc ${discord_rpc_SOURCE_DIR}/src)
set(_drpc_src
${_drpc}/discord_rpc.cpp
${_drpc}/rpc_connection.cpp
${_drpc}/serialization.cpp
)
if (WIN32)
list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp)
elseif (APPLE)
list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.m)
else ()
list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp)
endif ()
add_library(discord-rpc STATIC ${_drpc_src})
target_include_directories(discord-rpc PUBLIC
${discord_rpc_SOURCE_DIR}/include
${rapidjson_SOURCE_DIR}/include
)
if (UNIX)
target_link_libraries(discord-rpc PUBLIC pthread)
endif ()
endif ()
list(APPEND GAME_LIBS discord-rpc)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD_RPC=1)
set(DUSK_ENABLE_DISCORD_DEFAULT ON)
if (DEFINED DUSK_ENABLE_DISCORD_RPC AND NOT DEFINED DUSK_ENABLE_DISCORD)
set(DUSK_ENABLE_DISCORD_DEFAULT ${DUSK_ENABLE_DISCORD_RPC})
endif ()
option(DUSK_ENABLE_DISCORD "Enable Discord Rich Presence support" ${DUSK_ENABLE_DISCORD_DEFAULT})
if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
endif ()
# Edit & Continue
+2
View File
@@ -1512,6 +1512,8 @@ set(DUSK_FILES
src/dusk/OSReport.cpp
src/dusk/OSThread.cpp
src/dusk/OSMutex.cpp
src/dusk/discord.cpp
src/dusk/discord.hpp
src/dusk/discord_presence.cpp
src/dusk/version.cpp
)
+8 -12
View File
@@ -1,18 +1,14 @@
#pragma once
#ifdef DUSK_DISCORD_RPC
#ifdef DUSK_DISCORD
namespace dusk {
namespace discord {
namespace dusk::discord {
void Initialize();
void initialize();
void run_callbacks();
void update_presence();
void shutdown();
void RunCallbacks();
} // namespace dusk::discord
void UpdatePresence();
void Shutdown();
}
}
#endif // DUSK_DISCORD_RPC
#endif // DUSK_DISCORD
+867
View File
@@ -0,0 +1,867 @@
#ifdef DUSK_DISCORD
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "discord.hpp"
#include "dusk/logging.h"
#include "nlohmann/json.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <deque>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <windows.h>
#else
#include <cerrno>
#include <cstdlib>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#endif
namespace dusk::discord::rpc {
namespace {
using json = nlohmann::json;
constexpr uint32_t kRpcVersion = 1;
constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2;
constexpr size_t kMaxFramePayloadSize = 64 * 1024;
constexpr auto kIoWait = std::chrono::milliseconds(500);
constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200);
constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500);
constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000);
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
enum class ConnectionState {
Disconnected,
SentHandshake,
Connected,
};
enum class DisconnectCode : int {
PipeClosed = 1,
ReadCorrupt = 2,
BadFrame = 3,
};
struct Frame {
Opcode opcode = Opcode::Frame;
std::string payload;
};
struct QueuedEvent {
enum class Type {
Ready,
Disconnected,
Error,
};
Type type = Type::Ready;
User user;
int code = 0;
std::string message;
};
class Backoff {
public:
std::chrono::milliseconds next_delay() {
const auto delay = currentDelay;
currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay);
return delay;
}
void reset() { currentDelay = kInitialReconnectDelay; }
private:
std::chrono::milliseconds currentDelay = kInitialReconnectDelay;
};
class IpcConnection {
public:
IpcConnection() = default;
~IpcConnection() { close(); }
IpcConnection(const IpcConnection&) = delete;
IpcConnection& operator=(const IpcConnection&) = delete;
bool open() {
#ifdef _WIN32
wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0";
constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) {
pipeName[kPipeDigit] = pipeNumber;
pipe = CreateFileW(
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe != INVALID_HANDLE_VALUE) {
return true;
}
if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) {
--pipeNumber;
}
}
return false;
#else
const auto tempPaths = get_temp_paths();
for (const std::string& tempPath : tempPaths) {
for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) {
socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketFd == -1) {
return false;
}
sockaddr_un pipeAddress{};
pipeAddress.sun_family = AF_UNIX;
const std::string socketPath =
tempPath + "/discord-ipc-" + std::to_string(pipeNumber);
if (socketPath.size() >= sizeof(pipeAddress.sun_path)) {
close();
continue;
}
std::strncpy(
pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1);
if (connect(socketFd, reinterpret_cast<const sockaddr*>(&pipeAddress),
sizeof(pipeAddress)) == 0)
{
fcntl(socketFd, F_SETFL, O_NONBLOCK);
#ifdef SO_NOSIGPIPE
int optval = 1;
setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
#endif
return true;
}
close();
}
}
return false;
#endif
}
void close() {
#ifdef _WIN32
if (pipe != INVALID_HANDLE_VALUE) {
CloseHandle(pipe);
pipe = INVALID_HANDLE_VALUE;
}
#else
if (socketFd != -1) {
::close(socketFd);
socketFd = -1;
}
#endif
pendingRead.clear();
}
bool is_open() const {
#ifdef _WIN32
return pipe != INVALID_HANDLE_VALUE;
#else
return socketFd != -1;
#endif
}
bool write_frame(const Frame& frame) {
std::array<uint8_t, kFrameHeaderSize> header{};
write_u32(header.data(), static_cast<uint32_t>(frame.opcode));
write_u32(header.data() + sizeof(uint32_t), static_cast<uint32_t>(frame.payload.size()));
return write_all(header.data(), header.size()) &&
write_all(
reinterpret_cast<const uint8_t*>(frame.payload.data()), frame.payload.size());
}
enum class ReadStatus {
None,
Frame,
Closed,
Corrupt,
};
ReadStatus read_frame(Frame& frame) {
if (!read_available()) {
return is_open() ? ReadStatus::None : ReadStatus::Closed;
}
if (pendingRead.size() < kFrameHeaderSize) {
return ReadStatus::None;
}
const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t));
if (payloadLength > kMaxFramePayloadSize) {
return ReadStatus::Corrupt;
}
const size_t frameLength = kFrameHeaderSize + payloadLength;
if (pendingRead.size() < frameLength) {
return ReadStatus::None;
}
frame.opcode = static_cast<Opcode>(read_u32(pendingRead.data()));
frame.payload.assign(
reinterpret_cast<const char*>(pendingRead.data() + kFrameHeaderSize), payloadLength);
pendingRead.erase(
pendingRead.begin(), pendingRead.begin() + static_cast<std::ptrdiff_t>(frameLength));
return ReadStatus::Frame;
}
private:
#ifndef _WIN32
static std::vector<std::string> get_temp_paths() {
std::vector<std::string> paths;
for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) {
if (const char* value = std::getenv(name); value && value[0] != '\0') {
if (std::find(paths.begin(), paths.end(), value) == paths.end()) {
paths.emplace_back(value);
}
}
}
if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) {
paths.emplace_back("/tmp");
}
return paths;
}
#endif
static void write_u32(uint8_t* out, uint32_t value) {
out[0] = static_cast<uint8_t>(value & 0xff);
out[1] = static_cast<uint8_t>((value >> 8) & 0xff);
out[2] = static_cast<uint8_t>((value >> 16) & 0xff);
out[3] = static_cast<uint8_t>((value >> 24) & 0xff);
}
static uint32_t read_u32(const uint8_t* in) {
return static_cast<uint32_t>(in[0]) | (static_cast<uint32_t>(in[1]) << 8) |
(static_cast<uint32_t>(in[2]) << 16) | (static_cast<uint32_t>(in[3]) << 24);
}
bool write_all(const uint8_t* data, size_t length) {
size_t written = 0;
while (written < length) {
#ifdef _WIN32
DWORD bytesWritten = 0;
if (WriteFile(pipe, data + written, static_cast<DWORD>(length - written), &bytesWritten,
nullptr) == FALSE ||
bytesWritten == 0)
{
close();
return false;
}
written += bytesWritten;
#else
#ifdef MSG_NOSIGNAL
constexpr int kMsgFlags = MSG_NOSIGNAL;
#else
constexpr int kMsgFlags = 0;
#endif
const ssize_t bytesWritten =
send(socketFd, data + written, length - written, kMsgFlags);
if (bytesWritten < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
close();
return false;
}
if (bytesWritten == 0) {
close();
return false;
}
written += static_cast<size_t>(bytesWritten);
#endif
}
return true;
}
bool read_available() {
std::array<uint8_t, 4096> buffer{};
bool readAny = false;
for (;;) {
#ifdef _WIN32
DWORD bytesAvailable = 0;
if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) {
close();
return readAny;
}
if (bytesAvailable == 0) {
return readAny;
}
const DWORD bytesToRead =
std::min<DWORD>(bytesAvailable, static_cast<DWORD>(buffer.size()));
DWORD bytesRead = 0;
if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) {
close();
return readAny;
}
if (bytesRead == 0) {
close();
return readAny;
}
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
readAny = true;
#else
#ifdef MSG_NOSIGNAL
constexpr int kMsgFlags = MSG_NOSIGNAL;
#else
constexpr int kMsgFlags = 0;
#endif
const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags);
if (bytesRead < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return readAny;
}
close();
return readAny;
}
if (bytesRead == 0) {
close();
return readAny;
}
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
readAny = true;
#endif
}
}
#ifdef _WIN32
HANDLE pipe = INVALID_HANDLE_VALUE;
#else
int socketFd = -1;
#endif
std::vector<uint8_t> pendingRead;
};
int current_process_id() {
#ifdef _WIN32
return static_cast<int>(GetCurrentProcessId());
#else
return static_cast<int>(getpid());
#endif
}
std::string next_nonce() {
static std::atomic_uint64_t sNonce{1};
return std::to_string(sNonce.fetch_add(1));
}
const json* find_member(const json& object, const char* key) {
if (!object.is_object()) {
return nullptr;
}
const auto member = object.find(key);
if (member == object.end()) {
return nullptr;
}
return &*member;
}
std::string json_string_member(const json& object, const char* key) {
const json* member = find_member(object, key);
if (!member || !member->is_string()) {
return {};
}
return member->get<std::string>();
}
int json_int_member(const json& object, const char* key, int defaultValue) {
const json* member = find_member(object, key);
if (!member || !member->is_number_integer()) {
return defaultValue;
}
try {
return member->get<int>();
} catch (const json::exception&) {
return defaultValue;
}
}
json make_presence_activity(const Presence& presence) {
json activity = json::object();
if (!presence.state.empty()) {
activity["state"] = presence.state;
}
if (!presence.details.empty()) {
activity["details"] = presence.details;
}
if (presence.startTimestamp != 0) {
activity["timestamps"] = {{"start", presence.startTimestamp}};
}
if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) {
json assets = json::object();
if (!presence.largeImageKey.empty()) {
assets["large_image"] = presence.largeImageKey;
}
if (!presence.largeImageText.empty()) {
assets["large_text"] = presence.largeImageText;
}
activity["assets"] = std::move(assets);
}
return activity;
}
Frame make_handshake_frame(std::string_view applicationId) {
return {
Opcode::Handshake,
json{
{"v", kRpcVersion},
{"client_id", std::string(applicationId)},
}
.dump(),
};
}
Frame make_set_activity_frame(std::string nonce, int pid, std::optional<Presence> presence) {
json args = {{"pid", pid}};
if (presence) {
args["activity"] = make_presence_activity(*presence);
} else {
args["activity"] = nullptr;
}
return {
Opcode::Frame,
json{
{"cmd", "SET_ACTIVITY"},
{"nonce", std::move(nonce)},
{"args", std::move(args)},
}
.dump(),
};
}
class Client {
public:
void initialize(std::string applicationId, EventHandlers handlers) {
shutdown();
{
std::lock_guard lock(mutex);
this->applicationId = std::move(applicationId);
this->handlers = std::move(handlers);
shouldRun = true;
queuedPresence.reset();
hasQueuedPresence = false;
clearRequested = false;
sentInitialConnectLog = false;
}
ioThread = std::thread([this] { io_loop(); });
}
void run_callbacks() {
std::deque<QueuedEvent> events;
EventHandlers localHandlers;
{
std::lock_guard lock(mutex);
events.swap(queuedEvents);
localHandlers = handlers;
}
for (const QueuedEvent& event : events) {
switch (event.type) {
case QueuedEvent::Type::Ready:
if (localHandlers.ready) {
localHandlers.ready(event.user);
}
break;
case QueuedEvent::Type::Disconnected:
if (localHandlers.disconnected) {
localHandlers.disconnected(event.code, event.message);
}
break;
case QueuedEvent::Type::Error:
if (localHandlers.error) {
localHandlers.error(event.code, event.message);
}
break;
}
}
}
void update_presence(Presence presence) {
{
std::lock_guard lock(mutex);
if (!shouldRun) {
return;
}
queuedPresence = std::move(presence);
hasQueuedPresence = true;
}
cv.notify_all();
}
void clear_presence() {
{
std::lock_guard lock(mutex);
if (!shouldRun) {
return;
}
queuedPresence.reset();
hasQueuedPresence = false;
clearRequested = true;
}
cv.notify_all();
}
void shutdown() {
{
std::lock_guard lock(mutex);
if (!shouldRun && !ioThread.joinable()) {
return;
}
shouldRun = false;
clearRequested = true;
}
cv.notify_all();
if (ioThread.joinable()) {
ioThread.join();
}
std::lock_guard lock(mutex);
queuedPresence.reset();
hasQueuedPresence = false;
clearRequested = false;
queuedEvents.clear();
handlers = {};
applicationId.clear();
}
private:
void io_loop() {
IpcConnection connection;
ConnectionState state = ConnectionState::Disconnected;
Backoff reconnectBackoff;
auto nextConnect = std::chrono::steady_clock::now();
const int pid = current_process_id();
std::string localApplicationId;
for (;;) {
{
std::unique_lock lock(mutex);
if (!shouldRun) {
break;
}
localApplicationId = applicationId;
}
const auto now = std::chrono::steady_clock::now();
if (state == ConnectionState::Disconnected && now >= nextConnect) {
if (connection.open()) {
if (connection.write_frame(make_handshake_frame(localApplicationId))) {
state = ConnectionState::SentHandshake;
} else {
connection.close();
}
}
if (state == ConnectionState::Disconnected) {
log_waiting_for_discord_once();
nextConnect = now + reconnectBackoff.next_delay();
}
}
if (state != ConnectionState::Disconnected) {
process_reads(connection, state, reconnectBackoff, nextConnect);
}
if (state == ConnectionState::Connected) {
flush_pending_presence(connection, pid);
}
std::unique_lock lock(mutex);
if (!shouldRun) {
break;
}
cv.wait_for(lock, kIoWait);
}
flush_shutdown_clear(connection, state, pid);
connection.close();
}
void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff,
std::chrono::steady_clock::time_point& nextConnect) {
for (;;) {
Frame frame;
const auto status = connection.read_frame(frame);
if (status == IpcConnection::ReadStatus::None) {
return;
}
if (status == IpcConnection::ReadStatus::Closed) {
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
static_cast<int>(DisconnectCode::PipeClosed), "Pipe closed");
return;
}
if (status == IpcConnection::ReadStatus::Corrupt) {
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
static_cast<int>(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame");
return;
}
switch (frame.opcode) {
case Opcode::Frame:
process_json_frame(frame.payload, state, reconnectBackoff);
break;
case Opcode::Close:
process_close_frame(
frame.payload, connection, state, reconnectBackoff, nextConnect);
return;
case Opcode::Ping:
connection.write_frame({Opcode::Pong, frame.payload});
break;
case Opcode::Pong:
break;
case Opcode::Handshake:
default:
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
static_cast<int>(DisconnectCode::BadFrame), "Bad Discord IPC frame");
return;
}
}
}
void process_json_frame(
const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) {
json message;
try {
message = json::parse(payload);
} catch (const json::parse_error&) {
enqueue_error(
static_cast<int>(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON");
return;
}
const std::string cmd = json_string_member(message, "cmd");
const std::string evt = json_string_member(message, "evt");
if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") {
state = ConnectionState::Connected;
reconnectBackoff.reset();
enqueue_ready(message);
return;
}
if (evt == "ERROR") {
const json* data = find_member(message, "data");
enqueue_error(data ? json_int_member(*data, "code", 0) : 0,
data ? json_string_member(*data, "message") : std::string{});
}
}
void process_close_frame(const std::string& payload, IpcConnection& connection,
ConnectionState& state, Backoff& reconnectBackoff,
std::chrono::steady_clock::time_point& nextConnect) {
int code = static_cast<int>(DisconnectCode::PipeClosed);
std::string message = "Discord closed IPC connection";
try {
const json closePayload = json::parse(payload);
code = json_int_member(closePayload, "code", code);
const std::string closeMessage = json_string_member(closePayload, "message");
if (!closeMessage.empty()) {
message = closeMessage;
}
} catch (const json::exception&) {
}
handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message);
}
void handle_disconnect(IpcConnection& connection, ConnectionState& state,
Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code,
std::string_view message) {
const bool wasConnected =
state == ConnectionState::Connected || state == ConnectionState::SentHandshake;
connection.close();
state = ConnectionState::Disconnected;
nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay();
if (wasConnected) {
enqueue_disconnected(code, message);
}
}
void flush_pending_presence(IpcConnection& connection, int pid) {
std::optional<Presence> presence;
bool shouldClear = false;
{
std::lock_guard lock(mutex);
if (hasQueuedPresence) {
presence = queuedPresence;
hasQueuedPresence = false;
} else if (clearRequested) {
shouldClear = true;
clearRequested = false;
}
}
if (presence) {
if (!connection.write_frame(
make_set_activity_frame(next_nonce(), pid, std::move(presence))))
{
requeue_presence();
}
} else if (shouldClear) {
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
}
}
void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) {
if (state != ConnectionState::Connected || !connection.is_open()) {
return;
}
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout;
while (std::chrono::steady_clock::now() < deadline) {
Frame frame;
const auto status = connection.read_frame(frame);
if (status == IpcConnection::ReadStatus::None) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (status != IpcConnection::ReadStatus::Frame) {
break;
}
if (frame.opcode == Opcode::Ping) {
connection.write_frame({Opcode::Pong, frame.payload});
}
}
}
void requeue_presence() {
std::lock_guard lock(mutex);
if (queuedPresence) {
hasQueuedPresence = true;
}
}
void enqueue_ready(const json& readyMessage) {
User user;
const auto data = readyMessage.find("data");
if (data != readyMessage.end() && data->is_object()) {
const auto userIt = data->find("user");
if (userIt != data->end() && userIt->is_object()) {
user.id = json_string_member(*userIt, "id");
user.username = json_string_member(*userIt, "username");
user.discriminator = json_string_member(*userIt, "discriminator");
user.avatar = json_string_member(*userIt, "avatar");
}
}
std::lock_guard lock(mutex);
queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)});
}
void enqueue_disconnected(int code, std::string_view message) {
std::lock_guard lock(mutex);
QueuedEvent event;
event.type = QueuedEvent::Type::Disconnected;
event.code = code;
event.message = message;
queuedEvents.push_back(std::move(event));
}
void enqueue_error(int code, std::string_view message) {
std::lock_guard lock(mutex);
QueuedEvent event;
event.type = QueuedEvent::Type::Error;
event.code = code;
event.message = message;
queuedEvents.push_back(std::move(event));
}
void log_waiting_for_discord_once() {
bool shouldLog = false;
{
std::lock_guard lock(mutex);
if (!sentInitialConnectLog) {
sentInitialConnectLog = true;
shouldLog = true;
}
}
if (shouldLog) {
DuskLog.info("Discord: Waiting for local Discord IPC");
}
}
std::mutex mutex;
std::condition_variable cv;
std::thread ioThread;
std::string applicationId;
EventHandlers handlers;
std::deque<QueuedEvent> queuedEvents;
std::optional<Presence> queuedPresence;
bool hasQueuedPresence = false;
bool clearRequested = false;
bool shouldRun = false;
bool sentInitialConnectLog = false;
};
Client& client() {
static Client sClient;
return sClient;
}
} // namespace
void initialize(std::string applicationId, EventHandlers handlers) {
client().initialize(std::move(applicationId), std::move(handlers));
}
void run_callbacks() {
client().run_callbacks();
}
void update_presence(Presence presence) {
client().update_presence(std::move(presence));
}
void clear_presence() {
client().clear_presence();
}
void shutdown() {
client().shutdown();
}
} // namespace dusk::discord::rpc
#endif // DUSK_DISCORD
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#ifdef DUSK_DISCORD
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
namespace dusk::discord::rpc {
struct User {
std::string id;
std::string username;
std::string discriminator;
std::string avatar;
};
struct Presence {
std::string state;
std::string details;
int64_t startTimestamp = 0;
std::string largeImageKey;
std::string largeImageText;
};
struct EventHandlers {
std::function<void(const User&)> ready;
std::function<void(int, std::string_view)> disconnected;
std::function<void(int, std::string_view)> error;
};
void initialize(std::string applicationId, EventHandlers handlers);
void run_callbacks();
void update_presence(Presence presence);
void clear_presence();
void shutdown();
} // namespace dusk::discord::rpc
#endif // DUSK_DISCORD
+53 -52
View File
@@ -1,38 +1,39 @@
#ifdef DUSK_DISCORD_RPC
#ifdef DUSK_DISCORD
#include "dusk/discord_presence.hpp"
#include "d/d_com_inf_game.h"
#include "discord.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/map_loader_definitions.h"
#include "d/d_com_inf_game.h"
#include "discord_rpc.h"
#include "fmt/format.h"
#include <chrono>
#include <cstring>
#include <string>
#include <string_view>
#include <utility>
namespace dusk {
namespace discord {
namespace dusk::discord {
static int64_t g_startTime = 0;
static bool g_initialized = false;
static const char* APPLICATION_ID = "1495632471994405035";
static constexpr const char* kApplicationId = "1495632471994405035";
static void OnReady(const DiscordUser* user) {
DuskLog.info("Discord: Connected as {}", user->username);
static void on_ready(const rpc::User& user) {
DuskLog.info("Discord: Connected as {}", user.username);
}
static void OnDisconnected(int errorCode, const char* message) {
static void on_disconnected(int errorCode, std::string_view message) {
DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message);
}
static void OnError(int errorCode, const char* message) {
static void on_error(int errorCode, std::string_view message) {
DuskLog.warn("Discord: Error ({}: {})", errorCode, message);
}
static const char* LookupMapName(const char* mapFile) {
if (!mapFile || mapFile[0] == '\0') return nullptr;
static const char* lookup_map_name(const char* mapFile) {
if (!mapFile || mapFile[0] == '\0')
return nullptr;
for (const auto& region : gameRegions) {
for (const auto& map : region.maps) {
if (map.mapFile && strcmp(mapFile, map.mapFile) == 0) {
@@ -43,80 +44,80 @@ static const char* LookupMapName(const char* mapFile) {
return nullptr;
}
void Initialize() {
g_startTime = static_cast<int64_t>(
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count()
);
void initialize() {
g_startTime = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
DiscordEventHandlers handlers{};
handlers.ready = OnReady;
handlers.disconnected = OnDisconnected;
handlers.errored = OnError;
Discord_Initialize(APPLICATION_ID, &handlers, 0, nullptr);
rpc::EventHandlers handlers{};
handlers.ready = on_ready;
handlers.disconnected = on_disconnected;
handlers.error = on_error;
rpc::initialize(kApplicationId, std::move(handlers));
g_initialized = true;
DuskLog.info("Discord Rich Presence initialized");
}
void RunCallbacks() {
if (!g_initialized) return;
Discord_RunCallbacks();
void run_callbacks() {
if (!g_initialized)
return;
rpc::run_callbacks();
}
void UpdatePresence() {
if (!g_initialized) return;
void update_presence() {
if (!g_initialized)
return;
static auto lastUpdate = std::chrono::steady_clock::time_point{};
static auto sLastUpdate = std::chrono::steady_clock::time_point{};
const auto now = std::chrono::steady_clock::now();
if (now - lastUpdate < std::chrono::seconds(15)) return;
lastUpdate = now;
if (now - sLastUpdate < std::chrono::seconds(15))
return;
sLastUpdate = now;
static std::string detailsBuf;
static std::string stateBuf;
static std::string sDetailsBuf;
static std::string sStateBuf;
DiscordRichPresence presence{};
rpc::Presence presence{};
presence.startTimestamp = g_startTime;
presence.largeImageKey = "icon";
presence.largeImageText = "Dusk";
if (dusk::IsGameLaunched) {
if (IsGameLaunched) {
const char* stageName = dComIfGp_getLastPlayStageName();
// stageName is empty until a room is actually entered
if (stageName[0] != '\0') {
const char* locationName = LookupMapName(stageName);
const char* locationName = lookup_map_name(stageName);
if (locationName) {
detailsBuf = locationName;
}
else {
detailsBuf = "Twilight Princess";
sDetailsBuf = locationName;
} else {
sDetailsBuf = "Twilight Princess";
}
presence.details = detailsBuf.c_str();
presence.details = sDetailsBuf;
stateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"),
sStateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"),
dComIfGs_getLife() / 4, dComIfGs_getMaxLife() / 5, dComIfGs_getRupee());
presence.state = stateBuf.c_str();
presence.state = sStateBuf;
}
}
Discord_UpdatePresence(&presence);
rpc::update_presence(std::move(presence));
DuskLog.debug("Discord Rich Presence sent");
}
void Shutdown() {
if (!g_initialized) return;
Discord_ClearPresence();
Discord_Shutdown();
void shutdown() {
if (!g_initialized)
return;
rpc::clear_presence();
rpc::shutdown();
g_initialized = false;
DuskLog.info("Discord Rich Presence shut down");
}
} // namespace discord
} // namespace dusk
} // namespace dusk::discord
#endif // DUSK_DISCORD_RPC
#endif // DUSK_DISCORD
+9 -9
View File
@@ -307,9 +307,9 @@ void main01(void) {
FrameMark;
#ifdef DUSK_DISCORD_RPC
dusk::discord::RunCallbacks();
dusk::discord::UpdatePresence();
#ifdef DUSK_DISCORD
dusk::discord::run_callbacks();
dusk::discord::update_presence();
#endif
} while (dusk::IsRunning);
@@ -571,8 +571,8 @@ int game_main(int argc, char* argv[]) {
auroraInfo = aurora_initialize(argc, argv, &config);
}
#ifdef DUSK_DISCORD_RPC
dusk::discord::Initialize();
#ifdef DUSK_DISCORD
dusk::discord::initialize();
#endif
VISetWindowTitle(
@@ -613,8 +613,8 @@ int game_main(int argc, char* argv[]) {
// pre game launch ui main loop
if (!launchUILoop()) {
dusk::ShutdownCrashReporting();
#ifdef DUSK_DISCORD_RPC
dusk::discord::Shutdown();
#ifdef DUSK_DISCORD
dusk::discord::shutdown();
#endif
dusk::ui::shutdown();
aurora_shutdown();
@@ -674,8 +674,8 @@ int game_main(int argc, char* argv[]) {
// Notifies all CVs and causes threads to exit
OSResetSystem(OS_RESET_SHUTDOWN, 0, 0);
#ifdef DUSK_DISCORD_RPC
dusk::discord::Shutdown();
#ifdef DUSK_DISCORD
dusk::discord::shutdown();
#endif
dusk::ui::shutdown();
aurora_shutdown();