// Copyright 2018 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "UICommon/DiscordPresence.h" #include "Core/Config/NetplaySettings.h" #include "Core/Config/UISettings.h" #include "Core/ConfigManager.h" #ifdef USE_DISCORD_PRESENCE #include #include #include #include #include #include #include "Common/Hash.h" #include "Common/StringUtil.h" #endif namespace Discord { #ifdef USE_DISCORD_PRESENCE namespace { Handler* event_handler = nullptr; const char* username = ""; void HandleDiscordReady(const DiscordUser* user) { username = user->username; } void HandleDiscordJoinRequest(const DiscordUser* user) { if (event_handler == nullptr) return; const std::string discord_tag = fmt::format("{}#{}", user->username, user->discriminator); event_handler->DiscordJoinRequest(user->userId, discord_tag, user->avatar); } void HandleDiscordJoin(const char* join_secret) { if (event_handler == nullptr) return; if (Config::Get(Config::NETPLAY_NICKNAME) == Config::NETPLAY_NICKNAME.GetDefaultValue()) 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(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(); } std::string ArtworkForGameId(const std::string& gameid) { static const std::set REGISTERED_GAMES{ "GAF", // GAFE01: Animal Crossing "RUU", // RUUE01: Animal Crossing: City Folk "SF8", // SF8E01: Donkey Kong Country Returns "RDB", // RDBE70: Dragon Ball Z: Budokai Tenkaichi 2 "RDS", // RDSE70: Dragon Ball Z: Budokai Tenkaichi 3 "GFZ", // GFZE01: F-Zero GX "GFE", // GFEE01: Fire Emblem: Path of Radiance "RFE", // RFEE01: Fire Emblem: Radiant Dawn "S5S", // S5SJHF: Inazuma Eleven GO: Strikers 2013 "GKY", // GKYE01: Kirby Air Ride "SUK", // SUKE01: Kirby's Return to Dream Land "GLM", // GLME01: Luigi's Mansion "GFT", // GFTE01: Mario Golf: Toadstool Tour "RMC", // RMCE01: Mario Kart Wii "GM4", // GM4E01: Mario Kart: Double Dash!! "GMP", // GMPE01: Mario Party 4 "GP5", // GP5E01: Mario Party 5 "GP6", // GP6E01: Mario Party 6 "GP7", // GP7E01: Mario Party 7 "RM8", // RM8E01: Mario Party 8 "SSQ", // SSQE01: Mario Party 9 "GOM", // GOME01: Mario Power Tennis "GYQ", // GYQE01: Mario Superstar Baseball "GGS", // GGSE01: Metal Gear Solid: The Twin Snakes "GM8", // GM8E01: Metroid Prime "G2M", // G2ME01: Metroid Prime 2: Echoes "RM3", // RM3E01: Metroid Prime 3: Corruption "R3M", // R3ME01: Metroid Prime: Trilogy "SMN", // SMNE01: New Super Mario Bros. Wii "G8M", // G8ME01: Paper Mario: The Thousand-Year Door "GPI", // GPIE01: Pikmin (GC) "R9I", // R9IE01: Pikmin (Wii) "GPV", // GPVE01: Pikmin 2 (GC) "R92", // R92E01: Pikmin 2 (Wii) "GC6", // GC6E01: Pokemon Colosseum "GXX", // GXXE01: Pokemon XD: Gale of Darkness "GBI", // GBIE08: Resident Evil "GHA", // GHAE08: Resident Evil 2 "GLE", // GLEE08: Resident Evil 3: Nemesis "G4B", // G4BE08: Resident Evil 4 "GSN", // GSNE8P: Sonic Adventure 2: Battle "GXS", // GXSE8P: Sonic Adventure DX: Director's Cut "SNC", // SNCE8P: Sonic Colors "G9S", // G9SE8P: Sonic Heroes "GRS", // GRSEAF: SoulCalibur II "RSL", // RSLEAF: SoulCalibur Legends "GK2", // GK2E52: Spider-Man 2 "GQP", // GQPE78: SpongeBob SquarePants: Battle for Bikini Bottom "SVM", // SVME01: Super Mario All-Stars: 25th Anniversary Edition "RMG", // RMGE01: Super Mario Galaxy "SB4", // SB4E01: Super Mario Galaxy 2 "G4Q", // G4QE01: Super Mario Strikers "GMS", // GMSE01: Super Mario Sunshine "GMB", // GMBE8P: Super Monkey Ball "GM2", // GM2E8P: Super Monkey Ball 2 "R8P", // R8PE01: Super Paper Mario "RSB", // RSBE01: Super Smash Bros. Brawl "GAL", // GALE01: Super Smash Bros. Melee "PZL", // PZLE01: The Legend of Zelda: Collector's Edition "G4S", // G4SE01: The Legend of Zelda: Four Swords Adventures "D43", // D43E01: The Legend of Zelda: Ocarina of Time / Master Quest "SOU", // SOUE01: The Legend of Zelda: Skyward Sword "GZL", // GZLE01: The Legend of Zelda: The Wind Waker "GZ2", // GZ2E01: The Legend of Zelda: Twilight Princess (GC) "RZD", // RZDE01: The Legend of Zelda: Twilight Princess (Wii) "GHQ", // GHQE7D: The Simpsons: Hit & Run "RSP", // RSPE01: Wii Sports "RZT", // RZTE01: Wii Sports Resort "SX4", // SX4E01: Xenoblade Chronicles }; std::string region_neutral_gameid = gameid.substr(0, 3); if (REGISTERED_GAMES.count(region_neutral_gameid) != 0) { // Discord asset keys can only be lowercase. Common::ToLower(®ion_neutral_gameid); return "game_" + region_neutral_gameid; } return ""; } } // namespace #endif Discord::Handler::~Handler() = default; void Init() { #ifdef USE_DISCORD_PRESENCE if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) return; DiscordEventHandlers handlers = {}; handlers.ready = HandleDiscordReady; handlers.joinRequest = HandleDiscordJoinRequest; handlers.joinGame = HandleDiscordJoin; Discord_Initialize(DEFAULT_CLIENT_ID.c_str(), &handlers, 1, nullptr); UpdateDiscordPresence(); #endif } void UpdateClientID(const std::string& new_client) { #ifdef USE_DISCORD_PRESENCE if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) return; s_using_custom_client = new_client.empty() || new_client.compare(DEFAULT_CLIENT_ID) != 0; Shutdown(); if (s_using_custom_client) Discord_Initialize(new_client.c_str(), nullptr, 0, nullptr); else // if initialising dolphin's client ID, make sure to restore event handlers Init(); #endif } void CallPendingCallbacks() { #ifdef USE_DISCORD_PRESENCE if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) return; Discord_RunCallbacks(); #endif } void InitNetPlayFunctionality(Handler& handler) { #ifdef USE_DISCORD_PRESENCE event_handler = &handler; #endif } bool UpdateDiscordPresenceRaw(const std::string& details, const std::string& state, const std::string& large_image_key, const std::string& large_image_text, const std::string& small_image_key, const std::string& small_image_text, const int64_t start_timestamp, const int64_t end_timestamp, const int party_size, const int party_max) { #ifdef USE_DISCORD_PRESENCE if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) return false; // only /dev/dolphin sets this, don't let homebrew change official client ID raw presence if (!s_using_custom_client) return false; DiscordRichPresence discord_presence = {}; discord_presence.details = details.c_str(); discord_presence.state = state.c_str(); discord_presence.largeImageKey = large_image_key.c_str(); discord_presence.largeImageText = large_image_text.c_str(); discord_presence.smallImageKey = small_image_key.c_str(); discord_presence.smallImageText = small_image_text.c_str(); discord_presence.startTimestamp = start_timestamp; discord_presence.endTimestamp = end_timestamp; discord_presence.partySize = party_size; discord_presence.partyMax = party_max; Discord_UpdatePresence(&discord_presence); return true; #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; // reset the client ID if running homebrew has changed it if (s_using_custom_client) UpdateClientID(DEFAULT_CLIENT_ID); const std::string& title = current_game.empty() ? SConfig::GetInstance().GetTitleDescription() : current_game; std::string game_artwork = ArtworkForGameId(SConfig::GetInstance().GetGameID()); DiscordRichPresence discord_presence = {}; if (game_artwork.empty()) { discord_presence.largeImageKey = "dolphin_logo"; discord_presence.largeImageText = "Dolphin is an emulator for the GameCube and the Wii."; } else { discord_presence.largeImageKey = game_artwork.c_str(); discord_presence.largeImageText = title.c_str(); discord_presence.smallImageKey = "dolphin_logo"; discord_presence.smallImageText = "Dolphin is an emulator for the GameCube and the Wii."; } discord_presence.details = title.empty() ? "Not in-game" : title.c_str(); 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(secret.c_str()), secret_length)); const std::string secret_type = std::to_string(static_cast(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); #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() { #ifdef USE_DISCORD_PRESENCE if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) return; Discord_ClearPresence(); Discord_Shutdown(); #endif } void SetDiscordPresenceEnabled(bool enabled) { if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE) == enabled) return; if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) Discord::Shutdown(); Config::SetBase(Config::MAIN_USE_DISCORD_PRESENCE, enabled); if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)) Discord::Init(); } } // namespace Discord