#include #include "loader/component_loader.hpp" #include "scheduler.hpp" #include "game/game.hpp" #include "console.hpp" #include "command.hpp" #include "network.hpp" #include "party.hpp" #include "materials.hpp" #include "discord.hpp" #include "game/ui_scripting/execution.hpp" #include #include #include #include #define DEFAULT_AVATAR "discord_default_avatar" #define AVATAR "discord_avatar_%s" #define DEFAULT_AVATAR_URL "https://cdn.discordapp.com/embed/avatars/0.png" #define AVATAR_URL "https://cdn.discordapp.com/avatars/%s/%s.png?size=128" namespace discord { namespace { DiscordRichPresence discord_presence; void update_discord() { if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) { discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer"); discord_presence.state = "Main Menu"; const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange"); if (in_firing_range && in_firing_range->current.enabled == 1) { discord_presence.state = "Firing Range"; } discord_presence.partySize = 0; discord_presence.partyMax = 0; discord_presence.startTimestamp = 0; discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer"); // set to blank when in lobby discord_presence.matchSecret = ""; discord_presence.joinSecret = ""; discord_presence.partyId = ""; } else { static char details[0x80] = {0}; const auto map = game::Dvar_FindVar("mapname")->current.string; const auto mapname = game::UI_SafeTranslateString( utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map)); if (game::environment::is_mp()) { const auto gametype = game::UI_GetGameTypeDisplayName( game::Dvar_FindVar("g_gametype")->current.string); strcpy_s(details, 0x80, utils::string::va("%s on %s", gametype, mapname)); static char clean_hostname[0x80] = {0}; utils::string::strip(game::Dvar_FindVar("sv_hostname")->current.string, clean_hostname, sizeof(clean_hostname)); auto max_clients = party::server_client_count(); // When true, we are in Private Match if (game::SV_Loaded()) { strcpy_s(clean_hostname, "Private Match"); max_clients = game::Dvar_FindVar("sv_maxclients")->current.integer; discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE; } else { const auto server_net_info = party::get_state_host(); const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i", static_cast(server_net_info.ip[0]), static_cast(server_net_info.ip[1]), static_cast(server_net_info.ip[2]), static_cast(server_net_info.ip[3]), static_cast(ntohs(server_net_info.port)) ); static char join_secret[0x80] = {0}; strcpy_s(join_secret, 0x80, server_ip_port); static char party_id[0x80] = {0}; const auto server_ip_port_hash = utils::cryptography::sha1::compute(server_ip_port, true).substr(0, 8); strcpy_s(party_id, 0x80, server_ip_port_hash.data()); discord_presence.partyId = party_id; discord_presence.joinSecret = join_secret; discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; } const auto client_state = *game::mp::client_state; if (client_state != nullptr) { discord_presence.partySize = client_state->num_players; } else { discord_presence.partySize = 0; } discord_presence.partyMax = max_clients; discord_presence.state = clean_hostname; discord_presence.largeImageKey = map; } else if (game::environment::is_sp()) { discord_presence.state = ""; discord_presence.largeImageKey = map; strcpy_s(details, 0x80, mapname); } discord_presence.details = details; if (!discord_presence.startTimestamp) { discord_presence.startTimestamp = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); } } Discord_UpdatePresence(&discord_presence); } void download_user_avatar(const std::string& id, const std::string& avatar) { const auto data = utils::http::get_data( utils::string::va(AVATAR_URL, id.data(), avatar.data())); if (!data.has_value()) { return; } const auto& value = data.value(); if (value.code != CURLE_OK) { return; } materials::add(utils::string::va(AVATAR, id.data()), value.buffer); } bool has_default_avatar = false; void download_default_avatar() { const auto data = utils::http::get_data(DEFAULT_AVATAR_URL); if (!data.has_value()) { return; } const auto& value = data.value(); if (value.code != CURLE_OK) { return; } has_default_avatar = true; materials::add(DEFAULT_AVATAR, value.buffer); } } std::string get_avatar_material(const std::string& id) { const auto avatar_name = utils::string::va(AVATAR, id.data()); if (materials::exists(avatar_name)) { return avatar_name; } if (has_default_avatar) { return DEFAULT_AVATAR; } return "black"; } void respond(const std::string& id, int reply) { scheduler::once([=]() { Discord_Respond(id.data(), reply); }, scheduler::pipeline::async); } class component final : public component_interface { public: void post_load() override { if (game::environment::is_dedi()) { return; } DiscordEventHandlers handlers; ZeroMemory(&handlers, sizeof(handlers)); handlers.ready = ready; handlers.errored = errored; handlers.disconnected = errored; handlers.spectateGame = nullptr; if (game::environment::is_mp()) { handlers.joinGame = join_game; handlers.joinRequest = join_request; } Discord_Initialize("947125042930667530", &handlers, 1, nullptr); scheduler::once(download_default_avatar, scheduler::pipeline::async); scheduler::once([]() { scheduler::once(update_discord, scheduler::pipeline::async); scheduler::loop(update_discord, scheduler::pipeline::async, 5s); scheduler::loop(Discord_RunCallbacks, scheduler::pipeline::async, 1s); }, scheduler::pipeline::main); initialized_ = true; } void pre_destroy() override { if (!initialized_ || game::environment::is_dedi()) { return; } Discord_Shutdown(); } private: bool initialized_ = false; static void ready(const DiscordUser* request) { ZeroMemory(&discord_presence, sizeof(discord_presence)); discord_presence.instance = 1; console::info("Discord: Ready on %s (%s)\n", request->username, request->userId); Discord_UpdatePresence(&discord_presence); } static void errored(const int error_code, const char* message) { console::error("Discord: Error (%i): %s\n", error_code, message); } static void join_game(const char* join_secret) { console::info("Discord: Join game called with join secret: %s\n", join_secret); std::string secret = join_secret; scheduler::once([=]() { game::netadr_s target{}; if (game::NET_StringToAdr(secret.data(), &target)) { console::info("Discord: Connecting to server: %s\n", secret.data()); party::connect(target); } }, scheduler::pipeline::main); } static void join_request(const DiscordUser* request) { console::info("Discord: join_request from %s (%s)\n", request->username, request->userId); if (game::Com_InFrontend() || !ui_scripting::lui_running()) { Discord_Respond(request->userId, DISCORD_REPLY_IGNORE); return; } std::string user_id = request->userId; std::string avatar = request->avatar; std::string discriminator = request->discriminator; std::string username = request->username; scheduler::once([=]() { const ui_scripting::table request_table{}; request_table.set("avatar", avatar); request_table.set("discriminator", discriminator); request_table.set("userid", user_id); request_table.set("username", username); ui_scripting::notify("discord_join_request", { {"request", request_table} }); }, scheduler::pipeline::lui); if (!materials::exists(utils::string::va(AVATAR, user_id.data()))) { download_user_avatar(user_id, avatar); } } }; } REGISTER_COMPONENT(discord::component)