Merge pull request #7249 from yourWaifu/discord-rpc-join

Add Discord Join Net Play functionally
This commit is contained in:
Pierre Bourdon 2018-08-19 13:43:33 +02:00 committed by GitHub
commit 0fdb6f4267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 574 additions and 6 deletions

View File

@ -29,6 +29,7 @@ public:
bool IsValid() const; bool IsValid() const;
void SetCookies(const std::string& cookies); void SetCookies(const std::string& cookies);
void UseIPv4();
Response Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload, Response Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload,
size_t size); size_t size);
@ -62,6 +63,11 @@ void HttpRequest::SetCookies(const std::string& cookies)
m_impl->SetCookies(cookies); m_impl->SetCookies(cookies);
} }
void HttpRequest::UseIPv4()
{
m_impl->UseIPv4();
}
HttpRequest::Response HttpRequest::Get(const std::string& url, const Headers& headers) HttpRequest::Response HttpRequest::Get(const std::string& url, const Headers& headers)
{ {
return m_impl->Fetch(url, Impl::Method::GET, headers, nullptr, 0); 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()); 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) static size_t CurlWriteCallback(char* data, size_t size, size_t nmemb, void* userdata)
{ {
auto* buffer = static_cast<std::vector<u8>*>(userdata); auto* buffer = static_cast<std::vector<u8>*>(userdata);

View File

@ -32,6 +32,7 @@ public:
using Headers = std::map<std::string, std::optional<std::string>>; using Headers = std::map<std::string, std::optional<std::string>>;
void SetCookies(const std::string& cookies); void SetCookies(const std::string& cookies);
void UseIPv4();
Response Get(const std::string& url, const Headers& headers = {}); Response Get(const std::string& url, const Headers& headers = {});
Response Post(const std::string& url, const std::vector<u8>& payload, Response Post(const std::string& url, const std::vector<u8>& payload,
const Headers& headers = {}); const Headers& headers = {});

View File

@ -1330,6 +1330,7 @@ void NetPlayClient::OnTraversalStateChanged()
Disconnect(); Disconnect();
m_dialog->OnTraversalError(m_traversal_client->GetFailureReason()); m_dialog->OnTraversalError(m_traversal_client->GetFailureReason());
} }
m_dialog->OnTraversalStateChanged(state);
} }
// called from ---NETPLAY--- thread // called from ---NETPLAY--- thread

View File

@ -47,6 +47,7 @@ public:
virtual void OnConnectionLost() = 0; virtual void OnConnectionLost() = 0;
virtual void OnConnectionError(const std::string& message) = 0; virtual void OnConnectionError(const std::string& message) = 0;
virtual void OnTraversalError(TraversalClient::FailureReason error) = 0; virtual void OnTraversalError(TraversalClient::FailureReason error) = 0;
virtual void OnTraversalStateChanged(TraversalClient::State state) = 0;
virtual void OnSaveDataSyncFailure() = 0; virtual void OnSaveDataSyncFailure() = 0;
virtual bool IsRecording() = 0; virtual bool IsRecording() = 0;

View File

@ -807,8 +807,15 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
void NetPlayServer::OnTraversalStateChanged() 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->OnTraversalError(m_traversal_client->GetFailureReason());
m_dialog->OnTraversalStateChanged(state);
} }
// called from ---GUI--- thread // called from ---GUI--- thread

View File

@ -8,6 +8,8 @@ set(CMAKE_AUTOMOC ON)
add_executable(dolphin-emu add_executable(dolphin-emu
AboutDialog.cpp AboutDialog.cpp
CheatsManager.cpp CheatsManager.cpp
DiscordHandler.cpp
DiscordJoinRequestDialog.cpp
FIFO/FIFOPlayerWindow.cpp FIFO/FIFOPlayerWindow.cpp
FIFO/FIFOAnalyzer.cpp FIFO/FIFOAnalyzer.cpp
HotkeyScheduler.cpp HotkeyScheduler.cpp

View File

@ -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 <iterator>
#include <QApplication>
#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<std::mutex> 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<std::mutex> 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

View File

@ -0,0 +1,49 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <list>
#include <mutex>
#include <thread>
#include <QObject>
#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<DiscordJoinRequestDialog> m_request_dialogs;
std::mutex m_request_dialogs_mutex;
#endif
};

View File

@ -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 <QGridLayout>
#include <QLabel>
#include <QPixmap>
#include <QPushButton>
#include <discord-rpc/include/discord_rpc.h>
#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<uint>(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

View File

@ -0,0 +1,35 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <QDialog>
#include <ctime>
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;
};

