diff --git a/src/common/log_channels.h b/src/common/log_channels.h index 083c84e9e..d887315cd 100644 --- a/src/common/log_channels.h +++ b/src/common/log_channels.h @@ -16,6 +16,7 @@ X(Controller) \ X(Core) \ X(CueParser) \ + X(DiscordPresence) \ X(DInputSource) \ X(DMA) \ X(DynamicLibrary) \ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index e2d0294bf..0c13417ba 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -38,6 +38,8 @@ add_library(core ddgo_controller.h digital_controller.cpp digital_controller.h + discord_presence.cpp + discord_presence.h dma.cpp dma.h fullscreenui.cpp diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index c19b7c52a..40460d864 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -10,6 +10,7 @@ #include "cheats.h" #include "core.h" #include "cpu_core.h" +#include "discord_presence.h" #include "fullscreenui.h" #include "fullscreenui_private.h" #include "game_list.h" @@ -1098,9 +1099,9 @@ void Achievements::UpdateRichPresence(std::unique_lock& lo INFO_LOG("Rich presence updated: {}", s_state.rich_presence_string); - lock.unlock(); - System::UpdateRichPresence(false); - lock.lock(); +#ifdef ENABLE_DISCORD_PRESENCE + DiscordPresence::UpdateDetails(GetCurrentGameBadgeURL(), s_state.rich_presence_string); +#endif } void Achievements::OnSystemStarting(CDImage* image, bool disable_hardcore_mode) @@ -1337,6 +1338,10 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, // update progress database on first load, in case it was played on another PC UpdateGameSummary(true); +#ifdef ENABLE_DISCORD_PRESENCE + DiscordPresence::UpdateDetails(s_state.game_badge_url, s_state.rich_presence_string); +#endif + // Defer starting the prefetch, because otherwise when loading state we'll block until it's all downloaded. // TODO: This can be removed once we're counting requests. if (g_settings.achievements_prefetch_badges) @@ -1355,6 +1360,10 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message, void Achievements::ClearGameInfo() { +#ifdef ENABLE_DISCORD_PRESENCE + DiscordPresence::UpdateDetails({}, {}); +#endif + FullscreenUI::ClearAchievementsState(); s_state.active_leaderboard_trackers = {}; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 0140ea772..fcc2717c8 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -31,6 +31,7 @@ + @@ -118,6 +119,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index d0a2cdb50..3afaeecdb 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -73,6 +73,7 @@ + @@ -158,6 +159,7 @@ + diff --git a/src/core/discord_presence.cpp b/src/core/discord_presence.cpp new file mode 100644 index 000000000..0f60049e3 --- /dev/null +++ b/src/core/discord_presence.cpp @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "discord_presence.h" + +#ifdef ENABLE_DISCORD_PRESENCE + +#include "achievements.h" +#include "game_database.h" +#include "system.h" + +#include "common/dynamic_library.h" +#include "common/error.h" +#include "common/log.h" +#include "common/string_util.h" + +#include "discord_rpc.h" + +#include + +LOG_CHANNEL(DiscordPresence); + +#define DISCORD_RPC_FUNCTIONS(X) \ + X(Discord_Initialize) \ + X(Discord_Shutdown) \ + X(Discord_RunCallbacks) \ + X(Discord_UpdatePresence) \ + X(Discord_ClearPresence) + +namespace DiscordPresence { + +static bool OpenDiscordRPC(Error* error); +static void CloseDiscordRPC(); + +namespace { +struct Locals +{ + bool discord_presence_active; + + std::time_t session_start_time; + std::string current_state; + std::string current_image_url; + + DynamicLibrary rpc_library; + +#define ADD_FUNC(F) decltype(&::F) rpc_##F; + DISCORD_RPC_FUNCTIONS(ADD_FUNC) +#undef ADD_FUNC +}; +} // namespace + +static Locals s_locals; + +} // namespace DiscordPresence + +bool DiscordPresence::OpenDiscordRPC(Error* error) +{ + if (s_locals.rpc_library.IsOpen()) + return true; + + const std::string libname = DynamicLibrary::GetVersionedFilename("discord-rpc"); + if (!s_locals.rpc_library.Open(libname.c_str(), error)) + { + Error::AddPrefix(error, "Failed to load discord-rpc: "); + return false; + } + +#define LOAD_FUNC(F) \ + if (!s_locals.rpc_library.GetSymbol(#F, &s_locals.rpc_##F)) \ + { \ + Error::SetStringFmt(error, "Failed to find function {}", #F); \ + CloseDiscordRPC(); \ + return false; \ + } + DISCORD_RPC_FUNCTIONS(LOAD_FUNC) +#undef LOAD_FUNC + + return true; +} + +void DiscordPresence::CloseDiscordRPC() +{ + if (!s_locals.rpc_library.IsOpen()) + return; + +#define UNLOAD_FUNC(F) s_locals.rpc_##F = nullptr; + DISCORD_RPC_FUNCTIONS(UNLOAD_FUNC) +#undef UNLOAD_FUNC + + s_locals.rpc_library.Close(); +} + +bool DiscordPresence::Initialize() +{ + if (s_locals.discord_presence_active) + return true; + + Error error; + if (!OpenDiscordRPC(&error)) + { + ERROR_LOG("Failed to open discord-rpc: {}", error.GetDescription()); + return false; + } + + DiscordEventHandlers handlers = {}; + s_locals.rpc_Discord_Initialize("705325712680288296", &handlers, 0, nullptr); + s_locals.discord_presence_active = true; + s_locals.session_start_time = std::time(nullptr); + + INFO_LOG("Discord Rich Presence initialized successfully"); + + if (const auto lock = Achievements::GetLock(); Achievements::HasActiveGame()) + UpdateDetails(Achievements::GetCurrentGameBadgeURL(), Achievements::GetRichPresenceString()); + + Update(true); + return true; +} + +void DiscordPresence::Shutdown() +{ + if (!s_locals.discord_presence_active) + return; + + INFO_LOG("Shutting down Discord Rich Presence..."); + + s_locals.discord_presence_active = false; + + s_locals.rpc_Discord_ClearPresence(); + s_locals.rpc_Discord_Shutdown(); + CloseDiscordRPC(); +} + +void DiscordPresence::Poll() +{ + if (!s_locals.discord_presence_active) + return; + + s_locals.rpc_Discord_RunCallbacks(); +} + +void DiscordPresence::Update(bool is_new_session) +{ + if (!s_locals.discord_presence_active) + return; + + if (is_new_session) + s_locals.session_start_time = std::time(nullptr); + + // https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields + DiscordRichPresence rp = {}; + rp.largeImageKey = s_locals.current_image_url.empty() ? "duckstation_logo" : s_locals.current_image_url.c_str(); + rp.largeImageText = "DuckStation PS1/PSX Emulator"; + rp.startTimestamp = s_locals.session_start_time; + + TinyString game_details("No Game Running"); + if (System::IsValidOrInitializing()) + { + // Use disc set name if it's not a custom title. + const bool custom_title = System::IsRunningGameCustomTitle(); + const GameDatabase::Entry* game_entry = System::GetGameDatabaseEntry(); + if (!custom_title && game_entry && game_entry->disc_set) + { + game_details = game_entry->disc_set->GetDisplayTitle(true); + } + else + { + if (const std::string& game_title = System::GetGameTitle(); !game_title.empty()) + game_details = game_title; + else + game_details = "Unknown Game"; + } + } + rp.details = game_details.c_str(); + rp.state = s_locals.current_state.empty() ? nullptr : s_locals.current_state.c_str(); + + INFO_LOG("Updating: '{}' state='{}', badge='{}'", game_details, s_locals.current_state, rp.largeImageKey); + + s_locals.rpc_Discord_UpdatePresence(&rp); +} + +void DiscordPresence::UpdateDetails(std::string_view badge_url, std::string_view state) +{ + if (badge_url.empty()) + s_locals.current_image_url = {}; + else if (s_locals.current_image_url != badge_url) + s_locals.current_image_url = badge_url; + + if (state.empty()) + s_locals.current_state = {}; + else + s_locals.current_state = StringUtil::Ellipsise(state, 128); + + Update(false); +} + +#endif // ENABLE_DISCORD_PRESENCE \ No newline at end of file diff --git a/src/core/discord_presence.h b/src/core/discord_presence.h new file mode 100644 index 000000000..9d14234b7 --- /dev/null +++ b/src/core/discord_presence.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "common/types.h" + +#include + +// Discord presence is available on all platforms except Android. +#ifndef __ANDROID__ +#define ENABLE_DISCORD_PRESENCE +#endif + +#ifdef ENABLE_DISCORD_PRESENCE + +namespace DiscordPresence { + +bool Initialize(); +void Shutdown(); +void Poll(); + +/// Called when rich presence changes. +void Update(bool is_new_session); + +void UpdateDetails(std::string_view badge_url, std::string_view state); + +} // namespace DiscordPresence + +#endif // ENABLE_DISCORD_PRESENCE diff --git a/src/core/system.cpp b/src/core/system.cpp index 86ba1784c..a08eb20fe 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -13,6 +13,7 @@ #include "cpu_code_cache.h" #include "cpu_core.h" #include "cpu_pgxp.h" +#include "discord_presence.h" #include "dma.h" #include "fullscreenui.h" #include "game_database.h" @@ -61,7 +62,6 @@ #include "common/align.h" #include "common/binary_reader_writer.h" -#include "common/dynamic_library.h" #include "common/error.h" #include "common/file_system.h" #include "common/layered_settings_interface.h" @@ -99,10 +99,6 @@ LOG_CHANNEL(System); #include #endif -#ifndef __ANDROID__ -#define ENABLE_DISCORD_PRESENCE 1 -#endif - // #define PROFILE_MEMORY_SAVE_STATES 1 SystemBootParameters::SystemBootParameters() = default; @@ -235,12 +231,6 @@ static bool DoRunahead(); static void UpdateSessionTime(const std::string& prev_serial); -#ifdef ENABLE_DISCORD_PRESENCE -static void InitializeDiscordPresence(); -static void ShutdownDiscordPresence(); -static void PollDiscordPresence(); -#endif - namespace { struct ALIGN_TO_CACHE_LINE StateVars @@ -334,11 +324,6 @@ struct ALIGN_TO_CACHE_LINE StateVars // process start time Timer::Value process_start_time = 0; - -#ifdef ENABLE_DISCORD_PRESENCE - bool discord_presence_active = false; - std::time_t discord_presence_time_epoch; -#endif }; } // namespace @@ -537,7 +522,7 @@ bool System::CoreThreadInitialize(Error* error) #ifdef ENABLE_DISCORD_PRESENCE if (g_settings.enable_discord_presence) - InitializeDiscordPresence(); + DiscordPresence::Initialize(); #endif return true; @@ -546,7 +531,7 @@ bool System::CoreThreadInitialize(Error* error) void System::CoreThreadShutdown() { #ifdef ENABLE_DISCORD_PRESENCE - ShutdownDiscordPresence(); + DiscordPresence::Shutdown(); #endif Achievements::Shutdown(); @@ -587,7 +572,7 @@ void System::IdlePollUpdate() InputManager::PollSources(); #ifdef ENABLE_DISCORD_PRESENCE - PollDiscordPresence(); + DiscordPresence::Poll(); #endif HTTPCache::PollRequests(); @@ -802,6 +787,11 @@ GameHash System::GetGameHash() return s_state.running_game_hash; } +bool System::IsRunningGameCustomTitle() +{ + return s_state.running_game_custom_title; +} + bool System::IsRunningUnknownGame() { return !s_state.running_game_entry; @@ -2203,7 +2193,9 @@ void System::ClearRunningGame() Host::OnSystemGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, s_state.running_game_hash); - UpdateRichPresence(true); +#ifdef ENABLE_DISCORD_PRESENCE + DiscordPresence::Update(true); +#endif } void System::Execute() @@ -2261,7 +2253,7 @@ void System::FrameDone() } #ifdef ENABLE_DISCORD_PRESENCE - PollDiscordPresence(); + DiscordPresence::Poll(); #endif #ifdef ENABLE_GDB_SERVER @@ -4382,7 +4374,9 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo if (s_state.running_game_serial != prev_serial) UpdateSessionTime(prev_serial); - UpdateRichPresence(booting); +#ifdef ENABLE_DISCORD_PRESENCE + DiscordPresence::Update(booting); +#endif if (!s_state.running_game_title.empty()) { @@ -4985,9 +4979,9 @@ void System::CheckForSettingsChanges(const Settings& old_settings) if (g_settings.enable_discord_presence != old_settings.enable_discord_presence) { if (g_settings.enable_discord_presence) - InitializeDiscordPresence(); + DiscordPresence::Initialize(); else - ShutdownDiscordPresence(); + DiscordPresence::Shutdown(); } #endif @@ -6427,153 +6421,3 @@ u64 System::GetSessionPlayedTime() const Timer::Value ctime = Timer::GetCurrentValue(); return static_cast(std::round(Timer::ConvertValueToSeconds(ctime - s_state.session_start_time))); } - -#ifdef ENABLE_DISCORD_PRESENCE - -#include "discord_rpc.h" - -#define DISCORD_RPC_FUNCTIONS(X) \ - X(Discord_Initialize) \ - X(Discord_Shutdown) \ - X(Discord_RunCallbacks) \ - X(Discord_UpdatePresence) \ - X(Discord_ClearPresence) - -namespace dyn_libs { -static bool OpenDiscordRPC(Error* error); -static void CloseDiscordRPC(); - -static DynamicLibrary s_discord_rpc_library; - -#define ADD_FUNC(F) static decltype(&::F) F; -DISCORD_RPC_FUNCTIONS(ADD_FUNC) -#undef ADD_FUNC -} // namespace dyn_libs - -bool dyn_libs::OpenDiscordRPC(Error* error) -{ - if (s_discord_rpc_library.IsOpen()) - return true; - - const std::string libname = DynamicLibrary::GetVersionedFilename("discord-rpc"); - if (!s_discord_rpc_library.Open(libname.c_str(), error)) - { - Error::AddPrefix(error, "Failed to load discord-rpc: "); - return false; - } - -#define LOAD_FUNC(F) \ - if (!s_discord_rpc_library.GetSymbol(#F, &F)) \ - { \ - Error::SetStringFmt(error, "Failed to find function {}", #F); \ - CloseDiscordRPC(); \ - return false; \ - } - DISCORD_RPC_FUNCTIONS(LOAD_FUNC) -#undef LOAD_FUNC - - return true; -} - -void dyn_libs::CloseDiscordRPC() -{ - if (!s_discord_rpc_library.IsOpen()) - return; - -#define UNLOAD_FUNC(F) F = nullptr; - DISCORD_RPC_FUNCTIONS(UNLOAD_FUNC) -#undef UNLOAD_FUNC - - s_discord_rpc_library.Close(); -} - -void System::InitializeDiscordPresence() -{ - if (s_state.discord_presence_active) - return; - - Error error; - if (!dyn_libs::OpenDiscordRPC(&error)) - { - ERROR_LOG("Failed to open discord-rpc: {}", error.GetDescription()); - return; - } - - DiscordEventHandlers handlers = {}; - dyn_libs::Discord_Initialize("705325712680288296", &handlers, 0, nullptr); - s_state.discord_presence_active = true; - - UpdateRichPresence(true); -} - -void System::ShutdownDiscordPresence() -{ - if (!s_state.discord_presence_active) - return; - - dyn_libs::Discord_ClearPresence(); - dyn_libs::Discord_Shutdown(); - dyn_libs::CloseDiscordRPC(); - - s_state.discord_presence_active = false; -} - -void System::UpdateRichPresence(bool update_session_time) -{ - if (!s_state.discord_presence_active) - return; - - if (update_session_time) - s_state.discord_presence_time_epoch = std::time(nullptr); - - // https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields - DiscordRichPresence rp = {}; - rp.largeImageKey = "duckstation_logo"; - rp.largeImageText = "DuckStation PS1/PSX Emulator"; - rp.startTimestamp = s_state.discord_presence_time_epoch; - - TinyString game_details("No Game Running"); - if (IsValidOrInitializing()) - { - // Use disc set name if it's not a custom title. - if (!s_state.running_game_custom_title && s_state.running_game_entry && s_state.running_game_entry->disc_set) - { - game_details = s_state.running_game_entry->disc_set->GetDisplayTitle(true); - } - else - { - if (s_state.running_game_title.empty()) - game_details = "Unknown Game"; - else - game_details = std::string_view(s_state.running_game_title); - } - } - rp.details = game_details.c_str(); - - const auto lock = Achievements::GetLock(); - - std::string state_string; - if (Achievements::HasRichPresence()) - rp.state = (state_string = StringUtil::Ellipsise(Achievements::GetRichPresenceString(), 128)).c_str(); - - if (const std::string& badge_url = Achievements::GetCurrentGameBadgeURL(); !badge_url.empty()) - rp.largeImageKey = badge_url.c_str(); - - dyn_libs::Discord_UpdatePresence(&rp); -} - -void System::PollDiscordPresence() -{ - if (!s_state.discord_presence_active) - return; - - dyn_libs::Discord_RunCallbacks(); -} - -#else - -void System::UpdateRichPresence(bool update_session_time) -{ -} - -#endif diff --git a/src/core/system.h b/src/core/system.h index 0c5b4e7e0..f5d9bc3f2 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -234,6 +234,7 @@ const std::string& GetGamePath(); const std::string& GetExeOverride(); const GameDatabase::Entry* GetGameDatabaseEntry(); GameHash GetGameHash(); +bool IsRunningGameCustomTitle(); bool IsRunningUnknownGame(); bool IsUsingKnownPS1BIOS(); BootMode GetBootMode(); @@ -462,9 +463,6 @@ void CalculateRewindMemoryUsage(u32 num_saves, u32 resolution_scale, u32 multisa void ClearMemorySaveStates(bool reallocate_resources, bool recycle_textures); void SetRunaheadReplayFlag(bool is_analog_input); -/// Called when rich presence changes. -void UpdateRichPresence(bool update_session_time); - } // namespace System namespace Host {