#include "discord.h" #include "freespace.h" #include "events/events.h" #include "gamesequence/gamesequence.h" #include "io/timer.h" #include "mission/missioncampaign.h" #include "mission/missionparse.h" #include "parse/parselo.h" #include "playerman/player.h" #if WITH_DISCORD #include "discord_rpc.h" namespace { struct PresenceInfo { SCP_string state; int64_t timestamp; }; bool initialized = false; bool discord_ready = false; bool events_added = false; UI_TIMESTAMP next_mission_update = UI_TIMESTAMP::invalid(); bool in_mission = false; PresenceInfo current_info; const char* APPLICATION_ID = "465270111440470016"; SCP_unordered_map campaign_name_cache; SCP_string get_campaign_name(const char* filename) { SCP_string name = filename; auto iter = campaign_name_cache.find(name); if (iter != campaign_name_cache.end()) { return iter->second; } auto campaign_name = mission_campaign_get_name(filename); campaign_name_cache[name] = campaign_name; return campaign_name; } SCP_string get_current_campaign_name() { if (Game_mode & GM_CAMPAIGN_MODE) { return Campaign.name; } else if (Player != nullptr) { return get_campaign_name(Player->current_campaign); } else { return ""; } } SCP_string get_details() { SCP_string res; auto has_campaign = Player != nullptr && strlen(Player->current_campaign) > 0; if (in_mission && !(Game_mode & GM_CAMPAIGN_MODE)) { // This is a standalone mission so we don't actually have a campaign has_campaign = false; } if (has_campaign && in_mission) { sprintf(res, "%s: %s", get_current_campaign_name().c_str(), The_mission.name.c_str()); } else if (has_campaign) { sprintf(res, "Campaign %s", get_current_campaign_name().c_str()); } else if (in_mission) { res = The_mission.name; } else { res = "In game"; } return res; } void update_presence() { auto details = get_details(); DiscordRichPresence presence; memset(&presence, 0, sizeof(presence)); presence.details = details.c_str(); presence.state = current_info.state.c_str(); presence.startTimestamp = current_info.timestamp; Discord_UpdatePresence(&presence); } void set_presence(const SCP_string& state, int64_t timestamp = 0) { if (current_info.state == state && current_info.timestamp == timestamp) { // No changes return; } current_info.state = state; current_info.timestamp = timestamp; if (discord_ready) { update_presence(); } } void set_game_play_presence() { SCP_string state; sprintf(state, "In mission (%d %s)", Player->stats.m_kill_count_ok, Player->stats.m_kill_count_ok == 1 ? "kill" : "kills"); set_presence(state, (int64_t)time(nullptr) - f2i(Missiontime)); // Update this every 20 seconds since Discord already has a 15 second rate-limit next_mission_update = ui_timestamp(20 * MILLISECONDS_PER_SECOND); } void update_discord() { if (!initialized) return; Discord_RunCallbacks(); if (gameseq_get_state() == GS_STATE_GAME_PLAY && ui_timestamp_elapsed(next_mission_update)) { set_game_play_presence(); } } void shutdown_discord() { if (!initialized) return; Discord_Shutdown(); initialized = false; discord_ready = false; } void handleEnterState(int old_state, int new_state) { if (!initialized) return; if (old_state == new_state) { return; } if (new_state == GS_STATE_GAME_PLAY) { // Update immediately if we enter the game play state again set_game_play_presence(); return; } if (gameseq_get_state_idx(GS_STATE_GAME_PLAY) >= 0) { // Special case for when we come out of game play switch (new_state) { case GS_STATE_OPTIONS_MENU: case GS_STATE_HOTKEY_SCREEN: case GS_STATE_GAMEPLAY_HELP: case GS_STATE_MISSION_LOG_SCROLLBACK: set_presence("In game menu"); return; default: break; } } switch (new_state) { case GS_STATE_MAIN_MENU: set_presence("In main hall"); break; case GS_STATE_BRIEFING: set_presence("In mission briefing"); break; case GS_STATE_CMD_BRIEF: set_presence("In command briefing"); break; case GS_EVENT_DEBRIEF: set_presence("In debriefing"); break; case GS_STATE_GAME_PAUSED: set_presence("Paused"); break; case GS_STATE_RED_ALERT: set_presence("In red-alert briefing"); break; case GS_STATE_INITIAL_PLAYER_SELECT: set_presence("Selecting player"); break; case GS_STATE_FICTION_VIEWER: set_presence("In fiction viewer"); break; default: break; } } void handleLeaveState(int old_state, int /*new_state*/) { if (!initialized) return; if (old_state == GS_STATE_GAME_PAUSED) { // Reset the game play state immediately set_game_play_presence(); } if (old_state == GS_STATE_GAME_PLAY) { in_mission = false; next_mission_update = UI_TIMESTAMP::invalid(); } } void handleMissionLoad(const char* /*filename*/) { if (!initialized) return; set_presence("Loading mission"); in_mission = true; } } // namespace namespace libs { namespace discord { void init() { //If we've already initialized then just return if (initialized) return; DiscordEventHandlers handlers; memset(&handlers, 0, sizeof(handlers)); handlers.ready = [](const DiscordUser* connectedUser) { mprintf(("Discord: connected to user %s#%s - %s\n", connectedUser->username, connectedUser->discriminator, connectedUser->userId)); discord_ready = true; update_presence(); }; handlers.errored = [](int errcode, const char* message) { mprintf(("Discord: error (%d: %s)\n", errcode, message)); discord_ready = false; }; handlers.disconnected = [](int errcode, const char* message) { mprintf(("Discord: disconnected (%d: %s)\n", errcode, message)); discord_ready = false; }; Discord_Initialize(APPLICATION_ID, &handlers, 0, nullptr); //Only ever register these events once even if discord presence is toggled back on during the same game instance -Mjn if (!events_added) { events::EngineUpdate.add(update_discord); events::EngineShutdown.add(shutdown_discord); events::GameEnterState.add(handleEnterState); events::GameLeaveState.add(handleLeaveState); events::GameMissionLoad.add(handleMissionLoad); events_added = true; } initialized = true; } void shutdown() { shutdown_discord(); } void set_presence_string(const SCP_string &text) { set_presence(text); } void set_presence_gameplay() { set_game_play_presence(); } } // namespace discord } // namespace libs #else // Stubs for building without Discord support. namespace libs { namespace discord { void init() { mprintf(("Cannot create Discord session. Not built with Discord support.\n")); } void shutdown() {} void set_presence_string(const SCP_string &text) {} void set_presence_gameplay() {} } // namespace discord } // namespace libs #endif