View File

@ -107,6 +107,8 @@
<QtMoc Include="Config\PatchesWidget.h" /> <QtMoc Include="Config\PatchesWidget.h" />
<QtMoc Include="Config\PropertiesDialog.h" /> <QtMoc Include="Config\PropertiesDialog.h" />
<QtMoc Include="Config\SettingsWindow.h" /> <QtMoc Include="Config\SettingsWindow.h" />
<QtMoc Include="DiscordHandler.h" />
<QtMoc Include="DiscordJoinRequestDialog.h" />
<QtMoc Include="FIFO\FIFOAnalyzer.h" /> <QtMoc Include="FIFO\FIFOAnalyzer.h" />
<QtMoc Include="FIFO\FIFOPlayerWindow.h" /> <QtMoc Include="FIFO\FIFOPlayerWindow.h" />
<QtMoc Include="TAS\GCTASInputWindow.h" /> <QtMoc Include="TAS\GCTASInputWindow.h" />
@ -175,6 +177,8 @@
<ClCompile Include="$(QtMocOutPrefix)CodeViewWidget.cpp" /> <ClCompile Include="$(QtMocOutPrefix)CodeViewWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)CodeWidget.cpp" /> <ClCompile Include="$(QtMocOutPrefix)CodeWidget.cpp" />
<ClCompile Include="$(QtMocOutPrefix)ControllersWindow.cpp" /> <ClCompile Include="$(QtMocOutPrefix)ControllersWindow.cpp" />
<ClCompile Include="$(QtMocOutPrefix)DiscordHandler.cpp" />
<ClCompile Include="$(QtMocOutPrefix)DiscordJoinRequestDialog.cpp" />
<ClCompile Include="$(QtMocOutPrefix)DoubleClickEventFilter.cpp" /> <ClCompile Include="$(QtMocOutPrefix)DoubleClickEventFilter.cpp" />
<ClCompile Include="$(QtMocOutPrefix)ElidedButton.cpp" /> <ClCompile Include="$(QtMocOutPrefix)ElidedButton.cpp" />
<ClCompile Include="$(QtMocOutPrefix)FlowLayout.cpp" /> <ClCompile Include="$(QtMocOutPrefix)FlowLayout.cpp" />
@ -320,6 +324,8 @@
<ClCompile Include="Debugger\JITWidget.cpp" /> <ClCompile Include="Debugger\JITWidget.cpp" />
<ClCompile Include="Debugger\MemoryWidget.cpp" /> <ClCompile Include="Debugger\MemoryWidget.cpp" />
<ClCompile Include="Debugger\MemoryViewWidget.cpp" /> <ClCompile Include="Debugger\MemoryViewWidget.cpp" />
<ClCompile Include="DiscordHandler.cpp" />
<ClCompile Include="DiscordJoinRequestDialog.cpp" />
<ClCompile Include="FIFO\FIFOAnalyzer.cpp" /> <ClCompile Include="FIFO\FIFOAnalyzer.cpp" />
<ClCompile Include="FIFO\FIFOPlayerWindow.cpp" /> <ClCompile Include="FIFO\FIFOPlayerWindow.cpp" />
<ClCompile Include="QtUtils\WinIconHelper.cpp" /> <ClCompile Include="QtUtils\WinIconHelper.cpp" />

View File

