300 lines
8.0 KiB
C++
300 lines
8.0 KiB
C++
#include <std_include.hpp>
|
|
#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 <utils/string.hpp>
|
|
#include <utils/cryptography.hpp>
|
|
#include <utils/http.hpp>
|
|
|
|
#include <discord_rpc.h>
|
|
|
|
#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<int>(server_net_info.ip[0]),
|
|
static_cast<int>(server_net_info.ip[1]),
|
|
static_cast<int>(server_net_info.ip[2]),
|
|
static_cast<int>(server_net_info.ip[3]),
|
|
static_cast<int>(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::seconds>(
|
|
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())
|
|
{
|
|
materials::add(utils::string::va(AVATAR, id.data()), data.value());
|
|
}
|
|
}
|
|
|
|
bool has_default_avatar = false;
|
|
void download_default_avatar()
|
|
{
|
|
const auto data = utils::http::get_data(DEFAULT_AVATAR_URL);
|
|
if (data.has_value())
|
|
{
|
|
has_default_avatar = true;
|
|
materials::add(DEFAULT_AVATAR, data.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|