From f2fb484b57ef62328bcf45bb879b3eacb2a97d06 Mon Sep 17 00:00:00 2001 From: m Date: Wed, 13 Dec 2023 23:33:54 -0600 Subject: [PATCH 1/9] cleanup + add server rpc info --- src/client/component/discord.cpp | 54 ++++++++++++++++++++++---------- src/client/component/party.cpp | 16 ++++++++-- src/client/component/party.hpp | 2 ++ 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 4ec5413f..6777ad6f 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -42,6 +42,8 @@ namespace discord discord_presence.partyMax = 0; discord_presence.startTimestamp = 0; discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer"); + discord_presence.largeImageText = ""; + discord_presence.smallImageKey = ""; discord_presence.matchSecret = ""; discord_presence.joinSecret = ""; @@ -57,27 +59,25 @@ namespace discord else { static char details[0x80] = {0}; - const auto map = game::Dvar_FindVar("mapname")->current.string; - const auto key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map); - const char* mapname = map; + const auto mapname = game::Dvar_FindVar("mapname")->current.string; + const auto presence_key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), mapname); + const char* clean_mapname = mapname; - if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, key) && !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, key)) + if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, presence_key) && !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, presence_key)) { - mapname = game::UI_SafeTranslateString(key); + clean_mapname = game::UI_SafeTranslateString(presence_key); } if (game::environment::is_mp()) { static char clean_gametype[0x80] = {0}; - const auto gametype = game::UI_GetGameTypeDisplayName( - game::Dvar_FindVar("g_gametype")->current.string); - utils::string::strip(gametype, - clean_gametype, sizeof(clean_gametype)); - strcpy_s(details, 0x80, utils::string::va("%s on %s", clean_gametype, mapname)); + const auto gametype = game::UI_GetGameTypeDisplayName(game::Dvar_FindVar("g_gametype")->current.string); + utils::string::strip(gametype, clean_gametype, sizeof(clean_gametype)); + strcpy_s(details, 0x80, utils::string::va("%s on %s", clean_gametype, clean_mapname)); static char clean_hostname[0x80] = {0}; - utils::string::strip(game::Dvar_FindVar("sv_hostname")->current.string, - clean_hostname, sizeof(clean_hostname)); + utils::string::strip(game::Dvar_FindVar("sv_hostname")->current.string, clean_hostname, sizeof(clean_hostname)); + auto max_clients = party::server_client_count(); if (game::SV_Loaded()) @@ -121,18 +121,38 @@ namespace discord discord_presence.partyMax = max_clients; discord_presence.state = clean_hostname; - discord_presence.largeImageKey = map; - if (!fastfiles::is_stock_map(map)) + auto discord_map_image_asset = mapname; + if (!fastfiles::is_stock_map(mapname)) { - discord_presence.largeImageKey = "menu_multiplayer"; + discord_map_image_asset = "menu_multiplayer"; + } + + auto discord_server_info = party::get_discord_server_image(); + if (!discord_server_info.empty()) + { + // prioritize server image as large image instead + discord_presence.smallImageKey = discord_map_image_asset; + discord_presence.largeImageKey = discord_server_info.data(); + discord_server_info = party::get_discord_server_text(); + if (!discord_server_info.empty()) + { + discord_presence.largeImageText = discord_server_info.data(); + } + } + else + { + discord_presence.smallImageKey = ""; + discord_presence.largeImageKey = discord_map_image_asset; + discord_presence.largeImageText = ""; } } else if (game::environment::is_sp()) { discord_presence.state = ""; - discord_presence.largeImageKey = map; - strcpy_s(details, 0x80, mapname); + discord_presence.largeImageKey = mapname; + discord_presence.smallImageKey = ""; + strcpy_s(details, 0x80, clean_mapname); } discord_presence.details = details; diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 9a484405..eefb6656 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -767,6 +767,16 @@ namespace party return party::sv_maxclients; } + std::string get_discord_server_image() + { + return saved_info_response.info_string.get("sv_discordImageUrl"); + } + + std::string get_discord_server_text() + { + return saved_info_response.info_string.get("sv_discordImageText"); + } + class component final : public component_interface { public: @@ -777,7 +787,7 @@ namespace party return; } - // detour CL_Disconnect to clear motd + // clear motd & usermap cl_disconnect_hook.create(0x12F080_b, cl_disconnect_stub); if (game::environment::is_mp()) @@ -965,7 +975,7 @@ namespace party scheduler::once([]() { - sv_say_name = dvars::register_string("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE, ""); + sv_say_name = dvars::register_string("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE, "Custom name for RCON console"); }, scheduler::pipeline::main); command::add("tell", [](const command::params& params) @@ -1060,6 +1070,8 @@ namespace party info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running") && !game::VirtualLobby_Loaded())); info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated"))); info.set("sv_wwwBaseUrl", get_dvar_string("sv_wwwBaseUrl")); + info.set("sv_discordImageUrl", get_dvar_string("sv_discordImageUrl")); + info.set("sv_discordImageText", get_dvar_string("sv_discordImageText")); if (!fastfiles::is_stock_map(mapname)) { diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index ca4852b2..b5eeb1ec 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -16,6 +16,8 @@ namespace party void clear_sv_motd(); game::netadr_s get_state_host(); int server_client_count(); + std::string get_discord_server_image(); + std::string get_discord_server_text(); int get_client_num_by_name(const std::string& name); From 2915acf2acb2ae984074d3f3a375e9ec3809c9bc Mon Sep 17 00:00:00 2001 From: m Date: Wed, 13 Dec 2023 23:34:38 -0600 Subject: [PATCH 2/9] tiny error msg changes --- src/client/component/download.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/client/component/download.cpp b/src/client/component/download.cpp index 49bca99f..18e152ee 100644 --- a/src/client/component/download.cpp +++ b/src/client/component/download.cpp @@ -170,7 +170,7 @@ namespace download auto data = utils::http::get_data(url, {}, {}, &progress_callback); if (!data.has_value()) { - menu_error("Download failed: An unknown error occurred, please try again."); + menu_error(utils::string::va("Download failed: An unknown error occurred when getting data from '%s', please try again.", url)); return; } @@ -182,6 +182,13 @@ namespace download auto& result = data.value(); if (result.code != CURLE_OK) { + if (result.code == CURLE_COULDNT_CONNECT) + { + menu_error(utils::string::va("Download failed: Couldn't connect to server '%s' (%i)\n", + url, result.code)); + return; + } + menu_error(utils::string::va("Download failed: %s (%i)\n", curl_easy_strerror(result.code), result.code)); return; @@ -189,7 +196,7 @@ namespace download if (result.response_code >= 400) { - menu_error(utils::string::va("Download failed: Server returned bad response code %i\n", + menu_error(utils::string::va("Download failed: Server returned bad response code (%i)\n", result.response_code)); return; } @@ -197,7 +204,7 @@ namespace download const auto hash = utils::hash::get_buffer_hash(result.buffer, file.name); if (hash != file.hash) { - menu_error(utils::string::va("Download failed: file hash doesn't match the server's (%s: %s != %s)\n", + menu_error(utils::string::va("Download failed: File hash doesn't match the server's (%s: %s != %s)\n", file.name.data(), hash.data(), file.hash.data())); return; } @@ -233,7 +240,7 @@ namespace download scheduler::once([] { ui_scripting::notify("mod_download_done", {}); - party::menu_error("Download for server mod has been cancelled."); + party::menu_error("Download failed: Aborted"); }, scheduler::pipeline::lui); } From fe86fc31e141a999fe31ae819b9d4fd9def2b5ca Mon Sep 17 00:00:00 2001 From: m Date: Wed, 13 Dec 2023 23:40:54 -0600 Subject: [PATCH 3/9] add a todo for later --- src/client/component/discord.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 6777ad6f..8b50ff36 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -125,7 +125,7 @@ namespace discord auto discord_map_image_asset = mapname; if (!fastfiles::is_stock_map(mapname)) { - discord_map_image_asset = "menu_multiplayer"; + discord_map_image_asset = "menu_multiplayer"; // TODO: maybe add usermap images } auto discord_server_info = party::get_discord_server_image(); From 5acb5f7ba0df5e7dc454717f56589f3108ce9c47 Mon Sep 17 00:00:00 2001 From: m Date: Fri, 15 Dec 2023 14:12:36 -0600 Subject: [PATCH 4/9] cleanup discord and party [skip ci] --- src/client/component/discord.cpp | 419 +++++++++++++------------- src/client/component/party.cpp | 64 ++-- src/client/component/party.hpp | 25 +- src/client/component/server_list.cpp | 2 +- src/client/component/ui_scripting.cpp | 2 +- 5 files changed, 249 insertions(+), 263 deletions(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 8b50ff36..9c2299a6 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -31,142 +31,133 @@ namespace discord { DiscordRichPresence discord_presence; - void update_discord() + void update_discord_frontend() { - if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) + discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer"); + discord_presence.startTimestamp = 0; + + const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange"); + if (in_firing_range && in_firing_range->current.enabled == 1) { - discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer"); - discord_presence.state = "Main Menu"; - - discord_presence.partySize = 0; - discord_presence.partyMax = 0; - discord_presence.startTimestamp = 0; - discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer"); - discord_presence.largeImageText = ""; - discord_presence.smallImageKey = ""; - - discord_presence.matchSecret = ""; - discord_presence.joinSecret = ""; - discord_presence.partyId = ""; - - 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.largeImageKey = "mp_vlobby_room"; - } + discord_presence.state = "Firing Range"; + discord_presence.largeImageKey = "mp_vlobby_room"; } else { - static char details[0x80] = {0}; - const auto mapname = game::Dvar_FindVar("mapname")->current.string; - const auto presence_key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), mapname); - const char* clean_mapname = mapname; - - if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, presence_key) && !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, presence_key)) - { - clean_mapname = game::UI_SafeTranslateString(presence_key); - } - - if (game::environment::is_mp()) - { - static char clean_gametype[0x80] = {0}; - const auto gametype = game::UI_GetGameTypeDisplayName(game::Dvar_FindVar("g_gametype")->current.string); - utils::string::strip(gametype, clean_gametype, sizeof(clean_gametype)); - strcpy_s(details, 0x80, utils::string::va("%s on %s", clean_gametype, clean_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(); - - 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; - - auto discord_map_image_asset = mapname; - if (!fastfiles::is_stock_map(mapname)) - { - discord_map_image_asset = "menu_multiplayer"; // TODO: maybe add usermap images - } - - auto discord_server_info = party::get_discord_server_image(); - if (!discord_server_info.empty()) - { - // prioritize server image as large image instead - discord_presence.smallImageKey = discord_map_image_asset; - discord_presence.largeImageKey = discord_server_info.data(); - discord_server_info = party::get_discord_server_text(); - if (!discord_server_info.empty()) - { - discord_presence.largeImageText = discord_server_info.data(); - } - } - else - { - discord_presence.smallImageKey = ""; - discord_presence.largeImageKey = discord_map_image_asset; - discord_presence.largeImageText = ""; - } - } - else if (game::environment::is_sp()) - { - discord_presence.state = ""; - discord_presence.largeImageKey = mapname; - discord_presence.smallImageKey = ""; - strcpy_s(details, 0x80, clean_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_presence.state = "Main Menu"; + discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer"); } Discord_UpdatePresence(&discord_presence); } + void update_discord_ingame() + { + static const auto mapname_dvar = game::Dvar_FindVar("mapname"); + auto mapname = mapname_dvar->current.string; + + discord_presence.largeImageKey = mapname; + + const auto presence_key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), mapname); + if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, presence_key) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, presence_key)) + { + mapname = game::UI_SafeTranslateString(presence_key); + } + + if (game::environment::is_mp()) + { + // setup Discord details (Free-for-all on Shipment) + static char gametype[0x80] = {0}; + static const auto gametype_dvar = game::Dvar_FindVar("g_gametype"); + const auto gametype_display_name = game::UI_GetGameTypeDisplayName(gametype_dvar->current.string); + utils::string::strip(gametype_display_name, gametype, sizeof(gametype)); + + discord_presence.details = utils::string::va("%s on %s", gametype, mapname); + + // setup Discord party (1 of 18) + const auto client_state = *game::mp::client_state; + if (client_state != nullptr) + { + discord_presence.partySize = client_state->num_players; + } + + if (game::SV_Loaded()) + { + discord_presence.state = "Private Match"; + static const auto maxclients_dvar = game::Dvar_FindVar("sv_maxclients"); + discord_presence.partyMax = maxclients_dvar->current.integer; + discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE; + } + else + { + static char hostname[0x80] = {0}; + static const auto hostname_dvar = game::Dvar_FindVar("sv_hostname"); + utils::string::strip(hostname_dvar->current.string, hostname, sizeof(hostname)); + discord_presence.state = hostname; + + const auto server_connection_state = party::get_server_connection_state(); + + const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i", + static_cast(server_connection_state.host.ip[0]), + static_cast(server_connection_state.host.ip[1]), + static_cast(server_connection_state.host.ip[2]), + static_cast(server_connection_state.host.ip[3]), + static_cast(ntohs(server_connection_state.host.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.partyMax = server_connection_state.max_clients; + discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; + + static char join_secret[0x80] = { 0 }; + strcpy_s(join_secret, 0x80, server_ip_port); + discord_presence.joinSecret = join_secret; + } + + auto server_discord_information = party::get_server_discord_information(); + if (!server_discord_information.image.empty()) + { + discord_presence.smallImageKey = server_discord_information.image.data(); + discord_presence.smallImageText = server_discord_information.image_text.data(); + } + } + else if (game::environment::is_sp()) + { + discord_presence.details = mapname; + } + + if (discord_presence.startTimestamp == 0) + { + discord_presence.startTimestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } + + Discord_UpdatePresence(&discord_presence); + } + + void update_discord() + { + Discord_RunCallbacks(); + + // reset presence data + const auto saved_time = discord_presence.startTimestamp; + discord_presence = {}; + discord_presence.startTimestamp = saved_time; + + if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) + { + update_discord_frontend(); + } + else + { + update_discord_ingame(); + } + } + void download_user_avatar(const std::string& id, const std::string& avatar) { const auto data = utils::http::get_data( @@ -203,116 +194,39 @@ namespace discord 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)) + void ready(const DiscordUser* request) { - 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; - } - else - { - handlers.joinGame = nullptr; - handlers.joinRequest = nullptr; - } - - 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; + DiscordRichPresence presence{}; + presence.instance = 1; + presence.state = ""; console::info("Discord: Ready on %s (%s)\n", request->username, request->userId); - Discord_UpdatePresence(&discord_presence); + Discord_UpdatePresence(&presence); } - static void errored(const int error_code, const char* message) + void errored(const int error_code, const char* message) { - console::error("Discord: Error (%i): %s\n", error_code, message); + console::error("Discord: %s (%i)\n", message, error_code); } - static void join_game(const char* join_secret) + void join_game(const char* join_secret) { + console::debug("Discord: join_game called with secret '%s'\n", join_secret); + scheduler::once([=] { game::netadr_s target{}; if (game::NET_StringToAdr(join_secret, &target)) { - console::info("Discord: Connecting to server: %s\n", join_secret); + console::info("Discord: Connecting to server '%s'\n", join_secret); party::connect(target); } }, scheduler::pipeline::main); } - static void join_request(const DiscordUser* request) + void join_request(const DiscordUser* request) { - console::info("Discord: Join request from %s (%s)\n", request->username, request->userId); + console::debug("Discord: Join request from %s (%s)\n", request->username, request->userId); if (game::Com_InFrontend() || !ui_scripting::lui_running()) { @@ -344,6 +258,79 @@ namespace discord download_user_avatar(user_id, avatar); } } + } + + 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_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + DiscordEventHandlers 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; + } + else + { + handlers.joinGame = nullptr; + handlers.joinRequest = nullptr; + } + + Discord_Initialize("947125042930667530", &handlers, 1, nullptr); + + scheduler::once(download_default_avatar, scheduler::pipeline::async); + scheduler::loop(update_discord, scheduler::pipeline::async, 5s); + + initialized_ = true; + } + + void pre_destroy() override + { + if (!initialized_ || game::environment::is_dedi()) + { + return; + } + + Discord_Shutdown(); + } + + private: + bool initialized_ = false; }; } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index eefb6656..5aa10986 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -32,15 +32,8 @@ namespace party { namespace { - struct - { - game::netadr_s host{}; - std::string challenge{}; - bool hostDefined{false}; - } connect_state; - - std::string sv_motd; - int sv_maxclients; + connection_state server_connection_state{}; + discord_information server_discord_information{}; struct usermap_file { @@ -165,11 +158,11 @@ namespace party const char* get_didyouknow_stub(void* table, int row, int column) { - if (party::sv_motd.empty()) + if (server_connection_state.motd.empty()) { return utils::hook::invoke(0x5A0AC0_b, table, row, column); } - return utils::string::va("%s", party::sv_motd.data()); + return utils::string::va("%s", server_connection_state.motd.data()); } void disconnect() @@ -498,7 +491,7 @@ namespace party command::execute("disconnect"); scheduler::once([] { - connect(connect_state.host); + connect(server_connection_state.host); }, scheduler::pipeline::main); return; } @@ -614,7 +607,7 @@ namespace party void clear_sv_motd() { - party::sv_motd.clear(); + server_connection_state.motd.clear(); } int get_client_num_by_name(const std::string& name) @@ -636,9 +629,9 @@ namespace party return -1; } - void reset_connect_state() + void reset_server_connection_state() { - connect_state = {}; + server_connection_state = {}; } int get_client_count() @@ -691,16 +684,11 @@ namespace party command::execute("lui_open_popup popup_acceptinginvite", false); - connect_state.host = target; - connect_state.challenge = utils::cryptography::random::get_challenge(); - connect_state.hostDefined = true; + server_connection_state.host = target; + server_connection_state.challenge = utils::cryptography::random::get_challenge(); + server_connection_state.hostDefined = true; - network::send(target, "getInfo", connect_state.challenge); - } - - game::netadr_s get_state_host() - { - return connect_state.host; + network::send(target, "getInfo", server_connection_state.challenge); } void start_map(const std::string& mapname, bool dev) @@ -762,19 +750,14 @@ namespace party } } - int server_client_count() + connection_state get_server_connection_state() { - return party::sv_maxclients; + return server_connection_state; } - std::string get_discord_server_image() + discord_information get_server_discord_information() { - return saved_info_response.info_string.get("sv_discordImageUrl"); - } - - std::string get_discord_server_text() - { - return saved_info_response.info_string.get("sv_discordImageText"); + return server_discord_information; } class component final : public component_interface @@ -859,7 +842,7 @@ namespace party command::add("reconnect", [](const command::params& argument) { - if (!connect_state.hostDefined) + if (!server_connection_state.hostDefined) { console::info("Cannot connect to server.\n"); return; @@ -872,7 +855,7 @@ namespace party } else { - connect(connect_state.host); + connect(server_connection_state.host); } }); @@ -1104,7 +1087,7 @@ namespace party const utils::info_string info(data); server_list::handle_info_response(target, info); - if (connect_state.host != target) + if (server_connection_state.host != target) { return; } @@ -1120,7 +1103,7 @@ namespace party return; } - if (info.get("challenge") != connect_state.challenge) + if (info.get("challenge") != server_connection_state.challenge) { menu_error("Connection failed: Invalid challenge."); return; @@ -1166,8 +1149,11 @@ namespace party return; } - party::sv_motd = info.get("sv_motd"); - party::sv_maxclients = std::stoi(info.get("sv_maxclients")); + server_connection_state.motd = info.get("sv_motd"); + server_connection_state.max_clients = std::stoi(info.get("sv_maxclients")); + server_connection_state.base_url = info.get("sv_wwwBaseUrl"); + server_discord_information.image = info.get("sv_discordImageUrl"); + server_discord_information.image_text = info.get("sv_discordImageText"); connect_to_party(target, mapname, gametype); }); diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index b5eeb1ec..6a367d66 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -3,21 +3,34 @@ namespace party { - std::string get_www_url(); + struct connection_state + { + game::netadr_s host; + std::string challenge; + bool hostDefined; + std::string motd; + int max_clients; + std::string base_url; + }; + + struct discord_information + { + std::string image; + std::string image_text; + }; + void user_download_response(bool response); void menu_error(const std::string& error); - void reset_connect_state(); + void reset_server_connection_state(); void connect(const game::netadr_s& target); void start_map(const std::string& mapname, bool dev = false); void clear_sv_motd(); - game::netadr_s get_state_host(); - int server_client_count(); - std::string get_discord_server_image(); - std::string get_discord_server_text(); + connection_state get_server_connection_state(); + discord_information get_server_discord_information(); int get_client_num_by_name(const std::string& name); diff --git a/src/client/component/server_list.cpp b/src/client/component/server_list.cpp index 5f536c72..e3577e2e 100644 --- a/src/client/component/server_list.cpp +++ b/src/client/component/server_list.cpp @@ -77,7 +77,7 @@ namespace server_list server_list_page = 0; } - party::reset_connect_state(); + party::reset_server_connection_state(); if (get_master_server(master_state.address)) { diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index 3ab187d7..2bc4aeef 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -367,7 +367,7 @@ namespace ui_scripting download_table["abort"] = download::stop_download; download_table["userdownloadresponse"] = party::user_download_response; - download_table["getwwwurl"] = party::get_www_url; + download_table["getwwwurl"] = party::get_server_connection_state().base_url; } void start() From b4d31308aad8a7f47ee4dc657692fac95602dee9 Mon Sep 17 00:00:00 2001 From: m Date: Fri, 15 Dec 2023 21:39:58 -0600 Subject: [PATCH 5/9] fix discord ui script --- src/client/component/ui_scripting.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index 2bc4aeef..d0590f71 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -9,6 +9,7 @@ #include "localized_strings.hpp" #include "console.hpp" +#include "discord.hpp" #include "download.hpp" #include "game_module.hpp" #include "fps.hpp" @@ -33,6 +34,8 @@ #include "steam/steam.hpp" +#include + namespace ui_scripting { namespace @@ -368,6 +371,16 @@ namespace ui_scripting download_table["userdownloadresponse"] = party::user_download_response; download_table["getwwwurl"] = party::get_server_connection_state().base_url; + + auto discord_table = table(); + lua["discord"] = discord_table; + + discord_table["respond"] = discord::respond; + discord_table["getavatarmaterial"] = discord::get_avatar_material; + discord_table["reply"] = table(); + discord_table["reply"]["yes"] = DISCORD_REPLY_YES; + discord_table["reply"]["ignore"] = DISCORD_REPLY_IGNORE; + discord_table["reply"]["no"] = DISCORD_REPLY_NO; } void start() From dde3449195459f71d6852f126a5c43f74aa62831 Mon Sep 17 00:00:00 2001 From: m Date: Sun, 17 Dec 2023 14:51:51 -0600 Subject: [PATCH 6/9] fix discord details --- src/client/component/discord.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 9c2299a6..ec8ab143 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -73,7 +73,9 @@ namespace discord const auto gametype_display_name = game::UI_GetGameTypeDisplayName(gametype_dvar->current.string); utils::string::strip(gametype_display_name, gametype, sizeof(gametype)); - discord_presence.details = utils::string::va("%s on %s", gametype, mapname); + static char details[0x80] = {0}; + strcpy_s(details, 0x80, utils::string::va("%s on %s", gametype, mapname)); + discord_presence.details = details; // setup Discord party (1 of 18) const auto client_state = *game::mp::client_state; @@ -211,8 +213,6 @@ namespace discord void join_game(const char* join_secret) { - console::debug("Discord: join_game called with secret '%s'\n", join_secret); - scheduler::once([=] { game::netadr_s target{}; From 1d2fccdf8bf38fc1c1d5e6d0a15a05ff030b114c Mon Sep 17 00:00:00 2001 From: m Date: Sun, 17 Dec 2023 15:21:26 -0600 Subject: [PATCH 7/9] fix key event + run callbacks more --- data/cdata/ui_scripts/discord/__init__.lua | 4 +--- src/client/component/discord.cpp | 10 +++++++--- src/client/component/input.cpp | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/data/cdata/ui_scripts/discord/__init__.lua b/data/cdata/ui_scripts/discord/__init__.lua index fe988a68..05d8e25a 100644 --- a/data/cdata/ui_scripts/discord/__init__.lua +++ b/data/cdata/ui_scripts/discord/__init__.lua @@ -18,9 +18,6 @@ function canasktojoin(userid) end history[userid] = true - game:ontimeout(function() - history[userid] = nil - end, 15000) return true end @@ -206,6 +203,7 @@ function addrequest(request) invite:registerEventHandler("end_invite", function() close() discord.respond(request.userid, discord.reply.ignore) + history[request.userid] = nil end) local bar = LUI.UIImage.new({ diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index ec8ab143..d129974d 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -143,8 +143,6 @@ namespace discord void update_discord() { - Discord_RunCallbacks(); - // reset presence data const auto saved_time = discord_presence.startTimestamp; discord_presence = {}; @@ -314,7 +312,13 @@ namespace discord Discord_Initialize("947125042930667530", &handlers, 1, nullptr); scheduler::once(download_default_avatar, scheduler::pipeline::async); - scheduler::loop(update_discord, scheduler::pipeline::async, 5s); + + 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; } diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index ad14ae43..5112620e 100644 --- a/src/client/component/input.cpp +++ b/src/client/component/input.cpp @@ -36,7 +36,7 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { - if (game::environment::is_sp() && ui_scripting::lui_running()) + if (ui_scripting::lui_running()) { ui_scripting::notify(down ? "keydown" : "keyup", { From 581b60df1cc24a983d6b8ed33dc2cacbc64faa68 Mon Sep 17 00:00:00 2001 From: m Date: Sun, 17 Dec 2023 15:27:47 -0600 Subject: [PATCH 8/9] fix discord ui script history --- data/cdata/ui_scripts/discord/__init__.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/cdata/ui_scripts/discord/__init__.lua b/data/cdata/ui_scripts/discord/__init__.lua index 05d8e25a..b3c701a0 100644 --- a/data/cdata/ui_scripts/discord/__init__.lua +++ b/data/cdata/ui_scripts/discord/__init__.lua @@ -11,8 +11,9 @@ local container = LUI.UIVerticalList.new({ spacing = 5 }) +history = {} + function canasktojoin(userid) - history = history or {} if (history[userid] ~= nil) then return false end From 409bf1c00ce97a9fb3a033715811adb809dc782c Mon Sep 17 00:00:00 2001 From: fed <58637860+fedddddd@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:54:06 +0100 Subject: [PATCH 9/9] Discord rpc fixes & changes --- data/cdata/ui_scripts/discord/__init__.lua | 144 +++++++------ data/zonetool/localizedstrings/english.json | 7 +- src/client/component/discord.cpp | 222 ++++++++++++++------ src/client/component/discord.hpp | 4 +- src/client/component/input.cpp | 18 -- src/client/component/materials.cpp | 193 +++++------------ src/client/component/materials.hpp | 8 +- src/client/component/mods.cpp | 1 - src/client/component/party.cpp | 16 +- src/client/component/party.hpp | 2 +- src/client/component/ui_scripting.cpp | 14 +- src/client/game/structs.hpp | 18 +- src/client/game/symbols.hpp | 6 +- src/common/utils/string.cpp | 8 + src/common/utils/string.hpp | 1 + 15 files changed, 362 insertions(+), 300 deletions(-) diff --git a/data/cdata/ui_scripts/discord/__init__.lua b/data/cdata/ui_scripts/discord/__init__.lua index b3c701a0..b1cbd249 100644 --- a/data/cdata/ui_scripts/discord/__init__.lua +++ b/data/cdata/ui_scripts/discord/__init__.lua @@ -2,7 +2,7 @@ if (game:issingleplayer() or Engine.InFrontend()) then return end -local container = LUI.UIVerticalList.new({ +local container = LUI.UIElement.new({ topAnchor = true, rightAnchor = true, top = 20, @@ -11,18 +11,6 @@ local container = LUI.UIVerticalList.new({ spacing = 5 }) -history = {} - -function canasktojoin(userid) - if (history[userid] ~= nil) then - return false - end - - history[userid] = true - - return true -end - function truncatename(name, length) if (#name <= length - 3) then return name @@ -31,27 +19,57 @@ function truncatename(name, length) return name:sub(1, length - 3) .. "..." end +local requestlist = {} +local requestcount = 0 + function addrequest(request) - if (not canasktojoin(request.userid)) then - return + for i = 1, #requestlist do + if (requestlist[i].userid == request.userid or #requestlist > 5) then + return + end end - if (container.temp) then - container:removeElement(container.temp) - container.temp = nil - end + request.id = requestcount + requestcount = requestcount + 1 + local yoffset = #requestlist * (75 + 5) local invite = LUI.UIElement.new({ leftAnchor = true, rightAnchor = true, - height = 75 + height = 75, + top = yoffset }) + local getcurrentindex = function() + for i = 1, #requestlist do + if (requestlist[i].id == request.id) then + return i + end + end + + return 0 + end + + invite:registerEventHandler("update_position", function() + yoffset = (getcurrentindex() - 1) * (75 + 5) + local state = { + leftAnchor = true, + height = 75, + width = 200, + left = -220, + top = yoffset + } + + invite:registerAnimationState("default", state) + invite:animateToState("default", 50) + end) + invite:registerAnimationState("move_in", { leftAnchor = true, height = 75, width = 200, - left = -220 + left = -220, + top = yoffset }) invite:animateToState("move_in", 100) @@ -103,7 +121,7 @@ function addrequest(request) width = 32, height = 32, left = 1, - material = RegisterMaterial(avatarmaterial) + material = avatarmaterial }) local username = LUI.UIText.new({ @@ -117,8 +135,14 @@ function addrequest(request) font = CoD.TextSettings.BodyFontBold.Font }) - username:setText(string.format("%s^7#%s requested to join your game!", truncatename(request.username, 18), - request.discriminator)) + local requesttext = nil + if (request.discriminator == "0") then + requesttext = Engine.Localize("LUA_MENU_DISCORD_REQUEST", truncatename(request.username, 18)) + else + requesttext = Engine.Localize("LUA_MENU_DISCORD_REQUEST_DISCRIMINATOR", truncatename(request.username, 18), request.discriminator) + end + + username:setText(requesttext) local buttons = LUI.UIElement.new({ leftAnchor = true, @@ -154,57 +178,59 @@ function addrequest(request) return button end - buttons:addElement(createbutton("[F1] Accept", true)) - buttons:addElement(createbutton("[F2] Deny")) + local accepttext = Engine.Localize("LUA_MENU_DISCORD_ACCEPT", Engine.GetBinding("discord_accept")) + local denytext = Engine.Localize("LUA_MENU_DISCORD_DENY", Engine.GetBinding("discord_deny")) + + buttons:addElement(createbutton(accepttext, true)) + buttons:addElement(createbutton(denytext)) local fadeouttime = 50 local timeout = 10 * 1000 - fadeouttime local function close() - container:processEvent({ - name = "update_navigation", - dispatchToChildren = true + table.remove(requestlist, getcurrentindex()) + + invite:registerAnimationState("fade_out", { + leftAnchor = true, + rightAnchor = true, + height = 75, + alpha = 0, + left = 0, + top = yoffset }) + invite:animateToState("fade_out", fadeouttime) invite:addElement(LUI.UITimer.new(fadeouttime + 50, "remove")) invite:registerEventHandler("remove", function() container:removeElement(invite) - if (container.temp) then - container:removeElement(container.temp) - container.temp = nil - end - local temp = LUI.UIElement.new({}) - container.temp = temp - container:addElement(temp) + container:processEvent({ + name = "update_position", + dispatchToChildren = true + }) end) end - buttons:registerEventHandler("keydown_", function(element, event) - if (event.key == "F1") then - close() - discord.respond(request.userid, discord.reply.yes) + local closed = false + request.handleresponse = function(event) + if (closed) then + return end - if (event.key == "F2") then - close() + if (event.accept) then + discord.respond(request.userid, discord.reply.yes) + else discord.respond(request.userid, discord.reply.no) end - end) - invite:registerAnimationState("fade_out", { - leftAnchor = true, - rightAnchor = true, - height = 75, - alpha = 0, - left = 0 - }) + closed = true + close() + end invite:addElement(LUI.UITimer.new(timeout, "end_invite")) invite:registerEventHandler("end_invite", function() close() discord.respond(request.userid, discord.reply.ignore) - history[request.userid] = nil end) local bar = LUI.UIImage.new({ @@ -235,7 +261,7 @@ function addrequest(request) avatar:registerEventHandler("update", function() local avatarmaterial = discord.getavatarmaterial(request.userid) - avatar:setImage(RegisterMaterial(avatarmaterial)) + avatar:setImage(avatarmaterial) end) avatar:addElement(LUI.UITimer.new(100, "update")) @@ -249,19 +275,17 @@ function addrequest(request) padding:addElement(buttons) container:addElement(invite) + + table.insert(requestlist, request) end -container:registerEventHandler("keydown", function(element, event) - local first = container:getFirstChild() - - if (not first) then +LUI.roots.UIRoot0:registerEventHandler("discord_response", function(element, event) + if (#requestlist <= 0) then return end - first:processEvent({ - name = "keydown_", - key = event.key - }) + local request = requestlist[1] + request.handleresponse(event) end) LUI.roots.UIRoot0:registerEventHandler("discord_join_request", function(element, event) diff --git a/data/zonetool/localizedstrings/english.json b/data/zonetool/localizedstrings/english.json index f0b9b8dc..726e5cf0 100644 --- a/data/zonetool/localizedstrings/english.json +++ b/data/zonetool/localizedstrings/english.json @@ -2,5 +2,10 @@ "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Unlock All Missions and Intel", "LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancel Unlock All Missions", "LUA_MENU_CHOOSE_LANGUAGE_DESC": "Choose your language.", - "MENU_APPLY_LANGUAGE_SETTINGS": "Apply language settings?" + "MENU_APPLY_LANGUAGE_SETTINGS": "Apply language settings?", + + "LUA_MENU_DISCORD_REQUEST": "&&1^7 requested to join your game!", + "LUA_MENU_DISCORD_REQUEST_DISCRIMINATOR": "&&1^7#&&2 requested to join your game!", + "LUA_MENU_DISCORD_ACCEPT": "[&&1] Accept", + "LUA_MENU_DISCORD_DENY": "[&&1] Deny" } \ No newline at end of file diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index d129974d..98fa6cd9 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -4,9 +4,7 @@ #include "console.hpp" #include "command.hpp" #include "discord.hpp" -#include "fastfiles.hpp" #include "materials.hpp" -#include "network.hpp" #include "party.hpp" #include "scheduler.hpp" @@ -29,15 +27,39 @@ namespace discord { namespace { - DiscordRichPresence discord_presence; + struct discord_presence_state_t + { + int start_timestamp; + int party_size; + int party_max; + }; + + struct discord_presence_strings_t + { + std::string state; + std::string details; + std::string small_image_key; + std::string small_image_text; + std::string large_image_key; + std::string large_image_text; + std::string party_id; + std::string join_secret; + }; + + DiscordRichPresence discord_presence{}; + discord_presence_strings_t discord_strings; + + std::mutex avatar_map_mutex; + std::unordered_map avatar_material_map; + game::Material* default_avatar_material{}; void update_discord_frontend() { discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer"); discord_presence.startTimestamp = 0; - const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange"); - if (in_firing_range && in_firing_range->current.enabled == 1) + static const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange"); + if (in_firing_range != nullptr && in_firing_range->current.enabled == 1) { discord_presence.state = "Firing Range"; discord_presence.largeImageKey = "mp_vlobby_room"; @@ -56,7 +78,7 @@ namespace discord static const auto mapname_dvar = game::Dvar_FindVar("mapname"); auto mapname = mapname_dvar->current.string; - discord_presence.largeImageKey = mapname; + discord_strings.large_image_key = mapname; const auto presence_key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), mapname); if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, presence_key) && @@ -67,17 +89,15 @@ namespace discord if (game::environment::is_mp()) { - // setup Discord details (Free-for-all on Shipment) - static char gametype[0x80] = {0}; static const auto gametype_dvar = game::Dvar_FindVar("g_gametype"); + static const auto max_clients_dvar = game::Dvar_FindVar("sv_maxclients"); + static const auto hostname_dvar = game::Dvar_FindVar("sv_hostname"); + const auto gametype_display_name = game::UI_GetGameTypeDisplayName(gametype_dvar->current.string); - utils::string::strip(gametype_display_name, gametype, sizeof(gametype)); + const auto gametype = utils::string::strip(gametype_display_name); - static char details[0x80] = {0}; - strcpy_s(details, 0x80, utils::string::va("%s on %s", gametype, mapname)); - discord_presence.details = details; + discord_strings.details = std::format("{} on {}", gametype, mapname); - // setup Discord party (1 of 18) const auto client_state = *game::mp::client_state; if (client_state != nullptr) { @@ -86,21 +106,16 @@ namespace discord if (game::SV_Loaded()) { - discord_presence.state = "Private Match"; - static const auto maxclients_dvar = game::Dvar_FindVar("sv_maxclients"); - discord_presence.partyMax = maxclients_dvar->current.integer; + discord_strings.state = "Private Match"; + discord_presence.partyMax = max_clients_dvar->current.integer; discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE; } else { - static char hostname[0x80] = {0}; - static const auto hostname_dvar = game::Dvar_FindVar("sv_hostname"); - utils::string::strip(hostname_dvar->current.string, hostname, sizeof(hostname)); - discord_presence.state = hostname; + discord_strings.state = utils::string::strip(hostname_dvar->current.string); const auto server_connection_state = party::get_server_connection_state(); - - const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i", + const auto server_ip_port = std::format("{}.{}.{}.{}:{}", static_cast(server_connection_state.host.ip[0]), static_cast(server_connection_state.host.ip[1]), static_cast(server_connection_state.host.ip[2]), @@ -108,28 +123,22 @@ namespace discord static_cast(ntohs(server_connection_state.host.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_strings.party_id = utils::cryptography::sha1::compute(server_ip_port, true).substr(0, 8); discord_presence.partyMax = server_connection_state.max_clients; discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; - - static char join_secret[0x80] = { 0 }; - strcpy_s(join_secret, 0x80, server_ip_port); - discord_presence.joinSecret = join_secret; + discord_strings.join_secret = server_ip_port; } - auto server_discord_information = party::get_server_discord_information(); - if (!server_discord_information.image.empty()) + auto server_discord_info = party::get_server_discord_info(); + if (server_discord_info.has_value()) { - discord_presence.smallImageKey = server_discord_information.image.data(); - discord_presence.smallImageText = server_discord_information.image_text.data(); + discord_strings.small_image_key = server_discord_info->image; + discord_strings.small_image_text = server_discord_info->image_text; } } else if (game::environment::is_sp()) { - discord_presence.details = mapname; + discord_strings.details = mapname; } if (discord_presence.startTimestamp == 0) @@ -138,12 +147,20 @@ namespace discord std::chrono::system_clock::now().time_since_epoch()).count(); } + discord_presence.state = discord_strings.state.data(); + discord_presence.details = discord_strings.details.data(); + discord_presence.smallImageKey = discord_strings.small_image_key.data(); + discord_presence.smallImageText = discord_strings.small_image_text.data(); + discord_presence.largeImageKey = discord_strings.large_image_key.data(); + discord_presence.largeImageText = discord_strings.large_image_text.data(); + discord_presence.partyId = discord_strings.party_id.data(); + discord_presence.joinSecret = discord_strings.join_secret.data(); + Discord_UpdatePresence(&discord_presence); } void update_discord() { - // reset presence data const auto saved_time = discord_presence.startTimestamp; discord_presence = {}; discord_presence.startTimestamp = saved_time; @@ -158,6 +175,33 @@ namespace discord } } + game::Material* create_avatar_material(const std::string& name, const std::string& data) + { + const auto material = materials::create_material(name); + try + { + if (!materials::setup_material_image(material, data)) + { + materials::free_material(material); + return nullptr; + } + + { + std::lock_guard _0(avatar_map_mutex); + avatar_material_map.insert(std::make_pair(name, material)); + } + + return material; + } + catch (const std::exception& e) + { + materials::free_material(material); + console::error("Failed to load user avatar image: %s\n", e.what()); + } + + return nullptr; + } + void download_user_avatar(const std::string& id, const std::string& avatar) { const auto data = utils::http::get_data( @@ -173,10 +217,10 @@ namespace discord return; } - materials::add(utils::string::va(AVATAR, id.data()), value.buffer); + const auto name = utils::string::va(AVATAR, id.data()); + create_avatar_material(name, value.buffer); } - bool has_default_avatar = false; void download_default_avatar() { const auto data = utils::http::get_data(DEFAULT_AVATAR_URL); @@ -191,8 +235,7 @@ namespace discord return; } - has_default_avatar = true; - materials::add(DEFAULT_AVATAR, value.buffer); + default_avatar_material = create_avatar_material(DEFAULT_AVATAR, value.buffer); } void ready(const DiscordUser* request) @@ -211,6 +254,8 @@ namespace discord void join_game(const char* join_secret) { + console::debug("Discord: join_game called with secret '%s'\n", join_secret); + scheduler::once([=] { game::netadr_s target{}; @@ -232,10 +277,30 @@ namespace discord return; } - std::string user_id = request->userId; - std::string avatar = request->avatar; - std::string discriminator = request->discriminator; - std::string username = request->username; + static std::unordered_map last_requests; + + const std::string user_id = request->userId; + const std::string avatar = request->avatar; + const std::string discriminator = request->discriminator; + const std::string username = request->username; + + const auto now = std::chrono::high_resolution_clock::now(); + auto iter = last_requests.find(user_id); + if (iter != last_requests.end()) + { + if ((now - iter->second) < 15s) + { + return; + } + else + { + iter->second = now; + } + } + else + { + last_requests.insert(std::make_pair(user_id, now)); + } scheduler::once([=] { @@ -251,27 +316,47 @@ namespace discord }); }, scheduler::pipeline::lui); - if (!materials::exists(utils::string::va(AVATAR, user_id.data()))) + const auto material_name = utils::string::va(AVATAR, user_id.data()); + if (!avatar.empty() && !avatar_material_map.contains(material_name)) { download_user_avatar(user_id, avatar); } } + + void set_default_bindings() + { + const auto set_binding = [](const std::string& command, const game::keyNum_t key) + { + const auto binding = game::Key_GetBindingForCmd(command.data()); + for (auto i = 0; i < 256; i++) + { + if (game::playerKeys[0].keys[i].binding == binding) + { + return; + } + } + + if (game::playerKeys[0].keys[key].binding == 0) + { + game::Key_SetBinding(0, key, binding); + } + }; + + set_binding("discord_accept", game::K_F1); + set_binding("discord_deny", game::K_F2); + } } - std::string get_avatar_material(const std::string& id) + game::Material* get_avatar_material(const std::string& id) { - const auto avatar_name = utils::string::va(AVATAR, id.data()); - if (materials::exists(avatar_name)) + const auto material_name = utils::string::va(AVATAR, id.data()); + const auto iter = avatar_material_map.find(material_name); + if (iter == avatar_material_map.end()) { - return avatar_name; + return default_avatar_material; } - if (has_default_avatar) - { - return DEFAULT_AVATAR; - } - - return "black"; + return iter->second; } void respond(const std::string& id, int reply) @@ -311,16 +396,29 @@ namespace discord Discord_Initialize("947125042930667530", &handlers, 1, nullptr); - scheduler::once(download_default_avatar, scheduler::pipeline::async); - - scheduler::once([]() + if (game::environment::is_mp()) { - 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); + scheduler::on_game_initialized([] + { + scheduler::once(download_default_avatar, scheduler::async); + set_default_bindings(); + }, scheduler::main); + } + + scheduler::loop(Discord_RunCallbacks, scheduler::async, 500ms); + scheduler::loop(update_discord, scheduler::async, 5s); initialized_ = true; + + command::add("discord_accept", []() + { + ui_scripting::notify("discord_response", {{"accept", true}}); + }); + + command::add("discord_deny", []() + { + ui_scripting::notify("discord_response", {{"accept", false}}); + }); } void pre_destroy() override diff --git a/src/client/component/discord.hpp b/src/client/component/discord.hpp index 5399f952..54c75ca9 100644 --- a/src/client/component/discord.hpp +++ b/src/client/component/discord.hpp @@ -1,7 +1,9 @@ #pragma once +#include "game/game.hpp" + namespace discord { - std::string get_avatar_material(const std::string& id); + game::Material* get_avatar_material(const std::string& id); void respond(const std::string& id, int reply); } diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index 5112620e..36fbb49e 100644 --- a/src/client/component/input.cpp +++ b/src/client/component/input.cpp @@ -17,15 +17,6 @@ namespace input void cl_char_event_stub(const int local_client_num, const int key) { - if (game::environment::is_sp() && ui_scripting::lui_running()) - { - ui_scripting::notify("keypress", - { - {"keynum", key}, - {"key", game::Key_KeynumToString(key, 0, 1)}, - }); - } - if (!game_console::console_char_event(local_client_num, key)) { return; @@ -36,15 +27,6 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { - if (ui_scripting::lui_running()) - { - ui_scripting::notify(down ? "keydown" : "keyup", - { - {"keynum", key}, - {"key", game::Key_KeynumToString(key, 0, 1)}, - }); - } - if (!game_console::console_key_event(local_client_num, key, down)) { return; diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp index 5646bb06..60590679 100644 --- a/src/client/component/materials.cpp +++ b/src/client/component/materials.cpp @@ -21,7 +21,6 @@ namespace materials namespace { utils::hook::detour db_material_streaming_fail_hook; - utils::hook::detour material_register_handle_hook; utils::hook::detour db_get_material_index_hook; #ifdef DEBUG @@ -31,120 +30,8 @@ namespace materials const game::dvar_t* debug_materials = nullptr; #endif - struct material_data_t - { - std::unordered_map materials; - std::unordered_map images; - }; - char constant_table[0x20] = {}; - - utils::concurrency::container material_data; - - game::GfxImage* setup_image(game::GfxImage* image, const utils::image& raw_image) - { - image->imageFormat = 0x1000003; - image->resourceSize = -1; - - D3D11_SUBRESOURCE_DATA data{}; - data.SysMemPitch = raw_image.get_width() * 4; - data.SysMemSlicePitch = data.SysMemPitch * raw_image.get_height(); - data.pSysMem = raw_image.get_buffer(); - - game::Image_Setup(image, raw_image.get_width(), raw_image.get_height(), image->depth, image->numElements, - image->imageFormat, DXGI_FORMAT_R8G8B8A8_UNORM, image->name, &data); - - return image; - } - - game::Material* create_material(const std::string& name, const std::string& data) - { - const auto white = material_register_handle_hook.invoke("white"); - const auto material = utils::memory::get_allocator()->allocate(); - const auto texture_table = utils::memory::get_allocator()->allocate(); - const auto image = utils::memory::get_allocator()->allocate(); - - std::memcpy(material, white, sizeof(game::Material)); - std::memcpy(texture_table, white->textureTable, sizeof(game::MaterialTextureDef)); - std::memcpy(image, white->textureTable->u.image, sizeof(game::GfxImage)); - - material->constantTable = &constant_table; - material->name = utils::memory::get_allocator()->duplicate_string(name); - image->name = material->name; - - material->textureTable = texture_table; - material->textureTable->u.image = setup_image(image, data); - - return material; - } - - void free_material(game::Material* material) - { - material->textureTable->u.image->textures.___u0.map->Release(); - material->textureTable->u.image->textures.shaderView->Release(); - utils::memory::get_allocator()->free(material->textureTable->u.image); - utils::memory::get_allocator()->free(material->textureTable); - utils::memory::get_allocator()->free(material->name); - utils::memory::get_allocator()->free(material); - } - - game::Material* load_material(const std::string& name) - { - return material_data.access([&](material_data_t& data_) -> game::Material* - { - if (const auto i = data_.materials.find(name); i != data_.materials.end()) - { - return i->second; - } - - std::string data{}; - if (const auto i = data_.images.find(name); i != data_.images.end()) - { - data = i->second; - } - - if (data.empty() && !filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data)) - { - data_.materials[name] = nullptr; - return nullptr; - } - - const auto material = create_material(name, data); - data_.materials[name] = material; - - return material; - }); - } - - game::Material* try_load_material(const std::string& name) - { - if (name == "white") - { - return nullptr; - } - - try - { - return load_material(name); - } - catch (const std::exception& e) - { - console::error("Failed to load material %s: %s\n", name.data(), e.what()); - } - - return nullptr; - } - - game::Material* material_register_handle_stub(const char* name) - { - auto result = try_load_material(name); - if (result == nullptr) - { - result = material_register_handle_hook.invoke(name); - } - return result; - } - + int db_material_streaming_fail_stub(game::Material* material) { if (material->constantTable == &constant_table) @@ -238,38 +125,73 @@ namespace materials #endif } - void add(const std::string& name, const std::string& data) + bool setup_material_image(game::Material* material, const std::string& data) { - material_data.access([&](material_data_t& data_) + if (*game::d3d11_device == nullptr) { - data_.images[name] = data; - }); + console::error("Tried to create texture while d3d11 device isn't initialized\n"); + return false; + } + + const auto image = material->textureTable->u.image; + image->imageFormat = 0x1000003; + image->resourceSize = -1; + + auto raw_image = utils::image{data}; + + D3D11_SUBRESOURCE_DATA resource_data{}; + resource_data.SysMemPitch = raw_image.get_width() * 4; + resource_data.SysMemSlicePitch = resource_data.SysMemPitch * raw_image.get_height(); + resource_data.pSysMem = raw_image.get_buffer(); + + game::Image_Setup(image, raw_image.get_width(), raw_image.get_height(), image->depth, image->numElements, + image->imageFormat, DXGI_FORMAT_R8G8B8A8_UNORM, image->name, &resource_data); + return true; } - bool exists(const std::string& name) + game::Material* create_material(const std::string& name) { - return material_data.access([&](material_data_t& data_) - { - return data_.images.find(name) != data_.images.end(); - }); + const auto white = game::Material_RegisterHandle("$white"); + const auto material = utils::memory::allocate(); + const auto texture_table = utils::memory::allocate(); + const auto image = utils::memory::allocate(); + + std::memcpy(material, white, sizeof(game::Material)); + std::memcpy(texture_table, white->textureTable, sizeof(game::MaterialTextureDef)); + std::memcpy(image, white->textureTable->u.image, sizeof(game::GfxImage)); + + material->constantTable = &constant_table; + material->name = utils::memory::duplicate_string(name); + image->name = material->name; + + image->textures.map = nullptr; + image->textures.shaderView = nullptr; + image->textures.shaderViewAlternate = nullptr; + + material->textureTable = texture_table; + + return material; } - void clear() + void free_material(game::Material* material) { - material_data.access([&](material_data_t& data_) + const auto try_release = [](T** resource) { - for (auto& material : data_.materials) + if (*resource != nullptr) { - if (material.second == nullptr) - { - continue; - } - - free_material(material.second); + (*resource)->Release(); + *resource = nullptr; } + }; - data_.materials.clear(); - }); + try_release(&material->textureTable->u.image->textures.map); + try_release(&material->textureTable->u.image->textures.shaderView); + try_release(&material->textureTable->u.image->textures.shaderViewAlternate); + + utils::memory::free(material->textureTable->u.image); + utils::memory::free(material->textureTable); + utils::memory::free(material->name); + utils::memory::free(material); } class component final : public component_interface @@ -282,7 +204,6 @@ namespace materials return; } - material_register_handle_hook.create(game::Material_RegisterHandle, material_register_handle_stub); db_material_streaming_fail_hook.create(SELECT_VALUE(0x1FB400_b, 0x3A1600_b), db_material_streaming_fail_stub); db_get_material_index_hook.create(SELECT_VALUE(0x1F1D80_b, 0x396000_b), db_get_material_index_stub); @@ -296,7 +217,7 @@ namespace materials scheduler::once([] { - debug_materials = dvars::register_bool("debug_materials", 0, 0x0, "Print current material and images"); + debug_materials = dvars::register_bool("debug_materials", false, game::DVAR_FLAG_NONE, "Print current material and images"); }, scheduler::main); } #endif diff --git a/src/client/component/materials.hpp b/src/client/component/materials.hpp index 0c02227f..7ca0ddfa 100644 --- a/src/client/component/materials.hpp +++ b/src/client/component/materials.hpp @@ -1,8 +1,10 @@ #pragma once +#include "game/game.hpp" + namespace materials { - void add(const std::string& name, const std::string& data); - bool exists(const std::string& name); - void clear(); + bool setup_material_image(game::Material* material, const std::string& data); + game::Material* create_material(const std::string& name); + void free_material(game::Material* material); } diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index 83c0f6dd..cfabb84d 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -30,7 +30,6 @@ namespace mods { if (release_assets) { - materials::clear(); fonts::clear(); } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 5aa10986..0ccf100b 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -33,7 +33,7 @@ namespace party namespace { connection_state server_connection_state{}; - discord_information server_discord_information{}; + std::optional server_discord_info{}; struct usermap_file { @@ -755,9 +755,9 @@ namespace party return server_connection_state; } - discord_information get_server_discord_information() + std::optional get_server_discord_info() { - return server_discord_information; + return server_discord_info; } class component final : public component_interface @@ -1152,8 +1152,14 @@ namespace party server_connection_state.motd = info.get("sv_motd"); server_connection_state.max_clients = std::stoi(info.get("sv_maxclients")); server_connection_state.base_url = info.get("sv_wwwBaseUrl"); - server_discord_information.image = info.get("sv_discordImageUrl"); - server_discord_information.image_text = info.get("sv_discordImageText"); + + discord_information discord_info{}; + discord_info.image = info.get("sv_discordImageUrl"); + discord_info.image_text = info.get("sv_discordImageText"); + if (!discord_info.image.empty() || !discord_info.image_text.empty()) + { + server_discord_info.emplace(discord_info); + } connect_to_party(target, mapname, gametype); }); diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index 6a367d66..a9c387bf 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -30,7 +30,7 @@ namespace party void clear_sv_motd(); connection_state get_server_connection_state(); - discord_information get_server_discord_information(); + std::optional get_server_discord_info(); int get_client_num_by_name(const std::string& name); diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index d0590f71..c62dd8ea 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -376,7 +376,19 @@ namespace ui_scripting lua["discord"] = discord_table; discord_table["respond"] = discord::respond; - discord_table["getavatarmaterial"] = discord::get_avatar_material; + + discord_table["getavatarmaterial"] = [](const std::string& id) + -> script_value + { + const auto material = discord::get_avatar_material(id); + if (material == nullptr) + { + return {}; + } + + return lightuserdata(material); + }; + discord_table["reply"] = table(); discord_table["reply"]["yes"] = DISCORD_REPLY_YES; discord_table["reply"]["ignore"] = DISCORD_REPLY_IGNORE; diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 6150c8ee..f2f57509 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1542,18 +1542,16 @@ namespace game char data[1]; }; - union $3FA29451CE6F1FA138A5ABAB84BE9676 - { - ID3D11Texture1D* linemap; - ID3D11Texture2D* map; - ID3D11Texture3D* volmap; - ID3D11Texture2D* cubemap; - GfxImageLoadDef* loadDef; - }; - struct GfxTexture { - $3FA29451CE6F1FA138A5ABAB84BE9676 ___u0; + union + { + ID3D11Texture1D* linemap; + ID3D11Texture2D* map; + ID3D11Texture3D* volmap; + ID3D11Texture2D* cubemap; + GfxImageLoadDef* loadDef; + }; ID3D11ShaderResourceView* shaderView; ID3D11ShaderResourceView* shaderViewAlternate; }; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 7f8b2ea0..3c77e591 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -125,6 +125,8 @@ namespace game WEAK symbol I_CleanStr{0x4293E0, 0x5AF2E0}; WEAK symbol Key_KeynumToString{0x1AC410, 0x199990}; + WEAK symbol Key_GetBindingForCmd{0x377280, 0x1572B0}; + WEAK symbol Key_SetBinding{0x1AC570, 0x199AE0}; WEAK symbol Live_SyncOnlineDataFlags{0x0, 0x1A5C10}; @@ -329,7 +331,7 @@ namespace game WEAK symbol g_zoneInfo{0x0, 0x5F5A370}; WEAK symbol g_zoneIndex{0x0, 0x3D1008C}; - WEAK symbol< DB_FileSysInterface*> db_fs{0x25C1168, 0x1566C08}; + WEAK symbol db_fs{0x25C1168, 0x1566C08}; WEAK symbol keyCatchers{0x252AF70, 0x2EC82C4}; WEAK symbol playerKeys{0x2395B0C, 0x2999E1C}; @@ -347,6 +349,8 @@ namespace game WEAK symbol maps{0x7CE5A0, 0x926C80}; + WEAK symbol d3d11_device{0x1163B98, 0x12DFBF8}; + namespace mp { WEAK symbol g_entities{0x0, 0x71F19E0}; diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 30e35e9d..b28dab70 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -131,6 +131,14 @@ namespace utils::string *out = '\0'; } + std::string strip(const std::string& string) + { + std::string new_string; + new_string.resize(string.size(), 0); + strip(string.data(), new_string.data(), static_cast(new_string.size())); + return new_string; + } + std::string convert(const std::wstring& wstr) { std::string result; diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index 13bcbbf8..fa1c696f 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -92,6 +92,7 @@ namespace utils::string std::string get_clipboard_data(); void strip(const char* in, char* out, int max); + std::string strip(const std::string& string); std::string convert(const std::wstring& wstr); std::wstring convert(const std::string& str);