From 0dd3c62a435d78704429d76862c7f051a5410437 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 9 Mar 2021 21:47:33 -0500 Subject: [PATCH] Discord rich presence (#4500) * WIP RPC support * Might have windows support now, don't peek * Windows support * RichPresence: __SWITCH__ => DISABLE_RPC (for MOBILE_STK support) * RichPresence: Handle JSON strings according to spec, support for addons icon * RichPresence: use translated difficulty name * RichPresence: disable when client_id=-1 * RichPresence: thread connection, show server name on RPC * RichPresence: destroy on close * RichPresence: don't compile methods at all if DISABLE_RPC * RichPresence: fix windows compile (untested) * RichPresence: fix for mac * RichPresence: Linux needs MSG_NOSIGNAL still * RichPresence: fix memory leaks, don't spam update while not connected * RichPresence: free thread on terminate * RichPresence: handle initial registration * RichPresence: fix compiler warning --- data/po/en.po | 4 + sources.cmake | 2 +- src/config/hardware_stats.hpp | 49 +++- src/config/user_config.hpp | 8 + src/io/rich_presence.cpp | 495 ++++++++++++++++++++++++++++++++++ src/io/rich_presence.hpp | 42 +++ src/main.cpp | 3 + src/main_loop.cpp | 3 + src/modes/world_status.cpp | 3 +- src/modes/world_status.hpp | 7 + src/race/race_manager.cpp | 5 + 11 files changed, 616 insertions(+), 5 deletions(-) create mode 100644 src/io/rich_presence.cpp create mode 100644 src/io/rich_presence.hpp diff --git a/data/po/en.po b/data/po/en.po index cc94750e7..eea6e15e1 100644 --- a/data/po/en.po +++ b/data/po/en.po @@ -6297,3 +6297,7 @@ msgstr "If you need more stability, consider using the stable version: %s" #: supertuxkart.appdata.xml:44 msgid "SuperTuxKart Team" msgstr "SuperTuxKart Team" + +#: rich_presence.cpp:296 +msgid "Getting ready to race" +msgstr "Getting ready to race" diff --git a/sources.cmake b/sources.cmake index ba4868d71..d4f28ae4d 100644 --- a/sources.cmake +++ b/sources.cmake @@ -1,5 +1,5 @@ # Modify this file to change the last-modified date when you add/remove a file. -# This will then trigger a new cmake run automatically. +# This will then trigger a new cmake run automatically. file(GLOB_RECURSE STK_HEADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.hpp") file(GLOB_RECURSE STK_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.cpp") file(GLOB_RECURSE STK_SHADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "data/shaders/*") diff --git a/src/config/hardware_stats.hpp b/src/config/hardware_stats.hpp index 7558c040c..df882e516 100644 --- a/src/config/hardware_stats.hpp +++ b/src/config/hardware_stats.hpp @@ -42,6 +42,49 @@ namespace HardwareStats m_data ="{"; } // Constructor + const std::string sanitize(const std::string &value) + { + // Really confusing. Basically converts between utf8 and wide and irrlicht strings and std strings + std::wstring wide = StringUtils::utf8ToWide(value).c_str(); + std::string normalized = StringUtils::wideToUtf8(sanitize(wide).c_str()).c_str(); + return normalized; + } + + const std::wstring sanitize(std::wstring value) + { + // A string is a sequence of Unicode code points wrapped with quotation marks (U+0022). All code points may + // be placed within the quotation marks except for the code points that must be escaped: quotation mark + // (U+0022), reverse solidus (U+005C), and the control characters U+0000 to U+001F. There are two-character + // escape sequence representations of some characters. + + wchar_t temp[7] = {0}; + for(size_t i = 0; i < value.size(); ++i) + { + if (value[i] <= 0x1f) + { + swprintf(temp, sizeof(temp) / sizeof(wchar_t), L"\\u%04x", value[i]); + std::wstring suffix = value.substr(i + 1); + value = value.substr(0, i); + value.append(temp); + value.append(suffix); + i += 5; // \u0000 = 6 chars, but we're replacing one so 5 + } + else if (value[i] == '"' || value[i] == '\\') + { + char escaped = value[i]; + std::wstring suffix = value.substr(i + 1); + value = value.substr(0, i); + value.push_back('\\'); + value.push_back(escaped); + value.append(suffix); + // Skip the added solidus + ++i; + } + } + + return value; + } + // -------------------------------------------------------------------- /** Adds a key-value pair to the json string. */ template @@ -49,7 +92,7 @@ namespace HardwareStats { if(m_data.size()>1) // more than '{' m_data += ","; - m_data += "\""+key+"\":"+StringUtils::toString(value); + m_data += "\""+sanitize(key)+"\":"+StringUtils::toString(value); } // add // -------------------------------------------------------------------- /** Specialisation for adding string values. String values in @@ -58,7 +101,7 @@ namespace HardwareStats { if(m_data.size()>1) // more than '{' m_data += ","; - m_data += "\""+key+"\":\""+StringUtils::toString(value)+"\""; + m_data += "\""+sanitize(key)+"\":\""+StringUtils::toString(sanitize(value))+"\""; } // add // -------------------------------------------------------------------- /** Specialisation for adding character pointers. String values in @@ -67,7 +110,7 @@ namespace HardwareStats { if(m_data.size()>1) // more than '{' m_data += ","; - m_data += "\""+key+"\":\""+StringUtils::toString(s)+"\""; + m_data += "\""+sanitize(key)+"\":\""+StringUtils::toString(sanitize(s))+"\""; } // add // -------------------------------------------------------------------- void finish() diff --git a/src/config/user_config.hpp b/src/config/user_config.hpp index 102604dc4..e3f1ff0be 100644 --- a/src/config/user_config.hpp +++ b/src/config/user_config.hpp @@ -1152,6 +1152,14 @@ namespace UserConfigParams PARAM_DEFAULT( StringUserConfigParam("all", "last_track_group", "Last selected track group") ); + PARAM_PREFIX StringUserConfigParam m_discord_client_id + PARAM_DEFAULT( StringUserConfigParam("817760324983324753", "discord_client_id", + "Discord Client ID (Set to -1 to disable)") ); + + PARAM_PREFIX BoolUserConfigParam m_rich_presence_debug + PARAM_DEFAULT( BoolUserConfigParam(false, "rich_presence_debug", + "If debug logging should be enabled for rich presence") ); + PARAM_PREFIX StringUserConfigParam m_skin_file PARAM_DEFAULT( StringUserConfigParam("peach", "skin_name", "Name of the skin to use") ); diff --git a/src/io/rich_presence.cpp b/src/io/rich_presence.cpp new file mode 100644 index 000000000..95839a983 --- /dev/null +++ b/src/io/rich_presence.cpp @@ -0,0 +1,495 @@ +#include "utils/time.hpp" +#include "utils/string_utils.hpp" +#include "race/race_manager.hpp" +#include "io/rich_presence.hpp" +#include "config/player_manager.hpp" +#include "config/player_profile.hpp" +#include "modes/world.hpp" +#include "config/hardware_stats.hpp" +#include "config/user_config.hpp" +#include "tracks/track_manager.hpp" +#include "tracks/track.hpp" +#include "karts/abstract_kart.hpp" +#include "karts/kart_properties.hpp" +#include "utils/translation.hpp" +#include "network/protocols/client_lobby.hpp" +#include "network/protocols/lobby_protocol.hpp" +#include "network/server.hpp" + +#include +#include + +#if defined(__SWITCH__) || defined(MOBILE_STK) || defined(SERVER_ONLY) +#define DISABLE_RPC +#endif + +#if !defined(WIN32) && !defined(DISABLE_RPC) +#include +#include +#include +#elif defined(WIN32) +#include +#include +#include +#endif + +namespace RichPresenceNS { +RichPresence* g_rich_presence = nullptr; + +RichPresence* RichPresence::get() { + if (g_rich_presence == nullptr) + { + g_rich_presence = new RichPresence(); + } + return g_rich_presence; +} + +void RichPresence::destroy() { + if (g_rich_presence != nullptr) + { + delete g_rich_presence; + } +} + +RichPresence::RichPresence() : m_connected(false), m_ready(false), m_last(0), +#ifdef WIN32 + m_socket(INVALID_HANDLE_VALUE), +#else + m_socket(-1), +#endif + m_thread(nullptr) { + doConnect(); +} +RichPresence::~RichPresence() { + terminate(); +} + +void RichPresence::terminate() { +#ifndef DISABLE_RPC +#ifdef WIN32 +#define UNCLEAN m_socket != INVALID_HANDLE_VALUE +#else +#define UNCLEAN m_socket != -1 +#endif + if (m_connected || UNCLEAN) + { + if (UNCLEAN && !m_connected) + Log::fatal("RichPresence", "RichPresence terminated uncleanly! Socket is %d", m_socket); +#ifndef WIN32 + close(m_socket); + m_socket = -1; +#else + CloseHandle(m_socket); + m_socket = INVALID_HANDLE_VALUE; +#endif + m_connected = false; + m_ready = false; + } + if(m_thread != nullptr && STKProcess::getType() == PT_MAIN) + { + m_thread->join(); + delete m_thread; + m_thread = nullptr; + } +#endif // DISABLE_RPC +} + +bool RichPresence::doConnect() { +#ifndef DISABLE_RPC + if (std::string(UserConfigParams::m_discord_client_id) == "-1") + return false; +#ifndef DISABLE_RPC + // Just in case we're retrying or something: + terminate(); +#if !defined(WIN32) && defined(AF_UNIX) + m_socket = socket(AF_UNIX, SOCK_STREAM, 0); + if (m_socket < 0) + { + if (UserConfigParams::m_rich_presence_debug) + perror("Couldn't open a Unix socket!"); + return false; + } +#ifdef SO_NOSIGPIPE + const int set = 1; + setsockopt(m_socket, SOL_SOCKET, SO_NOSIGPIPE, &set, sizeof(set)); +#endif + + // Discord tries these env vars in order: + char* env; + std::string basePath = ""; +#define TRY_ENV(path) env = std::getenv(path); \ + if (env != nullptr) \ + {\ + basePath = env; \ + goto completed; \ + } + + TRY_ENV("XDG_RUNTIME_DIR") + TRY_ENV("TMPDIR") + TRY_ENV("TMP") + TRY_ENV("TEMP") +#undef TRY_ENV + // Falls back to /tmp + basePath = "/tmp"; + completed: + basePath = basePath + "/"; +#elif defined(WIN32) + // Windows uses named pipes + std::string basePath = "\\\\?\\pipe\\"; +#endif + // Discord will only bind up to socket 9 + for (int i = 0; i < 10; ++i) + { + if (tryConnect(basePath + "discord-ipc-" + StringUtils::toString(i))) + break; + } + + if (m_connected) + { + if (UserConfigParams::m_rich_presence_debug) + Log::info("RichPresence", "Connection opened with Discord!"); + m_thread = new std::thread(finishConnection, this); + return true; + } + else + { + // Force cleanup: + m_connected = true; + terminate(); + return false; + } +#else + return false; +#endif +#endif // DISABLE_RPC +} + +void RichPresence::readData() { +#ifndef DISABLE_RPC + size_t baseLength = sizeof(int32_t) * 2; + struct discordPacket* basePacket = (struct discordPacket*) malloc(baseLength); +#ifdef WIN32 + DWORD read; + if (!ReadFile(m_socket, basePacket, baseLength, &read, NULL)) + { + Log::error("RichPresence", "Couldn't read from pipe! Error %x", GetLastError()); + free(basePacket); + terminate(); + return; + } +#else + int read = recv(m_socket, basePacket, baseLength, 0); + if (read == -1) + { + if (UserConfigParams::m_rich_presence_debug) + perror("Couldn't read data from socket!"); + terminate(); + return; + } +#endif + // Add one char so we can printf easy + struct discordPacket* packet = (struct discordPacket*) + malloc(baseLength + basePacket->length + sizeof(char)); + // Copy over length and opcode from base packet + memcpy(packet, basePacket, baseLength); + free(basePacket); +#ifdef WIN32 + if (!ReadFile(m_socket, packet->data, packet->length, &read, NULL)) + { + Log::error("RichPresence", "Couldn't read from pipe! Error %x", GetLastError()); + free(packet); + terminate(); + return; + } +#else + read = recv(m_socket, packet->data, packet->length, 0); + if (read == -1) + { + terminate(); + } +#endif + + packet->data[packet->length] = '\0'; + if (UserConfigParams::m_rich_presence_debug) + Log::debug("RichPresence", "<= (OP %d len=%d) %s is data (READ %d bytes)", + packet->op, packet->length, packet->data, read); + + free(packet); +#endif +} + +void RichPresence::finishConnection(RichPresence* self) { +#ifndef DISABLE_RPC + // We read all the data from the socket. We're clear now to handshake! + self->handshake(); + + // Make sure we get a response! + self->readData(); + + // Okay to go nonblocking now! +#if !defined(WIN32) && defined(O_NONBLOCK) + fcntl(self->m_socket, F_SETFL, O_NONBLOCK); +#endif + + self->m_ready = true; +#endif +} + +bool RichPresence::tryConnect(std::string path) { +#ifndef DISABLE_RPC +#if !defined(WIN32) && defined(AF_UNIX) + struct sockaddr_un addr = { + .sun_family = AF_UNIX + }; + memset(addr.sun_path, 0, sizeof(addr.sun_path)); + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path.c_str()); + if(connect(m_socket, (struct sockaddr *) &addr, sizeof(addr)) == -1) + { + // Something is probably wrong: + if (errno != ENOENT && errno != ECONNREFUSED) + { + perror("Couldn't open Discord socket!"); + } + return false; + } + // Connected! + m_connected = true; +#elif defined(WIN32) + // Windows + m_socket = CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (m_socket == INVALID_HANDLE_VALUE) + { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND) + { + LPSTR errorText = NULL; + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&errorText, + 0, NULL); + + Log::warn("RichPresence", "Couldn't open file! %s Error: %ls", path.c_str(), errorText); + } + return false; + } + m_connected = true; +#endif +#endif // DISABLE_RPC + return m_connected; +} + +void RichPresence::handshake() { +#ifndef DISABLE_RPC + if (UserConfigParams::m_rich_presence_debug) + Log::debug("RichPresence", "Starting handshake..."); + HardwareStats::Json json; + json.add("v", 1); + json.add("client_id", std::string(UserConfigParams::m_discord_client_id)); + json.finish(); + sendData(OP_HANDSHAKE, json.toString()); +#endif +} + +void RichPresence::sendData(int32_t op, std::string json) { +#ifndef DISABLE_RPC + // Handshake will make us ready: + if (op != OP_HANDSHAKE && !m_ready) + { + Log::warn("RichPresence", "Tried sending data while not ready?"); + return; + } + if (UserConfigParams::m_rich_presence_debug) + Log::debug("RichPresence", "=> %s", json.c_str()); + int32_t size = json.size(); + size_t length = (sizeof(int32_t) * 2) + (size * sizeof(char)); + struct discordPacket* packet = (struct discordPacket*) malloc( + length + ); + packet->op = op; + packet->length = size; + // Note we aren't copying the NUL at the end + memcpy(&packet->data, json.c_str(), json.size()); +#if !defined(WIN32) && defined(AF_UNIX) + int flags = 0; +#ifdef MSG_NOSIGNAL + flags |= MSG_NOSIGNAL; +#endif + if (send(m_socket, packet, length, flags) == -1) + { + if (errno != EPIPE) + perror("Couldn't send data to Discord socket!"); + else + { + if (UserConfigParams::m_rich_presence_debug) + Log::debug("RichPresence", "Got an EPIPE, closing"); + // EPIPE, cleanup! + terminate(); + } + } +#elif defined(WIN32) + DWORD written; + WriteFile(m_socket, packet, length, &written, NULL); + // TODO + if(written != length) + { + if (UserConfigParams::m_rich_presence_debug) + Log::debug("RichPresence", "Amount written != data size! Closing"); + terminate(); + } +#endif // AF_UNIX + free(packet); +#endif +} + +void RichPresence::update(bool force) { +#ifndef DISABLE_RPC + if (STKProcess::getType() != PT_MAIN) + { + // Don't update on server thread + return; + } + time_t now = time(NULL); + if ((now - m_last) < 10 && !force) + { + // Only update every 10s + return; + } + // Check more often if we're not ready + if (m_ready || !m_connected) + { + // Update timer + m_last = now; + } + // Retry connection: + if (!m_connected) + { + doConnect(); + } + if (!m_ready) + { + return; + } + if (UserConfigParams::m_rich_presence_debug) + Log::debug("RichPresence", "Updating status!"); + + std::wstring_convert> convert; + + std::string playerName; + PlayerProfile *player = PlayerManager::getCurrentPlayer(); + if (player) + { + if (PlayerManager::getCurrentOnlineState() == PlayerProfile::OS_GUEST || + PlayerManager::getCurrentOnlineState() == PlayerProfile::OS_SIGNED_IN) + { + playerName = convert.to_bytes(player->getLastOnlineName().c_str()) + "@stk"; + } + else + { + playerName = convert.to_bytes(player->getName().c_str()); + } + } + else + playerName = "Guest"; + World* world = World::getWorld(); + RaceManager *raceManager = RaceManager::get(); + std::string trackId = raceManager->getTrackName(); + std::string difficulty = convert.to_bytes(raceManager->getDifficultyName( + raceManager->getDifficulty() + ).c_str()); + std::string minorModeName = convert.to_bytes(raceManager->getNameOf( + raceManager->getMinorMode() + ).c_str()); + // Discord takes the time when we started as unix timestamp + uint64_t since = (now * 1000) - StkTime::getMonoTimeMs(); + if (world) + { + since += world->getStart(); + } + + // {cmd:SET_ACTIVITY,args:{activity:{},pid:0},nonce:0} + + HardwareStats::Json base; + base.add("cmd", "SET_ACTIVITY"); + + HardwareStats::Json args; + HardwareStats::Json activity; + + std::string trackName = convert.to_bytes(_("Getting ready to race").c_str()); + if (world) + { + Track* track = track_manager->getTrack(trackId); + if (track) + trackName = convert.to_bytes(track->getName().c_str()); + } + + auto protocol = LobbyProtocol::get(); + if (protocol != nullptr && protocol.get()->getJoinedServer() != nullptr) + { + trackName.append(" - "); + trackName.append(convert.to_bytes( + protocol.get()->getJoinedServer().get()->getName().c_str() + )); + } + + activity.add("state", std::string(trackName.c_str())); + if (world) + activity.add("details", minorModeName + " (" + difficulty + ")"); + + HardwareStats::Json assets; + if (world) + { + Track* track = track_manager->getTrack(trackId); + assets.add("large_text", convert.to_bytes(track->getName().c_str())); + assets.add("large_image", track->isAddon() ? + "addons" : "track_" + trackId); + AbstractKart *abstractKart = world->getLocalPlayerKart(0); + if (abstractKart) + { + const KartProperties* kart = abstractKart->getKartProperties(); + assets.add("small_image", kart->isAddon() ? + "addons" : "kart_" + abstractKart->getIdent()); + std::string kartName = convert.to_bytes(kart->getName().c_str()); + assets.add("small_text", kartName + " (" + playerName + ")"); + } + } + else + { + assets.add("large_text", "SuperTuxKart"); + assets.add("large_image", "logo"); + assets.add("small_text", playerName); + // std::string filename = std::string(basename(player->getIconFilename().c_str())); + // assets->add("small_image", "kart_" + filename); + } + assets.finish(); + activity.add("assets", assets.toString()); + + HardwareStats::Json timestamps; + timestamps.add("start", std::to_string(since)); + + timestamps.finish(); + activity.add("timestamps", timestamps.toString()); + + activity.finish(); + args.add("activity", activity.toString()); + int pid = 0; +#ifdef WIN32 + pid = _getpid(); +#elif !defined(DISABLE_RPC) + pid = getppid(); +#endif + args.add("pid", pid); + args.finish(); + base.add("nonce", now); + base.add("args", args.toString()); + base.finish(); + + sendData(OP_DATA, base.toString()); +#endif // DISABLE_RPC +} + +} // namespace diff --git a/src/io/rich_presence.hpp b/src/io/rich_presence.hpp new file mode 100644 index 000000000..f83207aa4 --- /dev/null +++ b/src/io/rich_presence.hpp @@ -0,0 +1,42 @@ +#ifdef WIN32 +#include +#endif +#include + +namespace RichPresenceNS { + // There are more, but we don't need to use them + enum OPCodes { + OP_HANDSHAKE = 0, + OP_DATA = 1, + }; + struct discordPacket { + int32_t op; + int32_t length; + char data[]; + }; + class RichPresence { + private: + bool m_connected; + bool m_ready; + time_t m_last; +#ifdef WIN32 + HANDLE m_socket; +#else + int m_socket; +#endif + std::thread* m_thread; + bool tryConnect(std::string path); + bool doConnect(); + void terminate(); + void sendData(int32_t op, std::string json); + void handshake(); + void readData(); + static void finishConnection(RichPresence* self); + public: + RichPresence(); + ~RichPresence(); + void update(bool force); + static RichPresence* get(); + static void destroy(); + }; +} diff --git a/src/main.cpp b/src/main.cpp index 08420249c..32e2d346d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -290,6 +290,7 @@ extern "C" { #include "utils/stk_process.hpp" #include "utils/string_utils.hpp" #include "utils/translation.hpp" +#include "io/rich_presence.hpp" static void cleanSuperTuxKart(); static void cleanUserConfig(); @@ -2573,6 +2574,8 @@ int main(int argc, char *argv[]) cleanSuperTuxKart(); NetworkConfig::destroy(); + RichPresenceNS::RichPresence::destroy(); + #ifdef DEBUG MemoryLeaks::checkForLeaks(); #endif diff --git a/src/main_loop.cpp b/src/main_loop.cpp index c24ce7c4d..e78f9f0f8 100644 --- a/src/main_loop.cpp +++ b/src/main_loop.cpp @@ -56,6 +56,7 @@ #include "utils/string_utils.hpp" #include "utils/time.hpp" #include "utils/translation.hpp" +#include "io/rich_presence.hpp" #ifndef WIN32 #include @@ -714,6 +715,8 @@ void MainLoop::run() } } + RichPresenceNS::RichPresence::get()->update(false); + if (auto gp = GameProtocol::lock()) { gp->sendActions(); diff --git a/src/modes/world_status.cpp b/src/modes/world_status.cpp index 72537571d..e10771350 100644 --- a/src/modes/world_status.cpp +++ b/src/modes/world_status.cpp @@ -38,7 +38,7 @@ #include //----------------------------------------------------------------------------- -WorldStatus::WorldStatus() : m_process_type(STKProcess::getType()) +WorldStatus::WorldStatus() : m_process_type(STKProcess::getType()), m_started_at(StkTime::getMonoTimeMs()) { if (m_process_type == PT_MAIN) main_loop->setFrameBeforeLoadingWorld(); @@ -72,6 +72,7 @@ WorldStatus::WorldStatus() : m_process_type(STKProcess::getType()) */ void WorldStatus::reset(bool restart) { + m_started_at = StkTime::getMonoTimeMs(); m_time = 0.0f; m_time_ticks = 0; m_auxiliary_ticks = 0; diff --git a/src/modes/world_status.hpp b/src/modes/world_status.hpp index b9a1ed352..88535e64d 100644 --- a/src/modes/world_status.hpp +++ b/src/modes/world_status.hpp @@ -111,6 +111,9 @@ private: /** The third sound to be played in ready, set, go. */ SFXBase *m_start_sound; + /** (Unix) time when we started */ + uint64_t m_started_at; + /** The clock mode: normal counting forwards, or countdown */ ClockType m_clock_mode; protected: @@ -200,6 +203,10 @@ public: /** Returns the current race time. */ float getTime() const { return (float)m_time; } + // ------------------------------------------------------------------------ + /** Returns the start time. */ + uint64_t getStart() const { return m_started_at; } + // ------------------------------------------------------------------------ /** Returns the current race time in time ticks (i.e. based on the physics * time step size). */ diff --git a/src/race/race_manager.cpp b/src/race/race_manager.cpp index dcb3e1c55..c2db5829e 100644 --- a/src/race/race_manager.cpp +++ b/src/race/race_manager.cpp @@ -62,6 +62,7 @@ #include "utils/stk_process.hpp" #include "utils/string_utils.hpp" #include "utils/translation.hpp" +#include "io/rich_presence.hpp" #ifdef __SWITCH__ extern "C" { @@ -688,6 +689,8 @@ void RaceManager::startNextRace() #ifdef __SWITCH__ appletSetCpuBoostMode(ApmCpuBoostMode_Normal); #endif + + RichPresenceNS::RichPresence::get()->update(true); } // startNextRace //--------------------------------------------------------------------------------------------- @@ -970,6 +973,8 @@ void RaceManager::exitRace(bool delete_world) m_saved_gp = NULL; m_track_number = 0; + + RichPresenceNS::RichPresence::get()->update(true); } // exitRace //---------------------------------------------------------------------------------------------