@ -67,6 +67,7 @@
#include "DolphinQt/Debugger/MemoryWidget.h" #include "DolphinQt/Debugger/MemoryWidget.h"
#include "DolphinQt/Debugger/RegisterWidget.h" #include "DolphinQt/Debugger/RegisterWidget.h"
#include "DolphinQt/Debugger/WatchWidget.h" #include "DolphinQt/Debugger/WatchWidget.h"
#include "DolphinQt/DiscordHandler.h"
#include "DolphinQt/FIFO/FIFOPlayerWindow.h" #include "DolphinQt/FIFO/FIFOPlayerWindow.h"
#include "DolphinQt/GCMemcardManager.h" #include "DolphinQt/GCMemcardManager.h"
#include "DolphinQt/GameList/GameList.h" #include "DolphinQt/GameList/GameList.h"
@ -646,7 +647,8 @@ void MainWindow::OnStopComplete()
HideRenderWidget(); HideRenderWidget();
EnableScreenSaver(true); EnableScreenSaver(true);
#ifdef USE_DISCORD_PRESENCE #ifdef USE_DISCORD_PRESENCE
Discord::UpdateDiscordPresence(); if (!m_netplay_dialog->isVisible())
Discord::UpdateDiscordPresence();
#endif #endif
SetFullScreenResolution(false); SetFullScreenResolution(false);
@ -800,7 +802,8 @@ void MainWindow::StartGame(std::unique_ptr<BootParameters>&& parameters)
ShowRenderWidget(); ShowRenderWidget();
#ifdef USE_DISCORD_PRESENCE #ifdef USE_DISCORD_PRESENCE
Discord::UpdateDiscordPresence(); if (!NetPlay::IsNetPlayRunning())
Discord::UpdateDiscordPresence();
#endif #endif
if (SConfig::GetInstance().bFullscreen) if (SConfig::GetInstance().bFullscreen)
@ -1051,6 +1054,9 @@ void MainWindow::NetPlayInit()
{ {
m_netplay_setup_dialog = new NetPlaySetupDialog(this); m_netplay_setup_dialog = new NetPlaySetupDialog(this);
m_netplay_dialog = new NetPlayDialog; m_netplay_dialog = new NetPlayDialog;
#ifdef USE_DISCORD_PRESENCE
m_netplay_discord = new DiscordHandler(this);
#endif
connect(m_netplay_dialog, &NetPlayDialog::Boot, this, connect(m_netplay_dialog, &NetPlayDialog::Boot, this,
[this](const QString& path) { StartGame(path); }); [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_dialog, &NetPlayDialog::rejected, this, &MainWindow::NetPlayQuit);
connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Join, this, &MainWindow::NetPlayJoin); connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Join, this, &MainWindow::NetPlayJoin);
connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Host, this, &MainWindow::NetPlayHost); 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() bool MainWindow::NetPlayJoin()
@ -1176,6 +1188,9 @@ void MainWindow::NetPlayQuit()
{ {
Settings::Instance().ResetNetPlayClient(); Settings::Instance().ResetNetPlayClient();
Settings::Instance().ResetNetPlayServer(); Settings::Instance().ResetNetPlayServer();
#ifdef USE_DISCORD_PRESENCE
Discord::UpdateDiscordPresence();
#endif
} }
void MainWindow::EnableScreenSaver(bool enable) void MainWindow::EnableScreenSaver(bool enable)

View File

@ -19,6 +19,7 @@ struct BootParameters;
class CheatsManager; class CheatsManager;
class CodeWidget; class CodeWidget;
class ControllersWindow; class ControllersWindow;
class DiscordHandler;
class DragEnterEvent; class DragEnterEvent;
class FIFOPlayerWindow; class FIFOPlayerWindow;
class GameList; class GameList;
@ -183,6 +184,7 @@ private:
ControllersWindow* m_controllers_window; ControllersWindow* m_controllers_window;
SettingsWindow* m_settings_window; SettingsWindow* m_settings_window;
NetPlayDialog* m_netplay_dialog; NetPlayDialog* m_netplay_dialog;
DiscordHandler* m_netplay_discord;
NetPlaySetupDialog* m_netplay_setup_dialog; NetPlaySetupDialog* m_netplay_setup_dialog;
GraphicsWindow* m_graphics_window; GraphicsWindow* m_graphics_window;
static constexpr int num_gc_controllers = 4; static constexpr int num_gc_controllers = 4;

