diff --git a/Source/Core/Common/HttpRequest.cpp b/Source/Core/Common/HttpRequest.cpp index af8eb5f5c5..63e4e85878 100644 --- a/Source/Core/Common/HttpRequest.cpp +++ b/Source/Core/Common/HttpRequest.cpp @@ -29,6 +29,7 @@ public: bool IsValid() const; void SetCookies(const std::string& cookies); + void UseIPv4(); Response Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload, size_t size); @@ -62,6 +63,11 @@ void HttpRequest::SetCookies(const std::string& cookies) m_impl->SetCookies(cookies); } +void HttpRequest::UseIPv4() +{ + m_impl->UseIPv4(); +} + HttpRequest::Response HttpRequest::Get(const std::string& url, const Headers& headers) { return m_impl->Fetch(url, Impl::Method::GET, headers, nullptr, 0); @@ -136,6 +142,11 @@ void HttpRequest::Impl::SetCookies(const std::string& cookies) curl_easy_setopt(m_curl.get(), CURLOPT_COOKIE, cookies.c_str()); } +void HttpRequest::Impl::UseIPv4() +{ + curl_easy_setopt(m_curl.get(), CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +} + static size_t CurlWriteCallback(char* data, size_t size, size_t nmemb, void* userdata) { auto* buffer = static_cast*>(userdata); diff --git a/Source/Core/Common/HttpRequest.h b/Source/Core/Common/HttpRequest.h index fefb54c784..94916f5c3c 100644 --- a/Source/Core/Common/HttpRequest.h +++ b/Source/Core/Common/HttpRequest.h @@ -32,6 +32,7 @@ public: using Headers = std::map>; void SetCookies(const std::string& cookies); + void UseIPv4(); Response Get(const std::string& url, const Headers& headers = {}); Response Post(const std::string& url, const std::vector& payload, const Headers& headers = {}); diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index 6cc6435b88..a7016a2665 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -1330,6 +1330,7 @@ void NetPlayClient::OnTraversalStateChanged() Disconnect(); m_dialog->OnTraversalError(m_traversal_client->GetFailureReason()); } + m_dialog->OnTraversalStateChanged(state); } // called from ---NETPLAY--- thread diff --git a/Source/Core/Core/NetPlayClient.h b/Source/Core/Core/NetPlayClient.h index c096b53495..aa8b2f639f 100644 --- a/Source/Core/Core/NetPlayClient.h +++ b/Source/Core/Core/NetPlayClient.h @@ -47,6 +47,7 @@ public: virtual void OnConnectionLost() = 0; virtual void OnConnectionError(const std::string& message) = 0; virtual void OnTraversalError(TraversalClient::FailureReason error) = 0; + virtual void OnTraversalStateChanged(TraversalClient::State state) = 0; virtual void OnSaveDataSyncFailure() = 0; virtual bool IsRecording() = 0; diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index fcf1fc886a..abf0a63fe8 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -807,8 +807,15 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player) void NetPlayServer::OnTraversalStateChanged() { - if (m_dialog && m_traversal_client->GetState() == TraversalClient::Failure) + if (!m_dialog) + return; + + const TraversalClient::State state = m_traversal_client->GetState(); + + if (state == TraversalClient::Failure) m_dialog->OnTraversalError(m_traversal_client->GetFailureReason()); + + m_dialog->OnTraversalStateChanged(state); } // called from ---GUI--- thread diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index eff3bdd840..86a73c34ed 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -8,6 +8,8 @@ set(CMAKE_AUTOMOC ON) add_executable(dolphin-emu AboutDialog.cpp CheatsManager.cpp + DiscordHandler.cpp + DiscordJoinRequestDialog.cpp FIFO/FIFOPlayerWindow.cpp FIFO/FIFOAnalyzer.cpp HotkeyScheduler.cpp diff --git a/Source/Core/DolphinQt/DiscordHandler.cpp b/Source/Core/DolphinQt/DiscordHandler.cpp new file mode 100644 index 0000000000..8bfadd21dd --- /dev/null +++ b/Source/Core/DolphinQt/DiscordHandler.cpp @@ -0,0 +1,98 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#ifdef USE_DISCORD_PRESENCE + +#include "DolphinQt/DiscordHandler.h" + +#include + +#include + +#include "Common/Thread.h" + +#include "UICommon/DiscordPresence.h" + +#include "DolphinQt/DiscordJoinRequestDialog.h" +#include "DolphinQt/QtUtils/RunOnObject.h" + +DiscordHandler::DiscordHandler(QWidget* parent) : QObject{parent}, m_parent{parent} +{ + connect(this, &DiscordHandler::JoinRequest, this, &DiscordHandler::ShowNewJoinRequest); +} + +DiscordHandler::~DiscordHandler() +{ + Stop(); +} + +void DiscordHandler::Start() +{ + m_stop_requested.Set(false); + m_thread = std::thread(&DiscordHandler::Run, this); +} + +void DiscordHandler::Stop() +{ + m_stop_requested.Set(true); + + if (m_thread.joinable()) + m_thread.join(); +} + +void DiscordHandler::DiscordJoinRequest(const char* id, const std::string& discord_tag, + const char* avatar) +{ + emit DiscordHandler::JoinRequest(id, discord_tag, avatar); +} + +void DiscordHandler::DiscordJoin() +{ + emit DiscordHandler::Join(); +} + +void DiscordHandler::ShowNewJoinRequest(const std::string& id, const std::string& discord_tag, + const std::string& avatar) +{ + std::lock_guard lock(m_request_dialogs_mutex); + m_request_dialogs.emplace_front(m_parent, id, discord_tag, avatar); + DiscordJoinRequestDialog& request_dialog = m_request_dialogs.front(); + request_dialog.show(); + request_dialog.raise(); + request_dialog.activateWindow(); + QApplication::alert(nullptr, DiscordJoinRequestDialog::s_max_lifetime_seconds * 1000); +} + +void DiscordHandler::Run() +{ + while (!m_stop_requested.IsSet()) + { + if (m_thread.joinable()) + Discord::CallPendingCallbacks(); + + // close and remove dead requests + { + std::lock_guard lock(m_request_dialogs_mutex); + for (auto request_dialog = m_request_dialogs.begin(); + request_dialog != m_request_dialogs.end();) + { + if (std::time(nullptr) < request_dialog->GetCloseTimestamp()) + { + ++request_dialog; + continue; + } + + RunOnObject(m_parent, [this, &request_dialog] { + request_dialog->close(); + request_dialog = m_request_dialogs.erase(request_dialog); + return nullptr; + }); + } + } + + Common::SleepCurrentThread(1000 * 2); + } +} + +#endif diff --git a/Source/Core/DolphinQt/DiscordHandler.h b/Source/Core/DolphinQt/DiscordHandler.h new file mode 100644 index 0000000000..d1be3d01c0 --- /dev/null +++ b/Source/Core/DolphinQt/DiscordHandler.h @@ -0,0 +1,49 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#include + +#include "Common/Flag.h" + +#include "UICommon/DiscordPresence.h" + +class DiscordJoinRequestDialog; + +class DiscordHandler : public QObject, public Discord::Handler +{ + Q_OBJECT +#ifdef USE_DISCORD_PRESENCE +public: + explicit DiscordHandler(QWidget* parent); + ~DiscordHandler(); + + void Start(); + void Stop(); + void DiscordJoin() override; + void DiscordJoinRequest(const char* id, const std::string& discord_tag, + const char* avatar) override; + void ShowNewJoinRequest(const std::string& id, const std::string& discord_tag, + const std::string& avatar); +#endif + +signals: + void Join(); + void JoinRequest(const std::string id, const std::string discord_tag, const std::string avatar); + +#ifdef USE_DISCORD_PRESENCE +private: + void Run(); + QWidget* m_parent; + Common::Flag m_stop_requested; + std::thread m_thread; + std::list m_request_dialogs; + std::mutex m_request_dialogs_mutex; +#endif +}; diff --git a/Source/Core/DolphinQt/DiscordJoinRequestDialog.cpp b/Source/Core/DolphinQt/DiscordJoinRequestDialog.cpp new file mode 100644 index 0000000000..dd0a6eb64b --- /dev/null +++ b/Source/Core/DolphinQt/DiscordJoinRequestDialog.cpp @@ -0,0 +1,91 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#ifdef USE_DISCORD_PRESENCE + +#include "DolphinQt/DiscordJoinRequestDialog.h" + +#include +#include +#include +#include + +#include + +#include "Common/HttpRequest.h" +#include "Common/StringUtil.h" + +DiscordJoinRequestDialog::DiscordJoinRequestDialog(QWidget* parent, const std::string& id, + const std::string& discord_tag, + const std::string& avatar) + : QDialog(parent), m_user_id(id), m_close_timestamp(std::time(nullptr) + s_max_lifetime_seconds) +{ + setWindowTitle(tr("Request to Join Your Party")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + QPixmap avatar_pixmap; + + if (!avatar.empty()) + { + const std::string avatar_endpoint = StringFromFormat( + "https://cdn.discordapp.com/avatars/%s/%s.png", id.c_str(), avatar.c_str()); + + Common::HttpRequest request; + Common::HttpRequest::Response response = request.Get(avatar_endpoint); + + if (response.has_value()) + avatar_pixmap.loadFromData(response->data(), static_cast(response->size()), "png"); + } + + CreateLayout(discord_tag, avatar_pixmap); + ConnectWidgets(); +} + +std::time_t DiscordJoinRequestDialog::GetCloseTimestamp() const +{ + return m_close_timestamp; +} + +void DiscordJoinRequestDialog::CreateLayout(const std::string& discord_tag, const QPixmap& avatar) +{ + m_main_layout = new QGridLayout; + + m_invite_button = new QPushButton(tr("\u2714 Invite")); + m_decline_button = new QPushButton(tr("\u2716 Decline")); + m_ignore_button = new QPushButton(tr("Ignore")); + + QLabel* text = + new QLabel(tr("%1\nwants to join your party.").arg(QString::fromStdString(discord_tag))); + text->setAlignment(Qt::AlignCenter); + + if (!avatar.isNull()) + { + QLabel* picture = new QLabel(); + picture->setPixmap(avatar); + m_main_layout->addWidget(picture, 1, 0, 1, 3, Qt::AlignHCenter); + } + + m_main_layout->addWidget(text, 2, 0, 3, 3, Qt::AlignHCenter); + m_main_layout->addWidget(m_invite_button, 8, 0); + m_main_layout->addWidget(m_decline_button, 8, 1); + m_main_layout->addWidget(m_ignore_button, 8, 2); + + setLayout(m_main_layout); +} + +void DiscordJoinRequestDialog::ConnectWidgets() +{ + connect(m_invite_button, &QPushButton::pressed, [this] { Reply(DISCORD_REPLY_YES); }); + connect(m_decline_button, &QPushButton::pressed, [this] { Reply(DISCORD_REPLY_NO); }); + connect(m_ignore_button, &QPushButton::pressed, [this] { Reply(DISCORD_REPLY_IGNORE); }); + connect(this, &QDialog::rejected, this, [this] { Reply(DISCORD_REPLY_IGNORE); }); +} + +void DiscordJoinRequestDialog::Reply(int reply) +{ + Discord_Respond(m_user_id.c_str(), reply); + close(); +} + +#endif diff --git a/Source/Core/DolphinQt/DiscordJoinRequestDialog.h b/Source/Core/DolphinQt/DiscordJoinRequestDialog.h new file mode 100644 index 0000000000..d9a7a8c9dd --- /dev/null +++ b/Source/Core/DolphinQt/DiscordJoinRequestDialog.h @@ -0,0 +1,35 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class QGridLayout; +class QPixmap; + +class DiscordJoinRequestDialog : public QDialog +{ + Q_OBJECT +public: + explicit DiscordJoinRequestDialog(QWidget* parent, const std::string& id, + const std::string& discord_tag, const std::string& avatar); + std::time_t GetCloseTimestamp() const; + + static constexpr std::time_t s_max_lifetime_seconds = 30; + +private: + void CreateLayout(const std::string& discord_tag, const QPixmap& avatar); + void ConnectWidgets(); + void Reply(int reply); + + QGridLayout* m_main_layout; + QPushButton* m_invite_button; + QPushButton* m_decline_button; + QPushButton* m_ignore_button; + + const std::string m_user_id; + const std::time_t m_close_timestamp; +}; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 0228011308..216c0aa2f5 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -107,6 +107,8 @@ + + @@ -175,6 +177,8 @@ + + @@ -320,6 +324,8 @@ + + diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index bacd46df81..3043479b84 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -67,6 +67,7 @@ #include "DolphinQt/Debugger/MemoryWidget.h" #include "DolphinQt/Debugger/RegisterWidget.h" #include "DolphinQt/Debugger/WatchWidget.h" +#include "DolphinQt/DiscordHandler.h" #include "DolphinQt/FIFO/FIFOPlayerWindow.h" #include "DolphinQt/GCMemcardManager.h" #include "DolphinQt/GameList/GameList.h" @@ -646,7 +647,8 @@ void MainWindow::OnStopComplete() HideRenderWidget(); EnableScreenSaver(true); #ifdef USE_DISCORD_PRESENCE - Discord::UpdateDiscordPresence(); + if (!m_netplay_dialog->isVisible()) + Discord::UpdateDiscordPresence(); #endif SetFullScreenResolution(false); @@ -800,7 +802,8 @@ void MainWindow::StartGame(std::unique_ptr&& parameters) ShowRenderWidget(); #ifdef USE_DISCORD_PRESENCE - Discord::UpdateDiscordPresence(); + if (!NetPlay::IsNetPlayRunning()) + Discord::UpdateDiscordPresence(); #endif if (SConfig::GetInstance().bFullscreen) @@ -1051,6 +1054,9 @@ void MainWindow::NetPlayInit() { m_netplay_setup_dialog = new NetPlaySetupDialog(this); m_netplay_dialog = new NetPlayDialog; +#ifdef USE_DISCORD_PRESENCE + m_netplay_discord = new DiscordHandler(this); +#endif connect(m_netplay_dialog, &NetPlayDialog::Boot, this, [this](const QString& path) { StartGame(path); }); @@ -1058,6 +1064,12 @@ void MainWindow::NetPlayInit() connect(m_netplay_dialog, &NetPlayDialog::rejected, this, &MainWindow::NetPlayQuit); connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Join, this, &MainWindow::NetPlayJoin); connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Host, this, &MainWindow::NetPlayHost); +#ifdef USE_DISCORD_PRESENCE + connect(m_netplay_discord, &DiscordHandler::Join, this, &MainWindow::NetPlayJoin); + + Discord::InitNetPlayFunctionality(*m_netplay_discord); + m_netplay_discord->Start(); +#endif } bool MainWindow::NetPlayJoin() @@ -1176,6 +1188,9 @@ void MainWindow::NetPlayQuit() { Settings::Instance().ResetNetPlayClient(); Settings::Instance().ResetNetPlayServer(); +#ifdef USE_DISCORD_PRESENCE + Discord::UpdateDiscordPresence(); +#endif } void MainWindow::EnableScreenSaver(bool enable) diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index 8c387a5f20..360e7343de 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -19,6 +19,7 @@ struct BootParameters; class CheatsManager; class CodeWidget; class ControllersWindow; +class DiscordHandler; class DragEnterEvent; class FIFOPlayerWindow; class GameList; @@ -183,6 +184,7 @@ private: ControllersWindow* m_controllers_window; SettingsWindow* m_settings_window; NetPlayDialog* m_netplay_dialog; + DiscordHandler* m_netplay_discord; NetPlaySetupDialog* m_netplay_setup_dialog; GraphicsWindow* m_graphics_window; static constexpr int num_gc_controllers = 4; diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp index e5af25d6cf..a6775180de 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.cpp @@ -29,10 +29,12 @@ #include "Common/CommonPaths.h" #include "Common/Config/Config.h" +#include "Common/HttpRequest.h" #include "Common/TraversalClient.h" #include "Core/Config/GraphicsSettings.h" #include "Core/Config/MainSettings.h" +#include "Core/Config/NetplaySettings.h" #include "Core/Config/SYSCONFSettings.h" #include "Core/ConfigLoaders/GameConfigLoader.h" #include "Core/ConfigManager.h" @@ -49,6 +51,7 @@ #include "DolphinQt/Resources.h" #include "DolphinQt/Settings.h" +#include "UICommon/DiscordPresence.h" #include "UICommon/GameFile.h" #include "VideoCommon/VideoConfig.h" @@ -418,6 +421,7 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal) m_nickname = nickname; m_use_traversal = use_traversal; m_buffer_size = 0; + m_old_player_count = 0; m_room_box->clear(); m_chat_edit->clear(); @@ -458,6 +462,59 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal) UpdateGUI(); } +void NetPlayDialog::UpdateDiscordPresence() +{ +#ifdef USE_DISCORD_PRESENCE + // both m_current_game and m_player_count need to be set for the status to be displayed correctly + if (m_player_count == 0 || m_current_game.empty()) + return; + + const auto use_default = [this]() { + Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::Empty, "", m_current_game); + }; + + if (Core::IsRunning()) + return use_default(); + + if (IsHosting()) + { + if (g_TraversalClient) + { + const auto host_id = g_TraversalClient->GetHostID(); + if (host_id[0] == '\0') + return use_default(); + + Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::RoomID, + std::string(host_id.begin(), host_id.end()), m_current_game); + } + else + { + if (m_external_ip_address.empty()) + { + Common::HttpRequest request; + // ENet does not support IPv6, so IPv4 has to be used + request.UseIPv4(); + Common::HttpRequest::Response response = + request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}}); + + if (!response.has_value()) + return use_default(); + m_external_ip_address = std::string(response->begin(), response->end()); + } + const int port = Settings::Instance().GetNetPlayServer()->GetPort(); + + Discord::UpdateDiscordPresence( + m_player_count, Discord::SecretType::IPAddress, + Discord::CreateSecretFromIPAddress(m_external_ip_address, port), m_current_game); + } + } + else + { + use_default(); + } +#endif +} + void NetPlayDialog::UpdateGUI() { auto client = Settings::Instance().GetNetPlayClient(); @@ -565,6 +622,12 @@ void NetPlayDialog::UpdateGUI() m_hostcode_action_button->setText(tr("Copy")); m_hostcode_action_button->setEnabled(true); } + + if (m_old_player_count != m_player_count) + { + UpdateDiscordPresence(); + m_old_player_count = m_player_count; + } } // NetPlayUI methods @@ -632,6 +695,7 @@ void NetPlayDialog::OnMsgChangeGame(const std::string& title) QueueOnObject(this, [this, qtitle, title] { m_game_button->setText(qtitle); m_current_game = title; + UpdateDiscordPresence(); }); DisplayMessage(tr("Game changed to \"%1\"").arg(qtitle), "magenta"); } @@ -669,11 +733,13 @@ void NetPlayDialog::OnMsgStartGame() auto client = Settings::Instance().GetNetPlayClient(); if (client) client->StartGame(FindGame(m_current_game)); + UpdateDiscordPresence(); }); } void NetPlayDialog::OnMsgStopGame() { + QueueOnObject(this, [this] { UpdateDiscordPresence(); }); } void NetPlayDialog::OnPadBufferChanged(u32 buffer) @@ -727,6 +793,18 @@ void NetPlayDialog::OnTraversalError(TraversalClient::FailureReason error) }); } +void NetPlayDialog::OnTraversalStateChanged(TraversalClient::State state) +{ + switch (state) + { + case TraversalClient::State::Connected: + case TraversalClient::State::Failure: + UpdateDiscordPresence(); + default: + break; + } +} + void NetPlayDialog::OnSaveDataSyncFailure() { QueueOnObject(this, [this] { SetOptionsEnabled(true); }); diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h index f8f9926101..a9d835baf7 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlayDialog.h @@ -51,6 +51,7 @@ public: void OnConnectionLost() override; void OnConnectionError(const std::string& message) override; void OnTraversalError(TraversalClient::FailureReason error) override; + void OnTraversalStateChanged(TraversalClient::State state) override; void OnSaveDataSyncFailure() override; bool IsRecording() override; @@ -73,6 +74,7 @@ private: void OnStart(); void DisplayMessage(const QString& msg, const std::string& color, int duration = OSD::Duration::NORMAL); + void UpdateDiscordPresence(); void UpdateGUI(); void GameStatusChanged(bool running); void SetOptionsEnabled(bool enabled); @@ -113,6 +115,7 @@ private: MD5Dialog* m_md5_dialog; PadMappingDialog* m_pad_mapping; std::string m_current_game; + std::string m_external_ip_address; std::string m_nickname; GameListModel* m_game_list_model = nullptr; bool m_use_traversal = false; @@ -120,4 +123,5 @@ private: bool m_got_stop_request = true; int m_buffer_size = 0; int m_player_count = 0; + int m_old_player_count = 0; }; diff --git a/Source/Core/UICommon/DiscordPresence.cpp b/Source/Core/UICommon/DiscordPresence.cpp index dece60dcbc..5fb7ee676b 100644 --- a/Source/Core/UICommon/DiscordPresence.cpp +++ b/Source/Core/UICommon/DiscordPresence.cpp @@ -3,6 +3,11 @@ // Refer to the license.txt file included. #include "UICommon/DiscordPresence.h" + +#include "Common/Hash.h" +#include "Common/StringUtil.h" + +#include "Core/Config/NetplaySettings.h" #include "Core/Config/UISettings.h" #include "Core/ConfigManager.h" @@ -15,6 +20,72 @@ namespace Discord { +#ifdef USE_DISCORD_PRESENCE +static Handler* event_handler = nullptr; +static const char* username = ""; + +static void HandleDiscordReady(const DiscordUser* user) +{ + username = user->username; +} + +static void HandleDiscordJoinRequest(const DiscordUser* user) +{ + if (event_handler == nullptr) + return; + + const std::string discord_tag = StringFromFormat("%s#%s", user->username, user->discriminator); + event_handler->DiscordJoinRequest(user->userId, discord_tag, user->avatar); +} + +static void HandleDiscordJoin(const char* join_secret) +{ + if (event_handler == nullptr) + return; + + if (Config::Get(Config::NETPLAY_NICKNAME) == Config::NETPLAY_NICKNAME.default_value) + Config::SetCurrent(Config::NETPLAY_NICKNAME, username); + + std::string secret(join_secret); + + std::string type = secret.substr(0, secret.find('\n')); + size_t offset = type.length() + 1; + + switch (static_cast(std::stol(type))) + { + default: + case SecretType::Empty: + return; + + case SecretType::IPAddress: + { + // SetBaseOrCurrent will save the ip address, which isn't what's wanted in this situation + Config::SetCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, "direct"); + + std::string host = secret.substr(offset, secret.find_last_of(':') - offset); + Config::SetCurrent(Config::NETPLAY_ADDRESS, host); + + offset += host.length(); + if (secret[offset] == ':') + Config::SetCurrent(Config::NETPLAY_CONNECT_PORT, std::stoul(secret.substr(offset + 1))); + } + break; + + case SecretType::RoomID: + { + Config::SetCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, "traversal"); + + Config::SetCurrent(Config::NETPLAY_HOST_CODE, secret.substr(offset)); + } + break; + } + + event_handler->DiscordJoin(); +} +#endif + +Discord::Handler::~Handler() = default; + void Init() { #ifdef USE_DISCORD_PRESENCE @@ -22,29 +93,102 @@ void Init() return; DiscordEventHandlers handlers = {}; + + handlers.ready = HandleDiscordReady; + handlers.joinRequest = HandleDiscordJoinRequest; + handlers.joinGame = HandleDiscordJoin; // The number is the client ID for Dolphin, it's used for images and the appication name Discord_Initialize("455712169795780630", &handlers, 1, nullptr); UpdateDiscordPresence(); #endif } -void UpdateDiscordPresence() +void CallPendingCallbacks() { #ifdef USE_DISCORD_PRESENCE if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) return; - const std::string& title = SConfig::GetInstance().GetTitleDescription(); + Discord_RunCallbacks(); + +#endif +} + +void InitNetPlayFunctionality(Handler& handler) +{ +#ifdef USE_DISCORD_PRESENCE + event_handler = &handler; +#endif +} + +void UpdateDiscordPresence(int party_size, SecretType type, const std::string& secret, + const std::string& current_game) +{ +#ifdef USE_DISCORD_PRESENCE + if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) + return; + + const std::string& title = + current_game.empty() ? SConfig::GetInstance().GetTitleDescription() : current_game; DiscordRichPresence discord_presence = {}; discord_presence.largeImageKey = "dolphin_logo"; discord_presence.largeImageText = "Dolphin is an emulator for the GameCube and the Wii."; discord_presence.details = title.empty() ? "Not in-game" : title.c_str(); discord_presence.startTimestamp = std::time(nullptr); + + if (party_size > 0) + { + if (party_size < 4) + { + discord_presence.state = "In a party"; + discord_presence.partySize = party_size; + discord_presence.partyMax = 4; + } + else + { + // others can still join to spectate + discord_presence.state = "In a full party"; + discord_presence.partySize = party_size; + // Note: joining still works without partyMax + } + } + + std::string party_id; + std::string secret_final; + if (type != SecretType::Empty) + { + // Declearing party_id or secret_final here will deallocate the variable before passing the + // values over to Discord_UpdatePresence. + + const size_t secret_length = secret.length(); + party_id = std::to_string( + Common::HashAdler32(reinterpret_cast(secret.c_str()), secret_length)); + + const std::string secret_type = std::to_string(static_cast(type)); + secret_final.reserve(secret_type.length() + 1 + secret_length); + secret_final += secret_type; + secret_final += '\n'; + secret_final += secret; + } + discord_presence.partyId = party_id.c_str(); + discord_presence.joinSecret = secret_final.c_str(); + Discord_UpdatePresence(&discord_presence); #endif } +std::string CreateSecretFromIPAddress(const std::string& ip_address, int port) +{ + const std::string port_string = std::to_string(port); + std::string secret; + secret.reserve(ip_address.length() + 1 + port_string.length()); + secret += ip_address; + secret += ':'; + secret += port_string; + return secret; +} + void Shutdown() { #ifdef USE_DISCORD_PRESENCE diff --git a/Source/Core/UICommon/DiscordPresence.h b/Source/Core/UICommon/DiscordPresence.h index 9c4673f885..70ff1e46fb 100644 --- a/Source/Core/UICommon/DiscordPresence.h +++ b/Source/Core/UICommon/DiscordPresence.h @@ -4,10 +4,33 @@ #pragma once +#include +#include + namespace Discord { +class Handler +{ +public: + virtual ~Handler(); + virtual void DiscordJoin() = 0; + virtual void DiscordJoinRequest(const char* id, const std::string& discord_tag, + const char* avatar) = 0; +}; + +enum class SecretType +{ + Empty, + IPAddress, + RoomID, +}; + void Init(); -void UpdateDiscordPresence(); +void InitNetPlayFunctionality(Handler& handler); +void CallPendingCallbacks(); +void UpdateDiscordPresence(int party_size = 0, SecretType type = SecretType::Empty, + const std::string& secret = {}, const std::string& current_game = {}); +std::string CreateSecretFromIPAddress(const std::string& ip_address, int port); void Shutdown(); void SetDiscordPresenceEnabled(bool enabled); } // namespace Discord