From c52cfa73b4f7e150733afcbf47e96cb5ef51efb4 Mon Sep 17 00:00:00 2001 From: m Date: Tue, 15 Mar 2022 18:59:03 -0500 Subject: [PATCH 1/5] discord rich presence joining TODO: - add a "session ID" randomly generated string - improve the Joining game on DW initalized > main thread? - join requests with avatars in game (ui scripting) --- src/client/component/discord.cpp | 52 +++++++++++++++++++++++++++++--- src/client/component/party.cpp | 10 ++++++ src/client/component/party.hpp | 2 ++ src/client/main.cpp | 11 +++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 5925a64a..455c510f 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -9,6 +9,7 @@ #include "party.hpp" #include +#include #include @@ -37,6 +38,12 @@ namespace discord discord_presence.partyMax = 0; discord_presence.startTimestamp = 0; discord_presence.largeImageKey = game::environment::is_sp() ? "menu_singleplayer" : "menu_multiplayer"; + + // set to blank when in lobby + discord_presence.matchSecret = ""; + discord_presence.joinSecret = ""; + discord_presence.partyId = ""; + discord_presence.state = ""; } else { @@ -59,6 +66,22 @@ namespace discord { strcpy_s(clean_hostname, "Private Match"); max_clients = game::Dvar_FindVar("sv_maxclients")->current.integer; + discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE; + } + else + { + discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; + + // TODO: we need to make this a random string that represents the session ID + // const auto sessionId = party::get_state_challenge(); + discord_presence.partyId = "PLACEHOLDER"; + + const auto server_net_info = party::get_state_host(); + const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i", + server_net_info.ip[0], server_net_info.ip[1], server_net_info.ip[2], server_net_info.ip[3], + ntohs(server_net_info.port)); + + discord_presence.joinSecret = server_ip_port; } discord_presence.partySize = *reinterpret_cast(0x1429864C4); @@ -99,9 +122,9 @@ namespace discord handlers.ready = ready; handlers.errored = errored; handlers.disconnected = errored; - handlers.joinGame = nullptr; + handlers.joinGame = joinGame; handlers.spectateGame = nullptr; - handlers.joinRequest = nullptr; + handlers.joinRequest = joinRequest; Discord_Initialize("947125042930667530", &handlers, 1, nullptr); @@ -127,13 +150,13 @@ namespace discord private: bool initialized_ = false; - static void ready(const DiscordUser* /*request*/) + static void ready(const DiscordUser* request) { ZeroMemory(&discord_presence, sizeof(discord_presence)); discord_presence.instance = 1; - console::info("Discord: Ready\n"); + console::info("Discord: Ready on %s (%s)\n", request->username, request->userId); Discord_UpdatePresence(&discord_presence); } @@ -142,6 +165,27 @@ namespace discord { console::error("Discord: Error (%i): %s\n", error_code, message); } + + static void joinGame(const char* joinSecret) + { + console::info("Discord: Join game called with join secret: %s\n", joinSecret); + + scheduler::once([joinSecret]() + { + game::netadr_s target{}; + if (game::NET_StringToAdr(joinSecret, &target)) + { + console::info("Discord: Connecting to server: %s\n", joinSecret); + party::connect(target); + } + }, scheduler::pipeline::main); + } + + static void joinRequest(const DiscordUser* request) + { + console::info("Discord: joinRequest from %s (%s)\n", request->username, request->userId); + // Discord_Respond(request->userId, DISCORD_REPLY_YES); + } }; } diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index 38dd7277..5a88eb65 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -234,6 +234,16 @@ namespace party network::send(target, "getInfo", connect_state.challenge); } + game::netadr_s get_state_host() + { + return connect_state.host; + } + + std::string get_state_challenge() + { + return connect_state.challenge; + } + void start_map(const std::string& mapname) { if (game::Live_SyncOnlineDataFlags(0) > 32) diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index 0feb2e98..13990aea 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -9,6 +9,8 @@ namespace party void start_map(const std::string& mapname); void clear_sv_motd(); + game::netadr_s get_state_host(); + std::string get_state_challenge(); int server_client_count(); int get_client_num_by_name(const std::string& name); diff --git a/src/client/main.cpp b/src/client/main.cpp index 7329eb4c..500a09c4 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -146,6 +146,16 @@ void limit_parallel_dll_loading() RegCloseKey(key); } +// solution for other processes that may launch the mod +void apply_proper_directory() +{ + char module_path[MAX_PATH]; + GetModuleFileNameA(nullptr, module_path, MAX_PATH); + PathRemoveFileSpecA(module_path); + SetCurrentDirectoryA(module_path); + SetDllDirectoryA(module_path); +} + int main() { FARPROC entry_point; @@ -169,6 +179,7 @@ int main() try { + apply_proper_directory(); remove_crash_file(); if (!component_loader::post_start()) return 0; From 3a65d1d923426163d732d49f955397783b879324 Mon Sep 17 00:00:00 2001 From: m Date: Wed, 16 Mar 2022 05:43:54 -0700 Subject: [PATCH 2/5] Merge develop into rich-presence (#30) * Bump deps/asmjit from `6efd4d5` to `f1a399c` Bumps [deps/asmjit](https://github.com/asmjit/asmjit) from `6efd4d5` to `f1a399c`. - [Release notes](https://github.com/asmjit/asmjit/releases) - [Commits](https://github.com/asmjit/asmjit/compare/6efd4d563dee6832224295fa3bbf1647964246c4...f1a399c4fe74d1535a4190a2b8727c51045cc914) --- updated-dependencies: - dependency-name: deps/asmjit dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Custom material stupport * Remove this * Dont try to load 'white' material * removed git protocol submodules [skip ci] * readd submodules via http protocol Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Federico Cecchetto Co-authored-by: fed <58637860+fedddddd@users.noreply.github.com> --- .gitmodules | 15 +-- deps/asmjit | 2 +- src/client/component/materials.cpp | 172 +++++++++++++++++++++++++++++ src/client/component/materials.hpp | 6 + src/client/game/structs.hpp | 80 +++++++++++++- src/client/game/symbols.hpp | 5 +- 6 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 src/client/component/materials.cpp create mode 100644 src/client/component/materials.hpp diff --git a/.gitmodules b/.gitmodules index 7230acd8..e92c76fd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,19 +28,20 @@ [submodule "deps/stb"] path = deps/stb url = https://github.com/nothings/stb.git + branch = develop [submodule "deps/libtomcrypt"] path = deps/libtomcrypt - url = git://github.com/libtom/libtomcrypt.git - branch = develop -[submodule "deps/zlib"] - path = deps/zlib - url = git://github.com/madler/zlib.git + url = https://github.com/libtom/libtomcrypt.git branch = develop [submodule "deps/libtommath"] path = deps/libtommath - url = git://github.com/libtom/libtommath.git + url = https://github.com/libtom/libtommath.git branch = develop [submodule "deps/protobuf"] path = deps/protobuf - url = git://github.com/protocolbuffers/protobuf.git + url = https://github.com/protocolbuffers/protobuf.git branch = 3.17.x +[submodule "deps/zlib"] + path = deps/zlib + url = https://github.com/madler/zlib.git + branch = develop diff --git a/deps/asmjit b/deps/asmjit index 6efd4d56..f1a399c4 160000 --- a/deps/asmjit +++ b/deps/asmjit @@ -1 +1 @@ -Subproject commit 6efd4d563dee6832224295fa3bbf1647964246c4 +Subproject commit f1a399c4fe74d1535a4190a2b8727c51045cc914 diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp new file mode 100644 index 00000000..287238c0 --- /dev/null +++ b/src/client/component/materials.cpp @@ -0,0 +1,172 @@ +#include +#include "loader/component_loader.hpp" + +#include "materials.hpp" +#include "console.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace materials +{ + namespace + { + utils::hook::detour db_material_streaming_fail_hook; + utils::hook::detour material_register_handle_hook; + + struct material_data_t + { + std::unordered_map materials; + std::unordered_map images; + }; + + 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 = *reinterpret_cast(SELECT_VALUE(0x141F3D860, 0x14282C330)); + + 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->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; + } + + 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() + && !utils::io::read_file(utils::string::va("h1-mod/materials/%s.png", name.data()), &data) + && !utils::io::read_file(utils::string::va("data/materials/%s.png", name.data()), &data)) + { + 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; + } + + bool db_material_streaming_fail_stub(game::Material* material) + { + const auto found = material_data.access([material](material_data_t& data_) + { + if (data_.materials.find(material->name) != data_.materials.end()) + { + return true; + } + + return false; + }); + + if (found) + { + return false; + } + + return db_material_streaming_fail_hook.invoke(material); + } + } + + void add(const std::string& name, const std::string& data) + { + material_data.access([&](material_data_t& data_) + { + data_.images[name] = data; + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + material_register_handle_hook.create(game::Material_RegisterHandle, material_register_handle_stub); + db_material_streaming_fail_hook.create(SELECT_VALUE(0x1401D3180, 0x1402C6260), db_material_streaming_fail_stub); + } + }; +} + +REGISTER_COMPONENT(materials::component) diff --git a/src/client/component/materials.hpp b/src/client/component/materials.hpp new file mode 100644 index 00000000..ac58b511 --- /dev/null +++ b/src/client/component/materials.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace materials +{ + void add(const std::string& name, const std::string& data); +} diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 3437a026..bb7c3aec 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1101,11 +1101,90 @@ namespace game int forceTechType; }; + struct GfxImage; + + union MaterialTextureDefInfo + { + GfxImage* image; + void* water; + }; + + struct MaterialTextureDef + { + unsigned int nameHash; + char nameStart; + char nameEnd; + char samplerState; + char semantic; + MaterialTextureDefInfo u; + }; + + struct MaterialPass + { + void* vertexShader; + void* vertexDecl; + void* hullShader; + void* domainShader; + void* pixelShader; + char pixelOutputMask; + char perPrimArgCount; + char perObjArgCount; + char stableArgCount; + unsigned __int16 perPrimArgSize; + unsigned __int16 perObjArgSize; + unsigned __int16 stableArgSize; + char zone; + char perPrimConstantBuffer; + char perObjConstantBuffer; + char stableConstantBuffer; + unsigned int customBufferFlags; + char customSamplerFlags; + char precompiledIndex; + char stageConfig; + void* args; + }; + + struct MaterialTechnique + { + const char* name; + unsigned __int16 flags; + unsigned __int16 passCount; + MaterialPass passArray[1]; + }; + + struct MaterialTechniqueSet + { + const char* name; + unsigned __int16 flags; + char worldVertFormat; + char preDisplacementOnlyCount; + MaterialTechnique* techniques[309]; + }; + + struct GfxStateBits + { + unsigned int loadBits[3]; + char zone; + char depthStencilState[11]; + char blendState; + char rasterizerState; + }; + struct Material { const char* name; + char __pad0[0x118]; + char textureCount; + char __pad1[7]; + MaterialTechniqueSet* techniqueSet; + MaterialTextureDef* textureTable; + void* constantTable; + GfxStateBits* stateBitsTable; + char __pad2[0x108]; }; + static_assert(sizeof(Material) == 0x250); + struct Glyph { unsigned short letter; @@ -1252,7 +1331,6 @@ namespace game GfxImageLoadDef* loadDef; }; - struct GfxTexture { $3FA29451CE6F1FA138A5ABAB84BE9676 ___u0; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 257bbf15..9381150a 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -121,6 +121,9 @@ namespace game #define R_AddCmdDrawTextWithCursor(TXT, MC, F, UNK, X, Y, XS, YS, R, C, S, CP, CC) \ H1_AddBaseDrawTextCmd(TXT, MC, F, game::R_GetFontHeight(F), X, Y, XS, YS, R, C, S, CP, CC, game::R_DrawSomething(S)) + WEAK symbol Image_Setup{0x1404D7D50, 0x1405DCF90}; + WEAK symbol VM_Execute{0x140376360, 0x140444350}; @@ -138,7 +141,7 @@ namespace game WEAK symbol DB_EnumXAssets_Internal{0x1401C9C10, 0x1402BA830}; - WEAK symbol DB_GetXAssetName{0x14019A390, 0x14028BE50}; + WEAK symbol DB_GetXAssetName{0x14019A390, 0x14028BE50}; WEAK symbol DB_GetXAssetTypeSize{0x14019A3B0, 0x14028BE70}; WEAK symbol Date: Mon, 11 Apr 2022 11:05:21 +0200 Subject: [PATCH 3/5] Discord in game join requests --- data/ui_scripts/discord/__init__.lua | 272 +++++++++++++++++++ src/client/component/discord.cpp | 116 +++++++- src/client/component/discord.hpp | 7 + src/client/component/input.cpp | 9 +- src/client/component/materials.cpp | 8 + src/client/component/materials.hpp | 1 + src/client/component/scheduler.cpp | 11 + src/client/component/scheduler.hpp | 3 + src/client/component/ui_scripting.cpp | 20 +- src/client/component/ui_scripting.hpp | 2 + src/client/game/ui_scripting/lua/context.cpp | 74 +++-- src/client/game/ui_scripting/types.cpp | 40 +++ src/client/game/ui_scripting/types.hpp | 13 + 13 files changed, 514 insertions(+), 62 deletions(-) create mode 100644 data/ui_scripts/discord/__init__.lua create mode 100644 src/client/component/discord.hpp 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; From 2ae2831d06cd1d082e3bf422ba89a92b7f00288b Mon Sep 17 00:00:00 2001 From: Federico Cecchetto Date: Mon, 11 Apr 2022 11:42:42 +0200 Subject: [PATCH 4/5] Set partyId --- src/client/component/discord.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index 65fb74be..d75f9641 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -67,7 +67,7 @@ namespace discord discord_presence.details = utils::string::va("%s on %s", gametype, mapname); - char clean_hostname[0x100] = {0}; + 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(); @@ -81,18 +81,22 @@ namespace discord } else { - discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; - - // TODO: we need to make this a random string that represents the session ID - // const auto sessionId = party::get_state_challenge(); - discord_presence.partyId = "PLACEHOLDER"; - const auto server_net_info = party::get_state_host(); const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i", - server_net_info.ip[0], server_net_info.ip[1], server_net_info.ip[2], server_net_info.ip[3], - ntohs(server_net_info.port)); + server_net_info.ip[0], + server_net_info.ip[1], + server_net_info.ip[2], + server_net_info.ip[3], + ntohs(server_net_info.port) + ); + char party_id[0x80] = {0}; + const auto server_ip_port_hash = utils::cryptography::sha1::compute(server_ip_port, true); + strcpy_s(party_id, 0x80, server_ip_port_hash.data()); + + discord_presence.partyId = party_id; discord_presence.joinSecret = server_ip_port; + discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; } discord_presence.partySize = *reinterpret_cast(0x1429864C4); From 17bb717222a226dc8cbe91f103b4fa51d2ca05c4 Mon Sep 17 00:00:00 2001 From: Federico Cecchetto Date: Mon, 11 Apr 2022 12:12:26 +0200 Subject: [PATCH 5/5] Some fixes --- src/client/component/discord.cpp | 46 ++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/client/component/discord.cpp b/src/client/component/discord.cpp index d75f9641..3698e30f 100644 --- a/src/client/component/discord.cpp +++ b/src/client/component/discord.cpp @@ -8,6 +8,7 @@ #include "network.hpp" #include "party.hpp" #include "materials.hpp" +#include "discord.hpp" #include "ui_scripting.hpp" #include "game/ui_scripting/execution.hpp" @@ -24,8 +25,6 @@ #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 @@ -36,11 +35,11 @@ namespace discord { if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) { - discord_presence.details = game::environment::is_sp() ? "Singleplayer" : "Multiplayer"; + discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer"); discord_presence.state = "Main Menu"; - auto firingRangeDvar = game::Dvar_FindVar("virtualLobbyInFiringRange"); - if (firingRangeDvar && firingRangeDvar->current.enabled == 1) + const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange"); + if (in_firing_range && in_firing_range->current.enabled == 1) { discord_presence.state = "Firing Range"; } @@ -48,7 +47,7 @@ namespace discord discord_presence.partySize = 0; discord_presence.partyMax = 0; discord_presence.startTimestamp = 0; - discord_presence.largeImageKey = game::environment::is_sp() ? "menu_singleplayer" : "menu_multiplayer"; + discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer"); // set to blank when in lobby discord_presence.matchSecret = ""; @@ -58,16 +57,18 @@ namespace discord } else { + static char details[0x80] = {0}; const auto map = game::Dvar_FindVar("mapname")->current.string; - const auto mapname = game::UI_SafeTranslateString(utils::string::va("PRESENCE_%s%s", (game::environment::is_sp() ? "SP_" : ""), map)); + const auto mapname = game::UI_SafeTranslateString( + utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map)); if (game::environment::is_mp()) { - const auto gametype = game::UI_GetGameTypeDisplayName(game::Dvar_FindVar("g_gametype")->current.string); + const auto gametype = game::UI_GetGameTypeDisplayName( + game::Dvar_FindVar("g_gametype")->current.string); + strcpy_s(details, 0x80, utils::string::va("%s on %s", gametype, mapname)); - discord_presence.details = utils::string::va("%s on %s", gametype, mapname); - - char clean_hostname[0x80] = {0}; + 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(); @@ -83,19 +84,22 @@ namespace discord { const auto server_net_info = party::get_state_host(); const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i", - server_net_info.ip[0], - server_net_info.ip[1], - server_net_info.ip[2], - server_net_info.ip[3], - ntohs(server_net_info.port) + 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)) ); - char party_id[0x80] = {0}; - const auto server_ip_port_hash = utils::cryptography::sha1::compute(server_ip_port, true); + 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 = server_ip_port; + discord_presence.joinSecret = join_secret; discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC; } @@ -108,9 +112,11 @@ namespace discord { discord_presence.state = ""; discord_presence.largeImageKey = map; - discord_presence.details = mapname; + strcpy_s(details, 0x80, mapname); } + discord_presence.details = details; + if (!discord_presence.startTimestamp) { discord_presence.startTimestamp = std::chrono::duration_cast(