diff --git a/premake5.lua b/premake5.lua index a2736dc1..eb07c0f6 100644 --- a/premake5.lua +++ b/premake5.lua @@ -263,7 +263,7 @@ flags {"FatalCompileWarnings"} configuration "Debug" optimize "Debug" - +buildoptions {"/bigobj"} defines {"DEBUG", "_DEBUG"} configuration {} diff --git a/src/client/component/images.cpp b/src/client/component/images.cpp index 531a3e44..e82c532c 100644 --- a/src/client/component/images.cpp +++ b/src/client/component/images.cpp @@ -34,7 +34,7 @@ namespace images return {}; } - return { std::move(data) }; + return {std::move(data)}; } std::optional load_raw_image_from_file(game::GfxImage* image) diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index 11cd8d6c..175a5431 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 "game/ui_scripting/lua/engine.hpp" #include @@ -11,11 +12,20 @@ namespace input { namespace { + struct point + { + short x; + short y; + }; + utils::hook::detour cl_char_event_hook; utils::hook::detour cl_key_event_hook; + utils::hook::detour cl_mouse_move_hook; void cl_char_event_stub(const int local_client_num, const int key) { + ui_scripting::lua::engine::ui_event("char", {key}); + if (!game_console::console_char_event(local_client_num, key)) { return; @@ -26,6 +36,8 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { + ui_scripting::lua::engine::ui_event("key", {key, down}); + if (!game_console::console_key_event(local_client_num, key, down)) { return; @@ -33,6 +45,13 @@ namespace input cl_key_event_hook.invoke(local_client_num, key, down); } + + void cl_mouse_move_stub(const int local_client_num, int x, int y) + { + ui_scripting::lua::engine::ui_event("mousemove", {x, y}); + + cl_mouse_move_hook.invoke(local_client_num, x, y); + } } class component final : public component_interface @@ -42,6 +61,7 @@ namespace input { cl_char_event_hook.create(game::base_address + 0x3D27B0, cl_char_event_stub); cl_key_event_hook.create(game::base_address + 0x3D2AE0, cl_key_event_stub); + cl_mouse_move_hook.create(game::base_address + 0x3296F0, cl_mouse_move_stub); } }; } diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp new file mode 100644 index 00000000..362b5501 --- /dev/null +++ b/src/client/component/ui_scripting.cpp @@ -0,0 +1,63 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "chat.hpp" +#include "scheduler.hpp" +#include "command.hpp" +#include "ui_scripting.hpp" + +#include "game/ui_scripting/lua/engine.hpp" + +#include + +namespace ui_scripting +{ + class component final : public component_interface + { + public: + + void post_unpack() override + { + scheduler::once([]() + { + ui_scripting::lua::engine::start(); + }, scheduler::pipeline::renderer); + + scheduler::loop([]() + { + ui_scripting::lua::engine::run_frame(); + }, scheduler::pipeline::renderer); + + command::add("reloadmenus", []() + { + scheduler::once([]() + { + ui_scripting::lua::engine::start(); + }, scheduler::pipeline::renderer); + }); + + command::add("openluamenu", [](const command::params& params) + { + const std::string name = params.get(1); + scheduler::once([name]() + { + ui_scripting::lua::engine::open_menu(name); + }, scheduler::pipeline::renderer); + }); + + command::add("closeluamenu", [](const command::params& params) + { + const std::string name = params.get(1); + scheduler::once([name]() + { + ui_scripting::lua::engine::close_menu(name); + }, scheduler::pipeline::renderer); + }); + } + }; +} + +REGISTER_COMPONENT(ui_scripting::component) diff --git a/src/client/component/ui_scripting.hpp b/src/client/component/ui_scripting.hpp new file mode 100644 index 00000000..21cd1ad8 --- /dev/null +++ b/src/client/component/ui_scripting.hpp @@ -0,0 +1,6 @@ +#pragma once +#include "game/ui_scripting/menu.hpp" + +namespace ui_scripting +{ +} diff --git a/src/client/game/scripting/lua/context.cpp b/src/client/game/scripting/lua/context.cpp index f7b0982d..09e95902 100644 --- a/src/client/game/scripting/lua/context.cpp +++ b/src/client/game/scripting/lua/context.cpp @@ -119,12 +119,9 @@ namespace scripting::lua vector_type[sol::meta_function::equal_to] = [](const vector& a, const vector& b) { - const auto normal_a = normalize_vector(a); - const auto normal_b = normalize_vector(b); - - return normal_a.get_x() == normal_b.get_x() && - normal_a.get_y() == normal_b.get_y() && - normal_a.get_z() == normal_b.get_z(); + return a.get_x() == b.get_x() && + a.get_y() == b.get_y() && + a.get_z() == b.get_z(); }; vector_type[sol::meta_function::length] = [](const vector& a) diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 3c647ddb..ab366054 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -72,6 +72,22 @@ namespace game const char* name; }; + struct point + { + float x; + float y; + float f2; + float f3; + }; + + struct rectangle + { + point p0; + point p1; + point p2; + point p3; + }; + struct Glyph { unsigned short letter; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 7a785a72..b0d0cd4f 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -43,6 +43,7 @@ namespace game unsigned int flags)> Dvar_RegisterVec4{0x6185F0}; WEAK symbol Dvar_ValueToString{0x61B8F0}; WEAK symbol Dvar_SetCommand{0x61A5C0}; + WEAK symbol Dvar_SetFromStringFromSource{0x61A910}; WEAK symbol generateHashValue{0x343D20}; @@ -78,8 +79,11 @@ namespace game WEAK symbol R_AddCmdDrawStretchPic{0x3C9710}; + WEAK symbol R_AddCmdDrawStretchPicRotateXY{0x3C99B0}; WEAK symbol R_AddCmdDrawText{0x76C660}; + WEAK symbol R_DrawRectangle{0x76A280}; WEAK symbol R_AddCmdDrawTextWithCursor{0x76CAF0}; WEAK symbol R_RegisterFont{0x746FE0}; @@ -99,6 +103,7 @@ namespace game WEAK symbol Sys_IsDatabaseReady2{0x5A9FE0}; WEAK symbol UI_SafeTranslateString{0x5A2930}; + WEAK symbol UI_PlayLocalSoundAlias{0x606080}; WEAK symbol longjmp{0x89EED0}; WEAK symbol _setjmp{0x8EC2E0}; diff --git a/src/client/game/ui_scripting/element.cpp b/src/client/game/ui_scripting/element.cpp new file mode 100644 index 00000000..68e11d7c --- /dev/null +++ b/src/client/game/ui_scripting/element.cpp @@ -0,0 +1,342 @@ +#include +#include "element.hpp" + +#include + +#define fps_font game::R_RegisterFont("fonts/fira_mono_regular.ttf", 25) + +namespace ui_scripting +{ + namespace + { + uint64_t next_id; + float screen_max[2]; + + struct point + { + float x; + float y; + float f2; + float f3; + }; + + struct rectangle + { + point p0; + point p1; + point p2; + point p3; + }; + + std::unordered_map font_map = + { + {"bank", "fonts/bank.ttf"}, + {"fira_mono_bold", "fonts/fira_mono_bold.ttf"}, + {"fira_mono_regular", "fonts/fira_mono_regular.ttf"}, + {"defaultbold", "fonts/defaultbold.otf"}, + {"default", "fonts/default.otf"}, + }; + + std::unordered_map alignment_map = + { + {"left", alignment::start}, + {"top", alignment::start}, + {"center", alignment::middle}, + {"right", alignment::end}, + {"bottom", alignment::end}, + }; + + float get_align_value(alignment align, float text_width, float w) + { + switch (align) + { + case (alignment::start): + return 0.f; + case (alignment::middle): + return (w / 2.f) - (text_width / 2.f); + case (alignment::end): + return w - text_width; + default: + return 0.f; + } + } + + void draw_image(float x, float y, float w, float h, float* transform, float* color, game::Material* material) + { + game::rectangle rect; + + rect.p0.x = x; + rect.p0.y = y; + rect.p0.f2 = 0.f; + rect.p0.f3 = 1.f; + + rect.p1.x = x + w; + rect.p1.y = y; + rect.p1.f2 = 0.f; + rect.p1.f3 = 1.f; + + rect.p2.x = x + w; + rect.p2.y = y + h; + rect.p2.f2 = 0.f; + rect.p2.f3 = 1.f; + + rect.p3.x = x; + rect.p3.y = y + h; + rect.p3.f2 = 0.f; + rect.p3.f3 = 1.f; + + game::R_DrawRectangle(&rect, transform[0], transform[1], transform[2], transform[3], color, material); + } + + void check_resize() + { + screen_max[0] = game::ScrPlace_GetViewPlacement()->realViewportSize[0]; + screen_max[1] = game::ScrPlace_GetViewPlacement()->realViewportSize[1]; + } + + float relative(float value) + { + return (value / 1920.f) * screen_max[0]; + } + + int relative(int value) + { + return (int)(((float)value / 1920.f) * screen_max[0]); + } + } + + element::element() + : id(next_id++) + { + } + + void element::set_horzalign(const std::string& value) + { + const auto lower = utils::string::to_lower(value); + if (alignment_map.find(lower) == alignment_map.end()) + { + this->horzalign = alignment::start; + return; + } + + const auto align = alignment_map[lower]; + this->horzalign = align; + } + + void element::set_vertalign(const std::string& value) + { + const auto lower = utils::string::to_lower(value); + if (alignment_map.find(lower) == alignment_map.end()) + { + this->vertalign = alignment::start; + return; + } + + const auto align = alignment_map[lower]; + this->vertalign = align; + } + + void element::set_text(const std::string& _text) + { + this->text = _text; + } + + void element::set_font(const std::string& _font, const int _fontsize) + { + this->fontsize = _fontsize; + const auto lowercase = utils::string::to_lower(_font); + + if (font_map.find(lowercase) == font_map.end()) + { + this->font = "default"; + } + else + { + this->font = lowercase; + } + } + + void element::set_font(const std::string& _font) + { + const auto lowercase = utils::string::to_lower(_font); + + if (font_map.find(lowercase) == font_map.end()) + { + this->font = "default"; + } + else + { + this->font = lowercase; + } + } + + void element::set_text_offset(float _x, float _y) + { + this->text_offset[0] = _x; + this->text_offset[1] = _y; + } + + void element::set_background_color(float r, float g, float b, float a) + { + this->background_color[0] = r; + this->background_color[1] = g; + this->background_color[2] = b; + this->background_color[3] = a; + } + + void element::set_color(float r, float g, float b, float a) + { + this->color[0] = r; + this->color[1] = g; + this->color[2] = b; + this->color[3] = a; + } + + void element::set_border_material(const std::string& _material) + { + this->border_material = _material; + } + + void element::set_border_color(float r, float g, float b, float a) + { + this->border_color[0] = r; + this->border_color[1] = g; + this->border_color[2] = b; + this->border_color[3] = a; + } + + void element::set_border_width(float top) + { + this->border_width[0] = top; + this->border_width[1] = top; + this->border_width[2] = top; + this->border_width[3] = top; + } + + void element::set_border_width(float top, float right) + { + this->border_width[0] = top; + this->border_width[1] = right; + this->border_width[2] = top; + this->border_width[3] = right; + } + + void element::set_border_width(float top, float right, float bottom) + { + this->border_width[0] = top; + this->border_width[1] = right; + this->border_width[2] = bottom; + this->border_width[3] = bottom; + } + + void element::set_border_width(float top, float right, float bottom, float left) + { + this->border_width[0] = top; + this->border_width[1] = right; + this->border_width[2] = bottom; + this->border_width[3] = left; + } + + void element::set_slice(float left_percent, float top_percent, float right_percent, float bottom_percent) + { + this->slice[0] = left_percent; + this->slice[1] = top_percent; + this->slice[2] = right_percent; + this->slice[3] = bottom_percent; + } + + void element::set_material(const std::string& _material) + { + this->material = _material; + } + + void element::set_rect(const float _x, const float _y, const float _w, const float _h) + { + this->x = _x; + this->y = _y; + this->w = _w; + this->h = _h; + } + + void element::render() const + { + check_resize(); + + if (this->background_color[3] > 0) + { + const auto background_material = game::Material_RegisterHandle(this->material.data()); + + draw_image( + relative(this->x) + relative(this->border_width[3]), + relative(this->y) + relative(this->border_width[0]), + relative(this->w), + relative(this->h), + (float*)this->slice, + (float*)this->background_color, + background_material + ); + } + + if (this->border_color[3] > 0) + { + const auto _border_material = game::Material_RegisterHandle(this->border_material.data()); + + draw_image( + relative(this->x), + relative(this->y), + relative(this->w) + relative(this->border_width[1]) + relative(this->border_width[3]), + relative(this->border_width[0]), + (float*)this->slice, + (float*)this->border_color, + _border_material + ); + + draw_image( + relative(this->x) + relative(this->border_width[3]) + relative(this->w), + relative(this->y) + relative(this->border_width[0]), + relative(this->border_width[1]), + relative(this->h), + (float*)this->slice, + (float*)this->border_color, + _border_material + ); + + draw_image( + relative(this->x), + relative(this->y) + relative(this->h) + relative(this->border_width[0]), + relative(this->w) + relative(this->border_width[1]) + relative(this->border_width[3]), + relative(this->border_width[2]), + (float*)this->slice, + (float*)this->border_color, + _border_material + ); + + draw_image( + relative(this->x), + relative(this->y) + relative(this->border_width[0]), + relative(this->border_width[3]), + relative(this->h), + (float*)this->slice, + (float*)this->border_color, + _border_material + ); + } + + if (!this->text.empty()) + { + const auto fontname = font_map[this->font]; + const auto _font = game::R_RegisterFont(fontname.data(), relative(this->fontsize)); + const auto text_width = game::R_TextWidth(this->text.data(), 0x7FFFFFFF, _font); + + auto _horzalign = get_align_value(this->horzalign, (float)text_width, relative(this->w)); + auto _vertalign = get_align_value(this->vertalign, (float)relative(this->fontsize), relative(this->h)); + + game::R_AddCmdDrawText(this->text.data(), 0x7FFFFFFF, _font, + relative(this->x) + relative(this->text_offset[0]) + _horzalign + relative(this->border_width[3]), + relative(this->y) + relative(this->text_offset[1]) + _vertalign + relative(this->fontsize) + relative(this->border_width[0]), + 1.0f, 1.0f, 0.0f, + (float*)this->color, 0 + ); + } + } +} diff --git a/src/client/game/ui_scripting/element.hpp b/src/client/game/ui_scripting/element.hpp new file mode 100644 index 00000000..1820e432 --- /dev/null +++ b/src/client/game/ui_scripting/element.hpp @@ -0,0 +1,67 @@ +#pragma once +#include "game/game.hpp" + +namespace ui_scripting +{ + enum alignment + { + start, + middle, + end, + }; + + class element final + { + public: + element(); + + void set_horzalign(const std::string& value); + void set_vertalign(const std::string& value); + + void set_text(const std::string& text); + void set_font(const std::string& _font); + void set_font(const std::string& _font, const int _fontsize); + void set_color(float r, float g, float b, float a); + void set_text_offset(float x, float y); + + void set_background_color(float r, float g, float b, float a); + void set_material(const std::string& material); + + void set_border_material(const std::string& material); + void set_border_color(float r, float g, float b, float a); + void set_border_width(float top); + void set_border_width(float top, float right); + void set_border_width(float top, float right, float bottom); + void set_border_width(float top, float right, float bottom, float left); + + void set_slice(float left_percent, float top_percent, float right_percent, float bottom_percent); + + void set_rect(const float _x, const float _y, const float _w, const float _h); + + uint64_t id; + + void render() const; + + float x = 0.f; + float y = 0.f; + float w = 0.f; + float h = 0.f; + + int fontsize = 20; + + float text_offset[2] = {0.f, 0.f}; + float color[4] = {1.f, 1.f, 1.f, 1.f}; + float background_color[4] = {0.f, 0.f, 0.f, 0.f}; + float border_color[4] = {0.f, 0.f, 0.f, 0.f}; + float border_width[4] = {0.f, 0.f, 0.f, 0.f}; + float slice[4] = {0.f, 0.f, 1.f, 1.f}; + + alignment horzalign = alignment::start; + alignment vertalign = alignment::start; + + std::string font = "fonts/fira_mono_regular.ttf"; + std::string material = "white"; + std::string border_material = "white"; + std::string text{}; + }; +} diff --git a/src/client/game/ui_scripting/lua/context.cpp b/src/client/game/ui_scripting/lua/context.cpp new file mode 100644 index 00000000..58f513e8 --- /dev/null +++ b/src/client/game/ui_scripting/lua/context.cpp @@ -0,0 +1,879 @@ +#include +#include "context.hpp" +#include "error.hpp" +#include "../../scripting/execution.hpp" + +#include "../../../component/ui_scripting.hpp" + +#include "component/game_console.hpp" +#include "component/scheduler.hpp" + +#include +#include + +namespace ui_scripting::lua +{ + std::unordered_map menus; + std::vector elements; + element ui_element; + int mouse[2]; + + namespace + { + const auto animation_script = utils::nt::load_resource(LUA_ANIMATION_SCRIPT); + + scripting::script_value convert(const sol::lua_value& value) + { + if (value.is()) + { + return {value.as()}; + } + + if (value.is()) + { + return {value.as()}; + } + + if (value.is()) + { + return {value.as()}; + } + + if (value.is()) + { + return {value.as()}; + } + + if (value.is()) + { + return {value.as()}; + } + if (value.is()) + { + return {value.as()}; + } + + if (value.is()) + { + return {value.as()}; + } + + return {}; + } + + bool valid_dvar_name(const std::string& name) + { + for (const auto c : name) + { + if (!isalnum(c)) + { + return false; + } + } + + return true; + } + + void setup_types(sol::state& state, event_handler& handler, scheduler& scheduler) + { + auto vector_type = state.new_usertype("vector", sol::constructors()); + vector_type["x"] = sol::property(&scripting::vector::get_x, &scripting::vector::set_x); + vector_type["y"] = sol::property(&scripting::vector::get_y, &scripting::vector::set_y); + vector_type["z"] = sol::property(&scripting::vector::get_z, &scripting::vector::set_z); + + vector_type["r"] = sol::property(&scripting::vector::get_x, &scripting::vector::set_x); + vector_type["g"] = sol::property(&scripting::vector::get_y, &scripting::vector::set_y); + vector_type["b"] = sol::property(&scripting::vector::get_z, &scripting::vector::set_z); + + vector_type[sol::meta_function::addition] = sol::overload( + [](const scripting::vector& a, const scripting::vector& b) + { + return scripting::vector( + a.get_x() + b.get_x(), + a.get_y() + b.get_y(), + a.get_z() + b.get_z() + ); + }, + [](const scripting::vector& a, const int value) + { + return scripting::vector( + a.get_x() + value, + a.get_y() + value, + a.get_z() + value + ); + } + ); + + vector_type[sol::meta_function::subtraction] = sol::overload( + [](const scripting::vector& a, const scripting::vector& b) + { + return scripting::vector( + a.get_x() - b.get_x(), + a.get_y() - b.get_y(), + a.get_z() - b.get_z() + ); + }, + [](const scripting::vector& a, const int value) + { + return scripting::vector( + a.get_x() - value, + a.get_y() - value, + a.get_z() - value + ); + } + ); + + vector_type[sol::meta_function::multiplication] = sol::overload( + [](const scripting::vector& a, const scripting::vector& b) + { + return scripting::vector( + a.get_x() * b.get_x(), + a.get_y() * b.get_y(), + a.get_z() * b.get_z() + ); + }, + [](const scripting::vector& a, const int value) + { + return scripting::vector( + a.get_x() * value, + a.get_y() * value, + a.get_z() * value + ); + } + ); + + vector_type[sol::meta_function::division] = sol::overload( + [](const scripting::vector& a, const scripting::vector& b) + { + return scripting::vector( + a.get_x() / b.get_x(), + a.get_y() / b.get_y(), + a.get_z() / b.get_z() + ); + }, + [](const scripting::vector& a, const int value) + { + return scripting::vector( + a.get_x() / value, + a.get_y() / value, + a.get_z() / value + ); + } + ); + + vector_type[sol::meta_function::equal_to] = [](const scripting::vector& a, const scripting::vector& b) + { + return a.get_x() == b.get_x() && + a.get_y() == b.get_y() && + a.get_z() == b.get_z(); + }; + + vector_type[sol::meta_function::length] = [](const scripting::vector& a) + { + return sqrt((a.get_x() * a.get_x()) + (a.get_y() * a.get_y()) + (a.get_z() * a.get_z())); + }; + + vector_type[sol::meta_function::to_string] = [](const scripting::vector& a) + { + return utils::string::va("{x: %f, y: %f, z: %f}", a.get_x(), a.get_y(), a.get_z()); + }; + + auto element_type = state.new_usertype("element", "new", []() + { + const auto el = new element(); + elements.push_back(el); + return el; + }); + + element_type["setvertalign"] = &element::set_vertalign; + element_type["sethorzalign"] = &element::set_horzalign; + element_type["setrect"] = &element::set_rect; + element_type["setfont"] = sol::overload( + static_cast(&element::set_font), + static_cast(&element::set_font) + ); + element_type["settext"] = &element::set_text; + element_type["setmaterial"] = &element::set_material; + element_type["setcolor"] = &element::set_color; + element_type["setbackcolor"] = &element::set_background_color; + element_type["setbordercolor"] = &element::set_border_color; + element_type["setborderwidth"] = sol::overload( + static_cast(&element::set_border_width), + static_cast(&element::set_border_width), + static_cast(&element::set_border_width), + static_cast(&element::set_border_width) + ); + element_type["settextoffset"] = &element::set_text_offset; + element_type["setslice"] = &element::set_slice; + + element_type["getrect"] = [](const sol::this_state s, element& element) + { + auto rect = sol::table::create(s.lua_state()); + rect["x"] = element.x; + rect["y"] = element.y; + rect["w"] = element.w + element.border_width[1] + element.border_width[3]; + rect["h"] = element.h + element.border_width[0] + element.border_width[2]; + + return rect; + }; + + element_type["x"] = sol::property( + [](element& element) + { + return element.x; + }, + [](element& element, float x) + { + element.x = x; + } + ); + + element_type["y"] = sol::property( + [](element& element) + { + return element.y; + }, + [](element& element, float y) + { + element.y = y; + } + ); + + element_type["w"] = sol::property( + [](element& element) + { + return element.w; + }, + [](element& element, float w) + { + element.w = w; + } + ); + + element_type["h"] = sol::property( + [](element& element) + { + return element.h; + }, + [](element& element, float h) + { + element.h = h; + } + ); + + element_type["color"] = sol::property( + [](element& element, const sol::this_state s) + { + auto color = sol::table::create(s.lua_state()); + color["r"] = element.color[0]; + color["g"] = element.color[1]; + color["b"] = element.color[2]; + color["a"] = element.color[3]; + return color; + }, + [](element& element, const sol::lua_table color) + { + element.color[0] = color["r"].get_type() == sol::type::number ? color["r"].get() : 0.f; + element.color[1] = color["g"].get_type() == sol::type::number ? color["g"].get() : 0.f; + element.color[2] = color["b"].get_type() == sol::type::number ? color["b"].get() : 0.f; + element.color[3] = color["a"].get_type() == sol::type::number ? color["a"].get() : 0.f; + } + ); + + element_type["backcolor"] = sol::property( + [](element& element, const sol::this_state s) + { + auto color = sol::table::create(s.lua_state()); + color["r"] = element.background_color[0]; + color["g"] = element.background_color[1]; + color["b"] = element.background_color[2]; + color["a"] = element.background_color[3]; + return color; + }, + [](element& element, const sol::lua_table color) + { + element.background_color[0] = color["r"].get_type() == sol::type::number ? color["r"].get() : 0.f; + element.background_color[1] = color["g"].get_type() == sol::type::number ? color["g"].get() : 0.f; + element.background_color[2] = color["b"].get_type() == sol::type::number ? color["b"].get() : 0.f; + element.background_color[3] = color["a"].get_type() == sol::type::number ? color["a"].get() : 0.f; + } + ); + + element_type["bordercolor"] = sol::property( + [](element& element, const sol::this_state s) + { + auto color = sol::table::create(s.lua_state()); + color["r"] = element.border_color[0]; + color["g"] = element.border_color[1]; + color["b"] = element.border_color[2]; + color["a"] = element.border_color[3]; + return color; + }, + [](element& element, const sol::lua_table color) + { + element.border_color[0] = color["r"].get_type() == sol::type::number ? color["r"].get() : 0.f; + element.border_color[1] = color["g"].get_type() == sol::type::number ? color["g"].get() : 0.f; + element.border_color[2] = color["b"].get_type() == sol::type::number ? color["b"].get() : 0.f; + element.border_color[3] = color["a"].get_type() == sol::type::number ? color["a"].get() : 0.f; + } + ); + + element_type["borderwidth"] = sol::property( + [](element& element, const sol::this_state s) + { + auto color = sol::table::create(s.lua_state()); + color["top"] = element.border_width[0]; + color["right"] = element.border_width[1]; + color["bottom"] = element.border_width[2]; + color["left"] = element.border_width[3]; + return color; + }, + [](element& element, const sol::lua_table color) + { + element.border_width[0] = color["top"].get_type() == sol::type::number ? color["top"].get() : 0.f; + element.border_width[1] = color["right"].get_type() == sol::type::number ? color["right"].get() : element.border_width[1]; + element.border_width[2] = color["bottom"].get_type() == sol::type::number ? color["bottom"].get() : element.border_width[2]; + element.border_width[3] = color["left"].get_type() == sol::type::number ? color["left"].get() : element.border_width[3]; + } + ); + + element_type["font"] = sol::property( + [](element& element) + { + return element.font; + }, + [](element& element, const std::string& font) + { + element.set_font(font); + } + ); + + element_type["fontsize"] = sol::property( + [](element& element) + { + return element.fontsize; + }, + [](element& element, float fontsize) + { + element.fontsize = (int)fontsize; + } + ); + + element_type["onnotify"] = [&handler](element& element, const std::string& event, + const event_callback& callback) + { + event_listener listener{}; + listener.callback = callback; + listener.element = &element; + listener.event = event; + listener.is_volatile = false; + + return handler.add_event_listener(std::move(listener)); + }; + + element_type["onnotifyonce"] = [&handler](element& element, const std::string& event, + const event_callback& callback) + { + event_listener listener{}; + listener.callback = callback; + listener.element = &element; + listener.event = event; + listener.is_volatile = true; + + return handler.add_event_listener(std::move(listener)); + }; + + element_type["notify"] = [&handler](element& element, const sol::this_state s, const std::string& _event, + sol::variadic_args va) + { + event event; + event.element = &element; + event.name = _event; + + for (auto arg : va) + { + if (arg.get_type() == sol::type::number) + { + event.arguments.push_back(arg.as()); + } + + if (arg.get_type() == sol::type::string) + { + event.arguments.push_back(arg.as()); + } + } + + handler.dispatch(event); + }; + + auto menu_type = state.new_usertype("menu"); + + menu_type["onnotify"] = [&handler](menu& menu, const std::string& event, + const event_callback& callback) + { + event_listener listener{}; + listener.callback = callback; + listener.element = &menu; + listener.event = event; + listener.is_volatile = false; + + return handler.add_event_listener(std::move(listener)); + }; + + menu_type["onnotifyonce"] = [&handler](menu& menu, const std::string& event, + const event_callback& callback) + { + event_listener listener{}; + listener.callback = callback; + listener.element = &menu; + listener.event = event; + listener.is_volatile = true; + + return handler.add_event_listener(std::move(listener)); + }; + + menu_type["notify"] = [&handler](menu& element, const sol::this_state s, const std::string& _event, + sol::variadic_args va) + { + event event; + event.element = &element; + event.name = _event; + + for (auto arg : va) + { + if (arg.get_type() == sol::type::number) + { + event.arguments.push_back(arg.as()); + } + + if (arg.get_type() == sol::type::string) + { + event.arguments.push_back(arg.as()); + } + } + + handler.dispatch(event); + }; + + menu_type["addchild"] = [](const sol::this_state s, menu& menu, element& element) + { + menu.add_child(&element); + }; + + menu_type["cursor"] = sol::property( + [](menu& menu) + { + return menu.cursor; + }, + [](menu& menu, bool cursor) + { + menu.cursor = cursor; + } + ); + + menu_type["isopen"] = [](menu& menu) + { + return menu.visible || (menu.type == menu_type::overlay && game::Menu_IsMenuOpenAndVisible(0, menu.overlay_menu.data())); + }; + + menu_type["open"] = [&handler](menu& menu) + { + event event; + event.element = &menu; + event.name = "close"; + handler.dispatch(event); + + menu.open(); + }; + + menu_type["close"] = [&handler](menu& menu) + { + event event; + event.element = &menu; + event.name = "close"; + handler.dispatch(event); + + menu.close(); + }; + + struct game + { + }; + auto game_type = state.new_usertype("game_"); + state["game"] = game(); + + game_type["time"] = []() + { + const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); + return now.count(); + }; + + game_type["newmenu"] = [](const sol::lua_value&, const std::string& name) + { + menus[name] = {}; + return &menus[name]; + }; + + game_type["newmenuoverlay"] = [](const sol::lua_value&, const std::string& name, const std::string& menu_name) + { + menus[name] = {}; + menus[name].type = menu_type::overlay; + menus[name].overlay_menu = menu_name; + return &menus[name]; + }; + + game_type["getmouseposition"] = [](const sol::this_state s, const game&) + { + auto pos = sol::table::create(s.lua_state()); + pos["x"] = mouse[0]; + pos["y"] = mouse[1]; + + return pos; + }; + + game_type["openmenu"] = [&handler](const game&, const std::string& name) + { + if (menus.find(name) == menus.end()) + { + return; + } + + const auto menu = &menus[name]; + + event event; + event.element = menu; + event.name = "close"; + handler.dispatch(event); + + menu->open(); + }; + + game_type["closemenu"] = [&handler](const game&, const std::string& name) + { + if (menus.find(name) == menus.end()) + { + return; + } + + const auto menu = &menus[name]; + + event event; + event.element = menu; + event.name = "close"; + handler.dispatch(event); + + menu->close(); + }; + + game_type["onframe"] = [&scheduler](const game&, const sol::protected_function& callback) + { + return scheduler.add(callback, 0, false); + }; + + game_type["ontimeout"] = [&scheduler](const game&, const sol::protected_function& callback, + const long long milliseconds) + { + return scheduler.add(callback, milliseconds, true); + }; + + game_type["oninterval"] = [&scheduler](const game&, const sol::protected_function& callback, + const long long milliseconds) + { + return scheduler.add(callback, milliseconds, false); + }; + + game_type["onnotify"] = [&handler](const game&, const std::string& event, + const event_callback& callback) + { + event_listener listener{}; + listener.callback = callback; + listener.element = &ui_element; + listener.event = event; + listener.is_volatile = false; + + return handler.add_event_listener(std::move(listener)); + }; + + game_type["onnotifyonce"] = [&handler](const game&, const std::string& event, + const event_callback& callback) + { + event_listener listener{}; + listener.callback = callback; + listener.element = &ui_element; + listener.event = event; + listener.is_volatile = true; + + return handler.add_event_listener(std::move(listener)); + }; + + game_type["isingame"] = []() + { + return ::game::CL_IsCgameInitialized() && ::game::g_entities[0].client; + }; + + game_type["getdvar"] = [](const game&, const sol::this_state s, const std::string& name) + { + const auto dvar = ::game::Dvar_FindVar(name.data()); + if (!dvar) + { + return sol::lua_value{s, sol::lua_nil}; + } + + const std::string value = ::game::Dvar_ValueToString(dvar, nullptr, &dvar->current); + return sol::lua_value{s, value}; + }; + + game_type["getdvarint"] = [](const game&, const sol::this_state s, const std::string& name) + { + const auto dvar = ::game::Dvar_FindVar(name.data()); + if (!dvar) + { + return sol::lua_value{s, sol::lua_nil}; + } + + const auto value = atoi(::game::Dvar_ValueToString(dvar, nullptr, &dvar->current)); + return sol::lua_value{s, value}; + }; + + game_type["getdvarfloat"] = [](const game&, const sol::this_state s, const std::string& name) + { + const auto dvar = ::game::Dvar_FindVar(name.data()); + if (!dvar) + { + return sol::lua_value{s, sol::lua_nil}; + } + + const auto value = atof(::game::Dvar_ValueToString(dvar, nullptr, &dvar->current)); + return sol::lua_value{s, value}; + }; + + game_type["setdvar"] = [](const game&, const std::string& name, const sol::lua_value& value) + { + if (!valid_dvar_name(name)) + { + throw std::runtime_error("Invalid DVAR name, must be alphanumeric"); + } + + const auto hash = ::game::generateHashValue(name.data()); + std::string string_value; + + if (value.is()) + { + string_value = utils::string::va("%i", value.as()); + } + else if (value.is()) + { + string_value = utils::string::va("%i", value.as()); + } + else if (value.is()) + { + string_value = utils::string::va("%f", value.as()); + } + else if (value.is()) + { + const auto v = value.as(); + string_value = utils::string::va("%f %f %f", + v.get_x(), + v.get_y(), + v.get_z() + ); + } + + if (value.is()) + { + string_value = value.as(); + } + + ::game::Dvar_SetCommand(hash, "", string_value.data()); + }; + + game_type["drawmaterial"] = [](const game&, float x, float y, float width, float height, float s0, float t0, float s1, float t1, + const sol::lua_value& color_value, const std::string& material) + { + const auto color = color_value.as>(); + float _color[4] = + { + color[0], + color[1], + color[2], + color[3], + }; + + const auto _material = ::game::Material_RegisterHandle(material.data()); + ::game::R_AddCmdDrawStretchPic(x, y, width, height, s0, t0, s1, t1, _color, _material); + }; + + game_type["playsound"] = [](const game&, const std::string& sound) + { + ::game::UI_PlayLocalSoundAlias(0, sound.data()); + }; + + game_type["getwindowsize"] = [](const game&, const sol::this_state s) + { + const auto size = ::game::ScrPlace_GetViewPlacement()->realViewportSize; + + auto screen = sol::table::create(s.lua_state()); + screen["x"] = size[0]; + screen["y"] = size[1]; + + return screen; + }; + + struct player + { + }; + auto player_type = state.new_usertype("player_"); + state["player"] = player(); + + player_type["notify"] = [](const player&, const sol::this_state s, const std::string& name, sol::variadic_args va) + { + if (!::game::CL_IsCgameInitialized() || !::game::g_entities[0].client) + { + throw std::runtime_error("Not in game"); + } + + ::scheduler::once([s, name, args = std::vector(va.begin(), va.end())]() + { + std::vector arguments{}; + + for (auto arg : args) + { + arguments.push_back(convert({s, arg})); + } + + const auto player_value = scripting::call("getentbynum", {0}); + if (player_value.get_raw().type != ::game::SCRIPT_OBJECT) + { + return; + } + + const auto player = player_value.as(); + + scripting::notify(player, name, arguments); + }, ::scheduler::pipeline::server); + }; + + player_type["getorigin"] = [](const player&) + { + if (!::game::CL_IsCgameInitialized() || !::game::g_entities[0].client) + { + throw std::runtime_error("Not in game"); + } + + return scripting::vector( + ::game::g_entities[0].origin[0], + ::game::g_entities[0].origin[1], + ::game::g_entities[0].origin[2] + ); + }; + + player_type["setorigin"] = [](const player&, const scripting::vector& velocity) + { + if (!::game::CL_IsCgameInitialized() || !::game::g_entities[0].client) + { + throw std::runtime_error("Not in game"); + } + + ::game::g_entities[0].origin[0] = velocity.get_x(); + ::game::g_entities[0].origin[1] = velocity.get_y(); + ::game::g_entities[0].origin[2] = velocity.get_z(); + }; + + player_type["getvelocity"] = [](const player&) + { + if (!::game::CL_IsCgameInitialized() || !::game::g_entities[0].client) + { + throw std::runtime_error("Not in game"); + } + + return scripting::vector( + ::game::g_entities[0].client->velocity[0], + ::game::g_entities[0].client->velocity[1], + ::game::g_entities[0].client->velocity[2] + ); + }; + + player_type["setvelocity"] = [](const player&, const scripting::vector& velocity) + { + if (!::game::CL_IsCgameInitialized() || !::game::g_entities[0].client) + { + throw std::runtime_error("Not in game"); + } + + ::game::g_entities[0].client->velocity[0] = velocity.get_x(); + ::game::g_entities[0].client->velocity[1] = velocity.get_y(); + ::game::g_entities[0].client->velocity[2] = velocity.get_z(); + }; + + state.script(animation_script); + } + } + + context::context(std::string folder) + : folder_(std::move(folder)) + , scheduler_(state_) + , event_handler_(state_) + + { + this->state_.open_libraries(sol::lib::base, + sol::lib::package, + sol::lib::io, + sol::lib::string, + sol::lib::os, + sol::lib::math, + sol::lib::table); + + this->state_["include"] = [this](const std::string& file) + { + this->load_script(file); + }; + + sol::function old_require = this->state_["require"]; + auto base_path = utils::string::replace(this->folder_, "/", ".") + "."; + this->state_["require"] = [base_path, old_require](const std::string& path) + { + return old_require(base_path + path); + }; + + this->state_["scriptdir"] = [this]() + { + return this->folder_; + }; + + setup_types(this->state_, this->event_handler_, this->scheduler_); + + printf("Loading ui script '%s'\n", this->folder_.data()); + this->load_script("__init__"); + } + + context::~context() + { + this->state_.collect_garbage(); + this->scheduler_.clear(); + this->event_handler_.clear(); + this->state_ = {}; + } + + void context::run_frame() + { + this->scheduler_.run_frame(); + this->state_.collect_garbage(); + } + + void context::notify(const event& e) + { + this->scheduler_.dispatch(e); + this->event_handler_.dispatch(e); + } + + void context::load_script(const std::string& script) + { + if (!this->loaded_scripts_.emplace(script).second) + { + return; + } + + const auto file = (std::filesystem::path{this->folder_} / (script + ".lua")).generic_string(); + handle_error(this->state_.safe_script_file(file, &sol::script_pass_on_error)); + } +} diff --git a/src/client/game/ui_scripting/lua/context.hpp b/src/client/game/ui_scripting/lua/context.hpp new file mode 100644 index 00000000..7a45e4f3 --- /dev/null +++ b/src/client/game/ui_scripting/lua/context.hpp @@ -0,0 +1,47 @@ +#pragma once + +#pragma warning(push) +#pragma warning(disable: 4702) + +#define SOL_ALL_SAFETIES_ON 1 +#define SOL_PRINT_ERRORS 0 +#include + +#include "../menu.hpp" +#include "event.hpp" +#include "scheduler.hpp" +#include "event_handler.hpp" + +namespace ui_scripting::lua +{ + extern std::unordered_map menus; + extern std::vector elements; + extern element ui_element; + extern int mouse[2]; + + class context + { + public: + context(std::string folder); + ~context(); + + context(context&&) noexcept = delete; + context& operator=(context&&) noexcept = delete; + + context(const context&) = delete; + context& operator=(const context&) = delete; + + void run_frame(); + void notify(const event& e); + + private: + sol::state state_{}; + std::string folder_; + std::unordered_set loaded_scripts_; + + scheduler scheduler_; + event_handler event_handler_; + + void load_script(const std::string& script); + }; +} diff --git a/src/client/game/ui_scripting/lua/engine.cpp b/src/client/game/ui_scripting/lua/engine.cpp new file mode 100644 index 00000000..563b43ef --- /dev/null +++ b/src/client/game/ui_scripting/lua/engine.cpp @@ -0,0 +1,453 @@ +#include +#include "engine.hpp" +#include "context.hpp" + +#include "../../../component/scheduler.hpp" + +#include +#include + +namespace ui_scripting::lua::engine +{ + void notify(const event& e); + + namespace + { + float screen_max[2]; + + void check_resize() + { + screen_max[0] = game::ScrPlace_GetViewPlacement()->realViewportSize[0]; + screen_max[1] = game::ScrPlace_GetViewPlacement()->realViewportSize[1]; + } + + int relative_mouse(int value) + { + return (int)(((float)value / screen_max[0]) * 1920.f); + } + + int relative(int value) + { + return (int)(((float)value / 1920.f) * screen_max[0]); + } + + float relative(float value) + { + return (value / 1920.f) * screen_max[0]; + } + + bool point_in_rect(int px, int py, int x, int y, int w, int h) + { + return (px > x && px < x + w && py > y && py < y + h); + } + + bool is_menu_visible(const menu& menu) + { + return menu.visible || (menu.type == menu_type::overlay && game::Menu_IsMenuOpenAndVisible(0, menu.overlay_menu.data())); + } + + std::vector elements_in_point(int x, int y) + { + std::vector result; + + for (const auto& menu : menus) + { + if (!is_menu_visible(menu.second)) + { + continue; + } + + for (const auto& child : menu.second.children) + { + const auto in_rect = point_in_rect( + x, y, + (int)child->x, + (int)child->y, + (int)child->w + (int)child->border_width[1] + (int)child->border_width[3], + (int)child->h + (int)child->border_width[0] + (int)child->border_width[2] + ); + + if (in_rect) + { + result.push_back(child); + } + } + } + + return result; + } + + void handle_key_event(const int key, const int down) + { + const auto _elements = elements_in_point(mouse[0], mouse[1]); + + switch (key) + { + case game::K_MOUSE2: + case game::K_MOUSE1: + { + const auto click_name = key == game::K_MOUSE1 + ? "click" + : "rightclick"; + + const auto key_name = key == game::K_MOUSE1 + ? "mouse" + : "rightmouse"; + + { + event main_event; + main_event.element = &ui_element; + main_event.name = utils::string::va("%s%s", key_name, down ? "down" : "up"); + main_event.arguments = + { + mouse[0], + mouse[1], + }; + + engine::notify(main_event); + + for (const auto& element : _elements) + { + event event; + event.element = element; + event.name = utils::string::va("%s%s", key_name, down ? "down" : "up"); + event.arguments = + { + mouse[0], + mouse[1], + }; + + engine::notify(event); + } + } + + if (!down) + { + event main_event; + main_event.element = &ui_element; + main_event.name = click_name; + main_event.arguments = + { + mouse[0], + mouse[1], + }; + + engine::notify(main_event); + + for (const auto& element : _elements) + { + event event; + event.element = element; + event.name = click_name; + event.arguments = + { + mouse[0], + mouse[1], + }; + + engine::notify(event); + } + } + + break; + } + case game::K_MWHEELUP: + case game::K_MWHEELDOWN: + { + const auto key_name = key == game::K_MWHEELUP + ? "scrollup" + : "scrolldown"; + + if (!down) + { + break; + } + + { + event main_event; + main_event.element = &ui_element; + main_event.name = key_name; + main_event.arguments = + { + mouse[0], + mouse[1], + }; + + engine::notify(main_event); + + for (const auto& element : _elements) + { + event event; + event.element = element; + event.name = key_name; + event.arguments = {mouse[0], mouse[1]}; + + engine::notify(event); + } + } + + break; + } + default: + { + event event; + event.element = &ui_element; + event.name = down + ? "keydown" + : "keyup"; + event.arguments = {key}; + + notify(event); + + break; + } + } + } + + void handle_char_event(const int key) + { + std::string key_str = {(char)key}; + event event; + event.element = &ui_element; + event.name = "keypress"; + event.arguments = {key_str}; + + engine::notify(event); + } + + std::vector previous_elements; + void handle_mousemove_event(const int x, const int y) + { + if (mouse[0] == x && mouse[1] == y) + { + return; + } + + mouse[0] = x; + mouse[1] = y; + + { + event event; + event.element = &ui_element; + event.name = "mousemove"; + event.arguments = {x, y}; + + engine::notify(event); + } + + const auto _elements = elements_in_point(x, y); + for (const auto& element : _elements) + { + event event; + event.element = element; + event.name = "mouseover"; + + engine::notify(event); + } + + for (const auto& element : previous_elements) + { + auto found = false; + + for (const auto& _element : _elements) + { + if (element == _element) + { + found = true; + } + } + + if (!found) + { + event event; + event.element = element; + event.name = "mouseleave"; + + engine::notify(event); + } + } + + for (const auto& element : _elements) + { + auto found = false; + + for (const auto& _element : previous_elements) + { + if (element == _element) + { + found = true; + } + } + + if (!found) + { + event event; + event.element = element; + event.name = "mouseenter"; + + engine::notify(event); + } + } + + previous_elements = _elements; + } + + auto& get_scripts() + { + static std::vector> scripts{}; + return scripts; + } + + void load_scripts() + { + const auto script_dir = "ui_scripts/"s; + + if (!utils::io::directory_exists(script_dir)) + { + return; + } + + const auto scripts = utils::io::list_files(script_dir); + + for (const auto& script : scripts) + { + if (std::filesystem::is_directory(script) && utils::io::file_exists(script + "/__init__.lua")) + { + get_scripts().push_back(std::make_unique(script)); + } + } + } + + void render_menus() + { + check_resize(); + + for (const auto& menu : menus) + { + if (is_menu_visible(menu.second)) + { + menu.second.render(); + } + } + } + + void close_all_menus() + { + for (auto& menu : menus) + { + if (!is_menu_visible(menu.second)) + { + continue; + } + + event event; + event.element = &menu.second; + event.name = "close"; + notify(event); + + menu.second.close(); + } + } + + void clear_menus() + { + menus.clear(); + + for (const auto element : elements) + { + delete element; + } + + elements.clear(); + } + } + + void open_menu(const std::string& name) + { + if (menus.find(name) == menus.end()) + { + return; + } + + const auto menu = &menus[name]; + + event event; + event.element = menu; + event.name = "open"; + notify(event); + + menu->open(); + } + + void close_menu(const std::string& name) + { + if (menus.find(name) == menus.end()) + { + return; + } + + const auto menu = &menus[name]; + + event event; + event.element = menu; + event.name = "close"; + notify(event); + + menu->close(); + } + + void start() + { + close_all_menus(); + get_scripts().clear(); + clear_menus(); + load_scripts(); + } + + void stop() + { + close_all_menus(); + get_scripts().clear(); + clear_menus(); + } + + void ui_event(const std::string& type, const std::vector& arguments) + { + ::scheduler::once([type, arguments]() + { + if (type == "key") + { + handle_key_event(arguments[0], arguments[1]); + } + + if (type == "char") + { + handle_char_event(arguments[0]); + } + + if (type == "mousemove") + { + handle_mousemove_event(relative_mouse(arguments[0]), relative_mouse(arguments[1])); + } + }, ::scheduler::pipeline::renderer); + } + + void notify(const event& e) + { + for (auto& script : get_scripts()) + { + script->notify(e); + } + } + + void run_frame() + { + check_resize(); + render_menus(); + + for (auto& script : get_scripts()) + { + script->run_frame(); + } + } +} diff --git a/src/client/game/ui_scripting/lua/engine.hpp b/src/client/game/ui_scripting/lua/engine.hpp new file mode 100644 index 00000000..f8f6b032 --- /dev/null +++ b/src/client/game/ui_scripting/lua/engine.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace ui_scripting::lua::engine +{ + void start(); + void stop(); + + void close_menu(const std::string& name); + void open_menu(const std::string& name); + + void ui_event(const std::string&, const std::vector&); + void run_frame(); +} diff --git a/src/client/game/ui_scripting/lua/error.cpp b/src/client/game/ui_scripting/lua/error.cpp new file mode 100644 index 00000000..6e021a3b --- /dev/null +++ b/src/client/game/ui_scripting/lua/error.cpp @@ -0,0 +1,24 @@ +#include +#include "error.hpp" + +namespace ui_scripting::lua +{ + void handle_error(const sol::protected_function_result& result) + { + if (!result.valid()) + { + try + { + printf("************** UI Script execution error **************\n"); + + const sol::error err = result; + printf("%s\n", err.what()); + + printf("****************************************************\n"); + } + catch (...) + { + } + } + } +} diff --git a/src/client/game/ui_scripting/lua/error.hpp b/src/client/game/ui_scripting/lua/error.hpp new file mode 100644 index 00000000..28a5c453 --- /dev/null +++ b/src/client/game/ui_scripting/lua/error.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "context.hpp" + +namespace ui_scripting::lua +{ + void handle_error(const sol::protected_function_result& result); +} diff --git a/src/client/game/ui_scripting/lua/event.hpp b/src/client/game/ui_scripting/lua/event.hpp new file mode 100644 index 00000000..54f79b3a --- /dev/null +++ b/src/client/game/ui_scripting/lua/event.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "context.hpp" + +namespace ui_scripting::lua +{ + struct event + { + std::string name; + const void* element{}; + std::vector> arguments; + }; +} diff --git a/src/client/game/ui_scripting/lua/event_handler.cpp b/src/client/game/ui_scripting/lua/event_handler.cpp new file mode 100644 index 00000000..727a92dd --- /dev/null +++ b/src/client/game/ui_scripting/lua/event_handler.cpp @@ -0,0 +1,189 @@ +#include "std_include.hpp" +#include "context.hpp" +#include "error.hpp" +#include "../../scripting/lua/value_conversion.hpp" + +#include "event_handler.hpp" + +namespace ui_scripting::lua +{ + event_handler::event_handler(sol::state& state) + : state_(state) + { + auto event_listener_handle_type = state.new_usertype("event_listener_handle"); + + event_listener_handle_type["clear"] = [this](const event_listener_handle& handle) + { + this->remove(handle); + }; + + event_listener_handle_type["endon"] = [this](const event_listener_handle& handle, const element* entity, const std::string& event) + { + this->add_endon_condition(handle, entity, event); + }; + } + + void event_handler::dispatch(const event& event) + { + bool has_built_arguments = false; + event_arguments arguments{}; + + callbacks_.access([&](task_list& tasks) + { + this->merge_callbacks(); + this->handle_endon_conditions(event); + + for (auto i = tasks.begin(); i != tasks.end();) + { + if (i->event != event.name || i->element != event.element) + { + ++i; + continue; + } + + if (!i->is_deleted) + { + if (!has_built_arguments) + { + has_built_arguments = true; + arguments = this->build_arguments(event); + } + + handle_error(i->callback(sol::as_args(arguments))); + } + + if (i->is_volatile || i->is_deleted) + { + i = tasks.erase(i); + } + else + { + ++i; + } + } + }); + } + + event_listener_handle event_handler::add_event_listener(event_listener&& listener) + { + const uint64_t id = ++this->current_listener_id_; + listener.id = id; + listener.is_deleted = false; + + new_callbacks_.access([&listener](task_list& tasks) + { + tasks.emplace_back(std::move(listener)); + }); + + return {id}; + } + + void event_handler::add_endon_condition(const event_listener_handle& handle, const element* element, + const std::string& event) + { + auto merger = [&](task_list& tasks) + { + for (auto& task : tasks) + { + if (task.id == handle.id) + { + task.endon_conditions.emplace_back((uint64_t)element, event); + } + } + }; + + callbacks_.access([&](task_list& tasks) + { + merger(tasks); + new_callbacks_.access(merger); + }); + } + + void event_handler::clear() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + new_tasks.clear(); + tasks.clear(); + }); + }); + } + + void event_handler::remove(const event_listener_handle& handle) + { + auto mask_as_deleted = [&](task_list& tasks) + { + for (auto& task : tasks) + { + if (task.id == handle.id) + { + task.is_deleted = true; + break; + } + } + }; + + callbacks_.access(mask_as_deleted); + new_callbacks_.access(mask_as_deleted); + } + + void event_handler::merge_callbacks() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + tasks.insert(tasks.end(), std::move_iterator(new_tasks.begin()), + std::move_iterator(new_tasks.end())); + new_tasks = {}; + }); + }); + } + + void event_handler::handle_endon_conditions(const event& event) + { + auto deleter = [&](task_list& tasks) + { + for (auto& task : tasks) + { + for (auto& condition : task.endon_conditions) + { + if (condition.first == (uint64_t)event.element && condition.second == event.name) + { + task.is_deleted = true; + break; + } + } + } + }; + + callbacks_.access(deleter); + } + + event_arguments event_handler::build_arguments(const event& event) const + { + event_arguments arguments; + + for (const auto& argument : event.arguments) + { + const auto index = argument.index(); + + if (index == 0) + { + const sol::lua_value value = {this->state_, std::get(argument)}; + arguments.emplace_back(value); + } + + if (index == 1) + { + const sol::lua_value value = {this->state_, std::get(argument)}; + arguments.emplace_back(value); + } + + } + + return arguments; + } +} diff --git a/src/client/game/ui_scripting/lua/event_handler.hpp b/src/client/game/ui_scripting/lua/event_handler.hpp new file mode 100644 index 00000000..a203a23a --- /dev/null +++ b/src/client/game/ui_scripting/lua/event_handler.hpp @@ -0,0 +1,59 @@ +#pragma once +#include + +namespace ui_scripting::lua +{ + using event_arguments = std::vector; + using event_callback = sol::protected_function; + + class event_listener_handle + { + public: + uint64_t id = 0; + }; + + class event_listener final : public event_listener_handle + { + public: + std::string event = {}; + void* element{}; + event_callback callback = {}; + bool is_volatile = false; + bool is_deleted = false; + std::vector> endon_conditions{}; + }; + + class event_handler final + { + public: + event_handler(sol::state& state); + + event_handler(event_handler&&) noexcept = delete; + event_handler& operator=(event_handler&&) noexcept = delete; + + event_handler(const scheduler&) = delete; + event_handler& operator=(const event_handler&) = delete; + + void dispatch(const event& event); + + event_listener_handle add_event_listener(event_listener&& listener); + + void clear(); + + private: + sol::state& state_; + std::atomic_int64_t current_listener_id_ = 0; + + using task_list = std::vector; + utils::concurrency::container new_callbacks_; + utils::concurrency::container callbacks_; + + void remove(const event_listener_handle& handle); + void merge_callbacks(); + void handle_endon_conditions(const event& event); + + void add_endon_condition(const event_listener_handle& handle, const element* element, const std::string& event); + + event_arguments build_arguments(const event& event) const; + }; +} diff --git a/src/client/game/ui_scripting/lua/scheduler.cpp b/src/client/game/ui_scripting/lua/scheduler.cpp new file mode 100644 index 00000000..8ecb930a --- /dev/null +++ b/src/client/game/ui_scripting/lua/scheduler.cpp @@ -0,0 +1,171 @@ +#include "std_include.hpp" +#include "context.hpp" +#include "error.hpp" + +namespace ui_scripting::lua +{ + scheduler::scheduler(sol::state& state) + { + auto task_handle_type = state.new_usertype("task_handle"); + + task_handle_type["clear"] = [this](const task_handle& handle) + { + this->remove(handle); + }; + + task_handle_type["endon"] = [this](const task_handle& handle, const element* element, const std::string& event) + { + this->add_endon_condition(handle, element, event); + }; + } + + void scheduler::dispatch(const event& event) + { + auto deleter = [&](task_list& tasks) + { + for (auto& task : tasks) + { + for (auto& condition : task.endon_conditions) + { + if (condition.first == (uint64_t)event.element && condition.second == event.name) + { + task.is_deleted = true; + break; + } + } + } + }; + + callbacks_.access([&](task_list& tasks) + { + deleter(tasks); + new_callbacks_.access(deleter); + }); + } + + void scheduler::run_frame() + { + callbacks_.access([&](task_list& tasks) + { + this->merge_callbacks(); + + for (auto i = tasks.begin(); i != tasks.end();) + { + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - i->last_call; + + if (diff < i->delay) + { + ++i; + continue; + } + + i->last_call = now; + + if (!i->is_deleted) + { + handle_error(i->callback()); + } + + if (i->is_volatile || i->is_deleted) + { + i = tasks.erase(i); + } + else + { + ++i; + } + } + }); + } + + void scheduler::clear() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + new_tasks.clear(); + tasks.clear(); + }); + }); + } + + task_handle scheduler::add(const sol::protected_function& callback, const long long milliseconds, + const bool is_volatile) + { + return this->add(callback, std::chrono::milliseconds(milliseconds), is_volatile); + } + + task_handle scheduler::add(const sol::protected_function& callback, const std::chrono::milliseconds delay, + const bool is_volatile) + { + const uint64_t id = ++this->current_task_id_; + + task task; + task.is_volatile = is_volatile; + task.callback = callback; + task.delay = delay; + task.last_call = std::chrono::steady_clock::now(); + task.id = id; + task.is_deleted = false; + + new_callbacks_.access([&task](task_list& tasks) + { + tasks.emplace_back(std::move(task)); + }); + + return {id}; + } + + void scheduler::add_endon_condition(const task_handle& handle, const element* element, const std::string& event) + { + auto merger = [&](task_list& tasks) + { + for (auto& task : tasks) + { + if (task.id == handle.id) + { + task.endon_conditions.emplace_back(element->id, event); + } + } + }; + + callbacks_.access([&](task_list& tasks) + { + merger(tasks); + new_callbacks_.access(merger); + }); + } + + void scheduler::remove(const task_handle& handle) + { + auto mask_as_deleted = [&](task_list& tasks) + { + for (auto& task : tasks) + { + if (task.id == handle.id) + { + task.is_deleted = true; + break; + } + } + }; + + callbacks_.access(mask_as_deleted); + new_callbacks_.access(mask_as_deleted); + } + + void scheduler::merge_callbacks() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + tasks.insert(tasks.end(), std::move_iterator(new_tasks.begin()), + std::move_iterator(new_tasks.end())); + new_tasks = {}; + }); + }); + } +} diff --git a/src/client/game/ui_scripting/lua/scheduler.hpp b/src/client/game/ui_scripting/lua/scheduler.hpp new file mode 100644 index 00000000..ac730a03 --- /dev/null +++ b/src/client/game/ui_scripting/lua/scheduler.hpp @@ -0,0 +1,54 @@ +#pragma once +#include + +namespace ui_scripting::lua +{ + class context; + + class task_handle + { + public: + uint64_t id = 0; + }; + + class task final : public task_handle + { + public: + std::chrono::steady_clock::time_point last_call{}; + sol::protected_function callback{}; + std::chrono::milliseconds delay{}; + bool is_volatile = false; + bool is_deleted = false; + std::vector> endon_conditions{}; + }; + + class scheduler final + { + public: + scheduler(sol::state& state); + + scheduler(scheduler&&) noexcept = delete; + scheduler& operator=(scheduler&&) noexcept = delete; + + scheduler(const scheduler&) = delete; + scheduler& operator=(const scheduler&) = delete; + + void dispatch(const event& event); + void run_frame(); + void clear(); + + task_handle add(const sol::protected_function& callback, long long milliseconds, bool is_volatile); + task_handle add(const sol::protected_function& callback, std::chrono::milliseconds delay, bool is_volatile); + + private: + using task_list = std::vector; + utils::concurrency::container new_callbacks_; + utils::concurrency::container callbacks_; + std::atomic_int64_t current_task_id_ = 0; + + void add_endon_condition(const task_handle& handle, const element* element, const std::string& event); + + void remove(const task_handle& handle); + void merge_callbacks(); + }; +} diff --git a/src/client/game/ui_scripting/menu.cpp b/src/client/game/ui_scripting/menu.cpp new file mode 100644 index 00000000..844c90ef --- /dev/null +++ b/src/client/game/ui_scripting/menu.cpp @@ -0,0 +1,41 @@ +#include +#include "menu.hpp" +#include "lua/engine.hpp" +#include "component/ui_scripting.hpp" + +namespace ui_scripting +{ + menu::menu() + { + } + + void menu::add_child(element* el) + { + this->children.push_back(el); + } + + void menu::open() + { + *game::keyCatchers |= 0x40; + this->visible = true; + } + + void menu::close() + { + *game::keyCatchers &= ~0x40; + this->visible = false; + } + + void menu::render() const + { + if (this->cursor) + { + *game::keyCatchers |= 0x40; + } + + for (auto& element : this->children) + { + element->render(); + } + } +} diff --git a/src/client/game/ui_scripting/menu.hpp b/src/client/game/ui_scripting/menu.hpp new file mode 100644 index 00000000..a726fdaa --- /dev/null +++ b/src/client/game/ui_scripting/menu.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "game/game.hpp" +#include "element.hpp" + +namespace ui_scripting +{ + enum menu_type + { + normal, + overlay + }; + + class menu final + { + public: + menu(); + + bool visible = false; + bool cursor = false; + + void open(); + void close(); + + void add_child(element* el); + void render() const; + + menu_type type = normal; + + std::string overlay_menu; + std::vector children{}; + }; +} diff --git a/src/client/resource.hpp b/src/client/resource.hpp index c2ce9b6a..50ce9bb4 100644 --- a/src/client/resource.hpp +++ b/src/client/resource.hpp @@ -20,3 +20,5 @@ #define RUNNER 312 #define ICON_IMAGE 313 + +#define LUA_ANIMATION_SCRIPT 314 diff --git a/src/client/resource.rc b/src/client/resource.rc index 1b461518..ce4e7c05 100644 --- a/src/client/resource.rc +++ b/src/client/resource.rc @@ -97,6 +97,8 @@ ID_ICON ICON "resources/icon.ico" MENU_MAIN RCDATA "resources/main.html" +LUA_ANIMATION_SCRIPT RCDATA "resources/animation.lua" + #ifdef _DEBUG TLS_DLL RCDATA "../../build/bin/x64/Debug/tlsdll.dll" #else diff --git a/src/client/resources/animation.lua b/src/client/resources/animation.lua new file mode 100644 index 00000000..0294c264 --- /dev/null +++ b/src/client/resources/animation.lua @@ -0,0 +1,92 @@ +function element:animate(name, state, animationtime) + local start = { + x = self.x, + y = self.y, + w = self.w, + h = self.h, + color = self.color, + backcolor = self.backcolor, + bordercolor = self.bordercolor, + borderwidth = self.borderwidth, + fontsize = self.fontsize + } + + local _end = {} + for k, v in pairs(start) do + _end[k] = state[k] or v + end + + local diffs = {} + for k, v in pairs(_end) do + if (type(v) == "table") then + local value = {} + local different = false + + for _k, _v in pairs(v) do + value[_k] = _v - start[k][_k] + if (value[_k] ~= 0) then + different = true + end + end + + if (different) then + diffs[k] = value + end + else + local value = v - start[k] + if (value ~= 0) then + diffs[k] = v - start[k] + end + end + end + + local timeout = nil + local interval = nil + local starttime = game:time() + + interval = game:onframe(function() + local time = game:time() + local percentage = (time - starttime) / animationtime + + if (percentage >= 1) then + for k, v in pairs(diffs) do + self[k] = _end[k] + end + else + for k, v in pairs(diffs) do + if (type(v) == "table") then + local value = {} + + for _k, _v in pairs(v) do + value[_k] = start[k][_k] + _v * percentage + end + + self[k] = value + else + self[k] = start[k] + v * percentage + end + end + end + end) + + timeout = game:ontimeout(function() + interval:clear() + for k, v in pairs(diffs) do + self[k] = _end[k] + end + end, animationtime) + + self:onnotifyonce("cancel_animation", function(_name) + if (name == _name) then + timeout:clear() + interval:clear() + end + end) +end + +function element:cancelanimations(name, callback) + self:notify("cancel_animation", name) + if (type(callback) == "function") then + game:ontimeout(callback, 0) + end +end \ No newline at end of file