Add Discord Rich Presence Support (#3883)

* Initial Discord RPC support

Build with Discord Presence ON

Fix RPC detection

Fix Time elapsed on pause; will now continue to count.

* Fix CI builds with compile flag

Addressed reviews

Fix silly mistakes

Fix 'Not in-game' display

class instead of namespace

Fix

Revamped

remove redundant code

Using Pimpl pattern

* Implement Null class

* Fix config updation

* Addressed All Reviews

* externals/discord-rpc : Updated to latest commit
This commit is contained in:
Vamsi Krishna 2018-08-20 14:50:33 +05:30 committed by Ben
parent 96c025e4c2
commit 6cb9a45154
19 changed files with 193 additions and 11 deletions

3
.gitmodules vendored
View file

@ -34,3 +34,6 @@
[submodule "cubeb"] [submodule "cubeb"]
path = externals/cubeb path = externals/cubeb
url = https://github.com/kinetiknz/cubeb.git url = https://github.com/kinetiknz/cubeb.git
[submodule "discord-rpc"]
path = externals/discord-rpc
url = https://github.com/discordapp/discord-rpc.git

View file

@ -13,3 +13,4 @@ TRAVIS_TAG
# citra specific flags # citra specific flags
ENABLE_COMPATIBILITY_REPORTING ENABLE_COMPATIBILITY_REPORTING
USE_DISCORD_PRESENCE

View file

@ -3,7 +3,7 @@
cd /citra cd /citra
mkdir build && cd build mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
make -j4 make -j4
ctest -VV -C Release ctest -VV -C Release

View file

@ -3,7 +3,7 @@
cd /citra cd /citra
mkdir build && cd build mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
make -j4 make -j4
ctest -VV -C Release ctest -VV -C Release

View file

@ -7,7 +7,7 @@ export Qt5_DIR=$(brew --prefix)/opt/qt5
export PATH="/usr/local/opt/ccache/libexec:$PATH" export PATH="/usr/local/opt/ccache/libexec:$PATH"
mkdir build && cd build mkdir build && cd build
cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
make -j4 make -j4
ctest -VV -C Release ctest -VV -C Release

View file

@ -20,6 +20,8 @@ option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit)
message(STATUS "Copying pre-commit hook") message(STATUS "Copying pre-commit hook")
file(COPY hooks/pre-commit file(COPY hooks/pre-commit

View file

@ -44,9 +44,9 @@ before_build:
$COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING}
if ($env:BUILD_TYPE -eq 'msvc') { if ($env:BUILD_TYPE -eq 'msvc') {
# redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning
cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1 && exit 0' cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON .. 2>&1 && exit 0'
} else { } else {
C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1" C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON .. 2>&1"
} }
- cd .. - cd ..

View file

@ -62,6 +62,18 @@ endif()
add_subdirectory(enet) add_subdirectory(enet)
target_include_directories(enet INTERFACE ./enet/include) target_include_directories(enet INTERFACE ./enet/include)
# Cubeb
if (ENABLE_CUBEB)
set(BUILD_TESTS OFF CACHE BOOL "")
add_subdirectory(cubeb EXCLUDE_FROM_ALL)
endif()
# DiscordRPC
if (USE_DISCORD_PRESENCE)
add_subdirectory(discord-rpc)
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
endif()
if (ENABLE_WEB_SERVICE) if (ENABLE_WEB_SERVICE)
# LibreSSL # LibreSSL
set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "") set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
@ -80,9 +92,3 @@ if (ENABLE_WEB_SERVICE)
add_library(json-headers INTERFACE) add_library(json-headers INTERFACE)
target_include_directories(json-headers INTERFACE ./json) target_include_directories(json-headers INTERFACE ./json)
endif() endif()
# Cubeb
if(ENABLE_CUBEB)
set(BUILD_TESTS OFF CACHE BOOL "")
add_subdirectory(cubeb EXCLUDE_FROM_ALL)
endif()

1
externals/discord-rpc vendored Submodule

