diff --git a/.gitmodules b/.gitmodules index e92c76fd..ef590325 100644 --- a/.gitmodules +++ b/.gitmodules @@ -45,3 +45,6 @@ path = deps/zlib url = https://github.com/madler/zlib.git branch = develop +[submodule "deps/imgui"] + path = deps/imgui + url = https://github.com/fedddddd/imgui.git diff --git a/deps/imgui b/deps/imgui new file mode 160000 index 00000000..22e9093d --- /dev/null +++ b/deps/imgui @@ -0,0 +1 @@ +Subproject commit 22e9093da37f0443f008b947caa6221724e760d6 diff --git a/deps/premake/imgui.lua b/deps/premake/imgui.lua new file mode 100644 index 00000000..2947fe14 --- /dev/null +++ b/deps/premake/imgui.lua @@ -0,0 +1,31 @@ +imgui = { + source = path.join(dependencies.basePath, "imgui") +} + +function imgui.import() + links {"imgui"} + imgui.includes() +end + +function imgui.includes() + includedirs {imgui.source} +end + +function imgui.project() + project "imgui" + language "C++" + + imgui.includes() + + files {path.join(imgui.source, "*.cpp"), path.join(imgui.source, "*.hpp"), path.join(imgui.source, "*.c"), + path.join(imgui.source, "*.h"), path.join(imgui.source, "backends/imgui_impl_dx11.cpp"), + path.join(imgui.source, "backends/imgui_impl_dx11.h"), + path.join(imgui.source, "backends/imgui_impl_win32.cpp"), + path.join(imgui.source, "backends/imgui_impl_win32.h"), path.join(imgui.source, "misc/cpp/imgui_stdlib.cpp"), + path.join(imgui.source, "misc/cpp/imgui_stdlib.h")} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, imgui) diff --git a/src/client/component/gui.cpp b/src/client/component/gui.cpp new file mode 100644 index 00000000..36d31512 --- /dev/null +++ b/src/client/component/gui.cpp @@ -0,0 +1,334 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "scheduler.hpp" +#include "gui.hpp" + +#include +#include +#include + +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +namespace gui +{ + std::unordered_map enabled_menus; + + namespace + { + struct frame_callback + { + std::function callback; + bool always; + }; + + struct event + { + HWND hWnd; + UINT msg; + WPARAM wParam; + LPARAM lParam; + }; + + utils::concurrency::container> on_frame_callbacks; + utils::concurrency::container> notifications; + utils::concurrency::container> event_queue; + + ID3D11Device* device; + ID3D11DeviceContext* device_context; + bool initialized = false; + bool toggled = false; + + void initialize_gui_context() + { + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + + ImGui_ImplWin32_Init(*game::hWnd); + ImGui_ImplDX11_Init(device, device_context); + + initialized = true; + } + + void run_event_queue() + { + event_queue.access([](std::vector& queue) + { + for (const auto& event : queue) + { + ImGui_ImplWin32_WndProcHandler(event.hWnd, event.msg, event.wParam, event.lParam); + } + + queue.clear(); + }); + } + + void new_gui_frame() + { + ImGui::GetIO().MouseDrawCursor = toggled; + if (toggled) + { + *game::keyCatchers |= 0x10; + } + else + { + *game::keyCatchers &= ~0x10; + } + + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + run_event_queue(); + + ImGui::NewFrame(); + } + + void end_gui_frame() + { + ImGui::EndFrame(); + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + } + + void toggle_menu(const std::string& name) + { + enabled_menus[name] = !enabled_menus[name]; + } + + void show_notifications() + { + static const auto window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoMove; + + notifications.access([](std::deque& notifications_) + { + auto index = 0; + for (auto i = notifications_.begin(); i != notifications_.end();) + { + const auto now = std::chrono::high_resolution_clock::now(); + if (now - i->creation_time >= i->duration) + { + i = notifications_.erase(i); + continue; + } + + const auto title = utils::string::truncate(i->title, 34, "..."); + const auto text = utils::string::truncate(i->text, 34, "..."); + + ImGui::SetNextWindowSizeConstraints(ImVec2(250, 50), ImVec2(250, 50)); + ImGui::SetNextWindowBgAlpha(0.6f); + ImGui::Begin(utils::string::va("Notification #%i", index), nullptr, window_flags); + + ImGui::SetWindowPos(ImVec2(10, 30.f + static_cast(index) * 60.f)); + ImGui::SetWindowSize(ImVec2(250, 0)); + ImGui::Text(title.data()); + ImGui::Text(text.data()); + + ImGui::End(); + + ++i; + ++index; + } + }); + } + + void menu_checkbox(const std::string& name, const std::string& menu) + { + ImGui::Checkbox(name.data(), &enabled_menus[menu]); + } + + void run_frame_callbacks() + { + on_frame_callbacks.access([](std::vector& callbacks) + { + for (const auto& callback : callbacks) + { + if (callback.always || toggled) + { + callback.callback(); + } + } + }); + } + + void draw_main_menu_bar() + { + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("Windows")) + { + menu_checkbox("Console", "console"); + menu_checkbox("Script console", "script_console"); + menu_checkbox("Debug", "debug"); + + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } + } + + void gui_on_frame() + { + if (!initialized) + { + printf("initialzed gui context\n"); + initialize_gui_context(); + } + else + { + new_gui_frame(); + run_frame_callbacks(); + end_gui_frame(); + } + } + + HRESULT d3d11_create_device_stub(IDXGIAdapter* pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, + UINT Flags, const D3D_FEATURE_LEVEL* pFeatureLevels, UINT FeatureLevels, UINT SDKVersion, + ID3D11Device** ppDevice, D3D_FEATURE_LEVEL* pFeatureLevel, ID3D11DeviceContext** ppImmediateContext) + { + const auto result = D3D11CreateDevice(pAdapter, DriverType, Software, Flags, pFeatureLevels, + FeatureLevels, SDKVersion, ppDevice, pFeatureLevel, ppImmediateContext); + + if (ppDevice != nullptr && ppImmediateContext != nullptr) + { + device = *ppDevice; + device_context = *ppImmediateContext; + } + + return result; + } + + void dxgi_swap_chain_present_stub(utils::hook::assembler& a) + { + a.pushad64(); + a.call_aligned(gui_on_frame); + a.popad64(); + + a.mov(r8d, esi); + a.mov(edx, r15d); + a.mov(rcx, rdi); + a.call_aligned(rbx); + a.mov(dword_ptr(rsp, 0x20), eax); + + a.jmp(0x6CB185_b); + } + + utils::hook::detour wnd_proc_hook; + LRESULT wnd_proc_stub(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) + { + if (wParam != VK_ESCAPE && toggled) + { + event_queue.access([hWnd, msg, wParam, lParam](std::vector& queue) + { + queue.push_back({hWnd, msg, wParam, lParam}); + }); + } + + return wnd_proc_hook.invoke(hWnd, msg, wParam, lParam); + } + } + + bool gui_key_event(const int local_client_num, const int key, const int down) + { + printf("gui key event\n"); + + if (key == game::K_INS && down) + { + toggled = !toggled; + return false; + } + + if (key == game::K_ESCAPE && down && toggled) + { + toggled = false; + return false; + } + + return !toggled; + } + + bool gui_char_event(const int local_client_num, const int key) + { + return !toggled; + } + + bool gui_mouse_event(const int local_client_num, int x, int y) + { + return !toggled; + } + + void on_frame(const std::function& callback, bool always) + { + on_frame_callbacks.access([always, callback](std::vector& callbacks) + { + callbacks.push_back({callback, always}); + }); + } + + bool is_menu_open(const std::string& name) + { + return enabled_menus[name]; + } + + void notification(const std::string& title, const std::string& text, const std::chrono::milliseconds duration) + { + notification_t notification{}; + notification.title = title; + notification.text = text; + notification.duration = duration; + notification.creation_time = std::chrono::high_resolution_clock::now(); + + notifications.access([notification](std::deque& notifications_) + { + notifications_.push_front(notification); + }); + } + + void copy_to_clipboard(const std::string& text) + { + utils::string::set_clipboard_data(text); + gui::notification("Text copied to clipboard", utils::string::va("\"%s\"", text.data())); + } + + class component final : public component_interface + { + public: + void* load_import(const std::string& library, const std::string& function) override + { + if (function == "D3D11CreateDevice") + { + return d3d11_create_device_stub; + } + + return nullptr; + } + + void post_unpack() override + { + utils::hook::jump(0x6CB176_b, utils::hook::assemble(dxgi_swap_chain_present_stub), true); + + wnd_proc_hook.create(0x5162D0_b, wnd_proc_stub); + + on_frame([]() + { + show_notifications(); + draw_main_menu_bar(); + }); + } + + void pre_destroy() override + { + if (initialized) + { + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + } + } + }; +} + +REGISTER_COMPONENT(gui::component) diff --git a/src/client/component/gui.hpp b/src/client/component/gui.hpp new file mode 100644 index 00000000..c1e272a6 --- /dev/null +++ b/src/client/component/gui.hpp @@ -0,0 +1,23 @@ +#pragma once + +namespace gui +{ + struct notification_t + { + std::string title; + std::string text; + std::chrono::milliseconds duration{}; + std::chrono::high_resolution_clock::time_point creation_time{}; + }; + + extern std::unordered_map enabled_menus; + + bool gui_key_event(const int local_client_num, const int key, const int down); + bool gui_char_event(const int local_client_num, const int key); + bool gui_mouse_event(const int local_client_num, int x, int y); + + void on_frame(const std::function& callback, bool always = false); + bool is_menu_open(const std::string& name); + void notification(const std::string& title, const std::string& text, const std::chrono::milliseconds duration = 3s); + void copy_to_clipboard(const std::string& text); +} diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index 9e92188f..269a2c6c 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 "gui.hpp" #include "game/ui_scripting/execution.hpp" #include @@ -36,12 +37,15 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { + const auto key_to_string = game::Key_KeynumToString(key, 0, 1); + printf("key to string: %s\n", key_to_string); + if (ui_scripting::lui_running()) { ui_scripting::notify(down ? "keydown" : "keyup", { {"keynum", key}, - {"key", game::Key_KeynumToString(key, 0, 1)}, + {"key", key_to_string}, }); } @@ -50,6 +54,11 @@ namespace input return; } + if (!gui::gui_key_event(local_client_num, key, down)) + { + return; + } + cl_key_event_hook.invoke(local_client_num, key, down); } } diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 6f83e41e..37e285ed 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -228,6 +228,8 @@ namespace game * Variables **************************************************************/ + WEAK symbol hWnd{0x0, 0xC9DD2E0}; + WEAK symbol sv_cmd_args{0xB48FF90, 0x2ED1EB0}; WEAK symbol g_script_error_level{0xC3FD358, 0xB7AC1A4}; diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index e43275c3..ceae7ed9 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -89,6 +89,13 @@ #include #include +#include +#include +#include +#include +#include +#include + #include #include @@ -100,6 +107,7 @@ #pragma comment(lib, "urlmon.lib" ) #pragma comment(lib, "iphlpapi.lib") #pragma comment(lib, "Crypt32.lib") +#pragma comment(lib, "d3d11.lib") #include "resource.hpp" diff --git a/src/common/utils/string.cpp b/src/common/utils/string.cpp index 42486cf3..f4cb3748 100644 --- a/src/common/utils/string.cpp +++ b/src/common/utils/string.cpp @@ -105,6 +105,22 @@ namespace utils::string return {}; } + void set_clipboard_data(const std::string& text) + { + const auto len = text.size() + 1; + const auto mem = GlobalAlloc(GMEM_MOVEABLE, len); + + memcpy(GlobalLock(mem), text.data(), len); + GlobalUnlock(mem); + + if (OpenClipboard(nullptr)) + { + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + } + } + void strip(const char* in, char* out, int max) { if (!in || !out) return; @@ -177,6 +193,13 @@ namespace utils::string return str; } + std::string truncate(const std::string& text, const size_t length, const std::string& end) + { + return text.size() <= length + ? text + : text.substr(0, length - end.size()) + end; + } + bool match_compare(const std::string& input, const std::string& text, const bool exact) { if (exact && text == input) return true; diff --git a/src/common/utils/string.hpp b/src/common/utils/string.hpp index 322ce9ce..9931679d 100644 --- a/src/common/utils/string.hpp +++ b/src/common/utils/string.hpp @@ -90,6 +90,7 @@ namespace utils::string std::string dump_hex(const std::string& data, const std::string& separator = " "); std::string get_clipboard_data(); + void set_clipboard_data(const std::string& text); void strip(const char* in, char* out, int max); @@ -98,5 +99,7 @@ namespace utils::string std::string replace(std::string str, const std::string& from, const std::string& to); + std::string truncate(const std::string& text, const size_t length, const std::string& end); + bool match_compare(const std::string& input, const std::string& text, const bool exact); }