View File

@ -29,10 +29,12 @@
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
#include "Common/Config/Config.h" #include "Common/Config/Config.h"
#include "Common/HttpRequest.h"
#include "Common/TraversalClient.h" #include "Common/TraversalClient.h"
#include "Core/Config/GraphicsSettings.h" #include "Core/Config/GraphicsSettings.h"
#include "Core/Config/MainSettings.h" #include "Core/Config/MainSettings.h"
#include "Core/Config/NetplaySettings.h"
#include "Core/Config/SYSCONFSettings.h" #include "Core/Config/SYSCONFSettings.h"
#include "Core/ConfigLoaders/GameConfigLoader.h" #include "Core/ConfigLoaders/GameConfigLoader.h"
#include "Core/ConfigManager.h" #include "Core/ConfigManager.h"
@ -49,6 +51,7 @@
#include "DolphinQt/Resources.h" #include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h" #include "DolphinQt/Settings.h"
#include "UICommon/DiscordPresence.h"
#include "UICommon/GameFile.h" #include "UICommon/GameFile.h"
#include "VideoCommon/VideoConfig.h" #include "VideoCommon/VideoConfig.h"
@ -418,6 +421,7 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal)
m_nickname = nickname; m_nickname = nickname;
m_use_traversal = use_traversal; m_use_traversal = use_traversal;
m_buffer_size = 0; m_buffer_size = 0;
m_old_player_count = 0;
m_room_box->clear(); m_room_box->clear();
m_chat_edit->clear(); m_chat_edit->clear();
@ -458,6 +462,59 @@ void NetPlayDialog::show(std::string nickname, bool use_traversal)
UpdateGUI(); 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() void NetPlayDialog::UpdateGUI()
{ {
auto client = Settings::Instance().GetNetPlayClient(); auto client = Settings::Instance().GetNetPlayClient();
@ -565,6 +622,12 @@ void NetPlayDialog::UpdateGUI()
m_hostcode_action_button->setText(tr("Copy")); m_hostcode_action_button->setText(tr("Copy"));
m_hostcode_action_button->setEnabled(true); m_hostcode_action_button->setEnabled(true);
} }
if (m_old_player_count != m_player_count)
{
UpdateDiscordPresence();
m_old_player_count = m_player_count;
}
} }
// NetPlayUI methods // NetPlayUI methods
@ -632,6 +695,7 @@ void NetPlayDialog::OnMsgChangeGame(const std::string& title)
QueueOnObject(this, [this, qtitle, title] { QueueOnObject(this, [this, qtitle, title] {
m_game_button->setText(qtitle); m_game_button->setText(qtitle);
m_current_game = title; m_current_game = title;
UpdateDiscordPresence();
}); });
DisplayMessage(tr("Game changed to \"%1\"").arg(qtitle), "magenta"); DisplayMessage(tr("Game changed to \"%1\"").arg(qtitle), "magenta");
} }
@ -669,11 +733,13 @@ void NetPlayDialog::OnMsgStartGame()
auto client = Settings::Instance().GetNetPlayClient(); auto client = Settings::Instance().GetNetPlayClient();
if (client) if (client)
client->StartGame(FindGame(m_current_game)); client->StartGame(FindGame(m_current_game));
UpdateDiscordPresence();
}); });
} }
void NetPlayDialog::OnMsgStopGame() void NetPlayDialog::OnMsgStopGame()
{ {
QueueOnObject(this, [this] { UpdateDiscordPresence(); });
} }
void NetPlayDialog::OnPadBufferChanged(u32 buffer) 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() void NetPlayDialog::OnSaveDataSyncFailure()
{ {
QueueOnObject(this, [this] { SetOptionsEnabled(true); }); QueueOnObject(this, [this] { SetOptionsEnabled(true); });

View File

@ -51,6 +51,7 @@ public:
void OnConnectionLost() override; void OnConnectionLost() override;
void OnConnectionError(const std::string& message) override; void OnConnectionError(const std::string& message) override;
void OnTraversalError(TraversalClient::FailureReason error) override; void OnTraversalError(TraversalClient::FailureReason error) override;
void OnTraversalStateChanged(TraversalClient::State state) override;
void OnSaveDataSyncFailure() override; void OnSaveDataSyncFailure() override;
bool IsRecording() override; bool IsRecording() override;
@ -73,6 +74,7 @@ private:
void OnStart(); void OnStart();
void DisplayMessage(const QString& msg, const std::string& color, void DisplayMessage(const QString& msg, const std::string& color,
int duration = OSD::Duration::NORMAL); int duration = OSD::Duration::NORMAL);
void UpdateDiscordPresence();
void UpdateGUI(); void UpdateGUI();
void GameStatusChanged(bool running); void GameStatusChanged(bool running);
void SetOptionsEnabled(bool enabled); void SetOptionsEnabled(bool enabled);
@ -113,6 +115,7 @@ private:
MD5Dialog* m_md5_dialog; MD5Dialog* m_md5_dialog;
PadMappingDialog* m_pad_mapping; PadMappingDialog* m_pad_mapping;
std::string m_current_game; std::string m_current_game;
std::string m_external_ip_address;
std::string m_nickname; std::string m_nickname;
GameListModel* m_game_list_model = nullptr; GameListModel* m_game_list_model = nullptr;
bool m_use_traversal = false; bool m_use_traversal = false;
@ -120,4 +123,5 @@ private:
bool m_got_stop_request = true; bool m_got_stop_request = true;
int m_buffer_size = 0; int m_buffer_size = 0;
int m_player_count = 0; int m_player_count = 0;
int m_old_player_count = 0;
}; };