@ -0,0 +1 @@
Subproject commit 3d3ae7129d17643bc706da0a2eea85aafd10ab3a

View file

@ -68,6 +68,7 @@ add_executable(citra-qt
debugger/registers.h debugger/registers.h
debugger/wait_tree.cpp debugger/wait_tree.cpp
debugger/wait_tree.h debugger/wait_tree.h
discord.h
game_list.cpp game_list.cpp
game_list.h game_list.h
game_list_p.h game_list_p.h
@ -201,6 +202,15 @@ target_link_libraries(citra-qt PRIVATE audio_core common core input_common netwo
target_link_libraries(citra-qt PRIVATE Boost::boost glad nihstro-headers Qt5::OpenGL Qt5::Widgets Qt5::Multimedia) target_link_libraries(citra-qt PRIVATE Boost::boost glad nihstro-headers Qt5::OpenGL Qt5::Widgets Qt5::Multimedia)
target_link_libraries(citra-qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) target_link_libraries(citra-qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
if (USE_DISCORD_PRESENCE)
target_sources(citra-qt PUBLIC
discord_impl.cpp
discord_impl.h
)
target_link_libraries(citra-qt PRIVATE discord-rpc)
target_compile_definitions(citra-qt PRIVATE -DUSE_DISCORD_PRESENCE)
endif()
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
endif() endif()

View file

@ -207,6 +207,8 @@ void Config::ReadValues() {
qt_config->beginGroup("UI"); qt_config->beginGroup("UI");
UISettings::values.theme = ReadSetting("theme", UISettings::themes[0].second).toString(); UISettings::values.theme = ReadSetting("theme", UISettings::themes[0].second).toString();
UISettings::values.enable_discord_presence =
ReadSetting("enable_discord_presence", true).toBool();
qt_config->beginGroup("Updater"); qt_config->beginGroup("Updater");
UISettings::values.check_for_update_on_start = UISettings::values.check_for_update_on_start =
@ -440,6 +442,7 @@ void Config::SaveValues() {
qt_config->beginGroup("UI"); qt_config->beginGroup("UI");
WriteSetting("theme", UISettings::values.theme, UISettings::themes[0].second); WriteSetting("theme", UISettings::values.theme, UISettings::themes[0].second);
WriteSetting("enable_discord_presence", UISettings::values.enable_discord_presence, true);
qt_config->beginGroup("Updater"); qt_config->beginGroup("Updater");
WriteSetting("check_for_update_on_start", UISettings::values.check_for_update_on_start, true); WriteSetting("check_for_update_on_start", UISettings::values.check_for_update_on_start, true);

View file

@ -5,6 +5,7 @@
#include <QIcon> #include <QIcon>
#include <QMessageBox> #include <QMessageBox>
#include "citra_qt/configuration/configure_web.h" #include "citra_qt/configuration/configure_web.h"
#include "citra_qt/ui_settings.h"
#include "core/settings.h" #include "core/settings.h"
#include "core/telemetry_session.h" #include "core/telemetry_session.h"
#include "ui_configure_web.h" #include "ui_configure_web.h"
@ -17,6 +18,9 @@ ConfigureWeb::ConfigureWeb(QWidget* parent)
connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin); connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin);
connect(this, &ConfigureWeb::LoginVerified, this, &ConfigureWeb::OnLoginVerified); connect(this, &ConfigureWeb::LoginVerified, this, &ConfigureWeb::OnLoginVerified);
#ifndef USE_DISCORD_PRESENCE
ui->discord_group->setVisible(false);
#endif
this->setConfiguration(); this->setConfiguration();
} }
@ -49,10 +53,13 @@ void ConfigureWeb::setConfiguration() {
ui->label_telemetry_id->setText( ui->label_telemetry_id->setText(
tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper())); tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper()));
user_verified = true; user_verified = true;
ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence);
} }
void ConfigureWeb::applyConfiguration() { void ConfigureWeb::applyConfiguration() {
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked();
if (user_verified) { if (user_verified) {
Settings::values.citra_username = ui->edit_username->text().toStdString(); Settings::values.citra_username = ui->edit_username->text().toStdString();
Settings::values.citra_token = ui->edit_token->text().toStdString(); Settings::values.citra_token = ui->edit_token->text().toStdString();

View file

@ -170,6 +170,22 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QGroupBox" name="discord_group">
<property name="title">
<string>Discord Presence</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_21">
<item>
<widget class="QCheckBox" name="toggle_discordrpc">
<property name="text">
<string>Show Current Game in your Discord Status</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">

25
src/citra_qt/discord.h Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
namespace DiscordRPC {
class DiscordInterface {
public:
virtual ~DiscordInterface() = default;
virtual void Pause() = 0;
virtual void Update() = 0;
};
class NullImpl : public DiscordInterface {
public:
~NullImpl() = default;
void Pause() override {}
void Update() override {}
};
} // namespace DiscordRPC

View file

@ -0,0 +1,51 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <chrono>
#include <string>
#include <discord_rpc.h>
#include "citra_qt/discord_impl.h"
#include "citra_qt/ui_settings.h"
#include "common/common_types.h"
#include "core/core.h"
namespace DiscordRPC {
DiscordImpl::DiscordImpl() {
DiscordEventHandlers handlers{};
// The number is the client ID for Citra, it's used for images and the
// application name
Discord_Initialize("461729900748079114", &handlers, 1, nullptr);
}
DiscordImpl::~DiscordImpl() {
Discord_ClearPresence();
Discord_Shutdown();
}
void DiscordImpl::Pause() {
Discord_ClearPresence();
}
void DiscordImpl::Update() {
s64 start_time = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
std::string title;
if (Core::System::GetInstance().IsPoweredOn())
Core::System::GetInstance().GetAppLoader().ReadTitle(title);
DiscordRichPresence presence{};
presence.largeImageKey = "citra_logo";
presence.largeImageText = "Citra is an emulator for the Nintendo 3DS";
if (Core::System::GetInstance().IsPoweredOn()) {
presence.state = title.c_str();
presence.details = "Currently in game";
} else {
presence.details = "Not in game";
}
presence.startTimestamp = start_time;
Discord_UpdatePresence(&presence);
}
} // namespace DiscordRPC

View file

@ -0,0 +1,20 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "citra_qt/discord.h"
namespace DiscordRPC {
class DiscordImpl : public DiscordInterface {
public:
DiscordImpl();
~DiscordImpl();
void Pause() override;
void Update() override;
};
} // namespace DiscordRPC

View file

@ -34,6 +34,7 @@
#include "citra_qt/debugger/profiler.h" #include "citra_qt/debugger/profiler.h"
#include "citra_qt/debugger/registers.h" #include "citra_qt/debugger/registers.h"
#include "citra_qt/debugger/wait_tree.h" #include "citra_qt/debugger/wait_tree.h"
#include "citra_qt/discord.h"
#include "citra_qt/game_list.h" #include "citra_qt/game_list.h"
#include "citra_qt/hotkeys.h" #include "citra_qt/hotkeys.h"
#include "citra_qt/main.h" #include "citra_qt/main.h"
@ -58,6 +59,10 @@
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "core/settings.h" #include "core/settings.h"
#ifdef USE_DISCORD_PRESENCE
#include "citra_qt/discord_impl.h"
#endif
#ifdef QT_STATICPLUGIN #ifdef QT_STATICPLUGIN
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif #endif
@ -120,6 +125,9 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
default_theme_paths = QIcon::themeSearchPaths(); default_theme_paths = QIcon::themeSearchPaths();
UpdateUITheme(); UpdateUITheme();
SetDiscordEnabled(UISettings::values.enable_discord_presence);
discord_rpc->Update();
Network::Init(); Network::Init();
InitializeWidgets(); InitializeWidgets();
@ -748,6 +756,7 @@ void GMainWindow::BootGame(const QString& filename) {
} }
void GMainWindow::ShutdownGame() { void GMainWindow::ShutdownGame() {
discord_rpc->Pause();
emu_thread->RequestStop(); emu_thread->RequestStop();
// Release emu threads from any breakpoints // Release emu threads from any breakpoints
@ -763,6 +772,8 @@ void GMainWindow::ShutdownGame() {
emu_thread->wait(); emu_thread->wait();
emu_thread = nullptr; emu_thread = nullptr;
discord_rpc->Update();
Camera::QtMultimediaCameraHandler::ReleaseHandlers(); Camera::QtMultimediaCameraHandler::ReleaseHandlers();
// The emulation is stopped, so closing the window or not does not matter anymore // The emulation is stopped, so closing the window or not does not matter anymore
@ -1049,6 +1060,8 @@ void GMainWindow::OnStartGame() {
ui.action_Stop->setEnabled(true); ui.action_Stop->setEnabled(true);
ui.action_Restart->setEnabled(true); ui.action_Restart->setEnabled(true);
ui.action_Report_Compatibility->setEnabled(true); ui.action_Report_Compatibility->setEnabled(true);
discord_rpc->Update();
} }
void GMainWindow::OnPauseGame() { void GMainWindow::OnPauseGame() {
@ -1184,11 +1197,14 @@ void GMainWindow::OnConfigure() {
connect(&configureDialog, &ConfigureDialog::languageChanged, this, connect(&configureDialog, &ConfigureDialog::languageChanged, this,
&GMainWindow::OnLanguageChanged); &GMainWindow::OnLanguageChanged);
auto old_theme = UISettings::values.theme; auto old_theme = UISettings::values.theme;
const bool old_discord_presence = UISettings::values.enable_discord_presence;
auto result = configureDialog.exec(); auto result = configureDialog.exec();
if (result == QDialog::Accepted) { if (result == QDialog::Accepted) {
configureDialog.applyConfiguration(); configureDialog.applyConfiguration();
if (UISettings::values.theme != old_theme) if (UISettings::values.theme != old_theme)
UpdateUITheme(); UpdateUITheme();
if (UISettings::values.enable_discord_presence != old_discord_presence)
SetDiscordEnabled(UISettings::values.enable_discord_presence);
emit UpdateThemedIcons(); emit UpdateThemedIcons();
SyncMenuUISettings(); SyncMenuUISettings();
config->Save(); config->Save();
@ -1481,6 +1497,19 @@ void GMainWindow::RetranslateStatusBar() {
multiplayer_state->retranslateUi(); multiplayer_state->retranslateUi();
} }
void GMainWindow::SetDiscordEnabled(bool state) {
#ifdef USE_DISCORD_PRESENCE
if (state) {
discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>();
} else {
discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
}
#else
discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
#endif
discord_rpc->Update();
}
#ifdef main #ifdef main
#undef main #undef main
#endif #endif

View file

@ -38,6 +38,9 @@ class QProgressBar;
class RegistersWidget; class RegistersWidget;
class Updater; class Updater;
class WaitTreeWidget; class WaitTreeWidget;
namespace DiscordRPC {
class DiscordInterface;
}
class GMainWindow : public QMainWindow { class GMainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
@ -61,6 +64,7 @@ public:
~GMainWindow(); ~GMainWindow();
GameList* game_list; GameList* game_list;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
signals: signals:
@ -108,6 +112,7 @@ private:
void ShowUpdatePrompt(); void ShowUpdatePrompt();
void ShowNoUpdatePrompt(); void ShowNoUpdatePrompt();
void CheckForUpdates(); void CheckForUpdates();
void SetDiscordEnabled(bool state);
/** /**
* Stores the filename in the recently loaded files list. * Stores the filename in the recently loaded files list.

View file

@ -58,6 +58,9 @@ struct Values {
bool update_on_close; bool update_on_close;
bool check_for_update_on_start; bool check_for_update_on_start;
// Discord RPC
bool enable_discord_presence;
QString roms_path; QString roms_path;
QString symbols_path; QString symbols_path;
QString game_dir_deprecated; QString game_dir_deprecated;