diff --git a/data/ui_scripts/discord/__init__.lua b/data/ui_scripts/discord/__init__.lua new file mode 100644 index 00000000..cd2e70fc --- /dev/null +++ b/data/ui_scripts/discord/__init__.lua @@ -0,0 +1,272 @@ +if (game:issingleplayer() or Engine.InFrontend()) then + return +end + +local container = LUI.UIVerticalList.new({ + topAnchor = true, + rightAnchor = true, + top = 20, + right = 200, + width = 200, + 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 + end + + return name:sub(1, length - 3) .. "..." +end + +function addrequest(request) + if (not canasktojoin(request.userid)) then + return + end + + if (container.temp) then + container:removeElement(container.temp) + container.temp = nil + end + + local invite = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + height = 75, + }) + + invite:registerAnimationState("move_in", { + leftAnchor = true, + height = 75, + width = 200, + left = -220, + }) + + invite:animateToState("move_in", 100) + + local background = LUI.UIImage.new({ + topAnchor = true, + leftAnchor = true, + rightAnchor = true, + bottomAnchor = true, + top = 1, + left = 1, + bottom = -1, + right = -1, + material = luiglobals.RegisterMaterial("white"), + color = { + r = 0, + b = 0, + g = 0, + }, + alpha = 0.6, + }) + + local border = LUI.UIImage.new({ + topAnchor = true, + leftAnchor = true, + rightAnchor = true, + bottomAnchor = true, + material = luiglobals.RegisterMaterial("btn_focused_rect_innerglow"), + }) + + border:setup9SliceImage(10, 5, 0.25, 0.12) + + local paddingvalue = 10 + local padding = LUI.UIElement.new({ + topAnchor = true, + leftAnchor = true, + rightAnchor = true, + bottomAnchor = true, + top = paddingvalue, + left = paddingvalue, + right = -paddingvalue, + bottom = -paddingvalue, + }) + + local avatarmaterial = discord.getavatarmaterial(request.userid) + local avatar = LUI.UIImage.new({ + leftAnchor = true, + topAnchor = true, + width = 32, + height = 32, + left = 1, + material = luiglobals.RegisterMaterial(avatarmaterial) + }) + + local username = LUI.UIText.new({ + leftAnchor = true, + topAnchor = true, + height = 12, + left = 32 + paddingvalue, + color = luiglobals.Colors.white, + alignment = LUI.Alignment.Left, + rightAnchor = true, + font = CoD.TextSettings.BodyFontBold.Font + }) + + username:setText(string.format("%s^7#%s requested to join your game!", + truncatename(request.username, 18), request.discriminator)) + + local buttons = LUI.UIElement.new({ + leftAnchor = true, + rightAnchor = true, + topAnchor = true, + top = 37, + height = 18, + }) + + local createbutton = function(text, left) + local button = LUI.UIElement.new({ + leftAnchor = left, + rightAnchor = not left, + topAnchor = true, + height = 18, + width = 85, + material = luiglobals.RegisterMaterial("btn_focused_rect_innerglow"), + }) + + local center = LUI.UIText.new({ + rightAnchor = true, + height = 12, + width = 85, + top = -6.5, + alignment = LUI.Alignment.Center, + font = CoD.TextSettings.BodyFontBold.Font + }) + + button:setup9SliceImage(10, 5, 0.25, 0.12) + center:setText(text) + button:addElement(center) + + return button + end + + buttons:addElement(createbutton("[F1] Accept", true)) + buttons:addElement(createbutton("[F2] Deny")) + + local fadeouttime = 50 + local timeout = 10 * 1000 - fadeouttime + + local function close() + container:processEvent({ + name = "update_navigation", + dispatchToChildren = true + }) + 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) + end) + end + + buttons:registerEventHandler("keydown_", function(element, event) + if (event.key == "F1") then + close() + discord.respond(request.userid, discord.reply.yes) + end + + if (event.key == "F2") then + close() + discord.respond(request.userid, discord.reply.no) + end + end) + + invite:registerAnimationState("fade_out", { + leftAnchor = true, + rightAnchor = true, + height = 75, + alpha = 0, + left = 0 + }) + + invite:addElement(LUI.UITimer.new(timeout, "end_invite")) + invite:registerEventHandler("end_invite", function() + close() + discord.respond(request.userid, discord.reply.ignore) + end) + + local bar = LUI.UIImage.new({ + bottomAnchor = true, + leftAnchor = true, + bottom = -3, + left = 3, + width = 200 - 6, + material = luiglobals.RegisterMaterial("white"), + height = 2, + color = { + r = 92 / 255, + g = 206 / 255, + b = 113 / 255, + } + }) + + bar:registerAnimationState("closing", { + bottomAnchor = true, + leftAnchor = true, + bottom = -3, + left = 3, + width = 0, + height = 2, + }) + + bar:animateToState("closing", timeout) + + avatar:registerEventHandler("update", function() + local avatarmaterial = discord.getavatarmaterial(request.userid) + avatar:setImage(luiglobals.RegisterMaterial(avatarmaterial)) + end) + + avatar:addElement(LUI.UITimer.new(100, "update")) + + invite:addElement(background) + invite:addElement(bar) + invite:addElement(border) + invite:addElement(padding) + padding:addElement(username) + padding:addElement(avatar) + padding:addElement(buttons) + + container:addElement(invite) +end + +container:registerEventHandler("keydown", function(element, event) + local first = container:getFirstChild() + + if (not first) then + return + end + + first:processEvent({ + name = "keydown_", + key = event.key + }) +end) + +LUI.roots.UIRoot0:registerEventHandler("discord_join_request", function(element, event) + addrequest(event.request) +end) + +LUI.roots.UIRoot0:addElement(container) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 455c510f..65fb74be 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -7,12 +7,25 @@ #include "command.hpp" #include "network.hpp" #include "party.hpp" +#include "materials.hpp" + +#include "ui_scripting.hpp" +#include "game/ui_scripting/execution.hpp" #include #include +#include #include +#define DEFAULT_AVATAR "discord_default_avatar" +#define AVATAR "discord_avatar_%s" + +#define DEFAULT_AVATAR_URL "https://cdn.discordapp.com/embed/avatars/0.png" +#define AVATAR_URL "https://cdn.discordapp.com/avatars/%s/%s.png?size=128" + +#include "discord.hpp" + namespace discord { namespace @@ -21,8 +34,6 @@ namespace discord void update_discord() { - Discord_RunCallbacks(); - if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) { discord_presence.details = game::environment::is_sp() ? "Singleplayer" : "Multiplayer"; @@ -105,6 +116,51 @@ namespace discord Discord_UpdatePresence(&discord_presence); } + + void download_user_avatar(const std::string& id, const std::string& avatar) + { + const auto data = utils::http::get_data( + utils::string::va(AVATAR_URL, id.data(), avatar.data())); + if (data.has_value()) + { + materials::add(utils::string::va(AVATAR, id.data()), data.value()); + } + } + + bool has_default_avatar = false; + void download_default_avatar() + { + const auto data = utils::http::get_data(DEFAULT_AVATAR_URL); + if (data.has_value()) + { + has_default_avatar = true; + materials::add(DEFAULT_AVATAR, data.value()); + } + } + } + + std::string get_avatar_material(const std::string& id) + { + const auto avatar_name = utils::string::va(AVATAR, id.data()); + if (materials::exists(avatar_name)) + { + return avatar_name; + } + + if (has_default_avatar) + { + return DEFAULT_AVATAR; + } + + return "black"; + } + + void respond(const std::string& id, int reply) + { + scheduler::once([=]() + { + Discord_Respond(id.data(), reply); + }, scheduler::pipeline::async); } class component final : public component_interface @@ -122,16 +178,19 @@ namespace discord handlers.ready = ready; handlers.errored = errored; handlers.disconnected = errored; - handlers.joinGame = joinGame; + handlers.joinGame = join_game; handlers.spectateGame = nullptr; - handlers.joinRequest = joinRequest; + handlers.joinRequest = join_request; Discord_Initialize("947125042930667530", &handlers, 1, nullptr); + scheduler::once(download_default_avatar, scheduler::pipeline::async); + scheduler::once([]() { scheduler::once(update_discord, scheduler::pipeline::async); scheduler::loop(update_discord, scheduler::pipeline::async, 5s); + scheduler::loop(Discord_RunCallbacks, scheduler::pipeline::async, 1s); }, scheduler::pipeline::main); initialized_ = true; @@ -153,11 +212,8 @@ namespace discord 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); } @@ -166,25 +222,55 @@ namespace discord console::error("Discord: Error (%i): %s\n", error_code, message); } - static void joinGame(const char* joinSecret) + static void join_game(const char* join_secret) { - console::info("Discord: Join game called with join secret: %s\n", joinSecret); + console::info("Discord: Join game called with join secret: %s\n", join_secret); - scheduler::once([joinSecret]() + std::string secret = join_secret; + scheduler::once([=]() { game::netadr_s target{}; - if (game::NET_StringToAdr(joinSecret, &target)) + if (game::NET_StringToAdr(secret.data(), &target)) { - console::info("Discord: Connecting to server: %s\n", joinSecret); + console::info("Discord: Connecting to server: %s\n", secret.data()); party::connect(target); } }, scheduler::pipeline::main); } - static void joinRequest(const DiscordUser* request) + static void join_request(const DiscordUser* request) { - console::info("Discord: joinRequest from %s (%s)\n", request->username, request->userId); - // Discord_Respond(request->userId, DISCORD_REPLY_YES); + 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 new file mode 100644 index 00000000..5399f952 --- /dev/null +++ b/src/client/component/discord.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace discord +{ + std::string 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 7801bcbd..43aff87c 100644 --- a/src/client/component/input.cpp +++ b/src/client/component/input.cpp @@ -4,6 +4,7 @@ #include "game/game.hpp" #include "game_console.hpp" +#include "ui_scripting.hpp" #include "game/ui_scripting/execution.hpp" #include @@ -15,14 +16,10 @@ namespace input utils::hook::detour cl_char_event_hook; utils::hook::detour cl_key_event_hook; - bool lui_running() - { - return *game::hks::lua_state != nullptr; - } void cl_char_event_stub(const int local_client_num, const int key) { - if (lui_running()) + if (ui_scripting::lui_running()) { ui_scripting::notify("keypress", { @@ -41,7 +38,7 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { - if (lui_running()) + if (ui_scripting::lui_running()) { ui_scripting::notify(down ? "keydown" : "keyup", { diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp index 4fbb3ed8..c5fed9a2 100644 --- a/src/client/component/materials.cpp +++ b/src/client/component/materials.cpp @@ -166,6 +166,14 @@ namespace materials }); } + bool exists(const std::string& name) + { + return material_data.access([&](material_data_t& data_) + { + return data_.images.find(name) != data_.images.end(); + }); + } + void clear() { material_data.access([&](material_data_t& data_) diff --git a/src/client/component/materials.hpp b/src/client/component/materials.hpp index 3a548548..0c02227f 100644 --- a/src/client/component/materials.hpp +++ b/src/client/component/materials.hpp @@ -3,5 +3,6 @@ namespace materials { void add(const std::string& name, const std::string& data); + bool exists(const std::string& name); void clear(); } diff --git a/src/client/component/scheduler.cpp b/src/client/component/scheduler.cpp index de9b4983..68d82dd1 100644 --- a/src/client/component/scheduler.cpp +++ b/src/client/component/scheduler.cpp @@ -88,6 +88,7 @@ namespace scheduler utils::hook::detour r_end_frame_hook; utils::hook::detour g_run_frame_hook; utils::hook::detour main_frame_hook; + utils::hook::detour hks_frame_hook; void execute(const pipeline type) { @@ -112,6 +113,15 @@ namespace scheduler main_frame_hook.invoke(); execute(pipeline::main); } + + void hks_frame_stub() + { + const auto state = *game::hks::lua_state; + if (state) + { + execute(pipeline::lui); + } + } } void schedule(const std::function& callback, const pipeline type, @@ -183,6 +193,7 @@ namespace scheduler r_end_frame_hook.create(SELECT_VALUE(0x1404F7310, 0x1405FE470), scheduler::r_end_frame_stub); g_run_frame_hook.create(SELECT_VALUE(0x1402772D0, 0x14033A640), scheduler::server_frame_stub); main_frame_hook.create(SELECT_VALUE(0x1401CE8D0, 0x1400D8310), scheduler::main_frame_stub); + hks_frame_hook.create(SELECT_VALUE(0x1400E37F0, 0x1401755B0), scheduler::hks_frame_stub); } void pre_destroy() override diff --git a/src/client/component/scheduler.hpp b/src/client/component/scheduler.hpp index f26c60ce..68f78d54 100644 --- a/src/client/component/scheduler.hpp +++ b/src/client/component/scheduler.hpp @@ -16,6 +16,9 @@ namespace scheduler // The game's main thread main, + // LUI context + lui, + count, }; diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index c4f6d06f..c6b44883 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -93,6 +93,7 @@ namespace ui_scripting void hks_shutdown_stub() { + converted_functions.clear(); ui_scripting::lua::engine::stop(); hks_shutdown_hook.invoke(); } @@ -107,15 +108,6 @@ namespace ui_scripting return hks_allocator_hook.invoke(userData, oldMemory, oldSize, newSize); } - - void hks_frame_stub() - { - const auto state = *game::hks::lua_state; - if (state) - { - ui_scripting::lua::engine::run_frame(); - } - } } int main_function_handler(game::hks::lua_State* state) @@ -132,7 +124,7 @@ namespace ui_scripting return 0; } - const auto function = converted_functions[closure]; + const auto& function = converted_functions[closure]; const auto count = static_cast(state->m_apistack.top - state->m_apistack.base); const auto arguments = get_return_values(count); const auto s = function.lua_state(); @@ -175,6 +167,11 @@ namespace ui_scripting error_hook_enabled = false; } + bool lui_running() + { + return *game::hks::lua_state != nullptr; + } + class component final : public component_interface { public: @@ -186,11 +183,12 @@ namespace ui_scripting return; } + scheduler::loop(ui_scripting::lua::engine::run_frame, scheduler::pipeline::lui); + hks_start_hook.create(SELECT_VALUE(0x1400E4B40, 0x140176A40), hks_start_stub); hks_shutdown_hook.create(SELECT_VALUE(0x1400DD3D0, 0x14016CA80), hks_shutdown_stub); hksi_lual_error_hook.create(SELECT_VALUE(0x1400A5EA0, 0x14012F300), hksi_lual_error_stub); hks_allocator_hook.create(SELECT_VALUE(0x14009B570, 0x14012BAC0), hks_allocator_stub); - hks_frame_hook.create(SELECT_VALUE(0x1400E37F0, 0x1401755B0), hks_frame_stub); lui_error_hook.create(SELECT_VALUE(0x14007D7D0, 0x14010C9E0), lui_error_stub); hksi_hks_error_hook.create(SELECT_VALUE(0x14009DD80, 0x14012E390), hksi_hks_error_stub); diff --git a/src/client/component/ui_scripting.hpp b/src/client/component/ui_scripting.hpp index 2a48f6ec..b95710b3 100644 --- a/src/client/component/ui_scripting.hpp +++ b/src/client/component/ui_scripting.hpp @@ -9,4 +9,6 @@ namespace ui_scripting void enable_error_hook(); void disable_error_hook(); + + bool lui_running(); } \ No newline at end of file diff --git a/src/client/game/ui_scripting/lua/context.cpp b/src/client/game/ui_scripting/lua/context.cpp index d9a930f9..4a6294b6 100644 --- a/src/client/game/ui_scripting/lua/context.cpp +++ b/src/client/game/ui_scripting/lua/context.cpp @@ -14,10 +14,13 @@ #include "../../../component/fastfiles.hpp" #include "../../../component/scripting.hpp" #include "../../../component/mods.hpp" +#include "../../../component/discord.hpp" #include "component/game_console.hpp" #include "component/scheduler.hpp" +#include + #include #include #include @@ -265,36 +268,6 @@ namespace ui_scripting::lua return sol::as_returns(returns); }; - state["luiglobals"] = table((*::game::hks::lua_state)->globals.v.table); - state["CoD"] = state["luiglobals"]["CoD"]; - state["LUI"] = state["luiglobals"]["LUI"]; - state["Engine"] = state["luiglobals"]["Engine"]; - state["Game"] = state["luiglobals"]["Game"]; - - auto updater_table = sol::table::create(state.lua_state()); - - updater_table["relaunch"] = updater::relaunch; - - updater_table["sethastriedupdate"] = updater::set_has_tried_update; - updater_table["gethastriedupdate"] = updater::get_has_tried_update; - updater_table["autoupdatesenabled"] = updater::auto_updates_enabled; - - updater_table["startupdatecheck"] = updater::start_update_check; - updater_table["isupdatecheckdone"] = updater::is_update_check_done; - updater_table["getupdatecheckstatus"] = updater::get_update_check_status; - updater_table["isupdateavailable"] = updater::is_update_available; - - updater_table["startupdatedownload"] = updater::start_update_download; - updater_table["isupdatedownloaddone"] = updater::is_update_download_done; - updater_table["getupdatedownloadstatus"] = updater::get_update_download_status; - updater_table["cancelupdate"] = updater::cancel_update; - updater_table["isrestartrequired"] = updater::is_restart_required; - - updater_table["getlasterror"] = updater::get_last_error; - updater_table["getcurrentfile"] = updater::get_current_file; - - state["updater"] = updater_table; - if (::game::environment::is_sp()) { struct player @@ -339,6 +312,47 @@ namespace ui_scripting::lua }, ::scheduler::pipeline::server); }; } + + state["luiglobals"] = table((*::game::hks::lua_state)->globals.v.table); + state["CoD"] = state["luiglobals"]["CoD"]; + state["LUI"] = state["luiglobals"]["LUI"]; + state["Engine"] = state["luiglobals"]["Engine"]; + state["Game"] = state["luiglobals"]["Game"]; + + auto updater_table = sol::table::create(state.lua_state()); + + updater_table["relaunch"] = updater::relaunch; + + updater_table["sethastriedupdate"] = updater::set_has_tried_update; + updater_table["gethastriedupdate"] = updater::get_has_tried_update; + updater_table["autoupdatesenabled"] = updater::auto_updates_enabled; + + updater_table["startupdatecheck"] = updater::start_update_check; + updater_table["isupdatecheckdone"] = updater::is_update_check_done; + updater_table["getupdatecheckstatus"] = updater::get_update_check_status; + updater_table["isupdateavailable"] = updater::is_update_available; + + updater_table["startupdatedownload"] = updater::start_update_download; + updater_table["isupdatedownloaddone"] = updater::is_update_download_done; + updater_table["getupdatedownloadstatus"] = updater::get_update_download_status; + updater_table["cancelupdate"] = updater::cancel_update; + updater_table["isrestartrequired"] = updater::is_restart_required; + + updater_table["getlasterror"] = updater::get_last_error; + updater_table["getcurrentfile"] = updater::get_current_file; + + state["updater"] = updater_table; + + auto discord_table = sol::table::create(state.lua_state()); + + discord_table["respond"] = discord::respond; + discord_table["getavatarmaterial"] = discord::get_avatar_material; + discord_table["reply"] = sol::table::create(state.lua_state()); + discord_table["reply"]["yes"] = DISCORD_REPLY_YES; + discord_table["reply"]["ignore"] = DISCORD_REPLY_IGNORE; + discord_table["reply"]["no"] = DISCORD_REPLY_NO; + + state["discord"] = discord_table; } } diff --git a/src/client/game/ui_scripting/types.cpp b/src/client/game/ui_scripting/types.cpp index 66e8d497..4323df0a 100644 --- a/src/client/game/ui_scripting/types.cpp +++ b/src/client/game/ui_scripting/types.cpp @@ -13,6 +13,16 @@ namespace ui_scripting { } + bool lightuserdata::operator==(const lightuserdata& other) + { + return this->ptr == other.ptr; + } + + bool lightuserdata::operator!=(const lightuserdata& other) + { + return this->ptr != other.ptr; + } + /*************************************************************** * Userdata **************************************************************/ @@ -66,6 +76,16 @@ namespace ui_scripting return *this; } + bool userdata::operator==(const userdata& other) + { + return this->ptr == other.ptr; + } + + bool userdata::operator!=(const userdata& other) + { + return this->ptr != other.ptr; + } + void userdata::add() { game::hks::HksObject value{}; @@ -158,6 +178,16 @@ namespace ui_scripting return *this; } + bool table::operator==(const table& other) + { + return this->ptr == other.ptr; + } + + bool table::operator!=(const table& other) + { + return this->ptr != other.ptr; + } + void table::add() { game::hks::HksObject value{}; @@ -247,6 +277,16 @@ namespace ui_scripting return *this; } + bool function::operator==(const function& other) + { + return this->ptr == other.ptr; + } + + bool function::operator!=(const function& other) + { + return this->ptr != other.ptr; + } + void function::add() { game::hks::HksObject value{}; diff --git a/src/client/game/ui_scripting/types.hpp b/src/client/game/ui_scripting/types.hpp index bc2f7216..0eb4aeda 100644 --- a/src/client/game/ui_scripting/types.hpp +++ b/src/client/game/ui_scripting/types.hpp @@ -8,6 +8,10 @@ namespace ui_scripting { public: lightuserdata(void*); + + bool operator==(const lightuserdata& other); + bool operator!=(const lightuserdata& other); + void* ptr; }; @@ -24,6 +28,9 @@ namespace ui_scripting userdata& operator=(const userdata& other); userdata& operator=(userdata&& other) noexcept; + bool operator==(const userdata& other); + bool operator!=(const userdata& other); + script_value get(const script_value& key) const; void set(const script_value& key, const script_value& value) const; @@ -50,6 +57,9 @@ namespace ui_scripting table& operator=(const table& other); table& operator=(table&& other) noexcept; + bool operator==(const table& other); + bool operator!=(const table& other); + script_value get(const script_value& key) const; void set(const script_value& key, const script_value& value) const; @@ -75,6 +85,9 @@ namespace ui_scripting function& operator=(const function& other); function& operator=(function&& other) noexcept; + bool operator==(const function& other); + bool operator!=(const function& other); + arguments call(const arguments& arguments) const; game::hks::cclosure* ptr;