DiscordPresence.cpp 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // Copyright 2018 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "UICommon/DiscordPresence.h"
  4. #include "Core/Config/NetplaySettings.h"
  5. #include "Core/Config/UISettings.h"
  6. #include "Core/ConfigManager.h"
  7. #ifdef USE_DISCORD_PRESENCE
  8. #include <algorithm>
  9. #include <ctime>
  10. #include <set>
  11. #include <string>
  12. #include <discord_rpc.h>
  13. #include <fmt/format.h>
  14. #include "Common/Hash.h"
  15. #include "Common/HttpRequest.h"
  16. #include "Common/StringUtil.h"
  17. #include "Core/AchievementManager.h"
  18. #include "Core/Config/AchievementSettings.h"
  19. #include "Core/System.h"
  20. #endif
  21. namespace Discord
  22. {
  23. #ifdef USE_DISCORD_PRESENCE
  24. static bool s_using_custom_client = false;
  25. namespace
  26. {
  27. Handler* event_handler = nullptr;
  28. const char* username = "";
  29. static int64_t s_start_timestamp = std::chrono::duration_cast<std::chrono::seconds>(
  30. std::chrono::system_clock::now().time_since_epoch())
  31. .count();
  32. void HandleDiscordReady(const DiscordUser* user)
  33. {
  34. username = user->username;
  35. }
  36. void HandleDiscordJoinRequest(const DiscordUser* user)
  37. {
  38. if (event_handler == nullptr)
  39. return;
  40. const std::string discord_tag = fmt::format("{}#{}", user->username, user->discriminator);
  41. event_handler->DiscordJoinRequest(user->userId, discord_tag, user->avatar);
  42. }
  43. void HandleDiscordJoin(const char* join_secret)
  44. {
  45. if (event_handler == nullptr)
  46. return;
  47. if (Config::Get(Config::NETPLAY_NICKNAME) == Config::NETPLAY_NICKNAME.GetDefaultValue())
  48. Config::SetCurrent(Config::NETPLAY_NICKNAME, username);
  49. std::string secret(join_secret);
  50. std::string type = secret.substr(0, secret.find('\n'));
  51. size_t offset = type.length() + 1;
  52. switch (static_cast<SecretType>(std::stol(type)))
  53. {
  54. default:
  55. case SecretType::Empty:
  56. return;
  57. case SecretType::IPAddress:
  58. {
  59. // SetBaseOrCurrent will save the ip address, which isn't what's wanted in this situation
  60. Config::SetCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, "direct");
  61. std::string host = secret.substr(offset, secret.find_last_of(':') - offset);
  62. Config::SetCurrent(Config::NETPLAY_ADDRESS, host);
  63. offset += host.length();
  64. if (secret[offset] == ':')
  65. Config::SetCurrent(Config::NETPLAY_CONNECT_PORT, std::stoul(secret.substr(offset + 1)));
  66. }
  67. break;
  68. case SecretType::RoomID:
  69. {
  70. Config::SetCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, "traversal");
  71. Config::SetCurrent(Config::NETPLAY_HOST_CODE, secret.substr(offset));
  72. }
  73. break;
  74. }
  75. event_handler->DiscordJoin();
  76. }
  77. std::string ArtworkForGameId()
  78. {
  79. const DiscIO::Region region = SConfig::GetInstance().m_region;
  80. const bool is_wii = Core::System::GetInstance().IsWii();
  81. const std::string region_code = SConfig::GetInstance().GetGameTDBImageRegionCode(is_wii, region);
  82. static constexpr char cover_url[] = "https://discord.dolphin-emu.org/cover-art/{}/{}.png";
  83. return fmt::format(cover_url, region_code, SConfig::GetInstance().GetGameTDBID());
  84. }
  85. } // namespace
  86. #endif
  87. Discord::Handler::~Handler() = default;
  88. void Init()
  89. {
  90. #ifdef USE_DISCORD_PRESENCE
  91. if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  92. return;
  93. DiscordEventHandlers handlers = {};
  94. handlers.ready = HandleDiscordReady;
  95. handlers.joinRequest = HandleDiscordJoinRequest;
  96. handlers.joinGame = HandleDiscordJoin;
  97. Discord_Initialize(DEFAULT_CLIENT_ID.c_str(), &handlers, 1, nullptr);
  98. UpdateDiscordPresence();
  99. #endif
  100. }
  101. void UpdateClientID(const std::string& new_client)
  102. {
  103. #ifdef USE_DISCORD_PRESENCE
  104. if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  105. return;
  106. s_using_custom_client = new_client.empty() || new_client.compare(DEFAULT_CLIENT_ID) != 0;
  107. Shutdown();
  108. if (s_using_custom_client)
  109. Discord_Initialize(new_client.c_str(), nullptr, 0, nullptr);
  110. else // if initialising dolphin's client ID, make sure to restore event handlers
  111. Init();
  112. #endif
  113. }
  114. void CallPendingCallbacks()
  115. {
  116. #ifdef USE_DISCORD_PRESENCE
  117. if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  118. return;
  119. Discord_RunCallbacks();
  120. #endif
  121. }
  122. void InitNetPlayFunctionality(Handler& handler)
  123. {
  124. #ifdef USE_DISCORD_PRESENCE
  125. event_handler = &handler;
  126. #endif
  127. }
  128. bool UpdateDiscordPresenceRaw(const std::string& details, const std::string& state,
  129. const std::string& large_image_key,
  130. const std::string& large_image_text,
  131. const std::string& small_image_key,
  132. const std::string& small_image_text, const int64_t start_timestamp,
  133. const int64_t end_timestamp, const int party_size,
  134. const int party_max)
  135. {
  136. #ifdef USE_DISCORD_PRESENCE
  137. if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  138. return false;
  139. // only /dev/dolphin sets this, don't let homebrew change official client ID raw presence
  140. if (!s_using_custom_client)
  141. return false;
  142. DiscordRichPresence discord_presence = {};
  143. discord_presence.details = details.c_str();
  144. discord_presence.state = state.c_str();
  145. discord_presence.largeImageKey = large_image_key.c_str();
  146. discord_presence.largeImageText = large_image_text.c_str();
  147. discord_presence.smallImageKey = small_image_key.c_str();
  148. discord_presence.smallImageText = small_image_text.c_str();
  149. discord_presence.startTimestamp = start_timestamp;
  150. discord_presence.endTimestamp = end_timestamp;
  151. discord_presence.partySize = party_size;
  152. discord_presence.partyMax = party_max;
  153. Discord_UpdatePresence(&discord_presence);
  154. return true;
  155. #else
  156. return false;
  157. #endif
  158. }
  159. void UpdateDiscordPresence(int party_size, SecretType type, const std::string& secret,
  160. const std::string& current_game, bool reset_timer)
  161. {
  162. #ifdef USE_DISCORD_PRESENCE
  163. if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  164. return;
  165. // reset the client ID if running homebrew has changed it
  166. if (s_using_custom_client)
  167. UpdateClientID(DEFAULT_CLIENT_ID);
  168. const std::string& title =
  169. current_game.empty() ? SConfig::GetInstance().GetTitleDescription() : current_game;
  170. std::string game_artwork =
  171. SConfig::GetInstance().GetGameTDBID().empty() ? "" : ArtworkForGameId();
  172. DiscordRichPresence discord_presence = {};
  173. if (game_artwork.empty())
  174. {
  175. discord_presence.largeImageKey = "dolphin_logo";
  176. discord_presence.largeImageText = "Dolphin is an emulator for the GameCube and the Wii.";
  177. }
  178. else
  179. {
  180. discord_presence.largeImageKey = game_artwork.c_str();
  181. discord_presence.largeImageText = title.c_str();
  182. discord_presence.smallImageKey = "dolphin_logo";
  183. discord_presence.smallImageText = "Dolphin is an emulator for the GameCube and the Wii.";
  184. }
  185. discord_presence.details = title.empty() ? "Not in-game" : title.c_str();
  186. if (reset_timer)
  187. {
  188. s_start_timestamp = std::chrono::duration_cast<std::chrono::seconds>(
  189. std::chrono::system_clock::now().time_since_epoch())
  190. .count();
  191. }
  192. discord_presence.startTimestamp = s_start_timestamp;
  193. #ifdef USE_RETRO_ACHIEVEMENTS
  194. std::string state_string;
  195. #endif // USE_RETRO_ACHIEVEMENTS
  196. if (party_size > 0)
  197. {
  198. if (party_size < 4)
  199. {
  200. discord_presence.state = "In a party";
  201. discord_presence.partySize = party_size;
  202. discord_presence.partyMax = 4;
  203. }
  204. else
  205. {
  206. // others can still join to spectate
  207. discord_presence.state = "In a full party";
  208. discord_presence.partySize = party_size;
  209. // Note: joining still works without partyMax
  210. }
  211. }
  212. #ifdef USE_RETRO_ACHIEVEMENTS
  213. else if (Config::Get(Config::RA_ENABLED) && Config::Get(Config::RA_DISCORD_PRESENCE_ENABLED))
  214. {
  215. state_string = AchievementManager::GetInstance().GetRichPresence().data();
  216. if (state_string.length() >= 128)
  217. {
  218. // 124 characters + 3 dots + null terminator - thanks to Stenzek for format
  219. state_string.resize(124);
  220. state_string += "...";
  221. }
  222. discord_presence.state = state_string.c_str();
  223. }
  224. #endif // USE_RETRO_ACHIEVEMENTS
  225. std::string party_id;
  226. std::string secret_final;
  227. if (type != SecretType::Empty)
  228. {
  229. // Declearing party_id or secret_final here will deallocate the variable before passing the
  230. // values over to Discord_UpdatePresence.
  231. const size_t secret_length = secret.length();
  232. party_id = std::to_string(
  233. Common::HashAdler32(reinterpret_cast<const u8*>(secret.c_str()), secret_length));
  234. const std::string secret_type = std::to_string(static_cast<int>(type));
  235. secret_final.reserve(secret_type.length() + 1 + secret_length);
  236. secret_final += secret_type;
  237. secret_final += '\n';
  238. secret_final += secret;
  239. }
  240. discord_presence.partyId = party_id.c_str();
  241. discord_presence.joinSecret = secret_final.c_str();
  242. Discord_UpdatePresence(&discord_presence);
  243. #endif
  244. }
  245. std::string CreateSecretFromIPAddress(const std::string& ip_address, int port)
  246. {
  247. const std::string port_string = std::to_string(port);
  248. std::string secret;
  249. secret.reserve(ip_address.length() + 1 + port_string.length());
  250. secret += ip_address;
  251. secret += ':';
  252. secret += port_string;
  253. return secret;
  254. }
  255. void Shutdown()
  256. {
  257. #ifdef USE_DISCORD_PRESENCE
  258. if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  259. return;
  260. Discord_ClearPresence();
  261. Discord_Shutdown();
  262. #endif
  263. }
  264. void SetDiscordPresenceEnabled(bool enabled)
  265. {
  266. if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE) == enabled)
  267. return;
  268. if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  269. Discord::Shutdown();
  270. Config::SetBase(Config::MAIN_USE_DISCORD_PRESENCE, enabled);
  271. if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
  272. Discord::Init();
  273. }
  274. } // namespace Discord