View File

@ -3,6 +3,11 @@
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include "UICommon/DiscordPresence.h" #include "UICommon/DiscordPresence.h"
#include "Common/Hash.h"
#include "Common/StringUtil.h"
#include "Core/Config/NetplaySettings.h"
#include "Core/Config/UISettings.h" #include "Core/Config/UISettings.h"
#include "Core/ConfigManager.h" #include "Core/ConfigManager.h"
@ -15,6 +20,72 @@
namespace Discord 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<SecretType>(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() void Init()
{ {
#ifdef USE_DISCORD_PRESENCE #ifdef USE_DISCORD_PRESENCE
@ -22,29 +93,102 @@ void Init()
return; return;
DiscordEventHandlers handlers = {}; 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 // The number is the client ID for Dolphin, it's used for images and the appication name
Discord_Initialize("455712169795780630", &handlers, 1, nullptr); Discord_Initialize("455712169795780630", &handlers, 1, nullptr);
UpdateDiscordPresence(); UpdateDiscordPresence();
#endif #endif
} }
void UpdateDiscordPresence() void CallPendingCallbacks()
{ {
#ifdef USE_DISCORD_PRESENCE #ifdef USE_DISCORD_PRESENCE
if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
return; 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 = {}; DiscordRichPresence discord_presence = {};
discord_presence.largeImageKey = "dolphin_logo"; discord_presence.largeImageKey = "dolphin_logo";
discord_presence.largeImageText = "Dolphin is an emulator for the GameCube and the Wii."; 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.details = title.empty() ? "Not in-game" : title.c_str();
discord_presence.startTimestamp = std::time(nullptr); 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<const u8*>(secret.c_str()), secret_length));
const std::string secret_type = std::to_string(static_cast<int>(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); Discord_UpdatePresence(&discord_presence);
#endif #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() void Shutdown()
{ {
#ifdef USE_DISCORD_PRESENCE #ifdef USE_DISCORD_PRESENCE

View File

@ -4,10 +4,33 @@
#pragma once #pragma once
#include <functional>
#include <string>
namespace Discord 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 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 Shutdown();
void SetDiscordPresenceEnabled(bool enabled); void SetDiscordPresenceEnabled(bool enabled);
} // namespace Discord } // namespace Discord