diff --git a/data/cdata/ui_scripts/discord/__init__.lua b/data/cdata/ui_scripts/discord/__init__.lua index fe988a68..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,20 +11,6 @@ local container = LUI.UIVerticalList.new({ spacing = 5 }) -function canasktojoin(userid) - history = history or {} - if (history[userid] ~= nil) then - return false - end - - history[userid] = true - game:ontimeout(function() - history[userid] = nil - end, 15000) - - return true -end - function truncatename(name, length) if (#name <= length - 3) then return name @@ -33,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) @@ -105,7 +121,7 @@ function addrequest(request) width = 32, height = 32, left = 1, - material = RegisterMaterial(avatarmaterial) + material = avatarmaterial }) local username = LUI.UIText.new({ @@ -119,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, @@ -156,51 +178,54 @@ 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() @@ -236,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")) @@ -250,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 4ec5413f..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,124 +27,181 @@ namespace discord { namespace { - DiscordRichPresence discord_presence; - - void update_discord() + struct discord_presence_state_t { - if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) + 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; + + static const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange"); + if (in_firing_range != nullptr && 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.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 map = game::Dvar_FindVar("mapname")->current.string; - const auto key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map); - const char* mapname = map; - - if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, key) && !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, key)) - { - mapname = game::UI_SafeTranslateString(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)); - - 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; - discord_presence.largeImageKey = map; - - if (!fastfiles::is_stock_map(map)) - { - discord_presence.largeImageKey = "menu_multiplayer"; - } - } - 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_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_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) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, presence_key)) + { + mapname = game::UI_SafeTranslateString(presence_key); + } + + if (game::environment::is_mp()) + { + 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); + const auto gametype = utils::string::strip(gametype_display_name); + + discord_strings.details = std::format("{} on {}", gametype, mapname); + + const auto client_state = *game::mp::client_state; + if (client_state != nullptr) + { + discord_presence.partySize = client_state->num_players; + } + + if (game::SV_Loaded()) + { + discord_strings.state = "Private Match"; + discord_presence.partyMax = max_clients_dvar->current.integer; + discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE; + } + else + { + 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 = 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]), + static_cast(server_connection_state.host.ip[3]), + static_cast(ntohs(server_connection_state.host.port)) + ); + + 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; + discord_strings.join_secret = server_ip_port; + } + + auto server_discord_info = party::get_server_discord_info(); + if (server_discord_info.has_value()) + { + 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_strings.details = mapname; + } + + if (discord_presence.startTimestamp == 0) + { + discord_presence.startTimestamp = std::chrono::duration_cast( + 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() + { + 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(); + } + } + + 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( @@ -162,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); @@ -180,25 +235,128 @@ 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) + { + DiscordRichPresence presence{}; + presence.instance = 1; + presence.state = ""; + console::info("Discord: Ready on %s (%s)\n", request->username, request->userId); + Discord_UpdatePresence(&presence); + } + + void errored(const int error_code, const char* message) + { + console::error("Discord: %s (%i)\n", message, error_code); + } + + 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); + party::connect(target); + } + }, scheduler::pipeline::main); + } + + void join_request(const DiscordUser* request) + { + console::debug("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; + } + + 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([=] + { + 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); + + 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) @@ -212,15 +370,14 @@ namespace discord class component final : public component_interface { public: - void post_load() override + void post_unpack() override { if (game::environment::is_dedi()) { return; } - DiscordEventHandlers handlers; - ZeroMemory(&handlers, sizeof(handlers)); + DiscordEventHandlers handlers{}; handlers.ready = ready; handlers.errored = errored; handlers.disconnected = errored; @@ -239,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 @@ -263,67 +433,6 @@ namespace discord 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) - { - scheduler::once([=] - { - game::netadr_s target{}; - if (game::NET_StringToAdr(join_secret, &target)) - { - console::info("Discord: Connecting to server: %s\n", join_secret); - 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); - } - } }; } 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/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); } diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index ad14ae43..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 (game::environment::is_sp() && 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 9a484405..0ccf100b 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{}; + std::optional server_discord_info{}; 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,9 +750,14 @@ namespace party } } - int server_client_count() + connection_state get_server_connection_state() { - return party::sv_maxclients; + return server_connection_state; + } + + std::optional get_server_discord_info() + { + return server_discord_info; } class component final : public component_interface @@ -777,7 +770,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()) @@ -849,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; @@ -862,7 +855,7 @@ namespace party } else { - connect(connect_state.host); + connect(server_connection_state.host); } }); @@ -965,7 +958,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 +1053,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)) { @@ -1092,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; } @@ -1108,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; @@ -1154,8 +1149,17 @@ 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"); + + 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 ca4852b2..a9c387bf 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -3,19 +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(); + connection_state get_server_connection_state(); + std::optional get_server_discord_info(); 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..c62dd8ea 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 @@ -367,7 +370,29 @@ 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; + + auto discord_table = table(); + lua["discord"] = discord_table; + + discord_table["respond"] = discord::respond; + + 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; + discord_table["reply"]["no"] = DISCORD_REPLY_NO; } void start() 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);