diff --git a/.gitmodules b/.gitmodules index ace938e19a..00704ec70f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,3 +34,6 @@ [submodule "cubeb"] path = externals/cubeb url = https://github.com/kinetiknz/cubeb.git +[submodule "discord-rpc"] + path = externals/discord-rpc + url = https://github.com/discordapp/discord-rpc.git diff --git a/.travis/common/travis-ci.env b/.travis/common/travis-ci.env index 7d63674b4b..5fa27a706b 100644 --- a/.travis/common/travis-ci.env +++ b/.travis/common/travis-ci.env @@ -13,3 +13,4 @@ TRAVIS_TAG # citra specific flags ENABLE_COMPATIBILITY_REPORTING +USE_DISCORD_PRESENCE diff --git a/.travis/linux-frozen/docker.sh b/.travis/linux-frozen/docker.sh index fca1419f60..a4814da1a6 100755 --- a/.travis/linux-frozen/docker.sh +++ b/.travis/linux-frozen/docker.sh @@ -3,7 +3,7 @@ cd /citra 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 ctest -VV -C Release diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh index 7828c2b626..2a583d6a5a 100755 --- a/.travis/linux/docker.sh +++ b/.travis/linux/docker.sh @@ -3,7 +3,7 @@ cd /citra 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 ctest -VV -C Release diff --git a/.travis/macos/build.sh b/.travis/macos/build.sh index 281f6b5be5..363e88330f 100755 --- a/.travis/macos/build.sh +++ b/.travis/macos/build.sh @@ -7,7 +7,7 @@ export Qt5_DIR=$(brew --prefix)/opt/qt5 export PATH="/usr/local/opt/ccache/libexec:$PATH" 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 ctest -VV -C Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d67916fb4..51e66eb7c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,8 @@ option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" 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) message(STATUS "Copying pre-commit hook") file(COPY hooks/pre-commit diff --git a/appveyor.yml b/appveyor.yml index 6b910b6857..f44ab5f437 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -44,9 +44,9 @@ before_build: $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} 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 - 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 { - 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 .. diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index fa01363307..139454d69f 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -62,6 +62,18 @@ endif() add_subdirectory(enet) 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) # LibreSSL set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "") @@ -80,9 +92,3 @@ if (ENABLE_WEB_SERVICE) add_library(json-headers INTERFACE) target_include_directories(json-headers INTERFACE ./json) endif() - -# Cubeb -if(ENABLE_CUBEB) - set(BUILD_TESTS OFF CACHE BOOL "") - add_subdirectory(cubeb EXCLUDE_FROM_ALL) -endif() diff --git a/externals/discord-rpc b/externals/discord-rpc new file mode 160000 index 0000000000..3d3ae7129d --- /dev/null +++ b/externals/discord-rpc @@ -0,0 +1 @@ +Subproject commit 3d3ae7129d17643bc706da0a2eea85aafd10ab3a diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 571a30d7c9..fde59da139 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -68,6 +68,7 @@ add_executable(citra-qt debugger/registers.h debugger/wait_tree.cpp debugger/wait_tree.h + discord.h game_list.cpp game_list.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 ${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) install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") endif() diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 3aecc09425..e1cced42da 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -207,6 +207,8 @@ void Config::ReadValues() { qt_config->beginGroup("UI"); 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"); UISettings::values.check_for_update_on_start = @@ -440,6 +442,7 @@ void Config::SaveValues() { qt_config->beginGroup("UI"); WriteSetting("theme", UISettings::values.theme, UISettings::themes[0].second); + WriteSetting("enable_discord_presence", UISettings::values.enable_discord_presence, true); qt_config->beginGroup("Updater"); WriteSetting("check_for_update_on_start", UISettings::values.check_for_update_on_start, true); diff --git a/src/citra_qt/configuration/configure_web.cpp b/src/citra_qt/configuration/configure_web.cpp index 78dc81ca3d..ff66e45d19 100644 --- a/src/citra_qt/configuration/configure_web.cpp +++ b/src/citra_qt/configuration/configure_web.cpp @@ -5,6 +5,7 @@ #include #include #include "citra_qt/configuration/configure_web.h" +#include "citra_qt/ui_settings.h" #include "core/settings.h" #include "core/telemetry_session.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(this, &ConfigureWeb::LoginVerified, this, &ConfigureWeb::OnLoginVerified); +#ifndef USE_DISCORD_PRESENCE + ui->discord_group->setVisible(false); +#endif this->setConfiguration(); } @@ -49,10 +53,13 @@ void ConfigureWeb::setConfiguration() { ui->label_telemetry_id->setText( tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper())); user_verified = true; + + ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence); } void ConfigureWeb::applyConfiguration() { Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); + UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); if (user_verified) { Settings::values.citra_username = ui->edit_username->text().toStdString(); Settings::values.citra_token = ui->edit_token->text().toStdString(); diff --git a/src/citra_qt/configuration/configure_web.ui b/src/citra_qt/configuration/configure_web.ui index dd996ab62c..a8aeda9ada 100644 --- a/src/citra_qt/configuration/configure_web.ui +++ b/src/citra_qt/configuration/configure_web.ui @@ -170,6 +170,22 @@ + + + + Discord Presence + + + + + + Show Current Game in your Discord Status + + + + + + diff --git a/src/citra_qt/discord.h b/src/citra_qt/discord.h new file mode 100644 index 0000000000..a867cc4d6e --- /dev/null +++ b/src/citra_qt/discord.h @@ -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 diff --git a/src/citra_qt/discord_impl.cpp b/src/citra_qt/discord_impl.cpp new file mode 100644 index 0000000000..138ffaa092 --- /dev/null +++ b/src/citra_qt/discord_impl.cpp @@ -0,0 +1,51 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#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::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 diff --git a/src/citra_qt/discord_impl.h b/src/citra_qt/discord_impl.h new file mode 100644 index 0000000000..e714ae6d90 --- /dev/null +++ b/src/citra_qt/discord_impl.h @@ -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 diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 46ea44be0e..8495a83d5d 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -34,6 +34,7 @@ #include "citra_qt/debugger/profiler.h" #include "citra_qt/debugger/registers.h" #include "citra_qt/debugger/wait_tree.h" +#include "citra_qt/discord.h" #include "citra_qt/game_list.h" #include "citra_qt/hotkeys.h" #include "citra_qt/main.h" @@ -58,6 +59,10 @@ #include "core/loader/loader.h" #include "core/settings.h" +#ifdef USE_DISCORD_PRESENCE +#include "citra_qt/discord_impl.h" +#endif + #ifdef QT_STATICPLUGIN Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); #endif @@ -120,6 +125,9 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { default_theme_paths = QIcon::themeSearchPaths(); UpdateUITheme(); + SetDiscordEnabled(UISettings::values.enable_discord_presence); + discord_rpc->Update(); + Network::Init(); InitializeWidgets(); @@ -748,6 +756,7 @@ void GMainWindow::BootGame(const QString& filename) { } void GMainWindow::ShutdownGame() { + discord_rpc->Pause(); emu_thread->RequestStop(); // Release emu threads from any breakpoints @@ -763,6 +772,8 @@ void GMainWindow::ShutdownGame() { emu_thread->wait(); emu_thread = nullptr; + discord_rpc->Update(); + Camera::QtMultimediaCameraHandler::ReleaseHandlers(); // 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_Restart->setEnabled(true); ui.action_Report_Compatibility->setEnabled(true); + + discord_rpc->Update(); } void GMainWindow::OnPauseGame() { @@ -1184,11 +1197,14 @@ void GMainWindow::OnConfigure() { connect(&configureDialog, &ConfigureDialog::languageChanged, this, &GMainWindow::OnLanguageChanged); auto old_theme = UISettings::values.theme; + const bool old_discord_presence = UISettings::values.enable_discord_presence; auto result = configureDialog.exec(); if (result == QDialog::Accepted) { configureDialog.applyConfiguration(); if (UISettings::values.theme != old_theme) UpdateUITheme(); + if (UISettings::values.enable_discord_presence != old_discord_presence) + SetDiscordEnabled(UISettings::values.enable_discord_presence); emit UpdateThemedIcons(); SyncMenuUISettings(); config->Save(); @@ -1481,6 +1497,19 @@ void GMainWindow::RetranslateStatusBar() { multiplayer_state->retranslateUi(); } +void GMainWindow::SetDiscordEnabled(bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + #ifdef main #undef main #endif diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 90d1f86437..4d35d202f9 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -38,6 +38,9 @@ class QProgressBar; class RegistersWidget; class Updater; class WaitTreeWidget; +namespace DiscordRPC { +class DiscordInterface; +} class GMainWindow : public QMainWindow { Q_OBJECT @@ -61,6 +64,7 @@ public: ~GMainWindow(); GameList* game_list; + std::unique_ptr discord_rpc; signals: @@ -108,6 +112,7 @@ private: void ShowUpdatePrompt(); void ShowNoUpdatePrompt(); void CheckForUpdates(); + void SetDiscordEnabled(bool state); /** * Stores the filename in the recently loaded files list. diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index c0743de8e2..6b76a52f18 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -58,6 +58,9 @@ struct Values { bool update_on_close; bool check_for_update_on_start; + // Discord RPC + bool enable_discord_presence; + QString roms_path; QString symbols_path; QString game_dir_deprecated;